From dfbc3284b4a523ee3eed69c26bdb33104da9e97a Mon Sep 17 00:00:00 2001 From: thinkgem Date: Sun, 21 Jul 2024 20:27:22 +0800 Subject: [PATCH] webuploader move to static --- .../static/webuploader/0.1/Uploader.swf | Bin 0 -> 143099 bytes .../static/webuploader/0.1/expressInstall.swf | Bin 0 -> 756 bytes .../static/webuploader/0.1/i18n/en.js | 0 .../static/webuploader/0.1/i18n/ja_JP.js | 0 .../static/webuploader/0.1/i18n/zh_CN.js | 0 .../static/webuploader/0.1/images/bg.png | Bin 0 -> 2810 bytes .../static/webuploader/0.1/images/error.png | Bin 0 -> 508 bytes .../static/webuploader/0.1/images/icons.gif | Bin 0 -> 453 bytes .../static/webuploader/0.1/images/icons.png | Bin 0 -> 2678 bytes .../static/webuploader/0.1/images/image.png | Bin 0 -> 1145 bytes .../static/webuploader/0.1/images/image2.png | Bin 0 -> 1672 bytes .../static/webuploader/0.1/images/loading.gif | Bin 0 -> 2241 bytes .../webuploader/0.1/images/progress.png | Bin 0 -> 1269 bytes .../static/webuploader/0.1/images/success.gif | Bin 0 -> 445 bytes .../static/webuploader/0.1/images/success.png | Bin 0 -> 1621 bytes .../webuploader/0.1/images/title-bg.png | Bin 0 -> 938 bytes .../0.1/images/uploadify-cancel.png | Bin 0 -> 2960 bytes .../static/webuploader/0.1/webuploader.css | 5 + .../webuploader/0.1/webuploader.extend.css | 110 + .../static/webuploader/0.1/webuploader.js | 8122 +++++++++++++++++ .../webuploader/0.1/webuploader.mobile.js | 7477 +++++++++++++++ 21 files changed, 15714 insertions(+) create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/Uploader.swf create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/expressInstall.swf rename modules/{core => static}/src/main/resources/static/webuploader/0.1/i18n/en.js (100%) rename modules/{core => static}/src/main/resources/static/webuploader/0.1/i18n/ja_JP.js (100%) rename modules/{core => static}/src/main/resources/static/webuploader/0.1/i18n/zh_CN.js (100%) create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/bg.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/error.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/icons.gif create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/icons.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/image.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/image2.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/loading.gif create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/progress.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/success.gif create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/success.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/title-bg.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/images/uploadify-cancel.png create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/webuploader.css create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/webuploader.extend.css create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/webuploader.js create mode 100644 modules/static/src/main/resources/static/webuploader/0.1/webuploader.mobile.js diff --git a/modules/static/src/main/resources/static/webuploader/0.1/Uploader.swf b/modules/static/src/main/resources/static/webuploader/0.1/Uploader.swf new file mode 100644 index 0000000000000000000000000000000000000000..bd75d6082fc0c3e977390a71dcca78932727147c GIT binary patch literal 143099 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,+E) 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 literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/expressInstall.swf b/modules/static/src/main/resources/static/webuploader/0.1/expressInstall.swf new file mode 100644 index 0000000000000000000000000000000000000000..24853aedd1a1a4b07025e482bd3f714a770f5e31 GIT binary patch literal 756 zcmV`#vmqief0w%1_*ai}AA)z$3pbtjL zusfv-vzN@y7W)hIMIJX%;{Wp(_=;y{3vDCDHkr=%&73*kcg|@DBPPJxO&H0*mUeS< zvyt7&0emfLs~}vDfWXuOHc_~E>-kV zCoU3%i+vw|7y}X11$X^3_#oOLo!^apTeyM$5d!9%M6L?l3cQd}LBYm|Q(|A#-|tpW ziooU}+}b2k+0a2{=S&SAL-#g#VzoH{PYd&ji{IAOF!*=wj&df7@PL{F3-VRi&g zfawLJsmSBdA((bMxKviw?u2w1;@gxl>U61H;!4~Iom<8uHV-^ZO4jVagL*p>)PsoH zF?U59P;_v)9eBj`!ERFf;y~8W!3*N9tfJ{gU~`IccDzuuRaMg}D~(tYXbKUoTh>1l zTVZ@^H5iGSsbwn-wNM79zObUphDBl#q^|iI;ti+}(F9I>2ZhtmA*WH~rmeee26Vf8 zJ>F*k;5Q@#pySs}G|)3&G4=j2-p|C3cH z8`VByq-&e+>;8%)fO{nWW-3v?Vq*&WiPmk%#C5aciqmmYHrDo(|6Y(D=LO_HjFlQkE literal 0 HcmV?d00001 diff --git a/modules/core/src/main/resources/static/webuploader/0.1/i18n/en.js b/modules/static/src/main/resources/static/webuploader/0.1/i18n/en.js similarity index 100% rename from modules/core/src/main/resources/static/webuploader/0.1/i18n/en.js rename to modules/static/src/main/resources/static/webuploader/0.1/i18n/en.js diff --git a/modules/core/src/main/resources/static/webuploader/0.1/i18n/ja_JP.js b/modules/static/src/main/resources/static/webuploader/0.1/i18n/ja_JP.js similarity index 100% rename from modules/core/src/main/resources/static/webuploader/0.1/i18n/ja_JP.js rename to modules/static/src/main/resources/static/webuploader/0.1/i18n/ja_JP.js diff --git a/modules/core/src/main/resources/static/webuploader/0.1/i18n/zh_CN.js b/modules/static/src/main/resources/static/webuploader/0.1/i18n/zh_CN.js similarity index 100% rename from modules/core/src/main/resources/static/webuploader/0.1/i18n/zh_CN.js rename to modules/static/src/main/resources/static/webuploader/0.1/i18n/zh_CN.js diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/bg.png b/modules/static/src/main/resources/static/webuploader/0.1/images/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..580be0a01dff4c70c72f78a3f40186660ee8eee0 GIT binary patch literal 2810 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 literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/icons.png b/modules/static/src/main/resources/static/webuploader/0.1/images/icons.png new file mode 100644 index 0000000000000000000000000000000000000000..12e4700163ac87fa38ae3d92a2c39d0fb4690fed GIT binary patch literal 2678 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 hT|D{o%M0tDT1##DCDd_dsPirz4yL6Rn)4v zv)yNtNTj)n2&tMFwZy-gMkf07%J%iduo#zV@n|#ww?G(766w)+7*H6YEwCDf^l94~ z;7}6D-rJzj;#%bzP=^|skPXAM8chV7L<(JLH9@*07zg6vEe0fv@uKDu12E{r7#sLX zw$dbl6AcmDF*tgAlt#BbNf)ALtPBT2tso&_gmDP48j}$-XboeG>w?7Gw#{My;}AS4 zj4@$Kt5g9J6oUailNX?4b2xxNz~lt-1p#5{RT#D%e}kcq`%vTaA20xFgN4>cO6(Pmr?|Lb=~ zVY4R91hdqz8BM`-#KR?c*-Du}2?j$rifK?ZdFmEbi71Ym6HyZ&i537pN=RowY#YAg z45boOAZ8pwbg)7i#vnA927?~tOJt%Tb|9C-W%D^4880|QAQ8)WQofuMEEaJjQ(P&k zOEJO-KE>7l%jJEPYYT(XL{yf-m?0I`%Q4glj2{{_%;1I13uFtpaz3HQo7u|*S3iT7 zNv`0dToz%5WqaEHJoVHTkssT1I=IAQI{Gj|s zLrABQMF5Sm8=zAjTlVMGdII}WHeGsvSex8>aPW*~dEN^00n4|U<+`F)0+VSYbQ~WV z?F%~8cr8%bc)qMXNq@X|msi$rCx(p8ocoxvx95hP+ww7tVLzn=Dw}7u}%eHk8iyK22DPh{^tP z;8yePo+0GIiepheWjSKe7i~Cr%%6U%^FRaT+&;zKWI;yr)=Htb`;H~Dtv3>QdomZO zx?7%U#21`hX#U}z&+kj(Dq>0t3fzD1{LAk;=X>(P~1p?y-g&qe1}@0io|o5x=H z{k~yG;hpr+z9nb%C)FKYW7g36$AgPzSF1B*L3P7~x01H!jfYh9zSJYpH-vWSwGS*0 z9k5wfq^Y87ZYAwKPqD=-7B9#ulJ4(Z?b}N2rRFd6tR&}rUQ;I@OUoYF_>%W{SBSG; zOJyu`iA??nIWcheW%Lt#9#xJy!m>d^1>^7*Xzr3{YHG7TH`UuA+7(^ zqh7~c*~n7gqt4%h2dR`(bxr1l4`SNv%@iD(L0l(bA7m-!EPjeb=C literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/loading.gif b/modules/static/src/main/resources/static/webuploader/0.1/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..ce25214921e2ef23d1c02a511034af19c2614a98 GIT binary patch literal 2241 zcmbW1d0Z2B7Qkn6Cxnnd0trVPh9E*oG6?~aXeuNDL5*@Lh{z@+Q;CqnWRQs3<y_c>{?>O_u2VE(LFWh_bbY0)T zPftgxy8E6zdp0&U7CGiOCoL~mmztF(mmowC008``z>o#$O_&M)Pn+&>n#^S=29=G%!y3JOrE5V0dZR?v{)PBr!W| zQjf_gl}Qk(&E|`1T-uH0p#Q~qt+YFDxf7M7HbrOC9~1i3C*u1`x+snQUIYC<>D?#4`ZGdkhdiu-+`EB{4Zwa$f_ zFo!G8;V7S|fUHsn=5UufoRBUz5i*#}R=aQSd!Faf>QR?<8EQ#)Ic(5)e%033_*Wu2 zM5RU2B0SbcYpl*-|qZ%`1Y-#zuder_`{#C4}9PMr|+(P`^_J}zWUXbzRO=; z`a|!F>^ba{A*_Cr=#j`smovBZoiy?V*Fe>FhYrzQ1kX-aWfp zKiIW%$M%+OTeoc9^!~>8yc^axzuUB~ac#ppYgX6S)z(!1`t4OKS5z(cR4%K)+^(fg zhuv0Qw!~Vx_$}08E-@K@RlKNZVc~*;{Q2|pa&xk?=FTz9o;5QwV@7(KUZ+h>nf|6` z+ElelnVgiUNRZ=gDG|rN5f?ipW^%MB3XX&(2_wS8LIwPgU>-Lpki!mO`7`|(bQ+aH zCJ_nvfQcL>k*UBWAReDJfN#tJ z0Pz5g<7rUjr`&@3I;XXqYa=MLx)N!--33Kc`T`@pnT;&IAuE6*5RgRxrLZ(5J&I71 z#XvQZ2!64IO5qg-@+~YD!H*YFTNnZG4+6pQO>j6Xz`sNMKX@RH$HDP7<9Jb(P*6v6 z?QQ)6b~K}_k<@1m0yVXIQg?PN0-S9-q9!GE63_2ZgwL-XjB7U{`Rh}bD$Zr-k8g@P z0!1Lr{SDgc=5T)fN>c2Gi{cf+RaRj*5bTN-(%B*|k#CL*3-AMLq8t%baRh-Z%s*5V zsNN}}w1Nks0*F9hU~xcrs32fPc!YQ6j#?3ovDEbnDhPo1L;FxSw8pq7#=>2D*OvFP z+Ve^MgGb-T76F|p-m^Dvd5!U*ge4!*6-5kY9a@&SXiab9aH@U{Of_EQrPAWTHMzlb z_#kz^I7dIFqN(!J#O^nvqWWygM~x$LOQDgn0Z8iDYqJ{ zn=im8hl}>HdSLB*AoQ{cE9jJDYfX>x3`#gP{fBMF;A7sXqYsqG87wk&`P{4UA#Qa~ zF3m>ckvuSm4M*@8a8oP;%oh3sK|;EJ9JtMpO&6>}H#Edj1OhgR=||q4uwOuTb2!a$ zED&r8H?F4916Kbmr^VVFe#3CP0pnAO>Y&0lC%73}K%7mj$zDs^TC?}jfjl6PLok-o zf$EwJ0@9)`IMiM{H8y^hm)KhRF4yleXmsb&z*F@@Pq=|ASpn>o(OgcL(2ouZmd3Ea zStNmg9v&0Qp~H+#9Ol+&I;R*6BY+$>i%MW}NL*2ykQT<--f9&U=d3y#9SKbR4}@{% AyZ`_I literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/progress.png b/modules/static/src/main/resources/static/webuploader/0.1/images/progress.png new file mode 100644 index 0000000000000000000000000000000000000000..717c4865c90a959c6a0e9ad1af9c777d900a2e9c GIT binary patch literal 1269 zcmeAS@N?(olHy`uVBq!ia0vp^f8U}fi7AzZCsS=07??9MLn2Bde0{8v^Kf6`()~Xj@TAnpKdC8`Lf!&sHg;q@=(~U%$M( zT(8_%FTW^V-_X+15@d#vkuFe$ZgFK^Nn(X=Ua>OF1ees}t-3#_`hBq$Z(46Le)Ln;eW^@CE2 z^Gl18f$@>14ATq@JNy=b6armi>cVAJd5X6R;MWawh(V&G(G=xXffXz1o@ zX=ZL{;B08&Z0-!x>zP+vl9-pA3bQv8XfIT+GhV$`&PAz-CHX}m`T04p6cCV+Uy@&( zkzb(T9Bihb;hUJ8nFkWk1Vs?Uzb>gonPsUdZbkXI3g8g7%EaOV0~10%hv-cqC)D(T zj?o7t52WM*69T3|5EGvGfgE`DNzDW1nId53*cQ_-&cMK^?CIhdQgN$ga=rIq1A(^5 zYKwg&`wo`WvF`u>{iFU|`!+wz>b#B?t8F4hxRm$~lz-tTH#6E8xZnKlv%`YRwvA{oKseqmK8(Gfx|> z#)dT+Zy!CGH{+89Q&m{rC!PyyIjq?Y9m+ziHPqr6qxfF`+2Qt=-KQ=fE8_j%1#Y2} z>NffN)P;AQIhrE)QQzeqbFS^A8(M1XGuQqTO<=fcH+M~2lzlL$Ao6teb6Mw<&;$U? C61Fb@ literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/success.gif b/modules/static/src/main/resources/static/webuploader/0.1/images/success.gif new file mode 100644 index 0000000000000000000000000000000000000000..8d4f3112b9d1df2147ed3b67d9736163dedd11e1 GIT binary patch literal 445 zcmZ?wbhEHb)L_tHxXJ(m|NsC0{`>#WKmY&!`~ULm|4%>v3rzVXIOUh{)L$ageu+>2 zB{Acd%*pZwE#>TlQSztb-KopJH+%!_|l-~7Ag*59?a z{_c7B@6_{ur(gU#_wwKE_y6vG`1j=VzZYNsfBgCX$DjXizW@L859r4KV8@~pia%Kx z85k@WbU+dyKQXY~a+p)#p(E9QVoA}-BeJtDJbfs+R&065YSGzOop#2q7tzUUTK@E- z1Qpqe*fm*b*T-Fam_8!ZPiJg?eRTb9ku+5;xVnw>Wr$>Ce}(b zvq{bgY~#~oR8Cparp&0%zbdUw!o*Z6ds70hP&$teqekA&7*16-748@%Mnl0vSqU6+ zOpICr!NSHY@7SLk+rBLP3&#jWP!Xxbb jiN%_{Rv%uKKTTNF#>acL^G5)OtDBg<-IWs>91PX~W`xy? literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/success.png b/modules/static/src/main/resources/static/webuploader/0.1/images/success.png new file mode 100644 index 0000000000000000000000000000000000000000..94f968dc8fd3c7ca8f6cb599d006ef3f23b62c7d GIT binary patch literal 1621 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m{l@EB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvO-#?>2=9ZF3nBND}m`vLFhHXsTY(OatnYqyQCInmZhe+73JqDfIV%MiQ6rPIL(9V zO~LIJBb<8mfsWA!MJ-ZP!-Rn82gHOYTp$OY^i%VI>AeV;u(vXbonl~MI_~M>7*cWT z%njcRVMh`65C6~Q2yI{K`NBQQh9heAinXE1(JfB=Ul%M;Ke#x6g%L8^f5l4Te zj&BcK1znDIyZS2~eC7F3K+}tVTa$=u*HTU0W4>0#`+Qf%|GkyBERBs#$Y}P@k7@U2 zd#*XsYr~WGeCA1uSK>2HTD)g9mP}Xrpg$+`K&8dx2JS~bjCx(hj5}1;-8qn2_>|?> zp{d+2q{I0hbQS!U-jOut`kSEi+IhQJGmBl4O%se*W?OGKZ0Mp?XmGnRgezL^8vCJ4 z#-ek9(R&%4!h+;oXA~Mtbyie=u#0PDiI#!iX`%2?^@kRL8ma*r6?+*}W4e|_8~J&& z+-+*Ub3{dDcAIF|!MTDz0#78qKYeaeVU$wDOOs3cR=e5FZ*O2e-6xc{Uu=$;!s>*M z>nZ|V>zUp8-oE6^wXjV+_UOvuE&SORr`;3hnBKu~e3rzlY@NRjD|5Ui2t>^@Ve7jy z)2vT>u1vesjJgG`f;l_n`FmbR~b^~{VzXB}7CdU_pt>lO9l=E;X#4<2bZu32r~J^e^%TSISQ_Qm$= z7d{rOo2%^RUGseJkpn9(MWszUQdl(i&RN5PBgZ0d#S7ZipShU3W`>Nsy6C#pCD*KW zyxHW%b?m|pr;dF;L$saa#oaeO`u=>jkXTjl9*cLEvOD!Jdr4ooKJW9gY7?D{e?uM^ z`_}E4`0yR0boA=9>3>WXzUH{&pDfpZ;_2xb=j*pKsk#WyD%J#5t{1#azeB z4SJq8H2*dJVX$xugZHVh{7U`u4@oM|quz5j-hRJi{qwh1k5uNqu`W>3HJG&Y^R}6R z$Gq-HPue$sd${oT*XKO8zN>Oe_%K^bZNm3^-vf7@5X#2HZ7qm!>fg-@N3u_S6BtfcFRaLzUWGF!q z%AzEfB~`D;I)q^T@hn>GjrE?{UdLiPgP&3w>7tmXsgPC#5>G@))3jVemdgxLP8K0W zSvgGh3k4G=ZtO?YCn3lc(TL2c!850)DFo3*HcZxKVg(a36p4}m^OOpp{&p|bt$J&&x7~}fl`AqcVt%cEU2}27dM96QnwD)g zZI46N>oIHU?{_Z!WDf$^W^<7J82vsuXemZ#a;t%OX9%BFi literal 0 HcmV?d00001 diff --git a/modules/static/src/main/resources/static/webuploader/0.1/images/uploadify-cancel.png b/modules/static/src/main/resources/static/webuploader/0.1/images/uploadify-cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..35682b010b0cd8758b2cedfd230aba50c4392996 GIT binary patch literal 2960 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 {position:relative;padding:15px 0;background-color:#fdfdfd;box-shadow:inset 0 3px 30px rgba(0,0,0,.02);border:1px solid #dde1e8;border-radius:2px;} +.wup_container .area>div:after {position:absolute;top:15px;left:15px;font-size:14px;font-weight:bold;color:#bbb;text-transform:uppercase;letter-spacing:1px;} +.wup_container .wup_input {position:absolute;z-index:-1000;top:70px;left:2%;border:0;color:#fff;background:transparent;width:50px;} +.wup_container .wup_input.image {top:160px;left:47%;} +.wup_container .btns .webuploader-container {display:inline-block;float:left} + +.wup_container .placeholder .webuploader-pick {font-size:14px;background:#00b7ee;border-radius:3px;line-height:29px;padding:0 10px;color:#fff;display:inline-block;margin:0px auto 7px auto;cursor:pointer;box-shadow:0 1px 1px rgba(0, 0, 0, 0.1);} +.wup_container .placeholder .webuploader-pick-hover {background:#00a2d4;} +.wup_container .placeholder .flashTip {color:#666666;font-size:14px;position:absolute;width:100%;text-align:center;bottom:20px;} +.wup_container .placeholder .flashTip a {color:#0785d1;text-decoration:none;} +.wup_container .placeholder .flashTip a:hover {text-decoration:underline;} + +.wup_container .wup_file .queueList {margin:3px 18px;border:2px dashed transparent;} +.wup_container .wup_file .queueList.webuploader-dnd-over {border:2px dashed #999999;} +.wup_container .wup_file .placeholder {color:#cccccc;font-size:14px;position:relative;} + +.wup_container .wup_file .filetable .template-upload .name {width:300px!important;max-width:400px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} +.wup_container .wup_file .filetable .template-upload .name i {color:#aaa;cursor:move;padding-right:5px;font-size:14px;} +.wup_container .wup_file .filetable .template-upload .name a {color:#333;} +.wup_container .wup_file .filetable .template-upload .size {width:100px!important;white-space:nowrap;overflow:hidden;text-align:center;} +.wup_container .wup_file .filetable .template-upload .prog_bar {text-align:center;} +.wup_container .wup_file .filetable .template-upload .prog_bar .progress {width:100px!important;margin:0px;position:relative;border-radius:3px;display:inline-block;vertical-align:middle;} +.wup_container .wup_file .filetable .template-upload .prog_bar .progress-bar {min-width:2em;} +.wup_container .wup_file .filetable .template-upload .msg {text-align:center;} +.wup_container .wup_file .filetable .template-upload .msg p {margin-bottom:0;} +.wup_container .wup_file .filetable .template-upload .msg .label {font-size:12px;padding:1px 5px;line-height:1.5;overflow:hidden;display:inline-block;text-overflow:ellipsis;width:120px;margin-bottom:-4px;} +.wup_container .wup_file .filetable .template-upload .btncancel {width:80px!important;cursor:pointer;} +.wup_container .wup_file .filetable .template-download .size {width:200px!important;white-space:nowrap;overflow:hidden;} +.wup_container .wup_file .filetable .template-download .name {width:228px!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} +.wup_container .wup_file .filetable .hide-button {padding:0 !important;height:1px !important;} +.wup_container .wup_file .filetable td {border:0;border-top:1px solid #ddd;border-bottom:1px solid #ddd;padding:6px;vertical-align:middle;} + +.wup_container .wup_img .queueList {margin:3px 18px;border:2px dashed #e6e6e6;} +.wup_container .wup_img .queueList.webuploader-dnd-over {border:2px dashed #999999;} +.wup_container .wup_img .placeholder {min-height:180px;padding-top:100px;text-align:center;background:url(images/image.png) center 20px no-repeat;color:#cccccc;font-size:14px;position:relative} +.wup_container .wup_img .filelist {list-style:none;margin:0;padding:0} +.wup_container .wup_img .filelist:after {content:'';display:block;width:0;height:0;overflow:hidden;clear:both} +.wup_container .wup_img .filelist li {width:110px;height:110px;background:url(images/bg.png) no-repeat;text-align:center;margin:10px;position:relative;display:inline;float:left;overflow:hidden;font-size:14px;border-radius:3px;} +.wup_container .wup_img .filelist li p.log {position:relative;top:-45px} +.wup_container .wup_img .filelist li p.title {color:#fff;position:absolute;bottom:0px;left:0;width:100%;z-index:1000;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-indent:5px;text-align:left;filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#80000000',endColorstr='#80000000')\0;background:rgba( 0,0,0,0.5 );} +.wup_container .wup_img .filelist li p.title {margin:0;} +.wup_container .wup_img .filelist li p.title a {color:#fff;} +.wup_container .wup_img .filelist li p.progress {position:absolute;width:100%;bottom:0;left:0;height:8px;overflow:hidden;z-index:50;margin:0;border-radius:0;background:none;-webkit-box-shadow:0 0 0} +.wup_container .wup_img .filelist li p.progress span {display:none;overflow:hidden;width:0;height:100%;background:#1483d8 url(images/progress.png) repeat-x;-webit-transition:width 200ms linear;-moz-transition:width 200ms linear;-o-transition:width 200ms linear;-ms-transition:width 200ms linear;transition:width 200ms linear;-webkit-animation:progressmove 2s linear infinite;-moz-animation:progressmove 2s linear infinite;-o-animation:progressmove 2s linear infinite;-ms-animation:progressmove 2s linear infinite;animation:progressmove 2s linear infinite;-webkit-transform:translateZ(0)} +.wup_container .wup_img .filelist li p.imgWrap {position:relative;z-index:2;line-height:110px;vertical-align:middle;overflow:hidden;width:110px;height:110px;-webkit-transform-origin:50% 50%;-moz-transform-origin:50% 50%;-o-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%;-webit-transition:200ms ease-out;-moz-transition:200ms ease-out;-o-transition:200ms ease-out;-ms-transition:200ms ease-out;transition:200ms ease-out} +.wup_container .wup_img .filelist li img {width:100%} +.wup_container .wup_img .filelist li p.error {background:#f43838;color:#fff;position:absolute;bottom:0;left:0;height:20px;line-height:20px;width:100%;z-index:1000;margin-bottom:0;padding-left:1px;} +.wup_container .wup_img .filelist li .success {display:block;position:absolute;left:0;bottom:0;height:40px;width:100%;z-index:2000;background:url(images/success.png) no-repeat right bottom} +.wup_container .wup_img .filelist div.file-panel {position:absolute;height:0;filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#80000000',endColorstr='#80000000')\0;background:rgba( 0,0,0,0.5 );width:100%;top:0;left:0;overflow:hidden;z-index:300} +.wup_container .wup_img .filelist div.file-panel span {width:24px;height:24px;display:inline;float:right;text-indent:-9999px;overflow:hidden;background:url(images/icons.png) no-repeat;margin:5px 1px 1px;cursor:pointer} +.wup_container .wup_img .filelist div.file-panel span.rotateLeft {background-position:0 -24px} +.wup_container .wup_img .filelist div.file-panel span.rotateLeft:hover {background-position:0 0} +.wup_container .wup_img .filelist div.file-panel span.rotateRight {background-position:-24px -24px} +.wup_container .wup_img .filelist div.file-panel span.rotateRight:hover {background-position:-24px 0} +.wup_container .wup_img .filelist div.file-panel span.cancel {background-position:-48px -24px} +.wup_container .wup_img .filelist div.file-panel span.cancel:hover {background-position:-48px 0} + +.wup_container .statusBar {padding:0 20px;line-height:45px;vertical-align:middle;position:relative;} +.wup_container .statusBar .progress {border:1px solid #1483d8;width:198px;background:#fff;/* height:18px;*/ position:relative;display:inline-block;text-align:center;line-height:20px;color:#CAE8FF;position:relative;margin:0 10px -5px 0;} +.wup_container .statusBar .progress span.percentage {width:0;height:100%;left:0;top:0;background:#1483d8;position:absolute;} +.wup_container .statusBar .progress span.text {position:relative;z-index:10;} +.wup_container .statusBar .info {display:inline-block;font-size:14px;color:#666666;} +.wup_container .statusBar .btns {position:absolute;top:6px;right:20px;line-height:29px;} +.wup_container .statusBar .btns .webuploader-pick, +.wup_container .statusBar .btns .uploadBtn, +.wup_container .statusBar .btns .uploadBtn.state-uploading, +.wup_container .statusBar .btns .uploadBtn.state-paused {background:#ffffff;border:1px solid #cfcfcf;color:#565656;padding:0 10px;display:inline-block;border-radius:3px;margin-left:10px;cursor:pointer;font-size:14px;float:left;} +.wup_container .statusBar .btns .webuploader-pick-hover, +.wup_container .statusBar .btns .uploadBtn:hover, +.wup_container .statusBar .btns .uploadBtn.state-uploading:hover, +.wup_container .statusBar .btns .uploadBtn.state-paused:hover {background:#f0f0f0;} +.wup_container .statusBar .btns .uploadBtn {background:#00b7ee;color:#fff;border-color:transparent;} +.wup_container .statusBar .btns .uploadBtn:hover {background:#00a2d4;} +.wup_container .statusBar .btns .uploadBtn.disabled {pointer-events:none;opacity:0.6;} + +.wup_container.mini .area>div {padding:0;margin:0;border:0;box-shadow:none;background:none;} +.wup_container.mini .wup_input {top:50px;left:2%;} +.wup_container.mini .wup_input.image {top:23px;left:45%;} +.wup_container.mini .statusBar {padding:0;line-height:35px;} +.wup_container.mini .statusBar .btns {top:2px;right:10px;line-height:26px;} +.wup_container.mini .queueList {margin:0;border:0;} +.wup_container.mini .wup_file .filetable td {padding:4px 6px;} +.wup_container.mini .wup_img .placeholder .webuploader-pick {padding:0 10px;margin:5px 0;} +.wup_container.mini .wup_img .placeholder {min-height:initial;padding:3px;background:none;} +.wup_container.mini .wup_img .filelist li {margin:5px;} + +.wup_img_outerdiv {position:fixed;top:0;left:0;background:rgba(0,0,0,0.7);z-index:99999;width:100%;height:100%;display:none;} +.wup_img_outerdiv .innerdiv {position:fixed;} .wup_img_outerdiv .image {border:2px solid #fff;} +.wup_img_outerdiv .tools {font-size:14px;padding: 10px 0;text-align:center;margin-top:5px;} +.wup_img_outerdiv .tools a {color:#bbc75f;padding:8px 10px;margin:5px;border-radius:5px;background:#393939;} +.wup_img_outerdiv .tools a:hover {background:#2b2b2b;} + +.skin-dark .wup_container .area>div {background-color:#2e2e2e;border-color:#383838;} +.skin-dark .wup_container .statusBar .info {color:#c9c9c9;} +.skin-dark .wup_container .wup_img .queueList {border-color:#545454;} +.skin-dark .wup_container .wup_img .filelist li {background:#3e3e3e;} +.skin-dark .wup_container .wup_file .filetable td {border-color:#464646;} +.skin-dark .wup_container .wup_file .filetable .template-upload .name a {color:#c9c9c9;} +.skin-dark .wup_container .statusBar .btns .webuploader-pick, +.skin-dark .wup_container .statusBar .btns .uploadBtn, +.skin-dark .wup_container .statusBar .btns .uploadBtn.state-uploading, +.skin-dark .wup_container .statusBar .btns .uploadBtn.state-paused {background-color:#1a1a1a;color:#b5b5b5;border-color:#6c6c6c;} diff --git a/modules/static/src/main/resources/static/webuploader/0.1/webuploader.js b/modules/static/src/main/resources/static/webuploader/0.1/webuploader.js new file mode 100644 index 00000000..25900591 --- /dev/null +++ b/modules/static/src/main/resources/static/webuploader/0.1/webuploader.js @@ -0,0 +1,8122 @@ +/*! WebUploader 0.1.5 */ +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + */ + define('dollar-third',[],function() { + var $ = window.__dollar || window.jQuery || window.Zepto; + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.5', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + //parent = $( opts.container || document.body ); + // ThinkGem 修正多个上传控件时,获取的容器不正确导致,显示或隐藏控件时误隐藏了显示的选择文件按钮 + parent = $( /*opts.container || */document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + zIndex: 29891014, + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClent, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClent.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file' + }; + + Base.inherits( RuntimeClent, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button; + + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + // 只处理可见的按钮。ThinkGem + if (!button.is(':visible')){ + width = 1; height = 1; + pos = {top: -1000, left: -1000}; + } + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addButton + * @for Uploader + * @grammar addButton( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addButton({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 空文件不能上传 ThinkGem + if ( !file || !file.size ) { + me.owner.trigger( 'error', 'Q_EMPTY_FILE', file ); + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: true, // ThinkGem 改为true支持跨域 + fileVal: 'file', + timeout: 0, //2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [method='POST'] + * @namespace options + * @for Uploader + * @description 文件上传方式,`POST`或者`GET`。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + }); + + file.setStatus( Status.QUEUED ); + } else if (file.getStatus() === Status.PROGRESS) { + return; + } else { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return; + } + + me.runing = true; + + var files = []; + + // 如果有暂停的,则续传 + $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + files.push(file); + me._trigged = false; + v.transport && v.transport.send(); + } + }); + + var file; + while ( (file = files.shift()) ) { + file.setStatus( Status.PROGRESS ); + } + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.abort(); + me._putback(v); + me._popBlock(v); + }); + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { +// deferred.reject( reason ); + deferred.resolve( new Date().getTime() ); + if (typeof(console) !== 'undefined') { + console.log('md5File error.', reason); + } + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function() { + input.trigger('click'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + zIndex: 29891014, + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = window.JSON && window.JSON.parse || function( s ) { + try { + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( blob.uid, blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Uploader上传类 + */ + define('webuploader',[ + 'preset/all', + 'widgets/log' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/modules/static/src/main/resources/static/webuploader/0.1/webuploader.mobile.js b/modules/static/src/main/resources/static/webuploader/0.1/webuploader.mobile.js new file mode 100644 index 00000000..63b7e2ac --- /dev/null +++ b/modules/static/src/main/resources/static/webuploader/0.1/webuploader.mobile.js @@ -0,0 +1,7477 @@ +/*! WebUploader 0.1.5 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + */ + define('dollar-third',[],function() { + var $ = window.__dollar || window.jQuery || window.Zepto; + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * 直接来源于jquery的代码。 + * @fileOverview Promise/A+ + * @beta + */ + define('promise-builtin',[ + 'dollar' + ], function( $ ) { + + var api; + + // 简单版Callbacks, 默认memory,可选once. + function Callbacks( once ) { + var list = [], + stack = !once && [], + fire = function( data ) { + memory = data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ); + } + firing = false; + + if ( list ) { + if ( stack ) { + stack.length && fire( stack.shift() ); + } else { + list = []; + } + } + }, + self = { + add: function() { + if ( list ) { + var start = list.length; + (function add ( args ) { + $.each( args, function( _, arg ) { + var type = $.type( arg ); + if ( type === 'function' ) { + list.push( arg ); + } else if ( arg && arg.length && + type !== 'string' ) { + + add( arg ); + } + }); + })( arguments ); + + if ( firing ) { + firingLength = list.length; + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + + disable: function() { + list = stack = memory = undefined; + return this; + }, + + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + + fireWith: function( context, args ) { + if ( list && (!fired || stack) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + } + }, + + fired, firing, firingStart, firingLength, firingIndex, memory; + + return self; + } + + function Deferred( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ 'resolve', 'done', Callbacks( true ), 'resolved' ], + [ 'reject', 'fail', Callbacks( true ), 'rejected' ], + [ 'notify', 'progress', Callbacks() ] + ], + state = 'pending', + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return Deferred(function( newDefer ) { + $.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = $.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for + // forwarding actions to newDefer + deferred[ tuple[ 1 ] ](function() { + var returned; + + returned = fn && fn.apply( this, arguments ); + + if ( returned && + $.isFunction( returned.promise ) ) { + + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + 'With' ]( + this === promise ? + newDefer.promise() : + this, + fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + + return obj != null ? $.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + $.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + 'With' ]( this === deferred ? promise : + this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + 'With' ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + } + + api = { + /** + * 创建一个[Deferred](http://api.jquery.com/category/deferred-object/)对象。 + * 详细的Deferred用法说明,请参照jQuery的API文档。 + * + * Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。 + * + * @for Base + * @method Deferred + * @grammar Base.Deferred() => Deferred + * @example + * // 在文件开始发送前做些异步操作。 + * // WebUploader会等待此异步操作完成后,开始发送文件。 + * Uploader.register({ + * 'before-send-file': 'doSomthingAsync' + * }, { + * + * doSomthingAsync: function() { + * var deferred = Base.Deferred(); + * + * // 模拟一次异步操作。 + * setTimeout(deferred.resolve, 2000); + * + * return deferred.promise(); + * } + * }); + */ + Deferred: Deferred, + + /** + * 判断传入的参数是否为一个promise对象。 + * @method isPromise + * @grammar Base.isPromise( anything ) => Boolean + * @param {*} anything 检测对象。 + * @return {Boolean} + * @for Base + * @example + * console.log( Base.isPromise() ); // => false + * console.log( Base.isPromise({ key: '123' }) ); // => false + * console.log( Base.isPromise( Base.Deferred().promise() ) ); // => true + * + * // Deferred也是一个Promise + * console.log( Base.isPromise( Base.Deferred() ) ); // => true + */ + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + }, + + /** + * 返回一个promise,此promise在所有传入的promise都完成了后完成。 + * 详细请查看[这里](http://api.jquery.com/jQuery.when/)。 + * + * @method when + * @for Base + * @grammar Base.when( promise1[, promise2[, promise3...]] ) => Promise + */ + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + slice = [].slice, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || (subordinate && + $.isFunction( subordinate.promise )) ? length : 0, + + // the master Deferred. If resolveValues consist of + // only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? + slice.call( arguments ) : value; + + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && + $.isFunction( resolveValues[ i ].promise ) ) { + + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, + resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, + progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } + }; + + return api; + }); + define('promise',[ + 'promise-builtin' + ], function( $ ) { + return $; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.5', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClent, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClent.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file' + }; + + Base.inherits( RuntimeClent, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button; + + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addButton + * @for Uploader + * @grammar addButton( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addButton({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [method='POST'] + * @namespace options + * @for Uploader + * @description 文件上传方式,`POST`或者`GET`。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + }); + + file.setStatus( Status.QUEUED ); + } else if (file.getStatus() === Status.PROGRESS) { + return; + } else { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return; + } + + me.runing = true; + + var files = []; + + // 如果有暂停的,则续传 + $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + files.push(file); + me._trigged = false; + v.transport && v.transport.send(); + } + }); + + var file; + while ( (file = files.shift()) ) { + file.setStatus( Status.PROGRESS ); + } + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.abort(); + me._putback(v); + me._popBlock(v); + }); + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function() { + input.trigger('click'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + define('webuploader',[ + 'base', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + 'widgets/log', + 'runtime/html5/blob', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/androidpatch', + 'runtime/html5/transport', + 'runtime/html5/md5', + ], function( Base ) { + return Base; + }); + return require('webuploader'); +});