fileuploader.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282
  1. /*
  2. Multiple file upload component with progress-bar, drag-and-drop.
  3. http://github.com/valums/file-uploader
  4. Copyright (C) 2011 by Andris Valums
  5. Permission is hereby granted, free of charge, to any person obtaining a copy
  6. of this software and associated documentation files (the "Software"), to deal
  7. in the Software without restriction, including without limitation the rights
  8. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. copies of the Software, and to permit persons to whom the Software is
  10. furnished to do so, subject to the following conditions:
  11. The above copyright notice and this permission notice shall be included in
  12. all copies or substantial portions of the Software.
  13. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. THE SOFTWARE.
  20. */
  21. //
  22. // Helper functions
  23. //
  24. var qq = qq || {};
  25. /**
  26. * Adds all missing properties from second obj to first obj
  27. */
  28. qq.extend = function(first, second){
  29. for (var prop in second){
  30. first[prop] = second[prop];
  31. }
  32. };
  33. /**
  34. * Searches for a given element in the array, returns -1 if it is not present.
  35. * @param {Number} [from] The index at which to begin the search
  36. */
  37. qq.indexOf = function(arr, elt, from){
  38. if (arr.indexOf) return arr.indexOf(elt, from);
  39. from = from || 0;
  40. var len = arr.length;
  41. if (from < 0) from += len;
  42. for (; from < len; from++){
  43. if (from in arr && arr[from] === elt){
  44. return from;
  45. }
  46. }
  47. return -1;
  48. };
  49. qq.getUniqueId = (function(){
  50. var id = 0;
  51. return function(){ return id++; };
  52. })();
  53. //
  54. // Events
  55. qq.attach = function(element, type, fn){
  56. if (element.addEventListener){
  57. element.addEventListener(type, fn, false);
  58. } else if (element.attachEvent){
  59. element.attachEvent('on' + type, fn);
  60. }
  61. };
  62. qq.detach = function(element, type, fn){
  63. if (element.removeEventListener){
  64. element.removeEventListener(type, fn, false);
  65. } else if (element.attachEvent){
  66. element.detachEvent('on' + type, fn);
  67. }
  68. };
  69. qq.preventDefault = function(e){
  70. if (e.preventDefault){
  71. e.preventDefault();
  72. } else{
  73. e.returnValue = false;
  74. }
  75. };
  76. //
  77. // Node manipulations
  78. /**
  79. * Insert node a before node b.
  80. */
  81. qq.insertBefore = function(a, b){
  82. b.parentNode.insertBefore(a, b);
  83. };
  84. qq.remove = function(element){
  85. element.parentNode.removeChild(element);
  86. };
  87. qq.contains = function(parent, descendant){
  88. // compareposition returns false in this case
  89. if (parent == descendant) return true;
  90. if (parent.contains){
  91. return parent.contains(descendant);
  92. } else {
  93. return !!(descendant.compareDocumentPosition(parent) & 8);
  94. }
  95. };
  96. /**
  97. * Creates and returns element from html string
  98. * Uses innerHTML to create an element
  99. */
  100. qq.toElement = (function(){
  101. var div = document.createElement('div');
  102. return function(html){
  103. div.innerHTML = html;
  104. var element = div.firstChild;
  105. div.removeChild(element);
  106. return element;
  107. };
  108. })();
  109. //
  110. // Node properties and attributes
  111. /**
  112. * Sets styles for an element.
  113. * Fixes opacity in IE6-8.
  114. */
  115. qq.css = function(element, styles){
  116. if (styles.opacity != null){
  117. if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
  118. styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
  119. }
  120. }
  121. qq.extend(element.style, styles);
  122. };
  123. qq.hasClass = function(element, name){
  124. var re = new RegExp('(^| )' + name + '( |$)');
  125. return re.test(element.className);
  126. };
  127. qq.addClass = function(element, name){
  128. if (!qq.hasClass(element, name)){
  129. element.className += ' ' + name;
  130. }
  131. };
  132. qq.removeClass = function(element, name){
  133. var re = new RegExp('(^| )' + name + '( |$)');
  134. element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
  135. };
  136. qq.setText = function(element, text){
  137. element.innerText = text;
  138. element.textContent = text;
  139. };
  140. //
  141. // Selecting elements
  142. qq.children = function(element){
  143. var children = [],
  144. child = element.firstChild;
  145. while (child){
  146. if (child.nodeType == 1){
  147. children.push(child);
  148. }
  149. child = child.nextSibling;
  150. }
  151. return children;
  152. };
  153. qq.getByClass = function(element, className){
  154. if (element.querySelectorAll){
  155. return element.querySelectorAll('.' + className);
  156. }
  157. var result = [];
  158. var candidates = element.getElementsByTagName("*");
  159. var len = candidates.length;
  160. for (var i = 0; i < len; i++){
  161. if (qq.hasClass(candidates[i], className)){
  162. result.push(candidates[i]);
  163. }
  164. }
  165. return result;
  166. };
  167. /**
  168. * obj2url() takes a json-object as argument and generates
  169. * a querystring. pretty much like jQuery.param()
  170. *
  171. * how to use:
  172. *
  173. * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
  174. *
  175. * will result in:
  176. *
  177. * `http://any.url/upload?otherParam=value&a=b&c=d`
  178. *
  179. * @param Object JSON-Object
  180. * @param String current querystring-part
  181. * @return String encoded querystring
  182. */
  183. qq.obj2url = function(obj, temp, prefixDone){
  184. var uristrings = [],
  185. prefix = '&',
  186. add = function(nextObj, i){
  187. var nextTemp = temp
  188. ? (/\[\]$/.test(temp)) // prevent double-encoding
  189. ? temp
  190. : temp+'['+i+']'
  191. : i;
  192. if ((nextTemp != 'undefined') && (i != 'undefined')) {
  193. uristrings.push(
  194. (typeof nextObj === 'object')
  195. ? qq.obj2url(nextObj, nextTemp, true)
  196. : (Object.prototype.toString.call(nextObj) === '[object Function]')
  197. ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
  198. : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
  199. );
  200. }
  201. };
  202. if (!prefixDone && temp) {
  203. prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
  204. uristrings.push(temp);
  205. uristrings.push(qq.obj2url(obj));
  206. } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
  207. // we wont use a for-in-loop on an array (performance)
  208. for (var i = 0, len = obj.length; i < len; ++i){
  209. add(obj[i], i);
  210. }
  211. } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
  212. // for anything else but a scalar, we will use for-in-loop
  213. for (var i in obj){
  214. add(obj[i], i);
  215. }
  216. } else {
  217. uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
  218. }
  219. return uristrings.join(prefix)
  220. .replace(/^&/, '')
  221. .replace(/%20/g, '+');
  222. };
  223. //
  224. //
  225. // Uploader Classes
  226. //
  227. //
  228. var qq = qq || {};
  229. /**
  230. * Creates upload button, validates upload, but doesn't create file list or dd.
  231. */
  232. qq.FileUploaderBasic = function(o){
  233. this._options = {
  234. // set to true to see the server response
  235. debug: false,
  236. action: '/server/upload',
  237. dest: '/',
  238. fileFieldLabel: 'hdfs_file',
  239. params: {},
  240. button: null,
  241. multiple: true,
  242. maxConnections: 3,
  243. // validation
  244. allowedExtensions: [],
  245. sizeLimit: 0,
  246. minSizeLimit: 0,
  247. // events
  248. // return false to cancel submit
  249. onSubmit: function(id, fileName){},
  250. onProgress: function(id, fileName, loaded, total){},
  251. onComplete: function(id, fileName, responseJSON){},
  252. onCancel: function(id, fileName){},
  253. // messages
  254. messages: {
  255. typeError: "{file} has invalid extension. Only {extensions} are allowed.",
  256. sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
  257. minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
  258. emptyError: "{file} is empty, please select files again without it.",
  259. onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
  260. },
  261. showMessage: function(message){
  262. alert(message);
  263. }
  264. };
  265. qq.extend(this._options, o);
  266. // number of files being uploaded
  267. this._filesInProgress = 0;
  268. this._handler = this._createUploadHandler();
  269. if (this._options.button){
  270. this._button = this._createUploadButton(this._options.button);
  271. }
  272. this._preventLeaveInProgress();
  273. };
  274. qq.FileUploaderBasic.prototype = {
  275. setParams: function(params){
  276. this._options.params = params;
  277. },
  278. getInProgress: function(){
  279. return this._filesInProgress;
  280. },
  281. _createUploadButton: function(element){
  282. var self = this;
  283. return new qq.UploadButton({
  284. element: element,
  285. multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
  286. onChange: function(input){
  287. self._onInputChange(input);
  288. }
  289. });
  290. },
  291. _createUploadHandler: function(){
  292. var self = this,
  293. handlerClass;
  294. if(qq.UploadHandlerXhr.isSupported()){
  295. handlerClass = 'UploadHandlerXhr';
  296. } else {
  297. handlerClass = 'UploadHandlerForm';
  298. }
  299. var handler = new qq[handlerClass]({
  300. debug: this._options.debug,
  301. action: this._options.action,
  302. dest: '/',
  303. fileFieldLabel:'hdfs_file',
  304. maxConnections: this._options.maxConnections,
  305. onProgress: function(id, fileName, loaded, total){
  306. self._onProgress(id, fileName, loaded, total);
  307. self._options.onProgress(id, fileName, loaded, total);
  308. },
  309. onComplete: function(id, fileName, result){
  310. self._onComplete(id, fileName, result);
  311. self._options.onComplete(id, fileName, result);
  312. },
  313. onCancel: function(id, fileName){
  314. self._onCancel(id, fileName);
  315. self._options.onCancel(id, fileName);
  316. }
  317. });
  318. return handler;
  319. },
  320. _preventLeaveInProgress: function(){
  321. var self = this;
  322. qq.attach(window, 'beforeunload', function(e){
  323. if (!self._filesInProgress){return;}
  324. var e = e || window.event;
  325. // for ie, ff
  326. e.returnValue = self._options.messages.onLeave;
  327. // for webkit
  328. return self._options.messages.onLeave;
  329. });
  330. },
  331. _onSubmit: function(id, fileName){
  332. this._filesInProgress++;
  333. },
  334. _onProgress: function(id, fileName, loaded, total){
  335. },
  336. _onComplete: function(id, fileName, result){
  337. this._filesInProgress--;
  338. if (result.error){
  339. this._options.showMessage(result.error);
  340. }
  341. },
  342. _onCancel: function(id, fileName){
  343. this._filesInProgress--;
  344. },
  345. _onInputChange: function(input){
  346. if (this._handler instanceof qq.UploadHandlerXhr){
  347. this._uploadFileList(input.files);
  348. } else {
  349. if (this._validateFile(input)){
  350. this._uploadFile(input);
  351. }
  352. }
  353. this._button.reset();
  354. },
  355. _uploadFileList: function(files){
  356. for (var i=0; i<files.length; i++){
  357. if ( !this._validateFile(files[i])){
  358. return;
  359. }
  360. }
  361. for (var i=0; i<files.length; i++){
  362. this._uploadFile(files[i]);
  363. }
  364. },
  365. _uploadFile: function(fileContainer){
  366. var id = this._handler.add(fileContainer);
  367. var fileName = this._handler.getName(id);
  368. if (this._options.onSubmit(id, fileName) !== false){
  369. this._onSubmit(id, fileName);
  370. this._handler.upload(id, this._options.params);
  371. }
  372. },
  373. _validateFile: function(file){
  374. var name, size;
  375. if (file.value){
  376. // it is a file input
  377. // get input value and remove path to normalize
  378. name = file.value.replace(/.*(\/|\\)/, "");
  379. } else {
  380. // fix missing properties in Safari
  381. name = file.fileName != null ? file.fileName : file.name;
  382. size = file.fileSize != null ? file.fileSize : file.size;
  383. }
  384. if (! this._isAllowedExtension(name)){
  385. this._error('typeError', name);
  386. return false;
  387. } else if (size === 0){
  388. this._error('emptyError', name);
  389. return false;
  390. } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){
  391. this._error('sizeError', name);
  392. return false;
  393. } else if (size && size < this._options.minSizeLimit){
  394. this._error('minSizeError', name);
  395. return false;
  396. }
  397. return true;
  398. },
  399. _error: function(code, fileName){
  400. var message = this._options.messages[code];
  401. function r(name, replacement){ message = message.replace(name, replacement); }
  402. r('{file}', this._formatFileName(fileName));
  403. r('{extensions}', this._options.allowedExtensions.join(', '));
  404. r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
  405. r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
  406. this._options.showMessage(message);
  407. },
  408. _formatFileName: function(name){
  409. if (name.length > 33){
  410. name = name.slice(0, 19) + '...' + name.slice(-13);
  411. }
  412. return name;
  413. },
  414. _isAllowedExtension: function(fileName){
  415. var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
  416. var allowed = this._options.allowedExtensions;
  417. if (!allowed.length){return true;}
  418. for (var i=0; i<allowed.length; i++){
  419. if (allowed[i].toLowerCase() == ext){ return true;}
  420. }
  421. return false;
  422. },
  423. _formatSize: function(bytes){
  424. var i = -1;
  425. do {
  426. bytes = bytes / 1024;
  427. i++;
  428. } while (bytes > 99);
  429. return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
  430. }
  431. };
  432. /**
  433. * Class that creates upload widget with drag-and-drop and file list
  434. * @inherits qq.FileUploaderBasic
  435. */
  436. qq.FileUploader = function(o){
  437. // call parent constructor
  438. qq.FileUploaderBasic.apply(this, arguments);
  439. // additional options
  440. qq.extend(this._options, {
  441. element: null,
  442. // if set, will be used instead of qq-upload-list in template
  443. listElement: null,
  444. template: '<div class="qq-uploader">' +
  445. '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
  446. '<div class="qq-upload-button">Upload a file</div>' +
  447. '<ul class="qq-upload-list"></ul>' +
  448. '</div>',
  449. // template for one item in file list
  450. fileTemplate: '<li>' +
  451. '<span class="qq-upload-file"></span>' +
  452. '<span class="qq-upload-spinner"></span>' +
  453. '<span class="qq-upload-size"></span>' +
  454. '<a class="qq-upload-cancel" href="#">Cancel</a>' +
  455. '<span class="qq-upload-failed-text">Failed</span>' +
  456. '</li>',
  457. classes: {
  458. // used to get elements from templates
  459. button: 'qq-upload-button',
  460. drop: 'qq-upload-drop-area',
  461. dropActive: 'qq-upload-drop-area-active',
  462. list: 'qq-upload-list',
  463. file: 'qq-upload-file',
  464. spinner: 'qq-upload-spinner',
  465. size: 'qq-upload-size',
  466. cancel: 'qq-upload-cancel',
  467. // added to list item when upload completes
  468. // used in css to hide progress spinner
  469. success: 'qq-upload-success',
  470. fail: 'qq-upload-fail'
  471. }
  472. });
  473. // overwrite options with user supplied
  474. this._options.dest = "";
  475. this._options.fileFieldLabel = "";
  476. qq.extend(this._options, o);
  477. this._element = this._options.element;
  478. this._element.innerHTML = this._options.template;
  479. this._listElement = this._options.listElement || this._find(this._element, 'list');
  480. this._classes = this._options.classes;
  481. this._button = this._createUploadButton(this._find(this._element, 'button'));
  482. this._bindCancelEvent();
  483. this._setupDragDrop();
  484. };
  485. // inherit from Basic Uploader
  486. qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
  487. qq.extend(qq.FileUploader.prototype, {
  488. /**
  489. * Gets one of the elements listed in this._options.classes
  490. **/
  491. _find: function(parent, type){
  492. var element = qq.getByClass(parent, this._options.classes[type])[0];
  493. if (!element){
  494. throw new Error('element not found ' + type);
  495. }
  496. return element;
  497. },
  498. _setupDragDrop: function(){
  499. var self = this,
  500. dropArea = this._find(this._element, 'drop');
  501. var dz = new qq.UploadDropZone({
  502. element: dropArea,
  503. onEnter: function(e){
  504. qq.addClass(dropArea, self._classes.dropActive);
  505. e.stopPropagation();
  506. },
  507. onLeave: function(e){
  508. e.stopPropagation();
  509. },
  510. onLeaveNotDescendants: function(e){
  511. qq.removeClass(dropArea, self._classes.dropActive);
  512. },
  513. onDrop: function(e){
  514. dropArea.style.display = 'none';
  515. qq.removeClass(dropArea, self._classes.dropActive);
  516. self._uploadFileList(e.dataTransfer.files);
  517. }
  518. });
  519. dropArea.style.display = 'none';
  520. qq.attach(document, 'dragenter', function(e){
  521. if (!dz._isValidFileDrag(e)) return;
  522. dropArea.style.display = 'block';
  523. });
  524. qq.attach(document, 'dragleave', function(e){
  525. if (!dz._isValidFileDrag(e)) return;
  526. var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
  527. // only fire when leaving document out
  528. if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){
  529. dropArea.style.display = 'none';
  530. }
  531. });
  532. },
  533. _onSubmit: function(id, fileName){
  534. qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
  535. this._addToList(id, fileName);
  536. },
  537. _onProgress: function(id, fileName, loaded, total){
  538. qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
  539. var item = this._getItemByFileId(id);
  540. var size = this._find(item, 'size');
  541. size.style.display = 'inline';
  542. var text;
  543. if (loaded != total){
  544. text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
  545. } else {
  546. text = this._formatSize(total);
  547. }
  548. qq.setText(size, text);
  549. },
  550. _onComplete: function(id, fileName, result){
  551. qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
  552. // mark completed
  553. var item = this._getItemByFileId(id);
  554. qq.remove(this._find(item, 'cancel'));
  555. qq.remove(this._find(item, 'spinner'));
  556. console.log(result)
  557. // if (result.success){
  558. qq.addClass(item, this._classes.success);
  559. /*} else {
  560. qq.addClass(item, this._classes.fail);
  561. }*/
  562. },
  563. _addToList: function(id, fileName){
  564. var item = qq.toElement(this._options.fileTemplate);
  565. item.qqFileId = id;
  566. var fileElement = this._find(item, 'file');
  567. qq.setText(fileElement, this._formatFileName(fileName));
  568. this._find(item, 'size').style.display = 'none';
  569. this._listElement.appendChild(item);
  570. },
  571. _getItemByFileId: function(id){
  572. var item = this._listElement.firstChild;
  573. // there can't be txt nodes in dynamically created list
  574. // and we can use nextSibling
  575. while (item){
  576. if (item.qqFileId == id) return item;
  577. item = item.nextSibling;
  578. }
  579. },
  580. /**
  581. * delegate click event for cancel link
  582. **/
  583. _bindCancelEvent: function(){
  584. var self = this,
  585. list = this._listElement;
  586. qq.attach(list, 'click', function(e){
  587. e = e || window.event;
  588. var target = e.target || e.srcElement;
  589. if (qq.hasClass(target, self._classes.cancel)){
  590. qq.preventDefault(e);
  591. var item = target.parentNode;
  592. self._handler.cancel(item.qqFileId);
  593. qq.remove(item);
  594. }
  595. });
  596. }
  597. });
  598. qq.UploadDropZone = function(o){
  599. this._options = {
  600. element: null,
  601. onEnter: function(e){},
  602. onLeave: function(e){},
  603. // is not fired when leaving element by hovering descendants
  604. onLeaveNotDescendants: function(e){},
  605. onDrop: function(e){}
  606. };
  607. qq.extend(this._options, o);
  608. this._element = this._options.element;
  609. this._disableDropOutside();
  610. this._attachEvents();
  611. };
  612. qq.UploadDropZone.prototype = {
  613. _disableDropOutside: function(e){
  614. // run only once for all instances
  615. if (!qq.UploadDropZone.dropOutsideDisabled ){
  616. qq.attach(document, 'dragover', function(e){
  617. if (e.dataTransfer){
  618. e.dataTransfer.dropEffect = 'none';
  619. e.preventDefault();
  620. }
  621. });
  622. qq.UploadDropZone.dropOutsideDisabled = true;
  623. }
  624. },
  625. _attachEvents: function(){
  626. var self = this;
  627. qq.attach(self._element, 'dragover', function(e){
  628. if (!self._isValidFileDrag(e)) return;
  629. var effect = e.dataTransfer.effectAllowed;
  630. if (effect == 'move' || effect == 'linkMove'){
  631. e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
  632. } else {
  633. e.dataTransfer.dropEffect = 'copy'; // for Chrome
  634. }
  635. e.stopPropagation();
  636. e.preventDefault();
  637. });
  638. qq.attach(self._element, 'dragenter', function(e){
  639. if (!self._isValidFileDrag(e)) return;
  640. self._options.onEnter(e);
  641. });
  642. qq.attach(self._element, 'dragleave', function(e){
  643. if (!self._isValidFileDrag(e)) return;
  644. self._options.onLeave(e);
  645. var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
  646. // do not fire when moving a mouse over a descendant
  647. if (qq.contains(this, relatedTarget)) return;
  648. self._options.onLeaveNotDescendants(e);
  649. });
  650. qq.attach(self._element, 'drop', function(e){
  651. if (!self._isValidFileDrag(e)) return;
  652. e.preventDefault();
  653. self._options.onDrop(e);
  654. });
  655. },
  656. _isValidFileDrag: function(e){
  657. var dt = e.dataTransfer,
  658. // do not check dt.types.contains in webkit, because it crashes safari 4
  659. isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;
  660. isIE9 = navigator.appVersion.indexOf("MSIE 9") > -1;
  661. if (isIE9) return false;
  662. // dt.effectAllowed is none in Safari 5
  663. // dt.types.contains check is for firefox
  664. return dt && dt.effectAllowed != 'none' &&
  665. (dt.files || (!isWebkit && dt.types && dt.types.contains && dt.types.contains('Files')));
  666. }
  667. };
  668. qq.UploadButton = function(o){
  669. this._options = {
  670. element: null,
  671. // if set to true adds multiple attribute to file input
  672. multiple: false,
  673. // name attribute of file input
  674. name: 'file',
  675. onChange: function(input){},
  676. hoverClass: 'qq-upload-button-hover',
  677. focusClass: 'qq-upload-button-focus'
  678. };
  679. qq.extend(this._options, o);
  680. this._element = this._options.element;
  681. // make button suitable container for input
  682. qq.css(this._element, {
  683. position: 'relative',
  684. overflow: 'hidden',
  685. // Make sure browse button is in the right side
  686. // in Internet Explorer
  687. direction: 'ltr'
  688. });
  689. this._input = this._createInput();
  690. };
  691. qq.UploadButton.prototype = {
  692. /* returns file input element */
  693. getInput: function(){
  694. return this._input;
  695. },
  696. /* cleans/recreates the file input */
  697. reset: function(){
  698. if (this._input.parentNode){
  699. qq.remove(this._input);
  700. }
  701. qq.removeClass(this._element, this._options.focusClass);
  702. this._input = this._createInput();
  703. },
  704. _createInput: function(){
  705. var input = document.createElement("input");
  706. if (this._options.multiple){
  707. input.setAttribute("multiple", "multiple");
  708. }
  709. input.setAttribute("type", "file");
  710. input.setAttribute("name", this._options.name);
  711. qq.css(input, {
  712. position: 'absolute',
  713. // in Opera only 'browse' button
  714. // is clickable and it is located at
  715. // the right side of the input
  716. right: 0,
  717. top: 0,
  718. fontFamily: 'Arial',
  719. // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
  720. fontSize: '118px',
  721. margin: 0,
  722. padding: 0,
  723. cursor: 'pointer',
  724. opacity: 0
  725. });
  726. this._element.appendChild(input);
  727. var self = this;
  728. qq.attach(input, 'change', function(){
  729. self._options.onChange(input);
  730. });
  731. qq.attach(input, 'mouseover', function(){
  732. qq.addClass(self._element, self._options.hoverClass);
  733. });
  734. qq.attach(input, 'mouseout', function(){
  735. qq.removeClass(self._element, self._options.hoverClass);
  736. });
  737. qq.attach(input, 'focus', function(){
  738. qq.addClass(self._element, self._options.focusClass);
  739. });
  740. qq.attach(input, 'blur', function(){
  741. qq.removeClass(self._element, self._options.focusClass);
  742. });
  743. // IE and Opera, unfortunately have 2 tab stops on file input
  744. // which is unacceptable in our case, disable keyboard access
  745. if (window.attachEvent){
  746. // it is IE or Opera
  747. input.setAttribute('tabIndex', "-1");
  748. }
  749. return input;
  750. }
  751. };
  752. /**
  753. * Class for uploading files, uploading itself is handled by child classes
  754. */
  755. qq.UploadHandlerAbstract = function(o){
  756. this._options = {
  757. debug: false,
  758. action: '/upload.php',
  759. // maximum number of concurrent uploads
  760. maxConnections: 999,
  761. onProgress: function(id, fileName, loaded, total){},
  762. onComplete: function(id, fileName, response){},
  763. onCancel: function(id, fileName){}
  764. };
  765. qq.extend(this._options, o);
  766. this._queue = [];
  767. // params for files in queue
  768. this._params = [];
  769. };
  770. qq.UploadHandlerAbstract.prototype = {
  771. log: function(str){
  772. if (this._options.debug && window.console) console.log('[uploader] ' + str);
  773. },
  774. /**
  775. * Adds file or file input to the queue
  776. * @returns id
  777. **/
  778. add: function(file){},
  779. /**
  780. * Sends the file identified by id and additional query params to the server
  781. */
  782. upload: function(id, params){
  783. var len = this._queue.push(id);
  784. var copy = {};
  785. qq.extend(copy, params);
  786. this._params[id] = copy;
  787. // if too many active uploads, wait...
  788. if (len <= this._options.maxConnections){
  789. this._upload(id, this._params[id]);
  790. }
  791. },
  792. /**
  793. * Cancels file upload by id
  794. */
  795. cancel: function(id){
  796. this._cancel(id);
  797. this._dequeue(id);
  798. },
  799. /**
  800. * Cancells all uploads
  801. */
  802. cancelAll: function(){
  803. for (var i=0; i<this._queue.length; i++){
  804. this._cancel(this._queue[i]);
  805. }
  806. this._queue = [];
  807. },
  808. /**
  809. * Returns name of the file identified by id
  810. */
  811. getName: function(id){},
  812. /**
  813. * Returns size of the file identified by id
  814. */
  815. getSize: function(id){},
  816. /**
  817. * Returns id of files being uploaded or
  818. * waiting for their turn
  819. */
  820. getQueue: function(){
  821. return this._queue;
  822. },
  823. /**
  824. * Actual upload method
  825. */
  826. _upload: function(id){},
  827. /**
  828. * Actual cancel method
  829. */
  830. _cancel: function(id){},
  831. /**
  832. * Removes element from queue, starts upload of next
  833. */
  834. _dequeue: function(id){
  835. var i = qq.indexOf(this._queue, id);
  836. this._queue.splice(i, 1);
  837. var max = this._options.maxConnections;
  838. if (this._queue.length >= max && i < max){
  839. var nextId = this._queue[max-1];
  840. this._upload(nextId, this._params[nextId]);
  841. }
  842. }
  843. };
  844. /**
  845. * Class for uploading files using form and iframe
  846. * @inherits qq.UploadHandlerAbstract
  847. */
  848. qq.UploadHandlerForm = function(o){
  849. qq.UploadHandlerAbstract.apply(this, arguments);
  850. this._inputs = {};
  851. };
  852. // @inherits qq.UploadHandlerAbstract
  853. qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
  854. qq.extend(qq.UploadHandlerForm.prototype, {
  855. add: function(fileInput){
  856. fileInput.setAttribute('name', 'qqfile');
  857. var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
  858. this._inputs[id] = fileInput;
  859. // remove file input from DOM
  860. if (fileInput.parentNode){
  861. qq.remove(fileInput);
  862. }
  863. return id;
  864. },
  865. getName: function(id){
  866. // get input value and remove path to normalize
  867. return this._inputs[id].value.replace(/.*(\/|\\)/, "");
  868. },
  869. _cancel: function(id){
  870. this._options.onCancel(id, this.getName(id));
  871. delete this._inputs[id];
  872. var iframe = document.getElementById(id);
  873. if (iframe){
  874. // to cancel request set src to something else
  875. // we use src="javascript:false;" because it doesn't
  876. // trigger ie6 prompt on https
  877. iframe.setAttribute('src', 'javascript:false;');
  878. qq.remove(iframe);
  879. }
  880. },
  881. _upload: function(id, params){
  882. var input = this._inputs[id];
  883. if (!input){
  884. throw new Error('file with passed id was not added, or already uploaded or cancelled');
  885. }
  886. var fileName = this.getName(id);
  887. var iframe = this._createIframe(id);
  888. var form = this._createForm(iframe, params);
  889. input.name = params.fileFieldLabel;
  890. form.appendChild(input);
  891. var dest = document.createElement('input');
  892. dest.type = 'text';
  893. dest.name = 'dest';
  894. dest.value = params.dest;
  895. form.appendChild(dest);
  896. var self = this;
  897. this._attachLoadEvent(iframe, function(){
  898. self.log('iframe loaded');
  899. var response = self._getIframeContentJSON(iframe);
  900. self._options.onComplete(id, fileName, response);
  901. self._dequeue(id);
  902. delete self._inputs[id];
  903. // timeout added to fix busy state in FF3.6
  904. setTimeout(function(){
  905. qq.remove(iframe);
  906. }, 1);
  907. });
  908. form.submit();
  909. qq.remove(form);
  910. return id;
  911. },
  912. _attachLoadEvent: function(iframe, callback){
  913. qq.attach(iframe, 'load', function(){
  914. // when we remove iframe from dom
  915. // the request stops, but in IE load
  916. // event fires
  917. if (!iframe.parentNode){
  918. return;
  919. }
  920. // fixing Opera 10.53
  921. if (iframe.contentDocument &&
  922. iframe.contentDocument.body &&
  923. iframe.contentDocument.body.innerHTML == "false"){
  924. // In Opera event is fired second time
  925. // when body.innerHTML changed from false
  926. // to server response approx. after 1 sec
  927. // when we upload file with iframe
  928. return;
  929. }
  930. callback();
  931. });
  932. },
  933. /**
  934. * Returns json object received by iframe from server.
  935. */
  936. _getIframeContentJSON: function(iframe){
  937. // iframe.contentWindow.document - for IE<7
  938. var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
  939. response;
  940. this.log("converting iframe's innerHTML to JSON");
  941. this.log("innerHTML = " + doc.body.innerHTML);
  942. try {
  943. response = eval("(" + doc.body.innerHTML + ")");
  944. } catch(err){
  945. response = {};
  946. }
  947. return response;
  948. },
  949. /**
  950. * Creates iframe with unique name
  951. */
  952. _createIframe: function(id){
  953. // We can't use following code as the name attribute
  954. // won't be properly registered in IE6, and new window
  955. // on form submit will open
  956. // var iframe = document.createElement('iframe');
  957. // iframe.setAttribute('name', id);
  958. var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
  959. // src="javascript:false;" removes ie6 prompt on https
  960. iframe.setAttribute('id', id);
  961. iframe.style.display = 'none';
  962. document.body.appendChild(iframe);
  963. return iframe;
  964. },
  965. /**
  966. * Creates form, that will be submitted to iframe
  967. */
  968. _createForm: function(iframe, params){
  969. // We can't use the following code in IE6
  970. // var form = document.createElement('form');
  971. // form.setAttribute('method', 'post');
  972. // form.setAttribute('enctype', 'multipart/form-data');
  973. // Because in this case file won't be attached to request
  974. var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');
  975. form.setAttribute('action', this._options.action);
  976. form.setAttribute('target', iframe.name);
  977. form.style.display = 'none';
  978. document.body.appendChild(form);
  979. return form;
  980. }
  981. });
  982. /**
  983. * Class for uploading files using xhr
  984. * @inherits qq.UploadHandlerAbstract
  985. */
  986. qq.UploadHandlerXhr = function(o){
  987. qq.UploadHandlerAbstract.apply(this, arguments);
  988. this._files = [];
  989. this._xhrs = [];
  990. // current loaded size in bytes for each file
  991. this._loaded = [];
  992. };
  993. // static method
  994. qq.UploadHandlerXhr.isSupported = function(){
  995. var input = document.createElement('input');
  996. input.type = 'file';
  997. return (
  998. 'multiple' in input &&
  999. typeof File != "undefined" &&
  1000. typeof (new XMLHttpRequest()).upload != "undefined" );
  1001. };
  1002. // @inherits qq.UploadHandlerAbstract
  1003. qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
  1004. qq.extend(qq.UploadHandlerXhr.prototype, {
  1005. /**
  1006. * Adds file to the queue
  1007. * Returns id to use with upload, cancel
  1008. **/
  1009. add: function(file){
  1010. // HUE-815: [fb] Upload button does not work in Firefox 3.6
  1011. // see https://github.com/valums/ajax-upload/issues/91
  1012. //if (!(file instanceof File)){
  1013. if (!(file instanceof File || file.__proto__.constructor.name == 'File' || file instanceof Object) ){
  1014. throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
  1015. }
  1016. return this._files.push(file) - 1;
  1017. },
  1018. getName: function(id){
  1019. var file = this._files[id];
  1020. // fix missing name in Safari 4
  1021. return file.fileName != null ? file.fileName : file.name;
  1022. },
  1023. getSize: function(id){
  1024. var file = this._files[id];
  1025. return file.fileSize != null ? file.fileSize : file.size;
  1026. },
  1027. /**
  1028. * Returns uploaded bytes for file identified by id
  1029. */
  1030. getLoaded: function(id){
  1031. return this._loaded[id] || 0;
  1032. },
  1033. /**
  1034. * Sends the file identified by id and additional query params to the server
  1035. * @param {Object} params name-value string pairs
  1036. */
  1037. _upload: function(id, params){
  1038. var file = this._files[id],
  1039. name = this.getName(id),
  1040. size = this.getSize(id);
  1041. this._loaded[id] = 0;
  1042. var xhr = this._xhrs[id] = new XMLHttpRequest();
  1043. var self = this;
  1044. xhr.upload.onprogress = function(e){
  1045. if (e.lengthComputable){
  1046. self._loaded[id] = e.loaded;
  1047. self._options.onProgress(id, name, e.loaded, e.total);
  1048. }
  1049. };
  1050. xhr.onreadystatechange = function(){
  1051. if (xhr.readyState == 4){
  1052. self._onComplete(id, xhr);
  1053. }
  1054. };
  1055. var formData = new FormData();
  1056. formData.append(params.fileFieldLabel, file);
  1057. formData.append('dest', params.dest);
  1058. var action = this._options.action + "?dest=" + params.dest;
  1059. xhr.open("POST", action, true);
  1060. xhr.send(formData);
  1061. },
  1062. _onComplete: function(id, xhr){
  1063. // the request was aborted/cancelled
  1064. if (!this._files[id]) return;
  1065. var name = this.getName(id);
  1066. var size = this.getSize(id);
  1067. this._options.onProgress(id, name, size, size);
  1068. if (xhr.status == 200){
  1069. this.log("xhr - server response received");
  1070. this.log("responseText = " + xhr.responseText);
  1071. var response;
  1072. try {
  1073. response = eval("(" + xhr.responseText + ")");
  1074. } catch(err){
  1075. response = {};
  1076. }
  1077. this._options.onComplete(id, name, response);
  1078. } else {
  1079. this._options.onComplete(id, name, xhr);
  1080. }
  1081. this._files[id] = null;
  1082. this._xhrs[id] = null;
  1083. this._dequeue(id);
  1084. },
  1085. _cancel: function(id){
  1086. this._options.onCancel(id, this.getName(id));
  1087. this._files[id] = null;
  1088. if (this._xhrs[id]){
  1089. this._xhrs[id].abort();
  1090. this._xhrs[id] = null;
  1091. }
  1092. }
  1093. });