@getquick/content 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/consent-astro.js +2 -2
- package/dist/consent-client.js +19 -6
- package/dist/consent.js +2 -2
- package/dist/design-client.d.ts +19 -0
- package/dist/design-client.js +519 -0
- package/dist/{dist-D4-CMkrf.js → dist-5jNKLmbn.js} +198 -3
- package/dist/{embeds-p9SyGNZI.js → embeds-CJJemAmE.js} +2 -2
- package/dist/index.d.ts +134 -10
- package/dist/index.js +9 -2
- package/dist/metadata-content.js +1 -1
- package/dist/metadata-testing.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +126 -20
- package/dist/site-language-BcPac0Ia.js +148183 -0
- package/dist/{version-D_Ao31D3.js → version-DkpQm85L.js} +1 -1
- package/dist/video-hero-D8AcMR4q.js +69 -0
- package/dist/{wordpress-D_HG2rEt.d.ts → wordpress-C1xs3xHN.d.ts} +241 -3
- package/package.json +5 -1
- package/dist/site-language-cBP8SFrS.js +0 -528
package/README.md
CHANGED
|
@@ -290,3 +290,84 @@ content runtime, framework or WordPress client. Passing
|
|
|
290
290
|
at delivery and stores gq-config's Legal Contract and GA4 settings in each
|
|
291
291
|
language's chrome; `runtime.consentConfiguration(chrome)` returns the page's
|
|
292
292
|
consent configuration. See the repository's `docs/consent.md`.
|
|
293
|
+
|
|
294
|
+
## Design presentation and composition
|
|
295
|
+
|
|
296
|
+
Sites import the canonical presentation and theme APIs from `@getquick/content`.
|
|
297
|
+
The package bundles its pinned Design renderer and contract; Sites do not install
|
|
298
|
+
those packages themselves. Apply `gqd-content` to body and chrome regions and emit
|
|
299
|
+
`designThemeStyleSheet(theme)` followed by `presentationStyleSheet(theme)` once
|
|
300
|
+
per response, using the delivered `theme` from entry/home/chrome. Resolve defaults
|
|
301
|
+
with `resolveDesignTheme` for older stored publications. A changed theme requires
|
|
302
|
+
regenerating both stylesheets, including its media thresholds.
|
|
303
|
+
|
|
304
|
+
`presentationHtml(name, shell, presentation, { attributes, model? })` decorates an
|
|
305
|
+
existing root shell. Pass the complete block attributes for native Group layout
|
|
306
|
+
context. `presentationVariables` takes the same attributes context. Both delegate
|
|
307
|
+
validation and responsive rules to the bundled renderer; saved variables survive
|
|
308
|
+
when parsed attributes are absent. An optional extended `model` must also be
|
|
309
|
+
passed to the corresponding canonical stylesheet generator.
|
|
310
|
+
|
|
311
|
+
`renderDesignPattern` restores native text/list structures, media and layout
|
|
312
|
+
wrappers, resolved Navigation and Query composition. Site `options.blocks`
|
|
313
|
+
renderers retain precedence. Complete saved media HTML stays authoritative;
|
|
314
|
+
source-only Image nodes use `renderDesignImage`. The public low-level HTML
|
|
315
|
+
adapters also support parsed Site adapters without a second presentation parser.
|
|
316
|
+
|
|
317
|
+
Carousel uses `renderDesignCarousel(attributes, orderedChildren, shell?)`, where
|
|
318
|
+
children carry `{ name, attributes, html }` and each `html` is the complete rendered
|
|
319
|
+
child. Supply exactly one navigation child, even though WordPress saves its HTML
|
|
320
|
+
as null. `renderDesignCarouselItem` accepts its rendered foreground children and
|
|
321
|
+
an optional saved figure shell. Emit `designCarouselStyle()` once. Video Hero
|
|
322
|
+
uses `renderDesignVideoHero` and the canonical `videoHeroStyleSheet()`; deferred
|
|
323
|
+
iframes remain `data-src` until consent and motion preferences allow playback.
|
|
324
|
+
Navigation uses `navigationStyleSheet(theme)` for interaction geometry.
|
|
325
|
+
|
|
326
|
+
Browser lifecycle is isolated in `@getquick/content/design/client`, exporting
|
|
327
|
+
`mountDesignCarousels`, `mountVideoHero` and `mountDesignNavigation`. Each returns
|
|
328
|
+
cleanup; mount again after navigation replaces the DOM. Importing the root does
|
|
329
|
+
not require browser globals. The configured runtime gates generated Hero media
|
|
330
|
+
before composing already-delivered children, so consent hooks are not rewritten
|
|
331
|
+
a second time.
|
|
332
|
+
|
|
333
|
+
For a producer-resolved Query, preload outside the synchronous renderer:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
const loaded = await runtime.preloadDesignQueryPosts(pattern, siteOrigin);
|
|
337
|
+
const html = runtime.renderDesignPattern(pattern, {
|
|
338
|
+
siteOrigin,
|
|
339
|
+
resolvePost: loaded.resolvePost,
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
This uses the existing `publishedEntry(item.uri)` read for standard Post/Page
|
|
344
|
+
items. Its optional `databaseId` and canonical `designBlocks` become a typed
|
|
345
|
+
`DesignPostContext` through `designPostContext`. Older publications without the
|
|
346
|
+
neutral tree cannot supply that context. Custom post types require an explicit
|
|
347
|
+
authorized preloaded record through `preloadDesignQueryPosts(pattern, load)`;
|
|
348
|
+
this API adds no custom post type GraphQL field or query selection engine.
|
|
349
|
+
Delivered item order, count and duplicates are preserved without applying Query
|
|
350
|
+
arguments again. Synced references add no layout wrapper, and Post Content
|
|
351
|
+
recursively renders the matching preloaded neutral blocks.
|
|
352
|
+
|
|
353
|
+
Native decorative Cover backgrounds use the same explicit Site lifecycle:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import { mountDesignCoverVideos } from "@getquick/content/design/client";
|
|
357
|
+
const cleanup = mountDesignCoverVideos(document);
|
|
358
|
+
// Invoke cleanup before removing/replacing the rendered page; remount afterward.
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The configured publication delivery and `presentationHtml("core/cover", ...)`
|
|
362
|
+
remove eager `autoplay` from direct native Cover background videos. The initializer
|
|
363
|
+
plays when reduced motion is off, pauses when enabled, resumes when disabled,
|
|
364
|
+
and pauses/removes its preference listener on cleanup. It does not own foreground
|
|
365
|
+
or ordinary content videos. If a Site renders raw Cover HTML outside these paths,
|
|
366
|
+
apply `enhanceDesignCoverVideosHtml(html)` before SSR. Sources, posters and ordered
|
|
367
|
+
foreground remain authoritative; no legacy appearance properties are inferred.
|
|
368
|
+
|
|
369
|
+
`mountDesignNavigation` also enhances native Polylang dropdowns inside
|
|
370
|
+
`.wp-block-polylang-language-switcher`, including ordinary published HTML whose
|
|
371
|
+
inline controller was removed during delivery. It navigates to the delivered
|
|
372
|
+
selected option's HTTP(S) URL; it does not resolve languages or evaluate inline
|
|
373
|
+
scripts. Cleanup removes that handler and restores the original selection.
|
package/dist/consent-astro.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { S as SERVICE_CATALOG, T as copyLanguage, c as grantedCategories, d as promptReason, f as readConsentCookie, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
2
|
-
import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-
|
|
3
|
-
import { t as CONSENT_UI_VERSION } from "./version-
|
|
2
|
+
import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-CJJemAmE.js";
|
|
3
|
+
import { t as CONSENT_UI_VERSION } from "./version-DkpQm85L.js";
|
|
4
4
|
import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
|
|
5
5
|
import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
|
|
6
6
|
//#region frontend/src/consent/astro.ts
|
package/dist/consent-client.js
CHANGED
|
@@ -121,13 +121,26 @@ function loadEmbed(placeholder, id) {
|
|
|
121
121
|
if (src.protocol !== "https:" || embedService(src) !== id) return;
|
|
122
122
|
const iframe = document.createElement("iframe");
|
|
123
123
|
for (const [name, value] of Object.entries(embed.attrs)) if (EMBED_ATTRIBUTES.has(name.toLowerCase()) && typeof value === "string") iframe.setAttribute(name, value);
|
|
124
|
-
iframe.src = src.href;
|
|
125
124
|
const original = placeholder.cloneNode(true);
|
|
126
|
-
placeholder.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
if (placeholder.hasAttribute("data-gq-decorative") && iframe.classList.contains("gqd-video-hero__video")) {
|
|
126
|
+
iframe.dataset.src = src.href;
|
|
127
|
+
for (const name of [
|
|
128
|
+
"data-gq-consent-embed",
|
|
129
|
+
"data-gq-category",
|
|
130
|
+
"data-gq-embed",
|
|
131
|
+
"data-gq-decorative"
|
|
132
|
+
]) if (placeholder.hasAttribute(name)) iframe.setAttribute(name, placeholder.getAttribute(name));
|
|
133
|
+
iframe.setAttribute("data-gq-loaded", "");
|
|
134
|
+
placeholder.replaceWith(iframe);
|
|
135
|
+
originals.set(iframe, original);
|
|
136
|
+
} else {
|
|
137
|
+
iframe.src = src.href;
|
|
138
|
+
placeholder.replaceChildren(iframe);
|
|
139
|
+
placeholder.setAttribute("data-gq-loaded", "");
|
|
140
|
+
placeholder.removeAttribute("role");
|
|
141
|
+
placeholder.removeAttribute("aria-label");
|
|
142
|
+
originals.set(placeholder, original);
|
|
143
|
+
}
|
|
131
144
|
}
|
|
132
145
|
const originals = /* @__PURE__ */ new WeakMap();
|
|
133
146
|
function unloadEmbed(placeholder) {
|
package/dist/consent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
2
2
|
import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
|
|
3
|
-
import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-
|
|
4
|
-
import { t as CONSENT_UI_VERSION } from "./version-
|
|
3
|
+
import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-CJJemAmE.js";
|
|
4
|
+
import { t as CONSENT_UI_VERSION } from "./version-DkpQm85L.js";
|
|
5
5
|
export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region frontend/src/carousel-client.d.ts
|
|
2
|
+
/** Mount each Carousel once. Sites invoke on page load and call cleanup before
|
|
3
|
+
* client navigation replaces the page. DOM APIs are read only on invocation. */
|
|
4
|
+
export declare function mountDesignCarousels(root?: ParentNode): () => void;
|
|
5
|
+
//#endregion
|
|
6
|
+
/** Enhance only decorative media; nested content keeps its native interaction. */
|
|
7
|
+
export declare function mountVideoHero(hero: HTMLElement): () => void;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region frontend/src/navigation-client.d.ts
|
|
10
|
+
/** Mount the adapter's navigation markup once; cleanup restores server state.
|
|
11
|
+
* Native links retain ordinary Tab order (this is navigation, not an ARIA menu).
|
|
12
|
+
* Install navigationStyleSheet alongside the canonical presentation stylesheet. */
|
|
13
|
+
export declare function mountDesignNavigation(root?: ParentNode): () => void;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region frontend/src/cover-video-client.d.ts
|
|
16
|
+
/** Mount decorative native Cover backgrounds in neutral, saved or delivered HTML.
|
|
17
|
+
* No DOM access occurs until invocation. The Site owns mounting and cleanup. */
|
|
18
|
+
export declare function mountDesignCoverVideos(root?: ParentNode): () => void;
|
|
19
|
+
//#endregion
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { t as videoHeroFrameGeometry } from "./video-hero-D8AcMR4q.js";
|
|
2
|
+
//#region frontend/src/carousel-client.ts
|
|
3
|
+
/** Mount each Carousel once. Sites invoke on page load and call cleanup before
|
|
4
|
+
* client navigation replaces the page. DOM APIs are read only on invocation. */
|
|
5
|
+
function mountDesignCarousels(root) {
|
|
6
|
+
const scope = root ?? (typeof document === "undefined" ? void 0 : document);
|
|
7
|
+
if (!scope) return () => {};
|
|
8
|
+
const carousels = [...scope.querySelectorAll("[data-gqd-carousel]")];
|
|
9
|
+
if ("matches" in scope && typeof scope.matches === "function" && scope.matches("[data-gqd-carousel]")) carousels.unshift(scope);
|
|
10
|
+
const cleanups = [];
|
|
11
|
+
for (const carousel of carousels) {
|
|
12
|
+
if (carousel.hasAttribute("data-gqd-mounted")) continue;
|
|
13
|
+
const slides = [...carousel.children].filter((child) => child instanceof HTMLElement && child.classList.contains("wp-block-getquick-design-slider"));
|
|
14
|
+
if (!slides.length) continue;
|
|
15
|
+
carousel.setAttribute("data-gqd-mounted", "");
|
|
16
|
+
const view = carousel.ownerDocument.defaultView;
|
|
17
|
+
if (!view) continue;
|
|
18
|
+
const motion = view.matchMedia("(prefers-reduced-motion: reduce)");
|
|
19
|
+
const buttons = [...carousel.querySelectorAll("button[data-gqd-go]")].filter((node) => node.closest("[data-gqd-carousel]") === carousel);
|
|
20
|
+
const previous = slides.map((slide) => ({
|
|
21
|
+
slide,
|
|
22
|
+
hidden: slide.hidden,
|
|
23
|
+
inert: slide.inert,
|
|
24
|
+
role: slide.getAttribute("role"),
|
|
25
|
+
label: slide.getAttribute("aria-label"),
|
|
26
|
+
description: slide.getAttribute("aria-roledescription")
|
|
27
|
+
}));
|
|
28
|
+
let index = 0;
|
|
29
|
+
let timer;
|
|
30
|
+
let hovered = false;
|
|
31
|
+
let focused = false;
|
|
32
|
+
let stopped = false;
|
|
33
|
+
const listeners = [];
|
|
34
|
+
const listen = (target, name, handler) => {
|
|
35
|
+
target.addEventListener(name, handler);
|
|
36
|
+
listeners.push(() => target.removeEventListener(name, handler));
|
|
37
|
+
};
|
|
38
|
+
const cancel = () => {
|
|
39
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
40
|
+
timer = void 0;
|
|
41
|
+
};
|
|
42
|
+
const schedule = () => {
|
|
43
|
+
cancel();
|
|
44
|
+
if (stopped || carousel.dataset.gqdAutoplay !== "true" || slides.length < 2 || motion.matches || hovered || focused || carousel.ownerDocument.hidden) return;
|
|
45
|
+
const seconds = Number(carousel.dataset.gqdDuration);
|
|
46
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 120) return;
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
if (motion.matches || hovered || focused || carousel.ownerDocument.hidden) {
|
|
49
|
+
schedule();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
show(index + 1);
|
|
53
|
+
}, seconds * 1e3);
|
|
54
|
+
};
|
|
55
|
+
const show = (next) => {
|
|
56
|
+
index = (next + slides.length) % slides.length;
|
|
57
|
+
slides.forEach((slide, position) => {
|
|
58
|
+
slide.dataset.gqdSlide = "";
|
|
59
|
+
slide.hidden = position !== index;
|
|
60
|
+
slide.inert = position !== index;
|
|
61
|
+
slide.setAttribute("role", "group");
|
|
62
|
+
slide.setAttribute("aria-roledescription", "slide");
|
|
63
|
+
slide.setAttribute("aria-label", `${position + 1} of ${slides.length}`);
|
|
64
|
+
});
|
|
65
|
+
buttons.forEach((button) => {
|
|
66
|
+
if (Number(button.dataset.gqdGo) === index) button.setAttribute("aria-current", "true");
|
|
67
|
+
else button.removeAttribute("aria-current");
|
|
68
|
+
});
|
|
69
|
+
schedule();
|
|
70
|
+
};
|
|
71
|
+
listen(carousel, "click", (event) => {
|
|
72
|
+
const target = event.target instanceof Element ? event.target.closest("button") : null;
|
|
73
|
+
if (!target || target.closest("[data-gqd-carousel]") !== carousel) return;
|
|
74
|
+
if (target.hasAttribute("data-gqd-next")) show(index + 1);
|
|
75
|
+
else if (target.hasAttribute("data-gqd-previous")) show(index - 1);
|
|
76
|
+
else if (target.dataset.gqdGo !== void 0) show(Number(target.dataset.gqdGo));
|
|
77
|
+
});
|
|
78
|
+
listen(carousel, "keydown", (event) => {
|
|
79
|
+
if (!(event instanceof KeyboardEvent) || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
80
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
81
|
+
if (!target || target.closest("[data-gqd-carousel]") !== carousel || target.closest("input,textarea,select,[contenteditable=true]")) return;
|
|
82
|
+
const control = target.closest("button[data-gqd-next],button[data-gqd-previous],button[data-gqd-go]");
|
|
83
|
+
if (!control || control.closest(".wp-block-getquick-design-carousel-navigation")?.parentElement !== carousel) return;
|
|
84
|
+
if (![
|
|
85
|
+
"ArrowLeft",
|
|
86
|
+
"ArrowRight",
|
|
87
|
+
"Home",
|
|
88
|
+
"End"
|
|
89
|
+
].includes(event.key)) return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
const rtl = view.getComputedStyle(carousel).direction === "rtl";
|
|
92
|
+
show(event.key === "Home" ? 0 : event.key === "End" ? slides.length - 1 : index + (event.key === "ArrowRight" !== rtl ? 1 : -1));
|
|
93
|
+
});
|
|
94
|
+
listen(carousel, "mouseenter", () => {
|
|
95
|
+
hovered = true;
|
|
96
|
+
cancel();
|
|
97
|
+
});
|
|
98
|
+
listen(carousel, "mouseleave", () => {
|
|
99
|
+
hovered = false;
|
|
100
|
+
schedule();
|
|
101
|
+
});
|
|
102
|
+
listen(carousel, "focusin", () => {
|
|
103
|
+
focused = true;
|
|
104
|
+
cancel();
|
|
105
|
+
});
|
|
106
|
+
listen(carousel, "focusout", (event) => {
|
|
107
|
+
focused = event instanceof FocusEvent && event.relatedTarget instanceof Node && carousel.contains(event.relatedTarget);
|
|
108
|
+
schedule();
|
|
109
|
+
});
|
|
110
|
+
listen(motion, "change", schedule);
|
|
111
|
+
listen(carousel.ownerDocument, "visibilitychange", schedule);
|
|
112
|
+
show(0);
|
|
113
|
+
cleanups.push(() => {
|
|
114
|
+
stopped = true;
|
|
115
|
+
cancel();
|
|
116
|
+
listeners.forEach((remove) => remove());
|
|
117
|
+
carousel.removeAttribute("data-gqd-mounted");
|
|
118
|
+
for (const { slide, hidden, inert, role, label, description } of previous) {
|
|
119
|
+
slide.hidden = hidden;
|
|
120
|
+
slide.inert = inert;
|
|
121
|
+
slide.removeAttribute("data-gqd-slide");
|
|
122
|
+
for (const [name, value] of [
|
|
123
|
+
["role", role],
|
|
124
|
+
["aria-label", label],
|
|
125
|
+
["aria-roledescription", description]
|
|
126
|
+
]) if (value === null) slide.removeAttribute(name);
|
|
127
|
+
else slide.setAttribute(name, value);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
/** Enhance only decorative media; nested content keeps its native interaction. */
|
|
135
|
+
function mountVideoHero(hero) {
|
|
136
|
+
const media = hero.querySelector(":scope > .gqd-video-hero__media");
|
|
137
|
+
if (!media) return () => {};
|
|
138
|
+
let frame = null;
|
|
139
|
+
let video = null;
|
|
140
|
+
const sources = /* @__PURE__ */ new WeakMap();
|
|
141
|
+
const motion = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
|
|
142
|
+
const update = () => {
|
|
143
|
+
const nextFrame = media.querySelector("iframe");
|
|
144
|
+
const nextVideo = media.querySelector("video");
|
|
145
|
+
if (frame !== nextFrame) frame?.removeAttribute("src");
|
|
146
|
+
if (video !== nextVideo) video?.pause();
|
|
147
|
+
frame = nextFrame;
|
|
148
|
+
video = nextVideo;
|
|
149
|
+
if (video) {
|
|
150
|
+
if (motion.matches) video.pause();
|
|
151
|
+
else video.play().catch(() => {});
|
|
152
|
+
}
|
|
153
|
+
if (!frame) return;
|
|
154
|
+
const source = frame.dataset.src || frame.getAttribute("src") || sources.get(frame);
|
|
155
|
+
if (source) sources.set(frame, source);
|
|
156
|
+
if (motion.matches) {
|
|
157
|
+
frame.removeAttribute("src");
|
|
158
|
+
frame.hidden = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
frame.hidden = false;
|
|
162
|
+
if (source && frame.getAttribute("src") !== source) frame.setAttribute("src", source);
|
|
163
|
+
const style = globalThis.getComputedStyle(frame);
|
|
164
|
+
const [x, y] = style.objectPosition.split(" ").map((v) => Number.parseFloat(v) / 100);
|
|
165
|
+
const geometry = videoHeroFrameGeometry({
|
|
166
|
+
width: media.clientWidth,
|
|
167
|
+
height: media.clientHeight
|
|
168
|
+
}, style.objectFit, {
|
|
169
|
+
x: Number.isFinite(x) ? x : .5,
|
|
170
|
+
y: Number.isFinite(y) ? y : .5
|
|
171
|
+
});
|
|
172
|
+
for (const [property, value] of Object.entries(geometry)) frame.style.setProperty(property, `${value}px`);
|
|
173
|
+
};
|
|
174
|
+
const resize = new globalThis.ResizeObserver(update);
|
|
175
|
+
resize.observe(media);
|
|
176
|
+
const attributes = new globalThis.MutationObserver(update);
|
|
177
|
+
attributes.observe(hero, {
|
|
178
|
+
attributes: true,
|
|
179
|
+
attributeFilter: ["style", "class"]
|
|
180
|
+
});
|
|
181
|
+
const children = new globalThis.MutationObserver(update);
|
|
182
|
+
children.observe(media, {
|
|
183
|
+
childList: true,
|
|
184
|
+
subtree: true
|
|
185
|
+
});
|
|
186
|
+
globalThis.addEventListener("resize", update);
|
|
187
|
+
motion.addEventListener("change", update);
|
|
188
|
+
update();
|
|
189
|
+
return () => {
|
|
190
|
+
resize.disconnect();
|
|
191
|
+
attributes.disconnect();
|
|
192
|
+
children.disconnect();
|
|
193
|
+
globalThis.removeEventListener("resize", update);
|
|
194
|
+
motion.removeEventListener("change", update);
|
|
195
|
+
video?.pause();
|
|
196
|
+
frame?.removeAttribute("src");
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region frontend/src/navigation-client.ts
|
|
201
|
+
const mounted$1 = /* @__PURE__ */ new WeakSet();
|
|
202
|
+
const inertLocks = /* @__PURE__ */ new WeakMap();
|
|
203
|
+
function inertBackground(panel) {
|
|
204
|
+
const locked = [];
|
|
205
|
+
for (let branch = panel; branch.parentElement; branch = branch.parentElement) {
|
|
206
|
+
for (const sibling of branch.parentElement.children) {
|
|
207
|
+
if (sibling === branch || !("inert" in sibling)) continue;
|
|
208
|
+
const node = sibling;
|
|
209
|
+
const lock = inertLocks.get(node) ?? {
|
|
210
|
+
count: 0,
|
|
211
|
+
original: node.inert
|
|
212
|
+
};
|
|
213
|
+
lock.count += 1;
|
|
214
|
+
inertLocks.set(node, lock);
|
|
215
|
+
node.inert = true;
|
|
216
|
+
locked.push(node);
|
|
217
|
+
}
|
|
218
|
+
if (branch.parentElement === panel.ownerDocument.body) break;
|
|
219
|
+
}
|
|
220
|
+
return () => {
|
|
221
|
+
for (const node of locked) {
|
|
222
|
+
const lock = inertLocks.get(node);
|
|
223
|
+
if (!lock || --lock.count > 0) continue;
|
|
224
|
+
node.inert = lock.original;
|
|
225
|
+
inertLocks.delete(node);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Mount the adapter's navigation markup once; cleanup restores server state.
|
|
230
|
+
* Native links retain ordinary Tab order (this is navigation, not an ARIA menu).
|
|
231
|
+
* Install navigationStyleSheet alongside the canonical presentation stylesheet. */
|
|
232
|
+
function mountDesignNavigation(root) {
|
|
233
|
+
const scope = root ?? (typeof document === "undefined" ? void 0 : document);
|
|
234
|
+
if (!scope) return () => {};
|
|
235
|
+
const cleanups = [];
|
|
236
|
+
const languageSelects = [...scope.querySelectorAll(".wp-block-polylang-language-switcher select.pll-switcher-select")];
|
|
237
|
+
if ("matches" in scope && typeof scope.matches === "function" && scope.matches(".wp-block-polylang-language-switcher select.pll-switcher-select")) languageSelects.unshift(scope);
|
|
238
|
+
for (const select of languageSelects) {
|
|
239
|
+
const view = select.ownerDocument.defaultView;
|
|
240
|
+
if (!view || mounted$1.has(select)) continue;
|
|
241
|
+
mounted$1.add(select);
|
|
242
|
+
const original = select.selectedIndex;
|
|
243
|
+
const change = (event) => {
|
|
244
|
+
event.stopImmediatePropagation();
|
|
245
|
+
const option = select.selectedOptions[0];
|
|
246
|
+
const value = option?.value.trim();
|
|
247
|
+
if (!option || option.disabled || !value || value.includes("\\")) return;
|
|
248
|
+
for (let index = 0; index < value.length; index++) if (value.charCodeAt(index) <= 32 || value.charCodeAt(index) === 127) return;
|
|
249
|
+
try {
|
|
250
|
+
const url = new URL(value, select.ownerDocument.baseURI);
|
|
251
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return;
|
|
252
|
+
view.location.assign(url.href);
|
|
253
|
+
} catch {}
|
|
254
|
+
};
|
|
255
|
+
select.addEventListener("change", change, true);
|
|
256
|
+
cleanups.push(() => {
|
|
257
|
+
select.removeEventListener("change", change, true);
|
|
258
|
+
select.selectedIndex = original;
|
|
259
|
+
mounted$1.delete(select);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const languageMenus = [...scope.querySelectorAll("details[data-gqd-language-menu]")];
|
|
263
|
+
if ("matches" in scope && typeof scope.matches === "function" && scope.matches("details[data-gqd-language-menu]")) languageMenus.unshift(scope);
|
|
264
|
+
for (const menu of languageMenus) {
|
|
265
|
+
if (mounted$1.has(menu)) continue;
|
|
266
|
+
mounted$1.add(menu);
|
|
267
|
+
const original = menu.open;
|
|
268
|
+
const escape = (event) => {
|
|
269
|
+
if (event.key === "Escape" && menu.open) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
menu.open = false;
|
|
272
|
+
menu.querySelector("summary")?.focus();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
menu.addEventListener("keydown", escape);
|
|
276
|
+
cleanups.push(() => {
|
|
277
|
+
menu.removeEventListener("keydown", escape);
|
|
278
|
+
menu.open = original;
|
|
279
|
+
mounted$1.delete(menu);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const roots = [...scope.querySelectorAll("[data-gqd-navigation]")];
|
|
283
|
+
if ("matches" in scope && typeof scope.matches === "function" && scope.matches("[data-gqd-navigation]")) roots.unshift(scope);
|
|
284
|
+
for (const nav of roots) {
|
|
285
|
+
if (mounted$1.has(nav)) continue;
|
|
286
|
+
const view = nav.ownerDocument.defaultView;
|
|
287
|
+
const panel = nav.querySelector("[data-gqd-navigation-panel]");
|
|
288
|
+
const open = nav.querySelector("[data-gqd-navigation-open]");
|
|
289
|
+
const close = nav.querySelector("[data-gqd-navigation-close]");
|
|
290
|
+
if (!view || !panel || !open || !close) continue;
|
|
291
|
+
mounted$1.add(nav);
|
|
292
|
+
const buttons = [...nav.querySelectorAll("[data-gqd-submenu]")];
|
|
293
|
+
const lists = new Map(buttons.map((button) => [button, [...nav.querySelectorAll("[data-gqd-submenu-list]")].find((list) => list.id === button.getAttribute("aria-controls"))]));
|
|
294
|
+
const saved = /* @__PURE__ */ new Map();
|
|
295
|
+
const remember = (node, names) => saved.set(node, new Map(names.map((name) => [name, node.getAttribute(name)])));
|
|
296
|
+
remember(nav, ["data-gqd-navigation-enhanced", "data-gqd-overlay-active"]);
|
|
297
|
+
remember(panel, [
|
|
298
|
+
"hidden",
|
|
299
|
+
"role",
|
|
300
|
+
"aria-modal",
|
|
301
|
+
"aria-label",
|
|
302
|
+
"style"
|
|
303
|
+
]);
|
|
304
|
+
remember(open, ["aria-expanded"]);
|
|
305
|
+
buttons.forEach((button) => remember(button, ["aria-expanded"]));
|
|
306
|
+
for (const list of lists.values()) if (list) remember(list, ["hidden", "style"]);
|
|
307
|
+
const restore = (node, names) => {
|
|
308
|
+
for (const name of names) {
|
|
309
|
+
const value = saved.get(node)?.get(name);
|
|
310
|
+
if (value == null) node.removeAttribute(name);
|
|
311
|
+
else node.setAttribute(name, value);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const currentUrl = new URL(nav.ownerDocument.URL);
|
|
315
|
+
const path = (value) => value.replace(/\/+$/, "") || "/";
|
|
316
|
+
for (const anchor of nav.querySelectorAll("a[href]")) {
|
|
317
|
+
if (anchor.hasAttribute("aria-current")) continue;
|
|
318
|
+
try {
|
|
319
|
+
const target = new URL(anchor.getAttribute("href"), nav.ownerDocument.baseURI);
|
|
320
|
+
if (!target.hash && target.origin === currentUrl.origin && path(target.pathname) === path(currentUrl.pathname) && (!target.search || target.search === currentUrl.search)) {
|
|
321
|
+
remember(anchor, ["aria-current"]);
|
|
322
|
+
anchor.setAttribute("aria-current", "page");
|
|
323
|
+
}
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
const listeners = [];
|
|
327
|
+
const listen = (node, name, callback) => {
|
|
328
|
+
node.addEventListener(name, callback);
|
|
329
|
+
listeners.push(() => node.removeEventListener(name, callback));
|
|
330
|
+
};
|
|
331
|
+
let overlay = false;
|
|
332
|
+
let expanded = false;
|
|
333
|
+
let unlock;
|
|
334
|
+
const element = (target) => target && "nodeType" in target && target.nodeType === 1 ? target : null;
|
|
335
|
+
const setHidden = (node, hidden) => {
|
|
336
|
+
node.hidden = hidden;
|
|
337
|
+
restore(node, ["style"]);
|
|
338
|
+
if (hidden) node.style.setProperty("display", "none", "important");
|
|
339
|
+
};
|
|
340
|
+
const setSubmenu = (button, state) => {
|
|
341
|
+
const list = lists.get(button);
|
|
342
|
+
button.setAttribute("aria-expanded", String(state));
|
|
343
|
+
if (list) setHidden(list, !state);
|
|
344
|
+
if (!state && list) {
|
|
345
|
+
for (const nested of buttons) if (list.contains(nested)) {
|
|
346
|
+
nested.setAttribute("aria-expanded", "false");
|
|
347
|
+
const child = lists.get(nested);
|
|
348
|
+
if (child) setHidden(child, true);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const focusables = (node) => [...node.querySelectorAll("a[href],button:not([disabled]),summary,[tabindex=\"0\"]")].filter((candidate) => !candidate.closest("[hidden]") && view.getComputedStyle(candidate).visibility !== "hidden" && candidate.getClientRects().length > 0);
|
|
353
|
+
const setOverlay = (state, returnFocus = false) => {
|
|
354
|
+
expanded = state && overlay;
|
|
355
|
+
open.setAttribute("aria-expanded", String(expanded));
|
|
356
|
+
setHidden(panel, overlay && !expanded);
|
|
357
|
+
if (expanded) {
|
|
358
|
+
unlock ??= inertBackground(panel);
|
|
359
|
+
panel.setAttribute("role", "dialog");
|
|
360
|
+
panel.setAttribute("aria-modal", "true");
|
|
361
|
+
panel.setAttribute("aria-label", nav.getAttribute("aria-label") || "Navigation");
|
|
362
|
+
focusables(panel)[0]?.focus();
|
|
363
|
+
} else {
|
|
364
|
+
unlock?.();
|
|
365
|
+
unlock = void 0;
|
|
366
|
+
restore(panel, [
|
|
367
|
+
"role",
|
|
368
|
+
"aria-modal",
|
|
369
|
+
"aria-label"
|
|
370
|
+
]);
|
|
371
|
+
if (returnFocus) open.focus();
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
const threshold = view.getComputedStyle(nav).getPropertyValue("--gqd-navigation-breakpoint").trim() || "64rem";
|
|
375
|
+
const mobile = view.matchMedia(`(width < ${threshold})`);
|
|
376
|
+
const sync = () => {
|
|
377
|
+
const previous = overlay;
|
|
378
|
+
const focused = nav.ownerDocument.activeElement;
|
|
379
|
+
const panelFocused = focused !== null && panel.contains(focused);
|
|
380
|
+
overlay = nav.dataset.gqdOverlay === "always" || nav.dataset.gqdOverlay !== "never" && mobile.matches;
|
|
381
|
+
nav.toggleAttribute("data-gqd-overlay-active", overlay);
|
|
382
|
+
setOverlay(previous === overlay && expanded);
|
|
383
|
+
if (previous !== overlay) {
|
|
384
|
+
if (overlay && panelFocused) open.focus();
|
|
385
|
+
else if (!overlay && (focused === open || focused === close)) focusables(panel)[0]?.focus();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
nav.setAttribute("data-gqd-navigation-enhanced", "");
|
|
389
|
+
for (const button of buttons) setSubmenu(button, nav.dataset.gqdSubmenuVisibility === "always");
|
|
390
|
+
sync();
|
|
391
|
+
listen(mobile, "change", sync);
|
|
392
|
+
listen(nav.ownerDocument, "focusin", (event) => {
|
|
393
|
+
const target = element(event.target);
|
|
394
|
+
if (expanded && target && !panel.contains(target)) focusables(panel)[0]?.focus();
|
|
395
|
+
});
|
|
396
|
+
listen(open, "click", () => setOverlay(true));
|
|
397
|
+
listen(close, "click", () => {
|
|
398
|
+
setOverlay(false, true);
|
|
399
|
+
});
|
|
400
|
+
for (const button of buttons) {
|
|
401
|
+
listen(button, "click", () => {
|
|
402
|
+
if (nav.dataset.gqdSubmenuVisibility !== "always") setSubmenu(button, button.getAttribute("aria-expanded") !== "true");
|
|
403
|
+
});
|
|
404
|
+
const row = button.closest("li") ?? button.parentElement;
|
|
405
|
+
const hover = () => {
|
|
406
|
+
if (nav.dataset.gqdSubmenuVisibility === "hover" && !overlay) setSubmenu(button, true);
|
|
407
|
+
};
|
|
408
|
+
listen(row, "pointerenter", hover);
|
|
409
|
+
listen(row, "pointerleave", () => {
|
|
410
|
+
if (nav.dataset.gqdSubmenuVisibility === "hover" && !row.contains(nav.ownerDocument.activeElement)) setSubmenu(button, false);
|
|
411
|
+
});
|
|
412
|
+
listen(row, "focusin", (event) => {
|
|
413
|
+
if (event.target !== button) hover();
|
|
414
|
+
});
|
|
415
|
+
listen(row, "focusout", (event) => {
|
|
416
|
+
const next = event.relatedTarget;
|
|
417
|
+
if (!next || !row.contains(next)) {
|
|
418
|
+
if (nav.dataset.gqdSubmenuVisibility !== "always") setSubmenu(button, false);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
listen(nav, "keydown", (raw) => {
|
|
423
|
+
const event = raw;
|
|
424
|
+
const target = element(event.target);
|
|
425
|
+
if (!target || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
426
|
+
const rtl = view.getComputedStyle(nav).direction === "rtl";
|
|
427
|
+
const button = target.closest("[data-gqd-submenu]");
|
|
428
|
+
if (button && ["ArrowDown", rtl ? "ArrowLeft" : "ArrowRight"].includes(event.key)) {
|
|
429
|
+
event.preventDefault();
|
|
430
|
+
setSubmenu(button, true);
|
|
431
|
+
const list = lists.get(button);
|
|
432
|
+
if (list) focusables(list)[0]?.focus();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (event.key === "Escape" || event.key === (rtl ? "ArrowRight" : "ArrowLeft")) {
|
|
436
|
+
const list = target.closest("[data-gqd-submenu-list]");
|
|
437
|
+
const row = target.closest("li");
|
|
438
|
+
const owner = (event.key === "Escape" && row ? buttons.find((candidate) => candidate.closest("li") === row && candidate.getAttribute("aria-expanded") === "true") : null) ?? (button?.getAttribute("aria-expanded") === "true" ? button : list ? buttons.find((candidate) => lists.get(candidate) === list) : null);
|
|
439
|
+
if (owner && owner.getAttribute("aria-expanded") === "true" && nav.dataset.gqdSubmenuVisibility !== "always") {
|
|
440
|
+
event.preventDefault();
|
|
441
|
+
event.stopPropagation();
|
|
442
|
+
setSubmenu(owner, false);
|
|
443
|
+
owner.focus();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (event.key === "Escape" && expanded) {
|
|
447
|
+
event.preventDefault();
|
|
448
|
+
setOverlay(false, true);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (event.key === "Tab" && expanded) {
|
|
452
|
+
const candidates = focusables(panel);
|
|
453
|
+
const first = candidates[0], last = candidates.at(-1);
|
|
454
|
+
if (event.shiftKey && target === first) {
|
|
455
|
+
event.preventDefault();
|
|
456
|
+
last?.focus();
|
|
457
|
+
} else if (!event.shiftKey && target === last) {
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
first?.focus();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
listen(nav.ownerDocument, "pointerdown", (event) => {
|
|
464
|
+
const target = element(event.target);
|
|
465
|
+
if (target && !nav.contains(target) && nav.dataset.gqdSubmenuVisibility !== "always") buttons.forEach((button) => setSubmenu(button, false));
|
|
466
|
+
});
|
|
467
|
+
cleanups.push(() => {
|
|
468
|
+
listeners.forEach((remove) => remove());
|
|
469
|
+
unlock?.();
|
|
470
|
+
for (const [node, attributes] of saved) restore(node, [...attributes.keys()]);
|
|
471
|
+
mounted$1.delete(nav);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
let disposed = false;
|
|
475
|
+
return () => {
|
|
476
|
+
if (disposed) return;
|
|
477
|
+
disposed = true;
|
|
478
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region frontend/src/cover-video-client.ts
|
|
483
|
+
const mounted = /* @__PURE__ */ new WeakSet();
|
|
484
|
+
/** Mount decorative native Cover backgrounds in neutral, saved or delivered HTML.
|
|
485
|
+
* No DOM access occurs until invocation. The Site owns mounting and cleanup. */
|
|
486
|
+
function mountDesignCoverVideos(root) {
|
|
487
|
+
const scope = root ?? document;
|
|
488
|
+
const cleanups = Array.from(scope.querySelectorAll(".wp-block-cover > video.wp-block-cover__video-background")).map((video) => {
|
|
489
|
+
const view = video.ownerDocument.defaultView;
|
|
490
|
+
if (!view || mounted.has(video)) return () => {};
|
|
491
|
+
mounted.add(video);
|
|
492
|
+
const motion = view.matchMedia("(prefers-reduced-motion: reduce)");
|
|
493
|
+
let active = true;
|
|
494
|
+
video.autoplay = false;
|
|
495
|
+
video.removeAttribute("autoplay");
|
|
496
|
+
video.muted = true;
|
|
497
|
+
const update = () => {
|
|
498
|
+
if (!active || motion.matches) {
|
|
499
|
+
video.pause();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
video.play().then(() => {
|
|
503
|
+
if (active && motion.matches || !active && !mounted.has(video)) video.pause();
|
|
504
|
+
}, () => {});
|
|
505
|
+
};
|
|
506
|
+
motion.addEventListener("change", update);
|
|
507
|
+
update();
|
|
508
|
+
return () => {
|
|
509
|
+
if (!active) return;
|
|
510
|
+
active = false;
|
|
511
|
+
motion.removeEventListener("change", update);
|
|
512
|
+
mounted.delete(video);
|
|
513
|
+
video.pause();
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
519
|
+
export { mountDesignCarousels, mountDesignCoverVideos, mountDesignNavigation, mountVideoHero };
|