// à voir pour ajouter des fonctions plus poussées concernant l'upload e fichiers // https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications // AJAX Class declaration var AJAX_Class = function(){ this.xhr_list = []; this._up_loaded_bytes; this._up_total_bytes; this._down_loaded_bytes; this._down_total_bytes; // if the total size is unknow, these values will contains the upload/download bytes values this._up_overload = 0; this._down_overload = 0; this._up_speed = 0; this._down_speed = 0; this._consecutive_streams = 2; // how many simultaneous ajax streams are allowed at the same time this._busy_stream_count = 0; this._stream_queue = []; this._last_data_time = 0; this._debug = false; }; AJAX_Class.prototype.constructor = AJAX_Class; // these two variables have to be update by PHP script to reflect server the configuration AJAX_Class.max_upload_size = 314572800; // max file upload size AJAX_Class.max_file_uploads = 20; // max number of simultaneous file uploads // variable used in conjunction with the PHP AJAX_Class to identify json variables AJAX_Class.json_list_identifier = '_AJAX_json_decode_list'; AJAX_Class.JSON_OFF = 0; // json data stay as strings AJAX_Class.JSON_ON = 1; // json data are converted to objetcs and arrays AJAX_Class.JSON_ARRAY = 2; // json data are converted, and objects are converted to associative arrays // create a global variable for easy access everywhere var $_a = new AJAX_Class(); AJAX_Class.setFormSubmitter = function(submitDomElement){ if(submitDomElement.form){ submitDomElement.form._submitter = submitDomElement; }else{ return false; } return true; } // convert a raw bytes length into a human readable string AJAX_Class.readableFileSize = function(size){ let cnt = 0; let units = ['bytes','Kb','Mb','Gb','Tb']; while(size > 1024 && cnt < units.length-1){ size /= 1024; cnt ++; } return size.toFixed(2)+' '+units[cnt]; }; // check if the FILE inputs respect the server limitations AJAX_Class.checkFileData = function(domElement){ var lst = []; if(domElement.tagName == 'INPUT' && domElement.type == 'file' && domElement.files){ // if the given dom element is a FILE input, get its files lst = domElement.files; }else if(domElement.tagName == 'FORM'){ // if the given dom element is a FORM, get all the files from the FILE inputs inside this form let elements = domElement.elements; for(let i = 0; i < elements.length ; i++){ if(elements[i].type == 'file'){ for(let f = 0; f < elements[i].files.length ; f++){ lst.push(elements[i].files[f]); } } } } let count = lst.length; let size = 0; for(let i = 0 ; i < count ;i++){ //console.log(lst[i].name); size += lst[i].size; } let message = ''; if(count > AJAX_Class.max_file_uploads){ message += 'Too many files ('+count+')! Max = '+AJAX_Class.max_file_uploads; } if(size > AJAX_Class.max_upload_size){ message += 'Files are to heavy ('+AJAX_Class.readableFileSize(size)+')! Max = '+AJAX_Class.readableFileSize(AJAX_Class.max_upload_size); } if(message !== ''){ alert(message); }else{ if(this._debug){ console.log('Files : ',count,'/',AJAX_Class.max_file_uploads); console.log('Size : ',AJAX_Class.readableFileSize(size),'/',AJAX_Class.readableFileSize(AJAX_Class.max_upload_size)); } } }; // utility functions AJAX_Class.is_object = function(a){ if(a == null) return false; return typeof(a) == "object"; }; AJAX_Class.is_string = function(a){ return typeof(a) == "string"; }; AJAX_Class.is_array = function(a){ return Object.prototype.toString.apply(a) === '[object Array]'; }; AJAX_Class.is_formdata = function(a){ return Object.prototype.toString.apply(a) === '[object FormData]'; }; AJAX_Class.is_function = function(a){ return typeof(a) == "function"; }; // get the first available XHR, if none is found a new one is created AJAX_Class.prototype.getSender = function(){ let sender = null; for(let i = 0 , c = this.xhr_list.length ; i < c ; i++){ if(!this.xhr_list[i].busy){ if(this._debug) console.log('reuse sender ',i); sender = this.xhr_list[i]; break; } } if(sender === null){ if(this._debug) console.log('create sender'); sender = new AJAX_SENDER(); this.xhr_list.push(sender); } return sender; }; // send data according to the settings AJAX_Class.prototype.send = function(settings , delay){ let sender = this.getSender(); if(this.busy_stream_count <= this._consecutive_streams && !delay){ sender.send(settings); return true; }else{ // if too many ajax stream are running, put the current in a waiting queue and run it later sender.setData(settings); if(delay) sender._delayedSendTime = Date.now() + delay; this.queue(sender); return false; } }; AJAX_Class.prototype.queue = function(sender){ this._stream_queue.push(sender); sender._state = AJAX_SENDER.STATE_QUEUED; // try again to run the ajax call in 200ms if(this._queue_timer) clearTimeout(this._queue_timer); this._queue_timer = setTimeout(this._process_queue.bind(this),200); }; AJAX_Class.prototype._process_queue = function(){ if(this._queue_timer) clearTimeout(this._queue_timer); // try to run the next ajax stream if(this._stream_queue.length){ if(this.busy_stream_count <= this._consecutive_streams){ let sender = this._stream_queue[0]; let send = false; if(sender._delayedSendTime){ if(Date.now() >= sender._delayedSendTime){ send = true; } }else{ send = true; } if(send){ sender = this._stream_queue.shift(); sender.send(); } } this._queue_timer = setTimeout(this._process_queue.bind(this),200); } }; // compute the global progress for all the ajax streams running AJAX_Class.prototype.computeProgress = function() { let up_bytes_loaded = 0; let up_bytes_total = 0; let down_bytes_loaded = 0; let down_bytes_total = 0; let up_unknown_loaded = 0; let down_unknown_loaded = 0; this._up_overload = 0; this._up_speed = 0; this._down_speed = 0; this._speed = 0; this._down_zero_loaded = 0; this._down_zero_total = 0; for(let i = 0 , c = this.xhr_list.length ; i < c ; i++){ let x = this.xhr_list[i]; if(x.busy){ this._up_speed += x.up_speed; this._down_speed += x.down_speed; if(x._state == AJAX_SENDER.STATE_UPLOADING) this._speed += x.up_speed; if(x._state == AJAX_SENDER.STATE_DOWNLOADING) this._speed += x.down_speed; if(x.up_progress === null){ up_unknown_loaded += x.up_loaded_bytes; }else{ up_bytes_loaded += x.up_loaded_bytes; up_bytes_total += x.up_total_bytes; } if(x.down_progress === null){ down_unknown_loaded += x.down_loaded_bytes; }else{ down_bytes_loaded += x.down_loaded_bytes; down_bytes_total += x.down_total_bytes; if(x._download_done && x.down_total_bytes == 0){ this._down_zero_loaded ++; this._down_zero_total ++; } } } } if(up_unknown_loaded + up_bytes_loaded > up_bytes_total){ up_bytes_total = up_unknown_loaded + up_bytes_loaded; this._up_overload = up_unknown_loaded; } if(down_unknown_loaded + down_bytes_loaded > down_bytes_total){ down_bytes_total = down_unknown_loaded + down_bytes_loaded; down_bytes_loaded += down_unknown_loaded; this._down_overload = down_unknown_loaded; } this._down_loaded_bytes = down_bytes_loaded; this._down_total_bytes = down_bytes_total; this._up_loaded_bytes = up_bytes_loaded; this._up_total_bytes = up_bytes_total; this._last_data_time = Date.now(); }; Object.defineProperty(AJAX_Class.prototype, 'up_overload', { get: function() { return this._up_overload; } }); Object.defineProperty(AJAX_Class.prototype, 'down_overload', { get: function() { return this._down_overload; } }); Object.defineProperty(AJAX_Class.prototype, 'up_speed', { get: function() { return this._up_speed; } }); Object.defineProperty(AJAX_Class.prototype, 'down_speed', { get: function() { return this._down_speed; } }); Object.defineProperty(AJAX_Class.prototype, 'busy_stream_count', { get: function() { this._busy_stream_count = 0; for(let i = 0 , c = this.xhr_list.length ; i < c ; i++){ if(this.xhr_list[i].busy) this._busy_stream_count++; } return this._busy_stream_count; } }); Object.defineProperty(AJAX_Class.prototype, 'up_progress', { get: function() { if(this._last_data_time == 0 || (Date.now() - this._last_data_time) > 100) this.computeProgress(); return this._up_total_bytes > 0 ? this._up_loaded_bytes / this._up_total_bytes : 0; } }); Object.defineProperty(AJAX_Class.prototype, 'down_progress', { get: function() { if(this._last_data_time == 0 || (Date.now() - this._last_data_time) > 100) this.computeProgress(); return this._down_total_bytes > 0 ? this._down_loaded_bytes / this._down_total_bytes : this._down_zero_total > 0 ? this._down_zero_loaded / this._down_zero_total : 0; } }); Object.defineProperty(AJAX_Class.prototype, 'progress', { get: function() { return (this.up_progress + this.down_progress) * 0.5; } }); Object.defineProperty(AJAX_Class.prototype, 'speed', { get: function() { return this._speed; } }); //**************************************************************************************** var AJAX_SENDER = function(){ var _self = this; this._xhr = new XMLHttpRequest(); this._xhr._AJAX_SENDER = this; this._xhr.upload._AJAX_SENDER = this; this._xhr.addEventListener('loadend',this._onDownLoadend); this._xhr.addEventListener('progress',this._downProgress); this._xhr.upload.addEventListener('loadend',this._onUpLoadend); this._xhr.upload.addEventListener('progress',this._upProgress); this._xhr.addEventListener('readystatechange' , this._readyStateChange); this._state = AJAX_SENDER.STATE_NOTHING; this._reset(); }; AJAX_SENDER.prototype.constructor = AJAX_SENDER; AJAX_SENDER.STATE_NOTHING = 0; AJAX_SENDER.STATE_QUEUED = 1; AJAX_SENDER.STATE_UPLOADING = 2; AJAX_SENDER.STATE_DOWNLOADING = 3; AJAX_SENDER.prototype._readyStateChange = function(event){ switch(this.readyState){ case this.UNSENT: if(this._debug) console.log('init',event); break; case this.OPENED: if(this._debug) console.log('connexion open',event); break; case this.HEADERS_RECEIVED: if(this._debug){ console.log('header received',event); console.log(this.getAllResponseHeaders()); } break; case this.LOADING: if(this._debug) console.log('loading ...',event); break; case this.DONE: if(this._debug) console.log('all done',event); break; } }; AJAX_SENDER.prototype._reset = function(){ this._data = null; this._busy = false; this._action = ''; this._method = 'POST'; this._state = AJAX_SENDER.STATE_NOTHING; this._up_progress = 0; this._down_progress = 0; this._up_total_bytes = 0; this._down_total_bytes = 0; this._up_loaded_bytes = 0; this._down_loaded_bytes = 0; this._current_success_callback = null; this._current_error_callback = null; this._current_down_progress_callback = null; this._current_up_progress_callback = null; this._last_up_time = 0; this._last_down_time = 0; this._up_speed = 0; this._down_speed = 0; this._download_done = false; }; // compute the current upload progression of this sender AJAX_SENDER.prototype._upProgress = function(event){ let target = this._AJAX_SENDER || this; target._state = AJAX_SENDER.STATE_UPLOADING; if(target._last_up_time > 0 && event.loaded != event.total){ let time_offset = Date.now() - target._last_up_time; let bytes_offset = event.loaded - target._up_loaded_bytes; //console.log('up: ',time_offset,' ',event.loaded,'/',event.total); target._up_speed = bytes_offset * (1000 / time_offset); } target._last_up_time = Date.now(); if(target._send_time <= 0) target._send_time = target._last_up_time; target._up_loaded_bytes = event.loaded; if (event.lengthComputable) { target._up_progress = event.loaded / event.total; target._up_total_bytes = event.total; //console.log("UPLOAD : %d%%", this._AJAX_SENDER._up_progress*100); }else{ target._up_progress = null; target._up_total_bytes = null; } }; // compute the current download progression of this sender AJAX_SENDER.prototype._downProgress = function(event){ let target = this._AJAX_SENDER || this; target._state = AJAX_SENDER.STATE_DOWNLOADING; if(target._last_down_time > 0 && event.loaded != event.total){ let time_offset = Date.now() - target._last_down_time; let bytes_offset = event.loaded - target._down_loaded_bytes; //console.log('dn: ',time_offset,' ',event.loaded,'/',event.total); target._down_speed = bytes_offset * (1000 / time_offset); } target._last_down_time = Date.now(); if(target._send_time <= 0) target._send_time = target._last_down_time; target._down_loaded_bytes = event.loaded; if (event.lengthComputable) { target._down_progress = event.loaded / event.total; target._down_total_bytes = event.total; //console.log("DOWNLOAD : %d%%", this._AJAX_SENDER._down_progress*100); }else{ target._down_progress = null; target._down_total_bytes = null; } }; // action = url php script // data = form dom object, formdata object, or and object with variables to send // extra_data = object with additional variables to send // down_progress : progress callback for download // up_progress : progress callback for upload // error : error callback // success : success callback // json_mode: // AJAX_Class.JSON_OFF = 0 = the json string untouched server side // AJAX_Class.JSON_ON = 1 = basic json decode server side : js objects are converted to PHP objects and js arrays to PHP arrays // AJAX_Class.JSON_ARRAY = 2 = (default) automatically convert json strings to PHP associative arrays when an object or an array is sent AJAX_SENDER.prototype.setData = function(settings){ settings = settings || {}; this._reset(); if(!settings.action && settings.url) settings.action = settings.url; settings.action = settings.action || this._action; settings.method = settings.method || this._method; if(settings.json_mode === undefined){ settings.json_mode = AJAX_Class.JSON_ARRAY; } let json_decode_list = []; let json_list_identifier = AJAX_Class.json_list_identifier; if(AJAX_Class.is_object(settings.data)){ if(AJAX_Class.is_formdata(settings.data)){ // ok ! }else if(settings.data.tagName === 'FORM'){ // add form inputs settings.action = settings.action || settings.data.action; settings.method = settings.method || settings.data.method; let only_submitter = false; if(settings.data._submitter){ if(!settings.extra_data) settings.extra_data = {}; settings.extra_data[settings.data._submitter.name] = settings.data._submitter.value; if(settings.data._submitter.getAttribute('data-only')){ only_submitter = true; } } let formData; if(only_submitter){ formData = new FormData(); }else{ formData = new FormData(settings.data); } settings.data = formData; }else{ // add object data let formData = new FormData(); let keys = Object.keys(settings.data); for(let i = 0 , c = keys.length ; i < c ; i++){ let name = keys[i]; let value = settings.data[name]; if(AJAX_Class.is_object(value)){ if(AJAX_Class.is_object(value)){ formData.append( name , JSON.stringify(value)); if(settings.json_mode) json_decode_list.push(name); }else{ formData.append( name , value ); } }else{ formData.append( name, value ); } } settings.data = formData; } // add extra data if(AJAX_Class.is_object(settings.extra_data)){ let keys = Object.keys(settings.extra_data); for(let i = 0 , c = keys.length ; i < c ; i++){ let name = keys[i]; let value = settings.extra_data[name]; if(AJAX_Class.is_object(value) ){ settings.data.append( name , JSON.stringify(value) ); if(settings.json_mode) json_decode_list.push( name ); }else{ settings.data.append( name , value ); } } } if(settings.json_mode){ settings.data.append(json_list_identifier , JSON.stringify(json_decode_list)); settings.data.append(json_list_identifier+'_mode' , settings.json_mode); } } if(settings.dom_target){ if(AJAX_Class.is_function(settings.success)){ this._current_success_callback = settings.success; } this._xhr.addEventListener('load' , this.dom_redirect_success); if(AJAX_Class.is_function(settings.error)){ this._current_error_callback = settings.error; } this._xhr.addEventListener('error' , this.dom_redirect_error); }else{ if(AJAX_Class.is_function(settings.success)){ this._current_success_callback = settings.success; this._xhr.addEventListener('load' , settings.success); } if(AJAX_Class.is_function(settings.error)){ this._current_error_callback = settings.error; this._xhr.addEventListener('error' , settings.error); } } if(AJAX_Class.is_function(settings.up_progress)){ this._current_up_progress_callback = settings.up_progress; this._xhr.upload.addEventListener('progress' , settings.up_progress); } if(AJAX_Class.is_function(settings.down_progress)){ this._current_down_progress_callback = settings.down_progress; this._xhr.addEventListener('progress' , settings.down_progress); } this.busy = true; this._method = settings.method; this._action = settings.action; this._dom_target = DOM_Class.GetDomElement(settings.dom_target); this._data = settings.data; }; AJAX_SENDER.prototype.dom_redirect_success = function(event){ DOM_Class.WriteHtml(this._AJAX_SENDER._dom_target , this.responseText); }; AJAX_SENDER.prototype.dom_redirect_error = function(event){ DOM_Class.WriteHtml(this._AJAX_SENDER._dom_target , this.responseText); }; AJAX_SENDER.prototype.dom_write = function(htmlData){ } AJAX_SENDER.prototype.send = function(settings){ if(settings) this.setData(settings); this._xhr.open(this._method, this._action , true); //this._xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); //this._xhr.setRequestHeader("Content-type", "multipart/form-data"); this._xhr.send(this._data); this._state = AJAX_SENDER.STATE_UPLOADING; this._send_time = Date.now(); } AJAX_SENDER.prototype._onDownLoadend = function(event){ if(this._debug) console.log('DOWN end',event); let xhr = this; let ajax_sender = xhr._AJAX_SENDER; ajax_sender._down_total_bytes = event.total; ajax_sender._down_loaded_bytes = event.loaded; ajax_sender._download_done = true; // compute the average download speed if(ajax_sender._send_time > 0){ ajax_sender._down_speed = event.loaded * (1000 / (Date.now() - ajax_sender._send_time)); } // remove event listeners if(ajax_sender._current_success_callback !== null){ xhr.removeEventListener('load' , ajax_sender._current_success_callback); } if(ajax_sender._current_error_callback !== null){ xhr.removeEventListener('error' , ajax_sender._current_error_callback); } if(ajax_sender._current_down_progress_callback !== null){ // execute the progress call back for a last update ajax_sender._current_down_progress_callback(event); xhr.removeEventListener('progress' , ajax_sender._current_down_progress_callback); } ajax_sender._current_success_callback = null; ajax_sender._current_error_callback = null; ajax_sender._current_down_progress_callback = null; ajax_sender.busy = false; }; AJAX_SENDER.prototype._onUpLoadend = function(event){ if(this._debug) console.log('UP end',event); let xhr = this; let ajax_sender = xhr._AJAX_SENDER; ajax_sender._up_total_bytes = event.total; ajax_sender._up_loaded_bytes = event.loaded; // compute the average upload speed if(ajax_sender._send_time > 0){ ajax_sender._up_speed = event.loaded * (1000 / (Date.now() - ajax_sender._send_time)); } if(ajax_sender._current_up_progress_callback !== null){ // execute the progress call back for a last update ajax_sender._current_up_progress_callback(event); this.removeEventListener('progress' , ajax_sender._current_up_progress_callback); } ajax_sender._current_up_progress_callback = null; // reset time counter for the download step coming after this ajax_sender._send_time = Date.now(); }; Object.defineProperty(AJAX_SENDER.prototype, 'busy', { get: function() { return this._busy; }, set: function(value) { this._busy = value === true; } }); Object.defineProperty(AJAX_SENDER.prototype, 'progress', { get: function() { if(this._up_progress === null || this._down_progress === null) return null; return (this._up_progress + this._down_progress) * 0.5; } }); Object.defineProperty(AJAX_SENDER.prototype, 'up_progress', { get: function() { return this._up_progress; } }); Object.defineProperty(AJAX_SENDER.prototype, 'down_progress', { get: function() { return this._down_progress; } }); Object.defineProperty(AJAX_SENDER.prototype, 'up_loaded_bytes', { get: function() { return this._up_loaded_bytes; } }); Object.defineProperty(AJAX_SENDER.prototype, 'up_total_bytes', { get: function() { return this._up_total_bytes; } }); Object.defineProperty(AJAX_SENDER.prototype, 'down_loaded_bytes', { get: function() { return this._down_loaded_bytes; } }); Object.defineProperty(AJAX_SENDER.prototype, 'down_total_bytes', { get: function() { return this._down_total_bytes; } }); Object.defineProperty(AJAX_SENDER.prototype, 'up_speed', { get: function() { return this._up_speed; } }); Object.defineProperty(AJAX_SENDER.prototype, 'down_speed', { get: function() { return this._down_speed; } });