First working version
This commit is contained in:
parent
7bc448cd1c
commit
462056366b
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) 2015 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/bootstrap-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