@massimo-cassandro/js-file-uploader
Version:
HTML5 Async File Uploader
1,340 lines (1,076 loc) • 79 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.FileUploader = factory());
})(this, (function () { 'use strict';
const fupl_strings_it = {
// Mustache-like placeholders will be replaced with corresponding values
alert_too_much_files: 'Puoi selezionare un solo file!', // tentativo di trascinare più file con uploader singolo
alert_xhr_error: 'Si è verificato un errore nel caricamento del file “<strong>{{file_name}}</strong>”.', // errore ajax
alert_file_format_error: 'Il file “<strong>{{file_name}}</strong>” è di un formato non consentito',
alert_file_size_error: 'Le dimensioni di “<strong>{{file_name}}</strong>” ({{file_size}}) '+
'superano il valore massimo consentito ({{allowed_size}})',
fupl_alert_header: 'Errore',
fupl_alert_btn_text: 'OK',
// images sizes alerts
alert_img_exact_width_err: '<strong>{{file_name}}</strong>: larghezza non corrispondente ({{image_dimension}}px invece di {{allowed_dimension}}px)',
alert_img_min_width_err: '<strong>{{file_name}}</strong>: larghezza inferiore a quella minima consentita ({{image_dimension}}px invece di {{allowed_dimension}}px)',
alert_img_max_width_err: '<strong>{{file_name}}</strong>: larghezza superiore a quella massima consentita ({{image_dimension}}px invece di {{allowed_dimension}}px)',
alert_img_exact_height_err: '<strong>{{file_name}}</strong>: altezza non corrispondente ({{image_dimension}}px invece di {{allowed_dimension}}px)',
alert_img_min_height_err: '<strong>{{file_name}}</strong>: altezza inferiore a quella minima consentita ({{image_dimension}}px invece di {{allowed_dimension}}px)',
alert_img_max_height_err: '<strong>{{file_name}}</strong>: altezza superiore a quella massima consentita ({{image_dimension}}px invece di {{allowed_dimension}}px)',
alert_img_ratio_err: '<strong>{{file_name}}</strong>: la proporzione tra base e altezza dell’immagine non corrisponde a quella richiesta ({{aspect_ratio}})',
no_img_text: 'Nessuna immagine presente',
no_doc_text: 'Nessun file presente',
remove_btn_text: 'Elimina questo file',
// info text strings
info_text_std_imgs : 'immagini in formato <strong>JPEG</strong>, <strong>PNG</strong>, <strong>GIF</strong> o <strong>WEBP</strong>',
info_text_imgs_svg : 'immagini in formato <strong>JPEG</strong>, <strong>PNG</strong>, <strong>GIF</strong>, <strong>WEBP</strong> o <strong>SVG</strong>',
info_text_imgs_svg_size_info_text: '<strong>Solo per le immagini non SVG:</strong> ',
info_text_img_optimize_info: 'Per modificare e comprimere le immagini puoi utilizzare <a href="https://www.iloveimg.com/it" target="_blank" rel="noopener noreferrer">I Love IMG</a>.<br>' +
'Puoi ridurre le dimensioni dei documenti PDF con <a href="https://www.ilovepdf.com/it/comprimere_pdf" target="_blank" rel="noopener noreferrer">I Love PDF</a>.',
info_text_svg_optimize_info: 'È consigliabile ottimizzare i file SVG prima del caricamento, ' +
'ad esempio tramite <a href="https://jakearchibald.github.io/svgomg/" target="_blank" rel="noopener noreferrer">SVGO</a>',
info_text_img_fixed_size: 'dimensioni: <strong>{{img_w}}×{{img_h}}px</strong>',
info_text_img_equal_min_size: 'larghezza e altezza non inferiori a <strong>{{img_min_w}}px</strong>',
info_text_img_equal_max_size: 'larghezza e altezza non superiori a <strong>{{img_max_w}}px</strong>',
info_text_img_fixed_width : 'larghezza <strong>{{img_w}}px</strong>',
info_text_img_fixed_height : 'altezza <strong>{{img_h}}px</strong>',
info_text_img_width_range: 'larghezza compresa tra <strong>{{img_min_w}}px</strong> e <strong>{{img_max_w}}px</strong>',
info_text_img_min_width: 'larghezza non inferiore a <strong>{{img_min_w}}px</strong>',
info_text_img_max_width: 'larghezza non superiore a <strong>{{img_max_w}}px</strong>',
info_text_img_height_range: 'altezza compresa tra <strong>{{img_min_h}}px</strong> e <strong>{{img_max_h}}px</strong>',
info_text_img_min_height: 'altezza non inferiore a <strong>{{img_min_h}}px</strong>',
info_text_img_max_height: 'altezza non superiore a <strong>{{img_max_h}}px</strong>',
info_text_pdf_file: 'file in formato <strong>PDF</strong>',
info_text_svg_file: 'immagini in formato <strong>SVG</strong>',
info_text_max_filesize: 'max <strong>{{max_filesize}}</strong>',
info_text_img_aspect_ratio: 'Il rapporto tra base e altezza dell’immagine deve essere esattamente pari a <strong>{{img_aspect_ratio}}</strong>',
// select or drag labels
img_single_select_text : 'Seleziona un’immagine',
img_single_drag_text : '…oppure trascinala qui',
img_multiple_select_text : 'Seleziona una o più immagini',
img_multiple_drag_text : '…oppure trascinale qui',
doc_single_select_text : 'Seleziona un documento',
doc_single_drag_text : '…oppure trascinalo qui',
doc_multiple_select_text : 'Seleziona uno o più documenti',
doc_multiple_drag_text : '…oppure trascinali qui',
sortable_icon_title_text: 'Trascina per cambiare l’ordinamento'
};
/*
Some options contain Mustache-like variables (e.g.: {{xxx}}) that will be replaced
with the corresponding values of the localized strings items.
Variable name (the `xxx` part) must be the same as the string item name
*/
const default_options = {
// server side script url
uploader_url: null,
// locales for numbers parsing
locales: 'it-IT',
// ff true, console shows informations about current FileUploader Configuration
debug: false,
// method for ajax request
ajax_method: 'POST',
// disabled state
/*
If true, it prevents items from being loaded
The presence of the disabled attribute in the .fupl-wrapper element disables the element
input [type = file] generated by FileUploader (HTML 5 std behavior) and prevents drag & drop.
The disabled parameter can also be set via the element attribute
input [type = file] of fallback, if present.
Be carefully about changing disabled state of the input field on the fly:
to have it affect the FileUploader instance, you must also modify the disabled attribute
of the fieldset.fupl-wrapper element.
*/
disabled: false,
// types of selectable files. the value must match one of the `upl.mimetypes` arrays
filetype: 'auto',
/*
String of extensions or mimetypes separated by commas that identify
alloewd files to be uploaded,
in addition or replacement of what is set via the `filetype` parameter.
The `accept` attribute of the input field contained
in the FileUploader element (if present), performs the same function.
In both cases, the values are taken into account only if `filetype == 'auto'`
In the presence of both the `accept` parameter and the `accept` attribute,
a merge is performed, and the accept attribute takes precedence over the parameter.
If this parameter is `null`, the` accept` attribute is not present,
and `filetype` parameter is `auto` all file types are accepted.
*/
accept: null,
/*
Enable the ability to uploader multiple files.
This option can also be activated via the `multiple` attribute
of the `input` field (if present)
*/
multiple: false,
/*
Set the FileUploder as required
This option can also be activated via the `multiple` attribute
of the `input` field (if present)
*/
required: false,
// disables FileUploader submit button of the parent form element while
// FileUploader is performing an upload.
disable_submit: false,
// HTML templates
template_main: '<div class="fupl-result"></div>' +
'<div class="fupl-panel">' +
'<div class="fupl-button">' +
'<label><input type="file"></label>' +
'<div class="fupl-dd-text"></div>' +
'</div>' +
'<div class="fupl-info-text"></div>' +
'</div>',
template_empty_img: '<div class="fupl-result-empty text-muted small font-italic">{{no_img_text}}</div>',
template_empty_doc: '<div class="fupl-result-empty text-muted small font-italic">{{no_doc_text}}</div>',
// template of trigger for removing files
// This markup will be placed inside the `.fupl-remove` element,
// that must be present in the template of each FileUploader item
// It must be a button element
template_remove_btn: '<button type="button" class="close fupl-remove-trigger" aria-label="{{remove_btn_text}}" title="{{remove_btn_text}}">' +
'<span aria-hidden="true">×</span>' +
'</button>',
// Markup to show uploading progress
template_loading_element: '<div class="fupl-loading"><progress class="fupl-progress" max=100 value=0></progress></div>',
// Alternative loading feedback, used if progress.lengthComputable == false.
// In this case and if the `alternative_loading_func` function is not present,
// this string replaces the `.fupl-progress` element.
template_alternative_loading_element: '<div class="spinner-grow text-primary" role="status">' +
'<span class="sr-only">Loading...</span></div',
// items templates, if tag <a> is used, avoid `href` attribute
template_img_item_single: '<div class="fupl-item">' +
'<div class="fupl-remove"></div>' +
'<img alt="Immagine caricata" class="img-fluid fupl-img">' +
'<div class="fupl-file-info">' +
'<div class="text-truncate fupl-file-name"></div>' +
'<div class="fupl-file-size"></div>' +
'<div class="fupl-extra-fields"></div>' +
'</div>' +
'</div>',
template_img_item_multiple: '<div class="fupl-item">' +
'<div class="fupl-remove"></div>' +
'<div class="fupl-img-wrapper">' +
'<img alt="Immagine caricata" class="img-fluid fupl-img" draggable="false">' +
'</div>' +
'<div class="fupl-file-info">' +
'<div class="text-truncate fupl-file-name"></div>' +
'<div class="fupl-file-size"></div>' +
'</div>' +
'<div class="fupl-extra-fields"></div>' +
'<div class="fupl-sortable-icon"></div>' +
'</div>',
template_doc_item_single: '<div class="fupl-item">' +
'<div class="fupl-doc-wrapper">' +
'<div class="fupl-remove"></div>' +
'<div class="fupl-doc text-truncate">' +
'<a class="text-truncate fupl-file-name fupl-url" draggable="false"></a>' +
'</div>' +
'</div>' +
'<div class="small ml-1 text-nowrap fupl-file-size"></div>' +
'<div class="fupl-extra-fields"></div>' +
'<div class="fupl-sortable-icon"></div>' +
'</div>',
template_doc_item_multiple: null, // uses single doc templates
// Extra classes to be added to the FileUploader wrapper element (classes names separated by spaces)
wrapper_extra_class: null,
// template for error messages dialog
// if changed, .fupl-x classes must be maintained
fupl_alert_template:
`<div class="fupl-alert-overlay">
<div class="fupl-alert">
<div class="fupl-alert-header">{{fupl_alert_header}}</div>
<div class="fupl-alert-body"></div>
<div class="fupl-alert-footer">
<button type="button" class="fupl-alert-btn">{{fupl_alert_btn_text}}</button>
</div>
</div>
</div>`,
// classes to be added to alert button
// they can be changed without redefine entire alert template
// merging is perfomed in the `init` module
fupl_alert_btn_class: 'btn btn-secondary',
// Class(es) to be added to the FileUploader element when a file is dragged there from desktop
element_dragover_class: 'fupl-is-dragover',
// If true a legend tag will be added inside the the FileUploader wrapper (fieldset)
// It will be filled with the text of the label of the input[file] tag (if present)
// or with the uploader_legend_text parameter content
uploader_legend: true,
// Text of legend element (alternative to original label text)
uploader_legend_text: null,
// Class to be added to uploader_legend element
uploader_legend_class: null,
// classes to be added to the label of the generated nput[file] tag
input_label_class: 'btn btn-outline-primary btn-sm',
// show the information text on accepted formats, image size, etc.
show_info_text: true,
// show images optimization tips
show_optimize_info: true,
// strings added to the beginning and end of the generated info text
info_text_wrap_string: ['(', ')'],
// String used to join the various parts of the generated info text
info_text_join_string: ' - ',
// Custom info text. If present, replaces the generated info text
custom_info_text: null,
// help text. If present, is added after the info_text or custom_info_text
help_text: null,
/*
Images settings (only for filetype == 'img')
Numeric values that correspond to the pixel dimensions required for the image.
The img_min_* and img_max_* parameters can be assigned simultaneously,
but they are ignored if the corresponding exact parameters exist
(for example, if img_w is present, the parameters img_min_w and img_max_w
or img_aspect_ratio are not taken into consideration).
The default value of all parameters is null, which means that they are not applied.
Aspect ratio must be a number or string in w/h or w:h format or the result of w/h division.
Aspect ratio values are rounded to three decimal places
* `img_w` : exact image width
* `img_h` : exact image height
* `img_min_w` : min image width
* `img_min_h` : min image height
* `img_max_w` : max image width
* `img_max_h` : max image height
* `img_aspect_ratio` : aspect ratio w/h value (16/9, 4:3, 0.5, ecc)
*/
img_w : null,
img_h : null,
img_min_w : null,
img_min_h : null,
img_max_w : null,
img_max_h : null,
img_aspect_ratio : null,
/*
Aspect Ratio accuracy.
It is a number that indicates the decimal numbers to which the image size
ratio will be rounded for comparison with the `img_aspect_ratio` parameter
*/
aspect_ratio_accuracy: 2,
/*
Maximum size (weight) of the image. It can be a number,
and in this case it corresponds to a dimension in KB, or a string
composed of a numeric value and a suffix between ‘KB’ and ‘MB’ (also lowercase).
If the value is null or if the string is not recognized,
no limit is applied.
*/
max_filesize: null,
/*
Prefix of the variable used to send the data to the server.
The value indicates the base name of the generated varname
*/
varname: 'file',
/*
Prefix of the variable used to send the data to the server,
for extra fields of preregistered files only
(For compatibility with beta version)
*/
registered_extra_field_varname: null,
/*
Function called after the initialization of each FileUploader element.
Function is invoked passing the complete `fupl` obj as argument
*/
init_callback: null,
/*
Function called each time a file upload begins.
Function argument is an object that contains:
* `item`: current item object, it contains:
- `id`: element unique id
- `file`: current filelist object
- `width` and `height`: null or image dimensions (pixel)
* `img_preview` : base64 image preview (null if not an image)
* `fupl`: options & strings of current FileUploader instance
*/
upload_start_callback: null,
/*
Function called each time a file has been uploaded.
Function argument is an object that contains:
* `item`: current item object, it contains:
- `id`: element unique id
- `file`: current filelist object
- `width` and `height`: null or image dimensions (pixel)
- `tmp_file`: name of temporary file saved on the server
* `server_error`: error message strimg (null if no error occurred)
* `fupl`: options & strings of current FileUploader instance
*/
upload_complete_callback: null,
/*
Alternative upload progress function (see alternative_loading_element parameter)
If present, it replaces the standard one
Function argument is an object that contains:
- `progress_event`: upload progress event
- `fupl`: options & strings of current FileUploader instance
*/
alternative_loading_func: null,
/*
JSON array of preregistered element:
[
{
rel_id → id of the record related to the file (it's facultative and can be null).
It's usefully for many-to-many tables, and, if present,
is the one provided to delete a file from the server
id → unique id of the file (required)
name → file name (required)
url → url for <a> tag href attribute (if present) (facultative)
src → image `src` attribute (required for images only)
wi → image pixel width (for images only)
he → image pixel height (for images only)
size → bytes size
[...] → extra fields, if present
}
[...]
]
*/
values: [],
// varname use for deleted file generated hidden fields
delete_varname: 'elimina_file[]',
/*
if true fancybox integration is activated for images
fancybox is not present in FileUploader, and must be loaded in the page
Furthermore, to activate this option, an `url` parameter must be provided
(see `values` option)
*/
fancybox: false,
/*
markup of <a> tag to wrap image tags when fancybox option is active
if a `a.fupl-url` element is present in img templates string, this options
will not be used
*/
fancybox_anchor_markup: '<a class="fupl-url" data-fancybox="fupl-gallery"></a>',
/*
optional function to be called after fancybox markup is applied
to all elements and after `init_callback`.
Function is invoked passing the complete `fupl_options` obj as argument
*/
fancybox_callback_func: null,
// ========================================
// SORTABLE OPTIONS
// ========================================
/*
Enable the ability to rearrange items by dragging them.
This options adds one more hidden field for each file to the form, using the
`reorder_varname` parameter for generated varnames.
The reorder option is enabled if the reorder parameter is true, if
the multiple parameter is true and if the sortable_varname parameter is present
If the option is active, the values of any pre-recorded items
must be provided in the json `values` in the correct order
See `demo / reorder_demo.html` for a usage example
*/
sortable: false,
/*
sortable hidden item varname
it will be combined with main varname to obtain a string like
varname[item_id][sortable_varname]
*/
sortable_varname: 'fupl_order',
/*
markup for dragging icon
to be inserted in `.fupl-sortable-icon` element,
that must be present in the multiple item template
*/
sortable_icon: '<div title="{{sortable_icon_title_text}}"></div>',
// ========================================
// EXTRA FIELDS
// ========================================
/*
Markup to be added at the end of every uploaded item.
It is an array of objects each of which describes a field element:
[
{
"value_key": field key,
"markup": html string
},
...
]
* `value_key` identifies the field element and corresponds to the key used
also in the values object.
* `use_rel_id` (default false), if true, and the `values` parameter contains the
`rel_id` items, the `{{name}}` variable will be generated
using `rel_id` instead of `id`.
Only pre-registered items will be affected by this setting (since new items
never have a `rel_id`).
If `use_rel_id` is true but the `rel_id` item doesn't exist,
the `id` will be used
* `markup` is a HTML string which contains some Mustache-like placeholder:
- `{{idx}}` → unique id of the element
- `{{val}}` → content of `value` attribute, it corresponds to
`values[...][value_key]` value
- `{{checked}}` → if `values[...][value_key]` exists and it's different from
`0`, `null` or empty string, it is replaced with the `checked` attribute,
otherwise, with a space
- `{{selected}}` → same of the previous one, it is replaced with the `selected` attribute,
otherwise, with a a space
- `{{name}}` → is replaced with a PHP name string formed by
* the `varname` parameter
* the unique id or rel_id (according to use_rel_id setting) of the element
* the `value_key` string
Example: `file[fupl_00001][caption]`
Take a look to `extra_fields_demo.html` for a working demo.
*/
extra_fields: null
};
const fupl_utilities = {
/*
Mimetypes and extensions allowed according to `filetype` parameter
The `auto` parameter accepts all file types (except for limitations
added by `accept` parameter/attribute)
*/
mimetypes: (() => {
let mimetypes = {
auto : null,
img : ['image/png', 'image/jpeg', 'image/pjpeg', 'image/gif', 'image/webp',
'.png', '.jpg', '.jpeg', '.pjpg', '.pjpeg', '.gif', '.webp'],
pdf : ['application/pdf', '.pdf'],
svg : ['image/svg+xml', '.svg', '.svgz']
};
mimetypes['img+svg'] = mimetypes['img'].concat(mimetypes['svg']);
return mimetypes;
})(),
/*
isSuitableBrowser
Check the browser
returns `true` or `false`
*/
isSuitableBrowser: () => {
// IE
if( navigator.userAgent.indexOf('MSIE') !== -1 ||
navigator.appVersion.indexOf('Trident/') > -1 ||
navigator.userAgent.indexOf('Trident/') > -1 ){
return false;
}
var div = document.createElement( 'div' );
return ( ( 'draggable' in div ) || ( 'ondragstart' in div && 'ondrop' in div ) ) //TODO rimuovere ondragstart/ondrop (IE)?
&& 'FormData' in window
&& 'FileReader' in window
&& 'fetch' in window;
},
set_has_values: fupl => {
let items = fupl.opts.instance_result_wrapper.querySelectorAll('.fupl-item').length;
fupl.opts.wrapper.dataset.hasValues = items? 'true' : 'false';
if(!items) {
// fupl.opts.instance_result_wrapper.innerHTML = fupl.strs[`no_${fupl.opts._type}_text`];
fupl.opts.instance_result_wrapper.innerHTML =
fupl.opts._type === 'img'?
fupl.opts.template_empty_img.replace(/{{no_img_text}}/g, fupl.strs.no_img_text) :
fupl.opts.template_empty_doc.replace(/{{no_doc_text}}/g, fupl.strs.no_doc_text);
}
},
parse_bytes_value: (bytes, locales) => {
bytes = +bytes;
let mega = 1024*1024;
if(bytes >= mega ) {
return (bytes/mega).toLocaleString(locales, {maximumFractionDigits: 1}) + '<span class="fupl-unit">MB</span>';
} else if (bytes < 1024) {
return Number((bytes/1024).toFixed(2)).toLocaleString(locales, {maximumFractionDigits: 2}) +
'<span class="fupl-unit">KB</span>';
} else {
return Math.round(bytes/1024).toLocaleString(locales, {maximumFractionDigits: 0}) +
'<span class="fupl-unit">KB</span>';
}
},
/*
Parse the `max_filesize` parameter (if exists) and returns:
- `null` if max_filesize === null
- `false` if `max_filesize` is not a correct value
- the object `{ maxbytes: 123456, unit: 'KB', feedback_size: '1.2KB'}`
where
- `maxbytes` is max_filesize value in bytes
- `unit` is one of 'KB' and 'MB'
- `feedback_size` is a textual representation of max_filesize
for the purpose of providing end user informations
*/
parse_max_filesize: (filesize_value, locales) => {
// max_filesize checking and parsing
if( filesize_value ) {
var maxbytes, unit, feedback_size;
if(!isNaN(filesize_value)) { //solo numero, si assume siano KB
maxbytes = +filesize_value;
unit = 'KB';
feedback_size = maxbytes;
} else {
unit = filesize_value.substr(-2, 2).toUpperCase();
maxbytes = +filesize_value.substr(0, filesize_value.length-2);
feedback_size = maxbytes;
if(isNaN(maxbytes) || (unit !== 'KB' && unit !== 'MB') ) {
return false;
}
}
// If unit is KB but value is bigger than 1MB,
// unit is changed
if( maxbytes >= 1024 && unit === 'KB') {
unit = 'MB';
feedback_size = (Math.round((maxbytes / 1024) * 100) /100);
}
if(unit === 'KB') {
maxbytes = maxbytes * 1024;
} else {
maxbytes = maxbytes * 1024 * 1024;
}
return {
//'original_value': filesize_value,
//'unit' : unit,
'maxbytes' : maxbytes,
'feedback_size': feedback_size.toLocaleString(
locales, {maximumFractionDigits: (unit === 'KB'? 0 : 1)}
) + '<span class="fupl-unit">' + unit + '</span>'
};
} else {
return null;
}
},
exec_callback: (callback_function, params) => {
try {
if(typeof callback_function === 'string') {
let cbk_func = window;
callback_function.split('.').forEach( function(item) {
cbk_func = cbk_func[item];
});
cbk_func(params);
} else {
callback_function(params);
}
} catch(error) {
alert(`Error: “${callback_function}” callback function doesn't exist`);
console.error(error); // eslint-disable-line
}
}
};
/**
* create_info_text
* Creates a string of information about the file requirements
*
*/
function create_info_text(fupl) {
let info_text = [];
switch (fupl.opts.filetype) {
case 'img':
info_text.push( fupl.strs.info_text_std_imgs );
break;
case 'img+svg':
info_text.push( fupl.strs.info_text_imgs_svg);
break;
case 'svg':
info_text.push( fupl.strs.info_text_svg_file);
break;
case 'pdf':
info_text.push( fupl.strs.info_text_pdf_file);
break;
}
if( fupl.opts.max_filesize !== null ) {
info_text.push( fupl.strs.info_text_max_filesize );
}
if( fupl.opts.filetype === 'img' || fupl.opts.filetype === 'img+svg' ) {
let img_size_info = [];
if( fupl.opts.img_w && fupl.opts.img_h ) {
img_size_info.push( fupl.strs.info_text_img_fixed_size );
} else {
if(fupl.opts.img_min_w && fupl.opts.img_min_h && fupl.opts.img_min_w === fupl.opts.img_min_h) {
img_size_info.push( fupl.strs.info_text_img_equal_min_size );
} else if(fupl.opts.img_max_w && fupl.opts.img_max_h && fupl.opts.img_max_w === fupl.opts.img_max_h) {
img_size_info.push( fupl.strs.info_text_img_equal_max_size );
} else {
if( fupl.opts.img_w ) {
img_size_info.push( fupl.strs.info_text_img_fixed_width );
} else if( fupl.opts.img_min_w && fupl.opts.img_max_w ) {
img_size_info.push( fupl.strs.info_text_img_width_range );
} else if( fupl.opts.img_min_w ) {
img_size_info.push( fupl.strs.info_text_img_min_width );
} else if( fupl.opts.img_max_w ) {
img_size_info.push( fupl.strs.info_text_img_max_width );
}
if( fupl.opts.img_h ) {
img_size_info.push( fupl.strs.info_text_img_fixed_height );
} else if( fupl.opts.img_min_h && fupl.opts.img_max_h ) {
img_size_info.push( fupl.strs.info_text_img_height_range );
} else if( fupl.opts.img_min_h ) {
img_size_info.push( fupl.strs.info_text_img_min_height );
} else if( fupl.opts.img_max_h ) {
img_size_info.push( fupl.strs.info_text_img_max_height );
}
}
}
if(fupl.opts.img_aspect_ratio !== null) {
img_size_info.push( fupl.strs.info_text_img_aspect_ratio );
}
if(img_size_info.length) {
info_text.push(
(fupl.opts.filetype === 'img+svg' ? fupl.strs.info_text_imgs_svg_size_info_text : '') +
img_size_info.join(', ')
);
}
}
info_text = info_text.map( item => {
return item.replace(/{{img_w}}/, fupl.opts.img_w)
.replace(/{{img_h}}/, fupl.opts.img_h)
.replace(/{{img_min_w}}/, fupl.opts.img_min_w)
.replace(/{{img_min_h}}/, fupl.opts.img_min_h)
.replace(/{{img_max_w}}/, fupl.opts.img_max_w)
.replace(/{{img_max_h}}/, fupl.opts.img_max_h)
.replace(/{{img_aspect_ratio}}/, fupl.opts.img_aspect_ratio)
.replace(/{{max_filesize}}/, fupl.opts.max_filesize? fupl.opts.max_filesize.feedback_size : null);
});
let str = info_text.join(fupl.opts.info_text_join_string);
str = str.charAt(0).toUpperCase() + str.slice(1);
if( fupl.opts.info_text_wrap_string && str ) {
str = fupl.opts.info_text_wrap_string[0] + str + fupl.opts.info_text_wrap_string[1];
}
if(fupl.opts.filetype === 'img' || fupl.opts.filetype === 'img+svg') {
str += '<div>' + fupl.strs.info_text_img_optimize_info + '</div>';
}
if(fupl.opts.filetype === 'svg' || fupl.opts.filetype === 'img+svg') {
str += '<div>' + fupl.strs.info_text_svg_optimize_info + '</div>';
}
return str;
}
/*
based on
- https://github.com/WolfgangKurz/grabbable
references:
- https://codepen.io/therealDaze/pen/ZaoErp
- https://github.com/gridstack/gridstack.js
- https://developer.mozilla.org/it/docs/Web/API/HTML_Drag_and_Drop_API
- https://www.html5rocks.com/en/tutorials/dnd/basics/
- https://kryogenix.org/code/browser/custom-drag-image.html
- http://jsfiddle.net/zfnj5rv4/
*/
let dragged_element = null,
uploader_is_disabled = false;
const classes = {
// class added to the FileUploader elment (fupl_options.element)
// when a drag is started. It is removed at dragleave
sorting_class: 'fupl-sorting',
// same way, class added to the dragged element
sorting_item_class: 'fupl-item-sorting',
// class added to an item on dragover
over_item_class: 'fupl-item-dragover'
},
// broken events cleaning
resetAll = () => {
if( dragged_element ) {
dragged_element.classList.remove(classes.sorting_item_class);
dragged_element.parentNode.querySelectorAll('.' + classes.over_item_class).forEach(item => {
item.classList.remove(classes.over_item_class);
});
dragged_element.closest('.fupl').classList.remove(classes.sorting_class);
}
dragged_element = null;
};
function add_sortable_events(fupl_item, fupl_options) {
// start dragging
fupl_item.addEventListener('dragstart', function(e) {
uploader_is_disabled = fupl_options.wrapper.disabled;
resetAll();
if(!uploader_is_disabled) {
dragged_element = this;
// `classes.sorting_class` is added to the dragged element
// this prevents the feedback triggered when external files are dropped
// in the broswer (look at scss/_fupl.scss)
fupl_options.element.classList.add(classes.sorting_class);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text', 'fupl-sorting');
this.classList.add(classes.sorting_item_class);
}
}, false);
// enter positioning over another element (→ e.target == this)
fupl_item.addEventListener('dragenter', function(e) {
// disabled for external files or when the uploader is disabled
if( e.dataTransfer.getData('text') === 'fupl-sorting' && !uploader_is_disabled) {
if (e.stopPropagation) {
e.stopPropagation(); // stops the browser from redirecting.
}
if( this !== dragged_element ) {
this.classList.add(classes.over_item_class);
}
}
}, false);
// positioning over another element (→ e.target)
fupl_item.addEventListener('dragover', function(e) {
if( e.dataTransfer.getData('text') === 'fupl-sorting' && !uploader_is_disabled ) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
if( this !== dragged_element ) {
this.classList.add(classes.over_item_class);
}
}
return false;
}, false);
// exit positioning over another element
fupl_item.addEventListener('dragleave', function() {
this.classList.remove(classes.over_item_class);
}, false);
// drop
fupl_item.addEventListener('drop', function(e) {
if( e.dataTransfer.getData('text') === 'fupl-sorting' && !uploader_is_disabled) {
if (e.stopPropagation) {
e.stopPropagation(); // stops the browser from redirecting.
}
e.preventDefault();
if(dragged_element) {
if(this.previousElementSibling === dragged_element && this.nextElementSibling ) {
this.parentNode.insertBefore(dragged_element, this.nextElementSibling);
} else if( this.nextElementSibling ) {
this.parentNode.insertBefore(dragged_element, this);
// if target element is the last one, the dropped one is positioned after it
} else {
this.parentNode.insertAdjacentElement('beforeend', dragged_element);
}
}
resetAll();
}
return false;
}, false);
// end dragging
fupl_item.addEventListener('dragend', function(e) {
if( e.dataTransfer.getData('text') === 'fupl-sorting' && !uploader_is_disabled) {
fupl_options.element.classList.remove(classes.sorting_class);
resetAll();
// order variable
fupl_options.instance_result_wrapper.querySelectorAll('.fupl-sortable-order').forEach((item,idx) => {
item.value = idx;
});
}
}, false);
} // end add_sortable_events
function activate_sortable(fupl_options) {
// class added to `fupl_options.instance_result_wrapper`
// when `sortable` option is activated
fupl_options.instance_result_wrapper.classList.add( 'fupl-sortable' );
}
/*
Add an element to fupl.opts.instance_result_wrapper
and set all needed listeners
`item_data` obj contains all element data:
{
id → file unique id (can be file path too)
name → file name
url → url for any <a> tag present in the element (if it is an image can be a null or not set)
src → `src` attribute. Required if element is an image, otherwise null or not set
wi → image px width (null or not set if not image)
he → image px height (null or not set if not image)
size → file size (bytes)
loading → `true` if it is an item being uploaded
... → any additional instance-specific fields
}
`fupl` : is the fupl object
`preregistered` === true : means that the element was previously saved and comes
from the `fupl.opts.values` array
*/
function create_item(item_data, fupl, preregistered = false) {
try {
let item_markup = fupl.opts[`template_${fupl.opts._type}_item_${fupl.opts._mode}`];
if( item_markup === null && fupl.opts._mode === 'multiple' ) {
item_markup = fupl.opts[`template_${fupl.opts._type}_item_single`];
}
let fupl_item_wrapper = document.createElement('div'); // wrapper to be removed after inserting
fupl_item_wrapper.innerHTML = item_markup;
// delete button
let fupl_remove = fupl_item_wrapper.querySelector('.fupl-remove');
if(fupl_remove) {
fupl_remove.innerHTML = fupl.opts.template_remove_btn
.replace(/{{remove_btn_text}}/g, fupl.strs.remove_btn_text);
}
/*
element data
============================
each fupl-item element contains:
.fupl-file-name
.fupl-file-size
.fupl-img → img
.fupl-img → a.href
.fupl-doc → a.href
*/
// file name
let fupl_file_name = fupl_item_wrapper.querySelector('.fupl-file-name');
if(fupl_file_name && item_data.name ) {
fupl_file_name.innerHTML = item_data.name;
fupl_file_name.title = item_data.name;
}
// size info
let fupl_file_size = fupl_item_wrapper.querySelector('.fupl-file-size');
if(fupl_file_size ) {
let size_string = '';
if(fupl.opts._type === 'img' && item_data.wi && item_data.he) {
size_string = item_data.wi + '×' + item_data.he + '<span class="fupl-unit">px</span>';
if(item_data.size) {
size_string += ' – ';
}
}
if(item_data.size) {
size_string += fupl_utilities.parse_bytes_value(item_data.size, fupl.opts.locales);
}
fupl_file_size.innerHTML = size_string;
}
// image
if( fupl.opts._type === 'img' ) {
fupl_item_wrapper.querySelector('.fupl-img').src = item_data.src;
}
// url
let fupl_url = fupl_item_wrapper.querySelector('.fupl-url');
if( fupl_url && item_data.url) {
fupl_url.href = item_data.url;
}
let fupl_item = fupl_item_wrapper.querySelector('.fupl-item');
if(item_data.loading) {
fupl_item.classList.add('fupl-is-uploading');
fupl_item.insertAdjacentHTML('beforeend',
fupl.opts.template_loading_element
);
}
if(fupl.opts._mode === 'single') {
fupl.opts.instance_result_wrapper.innerHTML = fupl_item_wrapper.innerHTML;
} else {
// instance_result_wrapper cleaning (to remove 'no file' string, if present)
if( !fupl.opts.instance_result_wrapper.querySelector('.fupl-item')) {
fupl.opts.instance_result_wrapper.innerHTML = '';
}
fupl.opts.instance_result_wrapper.insertAdjacentHTML('beforeend',
fupl_item_wrapper.innerHTML
);
}
let fupl_item_dom = fupl.opts.instance_result_wrapper.querySelector('.fupl-item:last-child');
fupl_item_dom.dataset.id = item_data.id;
// remove element listener
let fupl_remove_trigger = fupl_item_dom.querySelector('.fupl-remove-trigger');
if(fupl_remove_trigger) {
fupl_remove_trigger.addEventListener('click', () => {
fupl_item_dom.remove();
let prereg_id = item_data.rel_id? item_data.rel_id : item_data.id;
if(prereg_id && preregistered) {
fupl.opts.wrapper.insertAdjacentHTML('beforeend',
`<input type="hidden" name="${fupl.opts.delete_varname}" value="${prereg_id}">`
);
}
// controllo se instance_result_wrapper è vuoto
// e impostazione di attributo e contenuti
fupl_utilities.set_has_values(fupl);
});
}
//fancybox integration
if( fupl.opts.fancybox && fupl.opts._type === 'img' && item_data.url && fupl.opts.fancybox_anchor_markup) {
// check for `a.fupl-url` tag and add it if necessary
if( !fupl_item_dom.querySelector('a.fupl-url') ) {
let img_element = fupl_item_dom.querySelector('.fupl-img'),
fancybox_wrapper = document.createElement('div');
fancybox_wrapper.innerHTML = fupl.opts.fancybox_anchor_markup;
fancybox_wrapper = fancybox_wrapper.firstChild;
img_element.parentNode.insertBefore(fancybox_wrapper, img_element);
fancybox_wrapper.appendChild(img_element);
}
fupl_item_dom.querySelector('a.fupl-url').setAttribute('href', item_data.url);
}
// extra fields
let extra_fields_wrapper = fupl_item_dom.querySelector('.fupl-extra-fields');
if(fupl.opts.extra_fields !== null && extra_fields_wrapper) {
fupl.opts.extra_fields.forEach( item => {
extra_fields_wrapper.insertAdjacentHTML('beforeend',
item.markup.replace(/{{idx}}/g, item_data.id)
.replace(/{{val}}/g, preregistered && item_data[item.value_key]? item_data[item.value_key] : '')
.replace(/{{checked}}/g, preregistered && +item_data[item.value_key]? ' checked ' : ' ')
.replace(/{{name}}/g,
(preregistered && fupl.opts.registered_extra_field_varname?
fupl.opts.registered_extra_field_varname : fupl.opts.varname) +
'[' +
((item.use_rel_id && item_data.rel_id)? item_data.rel_id : item_data.id) +
'][' + item.value_key + ']'
)
);
});
// add `selected` attribute to select extra fields
extra_fields_wrapper.querySelectorAll('select[data-selected]:not([data-selected=""]').forEach(sel => {
const value = sel.dataset.selected;
sel.querySelector(`option[value="${value}"]`)?.setAttribute('selected', true);
});
// stop bubbling when sortable
if( fupl.opts.sortable ) {
extra_fields_wrapper.querySelectorAll('select, input, textarea').forEach(item => {
item.setAttribute('draggable', 'false');
['dragstart', 'drag', 'mousedown'].forEach( ev => {
item.addEventListener(ev, e => {
if(ev !== 'mousedown') {e.preventDefault();}
e.stopPropagation();
});
});
});
}
}
// sortable
if( fupl.opts.sortable ) {
fupl_item_dom.setAttribute('draggable', true);
// order_value starts from zero
let order_value = fupl.opts.instance_result_wrapper.querySelectorAll('.fupl-item').length -1;
fupl_item_dom.insertAdjacentHTML('beforeend',
'<input type="hidden" class="fupl-sortable-order" name="' +
(preregistered && fupl.opts.registered_extra_field_varname?
fupl.opts.registered_extra_field_varname :
fupl.opts.varname) +
`[${item_data.id}][${fupl.opts.sortable_varname}]" value="${order_value}">`
);
if(fupl.opts.sortable_icon) {
fupl_item_dom.querySelector('.fupl-sortable-icon').innerHTML =
fupl.opts.sortable_icon
.replace('{{sortable_icon_title_text}}', fupl.strs.sortable_icon_title_text);
}
add_sortable_events(fupl_item_dom, fupl.opts);
}
return fupl.opts.instance_result_wrapper.querySelector('.fupl-item:last-child');
} catch(e) {
console.error(e); // eslint-disable-line
}
}
/*
Generate hidden fields with values to be sent to server
Returns the hidden fields html string
*/
function build_hidden_fields(current_item, fupl_options) {
// normalize ascii chars > 127 (and more)
const normalize_file_name = filename => {
let converted = '';
const std_char = '-', // char for standrd substitutions
conversionTable = { // Reference table Unicode vs ASCII
'à' : 'a', // 224
'è' : 'e', // 232
'é' : 'e', // 233
'ì' : 'i', // 236
'ò' : 'o', // 242
'ù' : 'u', // 249
'À' : 'A', // 192
'È' : 'E', // 200
'É' : 'E', // 201
'Ì' : 'I', // 204
'Ò' : 'O', // 210
'Ù' : 'U', // 217
'\'' : std_char, // 39
'|' : std_char, // 124
'!' : std_char, // 33
'"' : std_char, // 34
'$' : std_char, // 36
'%' : std_char, // 37
'&' : std_char, // 38
'/' : std_char, // 47
'(' : std_char, // 40
')' : std_char, // 41
'=' : std_char, // 61
'?' : std_char, // 63
'^' : std_char, // 94
'*' : std_char, // 42
'[' : std_char, // 91
']' : std_char, // 93
'ç' : 'c', // 231
'@' : std_char, // 64
'#' : std_char, // 35
'<' : std_char, // 60
'>' : std_char, // 62
'ü' : 'u', // 252
'Ü' : 'U', // 220
'ñ' : 'n', // 241
'Ñ' : 'N', // 209
'~' : std_char, // 126
':' : std_char,
'\\' : std_char
};
for(var i = 0; i < filename.length; i++) {
if( filename[i] in conversionTable) {
converted += conversionTable[filename[i]];
} else if(filename.charCodeAt(i) === 768 || filename.charCodeAt(i) === 769 ) { // accento grave e accento acuto
converted += '';
} else if(filename.charCodeAt(i) > 127 ) {
converted += std_char;
} else {
converted += filename.charAt(i);
}
}
return converted.replace(/ /g, std_char)
.replace(new RegExp(`^${std_char}+`), '')
.replace(new RegExp(`${std_char}+`,'g'), std_char);
};
let hidden_fields = '',
field_values = {
'tmp_file' : current_item.tmp_file,
'file_name' : normalize_file_name(current_item.file.name),
'size' : current_item.file.size,
'type' : current_item.file.type
};
// if(fupl_options._type === 'img') {
if(current_item.isBitmapImg) {
field_values.width = current_item.width;
field_values.height = current_item.height;
}
for (let _key in field_values) {
hidden_fields += '<input type="hidden" '+
'name="' + fupl_options.varname + '[' + current_item.id +'][' + _key + ']" '+
'value="' + ((field_values[_key] !== null && field_values[_key] !== undefined)? field_values[_key] : '') +'">';
}
return '<div class="fupl-hiddens">' + hidden_fields + '</div>';
}
// TODO update to dialog element
function fupl_alert(mes, fupl_opts) {
let alert_container = fupl_opts.element.querySelector('.fupl-alert-body ul');
if(typeof mes === 'string') {
mes = [mes];
}
if(!alert_container) {
fupl_opts.element.insertAdjacentHTML('beforeend',
fupl_opts.fupl_alert_template
);
fupl_opts.element.querySelector('.fupl-alert-body').innerHTML = '<ul></ul>';
alert_container = fupl_opts.element.querySelector('.fupl-alert-body ul');
fupl_opts.element.querySelector('.fupl-alert button').addEventListener('click', () => {
fupl_opts.element.querySelector('.fupl-alert-overlay').remove();
}, false);
}
mes.forEach(item => {
alert_container.insertAdjacentHTML('afterbegin', `<li>${item}</li>`);
});
}
/*
Performs all the checks, calls the remote url via ajax
and generates the feedback for the user
*/
function send_files(filelist, fupl) {
// disable the form submit button until the upload is completed
const disable_submit = (mode) => {
// mode === true → disable, false → enable
const _form = fupl.opts.element.closest('form');
if(fupl.opts.disable_submit && _form) {
const submit_btns = _form.querySelectorAll('[type="submit"]');
submit_btns.forEach( btn => {
btn.disabled = mode;
});
// TODO: removeEventListener doesn't work
// const submitHandler = (e) => {
// e.preventDefault();
// //return false;
// };
// if(mode === true) {
// _form.addEventListener('submit', submitHandler, false);
// } else {
// if( !fupl.opts.instance_result_wrapper.querySelector('.fupl-item.fupl-is-uploading')) {
// _form.removeEventListener('submit', submitHandler, false);
// }
// }
}
},
/*
uploadFile
ajax upload
https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/
current_item contains:
- `id`: element unique id
- `file`: filelist obj
- `width` e height: image pixel sizes. null if not an image
- `tmp_file`: temporary server file name (new uplaods only)
*/
uploadFile = function ( current_item, img_preview ) {
// disabling form
disable_submit(true);
// aggiunta elemento all'uploader
let this_item = create_item({
id : current_item.id,
name : current_item.file.name,
url : null,
src : img_preview,
wi : current_item.width,
he : current_item.height,
size : current_item.file.size,
// img_type : current_item.file.img_type, // TODO non necessario, controllare
loading : tr