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