PageSourceSearch

https://www.sulz.de/_assets/094881798ba18447100691146b2e53f1/Components/Dialog/Dialog.js?1780471078

js sulz.de collected 2026-10-02 16:46:56 UTC 7,114 bytes, 274 lines download raw bytes

1/**
2 *  _   _  ___ __        __
3 * | | | |( _ )\ \      / /
4 * | |_| |/ _ \/\ \ /\ / /
5 * |  _  | (_>  <\ V  V /
6 * |_| |_|\___/\/ \_/\_/
7 *
8 * @file Dialog.js
9 * @version 2.0
10 * @description Custom element for animated <dialog> management.
11 *
12 * Registers a custom element (hw13-dialog or hw13-dialog-{name}) that:
13 * - Opens/closes via data-open attribute (observed, triggers animations)
14 * - Manages aria-expanded on associated buttons (inside = close, outside = open)
15 * - Reads animation parameters from CSS custom properties for full customization
16 * - Handles Escape key, backdrop click, and prefers-reduced-motion
17 *
18 * CSS custom properties read from the <dialog> element:
19 * - --hw13-dialog-animation-duration          (default: 400ms)
20 * - --hw13-dialog-animation-open-timing       (default: cubic-bezier(0, 0.5, 0.5, 1))
21 * - --hw13-dialog-animation-close-timing      (default: cubic-bezier(0.5, 0, 1, 0.5))
22 * - --hw13-dialog-animation-closed-transform  (default: scale(0.95))
23 * - --hw13-dialog-animation-open-transform    (default: scale(1))
24 * - --hw13-dialog-animation-closed-opacity    (default: 0)
25 * - --hw13-dialog-animation-open-opacity      (default: 1)
26 *
27 * Loading:
28 * - Without name param: registers <hw13-dialog>
29 * - With ?name=xyz:     registers <hw13-dialog-xyz>
30 */
31
32const OPEN_ATTR = 'data-open';
33
34class Hw13Dialog extends HTMLElement {
35	static observedAttributes = [OPEN_ATTR];
36
37	constructor() {
38		super();
39		this._dialogElement = null;
40		this._buttonsInside = null;
41		this._buttonsOutside = null;
42		this._currentAnimation = null;
43	}
44
45	// --- Element Getters (lazy, cached) ---
46
47	get dialogElement() {
48		if (!this._dialogElement) {
49			this._dialogElement = this.querySelector(':scope > dialog');
50		}
51		return this._dialogElement;
52	}
53
54	get buttonsInside() {
55		if (!this._buttonsInside && this.dialogElement) {
56			this._buttonsInside = Array.from(
57				this.querySelectorAll(
58					`:scope > dialog button[aria-controls="${this.dialogElement.id}"]`,
59				),
60			);
61		}
62		return this._buttonsInside || [];
63	}
64
65	get buttonsOutside() {
66		if (!this._buttonsOutside && this.dialogElement) {
67			this._buttonsOutside = Array.from(
68				document.querySelectorAll(`button[aria-controls="${this.dialogElement.id}"]`),
69			).filter((btn) => !this.buttonsInside.includes(btn));
70		}
71		return this._buttonsOutside || [];
72	}
73
74	get allButtons() {
75		return [...this.buttonsInside, ...this.buttonsOutside];
76	}
77
78	// --- Lifecycle ---
79
80	connectedCallback() {
81		if (!this.dialogElement) return;
82
83		this.buttonsInside.forEach((btn) => {
84			btn.addEventListener('click', () => {
85				this.setAttribute(OPEN_ATTR, 'false');
86			});
87		});
88
89		this.buttonsOutside.forEach((btn) => {
90			btn.addEventListener('click', () => {
91				this.setAttribute(OPEN_ATTR, 'true');
92			});
93		});
94
95		// Prevent native Escape from closing without animation
96		this.dialogElement.addEventListener('cancel', (e) => {
97			e.preventDefault();
98			this.setAttribute(OPEN_ATTR, 'false');
99		});
100
101		// Sync data-open when the dialog is closed by any means
102		// (native close(), form method="dialog", unhandled cancel, etc.)
103		this.dialogElement.addEventListener('close', () => {
104			if (this.getAttribute(OPEN_ATTR) === 'true') {
105				this.setAttribute(OPEN_ATTR, 'false');
106			}
107		});
108
109		// Close on backdrop click
110		this.dialogElement.addEventListener('click', (e) => {
111			if (e.target === this.dialogElement) {
112				this.setAttribute(OPEN_ATTR, 'false');
113			}
114		});
115
116		// Handle initial open state
117		if (this.getAttribute(OPEN_ATTR) === 'true' && !this.dialogElement.open) {
118			this.open();
119		}
120	}
121
122	attributeChangedCallback(name, oldValue, newValue) {
123		if (oldValue === null || oldValue === newValue) return;
124
125		if (newValue === 'true') {
126			this.open();
127		} else {
128			this.close();
129		}
130
131		this.allButtons.forEach((btn) => {
132			btn.setAttribute('aria-expanded', newValue);
133		});
134	}
135
136	// --- Animation Config (read from CSS custom properties) ---
137
138	getAnimationConfig() {
139		const dialog = this.dialogElement;
140		if (!dialog) return {};
141
142		const styles = getComputedStyle(dialog);
143		const prop = (name, fallback) => styles.getPropertyValue(name).trim() || fallback;
144
145		return {
146			duration: parseInt(prop('--hw13-dialog-animation-duration', '400'), 10),
147			openTiming: prop('--hw13-dialog-animation-open-timing', 'cubic-bezier(0, 0.5, 0.5, 1)'),
148			closeTiming: prop(
149				'--hw13-dialog-animation-close-timing',
150				'cubic-bezier(0.5, 0, 1, 0.5)',
151			),
152			closedTransform: prop('--hw13-dialog-animation-closed-transform', 'scale(0.95)'),
153			openTransform: prop('--hw13-dialog-animation-open-transform', 'scale(1)'),
154			closedOpacity: prop('--hw13-dialog-animation-closed-opacity', '0'),
155			openOpacity: prop('--hw13-dialog-animation-open-opacity', '1'),
156		};
157	}
158
159	prefersReducedMotion() {
160		return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
161	}
162
163	cancelCurrentAnimation() {
164		this._currentAnimation?.cancel();
165		this._currentAnimation = null;
166	}
167
168	// --- Open / Close ---
169
170	open() {
171		const element = this.dialogElement;
172		if (!element) return;
173
174		const config = this.getAnimationConfig();
175		this.cancelCurrentAnimation();
176		element.showModal();
177
178		if (this.prefersReducedMotion()) return;
179
180		this._currentAnimation = element.animate(
181			[
182				{
183					transform: config.closedTransform,
184					opacity: config.closedOpacity,
185				},
186				{
187					transform: config.openTransform,
188					opacity: config.openOpacity,
189				},
190			],
191			{
192				duration: config.duration,
193				easing: config.openTiming,
194				fill: 'forwards',
195			},
196		);
197
198		this._currentAnimation.finished
199			.then(() => {
200				this._currentAnimation = null;
201				element.style.transform = '';
202				element.style.opacity = '';
203			})
204			.catch(() => {
205				/* cancelled */
206			});
207	}
208
209	close() {
210		const element = this.dialogElement;
211		if (!element) return;
212
213		const config = this.getAnimationConfig();
214		this.cancelCurrentAnimation();
215
216		if (this.prefersReducedMotion()) {
217			element.close();
218			return;
219		}
220
221		this._currentAnimation = element.animate(
222			[
223				{
224					transform: config.openTransform,
225					opacity: config.openOpacity,
226				},
227				{
228					transform: config.closedTransform,
229					opacity: config.closedOpacity,
230				},
231			],
232			{
233				duration: config.duration,
234				easing: config.closeTiming,
235				fill: 'forwards',
236			},
237		);
238
239		this._currentAnimation.finished
240			.then(() => {
241				this._currentAnimation = null;
242				element.style.transform = '';
243				element.style.opacity = '';
244				element.close();
245			})
246			.catch(() => {
247				/* cancelled */
248			});
249	}
250}
251
252// --- Registration ---
253// Without ?name param: registers hw13-dialog (standard)
254// With ?name=xyz:      registers hw13-dialog-xyz (custom variant)
255
256function register() {
257	const url = new URL(import.meta.url);
258	const name = url.searchParams.get('name');
259	const tagName = name ? `hw13-dialog-${name}` : 'hw13-dialog';
260
261	if (!customElements.get(tagName)) {
262		customElements.define(tagName, class extends Hw13Dialog {});
263	}
264}
265
266if (document.readyState === 'complete') {
267	register();
268} else {
269	document.addEventListener('readystatechange', () => {
270		if (document.readyState === 'complete') {
271			register();
272		}
273	});
274}

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.