fineuploader.js 118 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761
  1. /**
  2. * http://github.com/Valums-File-Uploader/file-uploader
  3. *
  4. * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
  5. *
  6. * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
  7. * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com )
  8. *
  9. * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
  10. */
  11. /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/
  12. var qq = function(element) {
  13. "use strict";
  14. return {
  15. hide: function() {
  16. element.style.display = 'none';
  17. return this;
  18. },
  19. /** Returns the function which detaches attached event */
  20. attach: function(type, fn) {
  21. if (element.addEventListener){
  22. element.addEventListener(type, fn, false);
  23. } else if (element.attachEvent){
  24. element.attachEvent('on' + type, fn);
  25. }
  26. return function() {
  27. qq(element).detach(type, fn);
  28. };
  29. },
  30. detach: function(type, fn) {
  31. if (element.removeEventListener){
  32. element.removeEventListener(type, fn, false);
  33. } else if (element.attachEvent){
  34. element.detachEvent('on' + type, fn);
  35. }
  36. return this;
  37. },
  38. contains: function(descendant) {
  39. // compareposition returns false in this case
  40. if (element === descendant) {
  41. return true;
  42. }
  43. if (element.contains){
  44. return element.contains(descendant);
  45. } else {
  46. /*jslint bitwise: true*/
  47. return !!(descendant.compareDocumentPosition(element) & 8);
  48. }
  49. },
  50. /**
  51. * Insert this element before elementB.
  52. */
  53. insertBefore: function(elementB) {
  54. elementB.parentNode.insertBefore(element, elementB);
  55. return this;
  56. },
  57. remove: function() {
  58. element.parentNode.removeChild(element);
  59. return this;
  60. },
  61. /**
  62. * Sets styles for an element.
  63. * Fixes opacity in IE6-8.
  64. */
  65. css: function(styles) {
  66. if (styles.opacity !== null){
  67. if (typeof element.style.opacity !== 'string' && typeof(element.filters) !== 'undefined'){
  68. styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
  69. }
  70. }
  71. qq.extend(element.style, styles);
  72. return this;
  73. },
  74. hasClass: function(name) {
  75. var re = new RegExp('(^| )' + name + '( |$)');
  76. return re.test(element.className);
  77. },
  78. addClass: function(name) {
  79. if (!qq(element).hasClass(name)){
  80. element.className += ' ' + name;
  81. }
  82. return this;
  83. },
  84. removeClass: function(name) {
  85. var re = new RegExp('(^| )' + name + '( |$)');
  86. element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
  87. return this;
  88. },
  89. getByClass: function(className) {
  90. var candidates,
  91. result = [];
  92. if (element.querySelectorAll){
  93. return element.querySelectorAll('.' + className);
  94. }
  95. candidates = element.getElementsByTagName("*");
  96. qq.each(candidates, function(idx, val) {
  97. if (qq(val).hasClass(className)){
  98. result.push(val);
  99. }
  100. });
  101. return result;
  102. },
  103. children: function() {
  104. var children = [],
  105. child = element.firstChild;
  106. while (child){
  107. if (child.nodeType === 1){
  108. children.push(child);
  109. }
  110. child = child.nextSibling;
  111. }
  112. return children;
  113. },
  114. setText: function(text) {
  115. element.innerText = text;
  116. element.textContent = text;
  117. return this;
  118. },
  119. clearText: function() {
  120. return qq(element).setText("");
  121. }
  122. };
  123. };
  124. qq.log = function(message, level) {
  125. "use strict";
  126. if (window.console) {
  127. if (!level || level === 'info') {
  128. window.console.log(message);
  129. }
  130. else
  131. {
  132. if (window.console[level]) {
  133. window.console[level](message);
  134. }
  135. else {
  136. window.console.log('<' + level + '> ' + message);
  137. }
  138. }
  139. }
  140. };
  141. qq.isObject = function(variable) {
  142. "use strict";
  143. return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
  144. };
  145. qq.isFunction = function(variable) {
  146. "use strict";
  147. return typeof(variable) === "function";
  148. };
  149. qq.trimStr = function(string) {
  150. if (String.prototype.trim) {
  151. return string.trim();
  152. }
  153. return string.replace(/^\s+|\s+$/g,'');
  154. };
  155. qq.isFileOrInput = function(maybeFileOrInput) {
  156. "use strict";
  157. if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) {
  158. return true;
  159. }
  160. else if (window.HTMLInputElement) {
  161. if (maybeFileOrInput instanceof HTMLInputElement) {
  162. if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
  163. return true;
  164. }
  165. }
  166. }
  167. else if (maybeFileOrInput.tagName) {
  168. if (maybeFileOrInput.tagName.toLowerCase() === 'input') {
  169. if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
  170. return true;
  171. }
  172. }
  173. }
  174. return false;
  175. };
  176. qq.isBlob = function(maybeBlob) {
  177. "use strict";
  178. return window.Blob && maybeBlob instanceof Blob;
  179. };
  180. qq.isXhrUploadSupported = function() {
  181. "use strict";
  182. var input = document.createElement('input');
  183. input.type = 'file';
  184. return (
  185. input.multiple !== undefined &&
  186. typeof File !== "undefined" &&
  187. typeof FormData !== "undefined" &&
  188. typeof (new XMLHttpRequest()).upload !== "undefined" );
  189. };
  190. qq.isFolderDropSupported = function(dataTransfer) {
  191. "use strict";
  192. return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
  193. };
  194. qq.isFileChunkingSupported = function() {
  195. "use strict";
  196. return !qq.android() && //android's impl of Blob.slice is broken
  197. qq.isXhrUploadSupported() &&
  198. (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice);
  199. };
  200. qq.extend = function (first, second, extendNested) {
  201. "use strict";
  202. qq.each(second, function(prop, val) {
  203. if (extendNested && qq.isObject(val)) {
  204. if (first[prop] === undefined) {
  205. first[prop] = {};
  206. }
  207. qq.extend(first[prop], val, true);
  208. }
  209. else {
  210. first[prop] = val;
  211. }
  212. });
  213. };
  214. /**
  215. * Searches for a given element in the array, returns -1 if it is not present.
  216. * @param {Number} [from] The index at which to begin the search
  217. */
  218. qq.indexOf = function(arr, elt, from){
  219. "use strict";
  220. if (arr.indexOf) {
  221. return arr.indexOf(elt, from);
  222. }
  223. from = from || 0;
  224. var len = arr.length;
  225. if (from < 0) {
  226. from += len;
  227. }
  228. for (; from < len; from+=1){
  229. if (arr.hasOwnProperty(from) && arr[from] === elt){
  230. return from;
  231. }
  232. }
  233. return -1;
  234. };
  235. //this is a version 4 UUID
  236. qq.getUniqueId = function(){
  237. "use strict";
  238. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  239. /*jslint eqeq: true, bitwise: true*/
  240. var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
  241. return v.toString(16);
  242. });
  243. };
  244. //
  245. // Browsers and platforms detection
  246. qq.ie = function(){
  247. "use strict";
  248. return navigator.userAgent.indexOf('MSIE') !== -1;
  249. };
  250. qq.ie10 = function(){
  251. "use strict";
  252. return navigator.userAgent.indexOf('MSIE 10') !== -1;
  253. };
  254. qq.safari = function(){
  255. "use strict";
  256. return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
  257. };
  258. qq.chrome = function(){
  259. "use strict";
  260. return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1;
  261. };
  262. qq.firefox = function(){
  263. "use strict";
  264. return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === '');
  265. };
  266. qq.windows = function(){
  267. "use strict";
  268. return navigator.platform === "Win32";
  269. };
  270. qq.android = function(){
  271. "use strict";
  272. return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
  273. };
  274. //
  275. // Events
  276. qq.preventDefault = function(e){
  277. "use strict";
  278. if (e.preventDefault){
  279. e.preventDefault();
  280. } else{
  281. e.returnValue = false;
  282. }
  283. };
  284. /**
  285. * Creates and returns element from html string
  286. * Uses innerHTML to create an element
  287. */
  288. qq.toElement = (function(){
  289. "use strict";
  290. var div = document.createElement('div');
  291. return function(html){
  292. div.innerHTML = html;
  293. var element = div.firstChild;
  294. div.removeChild(element);
  295. return element;
  296. };
  297. }());
  298. //key and value are passed to callback for each item in the object or array
  299. qq.each = function(obj, callback) {
  300. "use strict";
  301. var key, retVal;
  302. if (obj) {
  303. for (key in obj) {
  304. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  305. retVal = callback(key, obj[key]);
  306. if (retVal === false) {
  307. break;
  308. }
  309. }
  310. }
  311. }
  312. };
  313. /**
  314. * obj2url() takes a json-object as argument and generates
  315. * a querystring. pretty much like jQuery.param()
  316. *
  317. * how to use:
  318. *
  319. * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
  320. *
  321. * will result in:
  322. *
  323. * `http://any.url/upload?otherParam=value&a=b&c=d`
  324. *
  325. * @param Object JSON-Object
  326. * @param String current querystring-part
  327. * @return String encoded querystring
  328. */
  329. qq.obj2url = function(obj, temp, prefixDone){
  330. "use strict";
  331. /*jshint laxbreak: true*/
  332. var i, len,
  333. uristrings = [],
  334. prefix = '&',
  335. add = function(nextObj, i){
  336. var nextTemp = temp
  337. ? (/\[\]$/.test(temp)) // prevent double-encoding
  338. ? temp
  339. : temp+'['+i+']'
  340. : i;
  341. if ((nextTemp !== 'undefined') && (i !== 'undefined')) {
  342. uristrings.push(
  343. (typeof nextObj === 'object')
  344. ? qq.obj2url(nextObj, nextTemp, true)
  345. : (Object.prototype.toString.call(nextObj) === '[object Function]')
  346. ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
  347. : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
  348. );
  349. }
  350. };
  351. if (!prefixDone && temp) {
  352. prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
  353. uristrings.push(temp);
  354. uristrings.push(qq.obj2url(obj));
  355. } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) {
  356. // we wont use a for-in-loop on an array (performance)
  357. for (i = -1, len = obj.length; i < len; i+=1){
  358. add(obj[i], i);
  359. }
  360. } else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){
  361. // for anything else but a scalar, we will use for-in-loop
  362. for (i in obj){
  363. if (obj.hasOwnProperty(i)) {
  364. add(obj[i], i);
  365. }
  366. }
  367. } else {
  368. uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
  369. }
  370. if (temp) {
  371. return uristrings.join(prefix);
  372. } else {
  373. return uristrings.join(prefix)
  374. .replace(/^&/, '')
  375. .replace(/%20/g, '+');
  376. }
  377. };
  378. qq.obj2FormData = function(obj, formData, arrayKeyName) {
  379. "use strict";
  380. if (!formData) {
  381. formData = new FormData();
  382. }
  383. qq.each(obj, function(key, val) {
  384. key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key;
  385. if (qq.isObject(val)) {
  386. qq.obj2FormData(val, formData, key);
  387. }
  388. else if (qq.isFunction(val)) {
  389. formData.append(key, val());
  390. }
  391. else {
  392. formData.append(key, val);
  393. }
  394. });
  395. return formData;
  396. };
  397. qq.obj2Inputs = function(obj, form) {
  398. "use strict";
  399. var input;
  400. if (!form) {
  401. form = document.createElement('form');
  402. }
  403. qq.obj2FormData(obj, {
  404. append: function(key, val) {
  405. input = document.createElement('input');
  406. input.setAttribute('name', key);
  407. input.setAttribute('value', val);
  408. form.appendChild(input);
  409. }
  410. });
  411. return form;
  412. };
  413. qq.setCookie = function(name, value, days) {
  414. var date = new Date(),
  415. expires = "";
  416. if (days) {
  417. date.setTime(date.getTime()+(days*24*60*60*1000));
  418. expires = "; expires="+date.toGMTString();
  419. }
  420. document.cookie = name+"="+value+expires+"; path=/";
  421. };
  422. qq.getCookie = function(name) {
  423. var nameEQ = name + "=",
  424. ca = document.cookie.split(';'),
  425. c;
  426. for(var i=0;i < ca.length;i++) {
  427. c = ca[i];
  428. while (c.charAt(0)==' ') {
  429. c = c.substring(1,c.length);
  430. }
  431. if (c.indexOf(nameEQ) === 0) {
  432. return c.substring(nameEQ.length,c.length);
  433. }
  434. }
  435. };
  436. qq.getCookieNames = function(regexp) {
  437. var cookies = document.cookie.split(';'),
  438. cookieNames = [];
  439. qq.each(cookies, function(idx, cookie) {
  440. cookie = qq.trimStr(cookie);
  441. var equalsIdx = cookie.indexOf("=");
  442. if (cookie.match(regexp)) {
  443. cookieNames.push(cookie.substr(0, equalsIdx));
  444. }
  445. });
  446. return cookieNames;
  447. };
  448. qq.deleteCookie = function(name) {
  449. qq.setCookie(name, "", -1);
  450. };
  451. qq.areCookiesEnabled = function() {
  452. var randNum = Math.random() * 100000,
  453. name = "qqCookieTest:" + randNum;
  454. qq.setCookie(name, 1);
  455. if (qq.getCookie(name)) {
  456. qq.deleteCookie(name);
  457. return true;
  458. }
  459. return false;
  460. };
  461. /**
  462. * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
  463. * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
  464. */
  465. qq.parseJson = function(json) {
  466. /*jshint evil: true*/
  467. if (window.JSON && qq.isFunction(JSON.parse)) {
  468. return JSON.parse(json);
  469. } else {
  470. return eval("(" + json + ")");
  471. }
  472. };
  473. /**
  474. * A generic module which supports object disposing in dispose() method.
  475. * */
  476. qq.DisposeSupport = function() {
  477. "use strict";
  478. var disposers = [];
  479. return {
  480. /** Run all registered disposers */
  481. dispose: function() {
  482. var disposer;
  483. do {
  484. disposer = disposers.shift();
  485. if (disposer) {
  486. disposer();
  487. }
  488. }
  489. while (disposer);
  490. },
  491. /** Attach event handler and register de-attacher as a disposer */
  492. attach: function() {
  493. var args = arguments;
  494. /*jslint undef:true*/
  495. this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
  496. },
  497. /** Add disposer to the collection */
  498. addDisposer: function(disposeFunction) {
  499. disposers.push(disposeFunction);
  500. }
  501. };
  502. };
  503. qq.UploadButton = function(o){
  504. this._options = {
  505. element: null,
  506. // if set to true adds multiple attribute to file input
  507. multiple: false,
  508. acceptFiles: null,
  509. // name attribute of file input
  510. name: 'file',
  511. onChange: function(input){},
  512. hoverClass: 'qq-upload-button-hover',
  513. focusClass: 'qq-upload-button-focus'
  514. };
  515. qq.extend(this._options, o);
  516. this._disposeSupport = new qq.DisposeSupport();
  517. this._element = this._options.element;
  518. // make button suitable container for input
  519. qq(this._element).css({
  520. position: 'relative',
  521. overflow: 'hidden',
  522. // Make sure browse button is in the right side
  523. // in Internet Explorer
  524. direction: 'ltr'
  525. });
  526. this._input = this._createInput();
  527. };
  528. qq.UploadButton.prototype = {
  529. /* returns file input element */
  530. getInput: function(){
  531. return this._input;
  532. },
  533. /* cleans/recreates the file input */
  534. reset: function(){
  535. if (this._input.parentNode){
  536. qq(this._input).remove();
  537. }
  538. qq(this._element).removeClass(this._options.focusClass);
  539. this._input = this._createInput();
  540. },
  541. _createInput: function(){
  542. var input = document.createElement("input");
  543. if (this._options.multiple){
  544. input.setAttribute("multiple", "multiple");
  545. }
  546. if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
  547. input.setAttribute("type", "file");
  548. input.setAttribute("name", this._options.name);
  549. qq(input).css({
  550. position: 'absolute',
  551. // in Opera only 'browse' button
  552. // is clickable and it is located at
  553. // the right side of the input
  554. right: 0,
  555. top: 0,
  556. fontFamily: 'Arial',
  557. // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
  558. fontSize: '118px',
  559. margin: 0,
  560. padding: 0,
  561. cursor: 'pointer',
  562. opacity: 0
  563. });
  564. this._element.appendChild(input);
  565. var self = this;
  566. this._disposeSupport.attach(input, 'change', function(){
  567. self._options.onChange(input);
  568. });
  569. this._disposeSupport.attach(input, 'mouseover', function(){
  570. qq(self._element).addClass(self._options.hoverClass);
  571. });
  572. this._disposeSupport.attach(input, 'mouseout', function(){
  573. qq(self._element).removeClass(self._options.hoverClass);
  574. });
  575. this._disposeSupport.attach(input, 'focus', function(){
  576. qq(self._element).addClass(self._options.focusClass);
  577. });
  578. this._disposeSupport.attach(input, 'blur', function(){
  579. qq(self._element).removeClass(self._options.focusClass);
  580. });
  581. // IE and Opera, unfortunately have 2 tab stops on file input
  582. // which is unacceptable in our case, disable keyboard access
  583. if (window.attachEvent){
  584. // it is IE or Opera
  585. input.setAttribute('tabIndex', "-1");
  586. }
  587. return input;
  588. }
  589. };
  590. qq.FineUploaderBasic = function(o){
  591. var that = this;
  592. this._options = {
  593. debug: false,
  594. button: null,
  595. multiple: true,
  596. maxConnections: 3,
  597. disableCancelForFormUploads: false,
  598. autoUpload: true,
  599. request: {
  600. endpoint: '/server/upload',
  601. params: {},
  602. paramsInBody: true,
  603. customHeaders: {},
  604. forceMultipart: true,
  605. inputName: 'qqfile',
  606. uuidName: 'qquuid',
  607. totalFileSizeName: 'qqtotalfilesize'
  608. },
  609. validation: {
  610. allowedExtensions: [],
  611. sizeLimit: 0,
  612. minSizeLimit: 0,
  613. stopOnFirstInvalidFile: true
  614. },
  615. callbacks: {
  616. onSubmit: function(id, name){},
  617. onComplete: function(id, name, responseJSON){},
  618. onCancel: function(id, name){},
  619. onUpload: function(id, name){},
  620. onUploadChunk: function(id, name, chunkData){},
  621. onResume: function(id, fileName, chunkData){},
  622. onProgress: function(id, name, loaded, total){},
  623. onError: function(id, name, reason) {},
  624. onAutoRetry: function(id, name, attemptNumber) {},
  625. onManualRetry: function(id, name) {},
  626. onValidateBatch: function(fileOrBlobData) {},
  627. onValidate: function(fileOrBlobData) {},
  628. onSubmitDelete: function(id) {},
  629. onDelete: function(id){},
  630. onDeleteComplete: function(id, xhr, isError){}
  631. },
  632. messages: {
  633. typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
  634. sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
  635. minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
  636. emptyError: "{file} is empty, please select files again without it.",
  637. noFilesError: "No files to upload.",
  638. onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
  639. },
  640. retry: {
  641. enableAuto: false,
  642. maxAutoAttempts: 3,
  643. autoAttemptDelay: 5,
  644. preventRetryResponseProperty: 'preventRetry'
  645. },
  646. classes: {
  647. buttonHover: 'qq-upload-button-hover',
  648. buttonFocus: 'qq-upload-button-focus'
  649. },
  650. chunking: {
  651. enabled: false,
  652. partSize: 2000000,
  653. paramNames: {
  654. partIndex: 'qqpartindex',
  655. partByteOffset: 'qqpartbyteoffset',
  656. chunkSize: 'qqchunksize',
  657. totalFileSize: 'qqtotalfilesize',
  658. totalParts: 'qqtotalparts',
  659. filename: 'qqfilename'
  660. }
  661. },
  662. resume: {
  663. enabled: false,
  664. id: null,
  665. cookiesExpireIn: 7, //days
  666. paramNames: {
  667. resuming: "qqresume"
  668. }
  669. },
  670. formatFileName: function(fileOrBlobName) {
  671. if (fileOrBlobName.length > 33) {
  672. fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
  673. }
  674. return fileOrBlobName;
  675. },
  676. text: {
  677. sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
  678. },
  679. deleteFile : {
  680. enabled: false,
  681. endpoint: '/server/upload',
  682. customHeaders: {},
  683. params: {}
  684. },
  685. cors: {
  686. expected: false,
  687. sendCredentials: false
  688. },
  689. blobs: {
  690. defaultName: 'Misc data',
  691. paramNames: {
  692. name: 'qqblobname'
  693. }
  694. }
  695. };
  696. qq.extend(this._options, o, true);
  697. this._wrapCallbacks();
  698. this._disposeSupport = new qq.DisposeSupport();
  699. // number of files being uploaded
  700. this._filesInProgress = [];
  701. this._storedIds = [];
  702. this._autoRetries = [];
  703. this._retryTimeouts = [];
  704. this._preventRetries = [];
  705. this._paramsStore = this._createParamsStore("request");
  706. this._deleteFileParamsStore = this._createParamsStore("deleteFile");
  707. this._endpointStore = this._createEndpointStore("request");
  708. this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
  709. this._handler = this._createUploadHandler();
  710. this._deleteHandler = this._createDeleteHandler();
  711. if (this._options.button){
  712. this._button = this._createUploadButton(this._options.button);
  713. }
  714. this._preventLeaveInProgress();
  715. };
  716. qq.FineUploaderBasic.prototype = {
  717. log: function(str, level) {
  718. if (this._options.debug && (!level || level === 'info')) {
  719. qq.log('[FineUploader] ' + str);
  720. }
  721. else if (level && level !== 'info') {
  722. qq.log('[FineUploader] ' + str, level);
  723. }
  724. },
  725. setParams: function(params, id) {
  726. /*jshint eqeqeq: true, eqnull: true*/
  727. if (id == null) {
  728. this._options.request.params = params;
  729. }
  730. else {
  731. this._paramsStore.setParams(params, id);
  732. }
  733. },
  734. setDeleteFileParams: function(params, id) {
  735. /*jshint eqeqeq: true, eqnull: true*/
  736. if (id == null) {
  737. this._options.deleteFile.params = params;
  738. }
  739. else {
  740. this._deleteFileParamsStore.setParams(params, id);
  741. }
  742. },
  743. setEndpoint: function(endpoint, id) {
  744. /*jshint eqeqeq: true, eqnull: true*/
  745. if (id == null) {
  746. this._options.request.endpoint = endpoint;
  747. }
  748. else {
  749. this._endpointStore.setEndpoint(endpoint, id);
  750. }
  751. },
  752. getInProgress: function(){
  753. return this._filesInProgress.length;
  754. },
  755. uploadStoredFiles: function(){
  756. "use strict";
  757. var idToUpload;
  758. while(this._storedIds.length) {
  759. idToUpload = this._storedIds.shift();
  760. this._filesInProgress.push(idToUpload);
  761. this._handler.upload(idToUpload);
  762. }
  763. },
  764. clearStoredFiles: function(){
  765. this._storedIds = [];
  766. },
  767. retry: function(id) {
  768. if (this._onBeforeManualRetry(id)) {
  769. this._handler.retry(id);
  770. return true;
  771. }
  772. else {
  773. return false;
  774. }
  775. },
  776. cancel: function(id) {
  777. this._handler.cancel(id);
  778. },
  779. cancelAll: function() {
  780. var storedIdsCopy = [],
  781. self = this;
  782. qq.extend(storedIdsCopy, this._storedIds);
  783. qq.each(storedIdsCopy, function(idx, storedFileId) {
  784. self.cancel(storedFileId);
  785. });
  786. this._handler.cancelAll();
  787. },
  788. reset: function() {
  789. this.log("Resetting uploader...");
  790. this._handler.reset();
  791. this._filesInProgress = [];
  792. this._storedIds = [];
  793. this._autoRetries = [];
  794. this._retryTimeouts = [];
  795. this._preventRetries = [];
  796. this._button.reset();
  797. this._paramsStore.reset();
  798. this._endpointStore.reset();
  799. },
  800. addFiles: function(filesBlobDataOrInputs) {
  801. var self = this,
  802. verifiedFilesOrInputs = [],
  803. index, fileOrInput;
  804. if (filesBlobDataOrInputs) {
  805. if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) {
  806. filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs);
  807. }
  808. for (index = 0; index < filesBlobDataOrInputs.length; index+=1) {
  809. fileOrInput = filesBlobDataOrInputs[index];
  810. if (qq.isFileOrInput(fileOrInput)) {
  811. verifiedFilesOrInputs.push(fileOrInput);
  812. }
  813. else {
  814. self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn');
  815. }
  816. }
  817. this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
  818. this._uploadFileOrBlobDataList(verifiedFilesOrInputs);
  819. }
  820. },
  821. addBlobs: function(blobDataOrArray) {
  822. if (blobDataOrArray) {
  823. var blobDataArray = [].concat(blobDataOrArray),
  824. verifiedBlobDataList = [],
  825. self = this;
  826. qq.each(blobDataArray, function(idx, blobData) {
  827. if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
  828. verifiedBlobDataList.push({
  829. blob: blobData,
  830. name: self._options.blobs.defaultName
  831. });
  832. }
  833. else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
  834. verifiedBlobDataList.push(blobData);
  835. }
  836. else {
  837. self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
  838. }
  839. });
  840. this._uploadFileOrBlobDataList(verifiedBlobDataList);
  841. }
  842. else {
  843. this.log("undefined or non-array parameter passed into addBlobs", "error");
  844. }
  845. },
  846. getUuid: function(id) {
  847. return this._handler.getUuid(id);
  848. },
  849. getResumableFilesData: function() {
  850. return this._handler.getResumableFilesData();
  851. },
  852. getSize: function(id) {
  853. return this._handler.getSize(id);
  854. },
  855. getFile: function(fileOrBlobId) {
  856. return this._handler.getFile(fileOrBlobId);
  857. },
  858. deleteFile: function(id) {
  859. this._onSubmitDelete(id);
  860. },
  861. setDeleteFileEndpoint: function(endpoint, id) {
  862. /*jshint eqeqeq: true, eqnull: true*/
  863. if (id == null) {
  864. this._options.deleteFile.endpoint = endpoint;
  865. }
  866. else {
  867. this._deleteFileEndpointStore.setEndpoint(endpoint, id);
  868. }
  869. },
  870. _createUploadButton: function(element){
  871. var self = this;
  872. var button = new qq.UploadButton({
  873. element: element,
  874. multiple: this._options.multiple && qq.isXhrUploadSupported(),
  875. acceptFiles: this._options.validation.acceptFiles,
  876. onChange: function(input){
  877. self._onInputChange(input);
  878. },
  879. hoverClass: this._options.classes.buttonHover,
  880. focusClass: this._options.classes.buttonFocus
  881. });
  882. this._disposeSupport.addDisposer(function() { button.dispose(); });
  883. return button;
  884. },
  885. _createUploadHandler: function(){
  886. var self = this;
  887. return new qq.UploadHandler({
  888. debug: this._options.debug,
  889. forceMultipart: this._options.request.forceMultipart,
  890. maxConnections: this._options.maxConnections,
  891. customHeaders: this._options.request.customHeaders,
  892. inputName: this._options.request.inputName,
  893. uuidParamName: this._options.request.uuidName,
  894. totalFileSizeParamName: this._options.request.totalFileSizeName,
  895. cors: this._options.cors,
  896. demoMode: this._options.demoMode,
  897. paramsInBody: this._options.request.paramsInBody,
  898. paramsStore: this._paramsStore,
  899. endpointStore: this._endpointStore,
  900. chunking: this._options.chunking,
  901. resume: this._options.resume,
  902. blobs: this._options.blobs,
  903. log: function(str, level) {
  904. self.log(str, level);
  905. },
  906. onProgress: function(id, name, loaded, total){
  907. self._onProgress(id, name, loaded, total);
  908. self._options.callbacks.onProgress(id, name, loaded, total);
  909. },
  910. onComplete: function(id, name, result, xhr){
  911. self._onComplete(id, name, result, xhr);
  912. self._options.callbacks.onComplete(id, name, result);
  913. },
  914. onCancel: function(id, name){
  915. self._onCancel(id, name);
  916. self._options.callbacks.onCancel(id, name);
  917. },
  918. onUpload: function(id, name){
  919. self._onUpload(id, name);
  920. self._options.callbacks.onUpload(id, name);
  921. },
  922. onUploadChunk: function(id, name, chunkData){
  923. self._options.callbacks.onUploadChunk(id, name, chunkData);
  924. },
  925. onResume: function(id, name, chunkData) {
  926. return self._options.callbacks.onResume(id, name, chunkData);
  927. },
  928. onAutoRetry: function(id, name, responseJSON, xhr) {
  929. self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
  930. if (self._shouldAutoRetry(id, name, responseJSON)) {
  931. self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
  932. self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
  933. self._onBeforeAutoRetry(id, name);
  934. self._retryTimeouts[id] = setTimeout(function() {
  935. self._onAutoRetry(id, name, responseJSON)
  936. }, self._options.retry.autoAttemptDelay * 1000);
  937. return true;
  938. }
  939. else {
  940. return false;
  941. }
  942. }
  943. });
  944. },
  945. _createDeleteHandler: function() {
  946. var self = this;
  947. return new qq.DeleteFileAjaxRequestor({
  948. maxConnections: this._options.maxConnections,
  949. customHeaders: this._options.deleteFile.customHeaders,
  950. paramsStore: this._deleteFileParamsStore,
  951. endpointStore: this._deleteFileEndpointStore,
  952. demoMode: this._options.demoMode,
  953. cors: this._options.cors,
  954. log: function(str, level) {
  955. self.log(str, level);
  956. },
  957. onDelete: function(id) {
  958. self._onDelete(id);
  959. self._options.callbacks.onDelete(id);
  960. },
  961. onDeleteComplete: function(id, xhr, isError) {
  962. self._onDeleteComplete(id, xhr, isError);
  963. self._options.callbacks.onDeleteComplete(id, xhr, isError);
  964. }
  965. });
  966. },
  967. _preventLeaveInProgress: function(){
  968. var self = this;
  969. this._disposeSupport.attach(window, 'beforeunload', function(e){
  970. if (!self._filesInProgress.length){return;}
  971. var e = e || window.event;
  972. // for ie, ff
  973. e.returnValue = self._options.messages.onLeave;
  974. // for webkit
  975. return self._options.messages.onLeave;
  976. });
  977. },
  978. _onSubmit: function(id, name){
  979. if (this._options.autoUpload) {
  980. this._filesInProgress.push(id);
  981. }
  982. },
  983. _onProgress: function(id, name, loaded, total){
  984. },
  985. _onComplete: function(id, name, result, xhr){
  986. this._removeFromFilesInProgress(id);
  987. this._maybeParseAndSendUploadError(id, name, result, xhr);
  988. },
  989. _onCancel: function(id, name){
  990. this._removeFromFilesInProgress(id);
  991. clearTimeout(this._retryTimeouts[id]);
  992. var storedItemIndex = qq.indexOf(this._storedIds, id);
  993. if (!this._options.autoUpload && storedItemIndex >= 0) {
  994. this._storedIds.splice(storedItemIndex, 1);
  995. }
  996. },
  997. _isDeletePossible: function() {
  998. return (this._options.deleteFile.enabled &&
  999. (!this._options.cors.expected ||
  1000. (this._options.cors.expected && (qq.ie10() || !qq.ie()))
  1001. )
  1002. );
  1003. },
  1004. _onSubmitDelete: function(id) {
  1005. if (this._isDeletePossible()) {
  1006. if (this._options.callbacks.onSubmitDelete(id)) {
  1007. this._deleteHandler.sendDelete(id, this.getUuid(id));
  1008. }
  1009. }
  1010. else {
  1011. this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
  1012. "due to CORS on a user agent that does not support pre-flighting.", "warn");
  1013. return false;
  1014. }
  1015. },
  1016. _onDelete: function(fileId) {},
  1017. _onDeleteComplete: function(id, xhr, isError) {
  1018. var name = this._handler.getName(id);
  1019. if (isError) {
  1020. this.log("Delete request for '" + name + "' has failed.", "error");
  1021. this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status);
  1022. }
  1023. else {
  1024. this.log("Delete request for '" + name + "' has succeeded.");
  1025. }
  1026. },
  1027. _removeFromFilesInProgress: function(id) {
  1028. var index = qq.indexOf(this._filesInProgress, id);
  1029. if (index >= 0) {
  1030. this._filesInProgress.splice(index, 1);
  1031. }
  1032. },
  1033. _onUpload: function(id, name){},
  1034. _onInputChange: function(input){
  1035. if (qq.isXhrUploadSupported()){
  1036. this.addFiles(input.files);
  1037. } else {
  1038. this.addFiles(input);
  1039. }
  1040. this._button.reset();
  1041. },
  1042. _onBeforeAutoRetry: function(id, name) {
  1043. this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
  1044. },
  1045. _onAutoRetry: function(id, name, responseJSON) {
  1046. this.log("Retrying " + name + "...");
  1047. this._autoRetries[id]++;
  1048. this._handler.retry(id);
  1049. },
  1050. _shouldAutoRetry: function(id, name, responseJSON) {
  1051. if (!this._preventRetries[id] && this._options.retry.enableAuto) {
  1052. if (this._autoRetries[id] === undefined) {
  1053. this._autoRetries[id] = 0;
  1054. }
  1055. return this._autoRetries[id] < this._options.retry.maxAutoAttempts
  1056. }
  1057. return false;
  1058. },
  1059. //return false if we should not attempt the requested retry
  1060. _onBeforeManualRetry: function(id) {
  1061. if (this._preventRetries[id]) {
  1062. this.log("Retries are forbidden for id " + id, 'warn');
  1063. return false;
  1064. }
  1065. else if (this._handler.isValid(id)) {
  1066. var fileName = this._handler.getName(id);
  1067. if (this._options.callbacks.onManualRetry(id, fileName) === false) {
  1068. return false;
  1069. }
  1070. this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
  1071. this._filesInProgress.push(id);
  1072. return true;
  1073. }
  1074. else {
  1075. this.log("'" + id + "' is not a valid file ID", 'error');
  1076. return false;
  1077. }
  1078. },
  1079. _maybeParseAndSendUploadError: function(id, name, response, xhr) {
  1080. //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
  1081. if (!response.success){
  1082. if (xhr && xhr.status !== 200 && !response.error) {
  1083. this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status);
  1084. }
  1085. else {
  1086. var errorReason = response.error ? response.error : "Upload failure reason unknown";
  1087. this._options.callbacks.onError(id, name, errorReason);
  1088. }
  1089. }
  1090. },
  1091. _uploadFileOrBlobDataList: function(fileOrBlobDataList){
  1092. var validationDescriptors, index, batchInvalid;
  1093. validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList);
  1094. batchInvalid = this._options.callbacks.onValidateBatch(validationDescriptors) === false;
  1095. if (!batchInvalid) {
  1096. if (fileOrBlobDataList.length > 0) {
  1097. for (index = 0; index < fileOrBlobDataList.length; index++){
  1098. if (this._validateFileOrBlobData(fileOrBlobDataList[index])){
  1099. this._upload(fileOrBlobDataList[index]);
  1100. } else {
  1101. if (this._options.validation.stopOnFirstInvalidFile){
  1102. return;
  1103. }
  1104. }
  1105. }
  1106. }
  1107. else {
  1108. this._error('noFilesError', "");
  1109. }
  1110. }
  1111. },
  1112. _upload: function(blobOrFileContainer){
  1113. var id = this._handler.add(blobOrFileContainer);
  1114. var name = this._handler.getName(id);
  1115. if (this._options.callbacks.onSubmit(id, name) !== false){
  1116. this._onSubmit(id, name);
  1117. if (this._options.autoUpload) {
  1118. this._handler.upload(id);
  1119. }
  1120. else {
  1121. this._storeForLater(id);
  1122. }
  1123. }
  1124. },
  1125. _storeForLater: function(id) {
  1126. this._storedIds.push(id);
  1127. },
  1128. _validateFileOrBlobData: function(fileOrBlobData){
  1129. var validationDescriptor, name, size;
  1130. validationDescriptor = this._getValidationDescriptor(fileOrBlobData);
  1131. name = validationDescriptor.name;
  1132. size = validationDescriptor.size;
  1133. if (this._options.callbacks.onValidate(validationDescriptor) === false) {
  1134. return false;
  1135. }
  1136. if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){
  1137. this._error('typeError', name);
  1138. return false;
  1139. }
  1140. else if (size === 0){
  1141. this._error('emptyError', name);
  1142. return false;
  1143. }
  1144. else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
  1145. this._error('sizeError', name);
  1146. return false;
  1147. }
  1148. else if (size && size < this._options.validation.minSizeLimit){
  1149. this._error('minSizeError', name);
  1150. return false;
  1151. }
  1152. return true;
  1153. },
  1154. _error: function(code, name){
  1155. var message = this._options.messages[code];
  1156. function r(name, replacement){ message = message.replace(name, replacement); }
  1157. var extensions = this._options.validation.allowedExtensions.join(', ').toLowerCase();
  1158. r('{file}', this._options.formatFileName(name));
  1159. r('{extensions}', extensions);
  1160. r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
  1161. r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
  1162. this._options.callbacks.onError(null, name, message);
  1163. return message;
  1164. },
  1165. _isAllowedExtension: function(fileName){
  1166. var allowed = this._options.validation.allowedExtensions,
  1167. valid = false;
  1168. if (!allowed.length) {
  1169. return true;
  1170. }
  1171. qq.each(allowed, function(idx, allowedExt) {
  1172. /*jshint eqeqeq: true, eqnull: true*/
  1173. var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
  1174. if (fileName.match(extRegex) != null) {
  1175. valid = true;
  1176. return false;
  1177. }
  1178. });
  1179. return valid;
  1180. },
  1181. _formatSize: function(bytes){
  1182. var i = -1;
  1183. do {
  1184. bytes = bytes / 1024;
  1185. i++;
  1186. } while (bytes > 99);
  1187. return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
  1188. },
  1189. _wrapCallbacks: function() {
  1190. var self, safeCallback;
  1191. self = this;
  1192. safeCallback = function(name, callback, args) {
  1193. try {
  1194. return callback.apply(self, args);
  1195. }
  1196. catch (exception) {
  1197. self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
  1198. }
  1199. }
  1200. for (var prop in this._options.callbacks) {
  1201. (function() {
  1202. var callbackName, callbackFunc;
  1203. callbackName = prop;
  1204. callbackFunc = self._options.callbacks[callbackName];
  1205. self._options.callbacks[callbackName] = function() {
  1206. return safeCallback(callbackName, callbackFunc, arguments);
  1207. }
  1208. }());
  1209. }
  1210. },
  1211. _parseFileOrBlobDataName: function(fileOrBlobData) {
  1212. var name;
  1213. if (qq.isFileOrInput(fileOrBlobData)) {
  1214. if (fileOrBlobData.value) {
  1215. // it is a file input
  1216. // get input value and remove path to normalize
  1217. name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
  1218. } else {
  1219. // fix missing properties in Safari 4 and firefox 11.0a2
  1220. name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
  1221. }
  1222. }
  1223. else {
  1224. name = fileOrBlobData.name;
  1225. }
  1226. return name;
  1227. },
  1228. _parseFileOrBlobDataSize: function(fileOrBlobData) {
  1229. var size;
  1230. if (qq.isFileOrInput(fileOrBlobData)) {
  1231. if (!fileOrBlobData.value){
  1232. // fix missing properties in Safari 4 and firefox 11.0a2
  1233. size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
  1234. }
  1235. }
  1236. else {
  1237. size = fileOrBlobData.blob.size;
  1238. }
  1239. return size;
  1240. },
  1241. _getValidationDescriptor: function(fileOrBlobData) {
  1242. var name, size, fileDescriptor;
  1243. fileDescriptor = {};
  1244. name = this._parseFileOrBlobDataName(fileOrBlobData);
  1245. size = this._parseFileOrBlobDataSize(fileOrBlobData);
  1246. fileDescriptor.name = name;
  1247. if (size) {
  1248. fileDescriptor.size = size;
  1249. }
  1250. return fileDescriptor;
  1251. },
  1252. _getValidationDescriptors: function(files) {
  1253. var self = this,
  1254. fileDescriptors = [];
  1255. qq.each(files, function(idx, file) {
  1256. fileDescriptors.push(self._getValidationDescriptor(file));
  1257. });
  1258. return fileDescriptors;
  1259. },
  1260. _createParamsStore: function(type) {
  1261. var paramsStore = {},
  1262. self = this;
  1263. return {
  1264. setParams: function(params, id) {
  1265. var paramsCopy = {};
  1266. qq.extend(paramsCopy, params);
  1267. paramsStore[id] = paramsCopy;
  1268. },
  1269. getParams: function(id) {
  1270. /*jshint eqeqeq: true, eqnull: true*/
  1271. var paramsCopy = {};
  1272. if (id != null && paramsStore[id]) {
  1273. qq.extend(paramsCopy, paramsStore[id]);
  1274. }
  1275. else {
  1276. qq.extend(paramsCopy, self._options[type].params);
  1277. }
  1278. return paramsCopy;
  1279. },
  1280. remove: function(fileId) {
  1281. return delete paramsStore[fileId];
  1282. },
  1283. reset: function() {
  1284. paramsStore = {};
  1285. }
  1286. };
  1287. },
  1288. _createEndpointStore: function(type) {
  1289. var endpointStore = {},
  1290. self = this;
  1291. return {
  1292. setEndpoint: function(endpoint, id) {
  1293. endpointStore[id] = endpoint;
  1294. },
  1295. getEndpoint: function(id) {
  1296. /*jshint eqeqeq: true, eqnull: true*/
  1297. if (id != null && endpointStore[id]) {
  1298. return endpointStore[id];
  1299. }
  1300. return self._options[type].endpoint;
  1301. },
  1302. remove: function(fileId) {
  1303. return delete endpointStore[fileId];
  1304. },
  1305. reset: function() {
  1306. endpointStore = {};
  1307. }
  1308. };
  1309. }
  1310. };
  1311. /*globals qq, document*/
  1312. qq.DragAndDrop = function(o) {
  1313. "use strict";
  1314. var options, dz, dirPending,
  1315. droppedFiles = [],
  1316. droppedEntriesCount = 0,
  1317. droppedEntriesParsedCount = 0,
  1318. disposeSupport = new qq.DisposeSupport();
  1319. options = {
  1320. dropArea: null,
  1321. extraDropzones: [],
  1322. hideDropzones: true,
  1323. multiple: true,
  1324. classes: {
  1325. dropActive: null
  1326. },
  1327. callbacks: {
  1328. dropProcessing: function(isProcessing, files) {},
  1329. error: function(code, filename) {},
  1330. log: function(message, level) {}
  1331. }
  1332. };
  1333. qq.extend(options, o);
  1334. function maybeUploadDroppedFiles() {
  1335. if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) {
  1336. options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal.");
  1337. dz.dropDisabled(false);
  1338. options.callbacks.dropProcessing(false, droppedFiles);
  1339. }
  1340. }
  1341. function addDroppedFile(file) {
  1342. droppedFiles.push(file);
  1343. droppedEntriesParsedCount+=1;
  1344. maybeUploadDroppedFiles();
  1345. }
  1346. function traverseFileTree(entry) {
  1347. var dirReader, i;
  1348. droppedEntriesCount+=1;
  1349. if (entry.isFile) {
  1350. entry.file(function(file) {
  1351. addDroppedFile(file);
  1352. });
  1353. }
  1354. else if (entry.isDirectory) {
  1355. dirPending = true;
  1356. dirReader = entry.createReader();
  1357. dirReader.readEntries(function(entries) {
  1358. droppedEntriesParsedCount+=1;
  1359. for (i = 0; i < entries.length; i+=1) {
  1360. traverseFileTree(entries[i]);
  1361. }
  1362. dirPending = false;
  1363. if (!entries.length) {
  1364. maybeUploadDroppedFiles();
  1365. }
  1366. });
  1367. }
  1368. }
  1369. function handleDataTransfer(dataTransfer) {
  1370. var i, items, entry;
  1371. options.callbacks.dropProcessing(true);
  1372. dz.dropDisabled(true);
  1373. if (dataTransfer.files.length > 1 && !options.multiple) {
  1374. options.callbacks.dropProcessing(false);
  1375. options.callbacks.error('tooManyFilesError', "");
  1376. dz.dropDisabled(false);
  1377. }
  1378. else {
  1379. droppedFiles = [];
  1380. droppedEntriesCount = 0;
  1381. droppedEntriesParsedCount = 0;
  1382. if (qq.isFolderDropSupported(dataTransfer)) {
  1383. items = dataTransfer.items;
  1384. for (i = 0; i < items.length; i+=1) {
  1385. entry = items[i].webkitGetAsEntry();
  1386. if (entry) {
  1387. //due to a bug in Chrome's File System API impl - #149735
  1388. if (entry.isFile) {
  1389. droppedFiles.push(items[i].getAsFile());
  1390. if (i === items.length-1) {
  1391. maybeUploadDroppedFiles();
  1392. }
  1393. }
  1394. else {
  1395. traverseFileTree(entry);
  1396. }
  1397. }
  1398. }
  1399. }
  1400. else {
  1401. options.callbacks.dropProcessing(false, dataTransfer.files);
  1402. dz.dropDisabled(false);
  1403. }
  1404. }
  1405. }
  1406. function setupDropzone(dropArea){
  1407. dz = new qq.UploadDropZone({
  1408. element: dropArea,
  1409. onEnter: function(e){
  1410. qq(dropArea).addClass(options.classes.dropActive);
  1411. e.stopPropagation();
  1412. },
  1413. onLeaveNotDescendants: function(e){
  1414. qq(dropArea).removeClass(options.classes.dropActive);
  1415. },
  1416. onDrop: function(e){
  1417. if (options.hideDropzones) {
  1418. qq(dropArea).hide();
  1419. }
  1420. qq(dropArea).removeClass(options.classes.dropActive);
  1421. handleDataTransfer(e.dataTransfer);
  1422. }
  1423. });
  1424. disposeSupport.addDisposer(function() {
  1425. dz.dispose();
  1426. });
  1427. if (options.hideDropzones) {
  1428. qq(dropArea).hide();
  1429. }
  1430. }
  1431. function isFileDrag(dragEvent) {
  1432. var fileDrag;
  1433. qq.each(dragEvent.dataTransfer.types, function(key, val) {
  1434. if (val === 'Files') {
  1435. fileDrag = true;
  1436. return false;
  1437. }
  1438. });
  1439. return fileDrag;
  1440. }
  1441. function setupDragDrop(){
  1442. if (options.dropArea) {
  1443. options.extraDropzones.push(options.dropArea);
  1444. }
  1445. var i, dropzones = options.extraDropzones;
  1446. for (i=0; i < dropzones.length; i+=1){
  1447. setupDropzone(dropzones[i]);
  1448. }
  1449. // IE <= 9 does not support the File API used for drag+drop uploads
  1450. if (options.dropArea && (!qq.ie() || qq.ie10())) {
  1451. disposeSupport.attach(document, 'dragenter', function(e) {
  1452. if (!dz.dropDisabled() && isFileDrag(e)) {
  1453. if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) {
  1454. return;
  1455. }
  1456. options.dropArea.style.display = 'block';
  1457. for (i=0; i < dropzones.length; i+=1) {
  1458. dropzones[i].style.display = 'block';
  1459. }
  1460. }
  1461. });
  1462. }
  1463. disposeSupport.attach(document, 'dragleave', function(e){
  1464. if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
  1465. for (i=0; i < dropzones.length; i+=1) {
  1466. qq(dropzones[i]).hide();
  1467. }
  1468. }
  1469. });
  1470. disposeSupport.attach(document, 'drop', function(e){
  1471. if (options.hideDropzones) {
  1472. for (i=0; i < dropzones.length; i+=1) {
  1473. qq(dropzones[i]).hide();
  1474. }
  1475. }
  1476. e.preventDefault();
  1477. });
  1478. }
  1479. return {
  1480. setup: function() {
  1481. setupDragDrop();
  1482. },
  1483. setupExtraDropzone: function(element) {
  1484. options.extraDropzones.push(element);
  1485. setupDropzone(element);
  1486. },
  1487. removeExtraDropzone: function(element) {
  1488. var i, dzs = options.extraDropzones;
  1489. for(i in dzs) {
  1490. if (dzs[i] === element) {
  1491. return dzs.splice(i, 1);
  1492. }
  1493. }
  1494. },
  1495. dispose: function() {
  1496. disposeSupport.dispose();
  1497. dz.dispose();
  1498. }
  1499. };
  1500. };
  1501. qq.UploadDropZone = function(o){
  1502. "use strict";
  1503. var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport();
  1504. options = {
  1505. element: null,
  1506. onEnter: function(e){},
  1507. onLeave: function(e){},
  1508. // is not fired when leaving element by hovering descendants
  1509. onLeaveNotDescendants: function(e){},
  1510. onDrop: function(e){}
  1511. };
  1512. qq.extend(options, o);
  1513. element = options.element;
  1514. function dragover_should_be_canceled(){
  1515. return qq.safari() || (qq.firefox() && qq.windows());
  1516. }
  1517. function disableDropOutside(e){
  1518. // run only once for all instances
  1519. if (!dropOutsideDisabled ){
  1520. // for these cases we need to catch onDrop to reset dropArea
  1521. if (dragover_should_be_canceled){
  1522. disposeSupport.attach(document, 'dragover', function(e){
  1523. e.preventDefault();
  1524. });
  1525. } else {
  1526. disposeSupport.attach(document, 'dragover', function(e){
  1527. if (e.dataTransfer){
  1528. e.dataTransfer.dropEffect = 'none';
  1529. e.preventDefault();
  1530. }
  1531. });
  1532. }
  1533. dropOutsideDisabled = true;
  1534. }
  1535. }
  1536. function isValidFileDrag(e){
  1537. // e.dataTransfer currently causing IE errors
  1538. // IE9 does NOT support file API, so drag-and-drop is not possible
  1539. if (qq.ie() && !qq.ie10()) {
  1540. return false;
  1541. }
  1542. var effectTest, dt = e.dataTransfer,
  1543. // do not check dt.types.contains in webkit, because it crashes safari 4
  1544. isSafari = qq.safari();
  1545. // dt.effectAllowed is none in Safari 5
  1546. // dt.types.contains check is for firefox
  1547. effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none';
  1548. return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
  1549. }
  1550. function isOrSetDropDisabled(isDisabled) {
  1551. if (isDisabled !== undefined) {
  1552. preventDrop = isDisabled;
  1553. }
  1554. return preventDrop;
  1555. }
  1556. function attachEvents(){
  1557. disposeSupport.attach(element, 'dragover', function(e){
  1558. if (!isValidFileDrag(e)) {
  1559. return;
  1560. }
  1561. var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
  1562. if (effect === 'move' || effect === 'linkMove'){
  1563. e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
  1564. } else {
  1565. e.dataTransfer.dropEffect = 'copy'; // for Chrome
  1566. }
  1567. e.stopPropagation();
  1568. e.preventDefault();
  1569. });
  1570. disposeSupport.attach(element, 'dragenter', function(e){
  1571. if (!isOrSetDropDisabled()) {
  1572. if (!isValidFileDrag(e)) {
  1573. return;
  1574. }
  1575. options.onEnter(e);
  1576. }
  1577. });
  1578. disposeSupport.attach(element, 'dragleave', function(e){
  1579. if (!isValidFileDrag(e)) {
  1580. return;
  1581. }
  1582. options.onLeave(e);
  1583. var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
  1584. // do not fire when moving a mouse over a descendant
  1585. if (qq(this).contains(relatedTarget)) {
  1586. return;
  1587. }
  1588. options.onLeaveNotDescendants(e);
  1589. });
  1590. disposeSupport.attach(element, 'drop', function(e){
  1591. if (!isOrSetDropDisabled()) {
  1592. if (!isValidFileDrag(e)) {
  1593. return;
  1594. }
  1595. e.preventDefault();
  1596. options.onDrop(e);
  1597. }
  1598. });
  1599. }
  1600. disableDropOutside();
  1601. attachEvents();
  1602. return {
  1603. dropDisabled: function(isDisabled) {
  1604. return isOrSetDropDisabled(isDisabled);
  1605. },
  1606. dispose: function() {
  1607. disposeSupport.dispose();
  1608. }
  1609. };
  1610. };
  1611. /**
  1612. * Class that creates upload widget with drag-and-drop and file list
  1613. * @inherits qq.FineUploaderBasic
  1614. */
  1615. qq.FineUploader = function(o){
  1616. // call parent constructor
  1617. qq.FineUploaderBasic.apply(this, arguments);
  1618. // additional options
  1619. qq.extend(this._options, {
  1620. element: null,
  1621. listElement: null,
  1622. dragAndDrop: {
  1623. extraDropzones: [],
  1624. hideDropzones: true,
  1625. disableDefaultDropzone: false
  1626. },
  1627. text: {
  1628. uploadButton: 'Upload a file',
  1629. cancelButton: 'Cancel',
  1630. retryButton: 'Retry',
  1631. deleteButton: 'Delete',
  1632. failUpload: 'Upload failed',
  1633. dragZone: 'Drop files here to upload',
  1634. dropProcessing: 'Processing dropped files...',
  1635. formatProgress: "{percent}% of {total_size}",
  1636. waitingForResponse: "Processing..."
  1637. },
  1638. template: '<div class="qq-uploader">' +
  1639. ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
  1640. (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
  1641. '<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>' +
  1642. (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
  1643. '</div>',
  1644. // template for one item in file list
  1645. fileTemplate: '<li>' +
  1646. '<div class="qq-progress-bar"></div>' +
  1647. '<span class="qq-upload-spinner"></span>' +
  1648. '<span class="qq-upload-finished"></span>' +
  1649. '<span class="qq-upload-file"></span>' +
  1650. '<span class="qq-upload-size"></span>' +
  1651. '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
  1652. '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
  1653. '<a class="qq-upload-delete" href="#">{deleteButtonText}</a>' +
  1654. '<span class="qq-upload-status-text">{statusText}</span>' +
  1655. '</li>',
  1656. classes: {
  1657. button: 'qq-upload-button',
  1658. drop: 'qq-upload-drop-area',
  1659. dropActive: 'qq-upload-drop-area-active',
  1660. dropDisabled: 'qq-upload-drop-area-disabled',
  1661. list: 'qq-upload-list',
  1662. progressBar: 'qq-progress-bar',
  1663. file: 'qq-upload-file',
  1664. spinner: 'qq-upload-spinner',
  1665. finished: 'qq-upload-finished',
  1666. retrying: 'qq-upload-retrying',
  1667. retryable: 'qq-upload-retryable',
  1668. size: 'qq-upload-size',
  1669. cancel: 'qq-upload-cancel',
  1670. deleteButton: 'qq-upload-delete',
  1671. retry: 'qq-upload-retry',
  1672. statusText: 'qq-upload-status-text',
  1673. success: 'qq-upload-success',
  1674. fail: 'qq-upload-fail',
  1675. successIcon: null,
  1676. failIcon: null,
  1677. dropProcessing: 'qq-drop-processing',
  1678. dropProcessingSpinner: 'qq-drop-processing-spinner'
  1679. },
  1680. failedUploadTextDisplay: {
  1681. mode: 'default', //default, custom, or none
  1682. maxChars: 50,
  1683. responseProperty: 'error',
  1684. enableTooltip: true
  1685. },
  1686. messages: {
  1687. tooManyFilesError: "You may only drop one file"
  1688. },
  1689. retry: {
  1690. showAutoRetryNote: true,
  1691. autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
  1692. showButton: false
  1693. },
  1694. deleteFile: {
  1695. forceConfirm: false,
  1696. confirmMessage: "Are you sure you want to delete {filename}?",
  1697. deletingStatusText: "Deleting...",
  1698. deletingFailedText: "Delete failed"
  1699. },
  1700. display: {
  1701. fileSizeOnSubmit: false
  1702. },
  1703. showMessage: function(message){
  1704. setTimeout(function() {
  1705. alert(message);
  1706. }, 0);
  1707. },
  1708. showConfirm: function(message, okCallback, cancelCallback) {
  1709. setTimeout(function() {
  1710. var result = confirm(message);
  1711. if (result) {
  1712. okCallback();
  1713. }
  1714. else if (cancelCallback) {
  1715. cancelCallback();
  1716. }
  1717. }, 0);
  1718. }
  1719. }, true);
  1720. // overwrite options with user supplied
  1721. qq.extend(this._options, o, true);
  1722. this._wrapCallbacks();
  1723. // overwrite the upload button text if any
  1724. // same for the Cancel button and Fail message text
  1725. this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
  1726. this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
  1727. this._options.template = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing);
  1728. this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
  1729. this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
  1730. this._options.fileTemplate = this._options.fileTemplate.replace(/\{deleteButtonText\}/g, this._options.text.deleteButton);
  1731. this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
  1732. this._element = this._options.element;
  1733. this._element.innerHTML = this._options.template;
  1734. this._listElement = this._options.listElement || this._find(this._element, 'list');
  1735. this._classes = this._options.classes;
  1736. if (!this._button) {
  1737. this._button = this._createUploadButton(this._find(this._element, 'button'));
  1738. }
  1739. this._bindCancelAndRetryEvents();
  1740. this._dnd = this._setupDragAndDrop();
  1741. };
  1742. // inherit from Basic Uploader
  1743. qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype);
  1744. qq.extend(qq.FineUploader.prototype, {
  1745. clearStoredFiles: function() {
  1746. qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
  1747. this._listElement.innerHTML = "";
  1748. },
  1749. addExtraDropzone: function(element){
  1750. this._dnd.setupExtraDropzone(element);
  1751. },
  1752. removeExtraDropzone: function(element){
  1753. return this._dnd.removeExtraDropzone(element);
  1754. },
  1755. getItemByFileId: function(id){
  1756. var item = this._listElement.firstChild;
  1757. // there can't be txt nodes in dynamically created list
  1758. // and we can use nextSibling
  1759. while (item){
  1760. if (item.qqFileId == id) return item;
  1761. item = item.nextSibling;
  1762. }
  1763. },
  1764. reset: function() {
  1765. qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
  1766. this._element.innerHTML = this._options.template;
  1767. this._listElement = this._options.listElement || this._find(this._element, 'list');
  1768. if (!this._options.button) {
  1769. this._button = this._createUploadButton(this._find(this._element, 'button'));
  1770. }
  1771. this._bindCancelAndRetryEvents();
  1772. this._dnd.dispose();
  1773. this._dnd = this._setupDragAndDrop();
  1774. },
  1775. _removeFileItem: function(fileId) {
  1776. var item = this.getItemByFileId(fileId);
  1777. qq(item).remove();
  1778. },
  1779. _setupDragAndDrop: function() {
  1780. var self = this,
  1781. dropProcessingEl = this._find(this._element, 'dropProcessing'),
  1782. dnd, preventSelectFiles, defaultDropAreaEl;
  1783. preventSelectFiles = function(event) {
  1784. event.preventDefault();
  1785. };
  1786. if (!this._options.dragAndDrop.disableDefaultDropzone) {
  1787. defaultDropAreaEl = this._find(this._options.element, 'drop');
  1788. }
  1789. dnd = new qq.DragAndDrop({
  1790. dropArea: defaultDropAreaEl,
  1791. extraDropzones: this._options.dragAndDrop.extraDropzones,
  1792. hideDropzones: this._options.dragAndDrop.hideDropzones,
  1793. multiple: this._options.multiple,
  1794. classes: {
  1795. dropActive: this._options.classes.dropActive
  1796. },
  1797. callbacks: {
  1798. dropProcessing: function(isProcessing, files) {
  1799. var input = self._button.getInput();
  1800. if (isProcessing) {
  1801. qq(dropProcessingEl).css({display: 'block'});
  1802. qq(input).attach('click', preventSelectFiles);
  1803. }
  1804. else {
  1805. qq(dropProcessingEl).hide();
  1806. qq(input).detach('click', preventSelectFiles);
  1807. }
  1808. if (files) {
  1809. self.addFiles(files);
  1810. }
  1811. },
  1812. error: function(code, filename) {
  1813. self._error(code, filename);
  1814. },
  1815. log: function(message, level) {
  1816. self.log(message, level);
  1817. }
  1818. }
  1819. });
  1820. dnd.setup();
  1821. return dnd;
  1822. },
  1823. _leaving_document_out: function(e){
  1824. return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
  1825. || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
  1826. },
  1827. _storeForLater: function(id) {
  1828. qq.FineUploaderBasic.prototype._storeForLater.apply(this, arguments);
  1829. var item = this.getItemByFileId(id);
  1830. qq(this._find(item, 'spinner')).hide();
  1831. },
  1832. /**
  1833. * Gets one of the elements listed in this._options.classes
  1834. **/
  1835. _find: function(parent, type){
  1836. var element = qq(parent).getByClass(this._options.classes[type])[0];
  1837. if (!element){
  1838. throw new Error('element not found ' + type);
  1839. }
  1840. return element;
  1841. },
  1842. _onSubmit: function(id, name){
  1843. qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
  1844. this._addToList(id, name);
  1845. },
  1846. // Update the progress bar & percentage as the file is uploaded
  1847. _onProgress: function(id, name, loaded, total){
  1848. qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
  1849. var item, progressBar, percent, cancelLink;
  1850. item = this.getItemByFileId(id);
  1851. progressBar = this._find(item, 'progressBar');
  1852. percent = Math.round(loaded / total * 100);
  1853. if (loaded === total) {
  1854. cancelLink = this._find(item, 'cancel');
  1855. qq(cancelLink).hide();
  1856. qq(progressBar).hide();
  1857. qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
  1858. // If last byte was sent, display total file size
  1859. this._displayFileSize(id);
  1860. }
  1861. else {
  1862. // If still uploading, display percentage - total size is actually the total request(s) size
  1863. this._displayFileSize(id, loaded, total);
  1864. qq(progressBar).css({display: 'block'});
  1865. }
  1866. // Update progress bar element
  1867. qq(progressBar).css({width: percent + '%'});
  1868. },
  1869. _onComplete: function(id, name, result, xhr){
  1870. qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
  1871. var item = this.getItemByFileId(id);
  1872. qq(this._find(item, 'statusText')).clearText();
  1873. qq(item).removeClass(this._classes.retrying);
  1874. qq(this._find(item, 'progressBar')).hide();
  1875. if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
  1876. qq(this._find(item, 'cancel')).hide();
  1877. }
  1878. qq(this._find(item, 'spinner')).hide();
  1879. if (result.success) {
  1880. if (this._isDeletePossible()) {
  1881. this._showDeleteLink(id);
  1882. }
  1883. qq(item).addClass(this._classes.success);
  1884. if (this._classes.successIcon) {
  1885. this._find(item, 'finished').style.display = "inline-block";
  1886. qq(item).addClass(this._classes.successIcon);
  1887. }
  1888. } else {
  1889. qq(item).addClass(this._classes.fail);
  1890. if (this._classes.failIcon) {
  1891. this._find(item, 'finished').style.display = "inline-block";
  1892. qq(item).addClass(this._classes.failIcon);
  1893. }
  1894. if (this._options.retry.showButton && !this._preventRetries[id]) {
  1895. qq(item).addClass(this._classes.retryable);
  1896. }
  1897. this._controlFailureTextDisplay(item, result);
  1898. }
  1899. },
  1900. _onUpload: function(id, name){
  1901. qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
  1902. this._showSpinner(id);
  1903. },
  1904. _onCancel: function(id, name) {
  1905. qq.FineUploaderBasic.prototype._onCancel.apply(this, arguments);
  1906. this._removeFileItem(id);
  1907. },
  1908. _onBeforeAutoRetry: function(id) {
  1909. var item, progressBar, failTextEl, retryNumForDisplay, maxAuto, retryNote;
  1910. qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
  1911. item = this.getItemByFileId(id);
  1912. progressBar = this._find(item, 'progressBar');
  1913. this._showCancelLink(item);
  1914. progressBar.style.width = 0;
  1915. qq(progressBar).hide();
  1916. if (this._options.retry.showAutoRetryNote) {
  1917. failTextEl = this._find(item, 'statusText');
  1918. retryNumForDisplay = this._autoRetries[id] + 1;
  1919. maxAuto = this._options.retry.maxAutoAttempts;
  1920. retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
  1921. retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
  1922. qq(failTextEl).setText(retryNote);
  1923. if (retryNumForDisplay === 1) {
  1924. qq(item).addClass(this._classes.retrying);
  1925. }
  1926. }
  1927. },
  1928. //return false if we should not attempt the requested retry
  1929. _onBeforeManualRetry: function(id) {
  1930. if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
  1931. var item = this.getItemByFileId(id);
  1932. this._find(item, 'progressBar').style.width = 0;
  1933. qq(item).removeClass(this._classes.fail);
  1934. qq(this._find(item, 'statusText')).clearText();
  1935. this._showSpinner(id);
  1936. this._showCancelLink(item);
  1937. return true;
  1938. }
  1939. return false;
  1940. },
  1941. _onSubmitDelete: function(id) {
  1942. if (this._isDeletePossible()) {
  1943. if (this._options.callbacks.onSubmitDelete(id) !== false) {
  1944. if (this._options.deleteFile.forceConfirm) {
  1945. this._showDeleteConfirm(id);
  1946. }
  1947. else {
  1948. this._sendDeleteRequest(id);
  1949. }
  1950. }
  1951. }
  1952. else {
  1953. this.log("Delete request ignored for file ID " + id + ", delete feature is disabled.", "warn");
  1954. return false;
  1955. }
  1956. },
  1957. _onDeleteComplete: function(id, xhr, isError) {
  1958. qq.FineUploaderBasic.prototype._onDeleteComplete.apply(this, arguments);
  1959. var item = this.getItemByFileId(id),
  1960. spinnerEl = this._find(item, 'spinner'),
  1961. statusTextEl = this._find(item, 'statusText');
  1962. qq(spinnerEl).hide();
  1963. if (isError) {
  1964. qq(statusTextEl).setText(this._options.deleteFile.deletingFailedText);
  1965. this._showDeleteLink(id);
  1966. }
  1967. else {
  1968. this._removeFileItem(id);
  1969. }
  1970. },
  1971. _sendDeleteRequest: function(id) {
  1972. var item = this.getItemByFileId(id),
  1973. deleteLink = this._find(item, 'deleteButton'),
  1974. statusTextEl = this._find(item, 'statusText');
  1975. qq(deleteLink).hide();
  1976. this._showSpinner(id);
  1977. qq(statusTextEl).setText(this._options.deleteFile.deletingStatusText);
  1978. this._deleteHandler.sendDelete(id, this.getUuid(id));
  1979. },
  1980. _showDeleteConfirm: function(id) {
  1981. var fileName = this._handler.getName(id),
  1982. confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName),
  1983. uuid = this.getUuid(id),
  1984. self = this;
  1985. this._options.showConfirm(confirmMessage, function() {
  1986. self._sendDeleteRequest(id);
  1987. });
  1988. },
  1989. _addToList: function(id, name){
  1990. var item = qq.toElement(this._options.fileTemplate);
  1991. if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) {
  1992. var cancelLink = this._find(item, 'cancel');
  1993. qq(cancelLink).remove();
  1994. }
  1995. item.qqFileId = id;
  1996. var fileElement = this._find(item, 'file');
  1997. qq(fileElement).setText(this._options.formatFileName(name));
  1998. qq(this._find(item, 'size')).hide();
  1999. if (!this._options.multiple) {
  2000. this._handler.cancelAll();
  2001. this._clearList();
  2002. }
  2003. this._listElement.appendChild(item);
  2004. if (this._options.display.fileSizeOnSubmit && qq.isXhrUploadSupported()) {
  2005. this._displayFileSize(id);
  2006. }
  2007. },
  2008. _clearList: function(){
  2009. this._listElement.innerHTML = '';
  2010. this.clearStoredFiles();
  2011. },
  2012. _displayFileSize: function(id, loadedSize, totalSize) {
  2013. var item = this.getItemByFileId(id),
  2014. size = this.getSize(id),
  2015. sizeForDisplay = this._formatSize(size),
  2016. sizeEl = this._find(item, 'size');
  2017. if (loadedSize !== undefined && totalSize !== undefined) {
  2018. sizeForDisplay = this._formatProgress(loadedSize, totalSize);
  2019. }
  2020. qq(sizeEl).css({display: 'inline'});
  2021. qq(sizeEl).setText(sizeForDisplay);
  2022. },
  2023. /**
  2024. * delegate click event for cancel & retry links
  2025. **/
  2026. _bindCancelAndRetryEvents: function(){
  2027. var self = this,
  2028. list = this._listElement;
  2029. this._disposeSupport.attach(list, 'click', function(e){
  2030. e = e || window.event;
  2031. var target = e.target || e.srcElement;
  2032. if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry) || qq(target).hasClass(self._classes.deleteButton)){
  2033. qq.preventDefault(e);
  2034. var item = target.parentNode;
  2035. while(item.qqFileId === undefined) {
  2036. item = target = target.parentNode;
  2037. }
  2038. if (qq(target).hasClass(self._classes.deleteButton)) {
  2039. self.deleteFile(item.qqFileId);
  2040. }
  2041. else if (qq(target).hasClass(self._classes.cancel)) {
  2042. self.cancel(item.qqFileId);
  2043. }
  2044. else {
  2045. qq(item).removeClass(self._classes.retryable);
  2046. self.retry(item.qqFileId);
  2047. }
  2048. }
  2049. });
  2050. },
  2051. _formatProgress: function (uploadedSize, totalSize) {
  2052. var message = this._options.text.formatProgress;
  2053. function r(name, replacement) { message = message.replace(name, replacement); }
  2054. r('{percent}', Math.round(uploadedSize / totalSize * 100));
  2055. r('{total_size}', this._formatSize(totalSize));
  2056. return message;
  2057. },
  2058. _controlFailureTextDisplay: function(item, response) {
  2059. var mode, maxChars, responseProperty, failureReason, shortFailureReason;
  2060. mode = this._options.failedUploadTextDisplay.mode;
  2061. maxChars = this._options.failedUploadTextDisplay.maxChars;
  2062. responseProperty = this._options.failedUploadTextDisplay.responseProperty;
  2063. if (mode === 'custom') {
  2064. failureReason = response[responseProperty];
  2065. if (failureReason) {
  2066. if (failureReason.length > maxChars) {
  2067. shortFailureReason = failureReason.substring(0, maxChars) + '...';
  2068. }
  2069. }
  2070. else {
  2071. failureReason = this._options.text.failUpload;
  2072. this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn');
  2073. }
  2074. qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason);
  2075. if (this._options.failedUploadTextDisplay.enableTooltip) {
  2076. this._showTooltip(item, failureReason);
  2077. }
  2078. }
  2079. else if (mode === 'default') {
  2080. qq(this._find(item, 'statusText')).setText(this._options.text.failUpload);
  2081. }
  2082. else if (mode !== 'none') {
  2083. this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
  2084. }
  2085. },
  2086. _showTooltip: function(item, text) {
  2087. item.title = text;
  2088. },
  2089. _showSpinner: function(id) {
  2090. var item = this.getItemByFileId(id),
  2091. spinnerEl = this._find(item, 'spinner');
  2092. spinnerEl.style.display = "inline-block";
  2093. },
  2094. _showCancelLink: function(item) {
  2095. if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) {
  2096. var cancelLink = this._find(item, 'cancel');
  2097. qq(cancelLink).css({display: 'inline'});
  2098. }
  2099. },
  2100. _showDeleteLink: function(id) {
  2101. var item = this.getItemByFileId(id),
  2102. deleteLink = this._find(item, 'deleteButton');
  2103. qq(deleteLink).css({display: 'inline'});
  2104. },
  2105. _error: function(code, name){
  2106. var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
  2107. this._options.showMessage(message);
  2108. }
  2109. });
  2110. /** Generic class for sending non-upload ajax requests and handling the associated responses **/
  2111. //TODO Use XDomainRequest if expectCors = true. Not necessary now since only DELETE requests are sent and XDR doesn't support pre-flighting.
  2112. /*globals qq, XMLHttpRequest*/
  2113. qq.AjaxRequestor = function(o) {
  2114. "use strict";
  2115. var log, shouldParamsBeInQueryString,
  2116. queue = [],
  2117. requestState = [],
  2118. options = {
  2119. method: 'POST',
  2120. maxConnections: 3,
  2121. customHeaders: {},
  2122. endpointStore: {},
  2123. paramsStore: {},
  2124. successfulResponseCodes: [200],
  2125. demoMode: false,
  2126. cors: {
  2127. expected: false,
  2128. sendCredentials: false
  2129. },
  2130. log: function(str, level) {},
  2131. onSend: function(id) {},
  2132. onComplete: function(id, xhr, isError) {},
  2133. onCancel: function(id) {}
  2134. };
  2135. qq.extend(options, o);
  2136. log = options.log;
  2137. shouldParamsBeInQueryString = getMethod() === 'GET' || getMethod() === 'DELETE';
  2138. /**
  2139. * Removes element from queue, sends next request
  2140. */
  2141. function dequeue(id) {
  2142. var i = qq.indexOf(queue, id),
  2143. max = options.maxConnections,
  2144. nextId;
  2145. delete requestState[id];
  2146. queue.splice(i, 1);
  2147. if (queue.length >= max && i < max){
  2148. nextId = queue[max-1];
  2149. sendRequest(nextId);
  2150. }
  2151. }
  2152. function onComplete(id) {
  2153. var xhr = requestState[id].xhr,
  2154. method = getMethod(),
  2155. isError = false;
  2156. dequeue(id);
  2157. if (!isResponseSuccessful(xhr.status)) {
  2158. isError = true;
  2159. log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
  2160. }
  2161. options.onComplete(id, xhr, isError);
  2162. }
  2163. function sendRequest(id) {
  2164. var xhr = new XMLHttpRequest(),
  2165. method = getMethod(),
  2166. params = {},
  2167. url;
  2168. options.onSend(id);
  2169. if (options.paramsStore.getParams) {
  2170. params = options.paramsStore.getParams(id);
  2171. }
  2172. url = createUrl(id, params);
  2173. requestState[id].xhr = xhr;
  2174. xhr.onreadystatechange = getReadyStateChangeHandler(id);
  2175. xhr.open(method, url, true);
  2176. if (options.cors.expected && options.cors.sendCredentials) {
  2177. xhr.withCredentials = true;
  2178. }
  2179. setHeaders(id);
  2180. log('Sending ' + method + " request for " + id);
  2181. if (!shouldParamsBeInQueryString && params) {
  2182. xhr.send(qq.obj2url(params, ""));
  2183. }
  2184. else {
  2185. xhr.send();
  2186. }
  2187. }
  2188. function createUrl(id, params) {
  2189. var endpoint = options.endpointStore.getEndpoint(id),
  2190. addToPath = requestState[id].addToPath;
  2191. if (addToPath !== undefined) {
  2192. endpoint += "/" + addToPath;
  2193. }
  2194. if (shouldParamsBeInQueryString && params) {
  2195. return qq.obj2url(params, endpoint);
  2196. }
  2197. else {
  2198. return endpoint;
  2199. }
  2200. }
  2201. function getReadyStateChangeHandler(id) {
  2202. var xhr = requestState[id].xhr;
  2203. return function() {
  2204. if (xhr.readyState === 4) {
  2205. onComplete(id, xhr);
  2206. }
  2207. };
  2208. }
  2209. function setHeaders(id) {
  2210. var xhr = requestState[id].xhr,
  2211. customHeaders = options.customHeaders;
  2212. xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
  2213. xhr.setRequestHeader("Cache-Control", "no-cache");
  2214. qq.each(customHeaders, function(name, val) {
  2215. xhr.setRequestHeader(name, val);
  2216. });
  2217. }
  2218. function cancelRequest(id) {
  2219. var xhr = requestState[id].xhr,
  2220. method = getMethod();
  2221. if (xhr) {
  2222. xhr.onreadystatechange = null;
  2223. xhr.abort();
  2224. dequeue(id);
  2225. log('Cancelled ' + method + " for " + id);
  2226. options.onCancel(id);
  2227. return true;
  2228. }
  2229. return false;
  2230. }
  2231. function isResponseSuccessful(responseCode) {
  2232. return qq.indexOf(options.successfulResponseCodes, responseCode) >= 0;
  2233. }
  2234. function getMethod() {
  2235. if (options.demoMode) {
  2236. return "GET";
  2237. }
  2238. return options.method;
  2239. }
  2240. return {
  2241. send: function(id, addToPath) {
  2242. requestState[id] = {
  2243. addToPath: addToPath
  2244. };
  2245. var len = queue.push(id);
  2246. // if too many active connections, wait...
  2247. if (len <= options.maxConnections){
  2248. sendRequest(id);
  2249. }
  2250. },
  2251. cancel: function(id) {
  2252. return cancelRequest(id);
  2253. }
  2254. };
  2255. };
  2256. /** Generic class for sending non-upload ajax requests and handling the associated responses **/
  2257. /*globals qq, XMLHttpRequest*/
  2258. qq.DeleteFileAjaxRequestor = function(o) {
  2259. "use strict";
  2260. var requestor,
  2261. options = {
  2262. endpointStore: {},
  2263. maxConnections: 3,
  2264. customHeaders: {},
  2265. paramsStore: {},
  2266. demoMode: false,
  2267. cors: {
  2268. expected: false,
  2269. sendCredentials: false
  2270. },
  2271. log: function(str, level) {},
  2272. onDelete: function(id) {},
  2273. onDeleteComplete: function(id, xhr, isError) {}
  2274. };
  2275. qq.extend(options, o);
  2276. requestor = new qq.AjaxRequestor({
  2277. method: 'DELETE',
  2278. endpointStore: options.endpointStore,
  2279. paramsStore: options.paramsStore,
  2280. maxConnections: options.maxConnections,
  2281. customHeaders: options.customHeaders,
  2282. successfulResponseCodes: [200, 202, 204],
  2283. demoMode: options.demoMode,
  2284. log: options.log,
  2285. onSend: options.onDelete,
  2286. onComplete: options.onDeleteComplete
  2287. });
  2288. return {
  2289. sendDelete: function(id, uuid) {
  2290. requestor.send(id, uuid);
  2291. options.log("Submitted delete file request for " + id);
  2292. }
  2293. };
  2294. };
  2295. qq.WindowReceiveMessage = function(o) {
  2296. var options = {
  2297. log: function(message, level) {}
  2298. },
  2299. callbackWrapperDetachers = {};
  2300. qq.extend(options, o);
  2301. return {
  2302. receiveMessage : function(id, callback) {
  2303. var onMessageCallbackWrapper = function(event) {
  2304. callback(event.data);
  2305. };
  2306. if (window.postMessage) {
  2307. callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper);
  2308. }
  2309. else {
  2310. log("iframe message passing not supported in this browser!", "error");
  2311. }
  2312. },
  2313. stopReceivingMessages : function(id) {
  2314. if (window.postMessage) {
  2315. var detacher = callbackWrapperDetachers[id];
  2316. if (detacher) {
  2317. detacher();
  2318. }
  2319. }
  2320. }
  2321. };
  2322. };
  2323. /**
  2324. * Class for uploading files, uploading itself is handled by child classes
  2325. */
  2326. /*globals qq*/
  2327. qq.UploadHandler = function(o) {
  2328. "use strict";
  2329. var queue = [],
  2330. options, log, dequeue, handlerImpl;
  2331. // Default options, can be overridden by the user
  2332. options = {
  2333. debug: false,
  2334. forceMultipart: true,
  2335. paramsInBody: false,
  2336. paramsStore: {},
  2337. endpointStore: {},
  2338. cors: {
  2339. expected: false,
  2340. sendCredentials: false
  2341. },
  2342. maxConnections: 3, // maximum number of concurrent uploads
  2343. uuidParamName: 'qquuid',
  2344. totalFileSizeParamName: 'qqtotalfilesize',
  2345. chunking: {
  2346. enabled: false,
  2347. partSize: 2000000, //bytes
  2348. paramNames: {
  2349. partIndex: 'qqpartindex',
  2350. partByteOffset: 'qqpartbyteoffset',
  2351. chunkSize: 'qqchunksize',
  2352. totalParts: 'qqtotalparts',
  2353. filename: 'qqfilename'
  2354. }
  2355. },
  2356. resume: {
  2357. enabled: false,
  2358. id: null,
  2359. cookiesExpireIn: 7, //days
  2360. paramNames: {
  2361. resuming: "qqresume"
  2362. }
  2363. },
  2364. blobs: {
  2365. paramNames: {
  2366. name: 'qqblobname'
  2367. }
  2368. },
  2369. log: function(str, level) {},
  2370. onProgress: function(id, fileName, loaded, total){},
  2371. onComplete: function(id, fileName, response, xhr){},
  2372. onCancel: function(id, fileName){},
  2373. onUpload: function(id, fileName){},
  2374. onUploadChunk: function(id, fileName, chunkData){},
  2375. onAutoRetry: function(id, fileName, response, xhr){},
  2376. onResume: function(id, fileName, chunkData){}
  2377. };
  2378. qq.extend(options, o);
  2379. log = options.log;
  2380. /**
  2381. * Removes element from queue, starts upload of next
  2382. */
  2383. dequeue = function(id) {
  2384. var i = qq.indexOf(queue, id),
  2385. max = options.maxConnections,
  2386. nextId;
  2387. if (i >= 0) {
  2388. queue.splice(i, 1);
  2389. if (queue.length >= max && i < max){
  2390. nextId = queue[max-1];
  2391. handlerImpl.upload(nextId);
  2392. }
  2393. }
  2394. };
  2395. if (qq.isXhrUploadSupported()) {
  2396. handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log);
  2397. }
  2398. else {
  2399. handlerImpl = new qq.UploadHandlerForm(options, dequeue, log);
  2400. }
  2401. return {
  2402. /**
  2403. * Adds file or file input to the queue
  2404. * @returns id
  2405. **/
  2406. add: function(file){
  2407. return handlerImpl.add(file);
  2408. },
  2409. /**
  2410. * Sends the file identified by id
  2411. */
  2412. upload: function(id){
  2413. var len = queue.push(id);
  2414. // if too many active uploads, wait...
  2415. if (len <= options.maxConnections){
  2416. return handlerImpl.upload(id);
  2417. }
  2418. },
  2419. retry: function(id) {
  2420. var i = qq.indexOf(queue, id);
  2421. if (i >= 0) {
  2422. return handlerImpl.upload(id, true);
  2423. }
  2424. else {
  2425. return this.upload(id);
  2426. }
  2427. },
  2428. /**
  2429. * Cancels file upload by id
  2430. */
  2431. cancel: function(id) {
  2432. log('Cancelling ' + id);
  2433. options.paramsStore.remove(id);
  2434. handlerImpl.cancel(id);
  2435. dequeue(id);
  2436. },
  2437. /**
  2438. * Cancels all queued or in-progress uploads
  2439. */
  2440. cancelAll: function() {
  2441. var self = this,
  2442. queueCopy = [];
  2443. qq.extend(queueCopy, queue);
  2444. qq.each(queueCopy, function(idx, fileId) {
  2445. self.cancel(fileId);
  2446. });
  2447. queue = [];
  2448. },
  2449. /**
  2450. * Returns name of the file identified by id
  2451. */
  2452. getName: function(id){
  2453. return handlerImpl.getName(id);
  2454. },
  2455. /**
  2456. * Returns size of the file identified by id
  2457. */
  2458. getSize: function(id){
  2459. if (handlerImpl.getSize) {
  2460. return handlerImpl.getSize(id);
  2461. }
  2462. },
  2463. getFile: function(id) {
  2464. if (handlerImpl.getFile) {
  2465. return handlerImpl.getFile(id);
  2466. }
  2467. },
  2468. /**
  2469. * Returns id of files being uploaded or
  2470. * waiting for their turn
  2471. */
  2472. getQueue: function(){
  2473. return queue;
  2474. },
  2475. reset: function() {
  2476. log('Resetting upload handler');
  2477. queue = [];
  2478. handlerImpl.reset();
  2479. },
  2480. getUuid: function(id) {
  2481. return handlerImpl.getUuid(id);
  2482. },
  2483. /**
  2484. * Determine if the file exists.
  2485. */
  2486. isValid: function(id) {
  2487. return handlerImpl.isValid(id);
  2488. },
  2489. getResumableFilesData: function() {
  2490. if (handlerImpl.getResumableFilesData) {
  2491. return handlerImpl.getResumableFilesData();
  2492. }
  2493. return [];
  2494. }
  2495. };
  2496. };
  2497. /*globals qq, document, setTimeout*/
  2498. /*globals clearTimeout*/
  2499. qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) {
  2500. "use strict";
  2501. var options = o,
  2502. inputs = [],
  2503. uuids = [],
  2504. detachLoadEvents = {},
  2505. postMessageCallbackTimers = {},
  2506. uploadComplete = uploadCompleteCallback,
  2507. log = logCallback,
  2508. corsMessageReceiver = new qq.WindowReceiveMessage({log: log}),
  2509. onloadCallbacks = {},
  2510. api;
  2511. function detachLoadEvent(id) {
  2512. if (detachLoadEvents[id] !== undefined) {
  2513. detachLoadEvents[id]();
  2514. delete detachLoadEvents[id];
  2515. }
  2516. }
  2517. function registerPostMessageCallback(iframe, callback) {
  2518. var id = iframe.id;
  2519. onloadCallbacks[uuids[id]] = callback;
  2520. detachLoadEvents[id] = qq(iframe).attach('load', function() {
  2521. if (inputs[id]) {
  2522. log("Received iframe load event for CORS upload request (file id " + id + ")");
  2523. postMessageCallbackTimers[id] = setTimeout(function() {
  2524. var errorMessage = "No valid message received from loaded iframe for file id " + id;
  2525. log(errorMessage, "error");
  2526. callback({
  2527. error: errorMessage
  2528. });
  2529. }, 1000);
  2530. }
  2531. });
  2532. corsMessageReceiver.receiveMessage(id, function(message) {
  2533. log("Received the following window message: '" + message + "'");
  2534. var response = qq.parseJson(message),
  2535. uuid = response.uuid,
  2536. onloadCallback;
  2537. if (uuid && onloadCallbacks[uuid]) {
  2538. clearTimeout(postMessageCallbackTimers[id]);
  2539. delete postMessageCallbackTimers[id];
  2540. detachLoadEvent(id);
  2541. onloadCallback = onloadCallbacks[uuid];
  2542. delete onloadCallbacks[uuid];
  2543. corsMessageReceiver.stopReceivingMessages(id);
  2544. onloadCallback(response);
  2545. }
  2546. else if (!uuid) {
  2547. log("'" + message + "' does not contain a UUID - ignoring.");
  2548. }
  2549. });
  2550. }
  2551. function attachLoadEvent(iframe, callback) {
  2552. /*jslint eqeq: true*/
  2553. if (options.cors.expected) {
  2554. registerPostMessageCallback(iframe, callback);
  2555. }
  2556. else {
  2557. detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){
  2558. log('Received response for ' + iframe.id);
  2559. // when we remove iframe from dom
  2560. // the request stops, but in IE load
  2561. // event fires
  2562. if (!iframe.parentNode){
  2563. return;
  2564. }
  2565. try {
  2566. // fixing Opera 10.53
  2567. if (iframe.contentDocument &&
  2568. iframe.contentDocument.body &&
  2569. iframe.contentDocument.body.innerHTML == "false"){
  2570. // In Opera event is fired second time
  2571. // when body.innerHTML changed from false
  2572. // to server response approx. after 1 sec
  2573. // when we upload file with iframe
  2574. return;
  2575. }
  2576. }
  2577. catch (error) {
  2578. //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
  2579. log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error');
  2580. }
  2581. callback();
  2582. });
  2583. }
  2584. }
  2585. /**
  2586. * Returns json object received by iframe from server.
  2587. */
  2588. function getIframeContentJson(iframe) {
  2589. /*jshint evil: true*/
  2590. var response;
  2591. //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
  2592. try {
  2593. // iframe.contentWindow.document - for IE<7
  2594. var doc = iframe.contentDocument || iframe.contentWindow.document,
  2595. innerHTML = doc.body.innerHTML;
  2596. log("converting iframe's innerHTML to JSON");
  2597. log("innerHTML = " + innerHTML);
  2598. //plain text response may be wrapped in <pre> tag
  2599. if (innerHTML && innerHTML.match(/^<pre/i)) {
  2600. innerHTML = doc.body.firstChild.firstChild.nodeValue;
  2601. }
  2602. response = qq.parseJson(innerHTML);
  2603. } catch(error){
  2604. log('Error when attempting to parse form upload response (' + error + ")", 'error');
  2605. response = {success: false};
  2606. }
  2607. return response;
  2608. }
  2609. /**
  2610. * Creates iframe with unique name
  2611. */
  2612. function createIframe(id){
  2613. // We can't use following code as the name attribute
  2614. // won't be properly registered in IE6, and new window
  2615. // on form submit will open
  2616. // var iframe = document.createElement('iframe');
  2617. // iframe.setAttribute('name', id);
  2618. var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
  2619. iframe.setAttribute('id', id);
  2620. iframe.style.display = 'none';
  2621. document.body.appendChild(iframe);
  2622. return iframe;
  2623. }
  2624. /**
  2625. * Creates form, that will be submitted to iframe
  2626. */
  2627. function createForm(id, iframe){
  2628. var params = options.paramsStore.getParams(id),
  2629. protocol = options.demoMode ? "GET" : "POST",
  2630. form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'),
  2631. endpoint = options.endpointStore.getEndpoint(id),
  2632. url = endpoint;
  2633. params[options.uuidParamName] = uuids[id];
  2634. if (!options.paramsInBody) {
  2635. url = qq.obj2url(params, endpoint);
  2636. }
  2637. else {
  2638. qq.obj2Inputs(params, form);
  2639. }
  2640. form.setAttribute('action', url);
  2641. form.setAttribute('target', iframe.name);
  2642. form.style.display = 'none';
  2643. document.body.appendChild(form);
  2644. return form;
  2645. }
  2646. api = {
  2647. add: function(fileInput) {
  2648. fileInput.setAttribute('name', options.inputName);
  2649. var id = inputs.push(fileInput) - 1;
  2650. uuids[id] = qq.getUniqueId();
  2651. // remove file input from DOM
  2652. if (fileInput.parentNode){
  2653. qq(fileInput).remove();
  2654. }
  2655. return id;
  2656. },
  2657. getName: function(id) {
  2658. /*jslint regexp: true*/
  2659. // get input value and remove path to normalize
  2660. return inputs[id].value.replace(/.*(\/|\\)/, "");
  2661. },
  2662. isValid: function(id) {
  2663. return inputs[id] !== undefined;
  2664. },
  2665. reset: function() {
  2666. qq.UploadHandler.prototype.reset.apply(this, arguments);
  2667. inputs = [];
  2668. uuids = [];
  2669. detachLoadEvents = {};
  2670. },
  2671. getUuid: function(id) {
  2672. return uuids[id];
  2673. },
  2674. cancel: function(id) {
  2675. options.onCancel(id, this.getName(id));
  2676. delete inputs[id];
  2677. delete uuids[id];
  2678. delete detachLoadEvents[id];
  2679. if (options.cors.expected) {
  2680. clearTimeout(postMessageCallbackTimers[id]);
  2681. delete postMessageCallbackTimers[id];
  2682. corsMessageReceiver.stopReceivingMessages(id);
  2683. }
  2684. var iframe = document.getElementById(id);
  2685. if (iframe) {
  2686. // to cancel request set src to something else
  2687. // we use src="javascript:false;" because it doesn't
  2688. // trigger ie6 prompt on https
  2689. iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
  2690. qq(iframe).remove();
  2691. }
  2692. },
  2693. upload: function(id){
  2694. var input = inputs[id],
  2695. fileName = api.getName(id),
  2696. iframe = createIframe(id),
  2697. form;
  2698. if (!input){
  2699. throw new Error('file with passed id was not added, or already uploaded or cancelled');
  2700. }
  2701. options.onUpload(id, this.getName(id));
  2702. form = createForm(id, iframe);
  2703. form.appendChild(input);
  2704. attachLoadEvent(iframe, function(responseFromMessage){
  2705. log('iframe loaded');
  2706. var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe);
  2707. detachLoadEvent(id);
  2708. //we can't remove an iframe if the iframe doesn't belong to the same domain
  2709. if (!options.cors.expected) {
  2710. qq(iframe).remove();
  2711. }
  2712. if (!response.success) {
  2713. if (options.onAutoRetry(id, fileName, response)) {
  2714. return;
  2715. }
  2716. }
  2717. options.onComplete(id, fileName, response);
  2718. uploadComplete(id);
  2719. });
  2720. log('Sending upload request for ' + id);
  2721. form.submit();
  2722. qq(form).remove();
  2723. return id;
  2724. }
  2725. };
  2726. return api;
  2727. };
  2728. /*globals qq, File, XMLHttpRequest, FormData, Blob*/
  2729. qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
  2730. "use strict";
  2731. var options = o,
  2732. uploadComplete = uploadCompleteCallback,
  2733. log = logCallback,
  2734. fileState = [],
  2735. cookieItemDelimiter = "|",
  2736. chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
  2737. resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
  2738. resumeId = getResumeId(),
  2739. multipart = options.forceMultipart || options.paramsInBody,
  2740. api;
  2741. function addChunkingSpecificParams(id, params, chunkData) {
  2742. var size = api.getSize(id),
  2743. name = api.getName(id);
  2744. params[options.chunking.paramNames.partIndex] = chunkData.part;
  2745. params[options.chunking.paramNames.partByteOffset] = chunkData.start;
  2746. params[options.chunking.paramNames.chunkSize] = chunkData.size;
  2747. params[options.chunking.paramNames.totalParts] = chunkData.count;
  2748. params[options.totalFileSizeParamName] = size;
  2749. /**
  2750. * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
  2751. * or an empty string. So, we will need to include the actual file name as a param in this case.
  2752. */
  2753. if (multipart) {
  2754. params[options.chunking.paramNames.filename] = name;
  2755. }
  2756. }
  2757. function addResumeSpecificParams(params) {
  2758. params[options.resume.paramNames.resuming] = true;
  2759. }
  2760. function getChunk(fileOrBlob, startByte, endByte) {
  2761. if (fileOrBlob.slice) {
  2762. return fileOrBlob.slice(startByte, endByte);
  2763. }
  2764. else if (fileOrBlob.mozSlice) {
  2765. return fileOrBlob.mozSlice(startByte, endByte);
  2766. }
  2767. else if (fileOrBlob.webkitSlice) {
  2768. return fileOrBlob.webkitSlice(startByte, endByte);
  2769. }
  2770. }
  2771. function getChunkData(id, chunkIndex) {
  2772. var chunkSize = options.chunking.partSize,
  2773. fileSize = api.getSize(id),
  2774. fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
  2775. startBytes = chunkSize * chunkIndex,
  2776. endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
  2777. totalChunks = getTotalChunks(id);
  2778. return {
  2779. part: chunkIndex,
  2780. start: startBytes,
  2781. end: endBytes,
  2782. count: totalChunks,
  2783. blob: getChunk(fileOrBlob, startBytes, endBytes),
  2784. size: endBytes - startBytes
  2785. };
  2786. }
  2787. function getTotalChunks(id) {
  2788. var fileSize = api.getSize(id),
  2789. chunkSize = options.chunking.partSize;
  2790. return Math.ceil(fileSize / chunkSize);
  2791. }
  2792. function createXhr(id) {
  2793. var xhr = new XMLHttpRequest();
  2794. fileState[id].xhr = xhr;
  2795. return xhr;
  2796. }
  2797. function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
  2798. var formData = new FormData(),
  2799. method = options.demoMode ? "GET" : "POST",
  2800. endpoint = options.endpointStore.getEndpoint(id),
  2801. url = endpoint,
  2802. name = api.getName(id),
  2803. size = api.getSize(id),
  2804. blobData = fileState[id].blobData;
  2805. params[options.uuidParamName] = fileState[id].uuid;
  2806. if (multipart) {
  2807. params[options.totalFileSizeParamName] = size;
  2808. if (blobData) {
  2809. /**
  2810. * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
  2811. * or an empty string. So, we will need to include the actual file name as a param in this case.
  2812. */
  2813. params[options.blobs.paramNames.name] = blobData.name;
  2814. }
  2815. }
  2816. //build query string
  2817. if (!options.paramsInBody) {
  2818. if (!multipart) {
  2819. params[options.inputName] = name;
  2820. }
  2821. url = qq.obj2url(params, endpoint);
  2822. }
  2823. xhr.open(method, url, true);
  2824. if (options.cors.expected && options.cors.sendCredentials) {
  2825. xhr.withCredentials = true;
  2826. }
  2827. if (multipart) {
  2828. if (options.paramsInBody) {
  2829. qq.obj2FormData(params, formData);
  2830. }
  2831. formData.append(options.inputName, fileOrBlob);
  2832. return formData;
  2833. }
  2834. return fileOrBlob;
  2835. }
  2836. function setHeaders(id, xhr) {
  2837. var extraHeaders = options.customHeaders,
  2838. fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
  2839. xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
  2840. xhr.setRequestHeader("Cache-Control", "no-cache");
  2841. if (!multipart) {
  2842. xhr.setRequestHeader("Content-Type", "application/octet-stream");
  2843. //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
  2844. xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
  2845. }
  2846. qq.each(extraHeaders, function(name, val) {
  2847. xhr.setRequestHeader(name, val);
  2848. });
  2849. }
  2850. function handleCompletedItem(id, response, xhr) {
  2851. var name = api.getName(id),
  2852. size = api.getSize(id);
  2853. fileState[id].attemptingResume = false;
  2854. options.onProgress(id, name, size, size);
  2855. options.onComplete(id, name, response, xhr);
  2856. delete fileState[id].xhr;
  2857. uploadComplete(id);
  2858. }
  2859. function uploadNextChunk(id) {
  2860. var chunkIdx = fileState[id].remainingChunkIdxs[0],
  2861. chunkData = getChunkData(id, chunkIdx),
  2862. xhr = createXhr(id),
  2863. size = api.getSize(id),
  2864. name = api.getName(id),
  2865. toSend, params;
  2866. if (fileState[id].loaded === undefined) {
  2867. fileState[id].loaded = 0;
  2868. }
  2869. if (resumeEnabled && fileState[id].file) {
  2870. persistChunkData(id, chunkData);
  2871. }
  2872. xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
  2873. xhr.upload.onprogress = function(e) {
  2874. if (e.lengthComputable) {
  2875. var totalLoaded = e.loaded + fileState[id].loaded,
  2876. estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
  2877. options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
  2878. }
  2879. };
  2880. options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
  2881. params = options.paramsStore.getParams(id);
  2882. addChunkingSpecificParams(id, params, chunkData);
  2883. if (fileState[id].attemptingResume) {
  2884. addResumeSpecificParams(params);
  2885. }
  2886. toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
  2887. setHeaders(id, xhr);
  2888. log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
  2889. xhr.send(toSend);
  2890. }
  2891. function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
  2892. var chunkData = getChunkData(id, chunkIdx),
  2893. blobSize = chunkData.size,
  2894. overhead = requestSize - blobSize,
  2895. size = api.getSize(id),
  2896. chunkCount = chunkData.count,
  2897. initialRequestOverhead = fileState[id].initialRequestOverhead,
  2898. overheadDiff = overhead - initialRequestOverhead;
  2899. fileState[id].lastRequestOverhead = overhead;
  2900. if (chunkIdx === 0) {
  2901. fileState[id].lastChunkIdxProgress = 0;
  2902. fileState[id].initialRequestOverhead = overhead;
  2903. fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
  2904. }
  2905. else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
  2906. fileState[id].lastChunkIdxProgress = chunkIdx;
  2907. fileState[id].estTotalRequestsSize += overheadDiff;
  2908. }
  2909. return fileState[id].estTotalRequestsSize;
  2910. }
  2911. function getLastRequestOverhead(id) {
  2912. if (multipart) {
  2913. return fileState[id].lastRequestOverhead;
  2914. }
  2915. else {
  2916. return 0;
  2917. }
  2918. }
  2919. function handleSuccessfullyCompletedChunk(id, response, xhr) {
  2920. var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
  2921. chunkData = getChunkData(id, chunkIdx);
  2922. fileState[id].attemptingResume = false;
  2923. fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
  2924. if (fileState[id].remainingChunkIdxs.length > 0) {
  2925. uploadNextChunk(id);
  2926. }
  2927. else {
  2928. if (resumeEnabled) {
  2929. deletePersistedChunkData(id);
  2930. }
  2931. handleCompletedItem(id, response, xhr);
  2932. }
  2933. }
  2934. function isErrorResponse(xhr, response) {
  2935. return xhr.status !== 200 || !response.success || response.reset;
  2936. }
  2937. function parseResponse(xhr) {
  2938. var response;
  2939. try {
  2940. response = qq.parseJson(xhr.responseText);
  2941. }
  2942. catch(error) {
  2943. log('Error when attempting to parse xhr response text (' + error + ')', 'error');
  2944. response = {};
  2945. }
  2946. return response;
  2947. }
  2948. function handleResetResponse(id) {
  2949. log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
  2950. if (resumeEnabled) {
  2951. deletePersistedChunkData(id);
  2952. fileState[id].attemptingResume = false;
  2953. }
  2954. fileState[id].remainingChunkIdxs = [];
  2955. delete fileState[id].loaded;
  2956. delete fileState[id].estTotalRequestsSize;
  2957. delete fileState[id].initialRequestOverhead;
  2958. }
  2959. function handleResetResponseOnResumeAttempt(id) {
  2960. fileState[id].attemptingResume = false;
  2961. log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
  2962. handleResetResponse(id);
  2963. api.upload(id, true);
  2964. }
  2965. function handleNonResetErrorResponse(id, response, xhr) {
  2966. var name = api.getName(id);
  2967. if (options.onAutoRetry(id, name, response, xhr)) {
  2968. return;
  2969. }
  2970. else {
  2971. handleCompletedItem(id, response, xhr);
  2972. }
  2973. }
  2974. function onComplete(id, xhr) {
  2975. var response;
  2976. // the request was aborted/cancelled
  2977. if (!fileState[id]) {
  2978. return;
  2979. }
  2980. log("xhr - server response received for " + id);
  2981. log("responseText = " + xhr.responseText);
  2982. response = parseResponse(xhr);
  2983. if (isErrorResponse(xhr, response)) {
  2984. if (response.reset) {
  2985. handleResetResponse(id);
  2986. }
  2987. if (fileState[id].attemptingResume && response.reset) {
  2988. handleResetResponseOnResumeAttempt(id);
  2989. }
  2990. else {
  2991. handleNonResetErrorResponse(id, response, xhr);
  2992. }
  2993. }
  2994. else if (chunkFiles) {
  2995. handleSuccessfullyCompletedChunk(id, response, xhr);
  2996. }
  2997. else {
  2998. handleCompletedItem(id, response, xhr);
  2999. }
  3000. }
  3001. function getChunkDataForCallback(chunkData) {
  3002. return {
  3003. partIndex: chunkData.part,
  3004. startByte: chunkData.start + 1,
  3005. endByte: chunkData.end,
  3006. totalParts: chunkData.count
  3007. };
  3008. }
  3009. function getReadyStateChangeHandler(id, xhr) {
  3010. return function() {
  3011. if (xhr.readyState === 4) {
  3012. onComplete(id, xhr);
  3013. }
  3014. };
  3015. }
  3016. function persistChunkData(id, chunkData) {
  3017. var fileUuid = api.getUuid(id),
  3018. lastByteSent = fileState[id].loaded,
  3019. initialRequestOverhead = fileState[id].initialRequestOverhead,
  3020. estTotalRequestsSize = fileState[id].estTotalRequestsSize,
  3021. cookieName = getChunkDataCookieName(id),
  3022. cookieValue = fileUuid +
  3023. cookieItemDelimiter + chunkData.part +
  3024. cookieItemDelimiter + lastByteSent +
  3025. cookieItemDelimiter + initialRequestOverhead +
  3026. cookieItemDelimiter + estTotalRequestsSize,
  3027. cookieExpDays = options.resume.cookiesExpireIn;
  3028. qq.setCookie(cookieName, cookieValue, cookieExpDays);
  3029. }
  3030. function deletePersistedChunkData(id) {
  3031. if (fileState[id].file) {
  3032. var cookieName = getChunkDataCookieName(id);
  3033. qq.deleteCookie(cookieName);
  3034. }
  3035. }
  3036. function getPersistedChunkData(id) {
  3037. var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
  3038. filename = api.getName(id),
  3039. sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
  3040. if (chunkCookieValue) {
  3041. sections = chunkCookieValue.split(cookieItemDelimiter);
  3042. if (sections.length === 5) {
  3043. uuid = sections[0];
  3044. partIndex = parseInt(sections[1], 10);
  3045. lastByteSent = parseInt(sections[2], 10);
  3046. initialRequestOverhead = parseInt(sections[3], 10);
  3047. estTotalRequestsSize = parseInt(sections[4], 10);
  3048. return {
  3049. uuid: uuid,
  3050. part: partIndex,
  3051. lastByteSent: lastByteSent,
  3052. initialRequestOverhead: initialRequestOverhead,
  3053. estTotalRequestsSize: estTotalRequestsSize
  3054. };
  3055. }
  3056. else {
  3057. log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
  3058. }
  3059. }
  3060. }
  3061. function getChunkDataCookieName(id) {
  3062. var filename = api.getName(id),
  3063. fileSize = api.getSize(id),
  3064. maxChunkSize = options.chunking.partSize,
  3065. cookieName;
  3066. cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
  3067. if (resumeId !== undefined) {
  3068. cookieName += cookieItemDelimiter + resumeId;
  3069. }
  3070. return cookieName;
  3071. }
  3072. function getResumeId() {
  3073. if (options.resume.id !== null &&
  3074. options.resume.id !== undefined &&
  3075. !qq.isFunction(options.resume.id) &&
  3076. !qq.isObject(options.resume.id)) {
  3077. return options.resume.id;
  3078. }
  3079. }
  3080. function handleFileChunkingUpload(id, retry) {
  3081. var name = api.getName(id),
  3082. firstChunkIndex = 0,
  3083. persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
  3084. if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
  3085. fileState[id].remainingChunkIdxs = [];
  3086. if (resumeEnabled && !retry && fileState[id].file) {
  3087. persistedChunkInfoForResume = getPersistedChunkData(id);
  3088. if (persistedChunkInfoForResume) {
  3089. firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
  3090. if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
  3091. firstChunkIndex = persistedChunkInfoForResume.part;
  3092. fileState[id].uuid = persistedChunkInfoForResume.uuid;
  3093. fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
  3094. fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
  3095. fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
  3096. fileState[id].attemptingResume = true;
  3097. log('Resuming ' + name + " at partition index " + firstChunkIndex);
  3098. }
  3099. }
  3100. }
  3101. for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
  3102. fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
  3103. }
  3104. }
  3105. uploadNextChunk(id);
  3106. }
  3107. function handleStandardFileUpload(id) {
  3108. var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
  3109. name = api.getName(id),
  3110. xhr, params, toSend;
  3111. fileState[id].loaded = 0;
  3112. xhr = createXhr(id);
  3113. xhr.upload.onprogress = function(e){
  3114. if (e.lengthComputable){
  3115. fileState[id].loaded = e.loaded;
  3116. options.onProgress(id, name, e.loaded, e.total);
  3117. }
  3118. };
  3119. xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
  3120. params = options.paramsStore.getParams(id);
  3121. toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
  3122. setHeaders(id, xhr);
  3123. log('Sending upload request for ' + id);
  3124. xhr.send(toSend);
  3125. }
  3126. api = {
  3127. /**
  3128. * Adds File or Blob to the queue
  3129. * Returns id to use with upload, cancel
  3130. **/
  3131. add: function(fileOrBlobData){
  3132. var id;
  3133. if (fileOrBlobData instanceof File) {
  3134. id = fileState.push({file: fileOrBlobData}) - 1;
  3135. }
  3136. else if (fileOrBlobData.blob instanceof Blob) {
  3137. id = fileState.push({blobData: fileOrBlobData}) - 1;
  3138. }
  3139. else {
  3140. throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
  3141. }
  3142. fileState[id].uuid = qq.getUniqueId();
  3143. return id;
  3144. },
  3145. getName: function(id){
  3146. var file = fileState[id].file,
  3147. blobData = fileState[id].blobData;
  3148. if (file) {
  3149. // fix missing name in Safari 4
  3150. //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
  3151. return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
  3152. }
  3153. else {
  3154. return blobData.name;
  3155. }
  3156. },
  3157. getSize: function(id){
  3158. /*jshint eqnull: true*/
  3159. var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
  3160. if (qq.isFileOrInput(fileOrBlob)) {
  3161. return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
  3162. }
  3163. else {
  3164. return fileOrBlob.size;
  3165. }
  3166. },
  3167. getFile: function(id) {
  3168. if (fileState[id]) {
  3169. return fileState[id].file || fileState[id].blobData.blob;
  3170. }
  3171. },
  3172. /**
  3173. * Returns uploaded bytes for file identified by id
  3174. */
  3175. getLoaded: function(id){
  3176. return fileState[id].loaded || 0;
  3177. },
  3178. isValid: function(id) {
  3179. return fileState[id] !== undefined;
  3180. },
  3181. reset: function() {
  3182. fileState = [];
  3183. },
  3184. getUuid: function(id) {
  3185. return fileState[id].uuid;
  3186. },
  3187. /**
  3188. * Sends the file identified by id to the server
  3189. */
  3190. upload: function(id, retry){
  3191. var name = this.getName(id);
  3192. options.onUpload(id, name);
  3193. if (chunkFiles) {
  3194. handleFileChunkingUpload(id, retry);
  3195. }
  3196. else {
  3197. handleStandardFileUpload(id);
  3198. }
  3199. },
  3200. cancel: function(id){
  3201. var xhr = fileState[id].xhr;
  3202. options.onCancel(id, this.getName(id));
  3203. if (xhr) {
  3204. xhr.onreadystatechange = null;
  3205. xhr.abort();
  3206. }
  3207. if (resumeEnabled) {
  3208. deletePersistedChunkData(id);
  3209. }
  3210. delete fileState[id];
  3211. },
  3212. getResumableFilesData: function() {
  3213. var matchingCookieNames = [],
  3214. resumableFilesData = [];
  3215. if (chunkFiles && resumeEnabled) {
  3216. if (resumeId === undefined) {
  3217. matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
  3218. cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
  3219. }
  3220. else {
  3221. matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
  3222. cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
  3223. cookieItemDelimiter + resumeId + "="));
  3224. }
  3225. qq.each(matchingCookieNames, function(idx, cookieName) {
  3226. var cookiesNameParts = cookieName.split(cookieItemDelimiter);
  3227. var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
  3228. resumableFilesData.push({
  3229. name: decodeURIComponent(cookiesNameParts[1]),
  3230. size: cookiesNameParts[2],
  3231. uuid: cookieValueParts[0],
  3232. partIdx: cookieValueParts[1]
  3233. });
  3234. });
  3235. return resumableFilesData;
  3236. }
  3237. return [];
  3238. }
  3239. };
  3240. return api;
  3241. };