/** Class containing common UI functionalities */ class UIFactory { /** * Create and display a toast * @param {Object} options - The options to be passed to the Toaster class * @return {Object} The Toaster object */ toast(options) { const theToast = new Toaster(options); theToast.makeToast() theToast.show() return theToast } /** * Create and display a modal * @param {Object} options - The options to be passed to the ModalFactory class * @return {Object} The ModalFactory object */ modal(options) { const theModal = new ModalFactory(options); theModal.makeModal() theModal.show() return theModal } /** * Create a popover * @param {Object} element - The target element on which to attach the popover * @param {Object} options - The options to be passed to the PopoverFactory class * @return {Object} The PopoverFactory object */ popover(element, options) { const thePopover = new PopoverFactory(element, options); thePopover.makePopover() return thePopover } /** * Create and display a modal where the modal's content is fetched from the provided URL. Link an AJAXApi to the submission button * @param {string} url - The URL from which the modal's content should be fetched * @param {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that handles successful form submission * @param {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that handles form submissions errors and validation errors. * @param {Object=[]} modalOptions - Additional options to be passed to the modal constructor * @return {Promise} Promise object resolving to the ModalFactory object */ async submissionModal(url, POSTSuccessCallback, POSTFailCallback, modalOptions={}) { return AJAXApi.quickFetchURL(url).then((modalHTML) => { const defaultOptions = { rawHtml: modalHTML, POSTSuccessCallback: POSTSuccessCallback !== undefined ? POSTSuccessCallback : () => {}, POSTFailCallback: POSTFailCallback !== undefined ? POSTFailCallback : (errorMessage) => {}, } const options = Object.assign({}, defaultOptions, modalOptions) const theModal = new ModalFactory(options); theModal.makeModal() theModal.show() theModal.$modal.data('modalObject', theModal) return [theModal, theModal.ajaxApi] }).catch((error) => { UI.toast({ variant: 'danger', title: 'Error while loading the modal', body: error.message }) }) } /** * Create and display a modal where the modal's content is fetched from the provided URL * @param {string} url - The URL from which the modal's content should be fetched * @param {Object=[]} modalOptions - Additional options to be passed to the modal constructor * @return {Promise} Promise object resolving to the ModalFactory object */ async modalFromUrl(url, modalOptions={}) { return AJAXApi.quickFetchURL(url).then((modalHTML) => { const defaultOptions = { rawHtml: modalHTML, } const options = Object.assign({}, defaultOptions, modalOptions) const theModal = new ModalFactory(options); theModal.makeModal() theModal.show() theModal.$modal.data('modalObject', theModal) return [theModal, theModal.ajaxApi] }).catch((error) => { UI.toast({ variant: 'danger', title: 'Error while loading the modal', body: error.message }) }) } /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the single page view after a successful operation. * Supports `displayOnSuccess` option to show another modal after the submission * @param {string} url - The URL from which the modal's content should be fetched * @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming * @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success * @return {Promise} Promise object resolving to the ModalFactory object */ submissionModalForSinglePage(url, reloadUrl=false, $table=false) { let $statusNode, $reloadedElement if (reloadUrl === false) { reloadUrl = location.pathname } if ($table === false) { // Try to get information from the DOM const $elligibleTable = $('table[id^="single-view-table-"]') const $container = $elligibleTable.closest('div[id^="single-view-table-container-"]') $reloadedElement = $container $statusNode = $elligibleTable } else { if ($table instanceof jQuery) { $reloadedElement = $table $statusNode = $table.find('table[id^="single-view-table-"]') } else { $reloadedElement = $(`single-view-table-container-${$table}`) $statusNode = $(`single-view-table-${$table}`) } } if ($reloadedElement.length == 0) { UI.toast({ variant: 'danger', title: 'Could not find element to be reloaded', body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' }) return } return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode); } getContainerForTable($table) { const tableRandomID = $table.data('table-random-value') return $table.closest(`#table-container-${tableRandomID}`) } /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation. * Supports `displayOnSuccess` option to show another modal after the submission * @param {string} url - The URL from which the modal's content should be fetched * @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming * @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success * @return {Promise} Promise object resolving to the ModalFactory object */ submissionModalForIndex(url, reloadUrl=false, $table=false) { let $statusNode, $reloadedElement if (reloadUrl === false) { const currentModel = location.pathname.split('/')[1] if (currentModel.length > 0) { reloadUrl = `/${currentModel}/index` } else { UI.toast({ variant: 'danger', title: 'Could not find URL for the reload', body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' }) return } } if ($table === false) { // Try to get information from the DOM const $elligibleTable = $('table.table') const $container = $elligibleTable.closest('div[id^="table-container-"]') $reloadedElement = $container $statusNode = $elligibleTable } else { if ($table instanceof jQuery) { $reloadedElement = this.getContainerForTable($table) $statusNode = $table.find('table.table') } else { $reloadedElement = $(`#table-container-${$table}`) $statusNode = $(`#table-container-${$table} table.table`) } } if ($reloadedElement.length == 0) { UI.toast({ variant: 'danger', title: 'Could not find element to be reloaded', body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' }) return } return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode); } /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation. * Supports `displayOnSuccess` option to show another modal after the submission * @param {string} url - The URL from which the modal's content should be fetched * @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming * @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success * @return {Promise} Promise object resolving to the ModalFactory object */ submissionModalAutoGuess(url, reloadUrl=false, $table=false) { let currentAction = location.pathname.split('/')[2] if (currentAction !== undefined) { if (currentAction === 'index') { return UI.submissionModalForIndex(url, reloadUrl, $table) } else if (currentAction === 'view') { return UI.submissionModalForSinglePage(url, reloadUrl, $table) } } const successCallback = () => { UI.toast({ variant: 'danger', title: 'Could not reload the page', body: 'Reloading the page manually is advised.' }) } return UI.submissionModal(url, successCallback) } /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the provided element after a successful operation. * Supports `displayOnSuccess` option to show another modal after the submission * @param {string} url - The URL from which the modal's content should be fetched * @param {string} reloadUrl - The URL from which the data should be fetched after confirming * @param {(jQuery|string)} $reloadedElement - The element which should be reloaded on success * @param {(jQuery|string)} [$statusNode=null] - A reference to a HTML node on which the loading animation should be displayed. If not provided, $container will be used * @return {Promise} Promise object resolving to the ModalFactory object */ submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode=null) { const successCallback = function ([data, modalObject]) { UI.reload(reloadUrl, $reloadedElement, $statusNode) if (data.additionalData !== undefined) { if (data.additionalData.displayOnSuccess !== undefined) { UI.modal({ rawHtml: data.additionalData.displayOnSuccess }) } else if (data.additionalData.redirect !== undefined) { window.location = data.additionalData.redirect } } } return UI.submissionModal(url, successCallback) } /** * Fetch HTML from the provided URL and override the $container's content. $statusNode allows to specify another HTML node to display the loading * @param {string} url - The URL from which the $container's content should be fetched * @param {(jQuery|string)} $container - The container that should hold the data fetched * @param {(jQuery|string)} [$statusNode=null] - A reference to a HTML node on which the loading animation should be displayed. If not provided, $container will be used * @param {array} [additionalStatusNodes=[]] - A list of other node on which to apply overlay. Must contain the node and possibly the overlay configuration * @return {Promise} Promise object resolving to the $container object after its content has been replaced */ reload(url, $container, $statusNode=null, additionalStatusNodes=[]) { $container = $($container) $statusNode = $($statusNode) if (!$statusNode) { $statusNode = $container } const otherStatusNodes = [] additionalStatusNodes.forEach(otherStatusNode => { const loadingOverlay = new OverlayFactory(otherStatusNode.node, otherStatusNode.config) loadingOverlay.show() otherStatusNodes.push(loadingOverlay) }) return AJAXApi.quickFetchURL(url, { statusNode: $statusNode[0], }).then((theHTML) => { var $tmp = $(theHTML); $container.replaceWith($tmp) return $tmp; }).finally(() => { otherStatusNodes.forEach(overlay => { overlay.hide() }) }) } /** * Place an overlay onto a node and remove it whenever the promise resolves * @param {(jQuery|string)} node - The node on which the overlay should be placed * @param {Promise} promise - A promise to be fulfilled * @param {Object} [overlayOptions={} - The options to be passed to the overlay class * @return {Promise} Result of the passed promised */ overlayUntilResolve(node, promise, overlayOptions={}) { const $node = $(node) const loadingOverlay = new OverlayFactory($node[0], overlayOptions); loadingOverlay.show() promise.finally(() => { loadingOverlay.hide() }) return promise } /** * Place an overlay onto a node and remove it whenever the promise resolves * @param {(jQuery|string)} node - The node on which the confirm popover should be palced * @param {Object} options - The options to be passed to the overlay class * @return {Promise} Result of the passed promised */ quickConfirm(node, options={}) { const $node = $(node) const defaultOptions = { title: 'Confirm action', description: '', descriptionHtml: false, container: 'body', variant: 'success', confirmText: 'Confirm', confirm: function() {} } options = Object.assign({}, defaultOptions, options) options.description = options.descriptionHtml ? options.descriptionHtml : sanitize(options.description) const popoverOptions = { title: options.title, titleHtml: options.titleHtml, container: options.container, html: true, } var promiseResolve, promiseReject; const confirmPromise = new Promise(function (resolve, reject) { promiseResolve = resolve; promiseReject = reject; }) popoverOptions.content = function() { const $node = $(this) const $container = $('
') const $buttonCancel = $('Cancel') .click(function() { const popover = bootstrap.Popover.getInstance($node[0]) popover.dispose() }) const $buttonSubmit = $(`${options.confirmText}`) .click(function() { options.confirm() .then(function(result) { promiseResolve(result) }) .catch(function(error) { promiseReject(error) }) const popover = bootstrap.Popover.getInstance($node[0]) popover.dispose() }) $container.append(`

${options.description}

`) $container.append($(`
`).append($buttonCancel, $buttonSubmit)) return $container } const thePopover = this.popover($node, popoverOptions) thePopover.show() return confirmPromise // have to return a promise to avoid closing the modal } } /** Class representing a Toast */ class Toaster { /** * Create a Toast. * @param {Object} options - The options supported by Toaster#defaultOptions */ constructor(options) { this.options = Object.assign({}, Toaster.defaultOptions, options) if (this.options.delay == 'auto') { this.options.delay = this.computeDelay() } this.bsToastOptions = { autohide: this.options.autohide, delay: this.options.delay, } } /** * @namespace * @property {number} id - The ID to be used for the toast's container * @property {string} title - The title's content of the toast * @property {string} muted - The muted's content of the toast * @property {string} body - The body's content of the toast * @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the toast * @property {boolean} autohide - If the toast show be hidden after some time defined by the delay * @property {(number|string)} delay - The number of milliseconds the toast should stay visible before being hidden or 'auto' to deduce the delay based on the content * @property {(jQuery|string)} titleHtml - The raw HTML title's content of the toast * @property {(jQuery|string)} mutedHtml - The raw HTML muted's content of the toast * @property {(jQuery|string)} bodyHtml - The raw HTML body's content of the toast * @property {boolean} closeButton - If the toast's title should include a close button */ static defaultOptions = { id: false, title: false, muted: false, body: false, variant: 'default', autohide: true, delay: 'auto', titleHtml: false, mutedHtml: false, bodyHtml: false, closeButton: true, } /** Create the HTML of the toast and inject it into the DOM */ makeToast() { if (this.isValid()) { this.$toast = Toaster.buildToast(this.options) this.$toast.data('toastObject', this) $('#mainToastContainer').append(this.$toast) } } /** Display the toast to the user and remove it from the DOM once it get hidden */ show() { if (this.isValid()) { var that = this this.$toast.toast(this.bsToastOptions) .toast('show') .on('hide.bs.toast', function (evt) { const $toast = $(this) const hoveredElements = $(':hover').filter(function() { return $(this).is($toast) }); if (hoveredElements.length > 0) { evt.preventDefault() setTimeout(() => { $toast.toast('hide') }, that.options.delay); } }) .on('hidden.bs.toast', function () { that.removeToast() }) } } /** Remove the toast from the DOM */ removeToast() { this.$toast.remove(); } /** * Check wheter a toast is valid * @return {boolean} Return true if the toast contains at least data to be rendered */ isValid() { return this.options.title !== false || this.options.titleHtml !== false || this.options.muted !== false || this.options.mutedHtml !== false || this.options.body !== false || this.options.bodyHtml !== false } /** * Build the toast HTML * @param {Object} options - The options supported by Toaster#defaultOptions to build the toast * @return {jQuery} The toast jQuery object */ static buildToast(options) { var $toast = $('