ajaxfileupload.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. /**
  2. * AJAX Upload
  3. * Project page - http://valums.com/ajax-upload/
  4. * Copyright (c) 2008 Andris Valums, http://valums.com
  5. * Licensed under the MIT license (http://valums.com/mit-license/)
  6. */
  7. (function(){
  8. var d = document, w = window;
  9. /**
  10. * Get element by id
  11. */
  12. function get(element){
  13. if (typeof element == "string")
  14. element = d.getElementById(element);
  15. return element;
  16. }
  17. /**
  18. * Attaches event to a dom element
  19. */
  20. function addEvent(el, type, fn){
  21. if (w.addEventListener){
  22. el.addEventListener(type, fn, false);
  23. } else if (w.attachEvent){
  24. var f = function(){
  25. fn.call(el, w.event);
  26. };
  27. el.attachEvent('on' + type, f)
  28. }
  29. }
  30. /**
  31. * Creates and returns element from html chunk
  32. */
  33. var toElement = function(){
  34. var div = d.createElement('div');
  35. return function(html){
  36. div.innerHTML = html;
  37. var el = div.childNodes[0];
  38. div.removeChild(el);
  39. return el;
  40. }
  41. }();
  42. function hasClass(ele,cls){
  43. return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
  44. }
  45. function addClass(ele,cls) {
  46. if (!hasClass(ele,cls)) ele.className += " "+cls;
  47. }
  48. function removeClass(ele,cls) {
  49. var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
  50. ele.className=ele.className.replace(reg,' ');
  51. }
  52. // getOffset function copied from jQuery lib (http://jquery.com/)
  53. if (document.documentElement["getBoundingClientRect"]){
  54. // Get Offset using getBoundingClientRect
  55. // http://ejohn.org/blog/getboundingclientrect-is-awesome/
  56. var getOffset = function(el){
  57. var box = el.getBoundingClientRect(),
  58. doc = el.ownerDocument,
  59. body = doc.body,
  60. docElem = doc.documentElement,
  61. // for ie
  62. clientTop = docElem.clientTop || body.clientTop || 0,
  63. clientLeft = docElem.clientLeft || body.clientLeft || 0,
  64. // In Internet Explorer 7 getBoundingClientRect property is treated as physical,
  65. // while others are logical. Make all logical, like in IE8.
  66. zoom = 1;
  67. if (body.getBoundingClientRect) {
  68. var bound = body.getBoundingClientRect();
  69. zoom = (bound.right - bound.left)/body.clientWidth;
  70. }
  71. if (zoom > 1){
  72. clientTop = 0;
  73. clientLeft = 0;
  74. }
  75. var top = box.top/zoom + (window.pageYOffset || docElem && docElem.scrollTop/zoom || body.scrollTop/zoom) - clientTop,
  76. left = box.left/zoom + (window.pageXOffset|| docElem && docElem.scrollLeft/zoom || body.scrollLeft/zoom) - clientLeft;
  77. return {
  78. top: top,
  79. left: left
  80. };
  81. }
  82. } else {
  83. // Get offset adding all offsets
  84. var getOffset = function(el){
  85. if (w.jQuery){
  86. return jQuery(el).offset();
  87. }
  88. var top = 0, left = 0;
  89. do {
  90. top += el.offsetTop || 0;
  91. left += el.offsetLeft || 0;
  92. }
  93. while (el = el.offsetParent);
  94. return {
  95. left: left,
  96. top: top
  97. };
  98. }
  99. }
  100. function getBox(el){
  101. var left, right, top, bottom;
  102. var offset = getOffset(el);
  103. left = offset.left;
  104. top = offset.top;
  105. right = left + el.offsetWidth;
  106. bottom = top + el.offsetHeight;
  107. return {
  108. left: left,
  109. right: right,
  110. top: top,
  111. bottom: bottom
  112. };
  113. }
  114. /**
  115. * Crossbrowser mouse coordinates
  116. */
  117. function getMouseCoords(e){
  118. // pageX/Y is not supported in IE
  119. // http://www.quirksmode.org/dom/w3c_cssom.html
  120. if (!e.pageX && e.clientX){
  121. // In Internet Explorer 7 some properties (mouse coordinates) are treated as physical,
  122. // while others are logical (offset).
  123. var zoom = 1;
  124. var body = document.body;
  125. if (body.getBoundingClientRect) {
  126. var bound = body.getBoundingClientRect();
  127. zoom = (bound.right - bound.left)/body.clientWidth;
  128. }
  129. return {
  130. x: e.clientX / zoom + d.body.scrollLeft + d.documentElement.scrollLeft,
  131. y: e.clientY / zoom + d.body.scrollTop + d.documentElement.scrollTop
  132. };
  133. }
  134. return {
  135. x: e.pageX,
  136. y: e.pageY
  137. };
  138. }
  139. /**
  140. * Function generates unique id
  141. */
  142. var getUID = function(){
  143. var id = 0;
  144. return function(){
  145. return 'ValumsAjaxUpload' + id++;
  146. }
  147. }();
  148. function fileFromPath(file){
  149. return file.replace(/.*(\/|\\)/, "");
  150. }
  151. function getExt(file){
  152. return (/[.]/.exec(file)) ? /[^.]+$/.exec(file.toLowerCase()) : '';
  153. }
  154. /**
  155. * Cross-browser way to get xhr object
  156. */
  157. var getXhr = function(){
  158. var xhr;
  159. return function(){
  160. if (xhr) return xhr;
  161. if (typeof XMLHttpRequest !== 'undefined') {
  162. xhr = new XMLHttpRequest();
  163. } else {
  164. var v = [
  165. "Microsoft.XmlHttp",
  166. "MSXML2.XmlHttp.5.0",
  167. "MSXML2.XmlHttp.4.0",
  168. "MSXML2.XmlHttp.3.0",
  169. "MSXML2.XmlHttp.2.0"
  170. ];
  171. for (var i=0; i < v.length; i++){
  172. try {
  173. xhr = new ActiveXObject(v[i]);
  174. break;
  175. } catch (e){}
  176. }
  177. }
  178. return xhr;
  179. }
  180. }();
  181. // Please use AjaxUpload , Ajax_upload will be removed in the next version
  182. Ajax_upload = AjaxUpload = function(button, options){
  183. if (button.jquery){
  184. // jquery object was passed
  185. button = button[0];
  186. } else if (typeof button == "string" && /^#.*/.test(button)){
  187. button = button.slice(1);
  188. }
  189. button = get(button);
  190. this._input = null;
  191. this._button = button;
  192. this._disabled = false;
  193. this._submitting = false;
  194. // Variable changes to true if the button was clicked
  195. // 3 seconds ago (requred to fix Safari on Mac error)
  196. this._justClicked = false;
  197. this._parentDialog = d.body;
  198. if (window.jQuery && jQuery.ui && jQuery.ui.dialog){
  199. var parentDialog = jQuery(this._button).parents('.ui-dialog');
  200. if (parentDialog.length){
  201. this._parentDialog = parentDialog[0];
  202. }
  203. }
  204. this._settings = {
  205. // Location of the server-side upload script
  206. action: 'upload.php',
  207. // File upload name
  208. name: 'userfile',
  209. // Additional data to send
  210. data: {},
  211. // Submit file as soon as it's selected
  212. autoSubmit: true,
  213. // The type of data that you're expecting back from the server.
  214. // Html and xml are detected automatically.
  215. // Only useful when you are using json data as a response.
  216. // Set to "json" in that case.
  217. responseType: false,
  218. // Location of the server-side script that fixes Safari
  219. // hanging problem returning "Connection: close" header
  220. closeConnection: '',
  221. // Class applied to button when mouse is hovered
  222. hoverClass: 'hover',
  223. // When user selects a file, useful with autoSubmit disabled
  224. onChange: function(file, extension){},
  225. // Callback to fire before file is uploaded
  226. // You can return false to cancel upload
  227. onSubmit: function(file, extension){},
  228. // Fired when file upload is completed
  229. // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!
  230. onComplete: function(file, response) {}
  231. };
  232. // Merge the users options with our defaults
  233. for (var i in options) {
  234. this._settings[i] = options[i];
  235. }
  236. this._createInput();
  237. this._rerouteClicks();
  238. }
  239. // assigning methods to our class
  240. AjaxUpload.prototype = {
  241. setData : function(data){
  242. this._settings.data = data;
  243. },
  244. disable : function(){
  245. this._disabled = true;
  246. },
  247. enable : function(){
  248. this._disabled = false;
  249. },
  250. // removes instance
  251. destroy : function(){
  252. if(this._input){
  253. if(this._input.parentNode){
  254. this._input.parentNode.removeChild(this._input);
  255. }
  256. this._input = null;
  257. }
  258. },
  259. /**
  260. * Creates invisible file input above the button
  261. */
  262. _createInput : function(){
  263. var self = this;
  264. var input = d.createElement("input");
  265. input.setAttribute('type', 'file');
  266. input.setAttribute('name', this._settings.name);
  267. var styles = {
  268. 'position' : 'absolute'
  269. ,'margin': '-5px 0 0 -175px'
  270. ,'padding': 0
  271. ,'width': '220px'
  272. ,'height': '30px'
  273. ,'fontSize': '14px'
  274. ,'opacity': 0
  275. ,'cursor': 'pointer'
  276. ,'display' : 'none'
  277. ,'zIndex' : 2147483583 //Max zIndex supported by Opera 9.0-9.2x
  278. // Strange, I expected 2147483647
  279. // Doesn't work in IE :(
  280. //,'direction' : 'ltr'
  281. };
  282. for (var i in styles){
  283. input.style[i] = styles[i];
  284. }
  285. // Make sure that element opacity exists
  286. // (IE uses filter instead)
  287. if ( ! (input.style.opacity === "0")){
  288. input.style.filter = "alpha(opacity=0)";
  289. }
  290. this._parentDialog.appendChild(input);
  291. addEvent(input, 'change', function(){
  292. // get filename from input
  293. var file = fileFromPath(this.value);
  294. if(self._settings.onChange.call(self, file, getExt(file)) == false ){
  295. return;
  296. }
  297. // Submit form when value is changed
  298. if (self._settings.autoSubmit){
  299. self.submit();
  300. }
  301. });
  302. // Fixing problem with Safari
  303. // The problem is that if you leave input before the file select dialog opens
  304. // it does not upload the file.
  305. // As dialog opens slowly (it is a sheet dialog which takes some time to open)
  306. // there is some time while you can leave the button.
  307. // So we should not change display to none immediately
  308. addEvent(input, 'click', function(){
  309. self.justClicked = true;
  310. setTimeout(function(){
  311. // we will wait 3 seconds for dialog to open
  312. self.justClicked = false;
  313. }, 2500);
  314. });
  315. this._input = input;
  316. },
  317. _rerouteClicks : function (){
  318. var self = this;
  319. // IE displays 'access denied' error when using this method
  320. // other browsers just ignore click()
  321. // addEvent(this._button, 'click', function(e){
  322. // self._input.click();
  323. // });
  324. var box, dialogOffset = {top:0, left:0}, over = false;
  325. addEvent(self._button, 'mouseover', function(e){
  326. if (!self._input || over) return;
  327. over = true;
  328. box = getBox(self._button);
  329. if (self._parentDialog != d.body){
  330. dialogOffset = getOffset(self._parentDialog);
  331. }
  332. });
  333. // We can't use mouseout on the button,
  334. // because invisible input is over it
  335. addEvent(document, 'mousemove', function(e){
  336. var input = self._input;
  337. if (!input || !over) return;
  338. if (self._disabled){
  339. removeClass(self._button, self._settings.hoverClass);
  340. input.style.display = 'none';
  341. return;
  342. }
  343. var c = getMouseCoords(e);
  344. if ((c.x >= box.left) && (c.x <= box.right) &&
  345. (c.y >= box.top) && (c.y <= box.bottom)){
  346. input.style.top = c.y - dialogOffset.top + 'px';
  347. input.style.left = c.x - dialogOffset.left + 'px';
  348. input.style.display = 'block';
  349. addClass(self._button, self._settings.hoverClass);
  350. } else {
  351. // mouse left the button
  352. over = false;
  353. var check = setInterval(function(){
  354. // if input was just clicked do not hide it
  355. // to prevent safari bug
  356. if (self.justClicked){
  357. return;
  358. }
  359. if ( !over ){
  360. input.style.display = 'none';
  361. }
  362. clearInterval(check);
  363. }, 25);
  364. removeClass(self._button, self._settings.hoverClass);
  365. }
  366. });
  367. },
  368. /**
  369. * Creates iframe with unique name
  370. */
  371. _createIframe : function(){
  372. // unique name
  373. // We cannot use getTime, because it sometimes return
  374. // same value in safari :(
  375. var id = getUID();
  376. // Remove ie6 "This page contains both secure and nonsecure items" prompt
  377. // http://tinyurl.com/77w9wh
  378. var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');
  379. iframe.id = id;
  380. iframe.style.display = 'none';
  381. d.body.appendChild(iframe);
  382. return iframe;
  383. },
  384. /**
  385. * Upload file without refreshing the page
  386. */
  387. submit : function(){
  388. var self = this, settings = this._settings;
  389. if (this._input.value === ''){
  390. // there is no file
  391. return;
  392. }
  393. // get filename from input
  394. var file = fileFromPath(this._input.value);
  395. // execute user event
  396. if (! (settings.onSubmit.call(this, file, getExt(file)) == false)) {
  397. // Create new iframe for this submission
  398. var iframe = this._createIframe();
  399. // Do not submit if user function returns false
  400. var form = this._createForm(iframe);
  401. form.appendChild(this._input);
  402. // A pretty little hack to make uploads not hang in Safari. Just call this
  403. // immediately before the upload is submitted. This does an Ajax call to
  404. // the server, which returns an empty document with the "Connection: close"
  405. // header, telling Safari to close the active connection.
  406. // http://blog.airbladesoftware.com/2007/8/17/note-to-self-prevent-uploads-hanging-in-safari
  407. if (settings.closeConnection && /AppleWebKit|MSIE/.test(navigator.userAgent)){
  408. var xhr = getXhr();
  409. // Open synhronous connection
  410. xhr.open('GET', settings.closeConnection, false);
  411. xhr.send('');
  412. }
  413. form.submit();
  414. d.body.removeChild(form);
  415. form = null;
  416. this._input = null;
  417. // create new input
  418. this._createInput();
  419. var toDeleteFlag = false;
  420. addEvent(iframe, 'load', function(e){
  421. if (// For Safari
  422. iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||
  423. // For FF, IE
  424. iframe.src == "javascript:'<html></html>';"){
  425. // First time around, do not delete.
  426. if( toDeleteFlag ){
  427. // Fix busy state in FF3
  428. setTimeout( function() {
  429. d.body.removeChild(iframe);
  430. }, 0);
  431. }
  432. return;
  433. }
  434. var doc = iframe.contentDocument ? iframe.contentDocument : frames[iframe.id].document;
  435. // fixing Opera 9.26
  436. if (doc.readyState && doc.readyState != 'complete'){
  437. // Opera fires load event multiple times
  438. // Even when the DOM is not ready yet
  439. // this fix should not affect other browsers
  440. return;
  441. }
  442. // fixing Opera 9.64
  443. if (doc.body && doc.body.innerHTML == "false"){
  444. // In Opera 9.64 event was fired second time
  445. // when body.innerHTML changed from false
  446. // to server response approx. after 1 sec
  447. return;
  448. }
  449. var response;
  450. if (doc.XMLDocument){
  451. // response is a xml document IE property
  452. response = doc.XMLDocument;
  453. } else if (doc.body){
  454. // response is html document or plain text
  455. response = doc.body.innerHTML;
  456. if (settings.responseType && settings.responseType.toLowerCase() == 'json'){
  457. // If the document was sent as 'application/javascript' or
  458. // 'text/javascript', then the browser wraps the text in a <pre>
  459. // tag and performs html encoding on the contents. In this case,
  460. // we need to pull the original text content from the text node's
  461. // nodeValue property to retrieve the unmangled content.
  462. // Note that IE6 only understands text/html
  463. if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE'){
  464. response = doc.body.firstChild.firstChild.nodeValue;
  465. }
  466. if (response) {
  467. response = window["eval"]("(" + response + ")");
  468. } else {
  469. response = {};
  470. }
  471. }
  472. } else {
  473. // response is a xml document
  474. var response = doc;
  475. }
  476. settings.onComplete.call(self, file, response);
  477. // Reload blank page, so that reloading main page
  478. // does not re-submit the post. Also, remember to
  479. // delete the frame
  480. toDeleteFlag = true;
  481. // Fix IE mixed content issue
  482. iframe.src = "javascript:'<html></html>';";
  483. });
  484. } else {
  485. // clear input to allow user to select same file
  486. // Doesn't work in IE6
  487. // this._input.value = '';
  488. d.body.removeChild(this._input);
  489. this._input = null;
  490. // create new input
  491. this._createInput();
  492. }
  493. },
  494. /**
  495. * Creates form, that will be submitted to iframe
  496. */
  497. _createForm : function(iframe){
  498. var settings = this._settings;
  499. // method, enctype must be specified here
  500. // because changing this attr on the fly is not allowed in IE 6/7
  501. var form = toElement('<form method="post" enctype="multipart/form-data"></form>');
  502. form.style.display = 'none';
  503. form.action = settings.action;
  504. form.target = iframe.name;
  505. d.body.appendChild(form);
  506. // Create hidden input element for each data key
  507. for (var prop in settings.data){
  508. var el = d.createElement("input");
  509. el.type = 'hidden';
  510. el.name = prop;
  511. el.value = settings.data[prop];
  512. form.appendChild(el);
  513. }
  514. return form;
  515. }
  516. };
  517. })();