Files
js/
factories/
dialog-factory.js
libs/
windog.js
uis/
dialogs.js
index.html
utils.js
index.js
Source Code
index.js
{
urls: [
"js/factories/dialog-factory.js",
],
},
{
urls: [
"js/uis/dialogs.js",
"js/libs/windog.js",
],
},
js/uis/dialogs.js
let dialogActivity = DialogFactory({
templateSelector: '._dialogActivity',
onShow: ({slots}) => {
slots.inEntityId.value = entityId;
}
});
index.html
<template class="_dialogActivity">
<dialog class="wg-Windog">
<!-- # dialog RS -->
<div class="backdrop"></div>
<div class="wrapper">
<form method="dialog" data-ref="form">
<div class="flex flex-column gap-quarter">
<label class="flex flex-column">
Name
<input type="text" name="name"/>
</label>
</div>
<div class="void void-1x"></div>
<div class="d-flex justify-end gap-full">
<button value="ok" data-slot="confirmButtonText">OK</button>
<button data-slot="cancelButtonText">Cancel</button>
</div>
</form>
</div>
</dialog>
</template>
js/utils.js
let utils = (function () {
// # self
let SELF = {
DOMSlots,
FillFormWithData,
FormDataToObject,
};
// # function
function DOMSlots(parentNode) {
let slots = {};
[...parentNode.querySelectorAll("[data-slot]"), parentNode].forEach(
(node) => {
let key = node.dataset?.slot;
if (!key || slots[key]) return;
slots[key] = node;
}
);
return slots;
}
function FillFormWithData(form, formValuesObject) {
if (!form) return;
const formElements = form.elements;
for (const [key, value] of Object.entries(formValuesObject)) {
const input = formElements.namedItem(key);
if (!input) continue;
if (input.type === "checkbox" || input.type === "radio") {
input.checked = Boolean(value);
} else if (input.tagName === "SELECT") {
const option = Array.from(input.options).find(opt => opt.value == value);
if (option) option.selected = true;
} else {
input.value = value;
}
}
}
function FormDataToObject(formData) {
if (!(formData instanceof FormData)) {
console.error("The input must be an instance of FormData.");
return {};
}
const data = {};
for (const [key, value] of formData.entries()) {
// If the key already exists, it means it's likely a checkbox or a multi-select
if (data.hasOwnProperty(key)) {
// Convert existing value to an array if it's not already
if (!Array.isArray(data[key])) {
data[key] = [data[key]];
}
// Add the new value to the array
data[key].push(value);
} else {
data[key] = value;
}
}
return data;
}
return SELF;
})();
windog.js
let windog = (function() {
let $ = document.querySelector.bind(document);
let SELF = {
alert,
confirm,
prompt,
showDialogAsync,
};
// # options
let dialogOptions = {
alert: {
templateSelector: '#tmp-dialog-alert',
},
confirm: {
templateSelector: '#tmp-dialog-confirm',
onClose: (dialogEl) => {
return (dialogEl.returnValue == 'ok');
},
},
prompt: {
templateSelector: '#tmp-dialog-prompt',
onClose: (dialogEl) => {
if (dialogEl.returnValue == 'ok') {
return dialogEl.querySelector('[data-slot="input"]')?.value;
}
return null;
},
},
};
// # function
// # prompt
async function prompt(message='', defaultValue='', userOptions) {
return await showDialogAsync({
...dialogOptions.prompt,
...userOptions
}, onShowDefault, {
message,
defaultValue,
...userOptions,
});
}
async function confirm(message='', userOptions) {
return await showDialogAsync({
...dialogOptions.confirm,
...userOptions
}, onShowDefault, {
message,
...userOptions,
});
}
// # alert
async function alert(message='', userOptions) {
return await showDialogAsync({
...dialogOptions.alert,
...userOptions
}, onShowDefault, {
message,
});
}
function onShowDefault(dialogEl, extraData, options) {
let {message, defaultValue} = extraData;
let {confirmButtonText, cancelButtonText, showCancelButton} = options;
let slots = DOMSlots(dialogEl);
slots.message?.replaceChildren(message);
if (slots.input) slots.input.value = defaultValue;
slots.confirmButtonText?.replaceChildren(confirmButtonText);
slots.cancelButtonText?.replaceChildren(cancelButtonText);
if (!showCancelButton) {
slots.cancelButtonText?.remove();
}
}
function DOMSlots(itemEl) {
let slotData = Array.from(itemEl.querySelectorAll('[data-slot]')).map(x => {
return {
key: x.dataset.slot,
el: x,
};
});
let slotObj = slotData.reduce((obj, item) => {
obj[item.key] = item.el;
return obj;
}, {});
return slotObj;
}
async function showDialogAsync(dialogOptions, onShow, extraData) {
return new Promise(async resolve => {
// # default options
let defaultOptions = {
allowOutsideClick: true,
allowEscapeKey: true,
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
showCancelButton: true,
};
let persistentOptions = {
resolver: {
resolve,
},
};
let mixedOptions = Object.assign(defaultOptions, dialogOptions, extraData, persistentOptions);
let {allowOutsideClick, templateSelector} = mixedOptions;
// queue this dialog and wait for previous dialog to resolve
// await subscribeDialogAsync();
let el = $(templateSelector).content.cloneNode(true);
let dialogEl = el.querySelector('dialog');
let dialogData = {
dialogItem: mixedOptions,
};
if (allowOutsideClick) {
dialogEl.querySelector('.backdrop')?.addEventListener('click', () => {
onCancel(dialogEl);
});
}
dialogEl.addEventListener('close', onClose);
attachKeytrap(dialogEl, dialogData);
dialogEl._windogData = dialogData;
document.body.append(el);
dialogEl.showModal();
onShow?.(dialogEl, extraData, mixedOptions);
});
}
async function onCancel(dialogEl) {
let {dialogItem} = dialogEl._windogData;
let isShouldClose = await onBeforeClose(dialogEl, dialogItem);
if (isShouldClose) {
dialogEl.close();
}
}
async function onBeforeClose(dialogEl, dialogItem) {
let isShouldClose = true;
if (typeof(dialogItem.onBeforeClose) == 'function') {
isShouldClose = await dialogItem.onBeforeClose(dialogEl);
}
return isShouldClose;
}
// # close
async function onClose(evt) {
let dialogItem = evt.target._windogData.dialogItem;
let dialogEl = evt.target;
let dialogResult = await dialogItem.onClose?.(dialogEl);
dialogItem.resolver.resolve(dialogResult);
// wait close animation
await new Promise(resolve => setTimeout(resolve, dialogItem.closeAnimationTimeout ?? 3000));
dialogEl.remove();
}
function attachKeytrap(dialogEl, dialogData) {
let focusableContent = dialogEl.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
dialogData.firstFocusableElement = focusableContent[0];
dialogData.lastFocusableElement = focusableContent[focusableContent.length - 1];
window.addEventListener('keydown', keyTrapper);
}
async function keyTrapper(evt) {
let dialogEl = evt.target.closest('dialog');
let isTabPressed = (evt.key == 'Tab');
let isKeyEscape = (evt.key == 'Escape' || evt.code == 'Escape' || evt.keyCode == '27');
if (isKeyEscape) {
let {dialogItem} = dialogEl._windogData;
if (dialogItem.allowEscapeKey) {
let isShouldClose = await onBeforeClose(dialogEl, dialogItem)
if (isShouldClose) {
dialogEl.close();
}
}
evt.preventDefault(); // disable default close dialog behaviour
return;
}
if (!isTabPressed) return;
let {firstFocusableElement, lastFocusableElement} = dialogEl._windogData;
if (evt.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
evt.preventDefault();
}
} else if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
evt.preventDefault();
}
}
return SELF;
})();
https://vanillawebdev.blogspot.com/2024/12/dialog-component-setup.html
https://www.blogger.com/blog/post/edit/8166404610182826392/5122348024995671850
https://www.blogger.com/blog/page/edit/8166404610182826392/5122348024995671850
Dialog Component Setup
December 15, 2024
Files
js/
factories/
dialog-factory.js
libs/
windog.js
uis/
dialogs.js
index.html
utils.js
index.js
Source Code
index.js
{
urls: [
"js/factories/dialog-factory.js",
],
},
{
urls: [
"js/uis/dialogs.js",
"js/libs/windog.js",
],
},
js/uis/dialogs.js
let dialogActivity = DialogFactory({
templateSelector: '._dialogActivity',
onShow: ({slots}) => {
slots.inEntityId.value = entityId;
}
});
index.html
<template class="_dialogActivity">
<dialog class="wg-Windog">
<!-- # dialog RS -->
<div class="backdrop"></div>
<div class="wrapper">
<form method="dialog" data-ref="form">
<div class="flex flex-column gap-quarter">
<label class="flex flex-column">
Name
<input type="text" name="name"/>
</label>
</div>
<div class="void void-1x"></div>
<div class="d-flex justify-end gap-full">
<button value="ok" data-slot="confirmButtonText">OK</button>
<button data-slot="cancelButtonText">Cancel</button>
</div>
</form>
</div>
</dialog>
</template>
js/utils.js
let utils = (function () {
// # self
let SELF = {
DOMSlots,
FillFormWithData,
FormDataToObject,
};
// # function
function DOMSlots(parentNode) {
let slots = {};
[...parentNode.querySelectorAll("[data-slot]"), parentNode].forEach(
(node) => {
let key = node.dataset?.slot;
if (!key || slots[key]) return;
slots[key] = node;
}
);
return slots;
}
function FillFormWithData(form, formValuesObject) {
if (!form) return;
const formElements = form.elements;
for (const [key, value] of Object.entries(formValuesObject)) {
const input = formElements.namedItem(key);
if (!input) continue;
if (input.type === "checkbox" || input.type === "radio") {
input.checked = Boolean(value);
} else if (input.tagName === "SELECT") {
const option = Array.from(input.options).find(opt => opt.value == value);
if (option) option.selected = true;
} else {
input.value = value;
}
}
}
function FormDataToObject(formData) {
if (!(formData instanceof FormData)) {
console.error("The input must be an instance of FormData.");
return {};
}
const data = {};
for (const [key, value] of formData.entries()) {
// If the key already exists, it means it's likely a checkbox or a multi-select
if (data.hasOwnProperty(key)) {
// Convert existing value to an array if it's not already
if (!Array.isArray(data[key])) {
data[key] = [data[key]];
}
// Add the new value to the array
data[key].push(value);
} else {
data[key] = value;
}
}
return data;
}
return SELF;
})();
windog.js
let windog = (function() {
let $ = document.querySelector.bind(document);
let SELF = {
alert,
confirm,
prompt,
showDialogAsync,
};
// # options
let dialogOptions = {
alert: {
templateSelector: '#tmp-dialog-alert',
},
confirm: {
templateSelector: '#tmp-dialog-confirm',
onClose: (dialogEl) => {
return (dialogEl.returnValue == 'ok');
},
},
prompt: {
templateSelector: '#tmp-dialog-prompt',
onClose: (dialogEl) => {
if (dialogEl.returnValue == 'ok') {
return dialogEl.querySelector('[data-slot="input"]')?.value;
}
return null;
},
},
};
// # function
// # prompt
async function prompt(message='', defaultValue='', userOptions) {
return await showDialogAsync({
...dialogOptions.prompt,
...userOptions
}, onShowDefault, {
message,
defaultValue,
...userOptions,
});
}
async function confirm(message='', userOptions) {
return await showDialogAsync({
...dialogOptions.confirm,
...userOptions
}, onShowDefault, {
message,
...userOptions,
});
}
// # alert
async function alert(message='', userOptions) {
return await showDialogAsync({
...dialogOptions.alert,
...userOptions
}, onShowDefault, {
message,
});
}
function onShowDefault(dialogEl, extraData, options) {
let {message, defaultValue} = extraData;
let {confirmButtonText, cancelButtonText, showCancelButton} = options;
let slots = DOMSlots(dialogEl);
slots.message?.replaceChildren(message);
if (slots.input) slots.input.value = defaultValue;
slots.confirmButtonText?.replaceChildren(confirmButtonText);
slots.cancelButtonText?.replaceChildren(cancelButtonText);
if (!showCancelButton) {
slots.cancelButtonText?.remove();
}
}
function DOMSlots(itemEl) {
let slotData = Array.from(itemEl.querySelectorAll('[data-slot]')).map(x => {
return {
key: x.dataset.slot,
el: x,
};
});
let slotObj = slotData.reduce((obj, item) => {
obj[item.key] = item.el;
return obj;
}, {});
return slotObj;
}
async function showDialogAsync(dialogOptions, onShow, extraData) {
return new Promise(async resolve => {
// # default options
let defaultOptions = {
allowOutsideClick: true,
allowEscapeKey: true,
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
showCancelButton: true,
};
let persistentOptions = {
resolver: {
resolve,
},
};
let mixedOptions = Object.assign(defaultOptions, dialogOptions, extraData, persistentOptions);
let {allowOutsideClick, templateSelector} = mixedOptions;
// queue this dialog and wait for previous dialog to resolve
// await subscribeDialogAsync();
let el = $(templateSelector).content.cloneNode(true);
let dialogEl = el.querySelector('dialog');
let dialogData = {
dialogItem: mixedOptions,
};
if (allowOutsideClick) {
dialogEl.querySelector('.backdrop')?.addEventListener('click', () => {
onCancel(dialogEl);
});
}
dialogEl.addEventListener('close', onClose);
attachKeytrap(dialogEl, dialogData);
dialogEl._windogData = dialogData;
document.body.append(el);
dialogEl.showModal();
onShow?.(dialogEl, extraData, mixedOptions);
});
}
async function onCancel(dialogEl) {
let {dialogItem} = dialogEl._windogData;
let isShouldClose = await onBeforeClose(dialogEl, dialogItem);
if (isShouldClose) {
dialogEl.close();
}
}
async function onBeforeClose(dialogEl, dialogItem) {
let isShouldClose = true;
if (typeof(dialogItem.onBeforeClose) == 'function') {
isShouldClose = await dialogItem.onBeforeClose(dialogEl);
}
return isShouldClose;
}
// # close
async function onClose(evt) {
let dialogItem = evt.target._windogData.dialogItem;
let dialogEl = evt.target;
let dialogResult = await dialogItem.onClose?.(dialogEl);
dialogItem.resolver.resolve(dialogResult);
// wait close animation
await new Promise(resolve => setTimeout(resolve, dialogItem.closeAnimationTimeout ?? 3000));
dialogEl.remove();
}
function attachKeytrap(dialogEl, dialogData) {
let focusableContent = dialogEl.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
dialogData.firstFocusableElement = focusableContent[0];
dialogData.lastFocusableElement = focusableContent[focusableContent.length - 1];
window.addEventListener('keydown', keyTrapper);
}
async function keyTrapper(evt) {
let dialogEl = evt.target.closest('dialog');
let isTabPressed = (evt.key == 'Tab');
let isKeyEscape = (evt.key == 'Escape' || evt.code == 'Escape' || evt.keyCode == '27');
if (isKeyEscape) {
let {dialogItem} = dialogEl._windogData;
if (dialogItem.allowEscapeKey) {
let isShouldClose = await onBeforeClose(dialogEl, dialogItem)
if (isShouldClose) {
dialogEl.close();
}
}
evt.preventDefault(); // disable default close dialog behaviour
return;
}
if (!isTabPressed) return;
let {firstFocusableElement, lastFocusableElement} = dialogEl._windogData;
if (evt.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
evt.preventDefault();
}
} else if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
evt.preventDefault();
}
}
return SELF;
})();
Comments
Post a Comment