PageRenderTime 289ms CodeModel.GetById 101ms app.highlight 91ms RepoModel.GetById 90ms app.codeStats 0ms

/js/lib/ajaxupload.js

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