@panphora/clayjs 1.25.0 → 1.25.1
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/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +4432 -3831
- package/dist/m/attrs/{save-freeze-MH3AOD56.js → save-freeze-PUYX32AK.js} +1 -1
- package/dist/m/{c-XHZIKRCV.js → c-2A2ATCIH.js} +1 -1
- package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
- package/dist/m/{c-JD2VFAYG.js → c-2GB34XL4.js} +2 -2
- package/dist/m/{c-CITLTVQ4.js → c-2TJCFPX4.js} +1 -1
- package/dist/m/c-33SS5A4Y.js +1 -0
- package/dist/m/{c-V2FPBRO6.js → c-34J5HC7N.js} +1 -1
- package/dist/m/{c-ZEA3DCG3.js → c-3AAQJAKJ.js} +1 -1
- package/dist/m/c-3F3AADKG.js +1 -0
- package/dist/m/{c-GDNZEOOZ.js → c-3WX5FABT.js} +1 -1
- package/dist/m/{c-QWB53KRW.js → c-3XNTQQEJ.js} +1 -1
- package/dist/m/{c-AI3H6ETU.js → c-3Y234AUB.js} +2 -2
- package/dist/m/{c-GEAG5XAK.js → c-42RLKGEY.js} +1 -1
- package/dist/m/{c-XQTERKQA.js → c-44WITKWQ.js} +1 -1
- package/dist/m/c-47M4QHCK.js +1 -0
- package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
- package/dist/m/{c-3E2IK5HU.js → c-4NAYDOWH.js} +1 -1
- package/dist/m/{c-TLQINES7.js → c-4OZZ7ENT.js} +2 -2
- package/dist/m/{c-QDVG4NAZ.js → c-5IIEOPIV.js} +1 -1
- package/dist/m/{c-4Q6QIODW.js → c-755ZOHOM.js} +1 -1
- package/dist/m/c-7Q4XYDF4.js +1 -0
- package/dist/m/c-7RHOMAMR.js +1 -0
- package/dist/m/{c-BCWYEG6P.js → c-7Z2625LX.js} +1 -1
- package/dist/m/{c-C6YZNU6E.js → c-A3OKSYRI.js} +1 -1
- package/dist/m/{c-RGAALCDU.js → c-A5IYL7JI.js} +1 -1
- package/dist/m/c-A5LASUEZ.js +1 -0
- package/dist/m/c-AQVQKFKB.js +1 -0
- package/dist/m/{c-IESDKFO6.js → c-ASGHHLFX.js} +1 -1
- package/dist/m/c-ASMLERFA.js +7 -0
- package/dist/m/{c-ORLPQFNU.js → c-AZM34SPO.js} +1 -1
- package/dist/m/{c-TCFVG3I4.js → c-BHLYQGTA.js} +1 -1
- package/dist/m/{c-ZORVIXUR.js → c-C27EVUQ6.js} +1 -1
- package/dist/m/{c-SUAC5RSG.js → c-C5UGNOC2.js} +1 -1
- package/dist/m/c-C6DXZIXD.js +1 -0
- package/dist/m/{c-WSTUCBHV.js → c-CPRGR2II.js} +1 -1
- package/dist/m/{c-NFQFV7OP.js → c-D2OPWOWO.js} +1 -1
- package/dist/m/{c-PRZNJUYZ.js → c-D4N4UJCQ.js} +2 -2
- package/dist/m/{c-D62HRSZK.js → c-DAUQ6JZ3.js} +1 -1
- package/dist/m/{c-5PZYDWTW.js → c-DDCYVLGG.js} +1 -1
- package/dist/m/{c-FBOO2UY3.js → c-DVMDYRAE.js} +1 -1
- package/dist/m/{c-WGQVSKKC.js → c-DYTBSJBI.js} +1 -1
- package/dist/m/{c-O2QZDVZ2.js → c-EEWPNUM2.js} +1 -1
- package/dist/m/{c-MFB74XVN.js → c-EHAR4RJJ.js} +2 -2
- package/dist/m/c-EIDZHQHZ.js +1 -0
- package/dist/m/c-ELXMSEN5.js +1 -0
- package/dist/m/c-FC5RUFPR.js +1 -0
- package/dist/m/{c-V4B3RUWB.js → c-GFASNR55.js} +1 -1
- package/dist/m/{c-4AE2A2LK.js → c-GNFDX6SU.js} +1 -1
- package/dist/m/{c-UD4VDBGJ.js → c-H3M6UDNV.js} +1 -1
- package/dist/m/{c-MUM53J5J.js → c-H4M2T4A6.js} +1 -1
- package/dist/m/{c-U7F66I64.js → c-HYNM5U6B.js} +1 -1
- package/dist/m/c-J2YYLEVM.js +1 -0
- package/dist/m/c-J2ZW4HMR.js +1 -0
- package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
- package/dist/m/c-K2ZHQMPF.js +1 -0
- package/dist/m/{c-Z2OZFX3N.js → c-L6KVXPBO.js} +1 -1
- package/dist/m/{c-JWW6BHWD.js → c-LUBZU4AZ.js} +4 -4
- package/dist/m/{c-SEVOS4UE.js → c-LYNLY3MR.js} +1 -1
- package/dist/m/{c-NADLYVAH.js → c-MHTO3QAD.js} +1 -1
- package/dist/m/{c-SFYTKRU7.js → c-MKAUC6DB.js} +1 -1
- package/dist/m/c-MLMJUNRV.js +1 -0
- package/dist/m/{c-WDFQ7PBU.js → c-MMEVDN6D.js} +1 -1
- package/dist/m/{c-KYTVZT4D.js → c-NW4ZFKCY.js} +1 -1
- package/dist/m/{c-YFO6ZJLQ.js → c-O3DPFZCE.js} +1 -1
- package/dist/m/c-O4YIDULY.js +1 -0
- package/dist/m/c-OZOACVUD.js +1 -0
- package/dist/m/{c-MV7TEUTD.js → c-OZVK3TFI.js} +1 -1
- package/dist/m/c-PP3CUF3U.js +1 -0
- package/dist/m/{c-GY4BREDT.js → c-QL4NYM2L.js} +1 -1
- package/dist/m/c-RES2S5Q3.js +1 -0
- package/dist/m/c-RFGELYEU.js +2 -0
- package/dist/m/c-RGFPLQGV.js +1 -0
- package/dist/m/{c-FKTALXNV.js → c-STAPQ2X6.js} +1 -1
- package/dist/m/c-SYVF6ZMF.js +1 -0
- package/dist/m/{c-X3V4RWMA.js → c-T5IWJOMW.js} +1 -1
- package/dist/m/{c-DT7E76Z2.js → c-T7LOTEWO.js} +1 -1
- package/dist/m/{c-SFZRY6RT.js → c-TWB5ZQU4.js} +1 -1
- package/dist/m/c-U35T5NAY.js +1 -0
- package/dist/m/c-UW6MMZ4V.js +1 -0
- package/dist/m/c-VGG3UR7I.js +1 -0
- package/dist/m/c-VU4WEWPH.js +4 -0
- package/dist/m/{c-5Q5XBN56.js → c-W7LWCJHD.js} +1 -1
- package/dist/m/c-WOJ2RTV4.js +1 -0
- package/dist/m/{c-ZJG6C2WL.js → c-WOWGGNCJ.js} +1 -1
- package/dist/m/{c-SXZYVXD6.js → c-XCOQNISP.js} +1 -1
- package/dist/m/{c-IL5IVTOG.js → c-XS7G6U3Z.js} +1 -1
- package/dist/m/{c-HGG7M2S3.js → c-Y3J5LDKB.js} +1 -1
- package/dist/m/{c-JB5YU57S.js → c-Y3MPVURA.js} +1 -1
- package/dist/m/c-Y6HH7LTM.js +1 -0
- package/dist/m/{c-52OC3OH3.js → c-YRDKVEGR.js} +1 -1
- package/dist/m/{c-WVCT3ITX.js → c-YZGWSCMT.js} +1 -1
- package/dist/m/{c-YX2MC7YL.js → c-ZJ7726FO.js} +1 -1
- package/dist/m/core/{admin-attrs-ZI6K6UGM.js → admin-attrs-43JBA6JB.js} +1 -1
- package/dist/m/core/{autosave-BSM2ABJL.js → autosave-HD7Y7UNV.js} +1 -1
- package/dist/m/core/{conflict-notice-7EQLDYPA.js → conflict-notice-Y2NTMDGJ.js} +1 -1
- package/dist/m/core/{persist-2SZO3PCE.js → persist-MX3VYVBN.js} +1 -1
- package/dist/m/core/{save-K6Q47J2X.js → save-JZ7R4ET2.js} +1 -1
- package/dist/m/core/{save-core-U3Z44XBO.js → save-core-I4N6S4ED.js} +1 -1
- package/dist/m/core/{site-edits-CFMF6GHD.js → site-edits-TIJDPG6H.js} +1 -1
- package/dist/m/core/{snapshot-PWCHYFL6.js → snapshot-N5MKDGNI.js} +1 -1
- package/dist/m/core/{unsaved-warning-2WRZLJH5.js → unsaved-warning-BP6ZY3PG.js} +1 -1
- package/dist/m/internals/{index-FSLC4M7J.js → index-3LAVREAU.js} +1 -1
- package/dist/m/{loader-RC54UV7H.js → loader-7ORQVRCK.js} +1 -1
- package/dist/m/manifest-A27A4278.js +1 -0
- package/dist/m/manifest-edit-E8A61EB4.js +1 -0
- package/dist/m/manifest.json +1099 -1039
- package/dist/m/page/{editor-PKSPJ2XJ.js → editor-HS2CAV3W.js} +5 -5
- package/dist/m/plugins/{ai-edit-3LG6MGS3.js → ai-edit-WDV2GLDU.js} +2 -2
- package/dist/m/plugins/{cms-T2HLI7JG.js → cms-4KET6RZT.js} +9 -9
- package/dist/m/plugins/{derive-7DR5TUDH.js → derive-R4USHZ7P.js} +1 -1
- package/dist/m/plugins/{indicator-CTKX4EY7.js → indicator-OLPZ4WFI.js} +1 -1
- package/dist/m/plugins/{live-IFLL6YN5.js → live-CO4VJS46.js} +1 -1
- package/dist/m/plugins/{menu-YJZ3JWBC.js → menu-ESEFQARM.js} +1 -1
- package/dist/m/plugins/{page-GTDPYGE4.js → page-L3VKIQU2.js} +2 -2
- package/dist/m/plugins/{page-view-NMCCU6MF.js → page-view-6KNTXABH.js} +1 -1
- package/dist/m/plugins/{people-6WIWCZEG.js → people-EBWE337I.js} +1 -1
- package/dist/m/plugins/quickcrop-VNRXGRPC.js +26 -0
- package/dist/m/plugins/{richclay-L3BZMX4I.js → richclay-V6WKJTUF.js} +17 -17
- package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
- package/dist/m/plugins/{source-7LSGOV46.js → source-OYHDXKTB.js} +1 -1
- package/dist/m/plugins/undo-WUK2NLBA.js +1 -0
- package/dist/m/plugins/{upgrade-MMPZUCDM.js → upgrade-SZQJYEQQ.js} +1 -1
- package/dist/m/plugins/{visitor-drafts-V5YB7XDH.js → visitor-drafts-ZE6PCAFP.js} +2 -2
- package/dist/m/plugins/{visitor-menu-QDV2MYJN.js → visitor-menu-5XY57JQA.js} +1 -1
- package/dist/m/plugins/{wire-3YGUO3GE.js → wire-S5ATC4QI.js} +1 -1
- package/dist/m/reader-core-AHOAP5DH.js +77 -0
- package/dist/m/sync/{live-sync-XMXXZS3G.js → live-sync-DX4K5GDB.js} +1 -1
- package/dist/m/ui/index-G7ZJXNMG.js +1 -0
- package/dist/m/vendor/{sap-core.vendor-JANUQULT.js → sap-core.vendor-5TWOJYVM.js} +4 -4
- package/dist/offline-manifest.json +707 -677
- package/dist/server/core.cjs +4245 -2614
- package/dist/server/markdown.cjs +1 -0
- package/dist/server/markdown.mjs +7 -0
- package/docs/guide.md +26 -37
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +5 -5
- package/package.json +6 -2
- package/packed-contract.json +3 -0
- package/src/attrs/save-freeze.js +1 -1
- package/src/core/admin-contenteditable.js +1 -1
- package/src/core/admin-inputs.js +1 -1
- package/src/core/admin-onclick.js +1 -1
- package/src/core/admin-resources.js +5 -2
- package/src/core/conflict-notice-ui.js +59 -34
- package/src/core/save.js +9 -9
- package/src/core/snapshot.js +56 -43
- package/src/core/stale-host-notice.js +20 -7
- package/src/page/editor.js +10 -4
- package/src/page/import-markdown.js +22 -8
- package/src/page/save-projection.js +1 -1
- package/src/plugins/quickcrop.js +128 -69
- package/src/plugins/sortable.js +25 -9
- package/src/server/markdown/index.js +23 -0
- package/src/sync/conflicts.js +3 -1
- package/src/sync/data-guard-notice.js +137 -18
- package/src/ui/bevel-controls.js +4 -1
- package/src/ui/dialogs.js +11 -7
- package/src/ui/modal.js +9 -7
- package/src/ui/toast.js +67 -32
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +7 -7
- package/src/vendor/richclay.vendor.js +16 -16
- package/src/vendor/sap-core.vendor.js +4 -4
- package/tool-generation/context.md +69 -0
- package/tool-generation/system.md +1 -1
- package/dist/m/c-277QP5ET.js +0 -1
- package/dist/m/c-5SZ6KFR5.js +0 -1
- package/dist/m/c-CU33V6RW.js +0 -4
- package/dist/m/c-CVSWLX3X.js +0 -1
- package/dist/m/c-DXPOFLEJ.js +0 -1
- package/dist/m/c-EODTTNVA.js +0 -1
- package/dist/m/c-ESK4S67I.js +0 -1
- package/dist/m/c-FDWR3FL7.js +0 -1
- package/dist/m/c-FJDTDGDA.js +0 -1
- package/dist/m/c-FYDKYW6E.js +0 -1
- package/dist/m/c-HKSLOVU4.js +0 -1
- package/dist/m/c-IJHF4IO6.js +0 -1
- package/dist/m/c-IUL7PU4P.js +0 -1
- package/dist/m/c-K4M76VPM.js +0 -7
- package/dist/m/c-M7ITYKMF.js +0 -1
- package/dist/m/c-MR75YPRT.js +0 -1
- package/dist/m/c-NHXOBD6A.js +0 -1
- package/dist/m/c-NVU3DORS.js +0 -1
- package/dist/m/c-PNZIJFG7.js +0 -1
- package/dist/m/c-RHPHMTF2.js +0 -2
- package/dist/m/c-RWJ3OSCP.js +0 -1
- package/dist/m/c-SGG6MUXW.js +0 -1
- package/dist/m/c-SXNSSHPP.js +0 -1
- package/dist/m/c-TFLRT5JX.js +0 -1
- package/dist/m/c-U65PREMC.js +0 -1
- package/dist/m/c-VMXCX52Q.js +0 -1
- package/dist/m/c-YYGUHSZR.js +0 -1
- package/dist/m/manifest-3B84C829.js +0 -1
- package/dist/m/manifest-edit-1CB79AD4.js +0 -1
- package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
- package/dist/m/plugins/sortable-365SRRL3.js +0 -1
- package/dist/m/plugins/undo-UES4KS7B.js +0 -1
- package/dist/m/reader-core-VR7EL4WV.js +0 -77
- package/dist/m/ui/index-MGTWOW2C.js +0 -1
- /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
- /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-VNRXGRPC.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-L3BZMX4I.js.LEGAL.txt → richclay-V6WKJTUF.js.LEGAL.txt} +0 -0
|
@@ -17,8 +17,8 @@ import { safeHref, safeSrc } from "./address.js";
|
|
|
17
17
|
const EMBEDS = "iframe, frame, frameset, object, embed, applet, meta, base, link, form, style, animate, set, animateMotion, animateTransform, portal";
|
|
18
18
|
const LOADERS = /^(srcdoc|data|formaction|codebase|archive|ping|background|poster|lowsrc|dynsrc)$/i;
|
|
19
19
|
const SCRIPTED = /^\s*(javascript|vbscript|data):/i;
|
|
20
|
-
function cleanImported(html) {
|
|
21
|
-
const t =
|
|
20
|
+
function cleanImported(html, doc) {
|
|
21
|
+
const t = doc.createElement("template");
|
|
22
22
|
t.innerHTML = html;
|
|
23
23
|
for (const el of t.content.querySelectorAll(EMBEDS)) el.remove();
|
|
24
24
|
for (const el of t.content.querySelectorAll("*")) {
|
|
@@ -57,7 +57,19 @@ function added(marks, name, attrs) {
|
|
|
57
57
|
// One text block's inline content as the record keeps it: c, and m as runs (one
|
|
58
58
|
// key per UTF-16 unit, packed), exactly as flatten and runsOf write it; `parts`
|
|
59
59
|
// splits around an image, which the schema only carries as a block of its own.
|
|
60
|
-
|
|
60
|
+
// A link or image written as a relative path (`other.md`, `setup.sh`, `shot.png`) names a
|
|
61
|
+
// file beside the Markdown it came from. It never becomes a web address: `setup.sh` read
|
|
62
|
+
// as a host name is someone else's site, and an image there would make every reader's
|
|
63
|
+
// browser ask that site for it. The host may map it (`resolveLink`, `resolveImage`).
|
|
64
|
+
const RELATIVE = /^(?![a-z][a-z0-9+.-]*:|[/#?])\S/i;
|
|
65
|
+
|
|
66
|
+
function linkHref(raw, resolveLink) {
|
|
67
|
+
const resolved = typeof resolveLink === "function" ? resolveLink(raw) : null;
|
|
68
|
+
if (typeof resolved === "string" && resolved) return safeHref(resolved);
|
|
69
|
+
return RELATIVE.test(raw.trim()) ? null : safeHref(raw);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inlineOf(tokens, { images = true, refused = [], unsupported = [], resolveLink = null } = {}) {
|
|
61
73
|
const parts = [];
|
|
62
74
|
let c = "";
|
|
63
75
|
let m = [];
|
|
@@ -91,7 +103,7 @@ function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}
|
|
|
91
103
|
case "codespan": text(token.text, added(marks, "code")); break;
|
|
92
104
|
case "br": atom(BREAK); break;
|
|
93
105
|
case "link": {
|
|
94
|
-
const href =
|
|
106
|
+
const href = linkHref(String(token.href ?? ""), resolveLink);
|
|
95
107
|
if (href) walk(token.tokens, added(marks, "link", { href }));
|
|
96
108
|
else {
|
|
97
109
|
refused.push({ type: "link", href: String(token.href ?? "") });
|
|
@@ -121,7 +133,7 @@ function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}
|
|
|
121
133
|
return { parts };
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
export function importMarkdownTokens(tokens) {
|
|
136
|
+
export function importMarkdownTokens(tokens, { document: doc = globalThis.document, resolveImage = null, resolveLink = null } = {}) {
|
|
125
137
|
if (!Array.isArray(tokens)) throw new MarkdownImportError("malformed-input", "Markdown import needs the lexer's token list.");
|
|
126
138
|
let size = 0;
|
|
127
139
|
for (const token of tokens) {
|
|
@@ -155,7 +167,9 @@ export function importMarkdownTokens(tokens) {
|
|
|
155
167
|
return value;
|
|
156
168
|
};
|
|
157
169
|
const imageOf = (image) => {
|
|
158
|
-
const
|
|
170
|
+
const resolved = typeof resolveImage === "function" ? resolveImage(image.src) : null;
|
|
171
|
+
const mapped = typeof resolved === "string" && resolved ? resolved : null;
|
|
172
|
+
const src = mapped ? safeSrc(mapped) : RELATIVE.test(String(image.src).trim()) ? null : safeSrc(image.src);
|
|
159
173
|
if (src) return { t: "image", src, ...(image.alt ? { alt: image.alt } : {}), ...(image.title ? { caption: image.title } : {}) };
|
|
160
174
|
refused.push({ type: "image", href: image.src });
|
|
161
175
|
return image.alt ? block("paragraph", {}, { c: image.alt, m: [] }) : null;
|
|
@@ -168,7 +182,7 @@ export function importMarkdownTokens(tokens) {
|
|
|
168
182
|
}
|
|
169
183
|
};
|
|
170
184
|
const paragraph = (inline, ctx) => textBlocks(ctx.quote ? "quote" : "paragraph", {}, inline);
|
|
171
|
-
const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, ...options });
|
|
185
|
+
const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, resolveLink, ...options });
|
|
172
186
|
const literal = (token) => {
|
|
173
187
|
const raw = String(token.raw ?? token.text ?? "");
|
|
174
188
|
unsupported.push({ type: token.type, text: raw });
|
|
@@ -193,7 +207,7 @@ export function importMarkdownTokens(tokens) {
|
|
|
193
207
|
case "html": {
|
|
194
208
|
const raw = String(token.text ?? token.raw ?? "");
|
|
195
209
|
unsupported.push({ type: "HTML block cleaned", text: raw });
|
|
196
|
-
push({ t: "preserved", html: cleanImported(fragmentCleaner(
|
|
210
|
+
push({ t: "preserved", html: cleanImported(fragmentCleaner(doc).fallback(raw), doc) });
|
|
197
211
|
break;
|
|
198
212
|
}
|
|
199
213
|
case "blockquote": {
|
package/src/plugins/quickcrop.js
CHANGED
|
@@ -13,6 +13,9 @@ import { bevelDialog, dismissOf, keepFocusIn } from "../ui/bevel-dialog.js";
|
|
|
13
13
|
import { bevelButton } from "../ui/bevel-controls.js";
|
|
14
14
|
import { installSkin } from "../ui/vendor-skin.js";
|
|
15
15
|
import { CSS as SKIN, ROOTS as SKIN_ROOTS } from "../ui/skins/quickcrop.js";
|
|
16
|
+
import { resolveTheme } from "../ui/theme.js";
|
|
17
|
+
import { themed, partRules, markParts } from "../ui/theme-parts.js";
|
|
18
|
+
import { style } from "../lib/hostile-css.js";
|
|
16
19
|
|
|
17
20
|
const WIDTH = 844;
|
|
18
21
|
|
|
@@ -24,80 +27,136 @@ function isThemodal(m) {
|
|
|
24
27
|
typeof m.onYes === "function" && "html" in m;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
30
|
+
const TITLE = "Crop image";
|
|
31
|
+
|
|
32
|
+
// The frame for one crop, in the theme captured for that crop. Measuring and opening
|
|
33
|
+
// build the same frame, so the stage is sized for the padding, borders, heading and
|
|
34
|
+
// button the person will actually see.
|
|
35
|
+
function cropFrame(theme, confirmLabel, onConfirm = null) {
|
|
36
|
+
const frame = bevelDialog({ zIndex: "2147483001", width: `${WIDTH}px`, closable: true, titled: true, theme, surface: "crop" });
|
|
37
|
+
frame.heading.textContent = TITLE;
|
|
38
|
+
frame.panel.setAttribute("aria-label", TITLE);
|
|
39
|
+
const confirm = bevelButton(confirmLabel, {
|
|
40
|
+
variant: "primary", onClick: onConfirm, theme,
|
|
41
|
+
parts: ["dialog.button", "crop.button"], labelParts: ["dialog.buttonLabel", "crop.buttonLabel"],
|
|
42
|
+
});
|
|
43
|
+
frame.footer.append(confirm);
|
|
44
|
+
return { ...frame, confirm };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The stage is quickcrop's; its parts take only a page's explicit crop.* declarations,
|
|
48
|
+
// so dimming and crop lines keep their contrast unless the page asks otherwise.
|
|
49
|
+
function themeStage(stage, theme) {
|
|
50
|
+
if (!themed(theme)) return;
|
|
51
|
+
const paint = (el, names) => {
|
|
52
|
+
if (!el) return;
|
|
53
|
+
markParts(el, theme, names);
|
|
54
|
+
style(el, partRules(theme, names, {}));
|
|
55
|
+
};
|
|
56
|
+
paint(stage, ["crop.stage"]);
|
|
57
|
+
paint(stage.querySelector(":scope > img"), ["crop.image"]);
|
|
58
|
+
paint(stage.querySelector(".qc-dim"), ["crop.dim"]);
|
|
59
|
+
paint(stage.querySelector(".qc-box"), ["crop.selection"]);
|
|
60
|
+
for (const corner of ["nw", "ne", "sw", "se"]) paint(stage.querySelector(`.qc-handle.qc-${corner}`), ["crop.handle", `crop.handle.${corner}`]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A themed frame is measured: a hidden copy is mounted for one layout read and removed,
|
|
64
|
+
// without touching focus or the scroll lock. The room left for the stage is the body's
|
|
65
|
+
// inner width, and the overlay's inner height less the empty panel. The exact Bevel
|
|
66
|
+
// frame keeps its fixed allowances.
|
|
67
|
+
function measure(theme, confirmLabel) {
|
|
68
|
+
const { root, overlay, panel, body } = cropFrame(theme, confirmLabel);
|
|
69
|
+
root.setAttribute("aria-hidden", "true");
|
|
70
|
+
style(root, ["visibility:hidden", "pointer-events:none"]);
|
|
71
|
+
document.body.append(root);
|
|
72
|
+
try {
|
|
73
|
+
const px = (value) => parseFloat(value) || 0;
|
|
74
|
+
const o = getComputedStyle(overlay), b = getComputedStyle(body);
|
|
75
|
+
const width = body.clientWidth - px(b.paddingLeft) - px(b.paddingRight);
|
|
76
|
+
const height = overlay.clientHeight - px(o.paddingTop) - px(o.paddingBottom) - panel.offsetHeight;
|
|
77
|
+
return { width: Math.max(1, Math.floor(width)), height: Math.max(1, Math.floor(height)) };
|
|
78
|
+
} finally {
|
|
79
|
+
root.remove();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createCropAdapter({ theme = null, confirmLabel = "Crop" } = {}) {
|
|
84
|
+
return {
|
|
85
|
+
// Unthemed, the stage fits inside the panel by fixed allowances: the overlay's 16px
|
|
86
|
+
// side inset, the panel's 1px border and the body's 22px padding a side; below it the
|
|
87
|
+
// header, the footer and the overlay's top inset. Widths come from clientWidth, which
|
|
88
|
+
// leaves out a classic scrollbar as the overlay does.
|
|
89
|
+
fit() {
|
|
90
|
+
if (themed(theme)) return measure(theme, confirmLabel);
|
|
91
|
+
const width = document.documentElement.clientWidth;
|
|
92
|
+
return {
|
|
93
|
+
width: Math.max(120, Math.min(WIDTH, width - 32) - 2 - 44),
|
|
94
|
+
height: Math.max(120, window.innerHeight - Math.min(96, window.innerHeight * 0.1) - 200),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
open({ content, confirmLabel: label, onConfirm, onCancel }) {
|
|
98
|
+
const { root, overlay, panel, body, close, confirm } = cropFrame(theme, label, () => onConfirm());
|
|
99
|
+
themeStage(content, theme);
|
|
100
|
+
body.append(content);
|
|
101
|
+
|
|
102
|
+
close.addEventListener("click", () => onCancel());
|
|
103
|
+
panel.addEventListener("submit", (event) => event.preventDefault());
|
|
104
|
+
let pressedOnBackdrop = false;
|
|
105
|
+
overlay.addEventListener("mousedown", (event) => { pressedOnBackdrop = event.target === overlay; });
|
|
106
|
+
overlay.addEventListener("click", (event) => {
|
|
107
|
+
if (pressedOnBackdrop && event.target === overlay) onCancel();
|
|
108
|
+
pressedOnBackdrop = false;
|
|
109
|
+
});
|
|
110
|
+
// Capture, and stopped there, so a dialog underneath (a CMS confirm through
|
|
111
|
+
// clay.modal) does not take the same key as its own: Escape cancels this crop,
|
|
112
|
+
// Tab stays inside it.
|
|
113
|
+
const onKey = (event) => {
|
|
114
|
+
if (event.key === "Tab") {
|
|
115
|
+
event.stopPropagation();
|
|
116
|
+
keepFocusIn(panel, event);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (event.key !== "Escape") return;
|
|
62
120
|
event.stopPropagation();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
121
|
+
onCancel();
|
|
122
|
+
};
|
|
123
|
+
document.addEventListener("keydown", onKey, true);
|
|
124
|
+
|
|
125
|
+
// The page does not scroll behind the crop, as it did not under themodal. The
|
|
126
|
+
// previous value comes back, so a modal underneath keeps its own lock.
|
|
127
|
+
const overflow = document.body.style.overflow;
|
|
128
|
+
document.body.style.overflow = "hidden";
|
|
129
|
+
|
|
130
|
+
let done = false;
|
|
131
|
+
const finish = () => {
|
|
132
|
+
if (done) return;
|
|
133
|
+
done = true;
|
|
134
|
+
document.removeEventListener("keydown", onKey, true);
|
|
135
|
+
document.body.style.overflow = overflow;
|
|
136
|
+
root.remove();
|
|
137
|
+
};
|
|
138
|
+
// A modal opened on top supersedes this crop the way it supersedes itself: the
|
|
139
|
+
// crop is cancelled, so its promise settles and the next crop can open.
|
|
140
|
+
dismissOf.set(root, () => { finish(); onCancel(); });
|
|
141
|
+
|
|
142
|
+
root.setAttribute("aria-hidden", "false");
|
|
143
|
+
document.body.append(root);
|
|
144
|
+
confirm.focus();
|
|
145
|
+
|
|
146
|
+
return { close: finish };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The exact Bevel frame, for callers that want ClayJS's crop without the page's theme.
|
|
152
|
+
export const bevelCropAdapter = createCropAdapter();
|
|
96
153
|
|
|
97
154
|
export function quickcrop(file, options = {}) {
|
|
98
155
|
const modal = options.modal ?? "auto";
|
|
99
156
|
const framed = modal === "auto" || isThemodal(modal);
|
|
100
|
-
return vendorQuickcrop(file,
|
|
157
|
+
if (!framed) return vendorQuickcrop(file, options);
|
|
158
|
+
const adapter = createCropAdapter({ theme: resolveTheme(), confirmLabel: options.labels?.confirm ?? "Crop" });
|
|
159
|
+
return vendorQuickcrop(file, { ...options, modal: adapter });
|
|
101
160
|
}
|
|
102
161
|
|
|
103
162
|
export default quickcrop;
|
package/src/plugins/sortable.js
CHANGED
|
@@ -217,15 +217,31 @@ function makeSortable(sortableElem, Sortable, record = loose) {
|
|
|
217
217
|
// Add onsorting callback if attribute exists (fires during drag)
|
|
218
218
|
const onsortingCode = sortableElem.getAttribute('onsorting');
|
|
219
219
|
if (onsortingCode) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
// Sortable reads onMove's return value (false cancels the move, 1 or -1 picks a
|
|
221
|
+
// side), so the handler runs synchronously and its value goes back. A handler that
|
|
222
|
+
// awaits cannot answer in time; it still runs, as an async function, and Sortable
|
|
223
|
+
// gets no answer from it.
|
|
224
|
+
// Code that awaits compiles as async: a plain function would read `await (x)` as a
|
|
225
|
+
// call to a function named await and throw on every move.
|
|
226
|
+
let onsorting = null;
|
|
227
|
+
try {
|
|
228
|
+
onsorting = /\bawait\b/.test(onsortingCode)
|
|
229
|
+
? new Function(`return (async function(evt) { ${onsortingCode} })`)()
|
|
230
|
+
: new Function('evt', onsortingCode);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error('Error in onsorting compilation:', error);
|
|
233
|
+
}
|
|
234
|
+
if (onsorting) {
|
|
235
|
+
options.onMove = function(evt) {
|
|
236
|
+
if (!record.admitted) return;
|
|
237
|
+
try {
|
|
238
|
+
const answer = onsorting.call(sortableElem, evt);
|
|
239
|
+
return answer instanceof Promise ? undefined : answer;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('Error in onsorting execution:', error);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
// After a drop, run any author onsorted code, then notify reactive libraries.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Marked } from "../../vendor/marked.vendor.js";
|
|
2
|
+
import { importMarkdownTokens, MarkdownImportError, MAX_BYTES } from "../../page/import-markdown.js";
|
|
3
|
+
import { fragmentCleaner, project, renderModel, revOf } from "../../page/projection.js";
|
|
4
|
+
import { documentTitle } from "../../page/document-title.js";
|
|
5
|
+
|
|
6
|
+
export { MarkdownImportError, MAX_BYTES };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Markdown text as a Block editor article: the inner HTML of `<article data-blockeditor>`,
|
|
10
|
+
* the `data-rev` that article carries, and the title a save would write. The same codec
|
|
11
|
+
* and projection the editor uses, so the editor opens the result as its own page.
|
|
12
|
+
* `document` is any DOM document (jsdom on a server); raw HTML blocks are cleaned with it.
|
|
13
|
+
* `resolveImage(src)` may return a replacement address for an image, or null; `resolveLink(href)`
|
|
14
|
+
* does the same for a link (a link to another local file is otherwise kept as text).
|
|
15
|
+
*/
|
|
16
|
+
export function markdownArticle(text, { document, resolveImage = null, resolveLink = null } = {}) {
|
|
17
|
+
if (!document) throw new TypeError("markdownArticle needs a DOM document");
|
|
18
|
+
const tokens = new Marked().lexer(String(text ?? ""));
|
|
19
|
+
const { blocks, unsupported, refused } = importMarkdownTokens(tokens, { document, resolveImage, resolveLink });
|
|
20
|
+
const model = project(blocks);
|
|
21
|
+
const html = renderModel(model, { fallbackOf: () => "", clean: fragmentCleaner(document) });
|
|
22
|
+
return { html, rev: revOf(model), title: documentTitle(model), unsupported, refused };
|
|
23
|
+
}
|
package/src/sync/conflicts.js
CHANGED
|
@@ -62,8 +62,10 @@ function changed(detail) {
|
|
|
62
62
|
// A report is a loss unless the engine says this tab's operation survived, or a
|
|
63
63
|
// hook kept the incoming change off the page. One the engine could not map to the
|
|
64
64
|
// live page ("missing-output") still replaced this tab's work: it shows, without
|
|
65
|
-
// a place to point at.
|
|
65
|
+
// a place to point at. A region whose incoming version always wins (no-dirty,
|
|
66
|
+
// no-watch) held nothing to keep, so replacing it is never a loss.
|
|
66
67
|
function isLoss(c) {
|
|
68
|
+
if (c.kind === 'structure' && c.detail === 'remote-wins') return false;
|
|
67
69
|
const r = c.recovery;
|
|
68
70
|
if (!r) return true;
|
|
69
71
|
if (r.applied === false && r.unavailable === 'hook-veto') return false;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { saveToken } from "../core/host-attrs.js";
|
|
2
2
|
import { storage } from "../lib/doc-storage.js";
|
|
3
|
+
import { isSaveInProgress } from "../core/save-core.js";
|
|
3
4
|
|
|
4
|
-
const TITLE = "
|
|
5
|
+
const TITLE = "Some saved data was removed";
|
|
6
|
+
const TITLES = {
|
|
7
|
+
recent: "A change from outside this page removed something you typed",
|
|
8
|
+
"all-typed": "A change from outside this page removed everything you typed",
|
|
9
|
+
emptied: "A change from outside this page emptied this page's data",
|
|
10
|
+
unreadable: "A change from outside this page left its data unreadable",
|
|
11
|
+
};
|
|
12
|
+
const TYPED_SHOWN = 3;
|
|
13
|
+
const HOUR_MS = 3600000;
|
|
5
14
|
const DONE_KEY = "data-guard-done";
|
|
6
15
|
const READ_TIMEOUT_MS = 6000;
|
|
7
16
|
const AFTER_SAVE_MS = 1500;
|
|
8
17
|
const MIN_READ_GAP_MS = 5000;
|
|
9
18
|
const VISIBLE_GAP_MS = 30000;
|
|
19
|
+
const EXTERNAL_WINDOW_MS = 10000;
|
|
20
|
+
const SETTLE_CAP_MS = 6000;
|
|
21
|
+
const SAVE_UNSETTLED_EVENTS = ["clay:save-error", "clay:save-offline", "clay:save-conflict"];
|
|
10
22
|
const UNREACHED = "Could not reach the server. Try again.";
|
|
11
23
|
const REFUSED = "That did not go through. Try again.";
|
|
12
24
|
const SAVE_FIRST = "Save your changes first, then try again.";
|
|
@@ -20,7 +32,15 @@ const isPending = (event) => Boolean(event) && typeof event.id === "string" && e
|
|
|
20
32
|
const allowed = (current, event, choice, flag) => flag === true && !current.refused.has(`${event.id}:${choice}`);
|
|
21
33
|
const canRestore = (current, event) => allowed(current, event, "restore", event.restorable);
|
|
22
34
|
const canRevert = (current, event) => allowed(current, event, "revert", event.canRevert);
|
|
35
|
+
const canPutBack = (current, event) => allowed(current, event, "put-back", event.canPutBack);
|
|
36
|
+
const typed = (event) => (Array.isArray(event.typed) ? event.typed : []);
|
|
37
|
+
const reasoned = (event) => Boolean(event.reason);
|
|
38
|
+
const titleFor = (event) => TITLES[event.reason] ?? TITLE;
|
|
39
|
+
const typedLoss = (event) => event.reason === "recent" || event.reason === "all-typed";
|
|
40
|
+
const toneFor = (event) => (typedLoss(event) ? "info" : "warning");
|
|
23
41
|
const credentials = () => (saveToken() ? "omit" : "same-origin");
|
|
42
|
+
const settling = (current) =>
|
|
43
|
+
isSaveInProgress() || (current.lastExternal > 0 && Date.now() - current.lastExternal < EXTERNAL_WINDOW_MS);
|
|
24
44
|
const headers = (extra = {}) => ({ "Document-URL": location.href, ...extra });
|
|
25
45
|
|
|
26
46
|
function removeServerBar() {
|
|
@@ -34,7 +54,7 @@ function say(message, type) {
|
|
|
34
54
|
}, () => {});
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
function
|
|
57
|
+
function sourceOf(event) {
|
|
38
58
|
const summary = event.lossSummary || {};
|
|
39
59
|
const source = summary.provenance === "external" ? "A change from outside this page" : "A save from this page";
|
|
40
60
|
const loss = summary.removedItems > 0
|
|
@@ -43,12 +63,54 @@ function describe(current, event) {
|
|
|
43
63
|
const row = Array.isArray(event.preview) ? event.preview[0] : null;
|
|
44
64
|
const parts = [`${source} ${loss}.`];
|
|
45
65
|
if (row) parts.push(`${row.key}: ${row.yours} before, ${row.now} now.`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
return parts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function labelFor(path) {
|
|
70
|
+
const parts = String(path ?? "").split(".").filter((part) => part && !/^\[[^\]]*\]$/.test(part));
|
|
71
|
+
const last = (parts[parts.length - 1] ?? "").replace(/\[[^\]]*\]/g, "").trim();
|
|
72
|
+
return last.replace(/[_-]+/g, " ") || "value";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function typedLine(event) {
|
|
76
|
+
const rows = typed(event);
|
|
77
|
+
if (!rows.length) return "";
|
|
78
|
+
const shown = rows.slice(0, TYPED_SHOWN).map((row) => `${labelFor(row.path)}: "${row.yours}"`);
|
|
79
|
+
const more = rows.length - shown.length;
|
|
80
|
+
return `${shown.join("; ")}${more > 0 ? ` and ${more} more.` : "."}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function typedWhen(event) {
|
|
84
|
+
if (event.reason === "all-typed") return "";
|
|
85
|
+
const row = typed(event)[0];
|
|
86
|
+
const at = typeof row?.at === "number" ? row.at : Date.parse(row?.at);
|
|
87
|
+
if (!Number.isFinite(at)) return "";
|
|
88
|
+
const ago = Date.now() - at;
|
|
89
|
+
if (ago < 0 || ago >= HOUR_MS) return "";
|
|
90
|
+
const minutes = Math.max(1, Math.floor(ago / 60000));
|
|
91
|
+
return `You typed ${typed(event).length === 1 ? "it" : "them"} ${plural(minutes, "minute")} ago.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detailFor(current, event) {
|
|
95
|
+
if (!reasoned(event)) {
|
|
96
|
+
const parts = sourceOf(event);
|
|
97
|
+
if (canRestore(current, event)) {
|
|
98
|
+
if (event.droppedAdditions > 0) parts.push(`Restoring removes ${plural(event.droppedAdditions, "item")} added or changed since.`);
|
|
99
|
+
} else if (canRevert(current, event)) {
|
|
100
|
+
parts.push("This data no longer fits the page, but the whole change can be undone.");
|
|
101
|
+
} else {
|
|
102
|
+
parts.push("It cannot be restored from here.");
|
|
103
|
+
}
|
|
104
|
+
if (canRevert(current, event)) parts.push("Undo the change also removes edits made since.");
|
|
105
|
+
return parts.join(" ");
|
|
106
|
+
}
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (typedLoss(event)) {
|
|
109
|
+
parts.push(typedLine(event) || sourceOf(event).join(" "));
|
|
110
|
+
const when = typedWhen(event);
|
|
111
|
+
if (when) parts.push(when);
|
|
50
112
|
} else {
|
|
51
|
-
parts.push("
|
|
113
|
+
parts.push(sourceOf(event).join(" "));
|
|
52
114
|
}
|
|
53
115
|
if (canRevert(current, event)) parts.push("Undo the change also removes edits made since.");
|
|
54
116
|
return parts.join(" ");
|
|
@@ -56,14 +118,26 @@ function describe(current, event) {
|
|
|
56
118
|
|
|
57
119
|
function actionsFor(current, event) {
|
|
58
120
|
const actions = [];
|
|
59
|
-
if (
|
|
121
|
+
if (!reasoned(event)) {
|
|
122
|
+
if (canRestore(current, event)) actions.push({ label: "Restore", onClick: () => choose(current, event, "restore") });
|
|
123
|
+
if (canRevert(current, event)) actions.push({ label: "Undo the change", onClick: () => choose(current, event, "revert") });
|
|
124
|
+
actions.push({ label: "Keep as is", onClick: () => choose(current, event, "dismiss") });
|
|
125
|
+
return actions;
|
|
126
|
+
}
|
|
127
|
+
if (canPutBack(current, event)) actions.push({ label: "Put it back", onClick: () => choose(current, event, "put-back") });
|
|
60
128
|
if (canRevert(current, event)) actions.push({ label: "Undo the change", onClick: () => choose(current, event, "revert") });
|
|
61
|
-
actions.push({ label: "Keep
|
|
129
|
+
actions.push({ label: "Keep the change", onClick: () => choose(current, event, "dismiss") });
|
|
62
130
|
return actions;
|
|
63
131
|
}
|
|
64
132
|
|
|
65
133
|
async function draw(current, event) {
|
|
66
|
-
const
|
|
134
|
+
const title = titleFor(event);
|
|
135
|
+
const detail = detailFor(current, event);
|
|
136
|
+
const key = [
|
|
137
|
+
event.id, title, event.reason ?? "", JSON.stringify(typed(event)),
|
|
138
|
+
canRestore(current, event), canPutBack(current, event), canRevert(current, event),
|
|
139
|
+
event.lastWriteAt, detail,
|
|
140
|
+
].join("|");
|
|
67
141
|
if (current.drawn === key) return true;
|
|
68
142
|
current.drawn = key;
|
|
69
143
|
current.eventId = event.id;
|
|
@@ -77,15 +151,20 @@ async function draw(current, event) {
|
|
|
77
151
|
return false;
|
|
78
152
|
}
|
|
79
153
|
if (session !== current || current.drawn !== key) return false;
|
|
80
|
-
|
|
81
|
-
|
|
154
|
+
const previous = current.title;
|
|
155
|
+
current.title = title;
|
|
156
|
+
if (previous && previous !== title) ui.dismissPersistent(previous, { now: true });
|
|
157
|
+
ui.dismissPersistent(title, { now: true });
|
|
158
|
+
ui.toastPersistent(title, toneFor(event), { detail, actions: actionsFor(current, event) });
|
|
82
159
|
return true;
|
|
83
160
|
}
|
|
84
161
|
|
|
85
162
|
function clearNotice(current) {
|
|
163
|
+
const title = current.title;
|
|
86
164
|
current.drawn = null;
|
|
87
165
|
current.eventId = null;
|
|
88
|
-
|
|
166
|
+
current.title = null;
|
|
167
|
+
if (title) ui?.dismissPersistent(title, { now: true });
|
|
89
168
|
}
|
|
90
169
|
|
|
91
170
|
async function read(current) {
|
|
@@ -113,7 +192,11 @@ async function read(current) {
|
|
|
113
192
|
if (session !== current || turn !== current.turn) return;
|
|
114
193
|
if (!body || typeof body !== "object" || !("event" in body)) return;
|
|
115
194
|
if (isPending(body.event)) {
|
|
116
|
-
if (current.resolved.has(body.event.id)) return;
|
|
195
|
+
if (current.resolved.has(body.event.id) || current.waiting === body.event.id) return;
|
|
196
|
+
if (!current.waited.has(body.event.id) && settling(current)) {
|
|
197
|
+
waitForSettle(current, body.event.id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
117
200
|
if (await draw(current, body.event)) removeServerBar();
|
|
118
201
|
return;
|
|
119
202
|
}
|
|
@@ -150,7 +233,7 @@ function settle(current, event, choice, status, body) {
|
|
|
150
233
|
} else if (status === 422) {
|
|
151
234
|
current.refused.add(`${event.id}:${choice}`);
|
|
152
235
|
if (current.eventId === event.id && !current.resolved.has(event.id)) draw(current, event);
|
|
153
|
-
say(choice === "restore" ? "That data no longer fits this page, so it could not be restored." : "That earlier version is no longer available.", "warning");
|
|
236
|
+
say(choice === "restore" || choice === "put-back" ? "That data no longer fits this page, so it could not be restored." : "That earlier version is no longer available.", "warning");
|
|
154
237
|
} else if (status === 401 || status === 403) {
|
|
155
238
|
say(RELOAD_FIRST, "error");
|
|
156
239
|
} else {
|
|
@@ -202,17 +285,44 @@ function onNotification(current, e) {
|
|
|
202
285
|
if (typeof data?.id !== "string") return;
|
|
203
286
|
current.turn++;
|
|
204
287
|
current.resolved.add(data.id);
|
|
288
|
+
if (current.waiting === data.id) stopWaiting(current);
|
|
205
289
|
if (current.eventId === data.id) clearNotice(current);
|
|
206
290
|
return;
|
|
207
291
|
}
|
|
208
292
|
if (action === "raised" && isPending(data) && !current.resolved.has(data.id)) {
|
|
209
293
|
current.turn++;
|
|
294
|
+
if (current.waiting === data.id) return;
|
|
295
|
+
if (!current.waited.has(data.id) && settling(current)) {
|
|
296
|
+
waitForSettle(current, data.id);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
210
299
|
draw(current, data).then((drawn) => { if (drawn) removeServerBar(); }, () => {});
|
|
211
300
|
}
|
|
212
301
|
}
|
|
213
302
|
|
|
214
303
|
const readAt = (current) => Math.max(current.lastSave + AFTER_SAVE_MS, current.lastRead + MIN_READ_GAP_MS);
|
|
215
304
|
|
|
305
|
+
function stopWaiting(current) {
|
|
306
|
+
if (!current.waiting) return false;
|
|
307
|
+
current.waiting = null;
|
|
308
|
+
clearTimeout(current.waitTimer);
|
|
309
|
+
current.waitTimer = null;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function endWait(current, { now = false } = {}) {
|
|
314
|
+
if (!stopWaiting(current) || session !== current) return;
|
|
315
|
+
if (now) read(current);
|
|
316
|
+
else armRead(current);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function waitForSettle(current, id) {
|
|
320
|
+
current.waited.add(id);
|
|
321
|
+
current.waiting = id;
|
|
322
|
+
clearTimeout(current.waitTimer);
|
|
323
|
+
current.waitTimer = setTimeout(() => endWait(current, { now: true }), SETTLE_CAP_MS);
|
|
324
|
+
}
|
|
325
|
+
|
|
216
326
|
function armRead(current) {
|
|
217
327
|
if (current.timer) return;
|
|
218
328
|
current.timer = setTimeout(() => {
|
|
@@ -235,22 +345,30 @@ export function start({ href, reload = () => window.location.reload() }) {
|
|
|
235
345
|
const listeners = new AbortController();
|
|
236
346
|
const current = {
|
|
237
347
|
url: url.href, reload, listeners, turn: 0, lastRead: 0, lastSave: 0, timer: null,
|
|
238
|
-
|
|
348
|
+
lastExternal: 0, waiting: null, waitTimer: null, waited: new Set(),
|
|
349
|
+
busy: false, drawn: null, eventId: null, title: null, resolved: new Set(), refused: new Set(),
|
|
239
350
|
};
|
|
240
351
|
session = current;
|
|
241
352
|
const { signal } = listeners;
|
|
242
353
|
document.addEventListener("clay:notification", (e) => onNotification(current, e), { signal });
|
|
354
|
+
document.addEventListener("clay:sync-applied", (e) => {
|
|
355
|
+
if (e.detail?.source === "disk") current.lastExternal = Date.now();
|
|
356
|
+
}, { signal });
|
|
243
357
|
document.addEventListener("clay:save-saved", () => {
|
|
244
358
|
current.lastSave = Date.now();
|
|
245
|
-
|
|
359
|
+
if (current.waiting) endWait(current);
|
|
360
|
+
else armRead(current);
|
|
246
361
|
}, { signal });
|
|
362
|
+
for (const name of SAVE_UNSETTLED_EVENTS) {
|
|
363
|
+
document.addEventListener(name, () => endWait(current, { now: true }), { signal });
|
|
364
|
+
}
|
|
247
365
|
document.addEventListener("visibilitychange", () => {
|
|
248
366
|
if (document.visibilityState === "visible" && Date.now() - current.lastRead >= VISIBLE_GAP_MS) read(current);
|
|
249
367
|
}, { signal });
|
|
250
368
|
const done = storage.session.get(DONE_KEY);
|
|
251
369
|
if (done) {
|
|
252
370
|
storage.session.remove(DONE_KEY);
|
|
253
|
-
say(done === "restore" ? "Your saved data is back." : "The earlier version is back.", "success");
|
|
371
|
+
say(done === "restore" || done === "put-back" ? "Your saved data is back." : "The earlier version is back.", "success");
|
|
254
372
|
}
|
|
255
373
|
read(current);
|
|
256
374
|
}
|
|
@@ -261,5 +379,6 @@ export function stop() {
|
|
|
261
379
|
session = null;
|
|
262
380
|
current.listeners.abort();
|
|
263
381
|
clearTimeout(current.timer);
|
|
382
|
+
clearTimeout(current.waitTimer);
|
|
264
383
|
clearNotice(current);
|
|
265
384
|
}
|