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