@caltho/site-kit 0.2.10 → 0.3.0
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 +146 -16
- package/dist/behaviors/accordion.d.ts +4 -0
- package/dist/behaviors/accordion.js +56 -0
- package/dist/behaviors/carousel.d.ts +4 -0
- package/dist/behaviors/carousel.js +127 -0
- package/dist/behaviors/filter.d.ts +1 -0
- package/dist/behaviors/filter.js +25 -0
- package/dist/behaviors/index.d.ts +14 -0
- package/dist/behaviors/index.js +78 -0
- package/dist/behaviors/motion.d.ts +13 -0
- package/dist/behaviors/motion.js +88 -0
- package/dist/behaviors/tabs.d.ts +3 -0
- package/dist/behaviors/tabs.js +55 -0
- package/dist/built/css.d.ts +2 -0
- package/dist/built/css.js +26 -0
- package/dist/built/theme.d.ts +19 -0
- package/dist/built/theme.js +86 -0
- package/dist/built/tree.d.ts +24 -0
- package/dist/built/tree.js +96 -0
- package/dist/built.test.d.ts +1 -0
- package/dist/built.test.js +257 -0
- package/dist/cli/check.js +31 -1
- package/dist/consent.d.ts +33 -0
- package/dist/consent.js +36 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +61 -0
- package/dist/content/index.d.ts +450 -33
- package/dist/content/index.js +297 -15
- package/dist/content/lists.d.ts +69 -0
- package/dist/content/lists.js +77 -0
- package/dist/content/lists.test.d.ts +1 -0
- package/dist/content/lists.test.js +257 -0
- package/dist/content/look.d.ts +260 -9
- package/dist/content/look.js +386 -22
- package/dist/content-migrate.d.ts +51 -0
- package/dist/content-migrate.js +69 -0
- package/dist/finish.d.ts +1 -1
- package/dist/finish.js +27 -6
- package/dist/fonts/families/dm-sans/LICENSE +93 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/LICENSE +93 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/LICENSE +93 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/LICENSE +93 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/LICENSE +93 -0
- package/dist/fonts/families/manrope/manrope-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/LICENSE +93 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/LICENSE +93 -0
- package/dist/fonts/families/outfit/outfit-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/outfit-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/LICENSE +93 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/LICENSE +93 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/LICENSE +93 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/google.json +1 -0
- package/dist/fonts-google.d.ts +35 -0
- package/dist/fonts-google.js +70 -0
- package/dist/fonts.css +365 -0
- package/dist/frame.test.d.ts +1 -0
- package/dist/frame.test.js +58 -0
- package/dist/migrate.d.ts +45 -0
- package/dist/migrate.js +284 -0
- package/dist/next/block-mark.d.ts +7 -0
- package/dist/next/block-mark.js +16 -0
- package/dist/next/blocks.d.ts +11 -0
- package/dist/next/blocks.js +35 -0
- package/dist/next/built.d.ts +40 -0
- package/dist/next/built.js +318 -0
- package/dist/next/consent.d.ts +4 -0
- package/dist/next/consent.js +13 -0
- package/dist/next/data.d.ts +6 -2
- package/dist/next/data.js +10 -4
- package/dist/next/frame.d.ts +42 -0
- package/dist/next/frame.js +73 -0
- package/dist/next/index.d.ts +8 -2
- package/dist/next/index.js +9 -3
- package/dist/next/menu-group.d.ts +11 -0
- package/dist/next/menu-group.js +33 -0
- package/dist/next/metadata.js +3 -2
- package/dist/next/parts/Img.d.ts +6 -1
- package/dist/next/parts/Img.js +11 -3
- package/dist/next/parts/LookStyle.js +3 -1
- package/dist/next/preview.js +511 -27
- package/dist/next/sections.d.ts +19 -10
- package/dist/next/sections.js +134 -17
- package/dist/policies.js +22 -4
- package/dist/prepare.d.ts +15 -1
- package/dist/prepare.js +125 -3
- package/dist/preview-match.d.ts +29 -0
- package/dist/preview-match.js +58 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +29 -1
- package/dist/sections/icons.d.ts +49 -0
- package/dist/sections/icons.js +55 -0
- package/dist/sections/index.d.ts +188 -12
- package/dist/sections/index.js +136 -10
- package/dist/sections/presets.d.ts +13 -0
- package/dist/sections/presets.js +300 -0
- package/dist/sections.css +1584 -41
- package/dist/sections.test.js +276 -14
- package/dist/seo.js +2 -1
- package/dist/tree/behaviors.d.ts +50 -0
- package/dist/tree/behaviors.js +67 -0
- package/dist/tree/classes.d.ts +24 -0
- package/dist/tree/classes.js +94 -0
- package/dist/tree/index.d.ts +145 -0
- package/dist/tree/index.js +196 -0
- package/dist/tree/messages.d.ts +45 -0
- package/dist/tree/messages.js +7 -0
- package/package.json +22 -3
package/dist/next/preview.js
CHANGED
|
@@ -8,16 +8,62 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
//
|
|
9
9
|
// CalOS → page: { type: "calos:show", site, images, path, selected?, scroll?, labels? }
|
|
10
10
|
// page → CalOS: { type: "calos:ready" }, { type: "calos:pick", path, block },
|
|
11
|
-
// { type: "calos:error", message }
|
|
11
|
+
// { type: "calos:error", message }, { type: "calos:overflow", blocks } (the page's
|
|
12
|
+
// blocks with something past the screen's edge at this width, by index)
|
|
13
|
+
// Built sections (../tree/messages.ts): calos:built-select, -hover and -boxes to CalOS;
|
|
14
|
+
// calos:built-highlight and -state from it.
|
|
15
|
+
import { allPages } from "../content/lists.js";
|
|
12
16
|
import { Component, useEffect, useRef, useState } from "react";
|
|
13
17
|
import { lookVars } from "../content/look.js";
|
|
14
|
-
import {
|
|
18
|
+
import { fontFiles, googleFont } from "../fonts-google.js";
|
|
19
|
+
import { BUILT_IMAGES, fieldForImage, fieldForText, isShown, matchBlocks, withoutVariant } from "../preview-match.js";
|
|
20
|
+
import { showWithin, startBehaviors } from "../behaviors/index.js";
|
|
21
|
+
import { swatchSlugs, tailwindInput } from "../built/theme.js";
|
|
22
|
+
import { hasBuilt } from "../built/tree.js";
|
|
23
|
+
import { NODE_ATTR } from "../tree/index.js";
|
|
24
|
+
import { BLOCK_MARK, InPreview } from "./block-mark.js";
|
|
15
25
|
import { setPrepared } from "./data.js";
|
|
26
|
+
/** `labels`: what CalOS calls each of the page's blocks, shown on its outline. */
|
|
27
|
+
/** The Google fonts this preview has loaded for a draft (by name), once each. */
|
|
28
|
+
const loadedFonts = new Set();
|
|
16
29
|
/** `look`: the same Look options the site's defineSite has, for the draft's Look to be drawn as it's chosen. */
|
|
17
30
|
export function SitePreview({ origins, render, look }) {
|
|
18
31
|
const [shown, setShown] = useState(null);
|
|
32
|
+
// A Google font the draft's Look uses and this build doesn't serve yet: loaded from Fontsource's
|
|
33
|
+
// CDN (the browser's FontFace, which only the font policy governs), so the draft shows in it.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const l = shown?.site.look;
|
|
36
|
+
for (const name of [l?.headingFont, l?.bodyFont]) {
|
|
37
|
+
if (typeof name !== "string" || (look?.fonts && name in look.fonts) || loadedFonts.has(name))
|
|
38
|
+
continue;
|
|
39
|
+
const f = googleFont(name);
|
|
40
|
+
if (!f)
|
|
41
|
+
continue;
|
|
42
|
+
loadedFonts.add(name);
|
|
43
|
+
for (const x of fontFiles(f)) {
|
|
44
|
+
const face = new FontFace(f.family, `url(${x.url}) format("woff2")`, { weight: x.weight, style: x.style, unicodeRange: x.range, display: "swap" });
|
|
45
|
+
document.fonts.add(face);
|
|
46
|
+
face.load().catch(() => document.fonts.delete(face));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Fonts the business uploaded: from CalOS, where the editor says each file is (with the pictures).
|
|
50
|
+
const uploaded = shown?.site.look?.uploadedFonts;
|
|
51
|
+
for (const f of uploaded ?? [])
|
|
52
|
+
for (const x of f.files) {
|
|
53
|
+
const url = shown?.images[x.src]?.src;
|
|
54
|
+
const key = `${f.family}|${x.src}`;
|
|
55
|
+
if (!url || loadedFonts.has(key) || !/^https?:\/\//.test(url))
|
|
56
|
+
continue;
|
|
57
|
+
loadedFonts.add(key);
|
|
58
|
+
const face = new FontFace(f.family, `url(${JSON.stringify(url)})`, { weight: x.weight, style: x.style, display: "swap" });
|
|
59
|
+
document.fonts.add(face);
|
|
60
|
+
face.load().catch(() => document.fonts.delete(face));
|
|
61
|
+
}
|
|
62
|
+
}, [shown, look]);
|
|
19
63
|
const [round, setRound] = useState(0);
|
|
20
64
|
const latest = useRef(null);
|
|
65
|
+
const lookRef = useRef(look);
|
|
66
|
+
lookRef.current = look;
|
|
21
67
|
// Answers go to the address CalOS is open at; until it's spoken, "ready" goes to each it may be.
|
|
22
68
|
const replyTo = useRef(null);
|
|
23
69
|
const post = (msg) => {
|
|
@@ -33,12 +79,22 @@ export function SitePreview({ origins, render, look }) {
|
|
|
33
79
|
const onMessage = (e) => {
|
|
34
80
|
if (!origins.includes(e.origin) || e.source !== window.parent)
|
|
35
81
|
return;
|
|
82
|
+
const said = e.data;
|
|
83
|
+
if (said?.type === "calos:built-highlight" && latest.current) {
|
|
84
|
+
highlight = { block: said.block, nodes: Array.isArray(said.nodes) ? said.nodes.filter((x) => typeof x === "string") : [] };
|
|
85
|
+
return drawHighlight(latest.current, true);
|
|
86
|
+
}
|
|
87
|
+
if (said?.type === "calos:built-state" && latest.current) {
|
|
88
|
+
forced = said.state === "normal" ? null : { block: said.block, node: said.node, state: said.state };
|
|
89
|
+
return applyState(latest.current);
|
|
90
|
+
}
|
|
36
91
|
const sent = e.data;
|
|
37
92
|
if (sent?.type !== "calos:show")
|
|
38
93
|
return;
|
|
39
94
|
const m = { ...sent, images: asBuilt(sent.images) };
|
|
40
95
|
replyTo.current = e.origin;
|
|
41
|
-
|
|
96
|
+
// The draft's Look, its colours read by KitBlocks to know a dark background as it's chosen.
|
|
97
|
+
setPrepared({ site: m.site, images: m.images, csp: {}, ...(lookRef.current ? { look: lookVars(m.site.look, lookRef.current) } : {}) });
|
|
42
98
|
latest.current = m;
|
|
43
99
|
setShown(m);
|
|
44
100
|
setRound((r) => r + 1);
|
|
@@ -66,11 +122,29 @@ export function SitePreview({ origins, render, look }) {
|
|
|
66
122
|
const target = e.target;
|
|
67
123
|
if (!m || !target)
|
|
68
124
|
return;
|
|
125
|
+
// A built section's own controls (a carousel's dots and arrows, which aren't nodes) work as on
|
|
126
|
+
// the site, so a slide can be brought up to edit.
|
|
127
|
+
if (target.closest(".kit-carousel-controls button, button[data-kit-to]"))
|
|
128
|
+
return;
|
|
69
129
|
e.preventDefault();
|
|
70
130
|
e.stopPropagation();
|
|
131
|
+
// Only what a person sees is what they clicked: the top element under the pointer that's shown,
|
|
132
|
+
// not a slide faded out over it (a carousel's slides are stacked in one place, the hidden ones
|
|
133
|
+
// still under the pointer), nor the words of one.
|
|
134
|
+
const stack = document.elementsFromPoint(e.clientX, e.clientY);
|
|
135
|
+
const shownEl = stack.find(seen) ?? target;
|
|
136
|
+
// A built section's element names its node: exact, nothing guessed.
|
|
137
|
+
const node = shownEl.closest(`[${NODE_ATTR}]`);
|
|
138
|
+
if (node) {
|
|
139
|
+
const n = blockElements(m)?.findIndex((d) => holds(d, node)) ?? -1;
|
|
140
|
+
// Or in the site's own header or footer (built): -1 or -2, as the editor numbers them.
|
|
141
|
+
const at = n >= 0 ? n : frameOf(node);
|
|
142
|
+
if (at !== null)
|
|
143
|
+
return post({ type: "calos:built-select", block: at, node: node.getAttribute(NODE_ATTR), shift: e.shiftKey });
|
|
144
|
+
}
|
|
71
145
|
// A link anywhere (a section's, the menu's, the footer's) is edited, never followed: CalOS's page picker moves between pages.
|
|
72
146
|
// A picture under the pointer counts, even when something over it (a frame, a caption's box) took the click.
|
|
73
|
-
const under =
|
|
147
|
+
const under = stack.find((el) => el.tagName === "IMG" && seen(el)) ?? shownEl;
|
|
74
148
|
post({ type: "calos:pick", ...pick(under, m, wordsAt(e.clientX, e.clientY)) });
|
|
75
149
|
};
|
|
76
150
|
const onSubmit = (e) => e.preventDefault();
|
|
@@ -79,13 +153,30 @@ export function SitePreview({ origins, render, look }) {
|
|
|
79
153
|
const m = latest.current;
|
|
80
154
|
const target = e.target;
|
|
81
155
|
const blocks = m ? blockElements(m) : null;
|
|
82
|
-
const n = blocks && target ? blocks.findIndex((
|
|
156
|
+
const n = blocks && target ? blocks.findIndex((d) => holds(d, target)) : -1;
|
|
83
157
|
const selected = m ? blockElement(m) : null;
|
|
84
158
|
hovered = n >= 0 ? blocks[n] : null;
|
|
85
|
-
outline("section", hovered && hovered !== selected ? hovered : null, n >= 0 ? m?.labels?.[n] : undefined);
|
|
86
|
-
|
|
159
|
+
outline("section", hovered && hovered[0] !== selected?.[0] ? hovered : null, n >= 0 ? m?.labels?.[n] : undefined);
|
|
160
|
+
// Over a built section, the element itself (said to CalOS, for its layers), not the words in it.
|
|
161
|
+
const shownEl = document.elementsFromPoint(e.clientX, e.clientY).find(seen) ?? target;
|
|
162
|
+
// In the site's own header or footer (built): -1 or -2, as the editor knows them.
|
|
163
|
+
const framed = n < 0 && shownEl ? shownEl.closest(`[${NODE_ATTR}]`) : null;
|
|
164
|
+
const f = framed ? frameOf(framed) : null;
|
|
165
|
+
const node = n >= 0 ? (shownEl?.closest(`[${NODE_ATTR}]`) ?? null) : f !== null ? framed : null;
|
|
166
|
+
const id = node && (f !== null || holds(blocks[n], node)) ? node.getAttribute(NODE_ATTR) : null;
|
|
167
|
+
const at = f ?? n;
|
|
168
|
+
const said = id ? `${at}:${id}` : null;
|
|
169
|
+
if (said !== hoverSaid) {
|
|
170
|
+
if (id || hoverSaid)
|
|
171
|
+
post({ type: "calos:built-hover", block: id ? at : Number(hoverSaid.split(":")[0]), node: id });
|
|
172
|
+
hoverSaid = said;
|
|
173
|
+
}
|
|
174
|
+
outline("hover", id ? node : hoverable(target));
|
|
87
175
|
};
|
|
88
176
|
const leave = () => {
|
|
177
|
+
if (hoverSaid)
|
|
178
|
+
post({ type: "calos:built-hover", block: Number(hoverSaid.split(":")[0]), node: null });
|
|
179
|
+
hoverSaid = null;
|
|
89
180
|
hovered = null;
|
|
90
181
|
outline("section", null);
|
|
91
182
|
outline("hover", null);
|
|
@@ -103,6 +194,56 @@ export function SitePreview({ origins, render, look }) {
|
|
|
103
194
|
};
|
|
104
195
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
196
|
}, [key]);
|
|
197
|
+
// The footer's own colour, as designed (the site's CSS may set it, not the Look): said to CalOS so
|
|
198
|
+
// its "as designed" choice shows the colour it is. Only while the draft leaves it as designed.
|
|
199
|
+
const footerSaid = useRef(null);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!shown)
|
|
202
|
+
return;
|
|
203
|
+
const bg = shown.site.look?.footer?.background;
|
|
204
|
+
if (bg && bg !== "dark")
|
|
205
|
+
return;
|
|
206
|
+
const frame = requestAnimationFrame(() => {
|
|
207
|
+
const footer = document.querySelector("footer.kit-footer");
|
|
208
|
+
const color = footer ? getComputedStyle(footer).backgroundColor : "";
|
|
209
|
+
if (color && color !== footerSaid.current)
|
|
210
|
+
post({ type: "calos:footer", color });
|
|
211
|
+
footerSaid.current = color;
|
|
212
|
+
});
|
|
213
|
+
return () => cancelAnimationFrame(frame);
|
|
214
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
215
|
+
}, [shown]);
|
|
216
|
+
// What runs past the edge of the screen, after each drawing and as the width changes: said to
|
|
217
|
+
// CalOS by block, so the editor can point at the section to fix at this size.
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!shown)
|
|
220
|
+
return;
|
|
221
|
+
// Nothing said yet for this drawing: the first measure is always said, so a section fixed since
|
|
222
|
+
// (nothing past the edge any more) is cleared, not left flagged.
|
|
223
|
+
let last = null;
|
|
224
|
+
const measure = () => {
|
|
225
|
+
const { blocks, what } = overflowing(latest.current ?? shown);
|
|
226
|
+
const said = blocks.join(",");
|
|
227
|
+
if (said !== last)
|
|
228
|
+
post({ type: "calos:overflow", blocks, what });
|
|
229
|
+
last = said;
|
|
230
|
+
};
|
|
231
|
+
let frame = 0;
|
|
232
|
+
const soon = () => {
|
|
233
|
+
cancelAnimationFrame(frame);
|
|
234
|
+
frame = requestAnimationFrame(() => requestAnimationFrame(measure));
|
|
235
|
+
};
|
|
236
|
+
soon();
|
|
237
|
+
void document.fonts?.ready.then(soon);
|
|
238
|
+
window.addEventListener("resize", soon);
|
|
239
|
+
window.addEventListener("load", soon, true);
|
|
240
|
+
return () => {
|
|
241
|
+
cancelAnimationFrame(frame);
|
|
242
|
+
window.removeEventListener("resize", soon);
|
|
243
|
+
window.removeEventListener("load", soon, true);
|
|
244
|
+
};
|
|
245
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
246
|
+
}, [shown, round]);
|
|
106
247
|
// The selected block outlined (and brought into view when CalOS asks), after each drawing.
|
|
107
248
|
useEffect(() => {
|
|
108
249
|
if (!shown)
|
|
@@ -120,32 +261,118 @@ export function SitePreview({ origins, render, look }) {
|
|
|
120
261
|
drawnPath = shown.path;
|
|
121
262
|
// Scrolled here only: scrollIntoView would scroll CalOS's page around the frame too.
|
|
122
263
|
if (el && shown.scroll)
|
|
123
|
-
window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
264
|
+
window.scrollTo({ top: el[0].getBoundingClientRect().top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
265
|
+
// On a resize, once a frame at most: measured on every event, the page was re-laid out mid-change.
|
|
266
|
+
let frame = 0;
|
|
124
267
|
const keep = () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
268
|
+
if (frame)
|
|
269
|
+
return;
|
|
270
|
+
frame = requestAnimationFrame(() => {
|
|
271
|
+
frame = 0;
|
|
272
|
+
outline("selected", blockElement(latest.current), label(latest.current));
|
|
273
|
+
outline("section", null);
|
|
274
|
+
outline("hover", null);
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
// The outlines are in the page's coordinates, so scrolling moves them with it; the hover ones go.
|
|
278
|
+
let hidden = false;
|
|
279
|
+
const scrolled = () => {
|
|
280
|
+
if (hidden)
|
|
281
|
+
return;
|
|
282
|
+
hidden = true;
|
|
283
|
+
requestAnimationFrame(() => {
|
|
284
|
+
hidden = false;
|
|
285
|
+
outline("section", null);
|
|
286
|
+
outline("hover", null);
|
|
287
|
+
});
|
|
128
288
|
};
|
|
129
|
-
window.addEventListener("scroll",
|
|
289
|
+
window.addEventListener("scroll", scrolled, { passive: true });
|
|
130
290
|
window.addEventListener("resize", keep);
|
|
131
291
|
return () => {
|
|
132
|
-
|
|
292
|
+
cancelAnimationFrame(frame);
|
|
293
|
+
window.removeEventListener("scroll", scrolled);
|
|
133
294
|
window.removeEventListener("resize", keep);
|
|
134
295
|
};
|
|
135
296
|
}, [shown, round]);
|
|
297
|
+
// Built sections: their classes drawn as they're typed (Tailwind's browser build, given the same
|
|
298
|
+
// input as the build's compiler), their behaviors running (nothing moving by itself, so what's
|
|
299
|
+
// being edited stays put), where each element is said to CalOS after every drawing (and as the
|
|
300
|
+
// page scrolls, resizes or the classes' CSS changes), and the editor's selection and state kept.
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (!shown)
|
|
303
|
+
return;
|
|
304
|
+
const page = allPages(shown.site).find((p) => p.path === shown.path) ?? shown.site.pages[0];
|
|
305
|
+
if (!page || !hasBuilt(page.blocks))
|
|
306
|
+
return;
|
|
307
|
+
loadTailwind(tailwindInput(swatchSlugs(look ? lookVars(shown.site.look, look) : undefined)));
|
|
308
|
+
if (!behaving) {
|
|
309
|
+
behaving = true;
|
|
310
|
+
startBehaviors(true);
|
|
311
|
+
}
|
|
312
|
+
let frame = 0;
|
|
313
|
+
const soon = () => {
|
|
314
|
+
cancelAnimationFrame(frame);
|
|
315
|
+
frame = requestAnimationFrame(() => {
|
|
316
|
+
const m = latest.current ?? shown;
|
|
317
|
+
applyState(m);
|
|
318
|
+
drawHighlight(m, false);
|
|
319
|
+
for (const [block, boxes] of builtBoxes(m))
|
|
320
|
+
post({ type: "calos:built-boxes", block, boxes });
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
soon();
|
|
324
|
+
// The browser build writes the classes' CSS a moment after they're drawn: the boxes are said again then.
|
|
325
|
+
const css = new MutationObserver(soon);
|
|
326
|
+
css.observe(document.head, { childList: true, subtree: true, characterData: true });
|
|
327
|
+
void document.fonts?.ready.then(soon);
|
|
328
|
+
window.addEventListener("resize", soon);
|
|
329
|
+
window.addEventListener("scroll", soon, { passive: true });
|
|
330
|
+
return () => {
|
|
331
|
+
cancelAnimationFrame(frame);
|
|
332
|
+
css.disconnect();
|
|
333
|
+
window.removeEventListener("resize", soon);
|
|
334
|
+
window.removeEventListener("scroll", soon);
|
|
335
|
+
};
|
|
336
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
337
|
+
}, [shown, round, look]);
|
|
136
338
|
// The draft's Look, set on the page (LookStyle drew the published one into the build).
|
|
339
|
+
// One the draft no longer sets (a colour removed, the footer back as designed) is taken off again.
|
|
340
|
+
const setVars = useRef(new Set());
|
|
137
341
|
useEffect(() => {
|
|
138
342
|
if (!shown || !look)
|
|
139
343
|
return;
|
|
140
|
-
|
|
141
|
-
|
|
344
|
+
const vars = lookVars(shown.site.look, look);
|
|
345
|
+
const style = document.documentElement.style;
|
|
346
|
+
// The published Look's values (LookStyle's :root rule) the draft no longer has (a colour it
|
|
347
|
+
// removed): made invalid here, so what reads them falls back as the site will once published.
|
|
348
|
+
const published = new Set();
|
|
349
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
350
|
+
let rules;
|
|
351
|
+
try {
|
|
352
|
+
rules = sheet.cssRules;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
for (const r of Array.from(rules))
|
|
358
|
+
if (r instanceof CSSStyleRule && r.selectorText === ":root")
|
|
359
|
+
for (const name of Array.from(r.style))
|
|
360
|
+
if (name.startsWith("--look-") || name.startsWith("--kit-footer-"))
|
|
361
|
+
published.add(name);
|
|
362
|
+
}
|
|
363
|
+
for (const k of new Set([...setVars.current, ...published]))
|
|
364
|
+
if (!(k in vars))
|
|
365
|
+
style.setProperty(k, "initial");
|
|
366
|
+
for (const [k, v] of Object.entries(vars))
|
|
367
|
+
style.setProperty(k, v);
|
|
368
|
+
setVars.current = new Set(Object.keys(vars));
|
|
142
369
|
}, [shown, look]);
|
|
143
370
|
if (!shown)
|
|
144
371
|
return null;
|
|
145
|
-
const page = shown.site.
|
|
372
|
+
const page = allPages(shown.site).find((p) => p.path === shown.path) ?? shown.site.pages[0];
|
|
146
373
|
if (!page)
|
|
147
374
|
return null;
|
|
148
|
-
return (_jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round));
|
|
375
|
+
return (_jsx(InPreview.Provider, { value: true, children: _jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round) }));
|
|
149
376
|
}
|
|
150
377
|
/** The pictures this build made (finish writes the list into this page), by path. */
|
|
151
378
|
let built = null;
|
|
@@ -174,35 +401,113 @@ function asBuilt(images) {
|
|
|
174
401
|
const pageIndex = (m) => m.site.pages.findIndex((p) => p.path === m.path);
|
|
175
402
|
/** What's in `main` without being drawn: a page's structured data (JSON-LD) and the like. */
|
|
176
403
|
const UNDRAWN = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "LINK", "META"]);
|
|
177
|
-
/**
|
|
404
|
+
/**
|
|
405
|
+
* What each of the page's blocks drew, one list for each block (a block may draw nothing, or more than
|
|
406
|
+
* one). Exactly, where the site marks where each block starts (BlockMark); otherwise `main`'s
|
|
407
|
+
* children, found by their words when they aren't one for each block.
|
|
408
|
+
*/
|
|
178
409
|
function blockElements(m) {
|
|
179
410
|
const main = document.querySelector("main");
|
|
180
411
|
const page = m.site.pages[pageIndex(m)];
|
|
181
412
|
if (!main || !page)
|
|
182
413
|
return null;
|
|
414
|
+
const marked = marks(main, page.blocks.length);
|
|
415
|
+
if (marked)
|
|
416
|
+
return marked;
|
|
183
417
|
const kids = [...main.children].filter((e) => !UNDRAWN.has(e.tagName));
|
|
184
|
-
|
|
418
|
+
const words = (el) => [el.innerText ?? el.textContent ?? "", ...[...el.querySelectorAll("img[alt]")].map((i) => i.getAttribute("alt") ?? "")].join(" ");
|
|
419
|
+
const owner = matchBlocks(page.blocks, kids.length === page.blocks.length ? kids.map(() => "") : kids.map(words));
|
|
420
|
+
return page.blocks.map((_, i) => kids.filter((_, j) => owner[j] === i));
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The page's blocks with something drawn past the left or right edge of the screen (words running
|
|
424
|
+
* out of a card, a row too wide for a phone), by index. Something inside a box that clips what it
|
|
425
|
+
* holds on purpose (a slideshow's other slides, a scrolling row) doesn't count.
|
|
426
|
+
*/
|
|
427
|
+
function overflowing(m) {
|
|
428
|
+
const blocks = blockElements(m);
|
|
429
|
+
if (!blocks)
|
|
430
|
+
return { blocks: [], what: [] };
|
|
431
|
+
const edge = document.documentElement.clientWidth;
|
|
432
|
+
const clips = (el, root) => {
|
|
433
|
+
for (let at = el.parentElement; at && at !== root.parentElement; at = at.parentElement) {
|
|
434
|
+
const c = getComputedStyle(at);
|
|
435
|
+
if (c.overflowX !== "visible" || c.overflow !== "visible")
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
};
|
|
440
|
+
// What it was, said with the block (the element and its words), for whoever's looking into it.
|
|
441
|
+
const what = [];
|
|
442
|
+
const out = (root) => [root, ...root.querySelectorAll("*")].some((el) => {
|
|
443
|
+
const r = el.getBoundingClientRect();
|
|
444
|
+
if (!r.width || !r.height || getComputedStyle(el).visibility === "hidden")
|
|
445
|
+
return false;
|
|
446
|
+
// Across the edge, not wholly off the screen (a field kept out of sight on purpose).
|
|
447
|
+
const over = ((r.right > edge + 1 && r.left < edge) || (r.left < -1 && r.right > 0)) && !clips(el, root);
|
|
448
|
+
if (over)
|
|
449
|
+
what.push(`${el.tagName.toLowerCase()}${typeof el.className === "string" && el.className ? `.${el.className.trim().split(/\s+/).join(".")}` : ""} ${Math.round(r.left)}–${Math.round(r.right)} of ${edge}`);
|
|
450
|
+
return over;
|
|
451
|
+
});
|
|
452
|
+
return { blocks: blocks.flatMap((els, i) => (els.some(out) ? [i] : [])), what };
|
|
185
453
|
}
|
|
454
|
+
/** Each block's elements by the marks before them: what's drawn after a block's mark, up to the next. Null where there are none. */
|
|
455
|
+
function marks(main, count) {
|
|
456
|
+
const first = main.querySelector(`template[${BLOCK_MARK}]`);
|
|
457
|
+
if (!first?.parentElement)
|
|
458
|
+
return null;
|
|
459
|
+
const out = Array.from({ length: count }, () => []);
|
|
460
|
+
let at = -1;
|
|
461
|
+
for (const el of first.parentElement.children) {
|
|
462
|
+
const mark = el.tagName === "TEMPLATE" ? el.getAttribute(BLOCK_MARK) : null;
|
|
463
|
+
if (mark !== null)
|
|
464
|
+
at = Number(mark);
|
|
465
|
+
else if (at >= 0 && at < count && !UNDRAWN.has(el.tagName))
|
|
466
|
+
out[at].push(el);
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
const holds = (d, target) => !!d?.some((el) => el.contains(target));
|
|
186
471
|
function blockElement(m) {
|
|
187
472
|
if (!m?.selected)
|
|
188
473
|
return null;
|
|
189
474
|
const [k, p, b, n] = m.selected;
|
|
190
475
|
if (k !== "pages" || p !== pageIndex(m) || b !== "blocks" || typeof n !== "number")
|
|
191
476
|
return null;
|
|
192
|
-
|
|
477
|
+
const d = blockElements(m)?.[n];
|
|
478
|
+
return d?.length ? d : null;
|
|
193
479
|
}
|
|
194
480
|
/** The words of the text under the pointer: a name set beside other things in one element (a caption with an email and a phone) is still just the name. */
|
|
195
481
|
function wordsAt(x, y) {
|
|
196
482
|
const doc = document;
|
|
197
483
|
const node = doc.caretPositionFromPoint?.(x, y)?.offsetNode ?? doc.caretRangeFromPoint?.(x, y)?.startContainer;
|
|
198
|
-
|
|
484
|
+
// The caret snaps to the nearest text, even across empty space: only words actually under the pointer count.
|
|
485
|
+
const under = (t) => {
|
|
486
|
+
const range = document.createRange();
|
|
487
|
+
range.selectNodeContents(t);
|
|
488
|
+
return [...range.getClientRects()].some((r) => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom);
|
|
489
|
+
};
|
|
490
|
+
const words = (t) => (t.textContent?.trim() ?? null) || null;
|
|
491
|
+
// Words a person sees: the caret finds text in whatever's on top, a faded-out slide stacked over
|
|
492
|
+
// the one showing as much as the one showing.
|
|
493
|
+
if (node?.nodeType === Node.TEXT_NODE && node.parentElement && seen(node.parentElement))
|
|
494
|
+
return under(node) ? words(node) : null;
|
|
495
|
+
// Then the words of the shown elements under the pointer, the top one first.
|
|
496
|
+
for (const el of document.elementsFromPoint(x, y)) {
|
|
497
|
+
if (!seen(el))
|
|
498
|
+
continue;
|
|
499
|
+
const t = [...el.childNodes].find((c) => c.nodeType === Node.TEXT_NODE && c.textContent?.trim() && under(c));
|
|
500
|
+
if (t)
|
|
501
|
+
return words(t);
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
199
504
|
}
|
|
200
505
|
/** What a click on `target` meant: the field, and the block it's in. `words`: the text right under the pointer, tried first. */
|
|
201
506
|
function pick(target, m, words = null) {
|
|
202
507
|
const pi = pageIndex(m);
|
|
203
508
|
const page = m.site.pages[pi];
|
|
204
509
|
const blocks = blockElements(m);
|
|
205
|
-
const n = blocks ? blocks.findIndex((
|
|
510
|
+
const n = blocks ? blocks.findIndex((d) => holds(d, target)) : -1;
|
|
206
511
|
const block = n >= 0 ? ["pages", pi, "blocks", n] : null;
|
|
207
512
|
const scope = n >= 0 ? page.blocks[n] : null;
|
|
208
513
|
// What the whole site shares (its founders, its case studies), which a section may show too; the
|
|
@@ -216,6 +521,16 @@ function pick(target, m, words = null) {
|
|
|
216
521
|
if (found)
|
|
217
522
|
return { path: found, block: blockOf(found) ?? block };
|
|
218
523
|
}
|
|
524
|
+
// An icon part has no words to find it by: its name, and which of the section's icons of that name it is.
|
|
525
|
+
const icon = target.closest("svg[data-icon]");
|
|
526
|
+
if (icon && scope && n >= 0) {
|
|
527
|
+
const name = icon.getAttribute("data-icon");
|
|
528
|
+
const found = iconParts(scope, block).filter((x) => x.name === name);
|
|
529
|
+
const drawn = blocks[n].flatMap((el) => [...(el.matches("svg[data-icon]") ? [el] : []), ...el.querySelectorAll("svg[data-icon]")]).filter((el) => el.getAttribute("data-icon") === name);
|
|
530
|
+
const at = found[Math.max(0, drawn.indexOf(icon))] ?? found[0];
|
|
531
|
+
if (at)
|
|
532
|
+
return { path: at.path, block };
|
|
533
|
+
}
|
|
219
534
|
if (words) {
|
|
220
535
|
// Outside a section, a menu made from the pages' menu labels is any page's label.
|
|
221
536
|
const labels = { pages: m.site.pages.map((p) => ({ menu: p.menu })) };
|
|
@@ -223,6 +538,16 @@ function pick(target, m, words = null) {
|
|
|
223
538
|
if (exact)
|
|
224
539
|
return { path: exact, block: blockOf(exact) ?? block };
|
|
225
540
|
}
|
|
541
|
+
// The header (outside every section) away from its words and pictures, its own empty space: its
|
|
542
|
+
// settings in the Look, not whatever words its parents hold (the business's name).
|
|
543
|
+
// The phone's menu button: its own settings (what it shows, its frame), with the header's.
|
|
544
|
+
if (n < 0 && target.closest(".kit-menu-narrow > summary"))
|
|
545
|
+
return { path: ["look", "header", "menuShows"], block: null };
|
|
546
|
+
if (n < 0 && target.closest("header") && !words && !target.closest("a, button, summary, img"))
|
|
547
|
+
return { path: ["look", "header"], block: null };
|
|
548
|
+
// The kit's footer the same: its own settings (its colour), where the site's Look has them.
|
|
549
|
+
if (n < 0 && target.closest("footer.kit-footer") && !words && !target.closest("a, button, summary, img") && m.site.look?.footer)
|
|
550
|
+
return { path: ["look", "footer"], block: null };
|
|
226
551
|
// The clicked element's words, then its parents' until the block's edge: at each, the section's
|
|
227
552
|
// own fields first, then the site's shared ones, so a parent holding more of the section isn't
|
|
228
553
|
// mistaken for what was clicked.
|
|
@@ -236,12 +561,42 @@ function pick(target, m, words = null) {
|
|
|
236
561
|
if (found)
|
|
237
562
|
return { path: found, block: blockOf(found) ?? block };
|
|
238
563
|
}
|
|
239
|
-
if (n >= 0 &&
|
|
564
|
+
if (n >= 0 && blocks[n].includes(el))
|
|
240
565
|
break;
|
|
241
566
|
}
|
|
242
567
|
return { path: block, block };
|
|
243
568
|
}
|
|
569
|
+
/** A section's icon parts, in the order they're drawn: each one's path and name. */
|
|
570
|
+
function iconParts(value, base) {
|
|
571
|
+
const out = [];
|
|
572
|
+
const visit = (v, path) => {
|
|
573
|
+
if (Array.isArray(v))
|
|
574
|
+
return v.forEach((x, i) => visit(x, [...path, i]));
|
|
575
|
+
if (!v || typeof v !== "object")
|
|
576
|
+
return;
|
|
577
|
+
const o = v;
|
|
578
|
+
if (o.type === "icon" && typeof o.name === "string")
|
|
579
|
+
out.push({ path, name: o.name });
|
|
580
|
+
for (const [k, x] of Object.entries(o))
|
|
581
|
+
if (x && typeof x === "object")
|
|
582
|
+
visit(x, [...path, k]);
|
|
583
|
+
};
|
|
584
|
+
visit(value, base);
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
244
587
|
const blockOf = (path) => (path[0] === "pages" && path[2] === "blocks" && typeof path[3] === "number" ? path.slice(0, 4) : null);
|
|
588
|
+
/**
|
|
589
|
+
* Whether a picture is one a person sees: not in something hidden from them (a slideshow's other
|
|
590
|
+
* slides, stacked in the same place) and not faded out to nothing.
|
|
591
|
+
*/
|
|
592
|
+
function seen(el) {
|
|
593
|
+
const chain = [];
|
|
594
|
+
for (let at = el; at && at !== document.body; at = at.parentElement) {
|
|
595
|
+
const c = getComputedStyle(at);
|
|
596
|
+
chain.push({ ariaHidden: at.getAttribute("aria-hidden"), inert: at.hasAttribute("inert"), visibility: c.visibility, display: c.display, opacity: c.opacity });
|
|
597
|
+
}
|
|
598
|
+
return isShown(chain);
|
|
599
|
+
}
|
|
245
600
|
/** The smallest thing under the pointer that's words or a picture. */
|
|
246
601
|
function hoverable(el) {
|
|
247
602
|
for (let at = el; at && at !== document.body; at = at.parentElement) {
|
|
@@ -252,6 +607,130 @@ function hoverable(el) {
|
|
|
252
607
|
}
|
|
253
608
|
return null;
|
|
254
609
|
}
|
|
610
|
+
// ---- built sections ----
|
|
611
|
+
/** What the editor has selected in a built section (calos:built-highlight), outlined after every drawing. */
|
|
612
|
+
let highlight = null;
|
|
613
|
+
/** A node drawn as hovered or focused while those classes are edited (calos:built-state). */
|
|
614
|
+
let forced = null;
|
|
615
|
+
/** The built node last said to be under the pointer ("block:node"). */
|
|
616
|
+
let hoverSaid = null;
|
|
617
|
+
let behaving = false;
|
|
618
|
+
let tailwind = null;
|
|
619
|
+
/**
|
|
620
|
+
* Tailwind's browser build, loaded once (a chunk of its own: only this page carries it, never a
|
|
621
|
+
* visitor's), compiling the classes on the page as they change. Its input is set first, so it
|
|
622
|
+
* never compiles its default (which resets the page's own styles): the theme and utilities only,
|
|
623
|
+
* prefixed as built sections draw them, with the draft's Look as the theme.
|
|
624
|
+
*/
|
|
625
|
+
function loadTailwind(input) {
|
|
626
|
+
let style = document.getElementById("kit-tailwind");
|
|
627
|
+
if (!style) {
|
|
628
|
+
style = document.createElement("style");
|
|
629
|
+
style.id = "kit-tailwind";
|
|
630
|
+
style.setAttribute("type", "text/tailwindcss");
|
|
631
|
+
document.head.appendChild(style);
|
|
632
|
+
}
|
|
633
|
+
if (style.textContent !== input)
|
|
634
|
+
style.textContent = input;
|
|
635
|
+
tailwind ??= import("@tailwindcss/browser").catch((err) => {
|
|
636
|
+
tailwind = null;
|
|
637
|
+
console.error("site-kit: Tailwind's browser build didn't load", err);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/** The site's own header (-1) or footer (-2), as the editor numbers them: the one a built element is in, or null. */
|
|
641
|
+
const FRAMES = { "-1": "header", "-2": "footer" };
|
|
642
|
+
function frameOf(el) {
|
|
643
|
+
const f = el.closest("[data-kit-frame]")?.getAttribute("data-kit-frame");
|
|
644
|
+
return f === "header" ? -1 : f === "footer" ? -2 : null;
|
|
645
|
+
}
|
|
646
|
+
function frameEls(block) {
|
|
647
|
+
const which = FRAMES[String(block)];
|
|
648
|
+
const el = which ? document.querySelector(`[data-kit-frame="${which}"]`) : null;
|
|
649
|
+
return el ? [el] : [];
|
|
650
|
+
}
|
|
651
|
+
/** The elements drawn for a node of a block (a repeated node has one for each item). */
|
|
652
|
+
function nodeEls(m, block, node) {
|
|
653
|
+
const els = block < 0 ? frameEls(block) : (blockElements(m)?.[block] ?? []);
|
|
654
|
+
const sel = `[${NODE_ATTR}="${CSS.escape(node)}"]`;
|
|
655
|
+
return els.flatMap((el) => [...(el.matches(sel) ? [el] : []), ...el.querySelectorAll(sel)]);
|
|
656
|
+
}
|
|
657
|
+
/** Where each built element is on screen, by block, for CalOS's overlays: only what's shown (not a slide faded out). */
|
|
658
|
+
function builtBoxes(m) {
|
|
659
|
+
const page = m.site.pages[pageIndex(m)];
|
|
660
|
+
const blocks = blockElements(m);
|
|
661
|
+
if (!page || !blocks)
|
|
662
|
+
return [];
|
|
663
|
+
const out = [];
|
|
664
|
+
page.blocks.forEach((b, i) => {
|
|
665
|
+
if (b.type !== "built" && b.type !== "use")
|
|
666
|
+
return;
|
|
667
|
+
const boxes = (blocks[i] ?? [])
|
|
668
|
+
.flatMap((el) => [...(el.hasAttribute(NODE_ATTR) ? [el] : []), ...el.querySelectorAll(`[${NODE_ATTR}]`)])
|
|
669
|
+
.filter(seen)
|
|
670
|
+
.map((el) => {
|
|
671
|
+
const r = el.getBoundingClientRect();
|
|
672
|
+
return { node: el.getAttribute(NODE_ATTR), x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
|
|
673
|
+
});
|
|
674
|
+
out.push([i, boxes]);
|
|
675
|
+
});
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
/** The selected nodes outlined; brought into view when CalOS has just selected them, and shown where a behavior hides them (another slide, tab or closed item). */
|
|
679
|
+
const highlightBoxes = [];
|
|
680
|
+
function drawHighlight(m, scroll) {
|
|
681
|
+
const els = highlight ? highlight.nodes.flatMap((id) => nodeEls(m, highlight.block, id).slice(0, 1)) : [];
|
|
682
|
+
if (scroll)
|
|
683
|
+
els.forEach((el) => showWithin(el));
|
|
684
|
+
while (highlightBoxes.length < els.length) {
|
|
685
|
+
const box = document.createElement("div");
|
|
686
|
+
Object.assign(box.style, { position: "absolute", pointerEvents: "none", zIndex: "2147483647", ...looks.selected });
|
|
687
|
+
document.body.appendChild(box);
|
|
688
|
+
highlightBoxes.push(box);
|
|
689
|
+
}
|
|
690
|
+
highlightBoxes.forEach((box, i) => {
|
|
691
|
+
const el = els[i];
|
|
692
|
+
if (!el)
|
|
693
|
+
return void (box.style.display = "none");
|
|
694
|
+
const r = el.getBoundingClientRect();
|
|
695
|
+
Object.assign(box.style, { display: "block", top: `${r.top + window.scrollY}px`, left: `${r.left + window.scrollX}px`, width: `${r.width}px`, height: `${r.height}px` });
|
|
696
|
+
});
|
|
697
|
+
const first = els[0];
|
|
698
|
+
if (scroll && first) {
|
|
699
|
+
const r = first.getBoundingClientRect();
|
|
700
|
+
if (r.top < 0 || r.bottom > window.innerHeight)
|
|
701
|
+
window.scrollTo({ top: r.top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* A node drawn as hovered or focused: each of its classes for that state (hover:, focus:; and
|
|
706
|
+
* group-hover: and the like on what's inside it when it's a group) added without the state, so it
|
|
707
|
+
* shows while its hover classes are edited. Whatever was added before is taken off first.
|
|
708
|
+
*/
|
|
709
|
+
const STATE_VARIANTS = { hover: ["hover"], focus: ["focus", "focus-visible", "focus-within"] };
|
|
710
|
+
const GROUP_VARIANTS = { hover: ["group-hover"], focus: ["group-focus", "group-focus-within"] };
|
|
711
|
+
let forcedClasses = [];
|
|
712
|
+
function applyState(m) {
|
|
713
|
+
for (const f of forcedClasses)
|
|
714
|
+
f.el.classList.remove(f.cls);
|
|
715
|
+
forcedClasses = [];
|
|
716
|
+
if (!forced)
|
|
717
|
+
return;
|
|
718
|
+
const force = (el, variants) => {
|
|
719
|
+
for (const c of [...el.classList]) {
|
|
720
|
+
const plain = withoutVariant(c, variants);
|
|
721
|
+
if (plain && !el.classList.contains(plain)) {
|
|
722
|
+
el.classList.add(plain);
|
|
723
|
+
forcedClasses.push({ el, cls: plain });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
for (const el of nodeEls(m, forced.block, forced.node)) {
|
|
728
|
+
force(el, STATE_VARIANTS[forced.state]);
|
|
729
|
+
if ([...el.classList].some((c) => /:group$/.test(c)))
|
|
730
|
+
for (const inner of el.querySelectorAll(`[${NODE_ATTR}]`))
|
|
731
|
+
force(inner, GROUP_VARIANTS[forced.state]);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
255
734
|
// Outlines drawn over the page, sized through the style object (allowed by the content security
|
|
256
735
|
// policy, where a style attribute in the markup isn't), never catching the pointer. A section's
|
|
257
736
|
// outline carries its name.
|
|
@@ -269,7 +748,8 @@ function outline(which, el, label) {
|
|
|
269
748
|
let made = boxes[which];
|
|
270
749
|
if (!made) {
|
|
271
750
|
const box = document.createElement("div");
|
|
272
|
-
|
|
751
|
+
// In the page's own coordinates: it scrolls with the page, as the page does, with nothing to redraw.
|
|
752
|
+
Object.assign(box.style, { position: "absolute", pointerEvents: "none", zIndex: "2147483647", ...looks[which] });
|
|
273
753
|
const tag = document.createElement("span");
|
|
274
754
|
Object.assign(tag.style, {
|
|
275
755
|
position: "absolute",
|
|
@@ -286,10 +766,14 @@ function outline(which, el, label) {
|
|
|
286
766
|
document.body.appendChild(box);
|
|
287
767
|
made = boxes[which] = { box, tag };
|
|
288
768
|
}
|
|
289
|
-
|
|
769
|
+
const els = el === null ? [] : Array.isArray(el) ? el : [el];
|
|
770
|
+
if (!els.length)
|
|
290
771
|
return void (made.box.style.display = "none");
|
|
291
|
-
|
|
292
|
-
|
|
772
|
+
// Around all of it: a block that drew more than one thing is outlined as one.
|
|
773
|
+
const rs = els.map((e) => e.getBoundingClientRect());
|
|
774
|
+
const top = Math.min(...rs.map((x) => x.top)), left = Math.min(...rs.map((x) => x.left));
|
|
775
|
+
const r = { top, left, width: Math.max(...rs.map((x) => x.right)) - left, height: Math.max(...rs.map((x) => x.bottom)) - top };
|
|
776
|
+
Object.assign(made.box.style, { display: "block", top: `${r.top + window.scrollY}px`, left: `${r.left + window.scrollX}px`, width: `${r.width}px`, height: `${r.height}px` });
|
|
293
777
|
made.tag.textContent = label ?? "";
|
|
294
778
|
made.tag.style.display = label ? "block" : "none";
|
|
295
779
|
}
|