/* AJAX File upload simple */ // 'use strict'; /* jshint esversion: 6 */ /** * CUSTOM SUB CALL FUNCTIONS as passsd on in init call * run for each uploaded file * fileChange(target_file, file_pos, target_router) * fileChangeAll(target_file, target_router) * fileRemove(target_file, file_pos) * fileClear(target_file) * fileBeforeUploadAll(target_file, target_router) * fileBeforeUpload(target_file, file_pos, target_router) * fileUploaded(target_file, file_pos, target_router, control_data) * fileUploadedAll(target_file, target_router) * fileUploadError(target_file, file_pos, target_router, control_data) */ // translation strings var AFUS_strings = {}; // file lists for each target file var AFUS_file_list = {}; // file functions var AFUS_functions = {}; // config settings var AFUS_config = {}; /** * [from edit.js] * simple sprintf formater for replace * usage: "{0} is cool, {1} is not".format("Alpha", "Beta"); * First, checks if it isn't implemented yet. * @param {String} String.prototype.format string with elements to be replaced * @return {String} Formated string */ if (!String.prototype.format) { String.prototype.format = function() { var args = arguments; return this.replace(/{(\d+)}/g, function(match, number) { return typeof args[number] != 'undefined' ? args[number] : match ; }); }; } /** * [from edit.js] * checks if a key exists in a given object * @param {String} key key name * @param {Object} object object to search key in * @return {Boolean} true/false if key exists in object */ function keyInObject(key, object) { return (Object.prototype.hasOwnProperty.call(object, key)) ? true : false; } /** * [from edit.js] * converts a int number into bytes with prefix in two decimals precision * currently precision is fixed, if dynamic needs check for max/min precision * @param {Number} bytes bytes in int * @return {String} string in GB/MB/KB */ function formatBytes(bytes) { var i = -1; do { bytes = bytes / 1024; i++; } while (bytes > 99); return parseFloat(Math.round(bytes * Math.pow(10, 2)) / Math.pow(10, 2)) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; } /** * [from edit.js] * prints out error messages based on data available from the browser * @param {Object} err error from try/catch block */ function errorCatch(err) { // for FF & Chrome if (err.stack) { // only FF if (err.lineNumber) { console.log('ERROR[%s:%s] %s', err.name, err.lineNumber, err.message); } else if (err.line) { // only Safari console.log('ERROR[%s:%s] %s', err.name, err.line, err.message); } else { console.log('ERROR[%s] %s', err.name, err.message); } // stack trace console.log('ERROR[stack] %s', err.stack); } else if (err.number) { // IE console.log('ERROR[%s:%s] %s', err.name, err.number, err.message); console.log('ERROR[description] %s', err.description); } else { // the rest console.log('ERROR[%s] %s', err.name, err.message); } } /** * [9-A] add one to the running queue * @param {String} target_file prefix for elements * @return {Number} current running queues */ function afusRunningStart(target_file) { AFUS_config[target_file].running ++; return AFUS_config[target_file].running; } /** * [9-B] remove one from the running queue * @param {String} target_file prefix for elements * @return {Number} current running queues */ function afusRunningStop(target_file) { AFUS_config[target_file].running --; if (AFUS_config[target_file].running < 0) { AFUS_config[target_file].running = 0; } return AFUS_config[target_file].running; } /** * [9-C] get current running queue count * @param {String} target_file prefix for elements * @return {Number} current running queues */ function afusRunningGet(target_file) { return AFUS_config[target_file].running; } /** * [8-C] write out the cancel for upload * @param {String} target_file prefix for elements * @param {Number} file_pos position for progress info to clear */ function afusCancelUploadOutput(target_file, file_pos) { document.getElementById(target_file + '-status-progress-' + file_pos).innerHTML = AFUS_strings[target_file].upload_cancled || 'Upload cancled'; document.getElementById(target_file + '-status-progress-' + file_pos).style.display = ''; document.getElementById(target_file + '-status-progress-bar-span-' + file_pos).style.display = 'none'; document.getElementById(target_file + '-status-delete-' + file_pos).style.display = 'none'; document.getElementById(target_file + '-status-abort-' + file_pos).style.display = 'none'; } /** * [8-B] upload error * prints upload error messages into the status field * @param {String} target_file prefix for elements * @param {Number} file_pos position for progress info to clear * @param {Array} error_message error message, if not set standard error is shown * @param {Boolean} is_error if set true, then add error message class */ function afusUploadError(target_file, file_pos, error_message, is_error) { // set if empty if (error_message.length == 0) { error_message.push('[JS Upload Lib] Upload Error'); } // write error message, and show document.getElementById(target_file + '-status-progress-' + file_pos) .innerHTML = error_message.join(', '); // show as block document.getElementById(target_file + '-status-progress-' + file_pos).style.display = ''; document.getElementById(target_file + '-status-progress-bar-span-' + file_pos).style.display = 'none'; // add error style to upload status if (is_error === true) { document.getElementById(target_file + '-status-progress-' + file_pos).classList.add('SubError'); } else { document.getElementById(target_file + '-status-progress-' + file_pos).classList.remove('SubError'); } } /** * [8-A] final step after upload * The last action to be called, fills all the hidden values * - uid * - file_name * - file_size * Inserts thumbnail if url exists * Adds uploaded file name and file size if flags show_name/show_size are set * The function checks for "fileUploaded" and if it exists * it will be called with the "other_data" object * @param {String} target_file Prefix for elements * @param {Number} file_pos Position in upload queue * @param {String} target_router Target router name * @param {Object} file_info Object with all the additional data returned from AJAX * @param {Object} data Full Object set (including file_info) */ function afusPostUpload(target_file, file_pos, target_router, file_info, data) { console.log('[AJAX Uploader: %s] Post upload: File: %s, Name: %s, Size: %s', target_file, file_info.file_uid, file_info.file_name, file_info.file_size); // set internal uid document.getElementById(target_file + '-uid-' + file_pos).value = file_info.file_uid; // append uid, file name, size into uploaded too if (typeof AFUS_functions[target_file].fileUploaded === 'function') { AFUS_functions[target_file].fileUploaded(target_file, file_pos, target_router, data); } } /** * [8-D] afusFinalPostUpload * is called after ALL uploads are finished * @param {String} target_file Prefix for elements */ function afusFinalPostUpload(target_file) { console.log('[AJAX Uploader: %s] Final Post upload', target_file); // reset internal variables afusResetInternalVariables(target_file); // hide abort document.getElementById(target_file + '-abort-all-div').style.display = 'none'; // show clear button again document.getElementById(target_file + '-clear-div').style.display = ''; // show the upload button again after upload document.getElementById(target_file + '-file-div').style.display = ''; // reset file list? document.getElementById(target_file + '-file').value = ''; // safari issues? document.getElementById(target_file + '-file').type = ''; document.getElementById(target_file + '-file').type = 'file'; } /** * [8-E] reset all internal variables variables * @param {String} target_file Prefix for elements */ function afusResetInternalVariables(target_file) { AFUS_file_list[target_file] = []; AFUS_config[target_file].running = 0; AFUS_config[target_file].current_xhr = null; AFUS_config[target_file].current = -1; } /** * [1-A] Validate and init all config options * @param {Object} config Original config object to check * @return {Object} Check and validated config object */ function afusUploaderConfigCheck(config) { // first check if parameter is actualy object if (!(typeof config === 'object' && config !== null)) { config = {}; } // if target file is not set, major abort let empty = false; for (let ent of ['target_file', 'target_form']) { if (!keyInObject(ent, config)) { config[ent] = ''; } if (!(typeof config[ent] === 'string' || config[ent] instanceof String)) { // abort because of false type config[ent] = ''; } // empty flag = abort if (!config[ent]) { empty = true; } } // if one of those two is empty, abort if (empty === true) { console.log('[AJAX Uploader: %s] EMPTY target_file/target_form', config.target_file || '[UNSET TARGET FILE]'); throw new Error('target_file and target_form must be set'); } let target_file = config.target_file; // maximum files allowed to upload at once if (!keyInObject('max_files', config)) { config.max_files = 1; } else { config.max_files = parseInt(config.max_files); // must be between 0 and some reasonable number on upper bound if (config.max_files < 0 || config.max_files > 100) { // should we set to eg 100 as max? config.max_files = 0; } } // maximum file size allowed (in bytes) if (!keyInObject('max_file_size', config)) { config.max_file_size = 0; } else { // TODO: if has M/G/etc multiply by 1024 to bytes // else use as is config.max_file_size = parseInt(config.max_file_size); } config.file_accept = []; // allowed file extensions (eg jpg, gif) if (!keyInObject('allowed_extensions', config)) { config.allowed_extensions = []; } else { // must be array and always lower case // copy to new let _temp_list = config.allowed_extensions; // reset config.allowed_extensions = []; // must be array and always convert to lower case for (let ent of _temp_list) { // if empty remove remove if (ent.length > 0) { config.allowed_extensions.push(ent.toLowerCase()); config.file_accept.push('.' + ent.toLowerCase()); } } } // allowed file types in mime format, image/jpeg, etc if (!keyInObject('allowed_file_types', config)) { config.allowed_file_types = []; } else { // copy to new let _temp_list = config.allowed_file_types; // reset config.allowed_file_types = []; // must be array and always convert to lower case for (let ent of _temp_list) { // if empty remove remove if (ent.length > 0) { config.allowed_file_types.push(ent.toLowerCase()); config.file_accept.push(ent.toLowerCase()); } } } // target router for ajax submit if (!keyInObject('target_router', config)) { config.target_router = ''; } else { // must be string } // ajax form action target name if (!keyInObject('target_action', config)) { config.target_action = ''; } else { // must be string } // any additional parameters to be sent to the server if (!keyInObject('form_parameters', config)) { config.form_parameters = {}; } else if (!( typeof config.form_parameters === 'object' && config.form_parameters !== null )) { // must be object config.form_parameters = {}; } // upload without confirmation step if (!keyInObject('auto_submit', config)) { config.auto_submit = false; } else if (!( config.auto_submit === false || config.auto_submit === true )) { // must be boolean config.auto_submit = false; } // set path name config.path = window.location.pathname; // do we end in .php, we need to remove the name then, we just want the path if (config.path.indexOf('.php') != -1) { // remove trailing filename (all past last / ) config.path = config.path.replace(/\w+\.php/, ''); } if (config.path.substr(-1) != '/') { config.path += '/'; } // write general config things into config AFUS_config[target_file] = {}; for (var ent of [ 'target_file', 'target_form', 'path', 'max_files', 'max_file_size', 'allowed_extensions', 'allowed_file_types', 'target_router', 'target_action', 'form_parameters', 'auto_submit' ]) { AFUS_config[target_file][ent] = config[ent]; } // all files uploaded ok AND accepted by the server flag AFUS_config[target_file].all_success = true; // overall abort flag AFUS_config[target_file].abort = false; // currently running number of uploads AFUS_config[target_file].running = 0; // current running file pos AFUS_config[target_file].current = -1; // current running xhr object AFUS_config[target_file].current_xhr = null; // functions check and set AFUS_functions[target_file] = {}; for (var fkt of [ 'fileChange', 'fileChangeAll', 'fileRemove', 'fileClear', 'fileBeforeUploadAll', 'fileBeforeUpload', 'fileUploaded', 'fileUploadedAll', 'fileUploadError' ]) { if (!keyInObject(fkt, config)) { config[fkt] = ''; } AFUS_functions[target_file][fkt] = config[fkt]; } // init strings for this groups AFUS_strings[target_file] = {}; // if set translation strings, set them to the AFUS string if (keyInObject('translation', config)) { // upload_start, upload_finished, too_many_files only for (var k of [ 'invalid_type', 'invalid_size', 'cancel', 'remove', 'upload_start', 'upload_finished', 'upload_cancled', 'too_many_files' ]) { if (keyInObject(k, config.translation)) { AFUS_strings[target_file][k] = config.translation[k]; } } } return config; } /** * [2] Function that will allow us to know if Ajax uploads are supported * @return {Boolean} true on "ajax file uploaded supported", false on not possible */ function afusSupportAjaxUploadWithProgress() { return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData(); // Is the File API supported? function supportFileAPI() { var fi = document.createElement('INPUT'); fi.type = 'file'; return 'files' in fi; } // Are progress events supported? function supportAjaxUploadProgressEvents() { var xhr = new XMLHttpRequest(); return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload)); } // Is FormData supported? function supportFormData() { return !! window.FormData; } } /** * [1] this has to be called to start the uploader * checks if ajax upload is supported and if yes * checks if we need to set the pass through click event * then calls the final init that loads the form.submit catcher * * for config object, if missing will be inited with default values: * {String} target_file prefix for the element for the file upload * Must be set * {String} target_form the master form in which this element sits * Must be set * {Number} [max_files=1] maximum uploadable files, if 1, multiple will * be removed from input file field, if 0, no limit * {Number} [max_file_size=0] In bytes, maximum file size. If not set unlimited. * 0 is also unlimited * {Array} [allowed_extensions=[]] Allowed file extensions. Without leading '.' * Will be added to the input file accept list * {Array} [allowed_file_types=[]] Allowed file types in mime format * Will be added to the input file accept list * {String} [target_router=''] value of the action _POST variable, if not set or * if empty use "fileUpload" * {String} [target_action=''] override the target_form action field * {Object} [form_parameters={}] Key/Value list for additional parameters to add * to the form submit. * Added BEFORE fileBeforeUpload parameters * {Object} [translation={}] Translated strings pushed into the AFUS_strings object * {Boolean} [auto_submit=false] if we override the submit button and directly upload * {Function} [fileChange=''] Function run on change of -file element entry * Parameters are target_file, file_pos, target_router * {Function} [fileChangeAll=''] Function run on change of -file element entry * after all file changes for each entry are run * Parameters are target_file, target_router * {Function} [fileRemove=''] Called when an upload file is removed from the queue * before upload * Parameters are target_file, file_pos * {Functuon} [fileClear=''] called when clear button is pressed * Parameters are target_file * {Function} [fileBeforeUploadAll=''] Function called before uploads start * Parameters are target_file, target_router * {Function} [fileBeforeUpload=''] Function called before upload starts * Parameters are target_file, file_pos, target_router * {Function} [fileUploaded=''] Function called after upload has successfully finished * Parameters are target_file, file_pos, target_router, data * (object returned from upload function call) * {Function} [fileUploadedAll=''] After all uploads have been done, this one will be called * Parameters are target_file, target_router * {Function} [fileUploadError=''] Function called after upload has failed * Parameters are target_file, file_pos, target_router, data * (object returned from upload function call) * * @param {Object} config Configuration parameters. See explenatuion above */ function initAjaxUploader(config) // eslint-disable-line { let target_file = config.target_file || '[NO TARGET FILE]'; // Actually confirm support if (afusSupportAjaxUploadWithProgress()) { console.log('[AJAX Uploader: %s] init [%s] with %o', target_file, config.target_form || '[NO TARGET FORM]', config); // FAIL with no submit or no mask entry if (document.getElementById(target_file + '-submit') !== null && document.getElementById('mask-' + target_file) !== null ) { // check config block all set config = afusUploaderConfigCheck(config); // console.log('[AJAX Uploader: %s] config cleanup: %o', target_file, config); // remove multiple frmo -files input if (config.max_files == 1) { document.getElementById(target_file + '-file').removeAttribute('multiple'); } // if we have allowed file type add acceept to the file selector if (config.file_accept.length > 0) { document.getElementById(target_file + '-file').setAttribute('accept', config.file_accept.join(',')); } // add click event for the submit buttons to set which one submitted the file document.getElementById(target_file + '-submit').addEventListener('click', function() { this.form.submitted = this.id; }); // clear upload list click action if (document.getElementById(target_file + '-clear') !== null) { document.getElementById(target_file + '-clear').addEventListener('click', function() { console.log('[AJAX Uploader: %s] Clear upload queue list', target_file); // reset array list AFUS_file_list[target_file] = []; // clear and hide status list document.getElementById(target_file + '-upload-status').innerHTML = ''; document.getElementById(target_file + '-upload-status').style.display = 'none'; // hide self document.getElementById(target_file + '-clear-div').style.display = 'none'; // hide submit document.getElementById(target_file + '-submit-div').style.display = 'none'; // hide abort all document.getElementById(target_file + '-abort-all-div').style.display = 'none'; // call clear post if (typeof AFUS_functions[target_file].fileClear === 'function') { AFUS_functions[target_file].fileClear(target_file, config.target_router); } }); } // all abort all uploads if (document.getElementById(target_file + '-abort-all') !== null) { document.getElementById(target_file + '-abort-all').addEventListener('click', function() { console.log('[AJAX Uploader: %s] Abort all uploads, Current: %s | %o', target_file, AFUS_config[target_file].current, AFUS_config[target_file].current_xhr); // set full abort flag AFUS_config[target_file].abort = true; // abort current running xhr progress AFUS_config[target_file].current_xhr.abort(); // cancel current afusCancelUploadOutput(target_file, AFUS_config[target_file].current); // write cancel to all future ones too for (const el of AFUS_file_list[target_file]) { afusCancelUploadOutput(target_file, el.afus_file_pos); } // final show file upload again afusFinalPostUpload(target_file); }); } // set pass through events afusPassThroughEvent( target_file, config.max_files, config.target_router, config.auto_submit ); // Ajax uploads are supported! // Init the Ajax form submission // pass on the target_file and the master form name afusInitFullFormAjaxUpload( target_file, config.target_form, { target_action: config.target_action, target_router: config.target_router, form_parameters: config.form_parameters } ); } else { console.log('[AJAX Uploader: %s] Element -submit and mask- not found for init', target_file); throw new Error('Cannot find element -submit and mask- to init: ' + target_file); } } else { console.log('[AJAX Uploader: %s] failed with missing submit or form capabilities', target_file); throw new Error('Failed with missing submit or form capabilities: ' + target_file); } } /** * [3] pass through event * Checks if a mask- element is present and then attaches * the passthrough event to the internal files button * checks if fileChange variable is a function and calls it * @param {String} target_file Prefix for elements * @param {Number} max_files maximum allowed file check * @param {String} target_router Router target name * @param {Boolean} auto_submit If set true will pass by submit button click * and automatically upload */ function afusPassThroughEvent(target_file, max_files, target_router, auto_submit) { console.log('[AJAX Uploader: %s] Pass through call: %s, Max: %s', target_file, document.getElementById('mask-' + target_file).length, max_files); // hide the other elements // hide submit button until file is selected document.getElementById(target_file + '-file').style.display = 'none'; document.getElementById(target_file + '-submit-div').style.display = 'none'; document.getElementById(target_file + '-clear-div').style.display = 'none'; document.getElementById(target_file + '-abort-all-div').style.display = 'none'; // init wipe upload status document.getElementById(target_file + '-upload-status').innerHTML = ''; // write file name to upload status // show submit button document.getElementById(target_file + '-file').addEventListener('change', function() { // reset internal variables before adding files to the list afusResetInternalVariables(target_file); // upload status reseut document.getElementById(target_file + '-upload-status').innerHTML = ''; var input_files = document.getElementById(target_file + '-file'); // if max allowed is set, then check that we do not have more selected than allowed if (max_files > 0 && input_files.files.length > max_files) { // write error, abort console.log('[AJAX Uploader: %s] More files selected than allowed: %s/%s', target_file, input_files.files.length, max_files); // hide any clear/upload buttons as this is an error document.getElementById(target_file + '-submit-div').style.display = 'none'; document.getElementById(target_file + '-clear-div').style.display = 'none'; document.getElementById(target_file + '-abort-all-div').style.display = 'none'; document.getElementById(target_file + '-upload-status').classList.add('SubError'); document.getElementById(target_file + '-upload-status').innerHTML = (AFUS_strings[target_file].too_many_files || '{0} files selected, but maximum allowed is {1}').format(input_files.files.length, max_files); document.getElementById(target_file + '-upload-status').style.display = ''; // abort return false; } else { document.getElementById(target_file + '-upload-status').classList.remove('SubError'); } var el, el_sub, el_sub_sub; var valid_type = false, valid_size = false; for (let i = 0; i < input_files.files.length; i ++) { console.log('[AJAX Uploader: %s] Queue file %s with %o', target_file, i, input_files.files[i]); // check file type if requested // allow either of those. by extension or file type // Final valid check is done on backend if ( // file extension afusHasExtension(target_file, input_files.files[i].name) || // file type afusHasFileType(target_file, input_files.files[i].type) ) { valid_type = true; } if ( // file size (AFUS_config[target_file].max_file_size == 0 || input_files.files[i].size <= AFUS_config[target_file].max_file_size) ) { valid_size = true; } // we always add a base row, but we skip add to file list for submit // format: // name // progress/info // delete from queue // cancel upload // first set internal file pos if (valid_type === true && valid_size === true) { input_files.files[i].afus_file_pos = i; // push to global file list AFUS_file_list[target_file].push(input_files.files[i]); } // add to upload status list el = document.createElement('div'); el.id = target_file + '-status-' + i; // file pos in queue? would need pos update on delete // file name el_sub = document.createElement('span'); el_sub.id = target_file + '-status-name-' + i; el_sub.innerHTML = input_files.files[i].name; el.appendChild(el_sub); // progress info (just text) el_sub = document.createElement('span'); el_sub.id = target_file + '-status-progress-' + i; el_sub.setAttribute('style', 'display:none;margin-left:5px;'); el.appendChild(el_sub); // Toptional progress info as visual bar el_sub = document.createElement('span'); el_sub.id = target_file + '-status-progress-bar-span-' + i; el_sub.setAttribute('style', 'display:none;margin-left:5px;'); // attach progress element into it el_sub_sub = document.createElement('progress'); el_sub_sub.id = target_file + '-status-progress-bar-' + i; el_sub_sub.max = 100; el_sub_sub.value = 0; el_sub.appendChild(el_sub_sub); el.appendChild(el_sub); // delete queue row, only of we have no auto upload el_sub = document.createElement('span'); el_sub.id = target_file + '-status-delete-' + i; // if invalid types, show error if (valid_type === false || valid_size === false) { el_sub.setAttribute('style', 'margin-left:5px;'); el_sub.classList.add('SubError'); let error_list = []; if (valid_type === false) { error_list.push(AFUS_strings[target_file].invalid_type || 'Invalid file type'); } if (valid_size === false) { error_list.push((AFUS_strings[target_file].invalid_size || 'Maximum file size is {0}').format(formatBytes(AFUS_config[target_file].max_file_size))); } el_sub.innerHTML = error_list.join(', '); } else { // else show remove el_sub.setAttribute('style', 'margin-left:5px;'); // -W083 el_sub.addEventListener('click', function() { // jshint ignore:line AFUS_file_list[target_file].splice(i, 1); console.log('[AJAX Uploader: %s] Remove pre-upload: %s, Length: %s', target_file, i, AFUS_file_list[target_file].length); document.getElementById(target_file + '-status-' + i).remove(); // hide submit button if there are none to upload if (AFUS_file_list[target_file].length == 0) { document.getElementById(target_file + '-submit-div').style.display = 'none'; document.getElementById(target_file + '-clear-div').style.display = 'none'; document.getElementById(target_file + '-abort-all-div').style.display = 'none'; } // delete if (typeof AFUS_functions[target_file].fileRemove === 'function') { AFUS_functions[target_file].fileRemove(target_file, i, target_router); } }); el_sub.innerHTML = AFUS_strings[target_file].remove || 'Remove'; } el.appendChild(el_sub); // for upload abort el_sub = document.createElement('span'); el_sub.id = target_file + '-status-abort-' + i; el_sub.setAttribute('style', 'margin-left:5px;display:none;'); el_sub.innerHTML = AFUS_strings[target_file].cancel || 'Cancel'; el.appendChild(el_sub); // hidden info el_sub = document.createElement('input'); el_sub.id = target_file + '-uid-' + i; el_sub.name = target_file + '-uid-' + i; el_sub.type = 'hidden'; el.appendChild(el_sub); // append full block document.getElementById(target_file + '-upload-status').appendChild(el); // file change update per file if (typeof AFUS_functions[target_file].fileChange === 'function') { AFUS_functions[target_file].fileChange(target_file, i, target_router); } } // file change update after all files processed if (typeof AFUS_functions[target_file].fileChangeAll === 'function') { AFUS_functions[target_file].fileChangeAll(target_file, target_router); } // updated file list render document.getElementById(target_file + '-upload-status').style.display = ''; // show submit if all basic ok if (auto_submit === false && AFUS_file_list[target_file].length > 0) { document.getElementById(target_file + '-submit-div').style.display = ''; } // show clear div document.getElementById(target_file + '-clear-div').style.display = ''; // if auto submit flaged and basic file check ok if (auto_submit === true && AFUS_file_list[target_file].length > 0) { document.getElementById(target_file + '-submit').click(); } }); } /** * [3-A] check if input file name has an allowed extension * @param {String} file_name File name from upload file object * @return {Boolean} True for file extension in list, else False */ function afusHasExtension(target_file, file_name) { if (AFUS_config[target_file].allowed_extensions.length == 0) { return true; } return ( new RegExp( '(' + AFUS_config[target_file].allowed_extensions .join('|') .replace(/\./g, '\\.') + ')$', 'i' ) ).test(file_name); } /** * [3-B] check if input file type has matching file type listed * Note that this must have a full match * @param {String} file_type File type from upload file object * @param {Array} file_types Array of allowed file types (mime) * @return {Boolean} True for valid file, else False */ function afusHasFileType(target_file, file_type) { if (AFUS_config[target_file].allowed_file_types.length == 0) { return true; } // if file tyoe is not set if (file_type.length == 0) { return true; } return AFUS_config[target_file].allowed_file_types.includes(file_type.toLowerCase()); } /** * [4] MAIN CALL for upload * Creates the main ajax send request if a submit on the main form is detected * all the parameters are passed on from the init function * The function will throw an error of not action (target) can be found * From the config object the following parts are used * {String} target_router value of the action _POST variable * {String} target_action override the target_form action field (target file) * {Object} form_parameters additional parameters added to the form submit * @param {String} target_file prefix for the element for the file upload * @param {String} target_form the master form in which this element sits * @param {Object} config config object, without translations and functuons * @return {Boolean} false to not trigger normal form submit */ function afusInitFullFormAjaxUpload(target_file, target_form, config) { // should check that form exists // + '-form' var form = document.getElementById(target_form); if (!form.getAttribute('action') && !config.target_action) { console.log('[%s] !!!!! MISSING FORM ACTION ENTRY: %s', target_file, target_form); throw new Error('MISSING FORM ACTION ENTRY FOR: ' + target_file + ', FORM: ' + target_form); } form.onsubmit = function() { // TARGET FILE is unset because this is an attechd action call // IT WILL have the LAST set target_file if there are MANY set // WE need to grab it from the form.submitted name // get the form.submitted text (remove -submit) and compare to target_file, // if different overwrite the target file let _target_file = form.submitted.split('-')[0]; console.log('[AJAX Uploader: %s] Wanted: %s, Submitted: %s', target_file, _target_file, form.submitted); if (target_file != _target_file) { target_file = _target_file; } // remove previous highlight if set document.getElementById(target_file + '-upload-status').classList.remove('SubError'); document.getElementById(target_file + '-upload-status').style.display = ''; // hide the upload button itself so we don't press twice document.getElementById(target_file + '-file-div').style.display = 'none'; // hide submit button too document.getElementById(target_file + '-submit-div').style.display = 'none'; // hide clear button document.getElementById(target_file + '-clear-div').style.display = 'none'; // show all abort if >1 upload if (AFUS_file_list[target_file].length > 1) { document.getElementById(target_file + '-abort-all-div').style.display = ''; } // call class for promise type upload flow new afusAsyncUploader(target_file, afusSendFile).whenComplete .then( (t) => { // ALL OK console.log('[AJAX Uploader: %s] *** FILE UPLOAD *** GOT OK: %o', target_file, t); }, (t) => { // one failed -> allow reupload? console.log('[AJAX Uploader: %s] *** FILE UPLOAD *** FAILED: %o', target_file, t); } ) .finally(() => { // done; FINAL calls here console.log('[AJAX Uploader: %s] **** FILE UPLOAD FINAL ****', target_file); // post all done function call, only once all files are processed // check overall running queue numbers for this // show uploader select again afusFinalPostUpload(target_file); // if there is a final function, call it if (typeof AFUS_functions[target_file].fileUploadedAll === 'function') { AFUS_functions[target_file].fileUploadedAll(target_file, config.target_router, AFUS_config[target_file].all_success); } }); // Avoid normal form submission return false; }; } /** * [5] class group for handling promise loop uploads */ class afusAsyncUploader { /** * [5] constructor for afusAsyncUploader * @param {String} target_file prefix for the element for the file upload * @param {Function} function_call Function with internal Promise to be called * Is the actual send file function afusSendFile */ constructor(target_file, function_call) { // target file for AFUS_file shift this.target_file = target_file; // additional data to append to the form submit (global) this.form_append = {}; if (typeof AFUS_functions[this.target_file].fileBeforeUploadAll === 'function') { for (const [key, value] of Object.entries(AFUS_functions[this.target_file].fileBeforeUploadAll(this.target_file, AFUS_config[this.target_file].target_router))) { this.form_append[key] = value; } } // the function call (afusSendFile) with promise this.functionCall = function_call; // error flag for upload error this.errorFlag = false; // create a finish promise // this is called on the final iteration run // new afusAsyncUploader(target_file, afusSendFile).whenComplete .... this.whenComplete = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); // call the iterator for files // this is a self caller (recursive) this.iterator(); } /** * [5-a] main iterator function * This one calls the function given in the contructor * which is the actual send file function. * it will wait for a reject/resolve from this function before * launching itself again */ iterator() { // get the first file // call the function with self and the file to upload this.functionCall.call(this, this.target_file, AFUS_file_list[this.target_file].shift(), this.form_append) // if the uploader throws an error, flag it .catch((error) => { // errors console.log('[aAU] ITERATOR: !!!ERROR!!! catch: %s', error); this.errorFlag = true; }) // on final call iterator again if we have files left // or resolve/reject .finally(() => { console.log('[aAU] ITERATOR: finally: %s', AFUS_file_list[this.target_file].length); // after anything if (AFUS_file_list[this.target_file].length > 0) { // must catch/finally! this.iterator(); } else { console.log('[aAU] ITERATOR FINAL CALL: %s', this.errorFlag); // some error case, error: reject(); // else resolve as ok if (this.errorFlag === true) { this.reject('[aAU] ITERATOR FAILED'); } else { this.resolve('[aAU] ITERATOR RESOLVED'); } } }); } } /** * [6] action to create form and send file * @param {String} target_file prefix for the element for the file upload * @param {Object} file File to upload (object) * @param {Object} form_append Additional data to add to the form that gets submitted * In key: value format * @return {Promise} resolve, reject promise group */ function afusSendFile(target_file, file, form_append) { return new Promise((resolve, reject) => { // promise let form = document.getElementById(AFUS_config[target_file].target_form); // this is the actual index, we use this one because the array index can change // after an element gets removed let file_pos = file.afus_file_pos; console.log('[AJAX Uploader: %s/%s] Push submit %o, Abort: %s', target_file, file_pos, file, AFUS_config[target_file].abort); // if abort, reject if (AFUS_config[target_file].abort === true) { // reset file upload list AFUS_file_list[target_file] = []; reject('Abort All,' + target_file + ',' + file_pos); return false; } // set form, etc let formData = new FormData(); // We send the data where the form wanted let action = form.getAttribute('action'); // in case we have a target action set, we overwirde if (AFUS_config[target_file].target_action) { action = AFUS_config[target_file].target_action; } console.log('[AJAX Uploader: %s/%s] ACTION: %s, PATH: %s', target_file, file_pos, action, AFUS_config[target_file].path); // add action if (!AFUS_config[target_file].target_router) { AFUS_config[target_file].target_router = 'fileUpload'; } formData.append('action', AFUS_config[target_file].target_router); formData.append('uploadQueuePos', file_pos); formData.append('uploadQueueMax', AFUS_file_list[target_file].length); formData.append('uploadName', target_file + '-file'); // add file only (first file found) with target file name formData.append(target_file + '-file', file); formData.append(target_file + '-uid-' + file_pos, document.getElementById(target_file + '-uid-' + file_pos).value); // init params -> global -> per file // lower ones overwrite upper ones // add additional ones for (const [key, value] of Object.entries(AFUS_config[target_file].form_parameters)) { formData.append(key, value); } // add global additional data for (const [key, value] of Object.entries(form_append)) { formData.append(key, value); } // external data gets added if (typeof AFUS_functions[target_file].fileBeforeUpload === 'function') { for (const [key, value] of Object.entries(AFUS_functions[target_file].fileBeforeUpload(target_file, file_pos, AFUS_config[target_file].target_router))) { formData.append(key, value); } } console.log('[AJAX Uploader: %s/%s] Send data to: %s, with path: %s', target_file, file_pos, action, AFUS_config[target_file].path); // * Once the FormData instance is ready and we know // * where to send the data, the data gets submitted // * it adds event listeners for start/progress/load and general ready state change // * then it sends the data // * each event listener is called during the stages // * - start: afusOnLoadStartHandler // * - progress: afusOnProgressHandler // * - end: afusOnLoadHandler // * - finish: afusOnReadyStateChangeHandler // * - handler onload for resolve/reject flow only // * Note: on xhr.onerror function is called if an error happens // * and just calls the xhr.send again // * there are some issues on firefox that can trigger this // Get an XMLHttpRequest instance let xhr = new XMLHttpRequest(); let uri = AFUS_config[target_file].path + action; AFUS_config[target_file].current_xhr = xhr; // Set up events for upload progress xhr.upload.addEventListener('loadstart', afusOnLoadStartHandler.bind(null, target_file, file_pos), false); xhr.upload.addEventListener('progress', afusOnProgressHandler.bind(null, target_file, file_pos), false); xhr.upload.addEventListener('load', afusOnLoadHandler.bind(null, target_file, file_pos), false); // main events for loaded/error tracking // same as load level xhr.onload = () => { console.log('[AJAX Uploader: %s/%s] STATE: %s, Status: %s', target_file, file_pos, xhr.readyState, xhr.status); // on 4 + 200 resolve() if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { console.log('[AJAX Uploader: %s/%s] RESOLVED', target_file, file_pos); resolve('Upload OK,' + target_file + ',' + file_pos); } else { console.log('[AJAX Uploader: %s/%s] FAILED', target_file, file_pos); reject('Failed,' + target_file + ',' + file_pos); } }; // this one also gets errors readyState == 0 xhr.addEventListener('readystatechange', afusOnReadyStateChangeHandler.bind(null, target_file, file_pos, AFUS_config[target_file].target_router), false); // on error, open a new connection and try again xhr.onerror = function() { console.log('[AJAX Uploader: %s/%s] upload ERROR', target_file, file_pos); // try again xhr.open('POST', uri); xhr.send(formData); // limit max errors? }; // Set up request xhr.open('POST', uri); // on error log & try again // Fire! xhr.send(formData); // add a abort request on the delete button document.getElementById(target_file + '-status-abort-' + file_pos).innerHTML = AFUS_strings[target_file].cancel || 'Cancel'; document.getElementById(target_file + '-status-abort-' + file_pos).addEventListener('click', function() { // jshint ignore:line if (xhr) { console.log('[AJAX Uploader: %s/%s] Upload cancled', target_file, file_pos); // send abort xhr.abort(); xhr = null; // remove from running count afusRunningStop(target_file); // write cancled text to file row afusCancelUploadOutput(target_file, file_pos); // reject as cancled reject('Cancled,' + target_file + ',' + file_pos); } }); document.getElementById(target_file + '-status-abort-' + file_pos).style.display = ''; console.log('[AJAX Uploader: %s/%s] RUNNING: %s, SEND DATA: %o', target_file, file_pos, afusRunningGet(target_file), formData); console.log('[AJAX Uploader: %s/%s] Data sent to: %s', target_file, file_pos, action); }); } /** * [7-A] Handle the start of the transmission * @param {String} target_file element name prefix * @param {Number} file_pos Position in upload queue */ function afusOnLoadStartHandler(target_file, file_pos) { // console.log('[AJAX Uploader: %s] start uploading', target_file); // add one to the running queue afusRunningStart(target_file); // set current upload AFUS_config[target_file].current = file_pos; // progress init // clear progress info block document.getElementById(target_file + '-status-progress-' + file_pos).style.display = ''; // bar block document.getElementById(target_file + '-status-progress-bar-span-' + file_pos).style.display = ''; // hide delete (or delete) document.getElementById(target_file + '-status-delete-' + file_pos).style.display = 'none'; // must set dynamic document.getElementById(target_file + '-status-progress-' + file_pos).innerHTML = AFUS_strings[target_file].upload_start || 'Upload start'; document.getElementById(target_file + '-status-progress-bar-' + file_pos).value = 0; } /** * [7-B] Handle the end of the transmission * @param {String} target_file element name prefix * @param {Number} file_pos Position in upload queue */ function afusOnLoadHandler(target_file, file_pos) { // console.log('[AJAX Uploader: %s] finished uploading', target_file); // unset current running AFUS_config[target_file].current = -1; // must set dynamic document.getElementById(target_file + '-status-progress-' + file_pos).innerHTML = AFUS_strings[target_file].upload_finished || 'Upload finished'; // hide bar block document.getElementById(target_file + '-status-progress-bar-span-' + file_pos).style.displasy = 'none'; // hide abort document.getElementById(target_file + '-status-abort-' + file_pos).style.display = 'none'; } /** * [7-C] Handle the progress * calculates percent and show that in the upload status element * @param {String} target_file element name prefix * @param {Number} queue_count Amount of files in the upload queue * @param {Event} evt event data for upload progress * holds file size and bytes transmitted */ function afusOnProgressHandler(target_file, file_pos, evt) { // must set dynamic var percent = evt.loaded / evt.total * 100; // console.log('[AJAX Uploader: %s] Uploading: %s', target_file, Math.round(percent)); document.getElementById(target_file + '-status-progress-' + file_pos).innerHTML = Math.round(percent) + '%'; // write progress bar too document.getElementById(target_file + '-status-progress-bar-' + file_pos).value = Math.round(percent); } /** * [7-D] Handle the response from the server * If ready state is 4 it will call the final post upload function on status success * if status is other it will call the upload error function * on all other statii it currently only prints a debug log message * @param {String} target_file Element name prefix * @param {Number} file_pos Position in upload queue * @param {String} target_router Target router name * @param {Event} evt event data for return data and ready state info */ function afusOnReadyStateChangeHandler(target_file, file_pos, target_router, evt) { var status, readyState, responseText, responseData; try { readyState = evt.target.readyState; responseText = evt.target.responseText; status = evt.target.status; } catch(e) { errorCatch(e); return; } // XMLHttpRequest.DONE == 4 if (readyState == 4 && status == '200' && responseText) { responseData = JSON.parse(responseText); // upload finished afusRunningStop(target_file); // must set dynamic console.log('[AJAX Uploader ORSC: %s/%s] Running: %s, Uploader response: %s -> %o', target_file, file_pos, afusRunningGet(target_file), responseData.status, responseData); // then run status output afusUploadError(target_file, file_pos, responseData.content.msg, responseData.status == 'success' ? false : true); // run post uploader if (responseData.status == 'success') { afusPostUpload(target_file, file_pos, target_router, { file_uid: responseData.content.file_uid, file_size: responseData.content.file_size, file_size_raw: responseData.content.file_size_raw, file_name: responseData.content.file_name }, responseData.content); } else { // one file failed, we global flag not all ok AFUS_config[target_file].all_success = false; // call per file upload error function if exsts if (typeof AFUS_functions[target_file].fileUploadError === 'function') { AFUS_functions[target_file].fileUploadError(target_file, file_pos, target_router, responseData.content); } } } else { console.log('[AJAX Uploader ORSC: %s/%s] Running: %s, ReadyState: %s, status: %s, Text: %o', target_file, file_pos, afusRunningGet(target_file), readyState, status, responseText); // return fail for error } } // __END__