modal-stack/src/modals.js

197 lines
6.9 KiB
JavaScript

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,
})
);
}