jsOMS/Views/FormView.js
Dennis Eichhorn 7b75ec58f7
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / general_module_workflow_js (push) Has been cancelled
fix permissions
2025-04-02 14:15:07 +00:00

803 lines
22 KiB
JavaScript

import { jsOMS } from '../Utils/oLib.js';
/**
* Form view.
*
* The form view contains a single form and it's data elements. Form actions are no longer handled by
* the browser but through this view. The view also provides additional functionality for non-default
* form elements such as canvas etc.
*
* @copyright Dennis Eichhorn
* @license OMS License 2.2
* @version 1.0.0
* @since 1.0.0
*
* @todo On change listener
* Allow to add a on change listener in a form. This should result in automatic submits after changing a form.
* Consider the following cases to submit the form:
* * on Enter (all except textarea)
* * on Change (by using a timer)
* * on Leave (all elements)
* The listener should be defined in the form definition once and in js be applied to all form elements.
* https://github.com/Karaka-Management/jsOMS/issues/60
*/
export class FormView
{
/**
* @constructor
*
* @param {string} id Form id
*
* @since 1.0.0
*/
constructor (id)
{
/** @type {string} id */
this.id = id;
/** @type {function} success */
this.success = null;
/** @type {function} finally */
this.finally = null;
/** @type {number} lastSubmit Last submit date time */
this.lastSubmit = 0;
/** @type {null|Element} form */
this.form = null;
/** @type {Array} submitInjects */
this.submitInjects = [];
/** @type {string} method */
this.method = 'POST';
/** @type {string} action */
this.action = '';
/** @type {bool} hasChanges */
this.hasChanges = false;
this.bind();
};
/**
* Initialize members
*
* Pulled out since this is used in a cleanup process
*
* @return {void}
*
* @since 1.0.0
*/
initializeMembers ()
{
this.submitInjects = [];
this.method = 'POST';
this.action = '';
};
/**
* Get method
*
* @return {string}
*
* @since 1.0.0
*/
getMethod ()
{
return this.method;
};
/**
* Get action
*
* @return {string}
*
* @since 1.0.0
*/
getAction ()
{
return this.action;
};
/**
* Get time of last submit
*
* @return {int}
*
* @since 1.0.0
*/
getLastSubmit ()
{
return this.lastSubmit;
};
/**
* Update last submit time
*
* @return {void}
*
* @since 1.0.0
*/
updateLastSubmit ()
{
this.lastSubmit = Math.floor(Date.now());
};
/**
* Is form data submitted on change?
*
* @return {boolean}
*
* @since 1.0.0
*/
isOnChange ()
{
const isOnChange = this.getFormElement().getAttribute('data-on-change');
return (isOnChange === 'true' || isOnChange === '1');
};
/**
* Get submit elements
*
* @param {null|Element} [e] Root element for search (null = whole document)
*
* @return {NodeListOf<any>}
*
* @since 1.0.0
*/
getSubmit (e = null)
{
const parent = e === null ? document : e;
return parent.querySelectorAll(
'#' + this.id + ' input[type=submit], '
+ 'button[form=' + this.id + '][type=submit], '
+ 'input[form=' + this.id + '][type=submit], '
+ '#' + this.id + ' button[type=submit], '
+ '.submit[data-form=' + this.id + '], '
+ '#' + this.id + ' .submit'
+ (e !== null ? ' .submit' : '')
);
};
/**
* Get submit elements
*
* @return {NodeListOf<any>}
*
* @since 1.0.0
*/
getImagePreviews () {
return document.querySelectorAll(
'#' + this.id + ' input[type=file].preview'
);
};
/**
* Get edit elements
*
* @param {null|Element} [e] Root element for search (null = whole document)
*
* @return {NodeListOf<Element>}
*
* @since 1.0.0
*/
getUpdate (e = null)
{
const parent = e === null ? document : e;
return parent.querySelectorAll(
'button[form=' + this.id + '].update-form, '
+ '.update-form[data-form=' + this.id + '], '
+ '#' + this.id + ' .update-form, '
+ '[form="' + this.id + '"].update-form'
+ (e !== null ? ', .update-form' : '')
);
};
/**
* Get save elements
*
* @param {null|Element} [e] Root element for search (null = whole document)
*
* @return {NodeListOf<Element>}
*
* @since 1.0.0
*/
getSave (e = null)
{
const parent = e === null ? document : e;
return parent.querySelectorAll(
'button[form=' + this.id + '].save-form, '
+ '.save-form[data-form=' + this.id + '], '
+ '#' + this.id + ' .save-form, '
+ '[form="' + this.id + '"].save-form'
+ (e !== null ? ', .save-form' : '')
);
};
/**
* Get save elements
*
* @param {null|Element} [e] Root element for search (null = whole document)
*
* @return {NodeListOf<Element>}
*
* @since 1.0.0
*/
getCancel (e = null)
{
const parent = e === null ? document : e;
return parent.querySelectorAll(
'#' + this.id + ' input[type=reset], '
+ 'button[form=' + this.id + '][type=reset], '
+ 'input[form=' + this.id + '][type=reset], '
+ '#' + this.id + ' button[type=reset], '
+ 'button[form=' + this.id + '].cancel-form, '
+ '.cancel-form[data-form=' + this.id + '], '
+ '#' + this.id + ' .cancel-form, '
+ '[form="' + this.id + '"].cancel-form'
+ (e !== null ? ', .cancel-form' : '')
);
};
/**
* Get remove buttons
*
* @param {null|Element} [e] Root element for search (null = whole document)
*
* @return {NodeListOf<Element>}
*
* @since 1.0.0
*/
getRemove (e = null)
{
const parent = e === null ? document : e;
return parent.querySelectorAll(
'button[form=' + this.id + '].remove-form, '
+ '.remove-form[data-form=' + this.id + '], '
+ '#' + this.id + ' .remove-form, '
+ '[form="' + this.id + '"].remove-form'
+ (e !== null ? ', .remove-form' : '')
);
};
/**
* Get add buttons
*
* The add button is different from the submit button since sometimes you want to show data to the user before you submit it.
*
* @param {null|Element} [e] Root element for search (null = whole document)
*
* @return {NodeListOf<Element>}
*
* @since 1.0.0
*/
getAdd (e = null)
{
const parent = e === null ? document : e;
return parent.querySelectorAll(
'button[form=' + this.id + '].add-form, '
+ '.add-form[data-form=' + this.id + '], '
+ '#' + this.id + ' .add-form, '
+ '[form="' + this.id + '"].add-form'
+ (e !== null ? ', .add-form' : '')
);
};
/**
* Get success callback
*
* @return {function}
*
* @since 1.0.0
*/
getSuccess ()
{
return this.success;
};
/**
* Set success callback
*
* @param {function} callback Callback
*
* @return {void}
*
* @since 1.0.0
*/
setSuccess (callback)
{
this.success = callback;
};
/**
* Get finally callback
*
* @return {function}
*
* @since 1.0.0
*/
getFinally ()
{
return this.finally;
};
/**
* Set finally callback
*
* @param {function} callback Callback
*
* @return {void}
*
* @since 1.0.0
*/
setFinally (callback) {
this.finally = callback;
};
/**
* Inject submit with post callback
*
* @param {function} callback Callback
*
* @return {void}
*
* @since 1.0.0
*/
injectSubmit (callback)
{
this.submitInjects.push(callback);
};
/**
* Get form elements
*
* @param {null|Object} [container] Data container, null = entire form or element e.g. table row
*
* @return {Element[]}
*
* @since 1.0.0
*/
getFormElements (container = null)
{
const form = container === null ? this.getFormElement() : container;
if (!form) {
return [];
}
const selects = form.getElementsByTagName('select');
const textareas = form.getElementsByTagName('textarea');
const inputs = [].slice.call(form.getElementsByTagName('input'));
const buttons = form.getElementsByTagName('button');
const canvas = form.getElementsByTagName('canvas');
const external = [].slice.call(document.querySelectorAll(':not(#' + this.id + ') [form=' + this.id + ']'));
const special = form.querySelectorAll('[data-name]');
const specialExt = document.querySelectorAll(':not(#' + this.id + ') [data-form=' + this.id + ']');
const inputLength = inputs.length;
const externalLength = external.length;
const specialLength = specialExt.length;
for (let i = 0; i < inputLength; ++i) {
if (inputs[i] === undefined
|| (typeof inputs[i] !== 'undefined'
&& (inputs[i].type === 'checkbox' || inputs[i].type === 'radio')
&& !inputs[i].checked)
) {
delete inputs[i];
}
}
for (let i = 0; i < externalLength; ++i) {
if (external[i] === undefined
|| (typeof external[i] !== 'undefined'
&& form.contains(external[i]))
) {
delete external[i];
continue;
}
if (external[i] === undefined
|| (typeof external[i] !== 'undefined'
&& (external[i].type === 'checkbox' || external[i].type === 'radio')
&& !external[i].checked)
) {
delete external[i];
}
}
for (let i = 0; i < specialLength; ++i) {
if (specialExt[i] === undefined
|| (typeof specialExt[i] !== 'undefined'
&& form.contains(specialExt[i]))
) {
delete specialExt[i];
continue;
}
}
return Array.prototype.slice.call(inputs).concat(
Array.prototype.slice.call(selects),
Array.prototype.slice.call(textareas),
Array.prototype.slice.call(buttons),
Array.prototype.slice.call(external),
Array.prototype.slice.call(special),
Array.prototype.slice.call(specialExt),
Array.prototype.slice.call(canvas)
).filter(function (val) { return val; });
};
/**
* Get first form element
*
* E.g. used to auto select first form element.
*
* @param {Element} e Root element for search (null = whole document)
*
* @return {Element}
*
* @since 1.0.0
*/
getFirstInputElement (e = null)
{
const parent = e === null ? document : e;
return parent.querySelector(
'#' + this.id + ' input, '
+ '#' + this.id + ' textarea, '
+ '#' + this.id + ' select, '
+ '[form=' + this.id + '], '
+ '[data-form=' + this.id + ']'
);
};
/**
* Get unique form elements
*
* @param {Array} arr Form element array
*
* @return {Array}
*
* @since 1.0.0
*/
getUniqueFormElements (arr)
{
const seen = {};
return arr.filter(function (item) {
return Object.prototype.hasOwnProperty.call(seen, item.name) ? false : (seen[item.name] = true);
});
};
/**
* Get form data
*
* @param {null|Element} Data container. Null = entire form, container e.g. single row in a table
*
* @return {FormData}
*
* @since 1.0.0
*/
getData (container = null)
{
const data = {};
const formData = new FormData();
const elements = this.getFormElements(container);
const length = elements.length;
let value = null;
for (let i = 0; i < length; ++i) {
const id = FormView.getElementId(elements[i]);
if (id === null) {
continue;
}
if (elements[i].tagName.toLowerCase() === 'canvas') {
value = elements[i].toDataURL('image/png');
} else if (elements[i].tagName.toLowerCase() === 'input' && elements[i].type === 'file') {
const filesLength = elements[i].files.length;
for (let j = 0; j < filesLength; ++j) {
formData.append(id + j, elements[i].files[j]);
}
} else if (elements[i].tagName.toLowerCase() === 'iframe') {
const iframeElements = Array.prototype.slice.call(
elements[i].contentWindow.document.querySelectorAll('[form=' + this.id + ']')
).concat(
Array.prototype.slice.call(
elements[i].contentWindow.document.querySelectorAll('[data-form=' + this.id + '] [data-name]')
)
).filter(function (val) { return val; });
const iframeLength = iframeElements.length;
for (let j = 0; j < iframeLength; ++j) {
value = iframeElements[j].value;
const iframeId = FormView.getElementId(iframeElements[j]);
if (Object.prototype.hasOwnProperty.call(data, iframeId)) {
if (data[iframeId].constructor !== Array) {
data[iframeId] = [data[iframeId]];
}
data[iframeId].push(value);
} else {
data[iframeId] = value;
}
}
continue;
} else if (typeof elements[i].value !== 'undefined') {
value = elements[i].value;
} else if (elements[i].getAttribute('data-value') !== null) {
value = elements[i].getAttribute('data-value');
} else {
value = elements[i].innerHTML;
}
// handle array data (e.g. table rows with same name or name="myname[]")
if (Object.prototype.hasOwnProperty.call(data, id)) {
if (data[id].constructor !== Array) {
data[id] = [data[id]];
}
data[id].push(value);
} else {
data[id] = value;
}
}
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
formData.append(
key,
data[key] !== null && data[key].constructor === Array
? JSON.stringify(data[key])
: data[key]
);
}
}
return formData;
};
/**
* Reset the form values to the default values
*
* @return {void}
*
* @since 1.0.0
*/
resetValues ()
{
const elements = this.getFormElements();
const length = elements.length;
const form = this.getFormElement();
form.reset();
for (let i = 0; i < length; ++i) {
const id = FormView.getElementId(elements[i]);
if (id === null) {
continue;
}
if (elements[i].tagName.toLowerCase() === 'canvas') {
elements[i].clearRect(0, 0, elements[i].width, elements[i].height);
} else if (elements[i].tagName.toLowerCase() === 'textarea') {
elements[i].innerHTML = '';
}
if (elements[i].getAttribute('data-value') !== null) {
elements[i].setAttribute('data-value', '');
}
if (elements[i].getAttribute('data-name') !== null) {
elements[i].innerHTML = '';
}
}
};
/**
* Get form id
*
* @return {string}
*
* @since 1.0.0
*/
getId ()
{
return this.id;
};
/**
* Get the form element
*
* @return {Element}
*
* @since 1.0.0
*/
getFormElement ()
{
return this.form === null ? (this.form = document.getElementById(this.id)) : this.form;
};
/**
* Validate form
*
* @param {null|Element[]}
*
* @return {boolean}
*
* @since 1.0.0
*/
isValid (data = null)
{
const elements = data === null ? this.getFormElements() : data;
const length = elements.length;
try {
for (let i = 0; i < length; ++i) {
if (!elements[i].required && elements[i].value === '') {
continue;
} else if ((elements[i].required && elements[i].value === '')
|| (elements[i].required && elements[i].getAttribute('type') === 'checkbox' && !elements[i].checked)
|| (typeof elements[i].pattern !== 'undefined'
&& elements[i].pattern !== ''
&& !(new RegExp(elements[i].pattern)).test(elements[i].value))
|| (typeof elements[i].maxlength !== 'undefined' && elements[i].maxlength !== '' && elements[i].value.length > elements[i].maxlength)
|| (typeof elements[i].minlength !== 'undefined' && elements[i].minlength !== '' && elements[i].value.length < elements[i].minlength)
|| (typeof elements[i].max !== 'undefined' && elements[i].max !== '' && elements[i].value > elements[i].max)
|| (typeof elements[i].min !== 'undefined' && elements[i].min !== '' && elements[i].value < elements[i].min)
) {
return false;
}
}
} catch (e) {
/** global: jsOMS */
jsOMS.Log.Logger.instance.error(e);
}
return true;
};
/**
* Get invalid data
*
* @param {null|Element[]}
*
* @return {Element[]}
*
* @since 1.0.0
*/
getInvalid (data = null)
{
const elements = data === null ? this.getFormElements() : data;
const length = elements.length;
const invalid = [];
try {
for (let i = 0; i < length; ++i) {
if ((elements[i].required && elements[i].value === '')
|| (typeof elements[i].pattern !== 'undefined'
&& elements[i].pattern !== ''
&& !(new RegExp(elements[i].pattern)).test(elements[i].value))
|| (typeof elements[i].maxlength !== 'undefined' && elements[i].maxlength !== '' && elements[i].value.length > elements[i].maxlength)
|| (typeof elements[i].minlength !== 'undefined' && elements[i].minlength !== '' && elements[i].value.length < elements[i].minlength)
|| (typeof elements[i].max !== 'undefined' && elements[i].max !== '' && elements[i].value > elements[i].max)
|| (typeof elements[i].min !== 'undefined' && elements[i].min !== '' && elements[i].value < elements[i].min)
) {
invalid.push(elements[i]);
}
}
} catch (e) {
/** global: jsOMS */
jsOMS.Log.Logger.instance.error(e);
}
return invalid;
};
/**
* Get form element id
*
* @param {Element} e Element to get id from
*
* @return {null|string}
*
* @since 1.0.0
*/
static getElementId (e)
{
if (e.getAttribute('name') !== null) {
return e.getAttribute('name');
} else if (e.getAttribute('data-name') !== null) {
return e.getAttribute('data-name');
} else if (e.getAttribute('id') !== null) {
return e.getAttribute('id');
} else if (e.getAttribute('type') !== null) {
return e.getAttribute('type');
}
return null;
};
/**
* Get submit injects
*
* @return {Object}
*
* @since 1.0.0
*/
getSubmitInjects ()
{
return this.submitInjects;
};
/**
* Bind form
*
* @return {void}
*
* @since 1.0.0
*/
bind ()
{
this.clean();
this.form = this.getFormElement();
if (this.form === null) {
return;
}
if (typeof this.form.attributes.method !== 'undefined') {
this.method = this.form.attributes.method.value;
} else if (typeof this.form.attributes['data-method'] !== 'undefined') {
this.method = this.form.attributes['data-method'].value;
} else {
this.method = 'EMPTY';
}
if (typeof this.form.attributes.action !== 'undefined') {
this.action = this.form.attributes.action.value;
} else if (typeof this.form.attributes['data-uri'] !== 'undefined') {
this.action = this.form.attributes['data-uri'].value;
} else {
this.action = 'EMPTY';
}
};
getElementsToBind (e = null)
{
const parent = e === null ? document : e;
const externalElements = parent.querySelectorAll('[form=' + this.id + ']');
return Array.prototype.slice.call(externalElements).concat(
Array.prototype.slice.call([this.form])
).filter(function (val) { return val; });
};
/**
* Clean form
*
* @return {void}
*
* @since 1.0.0
*/
clean ()
{
this.initializeMembers();
};
};