First working version
This commit is contained in:
parent
7bc448cd1c
commit
4e925b0918
6 changed files with 315 additions and 0 deletions
19
License.txt
Normal file
19
License.txt
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2024 Ferdinand Kuhl <f.kuhl@digital-competence.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# modal stack
|
||||
|
||||
This package tries to provide a foundation to provide general utilities to work with dynamically loaded modals.
|
||||
|
||||
It provides a stack of functions to create modals. From buttom to top these are:
|
||||
|
||||
| Name | Description |
|
||||
|------------------------------------------|------------------------------------------------|
|
||||
| createModalFromElement(element, options) | Creates Modals from an HTML element |
|
||||
| createModalFromHtml(html, options) | Creates Modals from an HTML string |
|
||||
| createModalFromUri(uri, options) | Creates Modals from an uri which provides html |
|
||||
|
||||
And this is the table of the allowed options (in case of the `createModalFromElement` you can provide these options as
|
||||
data attributes of the element)
|
||||
|
||||
| Name | Description | Default |
|
||||
|-------------------------------------|---------------------------------------------------------------------|-------------------------------|
|
||||
| disposeOnHide | remove the dynamically loaded html from dom, if closed | true |
|
||||
| dynamicModalContainerSelector | container where the html should be appended to | "#dynamic-modals" |
|
||||
| eventClosed | event name, which gets triggerd on the opening of a modal | "hidden.bs.modal" |
|
||||
| eventOpened | event name, which gets triggerd on the closing of a modal | "shown.bs.modal" |
|
||||
| modalFunctionRef | a map modalProxyMethod => modalImplementationMethod | object |
|
||||
| modalImpl | the modal implementation, for example bootstraps Modal class | {} |
|
||||
| modalSelector | a selector to detect a modal root element | ".modal" |
|
||||
| modalStackFormSelector | selector for forms, which should be ajaxified | "form[data-modal-stack-form]" |
|
||||
| modalStackFormNoInit | skip form ajaxification | false |
|
||||
| modalStackFormSubmitButtonSimulator | a string in the uri, which will be replaced with the identity value | true |
|
||||
| unexpectedErrorModalSelector | a string in the uri, which will be replaced with the identity value | "#unexpected-error-modal" |
|
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "@digicomp/modal-stack",
|
||||
"version": "0.0.1",
|
||||
"description": "api to create modals, independent of the specific modal implementation",
|
||||
"main": "src/modals.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@digital-competence.de:npm/modal-stack.git"
|
||||
},
|
||||
"keywords": [
|
||||
"modal",
|
||||
"ajax"
|
||||
],
|
||||
"author": "Ferdinand Kuhl",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/remote-form": "^0.4.0"
|
||||
}
|
||||
}
|
42
src/modalProxy.js
Normal file
42
src/modalProxy.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
export class ModalProxy
|
||||
{
|
||||
#modalInstance = null;
|
||||
#modalElement = null;
|
||||
#modalFunctionRef = {
|
||||
open: 'show',
|
||||
close: 'hide',
|
||||
toggle: 'toggle'
|
||||
}
|
||||
|
||||
constructor(element, instance, options)
|
||||
{
|
||||
this.#modalElement = element;
|
||||
this.#modalInstance = instance;
|
||||
Object.assign(this.#modalFunctionRef, options)
|
||||
}
|
||||
|
||||
get element()
|
||||
{
|
||||
return this.#modalElement;
|
||||
}
|
||||
|
||||
get instance()
|
||||
{
|
||||
return this.#modalInstance;
|
||||
}
|
||||
|
||||
open()
|
||||
{
|
||||
return this.#modalInstance[this.#modalFunctionRef.open]();
|
||||
}
|
||||
|
||||
close()
|
||||
{
|
||||
return this.#modalInstance[this.#modalFunctionRef.close]();
|
||||
}
|
||||
|
||||
toggle()
|
||||
{
|
||||
return this.#modalInstance[this.#modalFunctionRef.toggle]();
|
||||
}
|
||||
}
|
197
src/modals.js
Normal file
197
src/modals.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
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,
|
||||
})
|
||||
);
|
||||
}
|
7
src/util.js
Normal file
7
src/util.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// taken from @github/remoteform
|
||||
export function parseHTML(html) {
|
||||
const template = document.createElement('template')
|
||||
// eslint-disable-next-line github/no-inner-html
|
||||
template.innerHTML = html
|
||||
return document.importNode(template.content, true)
|
||||
}
|
Loading…
Reference in a new issue