PageSourceSearch

https://www.sulz.de/fileadmin/templates/js/upgrade/hw-dialog.js

js sulz.de collected 2026-10-02 16:47:02 UTC 9,132 bytes, 273 lines download raw bytes

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.