From 32455459daad76c1c4b1360b73aa9b57aab7a3a0 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 12 Oct 2021 08:55:21 +0900 Subject: [PATCH] AJAX simple file uploader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A simple javascript powered file uploader with progress and hooks support. Was created as a loose replacement for FineUploader. Currently only works with local uploads Below is the log history from the previous folder: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit 6ea59f91314be8b2b936920d4b0a3f04932c9e73 ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Thu Jun 3 10:31:54 2021 +0900 AJAX file uploader code clean up Add global abort and connected clean up of function calls and element show/hide blocks Add simple progress bar for upload (in connection with percent upload) Move running AFUS object into the config object Added new config object entries for this: - abort: flagged true if global abort is triggered - running: moved from AFUS_running into config, how many uploads are currently running - current: current file_pos running (that is queue position in array) - current_xhr: current XHR upload object, used for abort calls ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit b061008ca0b496f6157b8f073d7ca14abb469ef5 ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Thu May 27 11:51:47 2021 +0900 Update AJAX file uploader to work sequential Rewrite flow in ajax uploader to use promsies to launch a new upload only if the previous upload was finished (in any way) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit a2ec369a8cae65e216fe402ac8b5d106e3db99af ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Thu May 20 11:48:38 2021 +0900 Updated ajax simple file uploader to allow multiple file uploads ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit 66878f89c82fe914031e113e780fd72e9ff09b78 ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Mon May 10 09:13:25 2021 +0900 Excel vendor sub module, ajax delay test, ajax file upload fix, others ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit e4efbbfdf843d8b2e53e4d3c873799b839875721 ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Fri Apr 9 13:44:28 2021 +0900 ajax file upload updates, byte format test fixes, ssh2 test fixes, array append test add, nested array recursive walk test add ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit 88734ebaf7420a704a538f0a923ce6996b0b5237 ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Mon Jan 25 17:15:14 2021 +0900 Add ajax uploader on error external function call Like post success uploaded, this function is called on any "non success" return falue. Passes on the full returned ajax data object ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit 2098fe53d510c03f291eaa5c4b2aefd60c24448e ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Wed Jan 13 06:29:20 2021 +0900 Add additional parameters method parameter Added on init of form, when submitted before additional external form parameters function is called ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit 0c2211e2312abf368c4151dbdf54d4fa96a121dc ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Tue Jan 12 10:09:42 2021 +0900 AJAX File uploader change for external function call. External functions are passed into the uploader as parameters. With this we can have unique functions for each init ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit c1eb15e30df319db1d5104bc5e4ab0cd8bb7cc8c ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Fri Jan 8 09:59:33 2021 +0900 AJAX file uploader target action router value dynamic setting update, other test php files updates and additions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit 429af6db892d6968ad87a3db662f3ff1a9d74270 ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━ Author: Clemens Schwaighofer Date: Thu Nov 5 11:33:30 2020 +0900 AJAX file uploader test code Simple single file AJAX file uploader with backend code sample. Work in progress with open - better, simpler frontend code insert part - multiple file upload - flag auto handling zip files (or tar, etc) - make it not rely on Jquery at all --- ReadMe.md | 185 +++++ src/ajaxFileUploadSimple.OLD.js | 532 ++++++++++++ src/ajaxFileUploadSimple.js | 1222 +++++++++++++++++++++++++++ test/backend.php | 336 ++++++++ test/edit.jq.js | 1376 +++++++++++++++++++++++++++++++ test/edit.js | 1 + test/index.html | 108 +++ test/jquery-3.6.0.min.js | 2 + test/jquery.min.js | 1 + test/log/.gitignore | 2 + test/multiple_files_test.html | 66 ++ test/other.css | 140 ++++ test/other.js | 382 +++++++++ test/uploaded/.gitignore | 2 + 14 files changed, 4355 insertions(+) create mode 100755 ReadMe.md create mode 100755 src/ajaxFileUploadSimple.OLD.js create mode 100755 src/ajaxFileUploadSimple.js create mode 100644 test/backend.php create mode 100644 test/edit.jq.js create mode 120000 test/edit.js create mode 100644 test/index.html create mode 100644 test/jquery-3.6.0.min.js create mode 120000 test/jquery.min.js create mode 100644 test/log/.gitignore create mode 100755 test/multiple_files_test.html create mode 100644 test/other.css create mode 100644 test/other.js create mode 100644 test/uploaded/.gitignore diff --git a/ReadMe.md b/ReadMe.md new file mode 100755 index 0000000..ecc819a --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,185 @@ +# AJAX simple file upload + +Upload multiple files with progress. Has hooks for pre/post work + +## How to use + +include `ajaxFileUploadSimeple.js` + +## How to run + +init with `initAjaxUploader(ConfigObject)` + +Where the config object can have the following settings + +## Config Object settiings + +### {String} target_file + +Must be set +prefix for the element for the file upload + +### {String} target_form + +Must be set +the master form in which this element sits + +### {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) + +## Method call flow + +```txt +[**1**] initAjaxUploader +[1 run 2] check if browser is capable for uploader +[1 run 1-A] run config check +[1 listen 8-C] cancel upload output +[1 run 3] afusPassThroughEvent +[1 run 4] run main file uploader function attachment +``` + +```txt +[1-A] afusUploaderConfigCheck check and clean config +``` + +```txt +[**2**] afusSupportAjaxUploadWithProgress +``` + +```txt +[**3**] afusPassThroughEvent +[3 set 1] change event on input file +[3 call 3-A] file extension check +[3 call 3-B] file type check +[3 set 2] sets upload queue delete in fileChange function +[3 ext 2-1] fileRemove function +[3 ext] fileChange function +[3 ext] fileChangeAll +[3 set 3] call click submit if auto submit +``` + +```txt +[3-A] afusHasExtension +[3-B] afusHasFileType +``` + +```txt +[**4**] afusInitFullFormAjaxUpload +[4 set 1] on submit function +[4 ext] fileBeforeUploadAll function +[4 run 6] afusSendFile main file send (promise) +[4 run 5] call class for async submitter iterator +[4 on finally call 8-C] finally promise: afusFinalPostUpload +[4 on finally ext] finally promise: call fileUploadedAll external +``` + +```txt +[**5**] class afusAsyncUploader +[5-A] class constructor for init +[5-B] iterator internal loop method for processing files +``` + +```txt +[**6**] afusSendFile +[6 ext] fileBeforeUpload function +[6] main xhr uploader start +[6 set 7-A] loadstart event afusOnloadStartHandler +[6 set 7-C] progress event afusOnProgressHandler +[6 set 7-B] load event afusOnloadHandler +[6 set 7-D] readystatechange event afusOnReadyStateChangeHandler +``` + +```txt +[7-A] afusOnloadStartHandler +[7-A call 9-A] add +1 to running +[7-B] afusOnloadHandler +[7-C] afusOnProgressHandler +[7-D] afusOnReadyStateChangeHandler +[7-D call 9-B] -1 for running queue count +[7-D call 8-B] afusUploadError +[7-D call 8-A] if success afusPostUpload +[7-D ext] if error fileUploadError function +``` + +```txt +[8-A] afusPostUpload +[8-B] afusUploadError +[8-C] afusCancelUploadOutput +[8-D] afusFinalPostUpload +[8-E] afusResetInternalVariables +``` + +```txt +[9-A] afusRunningStart +[9-B] afusRunningStop +[9-C] afusRunningGet +``` diff --git a/src/ajaxFileUploadSimple.OLD.js b/src/ajaxFileUploadSimple.OLD.js new file mode 100755 index 0000000..763495e --- /dev/null +++ b/src/ajaxFileUploadSimple.OLD.js @@ -0,0 +1,532 @@ +/* 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); + } +} diff --git a/src/ajaxFileUploadSimple.js b/src/ajaxFileUploadSimple.js new file mode 100755 index 0000000..f7db367 --- /dev/null +++ b/src/ajaxFileUploadSimple.js @@ -0,0 +1,1222 @@ +/* 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__ diff --git a/test/backend.php b/test/backend.php new file mode 100644 index 0000000..b8836ee --- /dev/null +++ b/test/backend.php @@ -0,0 +1,336 @@ += 1, round that size + $string .= substr(number_format(round($microtime, $set_microtime), $set_microtime), 1); + return $string; +} + +function errMsg(string $level, string $message): void +{ + global $error_string; + + $error_string[] = [ + 'level' => $level, + 'str' => $message + ]; +} + +/** + * [printAr description] + * @param array $array [description] + * @return string [description] + */ +function printAr(array $array): string +{ + return "
" . print_r($array, true) . "
"; +} + +/** + * helper function for PHP file upload error messgaes to messge string + * @param int $error_code integer _FILE upload error code + * @return string message string, translated + */ +function fileUploadErrorMessage(int $error_code): string +{ + switch ($error_code) { + case UPLOAD_ERR_INI_SIZE: + $message = 'The uploaded file exceeds the upload_max_filesize directive in php.ini'; + break; + case UPLOAD_ERR_FORM_SIZE: + $message = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'; + break; + case UPLOAD_ERR_PARTIAL: + $message = 'The uploaded file was only partially uploaded'; + break; + case UPLOAD_ERR_NO_FILE: + $message = 'No file was uploaded'; + break; + case UPLOAD_ERR_NO_TMP_DIR: + $message = 'Missing a temporary folder'; + break; + case UPLOAD_ERR_CANT_WRITE: + $message = 'Failed to write file to disk'; + break; + case UPLOAD_ERR_EXTENSION: + $message = 'File upload stopped by extension'; + break; + default: + $message = 'Unknown upload error'; + break; + } + return $message; +} + +/** + * creates psuedo random uuid v4 + * Code take from class here: + * https://www.php.net/manual/en/function.uniqid.php#94959 + * @return string pseudo random uuid v4 + */ +function uuidv4(): string +{ + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + // 32 bits for "time_low" + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + // 16 bits for "time_mid" + mt_rand(0, 0xffff), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand(0, 0x0fff) | 0x4000, + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand(0, 0x3fff) | 0x8000, + // 48 bits for "node" + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) + ); +} + +/** + * @param string|int|float $bytes bytes as string int or pure int + * @return string converted byte number (float) with suffix + */ +function humanReadableByteFormat($bytes): string +{ + // if not numeric, return as is + if (is_numeric($bytes)) { + // space before name + $space = true; + // use sprintf instead of round + $adjust = true; + // use SI 1000 mod and not 1024 mod + $si = false; + + // si or normal + $unit = $si ? 1000 : 1024; + // always positive + $abs_bytes = $bytes == PHP_INT_MIN ? PHP_INT_MAX : abs($bytes); + // smaller than unit is always B + if ($abs_bytes < $unit) { + return $bytes . 'B'; + } + // labels in order of size [Y, Z] + $labels = array('', 'K', 'M', 'G', 'T', 'P', 'E'); + // exp position calculation + $exp = floor(log($abs_bytes, $unit)); + // avoid printing out anything larger than max labels + if ($exp >= count($labels)) { + $exp = count($labels) - 1; + } + // deviation calculation + $dev = pow($unit, $exp) * ($unit - 0.05); + // shift the exp +1 for on the border units + if ( + $exp < 6 && + $abs_bytes > ($dev - (((int)$dev & 0xfff) == 0xd00 ? 52 : 0)) + ) { + $exp++; + } + // label name, including leading space if flagged + $pre = ($space ? ' ' : '') . ($labels[$exp] ?? '>E') . ($si ? 'i' : '') . 'B'; + $bytes_calc = $abs_bytes / pow($unit, $exp); + if ($adjust) { + return sprintf("%.2f%s", $bytes_calc, $pre); + } else { + return round($bytes_calc, 2) . $pre; + } + } else { + // if anything other return as string + return (string)$bytes; + } +} + +$error = false; +$status = 'error'; +$error_string = []; +$ajax_data = []; +$folder = DIR . 'uploaded' . DS; + +logWrite(print_r($_POST, true)); + +// backend receiver calls +if (isset($_POST['action'])) { + // action switch statement + switch ($_POST['action']) { + case 'chainAction': + $ajax_data['chain'] = 'Just A chain action: ' . ($_POST['chain'] ?? '-'); + $status = 'ok'; + errMsg($status, 'Successful chained action'); + break; + // list current files + case 'fileList': + $ajax_data['reference_id'] = uniqid(); + if (is_dir($folder)) { + $file_array = []; + // sorted by date + $files = glob($folder . DIRECTORY_SEPARATOR . '*'); + // oldest top + usort($files, function ($x, $y) { + return filemtime($x) > filemtime($y) ? 1 : -1; + }); + foreach ($files as $file) { + $file_array[] = [ + 'name' => basename($file), + 'create_date' => date("Y-m-d H:i:s", filemtime($file)), + 'size' => humanReadableByteFormat((int)filesize($file)) + ]; + } + $ajax_data['file_list'] = $file_array; + $status = 'ok'; + if (!count($file_array)) { + $status = 'warn'; + $ajax_data['info'] = 'No files uploaded yet'; + } + } else { + $error = true; + errMsg($status, 'Uploaded folder not found'); + } + break; + // for simple single file upload + case 'fileUpload': + // has a target name (file prefix flag) + $upload_name = $_POST['uploadName']; + // $this->debug('FILE UPLOAD', 'ALL FILES: '.$this->printAr($_FILES)); + // errMsg('debug', 'Called for: ' . $upload_name . ' with data: ' + // . (isset($_FILES[$upload_name]) ? printAr($_FILES[$upload_name]) : 'NOT FOUND')); + // return string & array init + $file_upload_message = []; + // do we have an upload actually + if ( + isset($_FILES[$upload_name]['error']) && + $_FILES[$upload_name]['error'] == UPLOAD_ERR_OK + ) { + // set status to success post all checks + $status = 'success'; + // strip out -file from upload name for search + $_upload_name = str_replace('-file', '', $upload_name); + $mime_type = null; + + $ajax_data = [ + 'msg' => [], + 'file_uid' => null, + 'file_url' => null, + 'file_name' => null, + 'file_size' => null, + 'file_size_raw' => null, + 'file_type' => null, + ]; + // check mime type for file and do check if we have a "valid_files" settings + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mime_type = $finfo->file($_FILES[$upload_name]['tmp_name']); + if (!in_array($mime_type, ['text/csv', 'text/plain', 'text/x-php'])) { + $file_upload_message[] = 'File type must be CSV or PHP: ' . $mime_type; + $ajax_data['msg'] = $file_upload_message; + $ajax_data['error'] = 'File type must be CSV or PHP: ' . $mime_type; + $status = 'error'; + } + // ON ERROR set status = 'error'; + // file type ok and file size ok, we commence actualy upload + if ($status == 'success') { + // basic file id data + $file_uid = null; + $file_url = null; + $file_name = null; + $file_size = null; + $file_size_raw = null; + + $file_upload_message[] = 'File upload successful'; + // internal file id + $file_uid = uuidv4(); + // move file to tmp location and return new name + move_uploaded_file( + $_FILES[$upload_name]['tmp_name'], + $folder . $file_uid + ); + $file_name = $_FILES[$upload_name]['name']; + $file_size = humanReadableByteFormat($_FILES[$upload_name]['size']); + $file_size_raw = $_FILES[$upload_name]['size']; + // correct the image rotation + // $this->correctImageOrientation(BASE.TMP.$file_uid); + // make a copy to layout/cache/images for file url + // as a thumbnail in defined size + // $file_url = $this->form_base_path + // . $this->createThumbnailSimple(BASE.TMP.$file_uid, THUMB_WIDTH, THUMB_HEIGHT); + // write back data for frontend processing + $ajax_data = [ + 'msg' => $file_upload_message, + 'file_uid' => $file_uid, + 'file_url' => $file_url, + 'file_name' => $file_name, + 'file_size' => $file_size, + 'file_size_raw' => $file_size_raw, + 'file_type' => $mime_type, + ]; + } else { + $info_msg = 'File uploaded and check failed'; + errMsg($status, $info_msg); + $ajax_data['msg'][] = $info_msg; + } + } else { + $info_msg = isset($_FILES[$upload_name]) ? + 'File upload filed: ' . fileUploadErrorMessage($_FILES[$upload_name]['error']) : + 'General file upload error'; + errMsg($status, $info_msg); + $ajax_data = [ + 'msg' => [ + $info_msg + ] + ]; + } + break; + default: + $error = true; + errMsg($status, 'Abnormal action'); + break; + } +} else { + $error = true; + errMsg($status, 'No action set'); +} + +$data = [ + 'status' => $status, + 'msg' => $error_string, + 'action' => $_POST['action'] ?? null, + 'content' => $ajax_data +]; + +// print the JSON data out to the browsers +$output = json_encode($data); +print $output; + +// __END__ diff --git a/test/edit.jq.js b/test/edit.jq.js new file mode 100644 index 0000000..10d4e50 --- /dev/null +++ b/test/edit.jq.js @@ -0,0 +1,1376 @@ +/* general edit javascript */ +/* jquery version */ + +/* jshint esversion: 6 */ + +/* global i18n */ + +// debug set +/*var FRONTEND_DEBUG = false; +var DEBUG = true; +if (!DEBUG) { + $($H(window.console)).each(function(w) { + window.console[w.key] = function() {}; + }); +}*/ + +// open overlay boxes counter +var GL_OB_S = 30; +var GL_OB_BASE = 30; + +/** + * opens a popup window with winName and given features (string) + * @param {String} theURL the url + * @param {String} winName window name + * @param {Object} features popup features + */ +function pop(theURL, winName, features) // eslint-disable-line no-unused-vars +{ + winName = window.open(theURL, winName, features); + winName.focus(); +} + +/** + * automatically resize a text area based on the amount of lines in it + * @param {[string} ta_id element id + */ +function expandTA(ta_id) // eslint-disable-line no-unused-vars +{ + var ta; + // if a string comes, its a get by id, else use it as an element pass on + if (!ta_id.length) { + ta = ta_id; + } else { + ta = document.getElementById(ta_id); + } + var maxChars = ta.cols; + var theRows = ta.value.split('\n'); + var numNewRows = 0; + + for ( var i = 0; i < theRows.length; i++ ) { + if ((theRows[i].length+2) > maxChars) { + numNewRows += Math.ceil( (theRows[i].length+2) / maxChars ) ; + } + } + ta.rows = numNewRows + theRows.length; +} + +/** + * wrapper to get the real window size for the current browser window + * @return {Object} object with width/height + */ +function getWindowSize() +{ + var width, height; + width = window.innerWidth || (window.document.documentElement.clientWidth || window.document.body.clientWidth); + height = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight); + return { + width: width, + height: height + }; +} + +/** + * wrapper to get the correct scroll offset + * @return {Object} object with x/y px + */ +function getScrollOffset() +{ + var left, top; + left = window.pageXOffset || (window.document.documentElement.scrollLeft || window.document.body.scrollLeft); + top = window.pageYOffset || (window.document.documentElement.scrollTop || window.document.body.scrollTop); + return { + left: left, + top: top + }; +} + +/** + * centers div to current window size middle + * @param {String} id element to center + * @param {Boolean} left if true centers to the middle from the left + * @param {Boolean} top if true centers to the middle from the top + */ +function setCenter(id, left, top) +{ + // get size of id + var dimensions = { + height: $('#' + id).height(), + width: $('#' + id).width() + }; + var type = $('#' + id).css('position'); + var viewport = getWindowSize(); + var offset = getScrollOffset(); + + // console.log('Id %s, type: %s, dimensions %s x %s, viewport %s x %s', id, type, dimensions.width, dimensions.height, viewport.width, viewport.height); + // console.log('Scrolloffset left: %s, top: %s', offset.left, offset.top); + // console.log('Left: %s, Top: %s (%s)', parseInt((viewport.width / 2) - (dimensions.width / 2) + offset.left), parseInt((viewport.height / 2) - (dimensions.height / 2) + offset.top), parseInt((viewport.height / 2) - (dimensions.height / 2))); + if (left) { + $('#' + id).css({ + left: parseInt((viewport.width / 2) - (dimensions.width / 2) + offset.left) + 'px' + }); + } + if (top) { + // if we have fixed, we do not add the offset, else it moves out of the screen + var top_pos = type == 'fixed' ? + parseInt((viewport.height / 2) - (dimensions.height / 2)) : + parseInt((viewport.height / 2) - (dimensions.height / 2) + offset.top); + $('#' + id).css({ + top: top_pos + 'px' + }); + } +} + +/** + * goes to an element id position + * @param {String} element element id to move to + * @param {Number} [offset=0] offset from top, default is 0 (px) + * @param {Number} [duration=500] animation time, default 500ms + * @param {String} [base='body,html'] base element for offset scroll + */ +function goToPos(element, offset = 0, duration = 500, base = 'body,html') // eslint-disable-line no-unused-vars +{ + try { + if ($('#' + element).length) { + $(base).animate({ + scrollTop: $('#' + element).offset().top - offset + }, duration); + } + } catch (err) { + errorCatch(err); + } +} + +/** + * uses the i18n object created in the translation template + * that is filled from gettext in PHP + * @param {String} string text to translate + * @return {String} translated text (based on PHP selected language) + */ +function __(string) +{ + if (typeof i18n !== 'undefined' && isObject(i18n) && i18n[string]) { + return i18n[string]; + } else { + return string; + } +} + +/** + * 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 + ; + }); + }; +} + +/** + * round to digits (float) + * @param {Float} Number.prototype.round Float type number to round + * @param {Number} prec Precision to round to + * @return {Float} Rounded number + */ +if (Number.prototype.round) { + Number.prototype.round = function (prec) { + return Math.round(this * Math.pow(10, prec)) / Math.pow(10, prec); + }; +} + +/** + * formats flat number 123456 to 123,456 + * @param {Number} x number to be formated + * @return {String} formatted with , in thousands + */ +function numberWithCommas(x) // eslint-disable-line no-unused-vars +{ + var parts = x.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return parts.join('.'); +} + +/** + * converts line breaks to br + * @param {String} string any string + * @return {String} string with
+ */ +function convertLBtoBR(string) // eslint-disable-line no-unused-vars +{ + return string.replace(/(?:\r\n|\r|\n)/g, '
'); +} + +/** + * escape HTML string + * @param {String} !String.prototype.escapeHTML HTML data string to be escaped + * @return {String} escaped string + */ +if (!String.prototype.escapeHTML) { + String.prototype.escapeHTML = function() { + return this.replace(/[&<>"'/]/g, function (s) { + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + return entityMap[s]; + }); + }; +} + +/** + * unescape a HTML encoded string + * @param {String} !String.prototype.unescapeHTML data with escaped entries + * @return {String} HTML formated string + */ +if (!String.prototype.unescapeHTML) { + String.prototype.unescapeHTML = function() { + return this.replace(/&[#\w]+;/g, function (s) { + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': '\'', + '/': '/' + }; + + return entityMap[s]; + }); + }; +} + +/** + * returns current timestamp (unix timestamp) + * @return {Number} timestamp (in milliseconds) + */ +function getTimestamp() // eslint-disable-line no-unused-vars +{ + var date = new Date(); + return date.getTime(); +} + +/** + * dec2hex :: Integer -> String + * i.e. 0-255 -> '00'-'ff' + * @param {Number} dec decimal string + * @return {String} hex encdoded number + */ +function dec2hex(dec) +{ + return ('0' + dec.toString(16)).substr(-2); +} + +/** + * generateId :: Integer -> String + * only works on mondern browsers + * @param {Number} len length of unique id string + * @return {String} random string in length of len + */ +function generateId(len) // eslint-disable-line no-unused-vars +{ + var arr = new Uint8Array((len || 40) / 2); + (window.crypto || window.msCrypto).getRandomValues(arr); + return Array.from(arr, dec2hex).join(''); +} + +/** + * creates a pseudo random string of 10 characters + * works on all browsers + * after many runs it will create duplicates + * @return {String} not true random string + */ +function randomIdF() // eslint-disable-line no-unused-vars +{ + return Math.random().toString(36).substring(2); +} + +/** + * check if name is a function + * @param {string} name Name of function to check if exists + * @return {Boolean} true/false + */ +function isFunction(name) // eslint-disable-line no-unused-vars +{ + if (typeof window[name] !== 'undefined' && + typeof window[name] === 'function') { + return true; + } else { + return false; + } +} + +/** + * call a function by its string name + * https://stackoverflow.com/a/359910 + * example: executeFunctionByName("My.Namespace.functionName", window, arguments); + * @param {string} functionName The function name or namespace + function + * @param {mixed} context context (window or first namespace) + * hidden next are all the arguments + * @return {mixed} Return values from functon + */ +function executeFunctionByName(functionName, context /*, args */) // eslint-disable-line no-unused-vars +{ + var args = Array.prototype.slice.call(arguments, 2); + var namespaces = functionName.split('.'); + var func = namespaces.pop(); + for (var i = 0; i < namespaces.length; i++) { + context = context[namespaces[i]]; + } + return context[func].apply(context, args); +} + +/** + * checks if a variable is an object + * @param {Mixed} val possible object + * @return {Boolean} true/false if it is an object or not + */ +function isObject(val) +{ + if (val === null) { + return false; + } + return ((typeof val === 'function') || (typeof val === 'object')); +} + +/** + * get the length of an object (entries) + * @param {Object} object object to check + * @return {Number} number of entry + */ +function getObjectCount(object) +{ + return Object.keys(object).length; +} + +/** + * 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; +} + +/** + * returns matching key of value + * @param {Object} obj object to search value in + * @param {Mixed} value any value (String, Number, etc) + * @return {String} the key found for the first matching value + */ +function getKeyByValue(object, value) // eslint-disable-line no-unused-vars +{ + return Object.keys(object).find(key => object[key] === value); + // return Object.keys(object).find(function (key) { + // return object[key] === value; + // }); +} + +/** + * returns true if value is found in object with a key + * @param {Object} obj object to search value in + * @param {Mixed} value any value (String, Number, etc) + * @return {Boolean} true on value found, false on not found + */ +function valueInObject(object, value) // eslint-disable-line no-unused-vars +{ + return Object.keys(object).find(key => object[key] === value) ? true : false; + // return Object.keys(object).find(function (key) { + // return object[key] === value; + // }) ? true : false; +} + +/** + * true deep copy for Javascript objects + * if Object.assign({}, obj) is not working (shallow) + * or if JSON.parse(JSON.stringify(obj)) is failing + * @param {Object} inObject Object to copy + * @return {Object} Copied Object + */ +function deepCopyFunction(inObject) +{ + var outObject, value, key; + if (typeof inObject !== 'object' || inObject === null) { + return inObject; // Return the value if inObject is not an object + } + // Create an array or object to hold the values + outObject = Array.isArray(inObject) ? [] : {}; + // loop over ech entry in object + for (key in inObject) { + value = inObject[key]; + // Recursively (deep) copy for nested objects, including arrays + outObject[key] = deepCopyFunction(value); + } + + return outObject; +} + +/** + * checks if a DOM element actually exists + * @param {String} id Element id to check for + * @return {Boolean} true if element exists, false on failure + */ +function exists(id) +{ + return $('#' + id).length > 0 ? true : false; +} + +/** + * 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) // eslint-disable-line no-unused-vars +{ + 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]; +} + +/** + * like formatBytes, but returns bytes for <1KB and not 0.n KB + * @param {Number} bytes bytes in int + * @return {String} string in GB/MB/KB + */ +function formatBytesLong(bytes) // eslint-disable-line no-unused-vars +{ + var i = Math.floor(Math.log(bytes) / Math.log(1024)); + var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; +} + +/** + * Convert a string with B/K/M/etc into a byte number + * @param {String|Number} bytes Any string with B/K/M/etc + * @return {String|Number} A byte number, or original string as is + */ +function stringByteFormat(bytes) // eslint-disable-line no-unused-vars +{ + // if anything not string return + if (!(typeof bytes === 'string' || bytes instanceof String)) { + return bytes; + } + // for pow exponent list + let valid_units = 'bkmgtpezy'; + // valid string that can be converted + let regex = /([\d.,]*)\s?(eb|pb|tb|gb|mb|kb|e|p|t|g|m|k|b)$/i; + let matches = bytes.match(regex); + // if nothing found, return original input + if (matches !== null) { + // remove all non valid entries outside numbers and . + // convert to float number + let m1 = parseFloat(matches[1].replace(/[^0-9.]/,'')); + // only get the FIRST letter from the size, convert it to lower case + let m2 = matches[2].replace(/[^bkmgtpezy]/i, '').charAt(0).toLowerCase(); + if (m2) { + // use the position in the valid unit list to do the math conversion + bytes = m1 * Math.pow(1024, valid_units.indexOf(m2)); + } + } + return bytes; +} + +/** + * 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); + } +} + +/************************************************************* + * OLD action indicator and overlay boxes calls + * DO NOT USE + * actionIndicator -> showActionIndicator + * actionIndicator -> hideActionIndicator + * actionIndicatorShow -> showActionIndicator + * actionIndicatorHide -> hideActionIndicator + * overlayBoxShow -> showOverlayBoxLayers + * overlayBoxHide -> hideOverlayBoxLayers + * setOverlayBox -> showOverlayBoxLayers + * hideOverlayBox -> hideOverlayBoxLayers + * ClearCall -> ClearCallActionBox + * ***********************************************************/ + +/** + * show or hide the "do" overlay + * @param {String} loc location name for action indicator + * default empty. for console.log + * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block + */ +function actionIndicator(loc, overlay = true) // eslint-disable-line no-unused-vars +{ + if ($('#indicator').is(':visible')) { + actionIndicatorHide(loc, overlay); + } else { + actionIndicatorShow(loc, overlay); + } +} + +/** + * explicit show for action Indicator + * instead of automatically show or hide, do on command show + * @param {String} loc location name for action indicator + * default empty. for console.log + * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block + */ +function actionIndicatorShow(loc, overlay = true) +{ + // console.log('Indicator: SHOW [%s]', loc); + if (!$('#indicator').is(':visible')) { + if (!$('#indicator').hasClass('progress')) { + $('#indicator').addClass('progress'); + } + setCenter('indicator', true, true); + $('#indicator').show(); + } + if (overlay === true) { + overlayBoxShow(); + } +} + +/** + * explicit hide for action Indicator + * instead of automatically show or hide, do on command hide + * @param {String} loc location name for action indicator + * default empty. for console.log + * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block + */ +function actionIndicatorHide(loc, overlay = true) +{ + // console.log('Indicator: HIDE [%s]', loc); + $('#indicator').hide(); + if (overlay === true) { + overlayBoxHide(); + } +} + +/** + * shows the overlay box or if already visible, bumps the zIndex to 100 + */ +function overlayBoxShow() +{ + // check if overlay box exists and if yes set the z-index to 100 + if ($('#overlayBox').is(':visible')) { + $('#overlayBox').css('zIndex', '100'); + } else { + $('#overlayBox').show(); + $('#overlayBox').css('zIndex', '98'); + } +} + +/** + * hides the overlay box or if zIndex is 100 bumps it down to previous level + */ +function overlayBoxHide() +{ + // if the overlay box z-index is 100, do no hide, but set to 98 + if ($('#overlayBox').css('zIndex') >= 100) { + $('#overlayBox').css('zIndex', '98'); + } else { + $('#overlayBox').hide(); + } +} + +/** + * position the overlay block box and shows it + */ +function setOverlayBox() // eslint-disable-line no-unused-vars +{ + if (!$('#overlayBox').is(':visible')) { + $('#overlayBox').show(); + } +} + +/** + * opposite of set, always hides overlay box + */ +function hideOverlayBox() // eslint-disable-line no-unused-vars +{ + if ($('#overlayBox').is(':visible')) { + $('#overlayBox').hide(); + } +} + +/** + * the abort call, clears the action box and hides it and the overlay box + */ +function ClearCall() // eslint-disable-line no-unused-vars +{ + $('#actionBox').html(''); + $('#actionBox').hide(); + $('#overlayBox').hide(); +} + +/************************************************************* + * NEW action indicator and overlay box calls + * USE THIS + * ***********************************************************/ + +/** + * show action indicator + * - checks if not existing and add + * - only shows if not visible (else ignore) + * - overlaybox check is called and shown on a fixzed + * zIndex of 1000 + * - indicator is page centered + * @param {String} loc ID string, only used for console log + */ +function showActionIndicator(loc) // eslint-disable-line no-unused-vars +{ + // console.log('Indicator: SHOW [%s]', loc); + // check if indicator element exists + if ($('#indicator').length == 0) { + var el = document.createElement('div'); + el.className = 'progress hide'; + el.id = 'indicator'; + $('body').append(el); + } else if (!$('#indicator').hasClass('progress')) { + // if I add a class it will not be hidden anymore + // hide it + $('#indicator').addClass('progress').hide(); + } + // indicator not visible + if (!$('#indicator').is(':visible')) { + // check if overlay box element exits + checkOverlayExists(); + // if not visible show + if (!$('#overlayBox').is(':visible')) { + $('#overlayBox').show(); + } + // always set to 1000 zIndex to be top + $('#overlayBox').css('zIndex', 1000); + // show indicator + $('#indicator').show(); + // center it + setCenter('indicator', true, true); + } +} + +/** + * hide action indicator, if it is visiable + * If the global variable GL_OB_S is > GL_OB_BASE then + * the overlayBox is not hidden but the zIndex + * is set to this value + * @param {String} loc ID string, only used for console log + */ +function hideActionIndicator(loc) // eslint-disable-line no-unused-vars +{ + // console.log('Indicator: HIDE [%s]', loc); + // check if indicator is visible + if ($('#indicator').is(':visible')) { + // hide indicator + $('#indicator').hide(); + // if global overlay box count is > 0 + // then set it to this level and keep + if (GL_OB_S > GL_OB_BASE) { + $('#overlayBox').css('zIndex', GL_OB_S); + } else { + // else hide overlay box and set zIndex to 0 + $('#overlayBox').hide(); + $('#overlayBox').css('zIndex', GL_OB_BASE); + } + } +} + +/** + * checks if overlayBox exists, if not it is + * added as hidden item at the body end + */ +function checkOverlayExists() +{ + // check if overlay box exists, if not create it + if ($('#overlayBox').length == 0) { + var el = document.createElement('div'); + el.className = 'overlayBoxElement hide'; + el.id = 'overlayBox'; + $('body').append(el); + } +} + +/** + * show overlay box + * if not visible show and set zIndex to 10 (GL_OB_BASE) + * if visible, add +1 to the GL_OB_S variable and + * up zIndex by this value + */ +function showOverlayBoxLayers(el_id) // eslint-disable-line no-unused-vars +{ + // console.log('SHOW overlaybox: %s', GL_OB_S); + // if overlay box is not visible show and set zIndex to 0 + if (!$('#overlayBox').is(':visible')) { + $('#overlayBox').show(); + $('#overlayBox').css('zIndex', GL_OB_BASE); + // also set start variable to 0 + GL_OB_S = GL_OB_BASE; + } + // up the overlay box counter by 1 + GL_OB_S ++; + // set zIndex + $('#overlayBox').css('zIndex', GL_OB_S); + // if element given raise zIndex and show + if (el_id) { + if ($('#' + el_id).length > 0) { + $('#' + el_id).css('zIndex', GL_OB_S + 1); + $('#' + el_id).show(); + } + } + // console.log('SHOW overlaybox NEW zIndex: %s', $('#overlayBox').css('zIndex')); +} + +/** + * hide overlay box + * lower GL_OB_S value by -1 + * if we are 10 (GL_OB_BASE) or below hide the overlayIndex + * and set zIndex and GL_OB_S to 0 + * else just set zIndex to the new GL_OB_S value + * @param {String} el_id Target to hide layer + */ +function hideOverlayBoxLayers(el_id) +{ + // console.log('HIDE overlaybox: %s', GL_OB_S); + // remove on layer + GL_OB_S --; + // if 0 or lower (overflow) hide it and + // set zIndex to 0 + if (GL_OB_S <= GL_OB_BASE) { + GL_OB_S = GL_OB_BASE; + $('#overlayBox').hide(); + $('#overlayBox').css('zIndex', GL_OB_BASE); + } else { + // if OB_S > 0 then set new zIndex + $('#overlayBox').css('zIndex', GL_OB_S); + } + if (el_id) { + $('#' + el_id).hide(); + $('#' + el_id).css('zIndex', 0); + } + // console.log('HIDE overlaybox NEW zIndex: %s', $('#overlayBox').css('zIndex')); +} + +/** + * only for single action box + */ +function clearCallActionBox() // eslint-disable-line no-unused-vars +{ + $('#actionBox').html(''); + $('#actionBox').hide(); + hideOverlayBoxLayers(); +} + +// *** DOM MANAGEMENT FUNCTIONS +/** + * reates object for DOM element creation flow + * @param {String} tag must set tag (div, span, etc) + * @param {String} [id=''] optional set for id, if input, select will be used for name + * @param {String} [content=''] text content inside, is skipped if sub elements exist + * @param {Array} [css=[]] array for css tags + * @param {Object} [options={}] anything else (value, placeholder, OnClick, style) + * @return {Object} created element as an object + */ +function cel(tag, id = '', content = '', css = [], options = {}) +{ + return { + tag: tag, + id: id, + name: options.name, // override name if set [name gets ignored in tree build anyway] + content: content, + css: css, + options: options, + sub: [] + }; +} + +/** + * attach a cel created object to another to create a basic DOM tree + * @param {Object} base object where to attach/search + * @param {Object} attach the object to be attached + * @param {String} [id=''] optional id, if given search in base for this id and attach there + * @return {Object} "none", technically there is no return needed as it is global attach + */ +function ael(base, attach, id = '') +{ + if (id) { + // base id match already + if (base.id == id) { + // base.sub.push(Object.assign({}, attach)); + base.sub.push(deepCopyFunction(attach)); + } else { + // sub check + if (isObject(base.sub) && base.sub.length > 0) { + for (var i = 0; i < base.sub.length; i ++) { + // recursive call to sub element + ael(base.sub[i], attach, id); + } + } + } + } else { + // base.sub.push(Object.assign({}, attach)); + base.sub.push(deepCopyFunction(attach)); + } + return base; +} + +/** + * directly attach n elements to one master base element + * this type does not support attach with optional id + * @param {Object} base object to where we attach the elements + * @param {...Object} attach attach 1..n: attach directly to the base element those attachments + * @return {Object} "none", technically there is no return needed, global attach + */ +function aelx(base, ...attach) +{ + for (var i = 0; i < attach.length; i ++) { + // base.sub.push(Object.assign({}, attach[i])); + base.sub.push(deepCopyFunction(attach[i])); + } + return base; +} + +/** + * same as aelx, but instead of using objects as parameters + * get an array of objects to attach + * @param {Object} base object to where we attach the elements + * @param {Array} attach array of objects to attach + * @return {Object} "none", technically there is no return needed, global attach + */ +function aelxar(base, attach) // eslint-disable-line no-unused-vars +{ + for (var i = 0; i < attach.length; i ++) { + // base.sub.push(Object.assign({}, attach[i])); + base.sub.push(deepCopyFunction(attach[i])); + } + return base; +} + +/** + * resets the sub elements of the base element given + * @param {Object} base cel created element + * @return {Object} returns reset base element + */ +function rel(base) // eslint-disable-line no-unused-vars +{ + base.sub = []; + return base; +} + +/** + * searches and removes style from css array + * @param {Object} _element element to work one + * @param {String css style sheet to remove (name) + * @return {Object} returns full element + */ +function rcssel(_element, css) +{ + var css_index = _element.css.indexOf(css); + if (css_index > -1) { + _element.css.splice(css_index, 1); + } + return _element; +} + +/** + * adds a new style sheet to the element given + * @param {Object} _element element to work on + * @param {String} css style sheet to add (name) + * @return {Object} returns full element + */ +function acssel(_element, css) +{ + var css_index = _element.css.indexOf(css); + if (css_index == -1) { + _element.css.push(css); + } + return _element; +} + +/** + * removes one css and adds another + * is a wrapper around rcssel/acssel + * @param {Object} _element element to work on + * @param {String} rcss style to remove (name) + * @param {String} acss style to add (name) + * @return {Object} returns full element + */ +function scssel(_element, rcss, acss) // eslint-disable-line no-unused-vars +{ + rcssel(_element, rcss); + acssel(_element, acss); +} + +/** + * parses the object tree created with cel/ael and converts it into an HTML string + * that can be inserted into the page + * @param {Object} tree object tree with dom element declarations + * @return {String} HTML string that can be used as innerHTML + */ +function phfo(tree) +{ + // holds the elements + var content = []; + // main part line + var line = '<' + tree.tag; + var i; + // first id, if set + if (tree.id) { + line += ' id="' + tree.id + '"'; + // if anything input (input, textarea, select then add name too) + if (['input', 'textarea', 'select'].includes(tree.tag)) { + line += ' name="' + (tree.name ? tree.name : tree.id) + '"'; + } + } + // second CSS + if (isObject(tree.css) && tree.css.length > 0) { + line += ' class="'; + for (i = 0; i < tree.css.length; i ++) { + line += tree.css[i] + ' '; + } + // strip last space + line = line.slice(0, -1); + line += '"'; + } + // options is anything key = "data" + if (isObject(tree.options)) { + // ignores id, name, class as key + for (const [key, item] of Object.entries(tree.options)) { + if (!['id', 'name', 'class'].includes(key)) { + line += ' ' + key + '="' + item + '"'; + } + } + } + // finish open tag + line += '>'; + // push finished line + content.push(line); + // dive into sub tree to attach sub nodes + // NOTES: we can have content (text) AND sub nodes at the same level + // CONTENT (TEXT) takes preference over SUB NODE in order + if (isObject(tree.sub) && tree.sub.length > 0) { + if (tree.content) { + content.push(tree.content); + } + for (i = 0; i < tree.sub.length; i ++) { + content.push(phfo(tree.sub[i])); + } + } else if (tree.content) { + content.push(tree.content); + } + // if not input close + if (tree.tag != 'input') { + content.push(''); + } + // combine to string + return content.join(''); +} + +/** + * Create HTML elements from array list + * as a flat element without master object file + * Is like tree.sub call + * @param {Array} list Array of cel created objects + * @return {String} HTML String + */ +function phfa(list) // eslint-disable-line no-unused-vars +{ + var content = []; + for (var i = 0; i < list.length; i ++) { + content.push(phfo(list[i])); + } + return content.join(''); +} +// *** DOM MANAGEMENT FUNCTIONS + +// BLOCK: html wrappers for quickly creating html data blocks + +/** + * NOTE: OLD FORMAT which misses multiple block set + * creates an select/options drop down block. + * the array needs to be key -> value format. + * key is for the option id and value is for the data output + * @param {String} name name/id + * @param {Object} data array for the options + * @param {String} [selected=''] selected item uid + * @param {Boolean} [options_only=false] if this is true, it will not print the select part + * @param {Boolean} [return_string=false] return as string and not as element + * @param {String} [sort=''] if empty as is, else allowed 'keys', + * 'values' all others are ignored + * @return {String} html with build options block + */ +function html_options(name, data, selected = '', options_only = false, return_string = false, sort = '') // eslint-disable-line no-unused-vars +{ + // wrapper to new call + return html_options_block(name, data, selected, false, options_only, return_string, sort); +} + +/** + * NOTE: USE THIS CALL, the above one is deprecated + * creates an select/options drop down block. + * the array needs to be key -> value format. + * key is for the option id and value is for the data output + * @param {String} name name/id + * @param {Object} data array for the options + * @param {String} [selected=''] selected item uid + * @param {Number} [multiple=0] if this is 1 or larger, the drop down + * will be turned into multiple select + * the number sets the size value unless it is 1, + * then it is default + * @param {Boolean} [options_only=false] if this is true, it will not print the select part + * @param {Boolean} [return_string=false] return as string and not as element + * @param {String} [sort=''] if empty as is, else allowed 'keys', + * 'values' all others are ignored + * @param {String} [onchange=''] onchange trigger call, default unset + * @return {String} html with build options block + */ +function html_options_block(name, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = '') +{ + var content = []; + var element_select; + var select_options = {}; + var element_option; + var data_list = []; // for sorted output + var value; + var options = {}; + // var option; + if (multiple > 0) { + select_options.multiple = ''; + if (multiple > 1) { + select_options.size = multiple; + } + } + if (onchange) { + select_options.OnChange = onchange; + } + // set outside select, gets stripped on return if options only is true + element_select = cel('select', name, '', [], select_options); + // console.log('Call for %s, options: %s', name, options_only); + if (sort == 'keys') { + data_list = Object.keys(data).sort(); + } else if (sort == 'values') { + data_list = Object.keys(data).sort((a, b) => ('' + data[a]).localeCompare(data[b])); + } else { + data_list = Object.keys(data); + } + // console.log('ORDER: %s', data_list); + // use the previously sorted list + // for (const [key, value] of Object.entries(data)) { + for (const key of data_list) { + value = data[key]; + // console.log('create [%s] options: key: %s, value: %s', name, key, value); + // basic options init + options = { + 'label': value, + 'value': key + }; + // add selected if matching + if (multiple == 0 && !Array.isArray(selected) && selected == key) { + options.selected = ''; + } + // for multiple, we match selected as array + if (multiple == 1 && Array.isArray(selected) && selected.indexOf(key) != -1) { + options.selected = ''; + } + // create the element option + element_option = cel('option', '', value, '', options); + // attach it to the select element + ael(element_select, element_option); + } + // if with select part, convert to text + if (!options_only) { + if (return_string) { + content.push(phfo(element_select)); + return content.join(''); + } else { + return element_select; + } + } else { + // strip select part + if (return_string) { + for (var i = 0; i < element_select.sub.length; i ++) { + content.push(phfo(element_select.sub[i])); + } + return content.join(''); + } else { + return element_select.sub; + } + } +} + +/** + * refills a select box with options and keeps the selected + * @param {String} name name/id + * @param {Object} data array of options + * @param {String} [sort=''] if empty as is, else allowed 'keys', 'values' + * all others are ignored + */ +function html_options_refill(name, data, sort = '') // eslint-disable-line no-unused-vars +{ + var element_option; + var option_selected; + var data_list = []; // for sorted output + var value; + // skip if not exists + if (document.getElementById(name)) { + // console.log('Call for %s, options: %s', name, options_only); + if (sort == 'keys') { + data_list = Object.keys(data).sort(); + } else if (sort == 'values') { + data_list = Object.keys(data).sort((a, b) => ('' + data[a]).localeCompare(data[b])); + } else { + data_list = Object.keys(data); + } + // first read in existing ones from the options and get the selected one + [].forEach.call(document.querySelectorAll('#' + name + ' :checked'), function(elm) { + option_selected = elm.value; + }); + document.getElementById(name).innerHTML = ''; + for (const key of data_list) { + value = data[key]; + // console.log('add [%s] options: key: %s, value: %s', name, key, value); + element_option = document.createElement('option'); + element_option.label = value; + element_option.value = key; + element_option.innerHTML = value; + if (key == option_selected) { + element_option.selected = true; + } + document.getElementById(name).appendChild(element_option); + } + } +} + +/** + * parses a query string from window.location.search.substring(1) + * ALTERNATIVE CODE + * var url = new URL(window.location.href); + * param_uid = url.searchParams.get('uid'); + * @param {String} [query=''] the query string to parse + * if not set will auto fill + * @param {String} [return_key=''] if set only returns this key entry + * or empty for none + * @return {Object|String} parameter entry list + */ +function parseQueryString(query = '', return_key = '') // eslint-disable-line no-unused-vars +{ + if (!query) { + query = window.location.search.substring(1); + } + var vars = query.split('&'); + var query_string = {}; + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + var key = decodeURIComponent(pair[0]); + var value = decodeURIComponent(pair[1]); + // skip over run if there is nothing + if (!key || value === 'undefined') { + continue; + } + // If first entry with this name + if (typeof query_string[key] === 'undefined') { + query_string[key] = decodeURIComponent(value); + // If second entry with this name + } else if (typeof query_string[key] === 'string') { + var arr = [query_string[key], decodeURIComponent(value)]; + query_string[key] = arr; + // If third or later entry with this name + } else { + query_string[key].push(decodeURIComponent(value)); + } + } + if (return_key) { + if (keyInObject(return_key, query_string)) { + return query_string[return_key]; + } else { + return ''; + } + } else { + return query_string; + } +} + +/** + * searches query parameters for entry and returns data either as string or array + * if no search is given the whole parameters are returned as an object + * if a parameter is set several times it will be returned as an array + * if search parameter set and nothing found and empty string is returned + * if no parametes exist and no serach is set and empty object is returned + * @param {String} [search=''] if set searches for this entry, if empty + * all parameters are returned + * @param {String} [query=''] different query string to parse, if not + * set (default) the current window href is used + * @param {Bool} [single=false] if set to true then only the first found + * will be returned + * @return {Object|Array|String} if search is empty, object, if search is set + * and only one entry, then string, else array + * unless single is true + */ +function getQueryStringParam(search = '', query = '', single = false) // eslint-disable-line no-unused-vars +{ + if (!query) { + query = window.location.href; + } + const url = new URL(query); + let param = ''; + if (search) { + let _params = url.searchParams.getAll(search); + if (_params.length == 1 || single === true) { + param = _params[0]; + } else if (_params.length > 1) { + param = _params; + } + } else { + // will be object, so declare it one + param = {}; + // loop over paramenters + for (const [key] of url.searchParams.entries()) { + // check if not yet set + if (typeof param[key] === 'undefined') { + // get the parameters multiple + let _params = url.searchParams.getAll(key); + // if 1 set as string, else attach array as is + param[key] = _params.length < 2 || single === true ? + _params[0] : + _params; + } + } + } + return param; +} + +// *** MASTER logout call +/** + * submits basic data for form logout + */ +function loginLogout() // eslint-disable-line no-unused-vars +{ + const form = document.createElement('form'); + form.method = 'post'; + const hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = 'login_logout'; + hiddenField.value = 'Logout'; + form.appendChild(hiddenField); + document.body.appendChild(form); + form.submit(); +} + +/** + * create login string and logout button elements + * @param {String} login_string the login string to show on the left + * @param {String} [header_id='mainHeader'] the target for the main element block + * if not set mainHeader is assumed + * this is the target div for the "loginRow" + */ +function createLoginRow(login_string, header_id = 'mainHeader') // eslint-disable-line no-unused-vars +{ + // if header does not exist, we do nothing + if (exists(header_id)) { + // that row must exist already, if not it must be the first in the "mainHeader" + if (!exists('loginRow')) { + $('#' + header_id).html(phfo(cel('div', 'loginRow', '', ['loginRow', 'flx-spbt']))); + } + // clear out just in case for first entry + // fill with div name & login/logout button + $('#loginRow').html(phfo(cel('div', '', login_string))); + $('#loginRow').append(phfo( + aelx( + // outer div + cel('div'), + // inner element + cel('input', 'logout', '', [], { + value: __('Logout'), + type: 'button', + onClick: 'loginLogout()' + }) + ) + )); + } +} + +/** + * create the top nav menu that switches physical between pages + * (edit access data based) + * @param {Object} nav_menu the built nav menu with highlight info + * @param {String} [header_id='mainHeader'] the target for the main element block + * if not set mainHeader is assumed + * this is the target div for the "menuRow" + */ +function createNavMenu(nav_menu, header_id = 'mainHeader') // eslint-disable-line no-unused-vars +{ + // must be an object + if (isObject(nav_menu) && getObjectCount(nav_menu) > 1) { + // do we have more than one entry, if not, do not show (single page) + if (!exists('menuRow')) { + $('#' + header_id).html(phfo(cel('div', 'menuRow', '', ['menuRow', 'flx-s']))); + } + var content = []; + $.each(nav_menu, function(key, item) { + // key is number + // item is object with entries + if (key != 0) { + content.push(phfo(cel('div', '', '·', ['pd-2']))); + } + // ignore item.popup for now + if (item.enabled) { + // set selected based on window.location.href as the php set will not work + if (window.location.href.indexOf(item.url) != -1) { + item.selected = 1; + } + // create the entry + content.push(phfo( + aelx( + cel('div'), + cel('a', '', item.name, ['pd-2'].concat(item.selected ? 'highlight': ''), { + href: item.url + }) + ) + )); + } + }); + $('#menuRow').html(content.join('')); + } +} + +/* END */ diff --git a/test/edit.js b/test/edit.js new file mode 120000 index 0000000..2d9dd30 --- /dev/null +++ b/test/edit.js @@ -0,0 +1 @@ +edit.jq.js \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..94f9f56 --- /dev/null +++ b/test/index.html @@ -0,0 +1,108 @@ + + + + AJAX File upload TEST + + + + + + + + +
+
+
+ +
+ + +
+ + +
+ + + + + + +
+ + + +
+
+ +
+ + + + +
+ + +
+
+ +
+ + + + +
+ + +
+
Previous uploaded
+
+
+
+
+ +
+
+
+ + diff --git a/test/jquery-3.6.0.min.js b/test/jquery-3.6.0.min.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/test/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 + + AJAX File multiple upload TEST + + + + + + + + + + +
+ + +
+
+ + diff --git a/test/other.css b/test/other.css new file mode 100644 index 0000000..4b2ac8b --- /dev/null +++ b/test/other.css @@ -0,0 +1,140 @@ +/* +CSS Other +*/ + +/* the overlay background black cover */ +.overlayBoxElement { + background-color: rgba(0, 0, 0, 0.3); + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 98; +} + +/* the progress guruguru */ +.progress { + width: 100px; + height: 100px; + background: rgba(255, 255, 255, 0.6); + border: 20px solid rgba(255, 255, 255, 0.25); + border-left-color: rgba(3, 155, 229 ,1); + border-top-color: rgba(3, 155, 229 ,1); + border-radius: 50%; + display: inline-block; + animation: rotate 600ms infinite linear; + /* align */ + left: 0; + top: 0; + position: absolute; + z-index: 120; +} +/* Animation for above progress */ +@keyframes rotate { + to { + transform: rotate(1turn) + } +} + +.tac { text-align: center; } +.mg-30 { margin: 30px; } +.mg-5 { margin: 5px; } +.flx-ss { display: flex; } +.hide { display: none; } +.uploaded { margin-top: 20px; } +.title { font-size: 1.5em; font-weight: bold; margin-bottom: 10px; } + +.file-upload label div { + text-align: center; + color: #e60012; + background: #eee; + width: 240px; + height: 60px; + margin: 10px 5px 5px; + line-height: 60px; + border-radius: 5px; + box-shadow: 0 0 3px 2px rgba(0,0,0,.2); + font-size: 137.5%; + font-weight: 600; + transition: .7s; + cursor: pointer; +} +.file-upload label div:hover { + opacity: .7; +} +/* .file-upload-input { + +} */ +.submitBtn { + text-align: center; + color: #e60012; + background: #eee; + width: 240px; + height: 60px; + margin: 10px 5px 5px; + /* line-height: 60px; */ + border-radius: 5px; + box-shadow: 0 0 3px 2px rgba(0,0,0,.2); + font-size: 137.5%; + font-weight: 600; + transition: .7s; + cursor: pointer; +} + +.SubError { + color: red; +} + +/* all error messages */ +.error-warn { + text-align: center; + color: orange; + font-size: 1.1em; + line-height: 1.2em; +} +.error-error { + text-align: center; + color: red; + font-size: 1.1em; + line-height: 1.2em; +} +.error-abort { + text-align: center; + color: #ffffff; + background-color: #ff0000; + font-weight: bold; + font-size: 1.4em; + line-height: 1.5em; + padding: 2px; + margin: 2px 0 2px 0; +} +.error-crash { + text-align: center; + color: #fbffe0; + background-color: #a00000; + font-weight: bold; + font-size: 1.6em; + line-height: 1.7em; + border: 2px solid #000000; + padding: 2px; + margin: 2px 0 2px 0; +} +.error-info, .error-ok { + text-align: center; + color: green; + font-size: 1.1em; + line-height: 1.2em; +} +/* special debug */ +.error-debug { + text-align: left; + color: gray; + font-size: 0.9em; + line-height: 1.0em; +} + +.afus-uploaded { + margin-top: 10px; + border-top: 1px solid black; +} diff --git a/test/other.js b/test/other.js new file mode 100644 index 0000000..8210bac --- /dev/null +++ b/test/other.js @@ -0,0 +1,382 @@ +/* + * JAVASCRIPT other + */ + +/* jshint esversion: 6 */ + +/* global initAjaxUploader, keyInObject, errorCatch, showActionIndicator, hideActionIndicator, exists, phfo, aelx, cel */ + +/** + * clear the alert div and hide it + */ +function clearAlerts() { + $('#alerts-div').html('').hide(); +} + +/** + * prints a single message line + * @param {String} msg message text + * @param {String} level level as style sheet name for highlight + */ +function printMsgStr(msg, level) { + printMsg([{ level: level, str: msg }]); +} + +/** + * prints master error message in case of master error + * @param {Object} msg messages as hash object + */ +function printMsg(msg) { + var content = []; + for (const t of msg) { + // console.log('v: %s, t: %o', v, t); + if (keyInObject('code', t) && t.code != null && t.code.length > 0) { + t.str = '[' + t.code + '] ' + t.str; + } + content.push(phfo(cel('div', '', t.str, ['error-' + t.level]))); + } + $('#alerts-div') + .html(content.join('')) + .show(); + // calcHeightTopHeader(); +} + +/** + * wrapper for ajax calls + * This will do a beforeSend call + * Success generall call (before .done) + * error call for general error catch (can be outside extended with .fail) + * complete call to hide the action indicator + * note: if pre-function is small we might want to put indicator call in this function + * @param {String} call_id Caller id for debug and action indicator debug + * @param {Object} queryString Query string to pass on, default empty + * @param {Object} control Control for action indicator and others + * no_action_indicator: true/false/empty, if true do not use action indicator + * @param {String} url Target url, defaults to generalBackend.php + * @return {jqXHR} JQuery XJR Object for .done, .fail, etc connection calls + */ +function ajaxWrapper(call_id, queryString = {}, control = {}, url = 'backend.php') { + var no_action_indicator = false; + if (keyInObject('no_action_indicator', control)) { + no_action_indicator = control.no_action_indicator ? true : false; + } + // if inidicator not visible, show before + if (!no_action_indicator) { + showActionIndicator(call_id); + } + // general ajax wrapper + // AJAX call + return $.ajax({ + url: url, + type: 'POST', + data: queryString, + // genera; before Sending + beforeSend: function () { + console.log('MAIN RUN: ' + call_id); + }, + success: function (data, status, xhr) { + // additional success call + console.log('[' + call_id + '] Return with [%s] as status: %s, xhr: %s, action: %s', status, data.status, xhr.status, data.action); + }, + // general error + error: function (xhr) { + console.log('[' + call_id + '] An error occured: %s %s', xhr.status, xhr.statusText); + // critical error + printMsgStr(call_id + ' Error', 'crash'); + }, + complete: function () { + console.log('[' + call_id + '] Complete'); + if (!no_action_indicator) { + hideActionIndicator(call_id); + } + } + }); +} + +/** + * FILE CHANGE: + * helper call for ajax file upload on file selected + * @param {Number} file_pos Position in upload queue + * @param {String} target_file The file upload target prefix id + */ +function fileChangeFunction(target_file, file_pos, target_router) +{ + console.log('{FILE} CHANGE [%s/%s] FUNCTION CALL [%s]', target_file, file_pos, target_router); + clearAlerts(); + // console.log('Upload Status: %s', $('#' + target_file + '-upload-status').outerHeight()); +} + +/** + * [fileChangeFunctionAll description] + * @param {String} target_file [description] + * @param {String} target_router [description] + */ +function fileChangeFunctionAll(target_file, target_router) +{ + console.log('{FILE} CHANGE ALL [%s] FUNCTION CALL [%s]', target_file, target_router); + clearAlerts(); + // console.log('Upload Status: %s', $('#' + target_file + '-upload-status').outerHeight()); +} + +/** + * [fileRemoveFunction description] + * @param {string} target_file [description] + * @param {Number} file_pos [description] + * @param {String} target_router [description] + *c + */ +function fileRemoveFunction(target_file, file_pos, target_router) +{ + console.log('{FILE} REMOVE [%s/%s] FUNCTION CALL [%s]', target_file, file_pos, target_router); + // console.log('Upload Status: %s', $('#' + target_file + '-upload-status').outerHeight()); + // clearAlerts(); +} + +/** + * [fileClearFunction description] + * @param {String} target_file [description] + * @param {String} target_router [description] + */ +function fileClearFunction(target_file, target_router) +{ + console.log('{FILE} CLEAR [%s] FUNCTION CALL [%s]', target_file, target_router); + // console.log('Upload Status: %s', $('#' + target_file + '-upload-status').outerHeight()); + // clearAlerts(); +} + +/** + * [fileAppendBeforeUploadFunctionAllFunction description] + * @param {String} target_file [description] + * @param {String} target_router [description] + * @return {Object} [description] + */ +function fileAppendBeforeUploadFunctionAllFunction(target_file, target_router) +{ + console.log('{FILE} BEFORE UPLOAD ALL [%s] FUNCTION CALL [%s]', target_file, target_router); + return { + 'primary_key': $('#reference-id').val(), + 'same-entry': 'A', + 'target-files': target_file, + 'target-router': target_router + }; +} + +/** + * FILE BEFORE UPLOAD: + * attach additional data to be sent to the server + * This is called before the data is submitted to the server + * @param {String} target_file The file upload target prefix id + * @param {Number} file_pos Position in upload queue + * @param {String} target_router + * @return {Object} key -> value object to be attached to the form + */ +function fileAppendBeforeUploadFunction(target_file, file_pos, target_router) +{ + console.log('{FILE} BEFORE UPLOAD [%s/%s] FUNCTION CALL [%s]', target_file, file_pos, target_router); + return { + 'file_data_count': $('#file-list').children().length, + 'same-entry': 'B', + 'target-files': target_file, + 'file-pos': file_pos, + 'target-router': target_router + }; +} + +/** + * FILE ULOAD FINISHED: + * helper call for ajax file upload on upload completed + * set the uploaded file list (hidden & visible), shows import button, + * adjusts hight of action box + * @param {String} target_file The file upload target prefix id + * @param {Object} control_data The data returned from the ajax call after upload + */ +function fileUploadedFunction(target_file, file_pos, target_router, control_data) +{ + console.log('{FILE} UPLAODED [%s/%s] FUNCTION CALL [%s]: %o', target_file, file_pos, target_router, control_data); + var file_id = target_file; + var file_entry_exists = false; + // add new uploaded file entry to the push list + // first find if there is an entry for this file yet (-file-name) + try { + console.log('Uploaded file UID: %s', $('#' + file_id + '-uid-' + file_pos).val()); + if (exists(file_id + '-uid-' + file_pos) && $('#' + file_id + '-uid-' + file_pos).val()) { + // hide file info first + $('#file-info').html('').hide(); + // add new uplaoded files + $('#file-list').append(phfo( + aelx(cel('div', '', '', ['flx-ss']), + cel('div', '', control_data.file_name, ['mg-5']), + cel('div', '', '(NOW)', ['mg-5']), + cel('div', '', control_data.file_size, ['mg-5']), + cel('input', file_id + '-uploaded-' + file_pos, '', [], {type: 'hidden', value: control_data.file_uid}) + ) + )); + // DOUBLE entry checker used in FormControl control file upload + // console.log('MAP: %o', $('#file-list :input')); + file_entry_exists = Object.values($('#file-list :input').map(function() { + // console.log('Matching: %s - %s', control_data.file_uid, $(this).val()); + return control_data.file_uid == $(this).val() ? true : false; + })).find(value => value === true); + console.log('Matched: %s', file_entry_exists); + if (!file_entry_exists) { + // add hidden and visible elment + // addControlFileElement(file_id, $('#' + file_id + '-file_name').val(), control_data.other_data.status_text); + } + } + } catch(err) { + errorCatch(err); + } + // chain action test + var call_id = 'chainAction'; + var queryString = { + action: call_id, + chain: control_data.file_uid + }; + ajaxWrapper(call_id, queryString).done(function (data) { + console.log('Data: %o', data); + try { + if (data.status == 'error') { + // + } else { + // list files + if (data.status == 'warn') { + // + } else { + // + } + } + // top message + printMsg(data.msg); + } catch (err) { + errorCatch(err); + } + }); +} + +/** + * [fileUploadedAllFunction description] + * @param {String} target_file [description] + * @param {Number} target_router [description] + * @param {Boolean} all_success If true all files where accepted by the servers + */ +function fileUploadedAllFunction(target_file, target_router, all_success) +{ + console.log('{FILE} UPLAODED ALL [%s] FUNCTION CALL [%s]: %o', target_file, target_router, all_success); +} + +/** + * [fileUploadErrorFunction description] + * @param {String} target_file [description] + * @param {Number} file_pos [description] + * @param {String} target_router [description] + * @param {Object} control_data [description] + */ +function fileUploadErrorFunction(target_file, file_pos, target_router, control_data) +{ + console.log('{FILE} UPLOAD ERROR [%s/%s] FUNCTION CALL [%s]: %o', target_file, file_pos, target_router, control_data); + // eg print special error data from control +} + +// ** init here **/file-list +$(document).ready(function () { + // run and fill uploaded + var call_id = 'fileList'; + var queryString = { + action: call_id + }; + ajaxWrapper(call_id, queryString).done(function (data) { + console.log('Data: %o', data); + try { + if (data.status == 'error') { + // + } else { + $('#reference-id').val(data.content.reference_id); + // list files + if (data.status == 'warn') { + $('#file-info') + .addClass('error-warn') + .html(data.content.info); + } else { + $('#file-info').html(''); + for (const files of data.content.file_list) { + // console.log('F: %o', files); + $('#file-list').append(phfo( + aelx(cel('div', '', '', ['flx-ss']), + cel('div', '', files.name, ['mg-5']), + cel('div', '', files.create_date, ['mg-5']), + cel('div', '', files.size, ['mg-5']), + cel('input', 'uploaded-' + files.name, '', [], {type: 'hidden', value: files.name}) + ) + )); + } + } + } + // top message + printMsg(data.msg); + } catch (err) { + errorCatch(err); + } + }); + + // check that html elements needed are there + // fill the file upload part + initAjaxUploader({ + target_file: 'first_upload', + target_form: 'formdata', + max_files: 5, + target_router: 'fileUpload', + target_action: '', + form_parameters: {'parameter_a': 'Value 123'}, + auto_submit: false, + fileChange: fileChangeFunction, + fileChangeAll: fileChangeFunctionAll, + fileRemove: fileRemoveFunction, + fileClear: fileClearFunction, + fileBeforeUploadAll: fileAppendBeforeUploadFunctionAllFunction, + fileBeforeUpload: fileAppendBeforeUploadFunction, + fileUploaded: fileUploadedFunction, + fileUploadedAll: fileUploadedAllFunction, + fileUploadError: fileUploadErrorFunction + }); + // for second test + initAjaxUploader({ + target_file: 'second_upload', + target_form: 'formdata', + max_files: 1, + max_file_size: 500000, // ~500KB + // allowed_extensions: ['txt', 'csv'], + allowed_file_types: ['text/plain', 'text/csv'], + target_router: 'fileUpload', + target_action: '', + form_parameters: {'parameter_b': 'Value 456'}, + auto_submit: false, + fileChange: fileChangeFunction, + fileChangeAll: fileChangeFunctionAll, + fileRemove: fileRemoveFunction, + fileClear: fileClearFunction, + fileBeforeUploadAll: fileAppendBeforeUploadFunctionAllFunction, + fileBeforeUpload: fileAppendBeforeUploadFunction, + fileUploaded: fileUploadedFunction, + fileUploadedAll: fileUploadedAllFunction, + fileUploadError: fileUploadErrorFunction + }); + // for third test + initAjaxUploader({ + target_file: 'third_upload', + target_form: 'formdata', + max_files: 0, + target_router: 'fileUpload', + target_action: '', + form_parameters: {'parameter_c': 'Value 789'}, + auto_submit: false, + fileChange: fileChangeFunction, + fileChangeAll: fileChangeFunctionAll, + fileRemove: fileRemoveFunction, + fileClear: fileClearFunction, + fileBeforeUploadAll: fileAppendBeforeUploadFunctionAllFunction, + fileBeforeUpload: fileAppendBeforeUploadFunction, + fileUploaded: fileUploadedFunction, + fileUploadedAll: fileUploadedAllFunction, + fileUploadError: fileUploadErrorFunction + }); +}); diff --git a/test/uploaded/.gitignore b/test/uploaded/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/test/uploaded/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore