import {ModalProxy} from './modalProxy'; import {parseHTML} from './util'; import {remoteForm} from '@github/remote-form'; let lastTaskId = 0; export const modalStackSettings = { disposeOnHide: true, dynamicModalContainerSelector: '#dynamic-modals', eventClosed: 'hidden.bs.modal', eventOpened: 'shown.bs.modal', modalFunctionRef: { open: 'show', close: 'hide', toggle: 'toggle' }, modalImpl: {}, modalSelector: '.modal', modalStackFormSelector: 'form[data-modal-stack-form]', modalStackFormNoInit: false, modalStackFormSubmitButtonSimulator: true, unexpectedErrorModalSelector: '#unexpected-error-modal' }; export default function initializeModalStack(options) { Object.assign(modalStackSettings, options); return { initializeModalOpener, initializeAjaxModalForm, createModalFromUri, createModalFromHtml, createModalFromElement }; } export function createModalFromElement(modalElement, options) { const { disposeOnHide, eventClosed, eventOpened, modalFunctionRef, modalImpl, modalStackFormSelector, modalStackFormNoInit, } = options = { ...modalStackSettings, ...modalElement.dataset, ...options }; if (!modalElement.matches('.modal')) { throw new TypeError('Your element needs the ".modal" class'); } modalElement.addEventListener(eventClosed, (e) => { modalElement.dispatchEvent(new CustomEvent('modal-stack-modal-closed', { detail: {originalEvent: e}, bubbles: true, })); }); modalElement.addEventListener(eventOpened, (e) => { modalElement.dispatchEvent(new CustomEvent('modal-stack-modal-opened', { detail: {originalEvent: e}, bubbles: true, })); }); if (!modalStackFormNoInit) { Array.from(modalElement.querySelectorAll(modalStackFormSelector)).forEach(form => initializeAjaxModalForm(form, options)); } const newModal = new ModalProxy(modalElement, new modalImpl(modalElement, options), modalFunctionRef); modalElement.modalStackProxy = newModal; if (disposeOnHide) { modalElement.addEventListener('modal-stack-modal-closed', () => modalElement.remove()); } return newModal; } export function createModalFromHtml(html, options) { const { dynamicModalContainerSelector, modalSelector } = { ...modalStackSettings, ...options }; const modalContainer = document.querySelector(dynamicModalContainerSelector); if (modalContainer === null) { throw new TypeError(`Modal container not found. Selector was: "${dynamicModalContainerSelector}"`); } const element = parseHTML(html).querySelector(modalSelector); if (element === null) { throw new TypeError('There was no modal element'); } modalContainer.append(element); dispatchEvent('modal-stack-element-created', {element}); return createModalFromElement(element, options); } export async function createModalFromUri(href, options) { options = { ...modalStackSettings, ...options }; const taskId = ++lastTaskId; dispatchEvent('modal-stack-fetching', {href, taskId}); const html = await fetch(href) .then(response => { if (response.status < 200 || response.status >= 300) { throw new Error('Unexpetected http response'); } dispatchEvent('modal-stack-fetching-finished', {href, taskId}); return response.text() }) .then(text => text) .catch((e) => { const errorModalElement = document.querySelector(options.unexpectedErrorModalSelector); if (errorModalElement === null) { console.error('Unexpected modal missing during error handling'); return; } createModalFromElement(errorModalElement, {disposeOnHide: false}).open(); // throw the error along, as nobody can work with this result ;) throw e; }); return createModalFromHtml(html, options); } export function initializeModalOpener(element, options) { const { ajaxModalUri } = options = { ...modalStackSettings, ...element.dataset, ...options }; element.addEventListener('click', async () => { if (element.dataset.ajaxModalUri === undefined) { throw new TypeError('Modal opener requires data-ajax-modal-uri'); } (await createModalFromUri(element.dataset.ajaxModalUri, options)).open(); }); } export function initializeAjaxModalForm(form, options) { const { modalSelector, modalStackFormSubmitButtonSimulator } = { ...modalStackSettings, ...form.dataset, ...options }; if (modalStackFormSubmitButtonSimulator) { const hidden = document.createElement('input'); hidden.type = 'hidden'; form.append(hidden); form.addEventListener('click', (event) => { if (!event.target || !event.target.matches('[type="submit"]')) { return; } hidden.value = event.target.value; hidden.name = event.target.name; }); } remoteForm(form, async (form, wants) => { form.dispatchEvent(new CustomEvent('modal-stack-form-submit-start', {bubbles: true})); const parentModal = form.closest(modalSelector); try { const response = await wants.html(); form.dispatchEvent(new CustomEvent('modal-stack-form-submit-finished', {response, bubbles: true})); const cancelResponseEvent = new CustomEvent('modal-stack-form-response', {response, bubbles: true, cancelable: true}); if (!form.dispatchEvent(cancelResponseEvent)) { return; }; if (response.status !== 204) { parentModal.innerHTML = response.html.querySelector(modalSelector).innerHTML; form.dispatchEvent(new CustomEvent('modal-stack-form-replaced', {response, bubbles: true})); initializeAjaxModalForm(parentModal.querySelector('form', options)); return; } form.dispatchEvent(new CustomEvent('modal-stack-form-submit-success', {response, bubbles: true})); parentModal.modalStackProxy.close(); } catch (e) { parentModal.modalStackProxy.close(); const errorModalElement = document.querySelector(options.unexpectedErrorModalSelector); if (errorModalElement === null) { console.error('Unexpected modal missing during error handling'); return; } createModalFromElement(errorModalElement, {disposeOnHide: false}).open(); } }); } function dispatchEvent(name, details) { if (details === undefined) { details = {}; } console.debug('dispatching event: ' + name); document.dispatchEvent( new CustomEvent(name, { detail: details, bubbles: true, }) ); }