@panphora/clayjs 1.24.0 → 1.25.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/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 +16610 -14400
- package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
- package/dist/m/attrs/save-freeze-MH3AOD56.js +1 -0
- package/dist/m/c-277QP5ET.js +1 -0
- package/dist/m/{c-SXGEWPFC.js → c-3E2IK5HU.js} +1 -1
- package/dist/m/{c-A4L7APIU.js → c-4AE2A2LK.js} +1 -1
- package/dist/m/{c-DZPO22O3.js → c-4Q6QIODW.js} +1 -1
- package/dist/m/{c-RO73MKKR.js → c-52OC3OH3.js} +1 -1
- package/dist/m/{c-VNAYHV2J.js → c-5PZYDWTW.js} +1 -1
- package/dist/m/c-5Q5XBN56.js +1 -0
- package/dist/m/c-5SZ6KFR5.js +1 -0
- package/dist/m/{c-ZRGEN6QQ.js → c-AI3H6ETU.js} +2 -2
- package/dist/m/{c-7V6MGMX2.js → c-BCWYEG6P.js} +1 -1
- package/dist/m/{c-A7N5TNSL.js → c-BJ2RRQBQ.js} +1 -1
- package/dist/m/{c-CTGRDT6E.js → c-C6YZNU6E.js} +1 -1
- package/dist/m/{c-DLBX4JCJ.js → c-C7SUMNMI.js} +1 -1
- package/dist/m/c-CITLTVQ4.js +4 -0
- package/dist/m/{c-TWOXQQKV.js → c-CL5LSATX.js} +1 -1
- package/dist/m/c-CU33V6RW.js +4 -0
- package/dist/m/{c-2R4OBWIP.js → c-D62HRSZK.js} +1 -1
- package/dist/m/{c-DD26VXAP.js → c-DT7E76Z2.js} +1 -1
- package/dist/m/{c-54ZZZYUN.js → c-EAV7A6BT.js} +1 -1
- package/dist/m/{c-RBPZNU2D.js → c-EQ5NBOGA.js} +1 -1
- package/dist/m/{c-KQKKIVLW.js → c-FBOO2UY3.js} +1 -1
- package/dist/m/{c-RTSRGGTP.js → c-FJDTDGDA.js} +1 -1
- package/dist/m/{c-MQKSKQHG.js → c-FKTALXNV.js} +1 -1
- package/dist/m/{c-GNIVHLEJ.js → c-GDNZEOOZ.js} +1 -1
- package/dist/m/{c-SUXHUL7U.js → c-GEAG5XAK.js} +1 -1
- package/dist/m/{c-UM7VZDTW.js → c-GY4BREDT.js} +1 -1
- package/dist/m/{c-4PO3YGDC.js → c-HGG7M2S3.js} +1 -1
- package/dist/m/c-HKSLOVU4.js +1 -0
- package/dist/m/{c-KJHVB2KG.js → c-HXGNH6GY.js} +1 -1
- package/dist/m/{c-FLEIU6CG.js → c-IESDKFO6.js} +1 -1
- package/dist/m/{c-SNVDWMM3.js → c-IJHF4IO6.js} +1 -1
- package/dist/m/{c-SGGGCIVD.js → c-IL5IVTOG.js} +1 -1
- package/dist/m/c-JB5YU57S.js +1 -0
- package/dist/m/{c-MP6S4AZU.js → c-JD2VFAYG.js} +2 -2
- package/dist/m/{c-CV4T65JE.js → c-JWW6BHWD.js} +3 -3
- package/dist/m/{c-GM7OA6EJ.js → c-K4M76VPM.js} +1 -1
- package/dist/m/{c-RXTPKCMC.js → c-KU3PX7LL.js} +1 -1
- package/dist/m/{c-2PVGJFI5.js → c-KYTVZT4D.js} +1 -1
- package/dist/m/{c-RJKKRWHW.js → c-M7ITYKMF.js} +1 -1
- package/dist/m/c-MFB74XVN.js +3 -0
- package/dist/m/c-MOP5KBWZ.js +1 -0
- package/dist/m/{c-YB3CDTDZ.js → c-MUM53J5J.js} +1 -1
- package/dist/m/{c-YK2L436K.js → c-MV7TEUTD.js} +1 -1
- package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
- package/dist/m/{c-YGGO2G2L.js → c-N4GEJMXW.js} +1 -1
- package/dist/m/{c-NQAPWM7O.js → c-NADLYVAH.js} +1 -1
- package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
- package/dist/m/{c-6UPEKW6O.js → c-O2QZDVZ2.js} +1 -1
- package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
- package/dist/m/{c-LUXAB2XI.js → c-ORLPQFNU.js} +1 -1
- package/dist/m/c-P73XPAOA.js +1 -0
- package/dist/m/{c-PWJSJEJU.js → c-PNZIJFG7.js} +1 -1
- package/dist/m/{c-ZKKAF4UT.js → c-PRZNJUYZ.js} +2 -2
- package/dist/m/{c-Q6VNQDJE.js → c-Q2CB57UG.js} +1 -1
- package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
- package/dist/m/{c-P5NKFRL4.js → c-QDVG4NAZ.js} +1 -1
- package/dist/m/c-QWB53KRW.js +1 -0
- package/dist/m/{c-GNI7WHKF.js → c-RGAALCDU.js} +1 -1
- package/dist/m/c-RHPHMTF2.js +2 -0
- package/dist/m/{c-BULK5J4M.js → c-RWJ3OSCP.js} +1 -1
- package/dist/m/{c-5FXEKU4Y.js → c-SEVOS4UE.js} +1 -1
- package/dist/m/{c-PMRRMT72.js → c-SFYTKRU7.js} +1 -1
- package/dist/m/{c-47IBJGPQ.js → c-SFZRY6RT.js} +1 -1
- package/dist/m/{c-QENKN7QI.js → c-SGG6MUXW.js} +1 -1
- package/dist/m/c-SMLAJASY.js +1 -0
- package/dist/m/{c-VSEUFJAO.js → c-SUAC5RSG.js} +1 -1
- package/dist/m/{c-6RGEQNVV.js → c-SXZYVXD6.js} +1 -1
- package/dist/m/{c-ZFO5RW6G.js → c-TCFVG3I4.js} +1 -1
- package/dist/m/{c-PZRWZZRO.js → c-TLQINES7.js} +2 -2
- package/dist/m/c-U4TWEIKZ.js +1 -0
- package/dist/m/{c-FZRZPIFS.js → c-U65PREMC.js} +1 -1
- package/dist/m/{c-7YETSAVY.js → c-U7F66I64.js} +1 -1
- package/dist/m/{c-WOELSNWS.js → c-UD4VDBGJ.js} +1 -1
- package/dist/m/{c-VI3BJHCM.js → c-V2FPBRO6.js} +1 -1
- package/dist/m/{c-2OIKXOY6.js → c-V4B3RUWB.js} +1 -1
- package/dist/m/{c-O5ARALMJ.js → c-V5L6HWVL.js} +1 -1
- package/dist/m/c-VMXCX52Q.js +1 -0
- package/dist/m/{c-PVJMJL3P.js → c-VO6RSFCO.js} +1 -1
- package/dist/m/c-WDFQ7PBU.js +1 -0
- package/dist/m/{c-OG6GFSUX.js → c-WGQVSKKC.js} +1 -1
- package/dist/m/c-WSTUCBHV.js +1 -0
- package/dist/m/{c-EY5KDUZF.js → c-WVCT3ITX.js} +1 -1
- package/dist/m/{c-SE3SIY7Y.js → c-X3V4RWMA.js} +1 -1
- package/dist/m/{c-HIX5H5L7.js → c-XHZIKRCV.js} +1 -1
- package/dist/m/{c-2BLVA6IN.js → c-XQTERKQA.js} +1 -1
- package/dist/m/{c-A5ZBFICF.js → c-YFO6ZJLQ.js} +1 -1
- package/dist/m/{c-PD5AKHY3.js → c-YX2MC7YL.js} +1 -1
- package/dist/m/{c-OYHI67B6.js → c-YYGUHSZR.js} +1 -1
- package/dist/m/{c-YYMZWQLR.js → c-Z2OZFX3N.js} +1 -1
- package/dist/m/{c-AT7GLQGV.js → c-ZEA3DCG3.js} +1 -1
- package/dist/m/{c-75N3KG35.js → c-ZJG6C2WL.js} +1 -1
- package/dist/m/{c-FVPNX2GO.js → c-ZORVIXUR.js} +1 -1
- package/dist/m/{c-FV46ZKGX.js → c-ZPI7M5U5.js} +1 -1
- package/dist/m/core/{admin-attrs-5UGMYCKW.js → admin-attrs-ZI6K6UGM.js} +1 -1
- package/dist/m/core/{autosave-4P66VXX7.js → autosave-BSM2ABJL.js} +1 -1
- package/dist/m/core/{conflict-notice-BAR23OWH.js → conflict-notice-7EQLDYPA.js} +1 -1
- package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
- package/dist/m/core/{host-permission-3HE4CFD6.js → host-permission-5BYP24GC.js} +1 -1
- package/dist/m/core/{is-edit-mode-GK7IFFMT.js → is-edit-mode-73OXKK5H.js} +1 -1
- package/dist/m/core/{persist-AIJGTKKI.js → persist-2SZO3PCE.js} +1 -1
- package/dist/m/core/{recovery-LPB6X72P.js → recovery-VZF7RJZF.js} +1 -1
- package/dist/m/core/{save-FV4ZCKBI.js → save-K6Q47J2X.js} +1 -1
- package/dist/m/core/{save-core-K7XFAWQ4.js → save-core-U3Z44XBO.js} +1 -1
- package/dist/m/core/{site-edits-TEQORBJU.js → site-edits-CFMF6GHD.js} +1 -1
- package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-PWCHYFL6.js} +1 -1
- package/dist/m/core/unsaved-warning-2WRZLJH5.js +1 -0
- package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
- package/dist/m/internals/index-FSLC4M7J.js +1 -0
- package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
- package/dist/m/loader-RC54UV7H.js +1 -0
- package/dist/m/manifest-3B84C829.js +1 -0
- package/dist/m/manifest-edit-1CB79AD4.js +1 -0
- package/dist/m/manifest.json +1215 -1129
- package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
- package/dist/m/page/{editor-KAZDJNND.js → editor-PKSPJ2XJ.js} +2 -2
- package/dist/m/plugins/{ai-edit-CUKQQWJX.js → ai-edit-3LG6MGS3.js} +1 -1
- package/dist/m/plugins/{cms-JANMGPHM.js → cms-T2HLI7JG.js} +16 -16
- package/dist/m/plugins/{demo-JIR7DXJA.js → demo-LMCMBILC.js} +1 -1
- package/dist/m/plugins/{derive-QVI3LI3A.js → derive-7DR5TUDH.js} +1 -1
- package/dist/m/plugins/{indicator-DAKZGAWI.js → indicator-CTKX4EY7.js} +1 -1
- package/dist/m/plugins/{live-UAJGWRNP.js → live-IFLL6YN5.js} +1 -1
- package/dist/m/plugins/{menu-BFJ644YX.js → menu-YJZ3JWBC.js} +1 -1
- package/dist/m/plugins/page-GTDPYGE4.js +2 -0
- package/dist/m/plugins/{page-view-OYN7Z5TS.js → page-view-NMCCU6MF.js} +1 -1
- package/dist/m/plugins/{people-VBDR5Y22.js → people-6WIWCZEG.js} +1 -1
- package/dist/m/plugins/{richclay-CBLZRFB4.js → richclay-L3BZMX4I.js} +3 -3
- package/dist/m/plugins/sortable-365SRRL3.js +1 -0
- package/dist/m/plugins/{source-ZXDD7F6X.js → source-7LSGOV46.js} +1 -1
- package/dist/m/plugins/{undo-5JI5G7CQ.js → undo-UES4KS7B.js} +1 -1
- package/dist/m/plugins/{upgrade-6B6QS3NR.js → upgrade-MMPZUCDM.js} +1 -1
- package/dist/m/plugins/{upload-DNJNW6H3.js → upload-NZET3DNQ.js} +1 -1
- package/dist/m/plugins/visitor-drafts-V5YB7XDH.js +20 -0
- package/dist/m/plugins/{visitor-drafts-owner-VIAZC3CF.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
- package/dist/m/plugins/{visitor-menu-ZHKWIKKY.js → visitor-menu-QDV2MYJN.js} +1 -1
- package/dist/m/plugins/wire-3YGUO3GE.js +1 -0
- package/dist/m/{reader-core-F6RY3V64.js → reader-core-VR7EL4WV.js} +2 -2
- package/dist/m/sync/live-sync-XMXXZS3G.js +1 -0
- package/dist/m/vendor/sap-core.vendor-JANUQULT.js +7 -0
- package/dist/offline-manifest.json +724 -664
- package/docs/guide.md +1 -0
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +6 -5
- package/package.json +2 -2
- package/src/core/draft-mode.js +11 -0
- package/src/core/save-core.js +16 -17
- package/src/core/save.js +1 -1
- package/src/core/unsaved-warning.js +2 -2
- package/src/lib/dirty-gate.js +19 -0
- package/src/lib/idb-error-code.js +20 -0
- package/src/loader.js +2 -0
- package/src/plugins/menu/visitor.js +33 -1
- package/src/plugins/page.js +19 -5
- package/src/plugins/sortable.js +7 -0
- package/src/plugins/visitor-drafts/commit.js +350 -0
- package/src/plugins/visitor-drafts/idb.js +227 -0
- package/src/plugins/visitor-drafts/prompt.js +23 -3
- package/src/plugins/visitor-drafts/recovery-menu.js +118 -0
- package/src/plugins/visitor-drafts/recovery.js +64 -0
- package/src/plugins/visitor-drafts/session.js +393 -0
- package/src/plugins/visitor-drafts/store.js +247 -27
- package/src/plugins/visitor-drafts.js +177 -28
- package/src/sync/live-sync.js +1 -1
- package/src/sync/wire-profile.js +9 -4
- 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/hypercms.vendor.js +16 -16
- package/src/vendor/richclay.vendor.js +1 -1
- package/src/vendor/sap-core.vendor.js +6 -5
- package/src/vendor/sap-ownership.vendor.js +181 -11
- package/tool-generation/system.md +1 -1
- package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
- package/dist/m/c-234RSEIW.js +0 -1
- package/dist/m/c-2YIJ6ZA6.js +0 -1
- package/dist/m/c-H2H7TI6O.js +0 -1
- package/dist/m/c-HSNGLIFB.js +0 -3
- package/dist/m/c-JMDKPBMC.js +0 -4
- package/dist/m/c-LO4C4J77.js +0 -1
- package/dist/m/c-N7JPUPGC.js +0 -1
- package/dist/m/c-NEXFVJIT.js +0 -1
- package/dist/m/c-R23UHMXG.js +0 -1
- package/dist/m/c-T527QHOB.js +0 -1
- package/dist/m/c-V2U3F4QX.js +0 -1
- package/dist/m/c-VG5XIN4T.js +0 -1
- package/dist/m/c-YINHPF4V.js +0 -1
- package/dist/m/core/draft-mode-WSQAT67S.js +0 -1
- package/dist/m/core/unsaved-warning-FF63QGKI.js +0 -1
- package/dist/m/internals/index-AX4CG7B2.js +0 -1
- package/dist/m/loader-5IA5OJ3J.js +0 -1
- package/dist/m/manifest-98258645.js +0 -1
- package/dist/m/manifest-edit-ABF56000.js +0 -1
- package/dist/m/plugins/page-ZBIYHGK4.js +0 -2
- package/dist/m/plugins/sortable-HDOQ6GAC.js +0 -1
- package/dist/m/plugins/visitor-drafts-A5FDQ2G7.js +0 -20
- package/dist/m/plugins/wire-XOT7XKJL.js +0 -1
- package/dist/m/sync/live-sync-M2PP2IWY.js +0 -1
- package/dist/m/vendor/sap-core.vendor-OA5SKL7Q.js +0 -6
- /package/dist/m/plugins/{richclay-CBLZRFB4.js.LEGAL.txt → richclay-L3BZMX4I.js.LEGAL.txt} +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IndexedDB half of the visitor draft store.
|
|
3
|
+
*
|
|
4
|
+
* One database per origin (`clay-visitor-drafts`), two stores:
|
|
5
|
+
*
|
|
6
|
+
* records key: the draft's text key (`clay:visitor-drafts:<slot>@<path>`) and its
|
|
7
|
+
* siblings `<key>:<name>`; value: JSON text
|
|
8
|
+
* parts key: `[recordKey, regionKey, partId]`; value: `{ rev, text }`
|
|
9
|
+
*
|
|
10
|
+
* Every change to a draft goes through `change()`, one readwrite transaction that reads,
|
|
11
|
+
* merges (the caller's `build`) and writes; see `commit.js`. The only other write is
|
|
12
|
+
* `putRow()`, a tab's own chain left blind at a hide. A transaction resolves only once it
|
|
13
|
+
* completes. Every failure rejects with an Error whose `code` says what happened
|
|
14
|
+
* (`unavailable`, `blocked`, `quota`, `aborted`, `unknown-schema`, `closed`, `bad-op`,
|
|
15
|
+
* `error`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { codeFor } from "../../lib/idb-error-code.js";
|
|
19
|
+
|
|
20
|
+
export const DB_NAME = "clay-visitor-drafts";
|
|
21
|
+
export const DB_VERSION = 1;
|
|
22
|
+
export const RECORDS = "records";
|
|
23
|
+
export const PARTS = "parts";
|
|
24
|
+
const OPEN_TIMEOUT_MS = 2000;
|
|
25
|
+
|
|
26
|
+
function failure(code, cause) {
|
|
27
|
+
const error = new Error(`visitor-drafts: the draft database failed (${code})`);
|
|
28
|
+
error.code = code;
|
|
29
|
+
if (cause !== undefined) error.cause = cause;
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cursorEntries(source, range) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const out = [];
|
|
36
|
+
const req = source.openCursor(range);
|
|
37
|
+
req.onerror = () => reject(req.error);
|
|
38
|
+
req.onsuccess = () => {
|
|
39
|
+
const cursor = req.result;
|
|
40
|
+
if (!cursor) return resolve(out);
|
|
41
|
+
out.push([cursor.key, cursor.value]);
|
|
42
|
+
cursor.continue();
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Open (and, the first time, create) the draft database.
|
|
49
|
+
*
|
|
50
|
+
* A database that does not open within `timeout` milliseconds (another tab holds an
|
|
51
|
+
* upgrade, or the browser never answers) rejects with `blocked`, so the caller can
|
|
52
|
+
* refuse to write over a draft it could not read rather than wait forever.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} [options]
|
|
55
|
+
* @param {?IDBFactory} [options.factory]
|
|
56
|
+
* @param {?typeof IDBKeyRange} [options.keyRange]
|
|
57
|
+
* @returns {Promise<{readAll: Function, readParts: Function, change: Function, putRow: Function, close: Function}>}
|
|
58
|
+
*/
|
|
59
|
+
export function openDrafts({ factory = globalThis.indexedDB, keyRange = globalThis.IDBKeyRange, name = DB_NAME, timeout = OPEN_TIMEOUT_MS } = {}) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
if (!factory || typeof factory.open !== "function" || !keyRange) return reject(failure("unavailable"));
|
|
62
|
+
let req;
|
|
63
|
+
try {
|
|
64
|
+
req = factory.open(name, DB_VERSION);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return reject(failure(codeFor(error), error));
|
|
67
|
+
}
|
|
68
|
+
let settled = false;
|
|
69
|
+
const finish = (fn, value) => {
|
|
70
|
+
if (settled) return false;
|
|
71
|
+
settled = true;
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
fn(value);
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
const timer = setTimeout(() => finish(reject, failure("blocked")), timeout);
|
|
77
|
+
req.onupgradeneeded = () => {
|
|
78
|
+
const db = req.result;
|
|
79
|
+
if (!db.objectStoreNames.contains(RECORDS)) db.createObjectStore(RECORDS);
|
|
80
|
+
if (!db.objectStoreNames.contains(PARTS)) db.createObjectStore(PARTS);
|
|
81
|
+
};
|
|
82
|
+
req.onsuccess = () => {
|
|
83
|
+
const db = req.result;
|
|
84
|
+
// An open that answered after the timeout is closed again, or it would hold the
|
|
85
|
+
// database against another tab's upgrade for as long as this page stays open.
|
|
86
|
+
if (!finish(resolve, wrap(db, keyRange))) db.close();
|
|
87
|
+
};
|
|
88
|
+
req.onerror = () => finish(reject, failure(codeFor(req.error), req.error));
|
|
89
|
+
req.onblocked = () => finish(reject, failure("blocked"));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function wrap(db, keyRange) {
|
|
94
|
+
let closed = false;
|
|
95
|
+
// Another tab opening a newer version needs this connection to stand aside.
|
|
96
|
+
db.onversionchange = () => {
|
|
97
|
+
closed = true;
|
|
98
|
+
try { db.close(); } catch {}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function transaction(stores, mode) {
|
|
102
|
+
if (closed) throw failure("closed");
|
|
103
|
+
try {
|
|
104
|
+
return db.transaction(stores, mode);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw failure(codeFor(error), error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The record named `recordKey` and its siblings (`<recordKey>:<anything>`), as a Map
|
|
111
|
+
// from key to stored text. A key that only starts with `recordKey`, such as another
|
|
112
|
+
// document whose path extends this one's, is not a sibling.
|
|
113
|
+
async function readAll(recordKey) {
|
|
114
|
+
const tx = transaction(RECORDS, "readonly");
|
|
115
|
+
const range = keyRange.bound(recordKey, recordKey + ":");
|
|
116
|
+
try {
|
|
117
|
+
const entries = await cursorEntries(tx.objectStore(RECORDS), range);
|
|
118
|
+
return new Map(entries.filter(([key]) => key === recordKey || key.startsWith(recordKey + ":")));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw failure(codeFor(error), error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The parts of one region of one record, as a Map from part id to `{ rev, text }`.
|
|
125
|
+
// An array sorts after every string, so `[recordKey, regionKey, []]` bounds them all.
|
|
126
|
+
async function readParts(recordKey, regionKey) {
|
|
127
|
+
const tx = transaction(PARTS, "readonly");
|
|
128
|
+
const range = keyRange.bound([recordKey, regionKey], [recordKey, regionKey, []]);
|
|
129
|
+
try {
|
|
130
|
+
const entries = await cursorEntries(tx.objectStore(PARTS), range);
|
|
131
|
+
return new Map(entries.map(([key, value]) => [key[2], value]));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw failure(codeFor(error), error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The one read-merge-write. Reads the record, `:meta`, `:v1base` and every `:op:` row of
|
|
138
|
+
// `key`, hands them to `build` inside the transaction, and writes the `puts` and
|
|
139
|
+
// `deletes` it returns before the transaction can complete. A `build` or a write that
|
|
140
|
+
// throws aborts it, so nothing lands. Resolves with `build`'s `result` on `complete`.
|
|
141
|
+
function change(key, build, { timeout = 0, recovery = false } = {}) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
let tx;
|
|
144
|
+
try {
|
|
145
|
+
tx = transaction([RECORDS, PARTS], "readwrite");
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return reject(error);
|
|
148
|
+
}
|
|
149
|
+
let thrown = null;
|
|
150
|
+
let result;
|
|
151
|
+
let settled = false;
|
|
152
|
+
let timer;
|
|
153
|
+
const finish = (fn, value) => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
fn(value);
|
|
158
|
+
};
|
|
159
|
+
if (timeout > 0) timer = setTimeout(() => {
|
|
160
|
+
thrown = failure("blocked");
|
|
161
|
+
try { tx.abort(); } catch {}
|
|
162
|
+
finish(reject, thrown);
|
|
163
|
+
}, timeout);
|
|
164
|
+
tx.oncomplete = () => finish(resolve, result);
|
|
165
|
+
tx.onabort = () => finish(reject, thrown || failure(tx.error ? codeFor(tx.error) : "aborted", tx.error));
|
|
166
|
+
try {
|
|
167
|
+
const store = tx.objectStore(RECORDS);
|
|
168
|
+
const rows = keyRange.bound(key + ":op:", key + ":op:\uffff");
|
|
169
|
+
const reads = { record: store.get(key), meta: store.get(key + ":meta"), v1base: store.get(key + ":v1base"), names: store.getAllKeys(rows), values: store.getAll(rows) };
|
|
170
|
+
const copies = recovery ? keyRange.bound(key + ":recovery:", key + ":recovery:\uffff") : null;
|
|
171
|
+
if (copies) {
|
|
172
|
+
reads.copyNames = store.getAllKeys(copies);
|
|
173
|
+
reads.copyValues = store.getAll(copies);
|
|
174
|
+
}
|
|
175
|
+
// Requests in one transaction answer in the order they were made: the last one in
|
|
176
|
+
// means every read is.
|
|
177
|
+
(reads.copyValues ?? reads.values).onsuccess = () => {
|
|
178
|
+
if (settled) return;
|
|
179
|
+
try {
|
|
180
|
+
const raw = {
|
|
181
|
+
record: reads.record.result ?? null,
|
|
182
|
+
meta: reads.meta.result ?? null,
|
|
183
|
+
v1base: reads.v1base.result ?? null,
|
|
184
|
+
rows: reads.names.result.map((name, i) => [name, reads.values.result[i]]),
|
|
185
|
+
...(copies && { recovery: reads.copyNames.result.map((name, i) => [name, reads.copyValues.result[i]]) }),
|
|
186
|
+
};
|
|
187
|
+
const planned = build(raw);
|
|
188
|
+
for (const [name, value] of planned.puts) store.put(value, name);
|
|
189
|
+
for (const name of planned.deletes) store.delete(name);
|
|
190
|
+
result = planned.result;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
thrown = failure(codeFor(error), error);
|
|
193
|
+
try { tx.abort(); } catch {}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
} catch (error) {
|
|
197
|
+
thrown = failure(codeFor(error), error);
|
|
198
|
+
try { tx.abort(); } catch {}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// A tab's own unconfirmed chain, written blind (no read) at a hide, with an explicit
|
|
204
|
+
// commit so it needs no callback in a page about to go away. `change()` applies it later.
|
|
205
|
+
function putRow(name, value) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
if (!/:op:[^:]+$/.test(name)) return reject(failure("bad-op"));
|
|
208
|
+
let tx;
|
|
209
|
+
try {
|
|
210
|
+
tx = transaction(RECORDS, "readwrite");
|
|
211
|
+
tx.objectStore(RECORDS).put(value, name);
|
|
212
|
+
tx.commit?.();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return reject(error instanceof Error && error.code ? error : failure(codeFor(error), error));
|
|
215
|
+
}
|
|
216
|
+
tx.oncomplete = () => resolve();
|
|
217
|
+
tx.onabort = () => reject(failure(tx.error ? codeFor(tx.error) : "aborted", tx.error));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function close() {
|
|
222
|
+
closed = true;
|
|
223
|
+
try { db.close(); } catch {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { readAll, readParts, change, putRow, close };
|
|
227
|
+
}
|
|
@@ -21,7 +21,9 @@ const ASIDE = `<aside class="hcp" id="hcp" hidden aria-label="Download this app"
|
|
|
21
21
|
</div>
|
|
22
22
|
</aside>`;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// `settle` returns a promise while the page's last changes are not yet where the download
|
|
25
|
+
// page reads them, and null once they are. False requests a download from this page.
|
|
26
|
+
export async function mountPrompt({ memory, appName, link, settle, canDownloadCurrent = () => true }) {
|
|
25
27
|
const { PROMPT_CSS } = await import("./prompt-style.js");
|
|
26
28
|
const existing = document.querySelector('clay-editor-ui[data-clay-ui="download-prompt"]');
|
|
27
29
|
let root = existing?.shadowRoot.getElementById("hcp");
|
|
@@ -118,6 +120,7 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
let rescueRequested = false;
|
|
121
124
|
root.addEventListener("click", async (e) => {
|
|
122
125
|
const t = e.target.closest("[data-hcp]");
|
|
123
126
|
if (t) {
|
|
@@ -131,9 +134,23 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
131
134
|
return;
|
|
132
135
|
}
|
|
133
136
|
if (e.target.closest("[data-hcp-link]")) {
|
|
134
|
-
if (
|
|
137
|
+
if (!canDownloadCurrent()) return;
|
|
138
|
+
let rescue = rescueRequested || root.getAttribute("data-hcp-reason") === "error";
|
|
139
|
+
if (!rescue) {
|
|
140
|
+
const waiting = settle?.();
|
|
141
|
+
if (waiting) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
if (await waiting !== false) {
|
|
144
|
+
linkEl.click();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
rescue = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (rescue) {
|
|
135
151
|
e.preventDefault();
|
|
136
152
|
if (!(await downloadCurrent())) {
|
|
153
|
+
showError();
|
|
137
154
|
root.querySelector(".hcp-body [data-hcp-for='error']").textContent = "The download could not be made. Keep this page open and try Download free again.";
|
|
138
155
|
return;
|
|
139
156
|
}
|
|
@@ -152,7 +169,10 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
152
169
|
function clearError() {
|
|
153
170
|
if (root.getAttribute("data-hcp-reason") === "error") root.setAttribute("data-hcp-reason", "use");
|
|
154
171
|
}
|
|
155
|
-
const api = { set, root, showError, clearError, download: () =>
|
|
172
|
+
const api = { set, root, showError, clearError, download: (rescue = false) => {
|
|
173
|
+
rescueRequested = rescue;
|
|
174
|
+
try { linkEl.click(); } finally { rescueRequested = false; }
|
|
175
|
+
} };
|
|
156
176
|
|
|
157
177
|
document.addEventListener("input", onUse, true);
|
|
158
178
|
document.addEventListener("change", onUse, true);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { COPY } from "./recovery.js";
|
|
2
|
+
import { bevelBox, bevelButton, bevelText, setShown } from "../../ui/bevel-controls.js";
|
|
3
|
+
import { FONT_MONO } from "../../ui/bevel.js";
|
|
4
|
+
import { resolveTheme } from "../../ui/theme.js";
|
|
5
|
+
import modal, { SURFACE_THEME } from "../../ui/modal.js";
|
|
6
|
+
|
|
7
|
+
export function attachRecoveryMenu(shell, load) {
|
|
8
|
+
let request = 0;
|
|
9
|
+
const refresh = async () => {
|
|
10
|
+
const id = ++request;
|
|
11
|
+
try {
|
|
12
|
+
const recovery = await load();
|
|
13
|
+
const rows = recovery ? await recovery.list() : [];
|
|
14
|
+
if (id !== request) return;
|
|
15
|
+
if (!rows.length) return shell.removeItem("clay.earlier-drafts");
|
|
16
|
+
shell.addItem({ id: "clay.earlier-drafts", label: COPY.label, run: () => openRecovery(recovery, { refresh, anchor: shell.goo }).catch(() => shell.note("The drafts could not be opened. Try again.")) });
|
|
17
|
+
} catch {
|
|
18
|
+
if (id === request) shell.removeItem("clay.earlier-drafts");
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
shell.beforeOpen.push(refresh);
|
|
22
|
+
refresh();
|
|
23
|
+
return refresh;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function openRecovery(recovery, { refresh = () => {}, anchor = document.activeElement } = {}) {
|
|
27
|
+
const theme = resolveTheme();
|
|
28
|
+
const box = bevelBox("div", ["display:flex", "flex-direction:column", "gap:16px"], { theme, parts: ["dialog.content"] });
|
|
29
|
+
box.dataset.draftCopies = "";
|
|
30
|
+
box.append(bevelText("p", ["display:block", "margin:0"], COPY.intro, { theme }));
|
|
31
|
+
const status = bevelText("p", ["display:block", "margin:0"], "", { theme });
|
|
32
|
+
status.setAttribute("role", "status");
|
|
33
|
+
const list = bevelBox("ol", ["display:flex", "flex-direction:column", "gap:16px", "padding:0", "margin:0", "list-style:none"], { theme });
|
|
34
|
+
box.append(status, list);
|
|
35
|
+
let busy = false;
|
|
36
|
+
|
|
37
|
+
async function draw() {
|
|
38
|
+
const rows = await recovery.list();
|
|
39
|
+
list.replaceChildren();
|
|
40
|
+
if (!rows.length) status.textContent = "No earlier drafts are kept.";
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
const item = bevelBox("li", ["display:flex", "flex-direction:column", "gap:8px"], { theme });
|
|
43
|
+
item.dataset.draftMark = row.mark;
|
|
44
|
+
const date = new Date(row.savedAt);
|
|
45
|
+
const when = Number.isNaN(date.getTime()) ? "Time unavailable" : date.toLocaleString();
|
|
46
|
+
const time = bevelText("time", ["display:block", `font-family:${FONT_MONO}`], when, { theme });
|
|
47
|
+
if (row.savedAt) time.dateTime = row.savedAt;
|
|
48
|
+
item.append(time, bevelText("p", ["display:block", "margin:0"], `${row.fields} saved ${row.fields === 1 ? "field" : "fields"}`, { theme, role: "mutedText" }));
|
|
49
|
+
const actions = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px"], { theme });
|
|
50
|
+
const preview = bevelBox("div", ["display:block"], { theme });
|
|
51
|
+
setShown(preview, false);
|
|
52
|
+
const confirm = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px", "align-items:center"], { theme });
|
|
53
|
+
setShown(confirm, false);
|
|
54
|
+
const action = (label, fn, options = {}) => {
|
|
55
|
+
const button = bevelButton(label, { small: true, theme, ...options, onClick: async () => {
|
|
56
|
+
if (busy) return;
|
|
57
|
+
busy = true;
|
|
58
|
+
button.setDisabled(true);
|
|
59
|
+
status.textContent = "";
|
|
60
|
+
try { await fn(button); }
|
|
61
|
+
catch (error) { status.textContent = error.message || "That did not work. Try again."; }
|
|
62
|
+
finally { busy = false; button.setDisabled(false); }
|
|
63
|
+
} });
|
|
64
|
+
button.setAttribute("aria-label", `${label}: ${when}`);
|
|
65
|
+
return button;
|
|
66
|
+
};
|
|
67
|
+
const open = action("Preview", () => {
|
|
68
|
+
if (!preview.hidden) { setShown(preview, false); open.setAttribute("aria-expanded", "false"); return; }
|
|
69
|
+
const doc = new DOMParser().parseFromString(recovery.html(row.copy), "text/html");
|
|
70
|
+
for (const el of doc.querySelectorAll('script, meta[http-equiv="refresh" i]')) el.remove();
|
|
71
|
+
for (const el of doc.querySelectorAll("input, textarea, select, button")) el.disabled = true;
|
|
72
|
+
for (const el of doc.querySelectorAll("[contenteditable]")) el.removeAttribute("contenteditable");
|
|
73
|
+
for (const el of doc.querySelectorAll("a, area")) {
|
|
74
|
+
el.removeAttribute("href");
|
|
75
|
+
el.removeAttributeNS("http://www.w3.org/1999/xlink", "href");
|
|
76
|
+
el.removeAttribute("xlink:href");
|
|
77
|
+
}
|
|
78
|
+
const policy = doc.createElement("meta");
|
|
79
|
+
policy.httpEquiv = "Content-Security-Policy";
|
|
80
|
+
policy.content = "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; form-action 'none'";
|
|
81
|
+
doc.head.prepend(policy);
|
|
82
|
+
const frame = bevelBox("iframe", ["display:block", "width:100%", "height:45vh", "border:0"], { theme });
|
|
83
|
+
frame.title = `Draft preview: ${when}`;
|
|
84
|
+
frame.setAttribute("sandbox", "");
|
|
85
|
+
frame.srcdoc = "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
|
|
86
|
+
preview.replaceChildren(frame);
|
|
87
|
+
setShown(preview, true);
|
|
88
|
+
open.setAttribute("aria-expanded", "true");
|
|
89
|
+
});
|
|
90
|
+
open.setAttribute("aria-expanded", "false");
|
|
91
|
+
const restore = action("Restore", async () => { await recovery.restore(row.mark); status.textContent = "Draft restored. Reloading the page."; await refresh(); });
|
|
92
|
+
const download = action("Download", () => recovery.download(row.copy));
|
|
93
|
+
for (const button of [open, restore, download]) button.setDisabled(!row.available);
|
|
94
|
+
const remove = action("Remove", () => { setShown(confirm, true, "flex"); cancel.focus(); });
|
|
95
|
+
const cancel = bevelButton("Keep draft", { small: true, theme, onClick: () => { setShown(confirm, false); remove.focus(); } });
|
|
96
|
+
confirm.append(bevelText("p", ["margin:0"], "Remove this saved draft?", { theme }), cancel, action("Remove draft", async () => {
|
|
97
|
+
await recovery.remove(row.mark);
|
|
98
|
+
await draw();
|
|
99
|
+
await refresh();
|
|
100
|
+
list.querySelector("button")?.focus();
|
|
101
|
+
}, { variant: "danger" }));
|
|
102
|
+
actions.append(open, restore, download, remove);
|
|
103
|
+
item.append(actions, confirm, preview);
|
|
104
|
+
list.append(item);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await draw();
|
|
109
|
+
modal.title = COPY.label;
|
|
110
|
+
modal.html = box;
|
|
111
|
+
modal.width = "720px";
|
|
112
|
+
modal[SURFACE_THEME] = theme;
|
|
113
|
+
modal.closeHtml = "x";
|
|
114
|
+
modal.no = "Close";
|
|
115
|
+
modal.onNo(() => anchor?.focus());
|
|
116
|
+
modal.open();
|
|
117
|
+
return box;
|
|
118
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { applyData, extractData } from "../../vendor/hyper-html-api-data.vendor.js";
|
|
2
|
+
import { finalizeControlForSave } from "../../vendor/control-serialize.vendor.js";
|
|
3
|
+
import { downloadHtml, documentName } from "../../core/download.js";
|
|
4
|
+
|
|
5
|
+
export const COPY = {
|
|
6
|
+
label: "Earlier drafts",
|
|
7
|
+
intro: "This page kept your earlier drafts aside, listed by time.",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function createRecovery({ store, session, getHTML = () => window.clay.getHTML(), download = downloadHtml }) {
|
|
11
|
+
const recordOf = (copy) => session.recoveryRecord(copy);
|
|
12
|
+
const savedAt = (copy) => copy?.savedAt ?? copy?.ops?.at(-1)?.savedAt ?? (copy?.at ? new Date(copy.at).toISOString() : null);
|
|
13
|
+
|
|
14
|
+
async function list() {
|
|
15
|
+
const rows = await session.listRecovery();
|
|
16
|
+
return rows.map(({ mark, copy }) => {
|
|
17
|
+
const record = recordOf(copy);
|
|
18
|
+
const fields = copy?.ops ? new Set(copy.ops.flatMap((op) => Object.keys(op.keys ?? {}))).size : Object.keys(record?.data ?? {}).length;
|
|
19
|
+
return { mark, copy, savedAt: savedAt(copy), fields, available: Boolean(record) };
|
|
20
|
+
}).sort((a, b) => (Date.parse(b.savedAt) || 0) - (Date.parse(a.savedAt) || 0));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function html(copy) {
|
|
24
|
+
const record = recordOf(copy);
|
|
25
|
+
if (!record) throw new Error("This draft cannot be opened by this version.");
|
|
26
|
+
const doc = new DOMParser().parseFromString(getHTML(), "text/html");
|
|
27
|
+
applyData(doc, store.seed(), store.rules);
|
|
28
|
+
applyData(doc, record.data, store.rules);
|
|
29
|
+
if (!store.fitsInto(record.data, extractData(doc, store.rules))) throw new Error("This draft no longer fits the page. Its saved copy is still kept.");
|
|
30
|
+
const unit = record.blockeditor ? record : store.servedUnit();
|
|
31
|
+
const article = doc.querySelector("article[data-blockeditor]");
|
|
32
|
+
if (unit) {
|
|
33
|
+
if (!article) throw new Error("This draft's page editor is no longer available.");
|
|
34
|
+
for (const tag of doc.querySelectorAll('script[type="application/json"][merge][clay~="live"]')) tag.remove();
|
|
35
|
+
for (const [name, value] of Object.entries(unit.live)) {
|
|
36
|
+
const tag = doc.createElement("script");
|
|
37
|
+
tag.type = "application/json";
|
|
38
|
+
tag.setAttribute("merge", name);
|
|
39
|
+
tag.setAttribute("clay", "live");
|
|
40
|
+
tag.textContent = JSON.stringify(value).replace(/</g, "\\u003c");
|
|
41
|
+
article.before(tag);
|
|
42
|
+
}
|
|
43
|
+
article.setAttribute("data-rev", unit.blockeditor.rev);
|
|
44
|
+
article.innerHTML = unit.blockeditor.html;
|
|
45
|
+
}
|
|
46
|
+
for (const control of doc.querySelectorAll('input:not([type="password"]):not([type="file"]), textarea, select')) finalizeControlForSave(control);
|
|
47
|
+
return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function downloadCopy(copy) {
|
|
51
|
+
const ok = await download({ html: html(copy), name: documentName() + ".htmlclay" });
|
|
52
|
+
if (!ok) throw new Error("The download could not be made. Try again.");
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function restore(mark) {
|
|
57
|
+
const row = (await list()).find((row) => row.mark === mark);
|
|
58
|
+
if (!row) throw new Error("That draft is no longer available. Open the list again.");
|
|
59
|
+
html(row.copy);
|
|
60
|
+
return session.restoreRecovery(mark);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { list, html, download: downloadCopy, restore, remove: (mark) => session.removeRecovery(mark) };
|
|
64
|
+
}
|