Files
JavaScript.ajax-file-upload/src/ajaxFileUploadSimple.js
Clemens Schwaighofer 32455459da AJAX simple file uploader
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
Date:   Thu May 20 11:48:38 2021 +0900

    Updated ajax simple file uploader to allow multiple file uploads

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
commit 66878f89c82fe914031e113e780fd72e9ff09b78 ┃
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━
Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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 <clemens.schwaighofer@egplusww.com>
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
2021-10-12 08:55:21 +09:00

1223 lines
48 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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:
// <span>name<span>
// <span>progress/info</span>
// <span>delete from queue</span>
// <span>cancel upload</span>
// 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__