@agence-zef/astro 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +4 -2
- package/components/Button.astro +42 -0
- package/components/CookieBanner.astro +183 -0
- package/components/Form.astro +291 -0
- package/components/Image.astro +57 -0
- package/components/Popups.astro +210 -0
- package/components/RichText.astro +11 -0
- package/components/Video.astro +38 -0
- package/components/ZefSections.astro +68 -0
- package/components/routes/page.astro +24 -0
- package/components/routes/preview-section.astro +37 -0
- package/components/routes/preview-thumbnail.astro +72 -0
- package/components/routes/preview.astro +77 -0
- package/components/routes/robots.ts +16 -0
- package/components/virtual.d.ts +12 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +17 -0
- package/dist/helpers.d.ts +53 -0
- package/dist/helpers.js +100 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +145 -0
- package/dist/overlay.d.ts +110 -0
- package/dist/overlay.js +561 -0
- package/package.json +50 -3
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { hasStega, parseStegaLocation, stegaRead } from "@agence-zef/schema";
|
|
2
|
+
const ATTR = "data-zef-field";
|
|
3
|
+
export function startOverlay() {
|
|
4
|
+
const config = document.getElementById("zef-preview")?.dataset ?? {};
|
|
5
|
+
const cms = config.cms ?? "";
|
|
6
|
+
const token = config.token ?? "";
|
|
7
|
+
const locale = config.locale ?? "";
|
|
8
|
+
if (!cms || window.parent === window)
|
|
9
|
+
return;
|
|
10
|
+
const post = (message) => window.parent.postMessage(message, cms);
|
|
11
|
+
let labels = { zef: "labels", sections: {}, fields: {} };
|
|
12
|
+
let selected = { section: null, path: null };
|
|
13
|
+
let hovered = null;
|
|
14
|
+
let editing = null;
|
|
15
|
+
let compare = null;
|
|
16
|
+
let pins = [];
|
|
17
|
+
let commentMode = false;
|
|
18
|
+
let locked = new Set();
|
|
19
|
+
/** Image survolée pendant qu'on glisse un fichier. */
|
|
20
|
+
let dropTarget = null;
|
|
21
|
+
/* ---------- Repérage des champs ---------- */
|
|
22
|
+
function scan(root = document.body) {
|
|
23
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
24
|
+
const nodes = [];
|
|
25
|
+
while (walker.nextNode()) {
|
|
26
|
+
const node = walker.currentNode;
|
|
27
|
+
if (node.nodeValue && hasStega(node.nodeValue))
|
|
28
|
+
nodes.push(node);
|
|
29
|
+
}
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
const { clean, locations } = stegaRead(node.nodeValue ?? "");
|
|
32
|
+
node.nodeValue = clean;
|
|
33
|
+
let element = node.parentElement;
|
|
34
|
+
// Texte riche : la marque est dans un élément caché au début du bloc.
|
|
35
|
+
if (element?.hidden && element.parentElement) {
|
|
36
|
+
const container = element.parentElement;
|
|
37
|
+
element.remove();
|
|
38
|
+
element = container;
|
|
39
|
+
}
|
|
40
|
+
if (element && locations[0] && !element.hasAttribute(ATTR))
|
|
41
|
+
element.setAttribute(ATTR, locations[0]);
|
|
42
|
+
}
|
|
43
|
+
// Marques restées dans des attributs (alt, title, aria-label) et le titre de l'onglet.
|
|
44
|
+
const elements = root instanceof Element
|
|
45
|
+
? [root, ...root.querySelectorAll("*")]
|
|
46
|
+
: [...document.querySelectorAll("*")];
|
|
47
|
+
for (const element of elements) {
|
|
48
|
+
for (const attr of [...element.attributes]) {
|
|
49
|
+
if (!hasStega(attr.value))
|
|
50
|
+
continue;
|
|
51
|
+
const { clean, locations } = stegaRead(attr.value);
|
|
52
|
+
element.setAttribute(attr.name, clean);
|
|
53
|
+
// Texte alternatif d'une image : l'image elle-même porte l'emplacement de son champ.
|
|
54
|
+
if (attr.name === "alt" && locations[0] && !element.hasAttribute(ATTR))
|
|
55
|
+
element.setAttribute(ATTR, locations[0]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (hasStega(document.title))
|
|
59
|
+
document.title = stegaRead(document.title).clean;
|
|
60
|
+
}
|
|
61
|
+
const locationOf = (element) => {
|
|
62
|
+
const value = element.getAttribute(ATTR);
|
|
63
|
+
return value ? parseStegaLocation(value) : null;
|
|
64
|
+
};
|
|
65
|
+
const sectionWrapper = (id) => document.querySelector(`[data-zef-section="${CSS.escape(id)}"]`);
|
|
66
|
+
/**
|
|
67
|
+
* Zone d'une section fixe (header, footer) : tout le `<header>` / `<footer>` du gabarit qui
|
|
68
|
+
* l'entoure, pas seulement le conteneur où elle est rendue.
|
|
69
|
+
*/
|
|
70
|
+
const fixedArea = (id) => labels.fixed?.includes(id) ? (sectionWrapper(id)?.closest("header, footer") ?? null) : null;
|
|
71
|
+
/** Élément encadré pour une section. */
|
|
72
|
+
const sectionBox = (id) => fixedArea(id) ?? sectionWrapper(id);
|
|
73
|
+
const sectionOf = (element) => {
|
|
74
|
+
const own = element.closest("[data-zef-section]")?.getAttribute("data-zef-section");
|
|
75
|
+
if (own)
|
|
76
|
+
return own;
|
|
77
|
+
const area = element.closest("header, footer");
|
|
78
|
+
return labels.fixed?.find((id) => area && fixedArea(id) === area) ?? null;
|
|
79
|
+
};
|
|
80
|
+
const fieldElement = (section, path) => document.querySelector(`[${ATTR}="${CSS.escape(`${section}:${path}`)}"]`);
|
|
81
|
+
const pattern = (path) => path.replace(/\.\d+(?=\.|$)/g, ".*");
|
|
82
|
+
const info = (element) => {
|
|
83
|
+
const location = locationOf(element);
|
|
84
|
+
if (!location)
|
|
85
|
+
return null;
|
|
86
|
+
const type = location.scope === "@"
|
|
87
|
+
? "@"
|
|
88
|
+
: (sectionWrapper(location.scope)?.getAttribute("data-zef-type") ?? "");
|
|
89
|
+
return labels.fields[type]?.[pattern(location.path)] ?? null;
|
|
90
|
+
};
|
|
91
|
+
/** Rectangle d'un élément ; une section (`display: contents`) prend celui de ses enfants. */
|
|
92
|
+
function rect(element) {
|
|
93
|
+
if (getComputedStyle(element).display !== "contents")
|
|
94
|
+
return element.getBoundingClientRect();
|
|
95
|
+
const rects = [...element.children].map((c) => rect(c)).filter((r) => r.width || r.height);
|
|
96
|
+
if (!rects.length)
|
|
97
|
+
return new DOMRect();
|
|
98
|
+
const left = Math.min(...rects.map((r) => r.left));
|
|
99
|
+
const top = Math.min(...rects.map((r) => r.top));
|
|
100
|
+
return new DOMRect(left, top, Math.max(...rects.map((r) => r.right)) - left, Math.max(...rects.map((r) => r.bottom)) - top);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Fait défiler l'aperçu jusqu'à l'élément s'il n'est pas entièrement visible. Calculé sur sa
|
|
104
|
+
* boîte (`rect`) : une section en `display: contents` peut commencer par un `<style>` sans boîte.
|
|
105
|
+
*/
|
|
106
|
+
function reveal(element) {
|
|
107
|
+
if (!element)
|
|
108
|
+
return;
|
|
109
|
+
const r = rect(element);
|
|
110
|
+
if (!r.width && !r.height)
|
|
111
|
+
return;
|
|
112
|
+
const margin = 24;
|
|
113
|
+
const view = window.innerHeight;
|
|
114
|
+
if (r.top >= margin && r.bottom <= view - margin)
|
|
115
|
+
return;
|
|
116
|
+
const delta = r.top < margin || r.height > view - 2 * margin ? r.top - margin : r.bottom - view + margin;
|
|
117
|
+
window.scrollBy({ top: delta, behavior: "smooth" });
|
|
118
|
+
}
|
|
119
|
+
/* ---------- Cadres ---------- */
|
|
120
|
+
// Couleurs de l'éditeur ZEF (violet d'action) : l'overlay vit dans le site, hors des tokens du CMS.
|
|
121
|
+
const style = document.createElement("style");
|
|
122
|
+
style.textContent = `
|
|
123
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
124
|
+
[${ATTR}] { cursor: pointer; }
|
|
125
|
+
[${ATTR}][contenteditable] { cursor: text; outline: none; }
|
|
126
|
+
#zef-overlay { position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; font: 500 11px/1.4 system-ui, sans-serif; }
|
|
127
|
+
#zef-overlay .zef-frame { position: fixed; border: 1.5px solid #8c40f7; border-radius: 2px; }
|
|
128
|
+
#zef-overlay .zef-frame.zef-section { border-style: dashed; border-color: #8c40f7aa; }
|
|
129
|
+
#zef-overlay .zef-frame.zef-selected { border-width: 2px; }
|
|
130
|
+
#zef-overlay .zef-tag { position: absolute; left: -1.5px; bottom: 100%; margin-bottom: 2px; padding: 2px 6px; border-radius: 4px; background: #8c40f7; color: #fff; white-space: nowrap; }
|
|
131
|
+
#zef-overlay .zef-compare { border: 2px solid #ea580c; }
|
|
132
|
+
#zef-overlay .zef-compare .zef-tag { left: 8px; top: 8px; bottom: auto; margin: 0; background: #fff7ed; color: #c2410c; }
|
|
133
|
+
#zef-overlay .zef-compare.zef-added { border-style: dashed; border-color: #16a34a; }
|
|
134
|
+
#zef-overlay .zef-compare.zef-added .zef-tag { background: #f0fdf4; color: #15803d; }
|
|
135
|
+
#zef-overlay .zef-pin { position: fixed; display: grid; place-items: center; width: 20px; height: 20px; border: 2px solid #fff; border-radius: 999px; background: #f97316; color: #fff; font: 600 10px/1 system-ui, sans-serif; box-shadow: 0 1px 4px rgb(0 0 0 / 0.25); cursor: pointer; pointer-events: auto; }
|
|
136
|
+
html.zef-commenting, html.zef-commenting * { cursor: crosshair !important; }
|
|
137
|
+
#zef-overlay .zef-toolbar { position: fixed; display: flex; gap: 2px; padding: 3px; border: 1px solid #e4e4e7; border-radius: 8px; background: #fff; box-shadow: 0 4px 12px rgb(0 0 0 / 0.12); pointer-events: auto; }
|
|
138
|
+
#zef-overlay .zef-toolbar button { display: grid; place-items: center; width: 26px; height: 26px; border: 0; border-radius: 6px; background: transparent; color: #3f3f46; cursor: pointer; }
|
|
139
|
+
#zef-overlay .zef-toolbar button:hover { background: #f4f4f5; }
|
|
140
|
+
#zef-overlay .zef-toolbar button:disabled { opacity: 0.35; cursor: default; background: transparent; }
|
|
141
|
+
#zef-overlay .zef-toolbar button.zef-danger { color: #e11d48; }
|
|
142
|
+
#zef-overlay .zef-insert { position: fixed; height: 2px; margin-top: -1px; background: #8c40f7; }
|
|
143
|
+
#zef-overlay .zef-insert button { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; align-items: center; gap: 4px; padding: 4px 12px 4px 10px; border: 0; border-radius: 999px; background: #8c40f7; color: #fff; font: 500 12px/1.4 system-ui, sans-serif; white-space: nowrap; cursor: pointer; pointer-events: auto; }
|
|
144
|
+
`;
|
|
145
|
+
document.head.append(style);
|
|
146
|
+
const layer = document.createElement("div");
|
|
147
|
+
layer.id = "zef-overlay";
|
|
148
|
+
document.body.append(layer);
|
|
149
|
+
function frame(r, className, tag) {
|
|
150
|
+
const div = document.createElement("div");
|
|
151
|
+
div.className = `zef-frame ${className}`;
|
|
152
|
+
Object.assign(div.style, {
|
|
153
|
+
left: `${r.left}px`,
|
|
154
|
+
top: `${r.top}px`,
|
|
155
|
+
width: `${r.width}px`,
|
|
156
|
+
height: `${r.height}px`,
|
|
157
|
+
});
|
|
158
|
+
if (tag) {
|
|
159
|
+
const span = document.createElement("span");
|
|
160
|
+
span.className = "zef-tag";
|
|
161
|
+
span.textContent = tag;
|
|
162
|
+
// Cadre collé en haut de l'aperçu (header) : l'étiquette passe à l'intérieur.
|
|
163
|
+
if (r.top < 20)
|
|
164
|
+
Object.assign(span.style, { bottom: "auto", top: "2px", left: "2px" });
|
|
165
|
+
div.append(span);
|
|
166
|
+
}
|
|
167
|
+
layer.append(div);
|
|
168
|
+
}
|
|
169
|
+
/* Icônes Lucide (16 px, trait 1,5) de la barre d'outils de section. */
|
|
170
|
+
const svg = (paths) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
|
171
|
+
const ICONS = {
|
|
172
|
+
up: svg('<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>'),
|
|
173
|
+
down: svg('<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>'),
|
|
174
|
+
duplicate: svg('<rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'),
|
|
175
|
+
variants: svg('<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>'),
|
|
176
|
+
delete: svg('<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>'),
|
|
177
|
+
plus: svg('<path d="M5 12h14"/><path d="M12 5v14"/>'),
|
|
178
|
+
};
|
|
179
|
+
const TITLES = {
|
|
180
|
+
up: "Monter",
|
|
181
|
+
down: "Descendre",
|
|
182
|
+
duplicate: "Dupliquer",
|
|
183
|
+
variants: "Variantes",
|
|
184
|
+
delete: "Supprimer",
|
|
185
|
+
};
|
|
186
|
+
const sectionIds = () => [...document.querySelectorAll("[data-zef-section]")].map((s) => s.getAttribute("data-zef-section") ?? "");
|
|
187
|
+
/** Barre d'outils en haut à droite de la section sélectionnée. */
|
|
188
|
+
function toolbar(section, r) {
|
|
189
|
+
const ids = sectionIds();
|
|
190
|
+
const bar = document.createElement("div");
|
|
191
|
+
bar.className = "zef-toolbar";
|
|
192
|
+
for (const action of ["up", "down", "duplicate", "variants", "delete"]) {
|
|
193
|
+
const button = document.createElement("button");
|
|
194
|
+
button.type = "button";
|
|
195
|
+
button.title = TITLES[action];
|
|
196
|
+
button.setAttribute("aria-label", TITLES[action]);
|
|
197
|
+
button.innerHTML = ICONS[action];
|
|
198
|
+
if (action === "delete")
|
|
199
|
+
button.className = "zef-danger";
|
|
200
|
+
button.disabled =
|
|
201
|
+
(action === "up" && ids[0] === section) || (action === "down" && ids.at(-1) === section);
|
|
202
|
+
button.addEventListener("click", () => post({ zef: "action", section, action }));
|
|
203
|
+
bar.append(button);
|
|
204
|
+
}
|
|
205
|
+
layer.append(bar);
|
|
206
|
+
const top = Math.max(r.top + 8, 8);
|
|
207
|
+
Object.assign(bar.style, { top: `${top}px`, left: `${r.right - bar.offsetWidth - 8}px` });
|
|
208
|
+
}
|
|
209
|
+
/** Ligne « + Ajouter une section » sous la section survolée. */
|
|
210
|
+
function insertLine(section, r) {
|
|
211
|
+
const line = document.createElement("div");
|
|
212
|
+
line.className = "zef-insert";
|
|
213
|
+
Object.assign(line.style, { left: `${r.left}px`, top: `${r.bottom}px`, width: `${r.width}px` });
|
|
214
|
+
const button = document.createElement("button");
|
|
215
|
+
button.type = "button";
|
|
216
|
+
button.innerHTML = `${ICONS.plus}Ajouter une section`;
|
|
217
|
+
button.addEventListener("click", () => post({ zef: "insert", after: section }));
|
|
218
|
+
line.append(button);
|
|
219
|
+
layer.append(line);
|
|
220
|
+
}
|
|
221
|
+
/** Élément d'une épingle : le champ visé, sinon le haut de la section. */
|
|
222
|
+
const pinElement = (pin) => (pin.path ? fieldElement(pin.section, pin.path) : null) ??
|
|
223
|
+
sectionWrapper(pin.section)?.firstElementChild ??
|
|
224
|
+
null;
|
|
225
|
+
const boxOf = (r) => ({
|
|
226
|
+
top: r.top,
|
|
227
|
+
left: r.left,
|
|
228
|
+
width: r.width,
|
|
229
|
+
height: r.height,
|
|
230
|
+
});
|
|
231
|
+
function drawPins() {
|
|
232
|
+
for (const pin of pins) {
|
|
233
|
+
const element = pinElement(pin);
|
|
234
|
+
if (!element)
|
|
235
|
+
continue;
|
|
236
|
+
const r = rect(element);
|
|
237
|
+
const dot = document.createElement("button");
|
|
238
|
+
dot.type = "button";
|
|
239
|
+
dot.className = "zef-pin";
|
|
240
|
+
dot.textContent = String(pin.number);
|
|
241
|
+
dot.setAttribute("aria-label", `Commentaire ${pin.number}`);
|
|
242
|
+
Object.assign(dot.style, { left: `${r.left - 10}px`, top: `${r.top - 10}px` });
|
|
243
|
+
dot.addEventListener("click", () => post({ zef: "pin", id: pin.id, box: boxOf(r) }));
|
|
244
|
+
layer.append(dot);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function draw() {
|
|
248
|
+
layer.replaceChildren();
|
|
249
|
+
if (dropTarget) {
|
|
250
|
+
const field = info(dropTarget);
|
|
251
|
+
frame(rect(dropTarget), "zef-selected", `Déposer pour remplacer · ${field?.label ?? "Image"}`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
drawPins();
|
|
255
|
+
// Comparaison avec une version : chaque section concernée encadrée, avec son badge.
|
|
256
|
+
if (compare) {
|
|
257
|
+
for (const [id, change] of Object.entries(compare)) {
|
|
258
|
+
const wrapper = sectionWrapper(id);
|
|
259
|
+
if (wrapper)
|
|
260
|
+
frame(rect(wrapper), `zef-compare ${change.state === "added" ? "zef-added" : ""}`, change.label);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// On sélectionne une section entière : ses champs se modifient dans le panneau de droite.
|
|
265
|
+
const target = hovered;
|
|
266
|
+
const section = target ? sectionOf(target) : null;
|
|
267
|
+
const tagOf = (id) => labels.sections[sectionWrapper(id)?.getAttribute("data-zef-type") ?? ""];
|
|
268
|
+
if (section && section !== selected.section) {
|
|
269
|
+
const box = sectionBox(section);
|
|
270
|
+
if (box)
|
|
271
|
+
frame(rect(box), "zef-section", tagOf(section));
|
|
272
|
+
}
|
|
273
|
+
if (selected.section && selected.section !== "@") {
|
|
274
|
+
const box = sectionBox(selected.section);
|
|
275
|
+
if (box)
|
|
276
|
+
frame(rect(box), "zef-selected zef-section", tagOf(selected.section));
|
|
277
|
+
const wrapper = sectionWrapper(selected.section);
|
|
278
|
+
if (wrapper && !editing && !labels.readOnly && !labels.fixed?.includes(selected.section))
|
|
279
|
+
toolbar(selected.section, rect(wrapper));
|
|
280
|
+
}
|
|
281
|
+
if (section && !editing && !labels.readOnly && !labels.fixed?.includes(section)) {
|
|
282
|
+
const wrapper = sectionWrapper(section);
|
|
283
|
+
if (wrapper)
|
|
284
|
+
insertLine(section, rect(wrapper));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
let frameRequest = 0;
|
|
288
|
+
const redraw = () => {
|
|
289
|
+
cancelAnimationFrame(frameRequest);
|
|
290
|
+
frameRequest = requestAnimationFrame(draw);
|
|
291
|
+
};
|
|
292
|
+
window.addEventListener("scroll", redraw, { passive: true });
|
|
293
|
+
window.addEventListener("resize", redraw);
|
|
294
|
+
function setCommentMode(on) {
|
|
295
|
+
commentMode = on;
|
|
296
|
+
document.documentElement.classList.toggle("zef-commenting", on);
|
|
297
|
+
}
|
|
298
|
+
/* ---------- Souris et clavier ---------- */
|
|
299
|
+
document.addEventListener("mouseover", (e) => {
|
|
300
|
+
if (layer.contains(e.target))
|
|
301
|
+
return;
|
|
302
|
+
const target = e.target;
|
|
303
|
+
const next = sectionOf(target) ? target : null;
|
|
304
|
+
if (next !== hovered) {
|
|
305
|
+
hovered = next;
|
|
306
|
+
redraw();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
document.addEventListener("mouseleave", () => {
|
|
310
|
+
hovered = null;
|
|
311
|
+
redraw();
|
|
312
|
+
});
|
|
313
|
+
document.addEventListener("click", (e) => {
|
|
314
|
+
const target = e.target;
|
|
315
|
+
if (editing?.contains(target) || layer.contains(target))
|
|
316
|
+
return;
|
|
317
|
+
// Dans l'aperçu, un clic sélectionne : liens et boutons ne partent pas.
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
const field = target.closest(`[${ATTR}]`);
|
|
321
|
+
const location = field ? locationOf(field) : null;
|
|
322
|
+
const section = location?.scope && location.scope !== "@" ? location.scope : sectionOf(target);
|
|
323
|
+
// Mode commentaire : le clic désigne l'élément commenté.
|
|
324
|
+
if (commentMode) {
|
|
325
|
+
const scope = location?.scope === "@" ? "@" : section;
|
|
326
|
+
if (!scope)
|
|
327
|
+
return;
|
|
328
|
+
const element = field ?? sectionWrapper(scope)?.firstElementChild ?? target;
|
|
329
|
+
setCommentMode(false);
|
|
330
|
+
post({
|
|
331
|
+
zef: "commentTarget",
|
|
332
|
+
section: scope,
|
|
333
|
+
path: location?.path ?? null,
|
|
334
|
+
box: boxOf(rect(element)),
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Un clic choisit toute la section (ou l'élément de collection pour ses propres champs).
|
|
339
|
+
selected = { section: location?.scope === "@" ? "@" : section, path: null };
|
|
340
|
+
post({ zef: "select", ...selected });
|
|
341
|
+
redraw();
|
|
342
|
+
}, true);
|
|
343
|
+
document.addEventListener("submit", (e) => e.preventDefault(), true);
|
|
344
|
+
/* Glisser un fichier sur une image de l'aperçu la remplace (E-01). */
|
|
345
|
+
const hasFiles = (e) => Boolean(e.dataTransfer?.types.includes("Files"));
|
|
346
|
+
const mediaUnder = (target) => {
|
|
347
|
+
const element = target?.closest?.(`[${ATTR}]`);
|
|
348
|
+
return element && info(element)?.kind === "media" ? element : null;
|
|
349
|
+
};
|
|
350
|
+
document.addEventListener("dragover", (e) => {
|
|
351
|
+
if (!hasFiles(e))
|
|
352
|
+
return;
|
|
353
|
+
// Jamais ouvrir le fichier dans l'aperçu.
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
const next = labels.readOnly ? null : mediaUnder(e.target);
|
|
356
|
+
if (e.dataTransfer)
|
|
357
|
+
e.dataTransfer.dropEffect = next ? "copy" : "none";
|
|
358
|
+
if (next !== dropTarget) {
|
|
359
|
+
dropTarget = next;
|
|
360
|
+
redraw();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
document.addEventListener("dragleave", (e) => {
|
|
364
|
+
if (e.relatedTarget)
|
|
365
|
+
return;
|
|
366
|
+
dropTarget = null;
|
|
367
|
+
redraw();
|
|
368
|
+
});
|
|
369
|
+
document.addEventListener("drop", (e) => {
|
|
370
|
+
if (!hasFiles(e))
|
|
371
|
+
return;
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
const target = dropTarget;
|
|
374
|
+
dropTarget = null;
|
|
375
|
+
redraw();
|
|
376
|
+
const file = e.dataTransfer?.files[0];
|
|
377
|
+
const location = target ? locationOf(target) : null;
|
|
378
|
+
if (!file || !location)
|
|
379
|
+
return;
|
|
380
|
+
post({ zef: "drop", section: location.scope, path: location.path, file });
|
|
381
|
+
});
|
|
382
|
+
document.addEventListener("dblclick", (e) => {
|
|
383
|
+
const field = e.target.closest(`[${ATTR}]`);
|
|
384
|
+
const kind = field && info(field)?.kind;
|
|
385
|
+
if (labels.readOnly || !field || !kind || !["text", "textarea", "label"].includes(kind))
|
|
386
|
+
return;
|
|
387
|
+
if (locked.has(field.getAttribute(ATTR) ?? ""))
|
|
388
|
+
return;
|
|
389
|
+
startEditing(field);
|
|
390
|
+
});
|
|
391
|
+
function startEditing(element) {
|
|
392
|
+
stopEditing();
|
|
393
|
+
editing = element;
|
|
394
|
+
element.setAttribute("contenteditable", "plaintext-only");
|
|
395
|
+
element.focus();
|
|
396
|
+
const range = document.createRange();
|
|
397
|
+
range.selectNodeContents(element);
|
|
398
|
+
const sel = window.getSelection();
|
|
399
|
+
sel?.removeAllRanges();
|
|
400
|
+
sel?.addRange(range);
|
|
401
|
+
redraw();
|
|
402
|
+
}
|
|
403
|
+
function stopEditing() {
|
|
404
|
+
if (!editing)
|
|
405
|
+
return;
|
|
406
|
+
editing.removeAttribute("contenteditable");
|
|
407
|
+
editing = null;
|
|
408
|
+
redraw();
|
|
409
|
+
}
|
|
410
|
+
document.addEventListener("input", (e) => {
|
|
411
|
+
if (!editing || e.target !== editing)
|
|
412
|
+
return;
|
|
413
|
+
const location = locationOf(editing);
|
|
414
|
+
if (!location)
|
|
415
|
+
return;
|
|
416
|
+
post({ zef: "edit", section: location.scope, path: location.path, value: editing.textContent });
|
|
417
|
+
});
|
|
418
|
+
document.addEventListener("keydown", (e) => {
|
|
419
|
+
if (!editing && e.key === "Escape") {
|
|
420
|
+
selected = { section: null, path: null };
|
|
421
|
+
post({ zef: "select", section: null, path: null });
|
|
422
|
+
redraw();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (!editing)
|
|
426
|
+
return;
|
|
427
|
+
const multiline = info(editing)?.kind === "textarea";
|
|
428
|
+
if (e.key === "Escape" || (e.key === "Enter" && !multiline)) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
stopEditing();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
document.addEventListener("focusout", (e) => {
|
|
434
|
+
if (e.target === editing)
|
|
435
|
+
stopEditing();
|
|
436
|
+
});
|
|
437
|
+
/* ---------- Messages de l'éditeur ---------- */
|
|
438
|
+
/** Textes reçus pendant un re-rendu : la réponse, partie avant eux, ne doit pas les effacer. */
|
|
439
|
+
const typed = new Map();
|
|
440
|
+
const renders = new Map();
|
|
441
|
+
function setText(section, path, value) {
|
|
442
|
+
const element = fieldElement(section, path);
|
|
443
|
+
if (!element || element === editing)
|
|
444
|
+
return;
|
|
445
|
+
if (info(element)?.kind === "richtext")
|
|
446
|
+
element.innerHTML = value;
|
|
447
|
+
else
|
|
448
|
+
element.textContent = value;
|
|
449
|
+
}
|
|
450
|
+
async function render(section, after) {
|
|
451
|
+
const request = (renders.get(section.id) ?? 0) + 1;
|
|
452
|
+
renders.set(section.id, request);
|
|
453
|
+
typed.set(section.id, new Map());
|
|
454
|
+
const response = await fetch("/__zef/preview/section", {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: { "content-type": "application/json" },
|
|
457
|
+
body: JSON.stringify({ token, locale, doc: config.doc, section }),
|
|
458
|
+
});
|
|
459
|
+
const html = response.ok ? await response.text() : "";
|
|
460
|
+
// Un re-rendu plus récent de la même section est parti entre-temps : il fait foi.
|
|
461
|
+
if (renders.get(section.id) !== request)
|
|
462
|
+
return;
|
|
463
|
+
const pending = typed.get(section.id);
|
|
464
|
+
typed.delete(section.id);
|
|
465
|
+
const template = document.createElement("template");
|
|
466
|
+
template.innerHTML = html;
|
|
467
|
+
const next = template.content.querySelector("[data-zef-section]");
|
|
468
|
+
if (!next)
|
|
469
|
+
return;
|
|
470
|
+
const current = sectionWrapper(section.id);
|
|
471
|
+
if (current)
|
|
472
|
+
current.replaceWith(next);
|
|
473
|
+
else {
|
|
474
|
+
// Conteneur des sections du document : le header et le footer partagés sont hors de lui.
|
|
475
|
+
const previous = after ? sectionWrapper(after) : null;
|
|
476
|
+
const container = document.querySelector("[data-zef-sections]");
|
|
477
|
+
const first = document.querySelector("[data-zef-section]");
|
|
478
|
+
if (previous)
|
|
479
|
+
previous.after(next);
|
|
480
|
+
else if (container)
|
|
481
|
+
container.prepend(next);
|
|
482
|
+
else if (first)
|
|
483
|
+
first.before(next);
|
|
484
|
+
else
|
|
485
|
+
document.querySelector("main")?.append(next);
|
|
486
|
+
}
|
|
487
|
+
scan(next);
|
|
488
|
+
for (const [path, value] of pending ?? [])
|
|
489
|
+
setText(section.id, path, value);
|
|
490
|
+
// Section ajoutée et déjà sélectionnée (insertion depuis la bibliothèque) : on la montre.
|
|
491
|
+
if (!current && selected.section === section.id && !selected.path) {
|
|
492
|
+
reveal(next);
|
|
493
|
+
}
|
|
494
|
+
redraw();
|
|
495
|
+
}
|
|
496
|
+
window.addEventListener("message", (event) => {
|
|
497
|
+
if (event.origin !== cms || typeof event.data !== "object" || !event.data?.zef)
|
|
498
|
+
return;
|
|
499
|
+
const message = event.data;
|
|
500
|
+
switch (message.zef) {
|
|
501
|
+
case "labels":
|
|
502
|
+
labels = message;
|
|
503
|
+
break;
|
|
504
|
+
case "select":
|
|
505
|
+
selected = { section: message.section, path: message.path };
|
|
506
|
+
reveal(selected.path && selected.section
|
|
507
|
+
? fieldElement(selected.section, selected.path)
|
|
508
|
+
: selected.section
|
|
509
|
+
? sectionBox(selected.section)
|
|
510
|
+
: null);
|
|
511
|
+
break;
|
|
512
|
+
case "text":
|
|
513
|
+
typed.get(message.section)?.set(message.path, message.value);
|
|
514
|
+
setText(message.section, message.path, message.value);
|
|
515
|
+
break;
|
|
516
|
+
case "render":
|
|
517
|
+
void render(message.section, message.after);
|
|
518
|
+
return;
|
|
519
|
+
case "order": {
|
|
520
|
+
const wrappers = message.ids.map(sectionWrapper).filter((w) => w !== null);
|
|
521
|
+
const anchor = wrappers[0]?.parentElement;
|
|
522
|
+
if (anchor) {
|
|
523
|
+
const marker = document.createComment("zef");
|
|
524
|
+
wrappers[0].before(marker);
|
|
525
|
+
for (const w of wrappers)
|
|
526
|
+
marker.before(w);
|
|
527
|
+
marker.remove();
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "remove":
|
|
532
|
+
sectionWrapper(message.id)?.remove();
|
|
533
|
+
break;
|
|
534
|
+
case "compare":
|
|
535
|
+
compare = message.sections;
|
|
536
|
+
break;
|
|
537
|
+
case "pins":
|
|
538
|
+
pins = message.pins;
|
|
539
|
+
break;
|
|
540
|
+
case "locked":
|
|
541
|
+
locked = new Set(message.fields);
|
|
542
|
+
break;
|
|
543
|
+
case "commentMode":
|
|
544
|
+
setCommentMode(message.on);
|
|
545
|
+
break;
|
|
546
|
+
case "showPin": {
|
|
547
|
+
const pin = pins.find((p) => p.id === message.id);
|
|
548
|
+
const element = pin ? pinElement(pin) : null;
|
|
549
|
+
if (pin && element) {
|
|
550
|
+
element.scrollIntoView({ block: "center" });
|
|
551
|
+
post({ zef: "pin", id: pin.id, box: boxOf(rect(element)) });
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
redraw();
|
|
557
|
+
});
|
|
558
|
+
scan();
|
|
559
|
+
post({ zef: "ready" });
|
|
560
|
+
redraw();
|
|
561
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agence-zef/astro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Intégration Astro du CMS ZEF : routes, rendu des sections, aperçu, composants réutilisables sans style.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./ZefSections.astro": "./components/ZefSections.astro",
|
|
13
|
+
"./routes/page.astro": "./components/routes/page.astro",
|
|
14
|
+
"./routes/robots.ts": "./components/routes/robots.ts",
|
|
15
|
+
"./Button.astro": "./components/Button.astro",
|
|
16
|
+
"./Image.astro": "./components/Image.astro",
|
|
17
|
+
"./Video.astro": "./components/Video.astro",
|
|
18
|
+
"./RichText.astro": "./components/RichText.astro",
|
|
19
|
+
"./overlay": {
|
|
20
|
+
"types": "./dist/overlay.d.ts",
|
|
21
|
+
"import": "./dist/overlay.js"
|
|
22
|
+
},
|
|
23
|
+
"./routes/preview.astro": "./components/routes/preview.astro",
|
|
24
|
+
"./routes/preview-section.astro": "./components/routes/preview-section.astro",
|
|
25
|
+
"./routes/preview-thumbnail.astro": "./components/routes/preview-thumbnail.astro",
|
|
26
|
+
"./Form.astro": "./components/Form.astro",
|
|
27
|
+
"./Popups.astro": "./components/Popups.astro",
|
|
28
|
+
"./CookieBanner.astro": "./components/CookieBanner.astro"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"components"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"astro": "^6.0.0 || ^7.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"astro": "^7.4.4",
|
|
42
|
+
"@types/node": "^24.0.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@agence-zef/sdk": "0.2.0",
|
|
46
|
+
"@agence-zef/schema": "0.2.0"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://agence-zef.fr",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
52
|
+
}
|
|
6
53
|
}
|