]> git.mxchange.org Git - friendica.git/blob - view/js/ajaxupload.js
Issue 6394: The contact location has to be BBEncoded
[friendica.git] / view / 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             div.setAttribute('class', 'ajaxbutton-wrapper');
385             addStyles(div, {
386                 'display' : 'block',
387                 'position' : 'absolute',
388                 'overflow' : 'hidden',
389                 'margin' : 0,
390                 'padding' : 0,                
391                 'opacity' : 0,
392                 // Make sure browse button is in the right side
393                 // in Internet Explorer
394                 'direction' : 'ltr',
395                 //Max zIndex supported by Opera 9.0-9.2
396                 'zIndex': 2147483583,
397                                 'cursor' : 'pointer'
398
399             });
400             
401             // Make sure that element opacity exists.
402             // Otherwise use IE filter            
403             if ( div.style.opacity !== "0") {
404                 if (typeof(div.filters) == 'undefined'){
405                     throw new Error('Opacity not supported by the browser');
406                 }
407                 div.style.filter = "alpha(opacity=0)";
408             }            
409             
410             addEvent(input, 'change', function(){
411                  
412                 if ( ! input || input.value === ''){                
413                     return;                
414                 }
415                             
416                 // Get filename from input, required                
417                 // as some browsers have path instead of it          
418                 var file = fileFromPath(input.value);
419                                 
420                 if (false === self._settings.onChange.call(self, file, getExt(file))){
421                     self._clearInput();                
422                     return;
423                 }
424                 
425                 // Submit form when value is changed
426                 if (self._settings.autoSubmit) {
427                     self.submit();
428                 }
429             });            
430
431             addEvent(input, 'mouseover', function(){
432                 addClass(self._button, self._settings.hoverClass);
433             });
434             
435             addEvent(input, 'mouseout', function(){
436                 removeClass(self._button, self._settings.hoverClass);
437                 removeClass(self._button, self._settings.focusClass);
438                 
439                 // We use visibility instead of display to fix problem with Safari 4
440                 // The problem is that the value of input doesn't change if it 
441                 // has display none when user selects a file           
442                 input.parentNode.style.visibility = 'hidden';
443
444             });   
445                         
446             addEvent(input, 'focus', function(){
447                 addClass(self._button, self._settings.focusClass);
448             });
449             
450             addEvent(input, 'blur', function(){
451                 removeClass(self._button, self._settings.focusClass);
452             });
453             
454                 div.appendChild(input);
455             document.body.appendChild(div);
456               
457             this._input = input;
458         },
459         _clearInput : function(){
460             if (!this._input){
461                 return;
462             }            
463                              
464             // this._input.value = ''; Doesn't work in IE6                               
465             removeNode(this._input.parentNode);
466             this._input = null;                                                                   
467             this._createInput();
468             
469             removeClass(this._button, this._settings.hoverClass);
470             removeClass(this._button, this._settings.focusClass);
471         },
472         /**
473          * Function makes sure that when user clicks upload button,
474          * the this._input is clicked instead
475          */
476         _rerouteClicks: function(){
477             var self = this;
478             
479             // IE will later display 'access denied' error
480             // if you use using self._input.click()
481             // other browsers just ignore click()
482
483             addEvent(self._button, 'mouseover', function(){
484                 if (self._disabled){
485                     return;
486                 }
487                                 
488                 if ( ! self._input){
489                         self._createInput();
490                 }
491                 
492                 var div = self._input.parentNode;                            
493                 copyLayout(self._button, div);
494                 div.style.visibility = 'visible';
495                                 
496             });
497             
498             
499             // commented because we now hide input on mouseleave
500             /**
501              * When the window is resized the elements 
502              * can be misaligned if button position depends
503              * on window size
504              */
505             //addResizeEvent(function(){
506             //    if (self._input){
507             //        copyLayout(self._button, self._input.parentNode);
508             //    }
509             //});            
510                                          
511         },
512         /**
513          * Creates iframe with unique name
514          * @return {Element} iframe
515          */
516         _createIframe: function(){
517             // We can't use getTime, because it sometimes return
518             // same value in safari :(
519             var id = getUID();            
520              
521             // We can't use following code as the name attribute
522             // won't be properly registered in IE6, and new window
523             // on form submit will open
524             // var iframe = document.createElement('iframe');
525             // iframe.setAttribute('name', id);                        
526  
527             var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');
528             // src="javascript:false; was added
529             // because it possibly removes ie6 prompt 
530             // "This page contains both secure and nonsecure items"
531             // Anyway, it doesn't do any harm.            
532             iframe.setAttribute('id', id);
533             
534             iframe.style.display = 'none';
535             document.body.appendChild(iframe);
536             
537             return iframe;
538         },
539         /**
540          * Creates form, that will be submitted to iframe
541          * @param {Element} iframe Where to submit
542          * @return {Element} form
543          */
544         _createForm: function(iframe){
545             var settings = this._settings;
546                         
547             // We can't use the following code in IE6
548             // var form = document.createElement('form');
549             // form.setAttribute('method', 'post');
550             // form.setAttribute('enctype', 'multipart/form-data');
551             // Because in this case file won't be attached to request                    
552             var form = toElement('<form method="post" enctype="multipart/form-data"></form>');
553                         
554             form.setAttribute('action', settings.action);
555             form.setAttribute('target', iframe.name);                                   
556             form.style.display = 'none';
557             document.body.appendChild(form);
558             
559             // Create hidden input element for each data key
560             for (var prop in settings.data) {
561                 if (settings.data.hasOwnProperty(prop)){
562                     var el = document.createElement("input");
563                     el.setAttribute('type', 'hidden');
564                     el.setAttribute('name', prop);
565                     el.setAttribute('value', settings.data[prop]);
566                     form.appendChild(el);
567                 }
568             }
569             return form;
570         },
571         /**
572          * Gets response from iframe and fires onComplete event when ready
573          * @param iframe
574          * @param file Filename to use in onComplete callback 
575          */
576         _getResponse : function(iframe, file){            
577             // getting response
578             var toDeleteFlag = false, self = this, settings = this._settings;   
579                
580             addEvent(iframe, 'load', function(){                
581                 
582                 if (// For Safari 
583                     iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||
584                     // For FF, IE
585                     iframe.src == "javascript:'<html></html>';"){                                                                        
586                         // First time around, do not delete.
587                         // We reload to blank page, so that reloading main page
588                         // does not re-submit the post.
589                         
590                         if (toDeleteFlag) {
591                             // Fix busy state in FF3
592                             setTimeout(function(){
593                                 removeNode(iframe);
594                             }, 0);
595                         }
596                                                 
597                         return;
598                 }
599                 
600                 var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document;
601                 
602                 // fixing Opera 9.26,10.00
603                 if (doc.readyState && doc.readyState != 'complete') {
604                    // Opera fires load event multiple times
605                    // Even when the DOM is not ready yet
606                    // this fix should not affect other browsers
607                    return;
608                 }
609                 
610                 // fixing Opera 9.64
611                 if (doc.body && doc.body.innerHTML == "false") {
612                     // In Opera 9.64 event was fired second time
613                     // when body.innerHTML changed from false 
614                     // to server response approx. after 1 sec
615                     return;
616                 }
617                 
618                 var response;
619                 
620                 if (doc.XMLDocument) {
621                     // response is a xml document Internet Explorer property
622                     response = doc.XMLDocument;
623                 } else if (doc.body){
624                     // response is html document or plain text
625                     response = doc.body.innerHTML;
626                     
627                     if (settings.responseType && settings.responseType.toLowerCase() == 'json') {
628                         // If the document was sent as 'application/javascript' or
629                         // 'text/javascript', then the browser wraps the text in a <pre>
630                         // tag and performs html encoding on the contents.  In this case,
631                         // we need to pull the original text content from the text node's
632                         // nodeValue property to retrieve the unmangled content.
633                         // Note that IE6 only understands text/html
634                         if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') {
635                             doc.normalize();
636                             response = doc.body.firstChild.firstChild.nodeValue;
637                         }
638                         
639                         if (response) {
640                             response = eval("(" + response + ")");
641                         } else {
642                             response = {};
643                         }
644                     }
645                 } else {
646                     // response is a xml document
647                     response = doc;
648                 }
649                 
650                 settings.onComplete.call(self, file, response);
651                 
652                 // Reload blank page, so that reloading main page
653                 // does not re-submit the post. Also, remember to
654                 // delete the frame
655                 toDeleteFlag = true;
656                 
657                 // Fix IE mixed content issue
658                 iframe.src = "javascript:'<html></html>';";
659             });            
660         },        
661         /**
662          * Upload file contained in this._input
663          */
664         submit: function(){                        
665             var self = this, settings = this._settings;
666             
667             if ( ! this._input || this._input.value === ''){                
668                 return;                
669             }
670                                     
671             var file = fileFromPath(this._input.value);
672             
673             // user returned false to cancel upload
674             if (false === settings.onSubmit.call(this, file, getExt(file))){
675                 this._clearInput();                
676                 return;
677             }
678             
679             // sending request    
680             var iframe = this._createIframe();
681             var form = this._createForm(iframe);
682             
683             // assuming following structure
684             // div -> input type='file'
685             removeNode(this._input.parentNode);            
686             removeClass(self._button, self._settings.hoverClass);
687             removeClass(self._button, self._settings.focusClass);
688                         
689             form.appendChild(this._input);
690                         
691             form.submit();
692
693             // request set, clean up                
694             removeNode(form); form = null;                          
695             removeNode(this._input); this._input = null;            
696             
697             // Get response from iframe and fire onComplete event when ready
698             this._getResponse(iframe, file);            
699
700             // get ready for next request            
701             this._createInput();
702         }
703     };
704 })();