1// Custom Web Component Dialog Element 2// from: https://github.com/argyleink/gui-challenges/blob/main/dialog/dialog.js 3 4// custom events to be added to <dialog> 5// Events must be created per dispatch: htmx marks handled event objects 6// (htmx-internal-data.handledFor), so a reused singleton would only ever 7// trigger an hx-trigger listener once. 8const dialogClosingEvent = () => new Event('closing') 9const dialogClosedEvent = () => new Event('closed') 10const dialogOpeningEvent = () => new Event('opening') 11const dialogOpenedEvent = () => new Event('opened') 12const dialogRemovedEvent = () => new Event('removed') 13 14// Track all dialogs on the page 15const dialogs = {}; 16 17// track opening 18const dialogAttrObserver = new MutationObserver((mutations, _observer) => { 19 mutations.forEach(async mutation => { 20 if (mutation.attributeName === 'open') { 21 const dialog = mutation.target 22 23 const isOpen = dialog.hasAttribute('open') 24 if (!isOpen) return 25 26 dialog.removeAttribute('inert') 27 document.body.style.overflow = 'hidden'; 28 29 // set focus 30 const focusTarget = dialog.querySelector('[autofocus]') 31 focusTarget 32 ? focusTarget.focus() 33 : dialog.querySelector('button').focus() 34 35 dialog.dispatchEvent(dialogOpeningEvent()) 36 await animationsComplete(dialog) 37 dialog.dispatchEvent(dialogOpenedEvent()) 38 } 39 }) 40}) 41 42// track deletion 43const dialogDeleteObserver = new MutationObserver((mutations, _observer) => { 44 mutations.forEach(mutation => { 45 mutation.removedNodes.forEach(removedNode => { 46 if (removedNode.nodeName === 'DIALOG') { 47 removedNode.removeEventListener('click', lightDismiss) 48 removedNode.removeEventListener('close', dialogClose) 49 removedNode.dispatchEvent(dialogRemovedEvent()) 50 } 51 }) 52 }) 53}) 54 55// wait for all dialog animations to complete their promises 56const animationsComplete = element => { 57 // Return early if getAnimations doesn't exist, as to not error 58 if (typeof element.getAnimations !== "function") { 59 return; 60 } 61 62 return Promise.allSettled( 63 element.getAnimations().map(animation => 64 animation.finished)) 65} 66 67// Open Dialog by Dialog ID or Target Element 68window.dialogOpen = (dialogIdOrElement) => { 69 const dialog = typeof dialogIdOrElement === 'string' 70 ? dialogs[dialogIdOrElement] 71 : dialogIdOrElement; 72 73 dialog.showModal(); 74 75 return dialog; 76}; 77 78 79// click outside the dialog handler 80const lightDismiss = ({target:dialog}) => { 81 if (dialog.nodeName === 'DIALOG') 82 dialog.close('dismiss') 83} 84 85// Extend dialog.close() to add custom events 86const dialogClose = async ({target:dialog}) => { 87 document.body.style.overflow = 'auto'; 88 dialog.setAttribute('inert', '') 89 dialog.dispatchEvent(dialogClosingEvent()) 90 91 await animationsComplete(dialog) 92 93 dialog.dispatchEvent(dialogClosedEvent()) 94} 95 96// page load dialogs setup 97async function SetupDialog(dialog) { 98 dialog.addEventListener('click', lightDismiss) 99 dialog.addEventListener('close', dialogClose) 100 101 dialogs[dialog.id] = dialog; 102 103 dialogAttrObserver.observe(dialog, { 104 attributes: true, 105 }) 106 107 dialogDeleteObserver.observe(document.body, { 108 attributes: false, 109 subtree: false, 110 childList: true, 111 }) 112 113 // remove loading attribute 114 // prevent page load @keyframes playing 115 await animationsComplete(dialog) 116 dialog.removeAttribute('loading') 117} 118 119window.SetupDialog = SetupDialog
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.