import { jsOMS } from '../../Utils/oLib.js'; import { Logger } from '../../Log/Logger.js'; import { NotificationLevel } from '../../Message/Notification/NotificationLevel.js'; import { NotificationMessage } from '../../Message/Notification/NotificationMessage.js'; import { NotificationType } from '../../Message/Notification/NotificationType.js'; import { Request } from '../../Message/Request/Request.js'; import { RequestMethod } from '../../Message/Request/RequestMethod.js'; import { RequestType } from '../../Message/Request/RequestType.js'; import { Response } from '../../Message/Response/Response.js'; import { FormView } from '../../Views/FormView.js'; import { GeneralUI } from '../GeneralUI.js'; import { UriFactory } from '../../Uri/UriFactory.js'; /** * Form manager class. * * @copyright Dennis Eichhorn * @license OMS License 2.0 * @version 1.0.0 * @since 1.0.0 * * @todo Adding a template to the DOM should modify its id/generate a custom/random id for the added element * for future handling as very often ids are required to identify and manage UI elements. * https://github.com/Karaka-Management/jsOMS/issues/102 * * @feature Auto update data changes in the backend (e.g. pull every x seconds, or use websockets for push) * https://github.com/Karaka-Management/Karaka/issues/151 */ export class Form { /** * @constructor * * @param {Object} app Application * * @since 1.0.0 */ constructor (app) { this.app = app; /** @type {FormView[]} forms */ this.forms = {}; /** @type {Object} ignore Forms to ignore */ this.ignore = {}; }; /** * Get form * * @param {string} id Form Id * * @return {void} * * @since 1.0.0 */ get (id) { if (!Object.prototype.hasOwnProperty.call(this.forms, id)) { this.bind(id); } return this.forms[id]; }; hasChanges () { const length = this.forms.length; for (let i = 0; i < length; ++i) { if (this.forms[i].hasChange()) { return true; } } return false; }; /** * Is form ignored? * * @param {string} id Form Id * * @return {boolean} * * @since 1.0.0 */ isIgnored (id) { return this.ignore.indexOf(id) !== -1; }; /** * Bind form * * @param {string} id Form Id (optional, if omitted all forms are searched and bound) * * @return {void} * * @since 1.0.0 */ bind (id = null) { if (id !== null && typeof this.ignore[id] === 'undefined') { this.bindForm(id); return; } const forms = document.querySelectorAll('form, [data-tag=form]'); const length = !forms ? 0 : forms.length; for (let i = 0; i < length; ++i) { const formId = forms[i].getAttribute('id'); if (typeof formId !== 'undefined' && formId !== null && typeof this.ignore[formId] === 'undefined') { this.bindForm(formId); } else { Logger.instance.info('A form doesn\'t have an ID.'); } } }; /** * Bind form * * @param {null|string} id Form Id * * @return {void} * * @since 1.0.0 */ bindForm (id = null) { if (id === null) { Logger.instance.info('A form doesn\'t have an ID.'); return; } // don't overwrite existing bind /* @todo removed because sometimes it is already bound but bound in a wrong way (e.g. no success is defined) if (Object.prototype.hasOwnProperty.call(this.forms, id)) { return; } */ this.forms[id] = new FormView(id); const self = this; let length = 0; // bind form clicks const toBind = this.forms[id].getElementsToBind(); const toBindLength = toBind.length; for (let i = 0; i < toBindLength; ++i) { toBind[i].addEventListener('click', function (event) { self.formActions(self, event, id); }); // If !isOnChange(): update window state on change // -> unhandled state changes // -> will be handled later // If isOnChange(): every change results in action // -> perform action on change if (!this.forms[id].isOnChange()) { toBind[i].addEventListener('change', function (evnt) { if (window.omsApp.state) { window.omsApp.state.hasChanges = true; } }); } } const imgPreviews = this.forms[id].getImagePreviews(); length = imgPreviews === null ? 0 : imgPreviews.length; for (let i = 0; i < length; ++i) { this.bindImagePreview(imgPreviews[i], id); } // if true submit form on change if (this.forms[id].isOnChange()) { const hasUiContainer = this.forms[id].getFormElement().getAttribute('data-ui-container'); const onChangeContainer = hasUiContainer !== null ? this.forms[id].getFormElement().querySelector(hasUiContainer) : this.forms[id].getFormElement(); onChangeContainer.addEventListener('change', function (event) { jsOMS.preventAll(event); if (window.omsApp.state) { window.omsApp.state.hasChanges = true; } const target = event.target.tagName.toLowerCase(); if (target === 'input' || target === 'textarea') { let dataParent = null; if (self.forms[id].getFormElement().tagName.toLowerCase() === 'table') { dataParent = event.srcElement.closest(self.forms[id].getFormElement().getAttribute('data-ui-element')); } self.submit(self.forms[id], null, dataParent, 'post'); } }); } }; formActionRemove (self, event, id, elementIndex) { jsOMS.preventAll(event); const remove = self.forms[id].getRemove()[elementIndex]; const callback = function (xhr) { if (xhr.status !== 200) { self.app.notifyManager.send( new NotificationMessage( NotificationLevel.ERROR, 'Failure', 'Some failure happened' ), NotificationType.APP_NOTIFICATION ); return; } /** * @var {Element} elementContainer Container which holds all the data for a specific element * (e.g. table row (tr), div, ...) */ const elementContainer = remove.closest(document.getElementById(id).getAttribute('data-ui-element')); window.omsApp.logger.log(document.getElementById(id).getAttribute('data-ui-element')); elementContainer.parentNode.removeChild(elementContainer); }; /** @var {Element} formElement Form element */ const formElement = document.getElementById(id).getAttribute('action') !== null || document.getElementById(id).getAttribute('data-action') !== null ? self.forms[id].getFormElement() : ( document.getElementById(id).getAttribute('data-update-form') !== null ? self.forms[document.getElementById(id).getAttribute('data-update-form')].getFormElement() : ( document.getElementById(id).getAttribute('data-delete-form') !== null ? self.forms[document.getElementById(id).getAttribute('data-delete-form')].getFormElement() : null ) ); // Perform the element deletion. // If the form has a remote endpoint this is called in advance if (formElement !== null) { const deleteRequest = new Request( formElement.hasAttribute('data-action-delete') ? formElement.getAttribute('data-action-delete') : (formElement.tagName.toLowerCase() !== 'form' ? formElement.getAttribute('data-action') : formElement.getAttribute('action') ), RequestMethod.DELETE ); deleteRequest.setResultCallback(0, callback); deleteRequest.send(); } else { callback(); } } formActionAdd (self, event, id, elementIndex) { jsOMS.preventAll(event); if (!self.forms[id].isValid()) { return; } if (document.getElementById(id).getAttribute('data-add-form') === null) { this.formActionAddInline(self, event, id, elementIndex); } else { this.formActionAddExternal(self, event, id, elementIndex); } } formActionAddInline (self, event, id, elementIndex) { // Since the add is inline no form exists which the user can use, hence it must be created /** @var {HTMLElement} formElement */ const formElement = self.forms[id].getFormElement(); /** @var {string} uiContainerName Container which holds all elements (e.g. div, tbody) */ const uiContainerName = formElement.getAttribute('data-ui-container'); /** @var {HTMLElement} uiContainer Container which holds all elements (e.g. div, tbody) */ const uiContainer = uiContainerName.charAt(0) === '#' ? document.querySelector(uiContainerName) : formElement.querySelector(uiContainerName); if (formElement.getAttribute('data-update-tpl')) { /** @var {HTMLElement} newElement New element to add */ const newElement = uiContainer.querySelector(formElement.getAttribute('data-update-tpl')).content.cloneNode(true); uiContainer.appendChild(newElement.firstElementChild); } else { /** @var {HTMLElement} newElement New element to add */ const newElement = uiContainer.querySelector(formElement.getAttribute('data-add-tpl')).content.cloneNode(true); uiContainer.appendChild(newElement.firstElementChild); } } formActionAddExternal (self, event, id, elementIndex) { /** @var {HTMLElement} formElement External form */ const formElement = self.forms[id].getFormElement(); /** @var {string} uiContainerName Container which holds all elements (e.g. div, tbody) */ const uiContainerName = formElement.getAttribute('data-ui-container'); /** @var {HTMLElement} uiContainer Container which holds all elements (e.g. div, tbody) */ const uiContainer = uiContainerName.charAt(0) === '#' ? document.querySelector(uiContainerName) : formElement.querySelector(uiContainerName); /** @var {string[]} addTpl Templates to add to container (usually only one) */ const addTpl = formElement.getAttribute('data-add-tpl').split(','); const addTplLength = addTpl.length; /** @var {string[]} vals Values to add (values can be different from the displayed text) */ let vals = []; /** @var {string[]} texts Text to add (values can be different from the displayed text) */ let texts = []; /** * @var {Element[]} newElements Array of added elements * (this is actually only one element/model/object but sometimes one * element might be split up into multiple templates) */ const newElements = []; // iterate over all add templates and find the elements for (let i = 0; i < addTplLength; ++i) { // add template to elements which should be added to the DOM newElements.push(document.querySelector(addTpl[i]).content.cloneNode(true)); /** @var {string} tplValue Where does the value come from for this template input element */ const tplValue = newElements[i].querySelector('[data-tpl-value]').getAttribute('data-tpl-value'); /** @var {Element} dataOriginElement Element where the value data comes from */ const dataOriginElement = tplValue.startsWith('http') || tplValue.startsWith('{') ? newElements[i].firstElementChild // data comes from remote source : formElement; // data comes from the form (even if the api returns something after adding). // What if remote returns a DB id? how do we add it? // is this a @todo ? probably yes. // maybe first use local data and then if remote data available replace local data? vals = vals.concat( dataOriginElement.hasAttribute('data-tpl-value') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-value]')) ); texts = texts.concat( dataOriginElement.hasAttribute('data-tpl-text') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-text]')) ); // set random id for element Form.setRandomIdForElement(newElements[i].firstElementChild); } /** @var {object} remoteUrls Texts and values which come from remote sources */ const remoteUrls = {}; // define remote response behavior self.forms[id].setSuccess(function (response, xhr) { if (xhr.status !== 200) { return; } // insert values into form (populate values) Form.setDataInElement('value', newElements, vals, remoteUrls); // insert text data into form (populate text) Form.setDataInElement('text', newElements, texts, remoteUrls); // add new elements to the DOM for (let i = 0; i < addTplLength; ++i) { uiContainer.appendChild(newElements[i].firstElementChild); } window.omsApp.logger.log(remoteUrls); UriFactory.setQuery('$id', response.get('response').id); // fill elements with remote data after submit (if the template expects data from a remote source) // this is usually the case for element ids, which can only be generated by the backend Form.setDataFromRemoteUrls(remoteUrls); // reset the form after adding an element self.forms[id].resetValues(); }); // submit to api self.submit(self.forms[id], self.forms[id].getAdd()[elementIndex]); } formActionSave (self, event, id, elementIndex) { jsOMS.preventAll(event); if (document.querySelector('[data-update-form="' + id + '"') === null) { this.formActionSaveInline(self, event, id, elementIndex); } else { this.formActionSaveExternal(self, event, id, elementIndex); } } formActionSaveInline (self, event, id, elementIndex) { if (!self.forms[id].isValid()) { return; } /** @var {HTMLElement} formElement */ const formElement = self.forms[id].getFormElement(); /** @var {string} uiContainerName Container which holds all elements (e.g. div, tbody) */ const uiContainerName = formElement.getAttribute('data-ui-container'); /** @var {HTMLElement} uiContainer Container which holds all elements (e.g. div, tbody) */ const uiContainer = uiContainerName.charAt(0) === '#' ? document.querySelector(uiContainerName) : formElement.querySelector(uiContainerName); /** * @var {string[]} updateElementNames Names/selectors of the containers which hold the data of a single element * (this is not the container which holds all elements. Most of the time this is just a single element (e.g. tr)) */ const updateElementNames = formElement.getAttribute('data-ui-element').split(','); const updateElementLength = updateElementNames.length; /** * @var {Element[]} updateElements Array of update elements * (this is actually only one element/model/object but sometimes one * element might be split up into multiple containers) */ const updateElements = []; /** @var {Element} elementContainer Element container that holds the data that should get updated */ const elementContainer = event.target.closest(formElement.getAttribute('data-ui-element')); /** @var {string[]} vals New values */ let vals = []; /** @var {string[]} texts New texts */ let texts = []; if (elementContainer.getAttribute('data-id') === null || elementContainer.getAttribute('data-id') === '' ) { // is save from add /** @var {string[]} addTpl Templates to add to container (usually only one) */ const addTpl = formElement.getAttribute('data-add-tpl').split(','); const addTplLength = addTpl.length; /** * @var {Element[]} newElements Array of added elements * (this is actually only one element/model/object but sometimes one * element might be split up into multiple templates) */ const newElements = []; // iterate over all add templates and find the elements for (let i = 0; i < addTplLength; ++i) { // add template to elements which should be added to the DOM newElements.push(document.querySelector(addTpl[i]).content.cloneNode(true)); /** @var {string} tplValue Where does the value come from for this template input element */ const tplValue = newElements[i].querySelector('[data-tpl-value]').getAttribute('data-tpl-value'); /** @var {Element} dataOriginElement Element where the value data comes from */ const dataOriginElement = tplValue.startsWith('http') || tplValue.startsWith('{') ? newElements[i].firstElementChild // data comes from remote source : elementContainer; // data comes from the form (even if the api returns something after adding). // What if remote returns a DB id? how do we add it? // is this a @todo ? probably yes. // maybe first use local data and then if remote data available replace local data? vals = vals.concat( dataOriginElement.hasAttribute('data-tpl-value') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-value]')) ); texts = texts.concat( dataOriginElement.hasAttribute('data-tpl-text') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-text]')) ); // set random id for element Form.setRandomIdForElement(newElements[i].firstElementChild); } /** @var {object} remoteUrls Texts and values which come from remote sources */ const remoteUrls = {}; // define remote response behavior self.forms[id].setSuccess(function (response, xhr) { if (xhr.status !== 200) { return; } // insert values into form (populate values) Form.setDataInElement('value', newElements, vals, remoteUrls); // insert text data into form (populate text) Form.setDataInElement('text', newElements, texts, remoteUrls); // add new elements to the DOM for (let i = 0; i < addTplLength; ++i) { uiContainer.appendChild(newElements[i].firstElementChild); } elementContainer.parentNode.removeChild(elementContainer); window.omsApp.logger.log(remoteUrls); UriFactory.setQuery('$id', response.get('response').id); // fill elements with remote data after submit (if the template expects data from a remote source) // this is usually the case for element ids, which can only be generated by the backend Form.setDataFromRemoteUrls(remoteUrls); }); } else { // is save from update // iterate all element containers (very often only one element container) and find the elements for (let i = 0; i < updateElementLength; ++i) { updateElementNames[i] = updateElementNames[i].trim(); // get the elment to update // @todo maybe stupid, because same as elementContainer. however this is more general? anyway, one can be replaced updateElements.push( formElement.querySelector(updateElementNames[i] + '[data-id="' + elementContainer.getAttribute('data-id') + '"]') ); /** @var {string} updateValue Where does the value come from for this template input element */ const updateValue = updateElements[i].querySelector('[data-tpl-value]').getAttribute('data-tpl-value'); /** @var {Element} dataOriginElement Element where the value data comes from */ const dataOriginElement = updateValue.startsWith('http') || updateValue.startsWith('{') ? updateElements[i].firstElementChild // data comes from remote source : elementContainer; // data comes from the form (even if the api returns something after adding). // What if remote returns a DB id? how do we add it? // is this a @todo ? probably yes. // maybe first use local data and then if remote data available replace local data? vals = vals.concat( dataOriginElement.hasAttribute('data-tpl-value') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-value]')) ); texts = texts.concat( dataOriginElement.hasAttribute('data-tpl-text') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-text]')) ); } /** @var {Element} elementContainer Original element */ const element = uiContainer.querySelector('.hidden[data-id="' + elementContainer.getAttribute('data-id') + '"]'); /** @var {object} remoteUrls Texts and values which come from remote sources */ const remoteUrls = {}; jsOMS.removeClass(element, 'vh'); // todo bind failure here, if failure do cancel, if success to remove edit template self.forms[id].setSuccess(function (response, xhr) { if (xhr.status !== 200) { return; } // update values in form (overwrite values) Form.setDataInElement('value', [element], vals, remoteUrls); // update text data in form (overwrite text) Form.setDataInElement('text', [element], texts, remoteUrls); elementContainer.parentNode.removeChild(elementContainer); // overwrite old values from remote response Form.setDataFromRemoteUrls(remoteUrls); }); } } formActionSaveExternal (self, event, id, elementIndex) { const mainForm = document.querySelector('[data-update-form="' + id + '"'); const externalFormId = id; id = mainForm.getAttribute('id'); if (!self.forms[id].isValid()) { return; } /** @var {HTMLElement} formElement */ const formElement = self.forms[id].getFormElement(); /** @var {HTMLElement} externalFormElement External form element */ const externalFormElement = self.forms[externalFormId].getFormElement(); /** * @var {string[]} updateElementNames Names/selectors of the containers which hold the data of a single element * (this is not the container which holds all elements. Most of the time this is just a single element (e.g. tr)) */ const updateElementNames = formElement.getAttribute('data-ui-element').split(','); const updateElementLength = updateElementNames.length; /** * @var {Element[]} updateElements Array of update elements * (this is actually only one element/model/object but sometimes one * element might be split up into multiple containers) */ const updateElements = []; /** @var {string[]} vals New values */ let vals = []; /** @var {string[]} texts New texts */ let texts = []; // iterate all element containers (very often only one element container) and find the elements for (let i = 0; i < updateElementLength; ++i) { updateElementNames[i] = updateElementNames[i].trim(); // get the elment to update updateElements.push( formElement.querySelector(updateElementNames[i] + '[data-id="' + externalFormElement.getAttribute('data-id') + '"]') ); /** @var {string} updateValue Where does the value come from for this template input element */ const updateValue = updateElements[i].querySelector('[data-tpl-value]').getAttribute('data-tpl-value'); /** @var {Element} dataOriginElement Element where the value data comes from */ const dataOriginElement = updateValue.startsWith('http') || updateValue.startsWith('{') ? updateElements[i].firstElementChild // data comes from remote source : externalFormElement; // data comes from the form (even if the api returns something after adding). // What if remote returns a DB id? how do we add it? // is this a @todo ? probably yes. // maybe first use local data and then if remote data available replace local data? vals = vals.concat( dataOriginElement.hasAttribute('data-tpl-value') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-value]')) ); texts = texts.concat( dataOriginElement.hasAttribute('data-tpl-text') ? dataOriginElement : Array.prototype.slice.call(dataOriginElement.querySelectorAll('[data-tpl-text]')) ); } /** @var {object} remoteUrls Texts and values which come from remote sources */ const remoteUrls = {}; for (let i = 0; i < updateElementLength; ++i) { jsOMS.removeClass(updateElements[i], 'animated'); jsOMS.removeClass(updateElements[i], 'greenCircleFade'); requestAnimationFrame((time) => { requestAnimationFrame((time) => { jsOMS.addClass(updateElements[i], 'animated'); jsOMS.addClass(updateElements[i], 'medium-duration'); jsOMS.addClass(updateElements[i], 'greenCircleFade'); }); }); } self.forms[externalFormId].setSuccess(function (response, xhr) { if (xhr.status !== 200) { // reset form values to default values after performing the update self.forms[externalFormId].resetValues(); return; } // update values in form (overwrite values) Form.setDataInElement('value', updateElements, vals, remoteUrls); // update text data in form (overwrite text) Form.setDataInElement('text', updateElements, texts, remoteUrls); // overwrite old values from remote response Form.setDataFromRemoteUrls(remoteUrls); // Color highlight for (let i = 0; i < updateElementLength; ++i) { jsOMS.removeClass(updateElements[i], 'animated'); jsOMS.removeClass(updateElements[i], 'greenCircleFade'); jsOMS.removeClass(updateElements[i], 'medium-duration'); requestAnimationFrame((_) => { requestAnimationFrame((_) => { // Important: all classes need to be done in one go otherwise it doesn't work (timing issue?) jsOMS.addClass(updateElements[i], 'animated medium-duration greenCircleFade'); }); }); } // reset form values to default values after performing the update self.forms[externalFormId].resetValues(); }); // clear element id after saving externalFormElement.setAttribute('data-id', ''); // submit to api self.submit(self.forms[externalFormId], self.forms[externalFormId].getSave()[elementIndex]); // show add button + hide update button + hide cancel button const addButtons = self.forms[externalFormId].getAdd(); let buttonLength = addButtons.length; for (let i = 0; i < buttonLength; ++i) { jsOMS.removeClass(addButtons[i], 'vh'); } const saveButtons = self.forms[externalFormId].getSave(); buttonLength = saveButtons.length; for (let i = 0; i < buttonLength; ++i) { jsOMS.addClass(saveButtons[i], 'vh'); } const cancelButtons = self.forms[externalFormId].getCancel(); buttonLength = cancelButtons.length; for (let i = 0; i < buttonLength; ++i) { jsOMS.addClass(cancelButtons[i], 'vh'); } } formActionCancel (self, event, id, elementIndex) { const ele = document.getElementById(id); if (ele.getAttribute('data-update-form') === null && ele.tagName.toLowerCase() !== 'form') { this.formActionCancelInline(self, event, id, elementIndex); } else { this.formActionCancelExternal(self, event, id, elementIndex); } } formActionCancelInline (self, event, id, elementIndex) { /** @var {HTMLElement} formElement Form */ const formElement = self.forms[id].getFormElement(); /** @var {string} uiContainerName Container which holds all elements (e.g. div, tbody) */ const uiContainerName = formElement.getAttribute('data-ui-container'); /** @var {HTMLElement} uiContainer Container which holds all elements (e.g. div, tbody) */ const uiContainer = uiContainerName.charAt(0) === '#' ? document.querySelector(uiContainerName) : formElement.querySelector(uiContainerName); /** @var {Element} elementContainer Element container that holds the data that should get updated */ const elementContainer = event.target.closest(formElement.getAttribute('data-ui-element')); /** @var {Element} elementContainer Original element */ const element = uiContainer.querySelector('.hidden[data-id="' + elementContainer.getAttribute('data-id') + '"]'); jsOMS.removeClass(element, 'vh'); elementContainer.parentNode.removeChild(elementContainer); } formActionCancelExternal (self, event, id, elementIndex) { // reset form values to default values self.forms[id].resetValues(); // reset element id self.forms[id].getFormElement().setAttribute('data-id', ''); let length = 0; // show add button + hide update button + hide cancel button const addButtons = self.forms[id].getAdd(); length = addButtons.length; for (let i = 0; i < length; ++i) { jsOMS.removeClass(addButtons[i], 'vh'); } const saveButtons = self.forms[id].getSave(); length = saveButtons.length; for (let i = 0; i < length; ++i) { jsOMS.addClass(saveButtons[i], 'vh'); } const cancelButtons = self.forms[id].getCancel(); length = cancelButtons.length; for (let i = 0; i < length; ++i) { jsOMS.addClass(cancelButtons[i], 'vh'); } } formActionUpdate (self, event, id, elementIndex) { // this doesn't handle setting new values but populating the update form jsOMS.preventAll(event); if (document.getElementById(id).getAttribute('data-update-form') === null) { this.formActionUpdateInline(self, event, id, elementIndex); } else { this.formActionUpdateExternal(self, event, id, elementIndex); } } formActionUpdateInline (self, event, id, elementIndex) { /** @var {HTMLElement} formElement Form */ const formElement = self.forms[id].getFormElement(); /** @var {string} uiContainerName Container which holds all elements (e.g. div, tbody) */ const uiContainerName = formElement.getAttribute('data-ui-container'); /** @var {Element} elementContainer Element container that holds the data that should get updated */ const elementContainer = event.target.closest(formElement.getAttribute('data-ui-element')); jsOMS.addClass(elementContainer, 'vh'); /** @var {NodeListOf} values Value elements of the element to update */ const values = elementContainer.querySelectorAll('[data-tpl-value]'); /** @var {NodeListOf} texts Text elements of the element to update */ const texts = elementContainer.querySelectorAll('[data-tpl-text]'); /** @var {HTMLElement} uiContainer Container which holds all elements (e.g. div, tbody) */ const uiContainer = uiContainerName.charAt(0) === '#' ? document.querySelector(uiContainerName) : formElement.querySelector(uiContainerName); /** @var {string[]} addTpl Templates to add to container (usually only one) */ const addTpl = formElement.getAttribute('data-update-tpl').split(','); const addTplLength = addTpl.length; /** * @var {Element[]} newElements Array of added elements * (this is actually only one element/model/object but sometimes one * element might be split up into multiple templates) */ const newElements = []; // iterate over all add templates and find the elements for (let i = 0; i < addTplLength; ++i) { // add template to elements which should be added to the DOM newElements.push(document.querySelector(addTpl[i]).content.cloneNode(true)); // set random id for element newElements[i].firstElementChild.setAttribute('data-id', elementContainer.getAttribute('data-id')); } /** @var {object} remoteUrls Texts and values which come from remote sources */ const remoteUrls = {}; // insert values into form (populate values) Form.setDataInElement('value', newElements, values, remoteUrls); // insert text data into form (populate text) Form.setDataInElement('text', newElements, texts, remoteUrls); // add new elements to the DOM for (let i = 0; i < addTplLength; ++i) { uiContainer.insertBefore(newElements[i].firstElementChild, elementContainer); } const saveButtons = self.forms[id].getSave(); let buttonLength = saveButtons.length; for (let i = 0; i < buttonLength; ++i) { jsOMS.removeClass(saveButtons[i], 'vh'); } const cancelButtons = self.forms[id].getCancel(); buttonLength = cancelButtons.length; for (let i = 0; i < buttonLength; ++i) { jsOMS.removeClass(cancelButtons[i], 'vh'); } // define remote response behavior self.forms[externalFormId].setSuccess(function (response) { if (response.get('status') !== 'undefined' && response.get('status') !== NotificationLevel.HIDDEN ) { self.app.notifyManager.send( new NotificationMessage(response.get('status'), response.get('title'), response.get('message')), NotificationType.APP_NOTIFICATION ); } window.omsApp.logger.log(remoteUrls); UriFactory.setQuery('$id', response.get('response').id); // fill elements with remote data after submit (if the template expects data from a remote source) Form.setDataFromRemoteUrls(remoteUrls); }); } formActionUpdateExternal (self, event, id, elementIndex) { /** @var {HTMLElement} formElement */ const formElement = self.forms[id].getFormElement(); /** @var {Element} elementContainer Element container that holds the data that should get updated */ const elementContainer = event.target.closest(formElement.getAttribute('data-ui-element')); /** @var {string} externalFormId Id of the form where the data should get populated to (= external form) */ const externalFormId = formElement.getAttribute('data-update-form'); /** @var {NodeListOf} values Value elements of the element to update */ const values = elementContainer.querySelectorAll('[data-tpl-value]'); /** @var {NodeListOf} texts Text elements of the element to update */ const texts = elementContainer.querySelectorAll('[data-tpl-text]'); let length = 0; // clear form values to prevent old values getting mixed with update values self.forms[externalFormId].resetValues(); // set the element id in the update form so we know which element is getting updated self.forms[externalFormId].getFormElement().setAttribute('data-id', elementContainer.getAttribute('data-id')); // hide add button + show update button + show cancel button const addButtons = self.forms[externalFormId].getAdd(); length = addButtons.length; for (let i = 0; i < length; ++i) { jsOMS.addClass(addButtons[i], 'vh'); } const saveButtons = self.forms[externalFormId].getSave(); length = saveButtons.length; for (let i = 0; i < length; ++i) { jsOMS.removeClass(saveButtons[i], 'vh'); } const cancelButtons = self.forms[externalFormId].getCancel(); length = cancelButtons.length; for (let i = 0; i < length; ++i) { jsOMS.removeClass(cancelButtons[i], 'vh'); } /** @var {object} remoteUrls Texts and values which come from remote sources */ const remoteUrls = {}; // insert values into form (populate values) Form.insertDataIntoForm(self, 'value', externalFormId, values, remoteUrls); // This prevents overwriting results from setting data by value in the next step length = texts.length; const length2 = values.length; for (let i = 0; i < length; ++i) { const tagName = texts[i].tagName.toLowerCase(); if (tagName === 'select') { for (let j = 0; j < length2; ++j) { if (values[j].getAttribute('name') === texts[i].getAttribute('name')) { texts[i] = null; break; } } } } // insert text data into form (populate text) Form.insertDataIntoForm(self, 'text', externalFormId, texts, remoteUrls); // define remote response behavior self.forms[externalFormId].setSuccess(function (response) { if (response.get('status') !== 'undefined' && response.get('status') !== NotificationLevel.HIDDEN ) { self.app.notifyManager.send( new NotificationMessage(response.get('status'), response.get('title'), response.get('message')), NotificationType.APP_NOTIFICATION ); } window.omsApp.logger.log(remoteUrls); UriFactory.setQuery('$id', response.get('response').id); // fill elements with remote data after submit (if the template expects data from a remote source) Form.setDataFromRemoteUrls(remoteUrls); }); } formActions (self, event, id) { let elementIndex = 0; if ((elementIndex = Array.from(self.forms[id].getRemove()).indexOf(event.target)) !== -1) { this.formActionRemove(self, event, id, elementIndex); } else if ((elementIndex = Array.from(self.forms[id].getAdd()).indexOf(event.target)) !== -1) { this.formActionAdd(self, event, id, elementIndex); } else if ((elementIndex = Array.from(self.forms[id].getSave()).indexOf(event.target)) !== -1) { this.formActionSave(self, event, id, elementIndex); } else if ((elementIndex = Array.from(self.forms[id].getCancel()).indexOf(event.target)) !== -1) { jsOMS.preventAll(event); // @todo currently only handling update cancel, what about add cancel? this.formActionCancel(self, event, id, elementIndex); } else if ((elementIndex = Array.from(self.forms[id].getUpdate()).indexOf(event.target)) !== -1) { // handle update // this doesn't handle setting new values but populating the update form this.formActionUpdate(self, event, id, elementIndex); } else if ((elementIndex = Array.from(self.forms[id].getSubmit()).indexOf(event.target)) !== -1 || (elementIndex = Array.from(self.forms[id].getSubmit()).indexOf(event.target.parentNode)) !== -1 ) { jsOMS.preventAll(event); self.submit(self.forms[id], self.forms[id].getSubmit()[elementIndex]); } else if (false) { // eslint-disable-line no-constant-condition // @todo if table head input field in popups changes -> check if input empty -> deactivate -> checkbox : else activate checkbox // careful: the same checkbox is used for showing the filter popup. maybe create a separate checkbox, which only handles the highlighting if filter is defined. // this means popup active highlights filter icon AND different content checkbox also highlights filter // -> two hidden checkboxes are necessary (one is already implemented) // Consider: It might make sense to do this in the Table.js??? Kinda depends on additional functionality together with the form probably. } // @todo if input change check if iframe needs to be reloaded (if there is a iframe that is attached/part of the form // e.g. media renderer based on currently selected element) // remote actions (maybe solvable with callbacks?): // filter // sort // reorder // remove // add // save // update // dragndrop } /** * Create the new input * * @param {string} imageUpload Create form * @param {Object} id Id * * @return {void} * * @since 1.0.0 */ bindImagePreview (imageUpload, id) { imageUpload.addEventListener('change', function () { const preview = document.querySelector('#preview-' + imageUpload.getAttribute('name')); preview.src = window.URL.createObjectURL(imageUpload.files[0]); preview.onload = function () { window.URL.revokeObjectURL(this.src); }; }); }; /** * Submit form * * Calls injections first before executing the actual form submit * * @param {FormView} form Form object * @param {Element} button Action different from the form action (e.g. formaction=*) * @param {string} method Form method * @param {string} action Form action * * @return {void} * * @since 1.0.0 */ submit (form, button = null, container = null, method = null, action = null) { /* Handle injects */ const self = this; const injects = form.getSubmitInjects(); let counter = 0; if (button !== null) { action = button.getAttribute('formaction'); } if (button !== null) { method = button.getAttribute('formmethod'); } // Register normal form behavior if (!this.app.eventManager.isAttached(form.getId())) { this.app.eventManager.attach(form.getId(), function () { self.submitForm(form, action, method, container); }, true); } // Run all injects first for (const property in injects) { if (Object.prototype.hasOwnProperty.call(injects, property)) { ++counter; // this.app.eventManager.addGroup(form.getId(), counter); const result = injects[property](form, form.getId()); if (result === false) { return; } } else { Logger.instance.warning('Invalid property.'); } } if (counter < 1) { this.app.eventManager.trigger(form.getId()); } // select first input element (this allows fast consecutive data input) const firstFormInputElement = form.getFirstInputElement(); if (firstFormInputElement !== null) { firstFormInputElement.focus(); } }; /** * Submit form data * * Submits the main form data * * @param {FormView} form Form object * @param {string} [action] Action different from the form action (e.g. formaction=*) * * @return {void} * * @since 1.0.0 */ submitForm (form, action = null, method = null, container = null) { const data = form.getData(container); if (!form.isValid(data)) { this.app.notifyManager.send( new NotificationMessage( NotificationLevel.INFO, jsOMS.lang.Info, jsOMS.lang.invalid_form ), NotificationType.APP_NOTIFICATION ); Logger.instance.debug('Form "' + form.getId() + '" has invalid values.'); return; } // Avoid multiple submits if (form.getMethod() !== RequestMethod.GET && Math.abs(Date.now() - form.getLastSubmit()) < 500 ) { return; } form.updateLastSubmit(); /* Handle default submit */ const request = new Request(); const self = this; const redirect = form.getFormElement().getAttribute('data-redirect'); request.setData(data); request.setType(RequestType.FORM_DATA); // @todo consider to allow different request type request.setUri(action !== null ? action : form.getAction()); request.setMethod(method !== null ? method : form.getMethod()); request.setResultCallback(0, function (xhr) { window.omsApp.logger.log(xhr.response); const headerLocation = xhr.getResponseHeader('location'); if (headerLocation !== null) { window.location = headerLocation; } if (xhr.getResponseHeader('content-type').includes('application/octet-stream')) { const blob = new Blob([xhr.response], { type: 'application/octet-stream' }); const doc = document.createElement('a'); doc.style = 'display: none'; document.body.appendChild(doc); const url = window.URL.createObjectURL(blob); doc.href = url; const disposition = xhr.getResponseHeader('content-disposition'); let filename = ''; if (disposition && disposition.indexOf('attachment') !== -1) { const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; const matches = filenameRegex.exec(disposition); if (matches !== null && matches[1]) { filename = matches[1].replace(/['"]/g, ''); } } doc.download = filename; doc.click(); window.URL.revokeObjectURL(url); document.body.removeChild(doc); } else if (xhr.getResponseHeader('content-type').includes('text/html')) { // window.location = UriFactory.build(uri); document.documentElement.innerHTML = xhr.response; /* This is not working as it reloads the page ?! document.open(); document.write(html); document.close(); */ window.omsApp.reInit(); // @todo fix memory leak which most likely exists because of continuous binding without removing binds } else { try { const o = JSON.parse(xhr.response)[0]; const response = new Response(o); let successInject = null; const statusCode = parseInt(xhr.getResponseHeader('status')); if ((successInject = form.getSuccess()) !== null && (statusCode === 200 || statusCode === null) ) { successInject(response, xhr); } if (response.get('type') !== null) { self.app.responseManager.run(response.get('type'), response.get(), null); } else if (typeof o.status !== 'undefined' && o.status !== NotificationLevel.HIDDEN) { self.app.notifyManager.send( new NotificationMessage(o.status, o.title, o.message), NotificationType.APP_NOTIFICATION ); } } catch (e) { Logger.instance.log(e); Logger.instance.error('Invalid form response. \n' + 'URL: ' + form.getAction() + '\n' + 'Request: ' + JSON.stringify(form.getData()) + '\n' + 'Response: ' + xhr.response ); self.app.notifyManager.send( new NotificationMessage( NotificationLevel.ERROR, 'Failure', 'Some failure happened' ), NotificationType.APP_NOTIFICATION ); } } if (redirect !== null && (statusCode === 200 || statusCode === null) ) { fetch(UriFactory.build(redirect)) .then((response) => response.text()) .then((html) => { document.documentElement.innerHTML = html; if (window.omsApp.state) { window.omsApp.state.hasChanges = false; } history.pushState({}, null, UriFactory.build(redirect)); /* This is not working as it reloads the page ?! document.open(); document.write(html); document.close(); */ // @todo fix memory leak which most likely exists because of continuous binding without removing binds window.omsApp.reInit(); }) .catch((error) => { console.warn(error); }); } }); if (window.omsApp.state) { window.omsApp.state.hasChanges = false; } request.send(); if (form.getFinally() !== null) { form.getFinally()(); } }; /** * Count the bound forms * * @return {int} * * @since 1.0.0 */ count () { return this.forms.length; }; static setDataFromRemoteUrls (remoteUrls) { for (const e in remoteUrls) { const request = new Request(e); request.setResultCallback(200, function (xhr) { const remoteUrlsLength = remoteUrls[e].length; for (let k = 0; k < remoteUrlsLength; ++k) { const path = remoteUrls[e][k].path; if (remoteUrls[e][k].type === 'value') { GeneralUI.setValueOfElement(remoteUrls[e][k].element, path !== null ? jsOMS.getArray(path, JSON.parse(xhr.response)) : xhr.response ); } else { GeneralUI.setTextOfElement(remoteUrls[e][k].element, path !== null ? jsOMS.getArray(path, JSON.parse(xhr.response)) : xhr.response ); } } }); request.send(); } }; /** * Set random data-id of a element * * @param {HTMLElement} element Element to set the data-id for * * @return {string} * * @since 1.0.0 */ static setRandomIdForElement (element) { if (element.getAttribute('data-id') !== null && element.getAttribute('data-id') !== '') { return; } let eleId = ''; do { eleId = 'r-' + Math.random().toString(36).substring(7); } while (document.querySelector('[data-id="' + eleId + '"]') !== null); element.setAttribute('data-id', eleId); return eleId; }; static setDataInElement (type, elements, data, remoteUrls = {}) { const changedNodes = []; // prevent same node touching const length = data.length; const elementsLength = elements.length; for (let i = 0; i < length; ++i) { // data path if data comes from remote object const path = data[i].hasAttribute('data-tpl-' + type + '-path') ? data[i].getAttribute('data-tpl-' + type + '-path') : null; for (let j = 0; j < elementsLength; ++j) { // sometimes elements contains templates, they need to get handled differently const element = elements[j] instanceof DocumentFragment ? elements[j].firstElementChild // is template -> need first element : elements[j]; // is element const matches = element.hasAttribute('data-tpl-' + type) && element.getAttribute('data-tpl-' + type) === data[i].getAttribute('data-tpl-' + type) ? [element] : element.querySelectorAll( '[data-tpl-' + type + '="' + data[i].getAttribute('data-tpl-' + type) + '"' ); const matchLength = matches.length; for (let c = 0; c < matchLength; ++c) { // ensure correct element. // if this doesn't exist the matches from above contains alle elements with the same uri/path but eventually different tpl-paths if (changedNodes.includes(matches[c]) || (path !== null && path !== matches[c].getAttribute('data-tpl-' + type + '-path')) ) { continue; } changedNodes.push(matches[c]); if (data[i].getAttribute('data-tpl-' + type).startsWith('http') || data[i].getAttribute('data-tpl-' + type).startsWith('{') ) { Form.populateRemoteUrls(matches[c], type, data[i], path, remoteUrls); } else { if (type === 'value') { GeneralUI.setValueOfElement(matches[c], GeneralUI.getValueFromDataSource(data[i])); } else if (type === 'text') { GeneralUI.setTextOfElement(matches[c], GeneralUI.getTextFromDataSource(data[i])); } } } } } }; static insertDataIntoForm (self, type, formId, data, remoteUrls = {}) { const length = data.length; for (let i = 0; i < length; ++i) { if (data[i] === null) { continue; } const matches = self.forms[formId].getFormElement().querySelectorAll('[data-tpl-' + type + '="' + data[i].getAttribute('data-tpl-' + type) + '"'); const path = data[i].hasAttribute('data-tpl-' + type + '-path') ? data[i].getAttribute('data-tpl-' + type + '-path') : null; const matchLength = matches.length; for (let c = 0; c < matchLength; ++c) { if (data[i].getAttribute('data-tpl-' + type).startsWith('http') || data[i].getAttribute('data-tpl-' + type).startsWith('{') ) { Form.populateRemoteUrls(matches[c], type, data[i], path, remoteUrls); } else { if (type === 'value') { GeneralUI.setValueOfElement(matches[c], GeneralUI.getValueFromDataSource(data[i])); } else if (type === 'text' && (data[i].getAttribute('data-tpl-text') !== data[i].getAttribute('data-tpl-value') || (data[i].getAttribute('data-value') !== null) ) ) { GeneralUI.setTextOfElement(matches[c], GeneralUI.getTextFromDataSource(data[i])); } } } } }; static populateRemoteUrls (ele, type, data, path, remoteUrls) { const uri = data.getAttribute('data-tpl-' + type).startsWith('/') ? document.getElementsByTagName('base')[0].href : ''; if (remoteUrls[uri + data.getAttribute('data-tpl-' + type)] === undefined) { remoteUrls[uri + data.getAttribute('data-tpl-' + type)] = []; } remoteUrls[uri + data.getAttribute('data-tpl-' + type)].push({ path: path, element: ele, type: type }); }; };