/* AJAX File upload simple */ /* jshint esversion: 6 */ /* global errorCatch */ /** * CUSTOM SUB CALL FUNCTIONS as passsd on in init call * fileChange(target_file, target_router) * fileBeforeUpload(target_file, target_router) * fileUploaded(target_file, target_router, control_data) * fileUploadError(target_file, target_router, control_data) */ var AFUS_strings = {}; /** * [5-A] show progress circle * Hides the submit button * If a "confirm" button is present hides this one too * !TODO! call external function for additional checks * and move eg confirm buttons check there * @param {String} target_file prefix for elements */ function showProgress(target_file) { // console.log('Show progress circle'); // hide the upload button itself so we don't press twice $('#' + target_file + '-submit').hide(); if ($('#mask-' + target_file).length > 0) { $('#mask-' + target_file).hide(); } else { $('#' + target_file + '-file').hide(); } // check if we have button "confirm" and disable it if ($('#confirm').length > 0) { $('#confirm').prop('disabled', true); } } /** * [7-C] hide progress circle * Show submit button again * If a "confirm" button is present show this one too * !TODO! call external function for additional checks * and move eg confirm buttons check there * @param {String} target_file prefix for elements */ function hideProgress(target_file) { // console.log('Hide progress circle'); // show the upload button again after upload $('#' + target_file + '-submit').show(); if ($('#' + target_file + '-submit-div').length > 0) { $('#' + target_file + '-submit-div').show(); } if ($('#mask-' + target_file).length > 0) { $('#mask-' + target_file).show(); // on mask hide button after upload $('#' + target_file + '-submit').hide(); if ($('#' + target_file + '-submit-div').length > 0) { $('#' + target_file + '-submit-div').hide(); } } else { $('#' + target_file + '-file').show(); } // check if we have button "confirm" and disable it if ($('#confirm').length > 0) { $('#confirm').prop('disabled', false); } } /** * [7-B] upload error * prints upload error messages into the status field * @param {String} target_file prefix for elements * @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 uploadError(target_file, error_message, is_error) { // set if empty if (error_message.length == 0) { error_message.push('[JS Upload Lib] Upload Error'); } // write error message document.getElementById(target_file + '-upload-status').innerHTML = error_message.join('
'); // add error style to upload status if (is_error === true) { $('#' + target_file + '-upload-status').addClass('SubError'); } else { $('#' + target_file + '-upload-status').removeClass('SubError'); } } /** * [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 {String} target_router Router target name * @param {Boolean} auto_submit If set true will pass by submit button click * and automatically upload * @param {Function} fileChange A function with two string parameters * target_file, target_router * Is called on change of -file entry */ function passThroughEvent(target_file, target_router, auto_submit, fileChange) { console.log('[AJAX Uploader: %s] Element exists: %s',target_file, $('#mask-' + target_file + '-file').length); if ($('#mask-' + target_file + '-file').length > 0) { // hide the other elements // hide submit button until file is selected $('#' + target_file + '-file, #mask-' + target_file + '-file, #' + target_file + '-submit, #' + target_file + '-submit-div').hide(); // init wipe upload status $('#' + target_file + '-upload-status').html(''); // write file name to upload status // show submit button $('#' + target_file + '-file').change(function() { $('#mask-' + target_file + '-file').val($('#' + target_file + '-file').val()); $('#' + target_file + '-upload-status').html(document.getElementById(target_file + '-file').files[0].name); $('#' + target_file + '-upload-status').show(); if (auto_submit === false) { $('#' + target_file + '-submit').show(); if ($('#' + target_file + '-submit-div').length > 0) { $('#' + target_file + '-submit-div').show(); } } // file upload changed function if (typeof fileChange === 'function') { fileChange(target_file, target_router); } // if auto submit flaged console.log('AUTO SUBMIT: %s', auto_submit); if (auto_submit === true) { document.getElementById(target_file + '-submit').click(); } }); $('#mask-' + target_file + '-file').click(function() { var $elm = $('#' + target_file + '-file'); if (document.createEvent) { var e = document.createEvent('MouseEvents'); e.initEvent('click', true, true ); $elm.get(0).dispatchEvent(e); } else { $elm.trigger('click'); } return false; }); } } /** * [7-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 {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) * @param {Function} fileUploaded A function with two string and one object parameters * target_file, target_router, data * Is called after the upload has finished successfully */ function postUpload(target_file, target_router, file_info, data, fileUploaded) { console.log('[AJAX Uploader: %s] Post upload: File: %s, URL: %s, Name: %s, Size: %s', target_file, file_info.file_uid, file_info.file_url, file_info.file_name, file_info.file_size); // reset upload file, etc document.getElementById(target_file + '-file').value = ''; // safari issues? document.getElementById(target_file + '-file').type = ''; document.getElementById(target_file + '-file').type = 'file'; // set internal uid document.getElementById(target_file + '-uid').value = file_info.file_uid; // name & size document.getElementById(target_file + '-file_name').value = file_info.file_name; document.getElementById(target_file + '-file_size').value = file_info.file_size_raw; // set thumb here, if we haveone // file_info also holds the output flags show_name, show_size if we want to show additional info // add remove file from here too (submits with uid to remove tmp uploaded file) // clear any previous content document.getElementById(target_file + '-uploaded').innerHTML = ''; var element; // set base CSS prefix if set or use standard one var base_css = file_info.css ? file_info.css : 'image-upload'; if (file_info.file_url) { element = document.createElement('img'); element.src = file_info.file_url; element.id = target_file + '-input-thumbnail'; element.className = base_css + '-img'; document.getElementById(target_file + '-uploaded').appendChild(element); $('#' + target_file + '-uploaded').show(); } // if we have name/size addition if (file_info.show_name || file_info.show_size) { element = document.createElement('div'); element.className = base_css + '-text'; if (file_info.show_name) { element.innerHTML = file_info.file_name; } if (file_info.show_size) { element.innerHTML += ' (' + file_info.file_size + ')'; } document.getElementById(target_file + '-uploaded').appendChild(element); $('#' + target_file + '-uploaded').show(); } if (typeof fileUploaded === 'function') { fileUploaded(target_file, target_router, data); } } /** * [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 supportAjaxUploadWithProgress() { 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 * @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 {String} [target_router=''] value of the action _POST variable, if not set or * if empty use "fileUpload" * NOTE: default = '' has been removed to work with IE11 * @param {String} [target_action=''] override the target_form action field * NOTE: default = '' has been removed to work with IE11 * @param {Object} [form_parameters={}] Key/Value list for additional parameters to add to the form submit. * Added BEFORE fileBeforeUpload parameters * @param {Boolean} [auto_submit=false] if we override the submit button * and directly upload * NOTE: default = false has been removed to work with IE11 * @param {Function} [fileChange=''] Function run on change of -file element entry * Parameters are target_file, target_router * NOTE: default = false has been removed to work with IE11 * @param {Function} [fileBeforeUpload=''] Function called before upload starts * Parameters are target_file, target_router * NOTE: default = false has been removed to work with IE11 * @param {Function} [fileUploaded=''] Function called after upload has successfully finished * Parameters are target_file, target_router, data (object returned from upload function call) * NOTE: default = false has been removed to work with IE11 * @param {Function} [fileUploadError=''] Function called after upload has failed * Parameters are target_file, target_router, data (object returned from upload function call) * NOTE: default = false has been removed to work with IE11 */ function initAjaxUploader(target_file, target_form, target_router, target_action, form_parameters, auto_submit, fileChange, fileBeforeUpload, fileUploaded, fileUploadError) { // Actually confirm support if (supportAjaxUploadWithProgress()) { console.log('[AJAX Uploader: %s] init [%s]', target_file, target_router); if (document.getElementById(target_file + '-submit') !== null) { // 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; }); // set pass through events passThroughEvent(target_file, target_router, auto_submit, fileChange); // Ajax uploads are supported! // Init the Ajax form submission // pass on the target_file and the master form name initFullFormAjaxUpload(target_file, target_form, target_router, target_action, form_parameters, fileBeforeUpload, fileUploaded, fileUploadError); } else { console.log('[AJAX Uploader: %s] Element not found for init', target_file); } } else { console.log('[AJAX Uploader: %s] failed', target_file); } } /** * [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 * @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 {String} target_router value of the action _POST variable * @param {String} target_action override the target_form action field (target file) * @param {Object} form_parameters additional parameters added to the form submit * @param {Function} fileBeforeUpload Function with two string parameters * target_file, target_router * Is called on submit before upload starts * @param {Function} fileUploaded Is passed through to sendXHRequest * @param {Function} fileUploadError Is passed through to sendXHRequest * @return {Boolean} false to not trigger normal form submit */ function initFullFormAjaxUpload(target_file, target_form, target_router, target_action, form_parameters, fileBeforeUpload, fileUploaded, fileUploadError) { // should check that form exists // + '-form' var form = document.getElementById(target_form); if (!form.getAttribute('action') && !target_action) { console.log('!!!!! MISSING FORM ACTION ENTRY'); throw new Error('!!!!! MISSING FORM ACTION ENTRY'); } form.onsubmit = function() { var path = window.location.pathname; // do we end in .php, we need to remove the name then, we just want the path if (path.indexOf('.php') != -1) { // remove trailing filename (all past last / ) path = path.replace(/\w+\.php/, ''); } if (path.substr(-1) != '/') { path += '/'; } // get the form.submitted text (remove -submit) and compare to target_file, // if different overwrite the target file var _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 $('#' + target_file + '-upload-status').removeClass('SubError'); $('#' + target_file + '-upload-status').show(); // progress highlight showProgress(target_file); // console.log('[AJAX Uploader: %s] Start upload', target_file); // create new form var formData = new FormData(); // We send the data where the form wanted var action = form.getAttribute('action'); // in case we have a target action set, we overwirde if (target_action) { action = target_action; } console.log('[AJAX Uploader: %s] ACTION: %s, PATH: %s', target_file, action, path); // add action if (!target_router) { target_router = 'fileUpload'; } formData.append('action', target_router); formData.append('uploadName', target_file + '-file'); // add file only (first file found) with target file name formData.append(target_file + '-file', document.getElementById(target_file + '-file').files[0]); // append file uid if exists formData.append(target_file + '-uid', $('#' + target_file + '-uid').val()); // add additional ones for (const [key, value] of Object.entries(form_parameters)) { formData.append(key, value); } // external data gets added if (typeof fileBeforeUpload === 'function') { for (const [key, value] of Object.entries(fileBeforeUpload(target_file, target_router))) { formData.append(key, value); } } console.log('[AJAX Uploader: %s] Send data to: %s, with path: %s', target_file, action, path); // run translation check, must have at least two basic strings set // if they are not set, set default values here /*if (!Object.prototype.hasOwnProperty.call(AFUS_strings, 'upload_start')) { AFUS_strings.upload_start = 'Upload start'; } if (!Object.prototype.hasOwnProperty.call(AFUS_strings, 'upload_finished')) { AFUS_strings.upload_finished = 'Upload finished'; }*/ // debugger; // Code common to both variants sendXHRequest(target_file, target_router, formData, path + action, fileUploaded, fileUploadError); console.log('[AJAX Uploader: %s] Data sent to: %s', target_file, action); // Avoid normal form submission return false; }; } /** * [5-B] * 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: onloadstartHandler * - progress: onprogressHandler * - end: onloadHandler * - finish: onreadystatechangeHandler * 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 * @param {String} target_file Element name prefix * @param {String} target_router Target router name * @param {Object} formData The form data created in the init full function * @param {String} uri The target uri to where the data should be sent * @param {Function} fileUploaded Is passed through to onreadystatechangeHandler * @param {Function} fileUploadError Is passed through to onreadystatechangeHandler */ function sendXHRequest(target_file, target_router, formData, uri, fileUploaded, fileUploadError) { // Get an XMLHttpRequest instance var xhr = new XMLHttpRequest(); // Set up events xhr.upload.addEventListener('loadstart', onloadstartHandler.bind(null, target_file), false); xhr.upload.addEventListener('progress', onprogressHandler.bind(null, target_file), false); xhr.upload.addEventListener('load', onloadHandler.bind(null, target_file), false); xhr.addEventListener('readystatechange', onreadystatechangeHandler.bind(null, target_file, target_router, fileUploaded, fileUploadError), false); // alternative pass on via target file // xhr.targetFile = target_file; // Set up request xhr.open('POST', uri, true); // Fire! xhr.send(formData); // on error log & try again xhr.onerror = function() { console.log('[AJAX Uploader: %s] upload ERROR', target_file); // try again xhr.open('POST', uri, true); xhr.send(formData); }; console.log('[AJAX Uploader: %s] SEND DATA: %o', target_file, formData); } /** * [6-A] Handle the start of the transmission * @param {String} target_file element name prefix */ function onloadstartHandler(target_file) { // console.log('[AJAX Uploader: %s] start uploading', target_file); // must set dynamic var div = document.getElementById(target_file + '-upload-status'); div.innerHTML = AFUS_strings.upload_start || 'Upload start'; } /** * [6-B] Handle the end of the transmission * @param {String} target_file element name prefix */ function onloadHandler(target_file) { // console.log('[AJAX Uploader: %s] finished uploading', target_file); // must set dynamic var div = document.getElementById(target_file + '-upload-status'); div.innerHTML = AFUS_strings.upload_finished || 'Upload finished'; } /** * [6-C] Handle the progress * calculates percent and show that in the upload status element * @param {String} target_file element name prefix * @param {Event} evt event data for upload progress * holds file size and bytes transmitted */ function onprogressHandler(target_file, evt) { // must set dynamic var div = document.getElementById(target_file + '-upload-status'); var percent = evt.loaded / evt.total * 100; // console.log('[AJAX Uploader: %s] Uploading: %s', target_file, Math.round(percent)); div.innerHTML = Math.round(percent) + '%'; } /** * [6-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 {String} target_router Target router name * @param {Function} fileUploaded Is passed through to postUpload * @param {Function} fileUploadError Is called on error * @param {Event} evt event data for return data and ready state info */ function onreadystatechangeHandler(target_file, target_router, fileUploaded, fileUploadError, evt) { var status, readyState, responseText, responseData; try { readyState = evt.target.readyState; responseText = evt.target.responseText; status = evt.target.status; } catch(e) { errorCatch(e); return; } if (readyState == 4 && status == '200' && responseText) { responseData = JSON.parse(responseText); // must set dynamic console.log('[AJAX Uploader: %s] Uploader response: %s -> %o', target_file, responseData.status, responseData); uploadError(target_file, responseData.content.msg, false); // run post uploader if (responseData.status == 'success') { postUpload(target_file, target_router, { file_uid: responseData.content.file_uid, file_url: responseData.content.file_url, file_size: responseData.content.file_size, file_size_raw: responseData.content.file_size_raw, file_name: responseData.content.file_name, show_name: responseData.content.show_name, show_size: responseData.content.show_size, css: responseData.content.css }, responseData.content, fileUploaded); } else { // uploadError(target_file, responseData.content.msg, true); if (typeof fileUploadError === 'function') { fileUploadError(target_file, target_router, responseData.content); } } hideProgress(target_file); } else { console.log('[AJAX Uploader: %s] ReadyState: %s, status: %s, Text: %o', target_file, readyState, status, responseText); } }