/** 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 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 {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that handles successful form submission * @param {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that handles form submissions errors and validation errors. * @return {Promise} Promise object resolving to the ModalFactory object */ modalFromURL(url, POSTSuccessCallback, POSTFailCallback) { return AJAXApi.quickFetchURL(url).then((modalHTML) => { const theModal = new ModalFactory({ rawHtml: modalHTML, POSTSuccessCallback: POSTSuccessCallback !== undefined ? POSTSuccessCallback : () => {}, POSTFailCallback: POSTFailCallback !== undefined ? POSTFailCallback : (errorMessage) => {}, }); theModal.makeModal(modalHTML) theModal.show() theModal.$modal.data('modalObject', theModal) return theModal }) } /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the table after a successful operation and handles displayOnSuccess option * @param {string} url - The URL from which the modal's content should be fetched * @param {string} tableId - The table ID which should be reloaded on success * @return {Promise} Promise object resolving to the ModalFactory object */ openModalFromURL(url, reloadUrl=false, tableId=false) { return UI.modalFromURL(url, (data) => { let reloaded = false if (reloadUrl === false || tableId === false) { // Try to get information from the DOM let $elligibleTable = $('table.table') let currentModel = location.pathname.split('/')[1] if ($elligibleTable.length == 1 && currentModel.length > 0) { let $container = $elligibleTable.closest('div[id^="table-container-"]') if ($container.length == 1) { UI.reload(`/${currentModel}/index`, $container, $elligibleTable) reloaded = true } else { $container = $elligibleTable.closest('div[id^="single-view-table-container-"]') if ($container.length == 1) { UI.reload(location.pathname, $container, $elligibleTable) reloaded = true } } } } else { UI.reload(reloadUrl, $(`#table-container-${tableId}`), $(`#table-container-${tableId} table.table`)) reloaded = true } if (data.additionalData !== undefined && data.additionalData.displayOnSuccess !== undefined) { UI.modal({ rawHtml: data.additionalData.displayOnSuccess }) } else { if (!reloaded) { location.reload() } } }) } /** * 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) => { $container.replaceWith(theHTML) return $container }).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 } } /** 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) 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} delay - The number of milliseconds the toast should stay visible before being hidden * @property {string} titleHtml - The raw HTML title's content of the toast * @property {string} mutedHtml - The raw HTML muted's content of the toast * @property {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: 5000, 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) $('#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('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 = $('