1/** 2 * _ _ ___ __ __ 3 * | | | |( _ )\ \ / / 4 * | |_| |/ _ \/\ \ /\ / / 5 * | _ | (_> <\ V V / 6 * |_| |_|\___/\/ \_/\_/ 7 * 8 * @file hw-dialog.js 9 * @description This function sets up the behavior of <dialog> elements on the page, allowing them to 10 * function with smooth animations, and handles the open/close state using data-attributes. The buttons 11 * that control the dialog are identified by their `aria-controls` attribute, and the dialog's open 12 * state is managed through a specified data-attribute. The `aria-expanded` state of the buttons is 13 * also managed automatically and the buttons can be placed anywhere on the page. Only this data attribute 14 * is used to open or close the dialog, ensuring that the dialog can be controlled consistently across the 15 * page and without the need for any additional JavaScript. 16 * 17 * Example usage: 18 * ``` 19 * <dialog data-hw-dialog-open="false" id="myDialog"> 20 * <p>This is a dialog.</p> 21 * <button type="button" aria-controls="myDialog" aria-expanded="false">Close</button> 22 * </dialog> 23 * ``` 24 * 25 * To set up the dialog, call: 26 * ``` 27 * hwDialogSetup('dialog#myDialog', 'data-hw-dialog-open'); 28 * ``` 29 * 30 * The button(s) outside the dialog are used to open the dialog, while the button(s) inside the dialog are 31 * used to close it. They automatically get the 'click' event listeners added to them which allow them to 32 * open or close the dialog based on the `data-hw-dialog-open` attribute. 33 * 34 * @param {string} dialogElementSelector Selector for the individual <dialog> element (e.g., 'hw-dialog dialog') 35 * @param {string} openAttributeName The attribute name that indicates whether a <dialog> element is open or closed (e.g., 'data-hw-dialog-open') 36 * 37 */ 38export function hwDialogSetup( 39 dialogElementSelector, 40 openAttributeName = 'data-hw-dialog-open' 41) { 42 43 const dialogElement = document.querySelector(dialogElementSelector); 44 45 // Add a check to ensure dialogElement is not null 46 if (!dialogElement) { 47 return; // Exit the function if the element is not found 48 } 49 50 const dialogElementButtonsInside = Array.from(dialogElement.querySelectorAll(`:scope button[aria-controls="${dialogElement.id}"]`)); 51 const dialogElementButtonsOutside = Array.from(document.querySelectorAll(`button[aria-controls="${dialogElement.id}`)).filter(button => !dialogElementButtonsInside.includes(button)); 52 const dialogElementButtons = [...dialogElementButtonsInside, ...dialogElementButtonsOutside]; 53 54 dialogElementButtonsInside.forEach(button => { 55 button.addEventListener('click', () => { 56 dialogElement.setAttribute(openAttributeName, 'false'); 57 }); 58 }); 59 60 dialogElementButtonsOutside.forEach(button => { 61 button.addEventListener('click', () => { 62 dialogElement.setAttribute(openAttributeName, 'true'); 63 }); 64 }); 65 66 // --- Prevent default dialog behavior for closing --- 67 dialogElement.addEventListener('cancel', (event) => { 68 69 // Prevent the default behavior of closing the dialog without animation 70 event.preventDefault(); 71 72 // Change the open state to false using the data-attribute 73 // This will trigger the mutation observer to handle the closing animation 74 dialogElement.setAttribute(openAttributeName, 'false'); 75 }); 76 77 // --- Allow dialog to be closed by clicking outside of it --- 78 dialogElement.addEventListener('click', (event) => { 79 80 // If the click is outside the dialog content, close it 81 if (event.target === dialogElement) { 82 dialogElement.setAttribute(openAttributeName, 'false'); 83 } 84 }); 85 86 // --- Setup for an individual <dialog> element --- 87 // Function to initialize each <dialog> element by adding event listeners and setting initial state 88 function initializeDialogElement(dialogElement) { 89 dialogElement.mutate = () => { 90 91 // Determine the desired open state based on the data-attribute 92 const shouldOpen = dialogElement.getAttribute(openAttributeName) === 'true'; 93 94 // Check if the dialog is currently open 95 const isDialogOpenNow = dialogElement.hasAttribute('open'); 96 97 // If the desired state matches the current state, reset styles and exit 98 if (shouldOpen === isDialogOpenNow) { 99 dialogElement.style.scale = ''; 100 dialogElement.style.opacity = ''; 101 102 return; 103 } 104 105 // Trigger animation to open or close the dialog element 106 if (shouldOpen) { 107 animateDialogElementOpen(dialogElement); 108 } else { 109 animateDialogElementClose(dialogElement); 110 } 111 }; 112 } 113 114 initializeDialogElement(dialogElement); 115 116 // --- Mutation Observer --- 117 // Observe changes to the open state and handle the dialog accordingly 118 const dialogMutationObserver = new MutationObserver((mutationRecords) => { 119 for (const mutationRecord of mutationRecords) { 120 const mutatedElement = mutationRecord.target; 121 122 // If the open state changes, apply changes based on the data-attribute 123 if (mutationRecord.attributeName === openAttributeName) { 124 const isOpen = mutatedElement.getAttribute(openAttributeName) === 'true'; 125 126 if (isOpen) { 127 animateDialogElementOpen(mutatedElement); 128 129 dialogElementButtons.forEach(button => { 130 button.setAttribute('aria-expanded', 'true'); 131 }); 132 } else { 133 animateDialogElementClose(mutatedElement); 134 135 dialogElementButtons.forEach(button => { 136 button.setAttribute('aria-expanded', 'false'); 137 }); 138 } 139 } 140 } 141 }); 142 143 // Start observing the container for changes to the open state 144 dialogMutationObserver.observe(dialogElement, { 145 subtree: false, 146 attributes: true, 147 attributeFilter: [openAttributeName] 148 }); 149} 150 151// --- Helper Functions for Animations --- 152// Check if the user prefers reduced motion 153const prefersReducedMotion = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; 154 155// Cancel the current animation if one is running 156function cancelCurrentAnimation(dialogElement) { 157 158 // Cancel the ongoing animation if present 159 dialogElement.dialogElementAnimation?.cancel?.(); 160 dialogElement.dialogElementAnimation = null; 161} 162
163// Animate the opening of the <dialog> element by scaling it from 0 to 1 and setting the opacity to 1 164function animateDialogElementOpen(dialogElement) { 165 const duration = parseInt(getComputedStyle(dialogElement).getPropertyValue('--hw-dialog-animation-duration') || '400ms', 10); 166 const timingFunction = getComputedStyle(dialogElement).getPropertyValue('--hw-dialog-animation-timing-function') || 'ease-in'; 167 168 // Cancel any ongoing animation 169 cancelCurrentAnimation(dialogElement); 170 171 // Make the target element visible and prevent overflow during the animation 172 dialogElement.showModal(); 173 174 // If reduced motion is preferred immediately open the dialog without animation 175 if (prefersReducedMotion()) { 176 requestAnimationFrame(() => { 177 return Promise.resolve(); 178 }); 179 } 180 181 // Set the starting scale and opacity to 0 and force a reflow 182 dialogElement.style.scale = '0.95'; 183 dialogElement.style.opacity = '0'; 184 dialogElement.getBoundingClientRect(); 185 186 requestAnimationFrame(() => { 187 188 // Start the animation to scale the dialog to its full scale 189 const animation = dialogElement.animate( 190 [{ 191 scale: '0.95', 192 opacity: '0' 193 }, { 194 scale: '1', 195 opacity: '1' 196 }], 197 { 198 duration, timingFunction, fill: 'forwards' 199 } 200 ); 201 202 // Store the animation reference for later cancellation 203 dialogElement.dialogElementAnimation = animation; 204 205 return animation.finished.then(() => { 206 dialogElement.dialogElementAnimation = null; 207 dialogElement.style.scale = ''; 208 dialogElement.style.opacity = ''; 209 210 }).catch(() => { /* Animation was canceled */ }); 211 }); 212} 213 214// Animate the closing of the <dialog> element by scaling it to 0 and reducing its opacity to 0 215function animateDialogElementClose(dialogElement) { 216 const duration = parseInt(getComputedStyle(dialogElement).getPropertyValue('--hw-dialog-animation-duration') || '400ms', 10); 217 const timingFunction = getComputedStyle(dialogElement).getPropertyValue('--hw-dialog-animation-timing-function') || 'ease-out'; 218 219 // Cancel any ongoing animation 220 cancelCurrentAnimation(dialogElement); 221 222 // If reduced motion is preferred, immediately close the dialog without animation 223 if (prefersReducedMotion()) { 224 dialogElement.close(); 225 226 requestAnimationFrame(() => { 227 return Promise.resolve(); 228 }); 229 } 230 231 dialogElement.style.scale = '1'; 232 dialogElement.getBoundingClientRect(); 233 234 requestAnimationFrame(() => { 235 236 // Start the animation to scale the dialog to 0 and reduce opacity to 0 237 const animation = dialogElement.animate( 238 [{ 239 scale: '1', 240 opacity: '1' 241 }, { 242 scale: '0.95', 243 opacity: '0' 244 }], 245 { 246 duration, timingFunction, fill: 'forwards' 247 } 248 ); 249 250 dialogElement.dialogElementAnimation = animation; 251 252 return animation.finished.then(() => { 253 dialogElement.dialogElementAnimation = null; 254 dialogElement.style.scale = ''; 255 dialogElement.style.opacity = ''; 256 dialogElement.close(); 257 }).catch(() => { /* Animation was canceled */ }); 258 }); 259} 260 261if (document.readyState === 'complete') { 262 hwDialogSetup( 263 'hw-content-elements > dialog', 264 ); 265} else { 266 document.addEventListener('readystatechange', () => { 267 if (document.readyState === 'complete') { 268 hwDialogSetup( 269 'hw-content-elements > dialog', 270 ); 271 } 272 }); 273}
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.