ajaxupload.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. /**
  2. * AJAX Upload ( http://valums.com/ajax-upload/ )
  3. * Copyright (c) Andris Valums
  4. * Licensed under the MIT license ( http://valums.com/mit-license/ )
  5. * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions
  6. */
  7. (function () {
  8. /* global window */
  9. /* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */
  10. /**
  11. * Wrapper for FireBug's console.log
  12. */
  13. function log(){
  14. if (typeof(console) != 'undefined' && typeof(console.log) == 'function'){
  15. Array.prototype.unshift.call(arguments, '[Ajax Upload]');
  16. console.log( Array.prototype.join.call(arguments, ' '));
  17. }
  18. }
  19. /**
  20. * Attaches event to a dom element.
  21. * @param {Element} el
  22. * @param type event name
  23. * @param fn callback This refers to the passed element
  24. */
  25. function addEvent(el, type, fn){
  26. if (el.addEventListener) {
  27. el.addEventListener(type, fn, false);
  28. } else if (el.attachEvent) {
  29. el.attachEvent('on' + type, function(){
  30. fn.call(el);
  31. });
  32. } else {
  33. throw new Error('not supported or DOM not loaded');
  34. }
  35. }
  36. /**
  37. * Attaches resize event to a window, limiting
  38. * number of event fired. Fires only when encounteres
  39. * delay of 100 after series of events.
  40. *
  41. * Some browsers fire event multiple times when resizing
  42. * http://www.quirksmode.org/dom/events/resize.html
  43. *
  44. * @param fn callback This refers to the passed element
  45. */
  46. function addResizeEvent(fn){
  47. var timeout;
  48. addEvent(window, 'resize', function(){
  49. if (timeout){
  50. clearTimeout(timeout);
  51. }
  52. timeout = setTimeout(fn, 100);
  53. });
  54. }
  55. // Needs more testing, will be rewriten for next version
  56. // getOffset function copied from jQuery lib (http://jquery.com/)
  57. if (document.documentElement.getBoundingClientRect){
  58. // Get Offset using getBoundingClientRect
  59. // http://ejohn.org/blog/getboundingclientrect-is-awesome/
  60. var getOffset = function(el){
  61. var box = el.getBoundingClientRect();
  62. var doc = el.ownerDocument;
  63. var body = doc.body;
  64. var docElem = doc.documentElement; // for ie
  65. var clientTop = docElem.clientTop || body.clientTop || 0;
  66. var clientLeft = docElem.clientLeft || body.clientLeft || 0;
  67. // In Internet Explorer 7 getBoundingClientRect property is treated as physical,
  68. // while others are logical. Make all logical, like in IE8.
  69. var zoom = 1;
  70. if (body.getBoundingClientRect) {
  71. var bound = body.getBoundingClientRect();
  72. zoom = (bound.right - bound.left) / body.clientWidth;
  73. }
  74. if (zoom > 1) {
  75. clientTop = 0;
  76. clientLeft = 0;
  77. }
  78. var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop, left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft;
  79. return {
  80. top: top,
  81. left: left
  82. };
  83. };
  84. } else {
  85. // Get offset adding all offsets
  86. var getOffset = function(el){
  87. var top = 0, left = 0;
  88. do {
  89. top += el.offsetTop || 0;
  90. left += el.offsetLeft || 0;
  91. el = el.offsetParent;
  92. } while (el);
  93. return {
  94. left: left,
  95. top: top
  96. };
  97. };
  98. }
  99. /**
  100. * Returns left, top, right and bottom properties describing the border-box,
  101. * in pixels, with the top-left relative to the body
  102. * @param {Element} el
  103. * @return {Object} Contains left, top, right,bottom
  104. */
  105. function getBox(el){
  106. var left, right, top, bottom;
  107. var offset = getOffset(el);
  108. left = offset.left;
  109. top = offset.top;
  110. right = left + el.offsetWidth;
  111. bottom = top + el.offsetHeight;
  112. return {
  113. left: left,
  114. right: right,
  115. top: top,
  116. bottom: bottom
  117. };
  118. }
  119. /**
  120. * Helper that takes object literal
  121. * and add all properties to element.style
  122. * @param {Element} el
  123. * @param {Object} styles
  124. */
  125. function addStyles(el, styles){
  126. for (var name in styles) {
  127. if (styles.hasOwnProperty(name)) {
  128. el.style[name] = styles[name];
  129. }
  130. }
  131. }
  132. /**
  133. * Function places an absolutely positioned
  134. * element on top of the specified element
  135. * copying position and dimentions.
  136. * @param {Element} from
  137. * @param {Element} to
  138. */
  139. function copyLayout(from, to){
  140. var box = getBox(from);
  141. addStyles(to, {
  142. position: 'absolute',
  143. left : box.left + 'px',
  144. top : box.top + 'px',
  145. width : from.offsetWidth + 'px',
  146. height : from.offsetHeight + 'px'
  147. });
  148. }
  149. /**
  150. * Creates and returns element from html chunk
  151. * Uses innerHTML to create an element
  152. */
  153. var toElement = (function(){
  154. var div = document.createElement('div');
  155. return function(html){
  156. div.innerHTML = html;
  157. var el = div.firstChild;
  158. return div.removeChild(el);
  159. };
  160. })();
  161. /**
  162. * Function generates unique id
  163. * @return unique id
  164. */
  165. var getUID = (function(){
  166. var id = 0;
  167. return function(){
  168. return 'ValumsAjaxUpload' + id++;
  169. };
  170. })();
  171. /**
  172. * Get file name from path
  173. * @param {String} file path to file
  174. * @return filename
  175. */
  176. function fileFromPath(file){
  177. return file.replace(/.*(\/|\\)/, "");
  178. }
  179. /**
  180. * Get file extension lowercase
  181. * @param {String} file name
  182. * @return file extenstion
  183. */
  184. function getExt(file){
  185. return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : '';
  186. }
  187. function hasClass(el, name){
  188. var re = new RegExp('\\b' + name + '\\b');
  189. return re.test(el.className);
  190. }
  191. function addClass(el, name){
  192. if ( ! hasClass(el, name)){
  193. el.className += ' ' + name;
  194. }
  195. }
  196. function removeClass(el, name){
  197. var re = new RegExp('\\b' + name + '\\b');
  198. el.className = el.className.replace(re, '');
  199. }
  200. function removeNode(el){
  201. el.parentNode.removeChild(el);
  202. }
  203. /**
  204. * Easy styling and uploading
  205. * @constructor
  206. * @param button An element you want convert to
  207. * upload button. Tested dimentions up to 500x500px
  208. * @param {Object} options See defaults below.
  209. */
  210. window.AjaxUpload = function(button, options){
  211. this._settings = {
  212. // Location of the server-side upload script
  213. action: 'upload.php',
  214. // File upload name
  215. name: 'userfile',
  216. // Additional data to send
  217. data: {},
  218. // Submit file as soon as it's selected
  219. autoSubmit: true,
  220. // The type of data that you're expecting back from the server.
  221. // html and xml are detected automatically.
  222. // Only useful when you are using json data as a response.
  223. // Set to "json" in that case.
  224. responseType: false,
  225. // Class applied to button when mouse is hovered
  226. hoverClass: 'hover',
  227. // Class applied to button when AU is disabled
  228. disabledClass: 'disabled',
  229. // When user selects a file, useful with autoSubmit disabled
  230. // You can return false to cancel upload
  231. onChange: function(file, extension){
  232. },
  233. // Callback to fire before file is uploaded
  234. // You can return false to cancel upload
  235. onSubmit: function(file, extension){
  236. },
  237. // Fired when file upload is completed
  238. // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!
  239. onComplete: function(file, response){
  240. }
  241. };
  242. // Merge the users options with our defaults
  243. for (var i in options) {
  244. if (options.hasOwnProperty(i)){
  245. this._settings[i] = options[i];
  246. }
  247. }
  248. // button isn't necessary a dom element
  249. if (button.jquery){
  250. // jQuery object was passed
  251. button = button[0];
  252. } else if (typeof button == "string") {
  253. if (/^#.*/.test(button)){
  254. // If jQuery user passes #elementId don't break it
  255. button = button.slice(1);
  256. }
  257. button = document.getElementById(button);
  258. }
  259. if ( ! button || button.nodeType !== 1){
  260. throw new Error("Please make sure that you're passing a valid element");
  261. }
  262. if ( button.nodeName.toUpperCase() == 'A'){
  263. // disable link
  264. addEvent(button, 'click', function(e){
  265. if (e && e.preventDefault){
  266. e.preventDefault();
  267. } else if (window.event){
  268. window.event.returnValue = false;
  269. }
  270. });
  271. }
  272. // DOM element
  273. this._button = button;
  274. // DOM element
  275. this._input = null;
  276. // If disabled clicking on button won't do anything
  277. this._disabled = false;
  278. // if the button was disabled before refresh if will remain
  279. // disabled in FireFox, let's fix it
  280. this.enable();
  281. this._rerouteClicks();
  282. };
  283. // assigning methods to our class
  284. AjaxUpload.prototype = {
  285. setData: function(data){
  286. this._settings.data = data;
  287. },
  288. disable: function(){
  289. addClass(this._button, this._settings.disabledClass);
  290. this._disabled = true;
  291. var nodeName = this._button.nodeName.toUpperCase();
  292. if (nodeName == 'INPUT' || nodeName == 'BUTTON'){
  293. this._button.setAttribute('disabled', 'disabled');
  294. }
  295. // hide input
  296. if (this._input){
  297. // We use visibility instead of display to fix problem with Safari 4
  298. // The problem is that the value of input doesn't change if it
  299. // has display none when user selects a file
  300. this._input.parentNode.style.visibility = 'hidden';
  301. }
  302. },
  303. enable: function(){
  304. removeClass(this._button, this._settings.disabledClass);
  305. this._button.removeAttribute('disabled');
  306. this._disabled = false;
  307. },
  308. /**
  309. * Creates invisible file input
  310. * that will hover above the button
  311. * <div><input type='file' /></div>
  312. */
  313. _createInput: function(){
  314. var self = this;
  315. var input = document.createElement("input");
  316. input.setAttribute('type', 'file');
  317. input.setAttribute('name', this._settings.name);
  318. addStyles(input, {
  319. 'position' : 'absolute',
  320. // in Opera only 'browse' button
  321. // is clickable and it is located at
  322. // the right side of the input
  323. 'right' : 0,
  324. 'margin' : 0,
  325. 'padding' : 0,
  326. 'fontSize' : '480px',
  327. 'cursor' : 'pointer'
  328. });
  329. var div = document.createElement("div");
  330. addStyles(div, {
  331. 'display' : 'block',
  332. 'position' : 'absolute',
  333. 'overflow' : 'hidden',
  334. 'margin' : 0,
  335. 'padding' : 0,
  336. 'opacity' : 0,
  337. // Make sure browse button is in the right side
  338. // in Internet Explorer
  339. 'direction' : 'ltr',
  340. //Max zIndex supported by Opera 9.0-9.2
  341. 'zIndex': 2147483583
  342. });
  343. // Make sure that element opacity exists.
  344. // Otherwise use IE filter
  345. if ( div.style.opacity !== "0") {
  346. if (typeof(div.filters) == 'undefined'){
  347. throw new Error('Opacity not supported by the browser');
  348. }
  349. div.style.filter = "alpha(opacity=0)";
  350. }
  351. addEvent(input, 'change', function(){
  352. if ( ! input || input.value === ''){
  353. return;
  354. }
  355. // Get filename from input, required
  356. // as some browsers have path instead of it
  357. var file = fileFromPath(input.value);
  358. if (false === self._settings.onChange.call(self, file, getExt(file))){
  359. self._clearInput();
  360. return;
  361. }
  362. // Submit form when value is changed
  363. if (self._settings.autoSubmit) {
  364. self.submit();
  365. }
  366. });
  367. addEvent(input, 'mouseover', function(){
  368. addClass(self._button, self._settings.hoverClass);
  369. });
  370. addEvent(input, 'mouseout', function(){
  371. removeClass(self._button, self._settings.hoverClass);
  372. // We use visibility instead of display to fix problem with Safari 4
  373. // The problem is that the value of input doesn't change if it
  374. // has display none when user selects a file
  375. input.parentNode.style.visibility = 'hidden';
  376. });
  377. div.appendChild(input);
  378. document.body.appendChild(div);
  379. this._input = input;
  380. },
  381. _clearInput : function(){
  382. if (!this._input){
  383. return;
  384. }
  385. // this._input.value = ''; Doesn't work in IE6
  386. removeNode(this._input.parentNode);
  387. this._input = null;
  388. this._createInput();
  389. removeClass(this._button, this._settings.hoverClass);
  390. },
  391. /**
  392. * Function makes sure that when user clicks upload button,
  393. * the this._input is clicked instead
  394. */
  395. _rerouteClicks: function(){
  396. var self = this;
  397. // IE will later display 'access denied' error
  398. // if you use using self._input.click()
  399. // other browsers just ignore click()
  400. addEvent(self._button, 'mouseover', function(){
  401. if (self._disabled){
  402. return;
  403. }
  404. if ( ! self._input){
  405. self._createInput();
  406. }
  407. var div = self._input.parentNode;
  408. copyLayout(self._button, div);
  409. div.style.visibility = 'visible';
  410. });
  411. // commented because we now hide input on mouseleave
  412. /**
  413. * When the window is resized the elements
  414. * can be misaligned if button position depends
  415. * on window size
  416. */
  417. //addResizeEvent(function(){
  418. // if (self._input){
  419. // copyLayout(self._button, self._input.parentNode);
  420. // }
  421. //});
  422. },
  423. /**
  424. * Creates iframe with unique name
  425. * @return {Element} iframe
  426. */
  427. _createIframe: function(){
  428. // We can't use getTime, because it sometimes return
  429. // same value in safari :(
  430. var id = getUID();
  431. // We can't use following code as the name attribute
  432. // won't be properly registered in IE6, and new window
  433. // on form submit will open
  434. // var iframe = document.createElement('iframe');
  435. // iframe.setAttribute('name', id);
  436. var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');
  437. // src="javascript:false; was added
  438. // because it possibly removes ie6 prompt
  439. // "This page contains both secure and nonsecure items"
  440. // Anyway, it doesn't do any harm.
  441. iframe.setAttribute('id', id);
  442. iframe.style.display = 'none';
  443. document.body.appendChild(iframe);
  444. return iframe;
  445. },
  446. /**
  447. * Creates form, that will be submitted to iframe
  448. * @param {Element} iframe Where to submit
  449. * @return {Element} form
  450. */
  451. _createForm: function(iframe){
  452. var settings = this._settings;
  453. // We can't use the following code in IE6
  454. // var form = document.createElement('form');
  455. // form.setAttribute('method', 'post');
  456. // form.setAttribute('enctype', 'multipart/form-data');
  457. // Because in this case file won't be attached to request
  458. var form = toElement('<form method="post" enctype="multipart/form-data"></form>');
  459. form.setAttribute('action', settings.action);
  460. form.setAttribute('target', iframe.name);
  461. form.style.display = 'none';
  462. document.body.appendChild(form);
  463. // Create hidden input element for each data key
  464. for (var prop in settings.data) {
  465. if (settings.data.hasOwnProperty(prop)){
  466. var el = document.createElement("input");
  467. el.setAttribute('type', 'hidden');
  468. el.setAttribute('name', prop);
  469. el.setAttribute('value', settings.data[prop]);
  470. form.appendChild(el);
  471. }
  472. }
  473. return form;
  474. },
  475. /**
  476. * Gets response from iframe and fires onComplete event when ready
  477. * @param iframe
  478. * @param file Filename to use in onComplete callback
  479. */
  480. _getResponse : function(iframe, file){
  481. // getting response
  482. var toDeleteFlag = false, self = this, settings = this._settings;
  483. addEvent(iframe, 'load', function(){
  484. if (// For Safari
  485. iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||
  486. // For FF, IE
  487. iframe.src == "javascript:'<html></html>';"){
  488. // First time around, do not delete.
  489. // We reload to blank page, so that reloading main page
  490. // does not re-submit the post.
  491. if (toDeleteFlag) {
  492. // Fix busy state in FF3
  493. setTimeout(function(){
  494. removeNode(iframe);
  495. }, 0);
  496. }
  497. return;
  498. }
  499. var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document;
  500. // fixing Opera 9.26,10.00
  501. if (doc.readyState && doc.readyState != 'complete') {
  502. // Opera fires load event multiple times
  503. // Even when the DOM is not ready yet
  504. // this fix should not affect other browsers
  505. return;
  506. }
  507. // fixing Opera 9.64
  508. if (doc.body && doc.body.innerHTML == "false") {
  509. // In Opera 9.64 event was fired second time
  510. // when body.innerHTML changed from false
  511. // to server response approx. after 1 sec
  512. return;
  513. }
  514. var response;
  515. if (doc.XMLDocument) {
  516. // response is a xml document Internet Explorer property
  517. response = doc.XMLDocument;
  518. } else if (doc.body){
  519. // response is html document or plain text
  520. response = doc.body.innerHTML;
  521. if (settings.responseType && settings.responseType.toLowerCase() == 'json') {
  522. // If the document was sent as 'application/javascript' or
  523. // 'text/javascript', then the browser wraps the text in a <pre>
  524. // tag and performs html encoding on the contents. In this case,
  525. // we need to pull the original text content from the text node's
  526. // nodeValue property to retrieve the unmangled content.
  527. // Note that IE6 only understands text/html
  528. if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') {
  529. response = doc.body.firstChild.firstChild.nodeValue;
  530. }
  531. if (response) {
  532. response = eval("(" + response + ")");
  533. } else {
  534. response = {};
  535. }
  536. }
  537. } else {
  538. // response is a xml document
  539. response = doc;
  540. }
  541. settings.onComplete.call(self, file, response);
  542. // Reload blank page, so that reloading main page
  543. // does not re-submit the post. Also, remember to
  544. // delete the frame
  545. toDeleteFlag = true;
  546. // Fix IE mixed content issue
  547. iframe.src = "javascript:'<html></html>';";
  548. });
  549. },
  550. /**
  551. * Upload file contained in this._input
  552. */
  553. submit: function(){
  554. var self = this, settings = this._settings;
  555. if ( ! this._input || this._input.value === ''){
  556. return;
  557. }
  558. var file = fileFromPath(this._input.value);
  559. // user returned false to cancel upload
  560. if (false === settings.onSubmit.call(this, file, getExt(file))){
  561. this._clearInput();
  562. return;
  563. }
  564. // sending request
  565. var iframe = this._createIframe();
  566. var form = this._createForm(iframe);
  567. // assuming following structure
  568. // div -> input type='file'
  569. removeNode(this._input.parentNode);
  570. removeClass(self._button, self._settings.hoverClass);
  571. form.appendChild(this._input);
  572. form.submit();
  573. // request set, clean up
  574. removeNode(form); form = null;
  575. removeNode(this._input); this._input = null;
  576. // Get response from iframe and fire onComplete event when ready
  577. this._getResponse(iframe, file);
  578. // get ready for next request
  579. this._createInput();
  580. }
  581. };
  582. })();