@cancia/toolbar 0.13.0 → 0.15.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/dist/cancia.d.ts +7 -0
- package/dist/cancia.iife.js +445 -430
- package/dist/cancia.js +360 -105
- package/dist/cancia.js.map +1 -1
- package/package.json +2 -2
package/dist/cancia.js
CHANGED
|
@@ -50,9 +50,9 @@ function renderRichToDom(target, blocks) {
|
|
|
50
50
|
}
|
|
51
51
|
listWrap = null;
|
|
52
52
|
listKind = null;
|
|
53
|
-
const
|
|
54
|
-
appendSpans(
|
|
55
|
-
target.appendChild(
|
|
53
|
+
const el2 = document.createElement(STYLE_TAG[block.style] ?? "p");
|
|
54
|
+
appendSpans(el2, block);
|
|
55
|
+
target.appendChild(el2);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function appendSpans(parent, block) {
|
|
@@ -87,8 +87,8 @@ var STYLE_OF_TAG = {
|
|
|
87
87
|
};
|
|
88
88
|
function domToRows(region) {
|
|
89
89
|
const rows = [];
|
|
90
|
-
const pushBlock = (
|
|
91
|
-
const text = inlineToShorthand(
|
|
90
|
+
const pushBlock = (el2, style, listItem) => {
|
|
91
|
+
const text = inlineToShorthand(el2);
|
|
92
92
|
if (text.trim() === "") return;
|
|
93
93
|
rows.push(listItem ? { text, style, listItem } : { text, style });
|
|
94
94
|
};
|
|
@@ -113,9 +113,9 @@ function domToRows(region) {
|
|
|
113
113
|
}
|
|
114
114
|
return rows;
|
|
115
115
|
}
|
|
116
|
-
function inlineToShorthand(
|
|
116
|
+
function inlineToShorthand(el2) {
|
|
117
117
|
let out = "";
|
|
118
|
-
for (const node of
|
|
118
|
+
for (const node of el2.childNodes) {
|
|
119
119
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
120
120
|
out += (node.textContent ?? "").replace(/\s+/g, " ");
|
|
121
121
|
continue;
|
|
@@ -180,36 +180,36 @@ function setPending(key, lang, value) {
|
|
|
180
180
|
state.pending.set(full, { key, lang, value });
|
|
181
181
|
}
|
|
182
182
|
function applyOverlay() {
|
|
183
|
-
document.querySelectorAll("[data-cms]").forEach((
|
|
184
|
-
if (
|
|
185
|
-
const key =
|
|
183
|
+
document.querySelectorAll("[data-cms]").forEach((el2) => {
|
|
184
|
+
if (el2.dataset.cmsList !== void 0) return;
|
|
185
|
+
const key = el2.dataset.cms;
|
|
186
186
|
if (!key) return;
|
|
187
187
|
const savedValue = state.cmsData[`${key}.${state.activeLang}`];
|
|
188
188
|
if (savedValue === void 0) return;
|
|
189
|
-
if (
|
|
190
|
-
|
|
189
|
+
if (el2.tagName === "IMG") {
|
|
190
|
+
el2.src = savedValue;
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
|
-
if (
|
|
193
|
+
if (el2.dataset.cmsType === "richtext") {
|
|
194
194
|
const blocks = parseRichValue(savedValue);
|
|
195
|
-
if (blocks) renderRichToDom(
|
|
195
|
+
if (blocks) renderRichToDom(el2, blocks);
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
|
-
if (
|
|
198
|
+
if (el2.dataset.cmsType === "link") {
|
|
199
199
|
const link = parseLinkValue(savedValue);
|
|
200
|
-
if (link.href &&
|
|
201
|
-
const labelEls =
|
|
200
|
+
if (link.href && el2.tagName === "A") el2.setAttribute("href", link.href);
|
|
201
|
+
const labelEls = el2.querySelectorAll("[data-cms-label]");
|
|
202
202
|
if (labelEls.length > 0) labelEls.forEach((n) => n.textContent = link.label);
|
|
203
|
-
else if (
|
|
203
|
+
else if (el2.childElementCount === 0) el2.textContent = link.label;
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
|
-
if (
|
|
206
|
+
if (el2.childElementCount > 0) {
|
|
207
207
|
console.warn(
|
|
208
208
|
`Cancia: skipping overlay for "${key}" \u2014 element has child elements (textContent would destroy them).`
|
|
209
209
|
);
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
el2.textContent = savedValue;
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
function revertPending() {
|
|
@@ -220,17 +220,17 @@ function revertPending() {
|
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
222
|
const savedValue = state.cmsData[fullKey];
|
|
223
|
-
document.querySelectorAll(`[data-cms="${key}"]`).forEach((
|
|
224
|
-
if (
|
|
225
|
-
|
|
223
|
+
document.querySelectorAll(`[data-cms="${key}"]`).forEach((el2) => {
|
|
224
|
+
if (el2.tagName === "IMG") {
|
|
225
|
+
el2.src = savedValue;
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
if (
|
|
228
|
+
if (el2.dataset.cmsType === "richtext") {
|
|
229
229
|
const blocks = parseRichValue(savedValue);
|
|
230
|
-
if (blocks) renderRichToDom(
|
|
230
|
+
if (blocks) renderRichToDom(el2, blocks);
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
233
|
-
|
|
233
|
+
el2.textContent = savedValue;
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
state.pending.clear();
|
|
@@ -242,6 +242,7 @@ function headers() {
|
|
|
242
242
|
if (state.sessionToken) h["Authorization"] = `Bearer ${state.sessionToken}`;
|
|
243
243
|
return h;
|
|
244
244
|
}
|
|
245
|
+
var CREDS = "include";
|
|
245
246
|
function currentRoute() {
|
|
246
247
|
return typeof location !== "undefined" ? location.pathname : "";
|
|
247
248
|
}
|
|
@@ -252,6 +253,7 @@ function routeParam() {
|
|
|
252
253
|
async function fetchContent() {
|
|
253
254
|
const { apiUrl, site } = state.config;
|
|
254
255
|
const res = await fetch(`${apiUrl}/api/cancia/content?site=${encodeURIComponent(site)}`, {
|
|
256
|
+
credentials: CREDS,
|
|
255
257
|
headers: headers()
|
|
256
258
|
});
|
|
257
259
|
if (!res.ok) throw new Error(`Cancia: failed to fetch content (${res.status})`);
|
|
@@ -260,6 +262,7 @@ async function fetchContent() {
|
|
|
260
262
|
async function saveEntry(key, lang, value) {
|
|
261
263
|
const { apiUrl, site } = state.config;
|
|
262
264
|
const res = await fetch(`${apiUrl}/api/cancia/save`, {
|
|
265
|
+
credentials: CREDS,
|
|
263
266
|
method: "POST",
|
|
264
267
|
headers: headers(),
|
|
265
268
|
// `route` lets the server invalidate this page's cache tag (034, invalidate mode).
|
|
@@ -270,6 +273,7 @@ async function saveEntry(key, lang, value) {
|
|
|
270
273
|
async function deleteContent(key, lang) {
|
|
271
274
|
const { apiUrl, site } = state.config;
|
|
272
275
|
const res = await fetch(`${apiUrl}/api/cancia/content`, {
|
|
276
|
+
credentials: CREDS,
|
|
273
277
|
method: "DELETE",
|
|
274
278
|
headers: headers(),
|
|
275
279
|
body: JSON.stringify({ site, key, lang, route: currentRoute() })
|
|
@@ -294,6 +298,7 @@ function uploadImage(file, onProgress) {
|
|
|
294
298
|
form.append("site", site);
|
|
295
299
|
const xhr = new XMLHttpRequest();
|
|
296
300
|
xhr.open("POST", `${apiUrl}/api/cancia/upload`);
|
|
301
|
+
xhr.withCredentials = true;
|
|
297
302
|
if (state.sessionToken) xhr.setRequestHeader("Authorization", `Bearer ${state.sessionToken}`);
|
|
298
303
|
xhr.upload.addEventListener("progress", (e) => {
|
|
299
304
|
if (e.lengthComputable) onProgress?.(Math.round(e.loaded / e.total * 100));
|
|
@@ -316,6 +321,7 @@ function uploadImage(file, onProgress) {
|
|
|
316
321
|
async function triggerPublish() {
|
|
317
322
|
const { apiUrl, site } = state.config;
|
|
318
323
|
const res = await fetch(`${apiUrl}/api/cancia/publish`, {
|
|
324
|
+
credentials: CREDS,
|
|
319
325
|
method: "POST",
|
|
320
326
|
headers: headers(),
|
|
321
327
|
body: JSON.stringify({ site })
|
|
@@ -324,7 +330,7 @@ async function triggerPublish() {
|
|
|
324
330
|
}
|
|
325
331
|
async function fetchSchemas() {
|
|
326
332
|
const { apiUrl } = state.config;
|
|
327
|
-
const res = await fetch(`${apiUrl}/api/cancia/schemas`, { headers: headers() });
|
|
333
|
+
const res = await fetch(`${apiUrl}/api/cancia/schemas`, { headers: headers(), credentials: CREDS });
|
|
328
334
|
if (!res.ok) throw new Error(`Cancia: failed to fetch schemas (${res.status})`);
|
|
329
335
|
const body = await res.json();
|
|
330
336
|
return body.schemas;
|
|
@@ -336,7 +342,7 @@ async function fetchList(listName, locale) {
|
|
|
336
342
|
const { apiUrl, site } = state.config;
|
|
337
343
|
const res = await fetch(
|
|
338
344
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
339
|
-
{ headers: headers() }
|
|
345
|
+
{ headers: headers(), credentials: CREDS }
|
|
340
346
|
);
|
|
341
347
|
if (!res.ok) throw new Error(`Cancia: failed to fetch list "${listName}" (${res.status})`);
|
|
342
348
|
const body = await res.json();
|
|
@@ -346,7 +352,7 @@ async function fetchTranslations(listName) {
|
|
|
346
352
|
const { apiUrl, site } = state.config;
|
|
347
353
|
const res = await fetch(
|
|
348
354
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/_translations?site=${encodeURIComponent(site)}`,
|
|
349
|
-
{ headers: headers() }
|
|
355
|
+
{ headers: headers(), credentials: CREDS }
|
|
350
356
|
);
|
|
351
357
|
if (!res.ok) throw new Error(`Cancia: failed to fetch translations (${res.status})`);
|
|
352
358
|
const body = await res.json();
|
|
@@ -356,7 +362,7 @@ async function createListEntry(listName, data, locale, id) {
|
|
|
356
362
|
const { apiUrl, site } = state.config;
|
|
357
363
|
const res = await fetch(
|
|
358
364
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
|
|
359
|
-
{ method: "POST", headers: headers(), body: JSON.stringify({ data, id }) }
|
|
365
|
+
{ method: "POST", headers: headers(), credentials: CREDS, body: JSON.stringify({ data, id }) }
|
|
360
366
|
);
|
|
361
367
|
if (!res.ok) {
|
|
362
368
|
throw await toApiError(res, "Could not create the entry. Check your connection and try again.");
|
|
@@ -368,7 +374,7 @@ async function updateListEntry(listName, id, data, rev, locale) {
|
|
|
368
374
|
const { apiUrl, site } = state.config;
|
|
369
375
|
const res = await fetch(
|
|
370
376
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
|
|
371
|
-
{ method: "PATCH", headers: headers(), body: JSON.stringify({ data, _rev: rev }) }
|
|
377
|
+
{ method: "PATCH", headers: headers(), credentials: CREDS, body: JSON.stringify({ data, _rev: rev }) }
|
|
372
378
|
);
|
|
373
379
|
if (!res.ok) {
|
|
374
380
|
throw await toApiError(res, "Could not save your changes. Check your connection and try again.");
|
|
@@ -380,7 +386,7 @@ async function deleteListEntry(listName, id, locale) {
|
|
|
380
386
|
const { apiUrl, site } = state.config;
|
|
381
387
|
const res = await fetch(
|
|
382
388
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
|
|
383
|
-
{ method: "DELETE", headers: headers() }
|
|
389
|
+
{ method: "DELETE", headers: headers(), credentials: CREDS }
|
|
384
390
|
);
|
|
385
391
|
if (!res.ok) throw new Error(`Cancia: failed to delete entry (${res.status})`);
|
|
386
392
|
}
|
|
@@ -388,9 +394,9 @@ async function reorderList(listName, ids) {
|
|
|
388
394
|
const { apiUrl, site } = state.config;
|
|
389
395
|
const res = await fetch(
|
|
390
396
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/reorder?site=${encodeURIComponent(site)}${routeParam()}`,
|
|
391
|
-
{ method: "POST", headers: headers(), body: JSON.stringify({ ids }) }
|
|
397
|
+
{ method: "POST", headers: headers(), credentials: CREDS, body: JSON.stringify({ ids, knownCount: ids.length }) }
|
|
392
398
|
);
|
|
393
|
-
if (!res.ok) throw
|
|
399
|
+
if (!res.ok) throw await toApiError(res, "Could not save the new order. Check your connection and try again.");
|
|
394
400
|
}
|
|
395
401
|
async function flushEdits(edits, save) {
|
|
396
402
|
const results = await Promise.allSettled(edits.map((edit) => save(edit)));
|
|
@@ -659,9 +665,9 @@ ${tokenCss(accent4, useAccent)}
|
|
|
659
665
|
`;
|
|
660
666
|
document.head.appendChild(style);
|
|
661
667
|
}
|
|
662
|
-
function markUi(
|
|
663
|
-
|
|
664
|
-
return
|
|
668
|
+
function markUi(el2) {
|
|
669
|
+
el2.dataset.canciaUi = "";
|
|
670
|
+
return el2;
|
|
665
671
|
}
|
|
666
672
|
var surface = (level = 2) => `
|
|
667
673
|
background: ${v(`surface-${level}`)};
|
|
@@ -768,41 +774,41 @@ var groupRow = () => `
|
|
|
768
774
|
align-items: flex-start;
|
|
769
775
|
gap: ${v("space-2")};
|
|
770
776
|
`;
|
|
771
|
-
function attachHover(
|
|
777
|
+
function attachHover(el2, opts = {}) {
|
|
772
778
|
const bg = opts.bg ?? v("surface-hover");
|
|
773
779
|
const color = opts.color;
|
|
774
|
-
const priorBg =
|
|
775
|
-
const priorColor =
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (color)
|
|
780
|
+
const priorBg = el2.style.background;
|
|
781
|
+
const priorColor = el2.style.color;
|
|
782
|
+
el2.addEventListener("mouseenter", () => {
|
|
783
|
+
el2.style.background = bg;
|
|
784
|
+
if (color) el2.style.color = color;
|
|
779
785
|
});
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (color)
|
|
786
|
+
el2.addEventListener("mouseleave", () => {
|
|
787
|
+
el2.style.background = priorBg;
|
|
788
|
+
if (color) el2.style.color = priorColor;
|
|
783
789
|
});
|
|
784
790
|
}
|
|
785
|
-
function attachPress(
|
|
791
|
+
function attachPress(el2, scale = 0.96) {
|
|
786
792
|
const down = (e) => {
|
|
787
|
-
if (
|
|
788
|
-
|
|
789
|
-
|
|
793
|
+
if (el2.disabled) return;
|
|
794
|
+
el2.setPointerCapture?.(e.pointerId);
|
|
795
|
+
el2.style.transform = `scale(${scale})`;
|
|
790
796
|
};
|
|
791
797
|
const up = () => {
|
|
792
|
-
|
|
798
|
+
el2.style.transform = "";
|
|
793
799
|
};
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
function attachInputFocus(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
800
|
+
el2.addEventListener("pointerdown", down);
|
|
801
|
+
el2.addEventListener("pointerup", up);
|
|
802
|
+
el2.addEventListener("pointercancel", up);
|
|
803
|
+
}
|
|
804
|
+
function attachInputFocus(el2) {
|
|
805
|
+
el2.addEventListener("focus", () => {
|
|
806
|
+
el2.style.borderColor = v("accent-ring");
|
|
807
|
+
el2.style.background = v("surface-hover");
|
|
802
808
|
});
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
809
|
+
el2.addEventListener("blur", () => {
|
|
810
|
+
el2.style.borderColor = v("border");
|
|
811
|
+
el2.style.background = v("surface-raised");
|
|
806
812
|
});
|
|
807
813
|
}
|
|
808
814
|
|
|
@@ -832,15 +838,15 @@ function withAlpha(color, alpha) {
|
|
|
832
838
|
}
|
|
833
839
|
return `rgba(24, 24, 27, ${alpha})`;
|
|
834
840
|
}
|
|
835
|
-
function fieldType(
|
|
836
|
-
if (
|
|
837
|
-
if (
|
|
838
|
-
if (
|
|
839
|
-
if (
|
|
841
|
+
function fieldType(el2) {
|
|
842
|
+
if (el2.dataset.cmsType === "image") return "image";
|
|
843
|
+
if (el2.dataset.cmsType === "link") return "link";
|
|
844
|
+
if (el2.dataset.cmsType === "richtext") return "richtext";
|
|
845
|
+
if (el2.tagName === "IMG") return "image";
|
|
840
846
|
return "text";
|
|
841
847
|
}
|
|
842
|
-
function elementMode(
|
|
843
|
-
return
|
|
848
|
+
function elementMode(el2) {
|
|
849
|
+
return el2.dataset.cmsList ? "list" : "field";
|
|
844
850
|
}
|
|
845
851
|
function injectStyles() {
|
|
846
852
|
if (styleInjected) return;
|
|
@@ -948,9 +954,9 @@ var KIND_LABEL = {
|
|
|
948
954
|
link: "Link",
|
|
949
955
|
richtext: "Rich text"
|
|
950
956
|
};
|
|
951
|
-
function positionOverlay(
|
|
952
|
-
const rect =
|
|
953
|
-
const mode = elementMode(
|
|
957
|
+
function positionOverlay(el2, animate = false) {
|
|
958
|
+
const rect = el2.getBoundingClientRect();
|
|
959
|
+
const mode = elementMode(el2);
|
|
954
960
|
const isList = mode === "list";
|
|
955
961
|
const a = isList ? listAccent(accent()) : accent();
|
|
956
962
|
const overlay = getOrCreateOverlay();
|
|
@@ -959,8 +965,8 @@ function positionOverlay(el, animate = false) {
|
|
|
959
965
|
overlay.style.transform = `translate(${rect.left - padding}px, ${rect.top - padding}px)`;
|
|
960
966
|
overlay.style.width = `${rect.width + padding * 2}px`;
|
|
961
967
|
overlay.style.height = `${rect.height + padding * 2}px`;
|
|
962
|
-
if (lastOverlayEl !==
|
|
963
|
-
lastOverlayEl =
|
|
968
|
+
if (lastOverlayEl !== el2) {
|
|
969
|
+
lastOverlayEl = el2;
|
|
964
970
|
if (isList) {
|
|
965
971
|
overlay.style.border = `1px dashed ${a}`;
|
|
966
972
|
overlay.style.borderRadius = "6px";
|
|
@@ -974,12 +980,12 @@ function positionOverlay(el, animate = false) {
|
|
|
974
980
|
if (isList) {
|
|
975
981
|
tagIcon = LIST_ICON;
|
|
976
982
|
tagLabel = "List";
|
|
977
|
-
tooltipText = `list: ${
|
|
983
|
+
tooltipText = `list: ${el2.dataset.cmsList}`;
|
|
978
984
|
} else {
|
|
979
|
-
const type = fieldType(
|
|
985
|
+
const type = fieldType(el2);
|
|
980
986
|
tagIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
|
|
981
987
|
tagLabel = KIND_LABEL[type];
|
|
982
|
-
tooltipText =
|
|
988
|
+
tooltipText = el2.dataset.cms ?? "";
|
|
983
989
|
}
|
|
984
990
|
overlay.style.border = `2px solid ${withAlpha(a, isList ? 0.55 : 0.45)}`;
|
|
985
991
|
overlay.style.background = withAlpha(a, 0.05);
|
|
@@ -1062,18 +1068,18 @@ function handleClick(e) {
|
|
|
1062
1068
|
}
|
|
1063
1069
|
function warnUnhoverableRegions() {
|
|
1064
1070
|
const els = document.querySelectorAll(CMS_SELECTOR);
|
|
1065
|
-
els.forEach((
|
|
1066
|
-
const rect =
|
|
1071
|
+
els.forEach((el2) => {
|
|
1072
|
+
const rect = el2.getBoundingClientRect();
|
|
1067
1073
|
if (rect.width > 0 && rect.height > 0) return;
|
|
1068
|
-
const cs = getComputedStyle(
|
|
1074
|
+
const cs = getComputedStyle(el2);
|
|
1069
1075
|
const selfInflicted = cs.display === "contents" || cs.position === "absolute" || cs.clipPath !== "none";
|
|
1070
1076
|
if (!selfInflicted) return;
|
|
1071
|
-
const name =
|
|
1077
|
+
const name = el2.dataset.cmsList ? `list "${el2.dataset.cmsList}"` : `field "${el2.dataset.cms}"`;
|
|
1072
1078
|
console.warn(
|
|
1073
1079
|
`[cancia] ${name} is annotated but has no layout box (${Math.round(rect.width)}\xD7${Math.round(
|
|
1074
1080
|
rect.height
|
|
1075
1081
|
)}) \u2014 it cannot be hovered or clicked in the editor. Avoid sr-only / display:contents on CMS regions; give the container a real box.`,
|
|
1076
|
-
|
|
1082
|
+
el2
|
|
1077
1083
|
);
|
|
1078
1084
|
});
|
|
1079
1085
|
}
|
|
@@ -1699,9 +1705,9 @@ function makePrimaryButton(label2, color) {
|
|
|
1699
1705
|
attachPress(btn);
|
|
1700
1706
|
return btn;
|
|
1701
1707
|
}
|
|
1702
|
-
function applyPopupStyles(
|
|
1703
|
-
markUi(
|
|
1704
|
-
|
|
1708
|
+
function applyPopupStyles(el2) {
|
|
1709
|
+
markUi(el2);
|
|
1710
|
+
el2.style.cssText = `
|
|
1705
1711
|
${surface(2)}
|
|
1706
1712
|
position: absolute;
|
|
1707
1713
|
z-index: ${v("z-panel")};
|
|
@@ -1944,12 +1950,12 @@ function closePopup() {
|
|
|
1944
1950
|
keyListener = null;
|
|
1945
1951
|
}
|
|
1946
1952
|
if (popupEl) {
|
|
1947
|
-
const
|
|
1953
|
+
const el2 = popupEl;
|
|
1948
1954
|
popupEl = null;
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
setTimeout(() =>
|
|
1955
|
+
el2.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}, transform ${v("duration-exit")} ${v("ease-out")}`;
|
|
1956
|
+
el2.style.opacity = "0";
|
|
1957
|
+
el2.style.transform = "scale(0.93)";
|
|
1958
|
+
setTimeout(() => el2.remove(), 200);
|
|
1953
1959
|
}
|
|
1954
1960
|
}
|
|
1955
1961
|
|
|
@@ -2436,15 +2442,15 @@ async function refreshListPanel() {
|
|
|
2436
2442
|
function closeListPanel() {
|
|
2437
2443
|
if (panelEl) {
|
|
2438
2444
|
panelEl.style.animation = `cancia-panel-out ${v("duration-exit")} ${v("ease-out")} forwards`;
|
|
2439
|
-
const
|
|
2440
|
-
setTimeout(() =>
|
|
2445
|
+
const el2 = panelEl;
|
|
2446
|
+
setTimeout(() => el2.remove(), 180);
|
|
2441
2447
|
panelEl = null;
|
|
2442
2448
|
}
|
|
2443
2449
|
if (backdropEl) {
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
setTimeout(() =>
|
|
2450
|
+
const el2 = backdropEl;
|
|
2451
|
+
el2.style.opacity = "0";
|
|
2452
|
+
el2.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}`;
|
|
2453
|
+
setTimeout(() => el2.remove(), 180);
|
|
2448
2454
|
backdropEl = null;
|
|
2449
2455
|
}
|
|
2450
2456
|
currentSchema = null;
|
|
@@ -2740,6 +2746,99 @@ function renderField(field, initial, depth = 0) {
|
|
|
2740
2746
|
getValue2 = () => sel.value === "" ? void 0 : sel.value;
|
|
2741
2747
|
break;
|
|
2742
2748
|
}
|
|
2749
|
+
case "file": {
|
|
2750
|
+
let currentUrl = typeof initial === "string" ? initial : "";
|
|
2751
|
+
const box = document.createElement("div");
|
|
2752
|
+
box.style.cssText = `
|
|
2753
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
2754
|
+
background: ${v("surface-raised")};
|
|
2755
|
+
border: 1px dashed ${v("border-strong")};
|
|
2756
|
+
border-radius: ${v("radius")};
|
|
2757
|
+
padding: 10px;
|
|
2758
|
+
`;
|
|
2759
|
+
const nameRow = document.createElement("div");
|
|
2760
|
+
nameRow.style.cssText = `display:flex; align-items:center; gap:8px; min-width:0;`;
|
|
2761
|
+
const nameLink = document.createElement("a");
|
|
2762
|
+
nameLink.target = "_blank";
|
|
2763
|
+
nameLink.rel = "noopener";
|
|
2764
|
+
nameLink.style.cssText = `
|
|
2765
|
+
font-size: ${v("text-sm")}; color: ${v("fg")}; text-decoration: underline;
|
|
2766
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
|
|
2767
|
+
`;
|
|
2768
|
+
const renderName = (url) => {
|
|
2769
|
+
if (!url) {
|
|
2770
|
+
nameLink.removeAttribute("href");
|
|
2771
|
+
nameLink.textContent = "No file attached";
|
|
2772
|
+
nameLink.style.color = v("fg-faint");
|
|
2773
|
+
nameLink.style.textDecoration = "none";
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
let label2 = url;
|
|
2777
|
+
try {
|
|
2778
|
+
label2 = decodeURIComponent(new URL(url, location.href).pathname.split("/").pop() || url);
|
|
2779
|
+
} catch {
|
|
2780
|
+
}
|
|
2781
|
+
nameLink.href = url;
|
|
2782
|
+
nameLink.textContent = label2;
|
|
2783
|
+
nameLink.style.color = v("fg");
|
|
2784
|
+
nameLink.style.textDecoration = "underline";
|
|
2785
|
+
};
|
|
2786
|
+
renderName(currentUrl);
|
|
2787
|
+
nameRow.appendChild(nameLink);
|
|
2788
|
+
box.appendChild(nameRow);
|
|
2789
|
+
const btnRow = document.createElement("div");
|
|
2790
|
+
btnRow.style.cssText = `display:flex; gap:8px; align-items:center;`;
|
|
2791
|
+
const fileInput = document.createElement("input");
|
|
2792
|
+
fileInput.type = "file";
|
|
2793
|
+
fileInput.accept = "application/pdf";
|
|
2794
|
+
fileInput.style.display = "none";
|
|
2795
|
+
const uploadBtn = document.createElement("button");
|
|
2796
|
+
uploadBtn.type = "button";
|
|
2797
|
+
uploadBtn.textContent = "Upload\u2026";
|
|
2798
|
+
uploadBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
|
|
2799
|
+
attachHover(uploadBtn, { bg: v("surface-active") });
|
|
2800
|
+
uploadBtn.addEventListener("click", () => fileInput.click());
|
|
2801
|
+
const clearBtn = document.createElement("button");
|
|
2802
|
+
clearBtn.type = "button";
|
|
2803
|
+
clearBtn.textContent = "Clear";
|
|
2804
|
+
clearBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
|
|
2805
|
+
attachHover(clearBtn, { bg: v("surface-active") });
|
|
2806
|
+
clearBtn.addEventListener("click", () => {
|
|
2807
|
+
currentUrl = "";
|
|
2808
|
+
renderName("");
|
|
2809
|
+
progressEl.textContent = "";
|
|
2810
|
+
});
|
|
2811
|
+
const progressEl = document.createElement("span");
|
|
2812
|
+
progressEl.style.cssText = `font-size: ${v("text-xs")}; color: ${v("fg-muted")};`;
|
|
2813
|
+
fileInput.addEventListener("change", async () => {
|
|
2814
|
+
const f = fileInput.files?.[0];
|
|
2815
|
+
if (!f) return;
|
|
2816
|
+
uploadBtn.disabled = true;
|
|
2817
|
+
try {
|
|
2818
|
+
const url = await uploadImage(f, (pct) => {
|
|
2819
|
+
progressEl.textContent = `Uploading\u2026 ${pct}%`;
|
|
2820
|
+
});
|
|
2821
|
+
currentUrl = url;
|
|
2822
|
+
renderName(url);
|
|
2823
|
+
progressEl.textContent = "Uploaded";
|
|
2824
|
+
} catch (err) {
|
|
2825
|
+
const msg = err instanceof Error && err.message.includes("(413)") ? "That file is too large." : "Upload failed \u2014 try again.";
|
|
2826
|
+
progressEl.textContent = msg;
|
|
2827
|
+
progressEl.style.color = v("danger");
|
|
2828
|
+
} finally {
|
|
2829
|
+
uploadBtn.disabled = false;
|
|
2830
|
+
fileInput.value = "";
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
btnRow.appendChild(uploadBtn);
|
|
2834
|
+
btnRow.appendChild(clearBtn);
|
|
2835
|
+
btnRow.appendChild(progressEl);
|
|
2836
|
+
box.appendChild(btnRow);
|
|
2837
|
+
box.appendChild(fileInput);
|
|
2838
|
+
wrapper.appendChild(box);
|
|
2839
|
+
getValue2 = () => currentUrl === "" ? void 0 : currentUrl;
|
|
2840
|
+
break;
|
|
2841
|
+
}
|
|
2743
2842
|
case "image": {
|
|
2744
2843
|
const initialUrl = typeof initial === "string" ? initial : "";
|
|
2745
2844
|
let currentUrl = initialUrl;
|
|
@@ -3804,22 +3903,22 @@ function openEntryModal(opts) {
|
|
|
3804
3903
|
}
|
|
3805
3904
|
function closeEntryModal() {
|
|
3806
3905
|
if (modalEl) {
|
|
3807
|
-
const
|
|
3906
|
+
const el2 = modalEl;
|
|
3808
3907
|
modalEl = null;
|
|
3809
3908
|
if (mountedInPanel) {
|
|
3810
|
-
popPanelView(
|
|
3909
|
+
popPanelView(el2);
|
|
3811
3910
|
} else {
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
setTimeout(() =>
|
|
3911
|
+
el2.style.transition = `opacity 0.16s ${v("ease-out")}, transform 0.16s ${v("ease-out")}`;
|
|
3912
|
+
el2.style.opacity = "0";
|
|
3913
|
+
el2.style.transform = "translate(-50%, calc(-50% + 8px)) scale(0.985)";
|
|
3914
|
+
setTimeout(() => el2.remove(), 180);
|
|
3816
3915
|
}
|
|
3817
3916
|
}
|
|
3818
3917
|
mountedInPanel = false;
|
|
3819
3918
|
if (backdropEl2) {
|
|
3820
|
-
const
|
|
3821
|
-
|
|
3822
|
-
setTimeout(() =>
|
|
3919
|
+
const el2 = backdropEl2;
|
|
3920
|
+
el2.style.opacity = "0";
|
|
3921
|
+
setTimeout(() => el2.remove(), 180);
|
|
3823
3922
|
backdropEl2 = null;
|
|
3824
3923
|
}
|
|
3825
3924
|
if (escListener) {
|
|
@@ -4551,6 +4650,141 @@ function unmountToolbar() {
|
|
|
4551
4650
|
}
|
|
4552
4651
|
}
|
|
4553
4652
|
|
|
4653
|
+
// src/login.ts
|
|
4654
|
+
var PANEL_ID = "cancia-login";
|
|
4655
|
+
function el(tag, css, text) {
|
|
4656
|
+
const node = document.createElement(tag);
|
|
4657
|
+
node.style.cssText = css;
|
|
4658
|
+
if (text) node.textContent = text;
|
|
4659
|
+
return node;
|
|
4660
|
+
}
|
|
4661
|
+
function closeLoginPrompt() {
|
|
4662
|
+
document.getElementById(PANEL_ID)?.remove();
|
|
4663
|
+
}
|
|
4664
|
+
function openLoginPrompt() {
|
|
4665
|
+
return new Promise((resolve) => {
|
|
4666
|
+
closeLoginPrompt();
|
|
4667
|
+
const card = el(
|
|
4668
|
+
"div",
|
|
4669
|
+
`position:fixed;bottom:24px;right:24px;z-index:2147483646;width:320px;
|
|
4670
|
+
background:#fff;border:1px solid #e5e5e5;border-radius:12px;
|
|
4671
|
+
box-shadow:0 10px 40px rgba(0,0,0,.15);padding:20px;
|
|
4672
|
+
font:14px/1.5 system-ui,-apple-system,sans-serif;color:#111;`
|
|
4673
|
+
);
|
|
4674
|
+
card.id = PANEL_ID;
|
|
4675
|
+
const title = el("div", "font-weight:600;font-size:15px;margin-bottom:4px;", "Sign in to edit");
|
|
4676
|
+
const blurb = el(
|
|
4677
|
+
"div",
|
|
4678
|
+
"color:#666;font-size:13px;margin-bottom:14px;",
|
|
4679
|
+
"We'll email you a link. No password needed."
|
|
4680
|
+
);
|
|
4681
|
+
const input2 = el(
|
|
4682
|
+
"input",
|
|
4683
|
+
`width:100%;box-sizing:border-box;padding:9px 11px;border:1px solid #d4d4d4;
|
|
4684
|
+
border-radius:8px;font:inherit;margin-bottom:10px;`
|
|
4685
|
+
);
|
|
4686
|
+
input2.type = "email";
|
|
4687
|
+
input2.placeholder = "you@example.com";
|
|
4688
|
+
input2.autocomplete = "email";
|
|
4689
|
+
const button2 = el(
|
|
4690
|
+
"button",
|
|
4691
|
+
`width:100%;padding:9px 12px;border:0;border-radius:8px;background:#111;
|
|
4692
|
+
color:#fff;font:inherit;font-weight:500;cursor:pointer;`,
|
|
4693
|
+
"Email me a link"
|
|
4694
|
+
);
|
|
4695
|
+
const status = el("div", "font-size:13px;margin-top:10px;color:#666;");
|
|
4696
|
+
status.setAttribute("aria-live", "polite");
|
|
4697
|
+
const dismiss = el(
|
|
4698
|
+
"button",
|
|
4699
|
+
`position:absolute;top:10px;right:12px;border:0;background:none;
|
|
4700
|
+
font-size:18px;line-height:1;color:#999;cursor:pointer;padding:2px 6px;`,
|
|
4701
|
+
"\xD7"
|
|
4702
|
+
);
|
|
4703
|
+
dismiss.setAttribute("aria-label", "Close");
|
|
4704
|
+
dismiss.onclick = () => closeLoginPrompt();
|
|
4705
|
+
card.style.position = "fixed";
|
|
4706
|
+
card.append(dismiss, title, blurb, input2, button2, status);
|
|
4707
|
+
document.body.appendChild(card);
|
|
4708
|
+
input2.focus();
|
|
4709
|
+
async function submit() {
|
|
4710
|
+
const email = input2.value.trim();
|
|
4711
|
+
if (!email) {
|
|
4712
|
+
status.textContent = "Enter your email address.";
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
button2.disabled = true;
|
|
4716
|
+
button2.textContent = "Sending\u2026";
|
|
4717
|
+
status.textContent = "";
|
|
4718
|
+
try {
|
|
4719
|
+
const res = await fetch("/api/cancia/auth/request", {
|
|
4720
|
+
method: "POST",
|
|
4721
|
+
headers: { "Content-Type": "application/json" },
|
|
4722
|
+
body: JSON.stringify({ email })
|
|
4723
|
+
});
|
|
4724
|
+
if (res.status === 404) {
|
|
4725
|
+
status.textContent = "Email login is not set up for this site.";
|
|
4726
|
+
button2.disabled = false;
|
|
4727
|
+
button2.textContent = "Email me a link";
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
if (res.status === 429) {
|
|
4731
|
+
status.textContent = "Too many attempts. Wait a minute and try again.";
|
|
4732
|
+
button2.disabled = false;
|
|
4733
|
+
button2.textContent = "Email me a link";
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
input2.remove();
|
|
4737
|
+
button2.remove();
|
|
4738
|
+
blurb.remove();
|
|
4739
|
+
title.textContent = "Check your email";
|
|
4740
|
+
status.textContent = "If that address can edit this site, a link is on its way. It expires in 15 minutes.";
|
|
4741
|
+
status.style.color = "#111";
|
|
4742
|
+
} catch {
|
|
4743
|
+
status.textContent = "Could not reach the server. Check your connection.";
|
|
4744
|
+
button2.disabled = false;
|
|
4745
|
+
button2.textContent = "Email me a link";
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
button2.onclick = submit;
|
|
4749
|
+
input2.onkeydown = (e) => {
|
|
4750
|
+
if (e.key === "Enter") submit();
|
|
4751
|
+
};
|
|
4752
|
+
void resolve;
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
async function redeemLoginToken() {
|
|
4756
|
+
const params = new URLSearchParams(window.location.search);
|
|
4757
|
+
const token = params.get("cancia_login");
|
|
4758
|
+
if (!token) return false;
|
|
4759
|
+
params.delete("cancia_login");
|
|
4760
|
+
const search = params.toString();
|
|
4761
|
+
window.history.replaceState(
|
|
4762
|
+
null,
|
|
4763
|
+
"",
|
|
4764
|
+
window.location.pathname + (search ? `?${search}` : "") + window.location.hash
|
|
4765
|
+
);
|
|
4766
|
+
try {
|
|
4767
|
+
const res = await fetch("/api/cancia/auth/callback", {
|
|
4768
|
+
method: "POST",
|
|
4769
|
+
headers: { "Content-Type": "application/json" },
|
|
4770
|
+
body: JSON.stringify({ token })
|
|
4771
|
+
});
|
|
4772
|
+
return res.ok;
|
|
4773
|
+
} catch {
|
|
4774
|
+
return false;
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
async function hasSession() {
|
|
4778
|
+
try {
|
|
4779
|
+
const res = await fetch("/api/cancia/auth/session", { credentials: "same-origin" });
|
|
4780
|
+
if (!res.ok) return false;
|
|
4781
|
+
const body = await res.json();
|
|
4782
|
+
return body.authenticated === true;
|
|
4783
|
+
} catch {
|
|
4784
|
+
return false;
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4554
4788
|
// src/index.ts
|
|
4555
4789
|
var SESSION_KEY = "cancia_session";
|
|
4556
4790
|
function getSessionToken() {
|
|
@@ -4626,6 +4860,27 @@ async function tryAutoInit() {
|
|
|
4626
4860
|
init(window.__CANCIA__);
|
|
4627
4861
|
return;
|
|
4628
4862
|
}
|
|
4863
|
+
if (window.__CANCIA__.magicLink) {
|
|
4864
|
+
const justSignedIn = await redeemLoginToken();
|
|
4865
|
+
if (justSignedIn || await hasSession()) {
|
|
4866
|
+
closeLoginPrompt();
|
|
4867
|
+
state.onLogout = async () => {
|
|
4868
|
+
try {
|
|
4869
|
+
await fetch("/api/cancia/auth/logout", {
|
|
4870
|
+
method: "POST",
|
|
4871
|
+
credentials: "include"
|
|
4872
|
+
});
|
|
4873
|
+
} catch {
|
|
4874
|
+
}
|
|
4875
|
+
unmountToolbar();
|
|
4876
|
+
};
|
|
4877
|
+
init(window.__CANCIA__);
|
|
4878
|
+
return;
|
|
4879
|
+
}
|
|
4880
|
+
const wantsLogin = new URLSearchParams(window.location.search).has("cancia_login") || window.location.hash === "#cancia-login";
|
|
4881
|
+
if (wantsLogin) void openLoginPrompt();
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4629
4884
|
const token = await handleMagicUrl() ?? getSessionToken();
|
|
4630
4885
|
if (!token) return;
|
|
4631
4886
|
state.sessionToken = token;
|