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