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