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

Thank You

for your visit