@panphora/clayjs 1.24.1 → 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 +6060 -4662
- package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
- package/dist/m/attrs/save-freeze-PUYX32AK.js +1 -0
- package/dist/m/{c-HIX5H5L7.js → c-2A2ATCIH.js} +1 -1
- package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
- package/dist/m/{c-MP6S4AZU.js → c-2GB34XL4.js} +2 -2
- package/dist/m/c-2TJCFPX4.js +4 -0
- package/dist/m/c-33SS5A4Y.js +1 -0
- package/dist/m/{c-PCNJETUA.js → c-34J5HC7N.js} +1 -1
- package/dist/m/{c-AT7GLQGV.js → c-3AAQJAKJ.js} +1 -1
- package/dist/m/c-3F3AADKG.js +1 -0
- package/dist/m/{c-HL6BLJNA.js → c-3WX5FABT.js} +1 -1
- package/dist/m/c-3XNTQQEJ.js +1 -0
- package/dist/m/{c-ZRGEN6QQ.js → c-3Y234AUB.js} +2 -2
- package/dist/m/{c-SUXHUL7U.js → c-42RLKGEY.js} +1 -1
- package/dist/m/{c-2BLVA6IN.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-SXGEWPFC.js → c-4NAYDOWH.js} +1 -1
- package/dist/m/{c-OCYG4NBQ.js → c-4OZZ7ENT.js} +2 -2
- package/dist/m/{c-UOOIF2AL.js → c-5IIEOPIV.js} +1 -1
- package/dist/m/{c-YDW4VCHP.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-7V6MGMX2.js → c-7Z2625LX.js} +1 -1
- package/dist/m/{c-CTGRDT6E.js → c-A3OKSYRI.js} +1 -1
- package/dist/m/{c-GNI7WHKF.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-FLEIU6CG.js → c-ASGHHLFX.js} +1 -1
- package/dist/m/c-ASMLERFA.js +7 -0
- package/dist/m/{c-LUXAB2XI.js → c-AZM34SPO.js} +1 -1
- package/dist/m/{c-ZFO5RW6G.js → c-BHLYQGTA.js} +1 -1
- package/dist/m/{c-C5AKJVRH.js → c-BJ2RRQBQ.js} +1 -1
- package/dist/m/{c-FVPNX2GO.js → c-C27EVUQ6.js} +1 -1
- package/dist/m/{c-DFKBGSN2.js → c-C5UGNOC2.js} +1 -1
- package/dist/m/c-C6DXZIXD.js +1 -0
- package/dist/m/{c-EDE45NM7.js → c-C7SUMNMI.js} +1 -1
- package/dist/m/{c-STRI73GQ.js → c-CL5LSATX.js} +1 -1
- package/dist/m/c-CPRGR2II.js +1 -0
- package/dist/m/{c-NFQFV7OP.js → c-D2OPWOWO.js} +1 -1
- package/dist/m/{c-ZKKAF4UT.js → c-D4N4UJCQ.js} +2 -2
- package/dist/m/{c-6LEYVQJK.js → c-DAUQ6JZ3.js} +1 -1
- package/dist/m/{c-ZGCO4J3H.js → c-DDCYVLGG.js} +1 -1
- package/dist/m/{c-KQKKIVLW.js → c-DVMDYRAE.js} +1 -1
- package/dist/m/{c-OG6GFSUX.js → c-DYTBSJBI.js} +1 -1
- package/dist/m/{c-P2HKMJAW.js → c-EAV7A6BT.js} +1 -1
- package/dist/m/{c-X75G43SZ.js → c-EEWPNUM2.js} +1 -1
- package/dist/m/c-EHAR4RJJ.js +3 -0
- package/dist/m/c-EIDZHQHZ.js +1 -0
- package/dist/m/c-ELXMSEN5.js +1 -0
- package/dist/m/{c-QQFHEODI.js → c-EQ5NBOGA.js} +1 -1
- package/dist/m/c-FC5RUFPR.js +1 -0
- package/dist/m/{c-ND3Q2G2I.js → c-GFASNR55.js} +1 -1
- package/dist/m/{c-A4L7APIU.js → c-GNFDX6SU.js} +1 -1
- package/dist/m/{c-LATNQ6Q5.js → c-H3M6UDNV.js} +1 -1
- package/dist/m/{c-YLHTD3KX.js → c-H4M2T4A6.js} +1 -1
- package/dist/m/{c-DDS23EY6.js → c-HXGNH6GY.js} +1 -1
- package/dist/m/{c-PDBNCJCT.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-6IJEVE6B.js → c-KU3PX7LL.js} +1 -1
- package/dist/m/{c-YYMZWQLR.js → c-L6KVXPBO.js} +1 -1
- package/dist/m/{c-CV4T65JE.js → c-LUBZU4AZ.js} +4 -4
- package/dist/m/{c-5FXEKU4Y.js → c-LYNLY3MR.js} +1 -1
- package/dist/m/{c-NQAPWM7O.js → c-MHTO3QAD.js} +1 -1
- package/dist/m/{c-HDVH3MVC.js → c-MKAUC6DB.js} +1 -1
- package/dist/m/c-MLMJUNRV.js +1 -0
- package/dist/m/c-MMEVDN6D.js +1 -0
- package/dist/m/c-MOP5KBWZ.js +1 -0
- package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
- package/dist/m/{c-XW5TIYYB.js → c-N4GEJMXW.js} +1 -1
- package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
- package/dist/m/{c-2PVGJFI5.js → c-NW4ZFKCY.js} +1 -1
- package/dist/m/{c-PYG2NGWU.js → c-O3DPFZCE.js} +1 -1
- package/dist/m/c-O4YIDULY.js +1 -0
- package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
- package/dist/m/c-OZOACVUD.js +1 -0
- package/dist/m/{c-YK2L436K.js → c-OZVK3TFI.js} +1 -1
- package/dist/m/c-P73XPAOA.js +1 -0
- package/dist/m/c-PP3CUF3U.js +1 -0
- package/dist/m/{c-B7GOFGQR.js → c-Q2CB57UG.js} +1 -1
- package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
- package/dist/m/{c-LSVN3OLG.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-SMLAJASY.js +1 -0
- package/dist/m/{c-MQKSKQHG.js → c-STAPQ2X6.js} +1 -1
- package/dist/m/c-SYVF6ZMF.js +1 -0
- package/dist/m/{c-SE3SIY7Y.js → c-T5IWJOMW.js} +1 -1
- package/dist/m/{c-DD26VXAP.js → c-T7LOTEWO.js} +1 -1
- package/dist/m/{c-47IBJGPQ.js → c-TWB5ZQU4.js} +1 -1
- package/dist/m/c-U35T5NAY.js +1 -0
- package/dist/m/c-U4TWEIKZ.js +1 -0
- package/dist/m/c-UW6MMZ4V.js +1 -0
- package/dist/m/{c-JWDCQL6J.js → c-V5L6HWVL.js} +1 -1
- package/dist/m/c-VGG3UR7I.js +1 -0
- package/dist/m/{c-CZHTXGJS.js → c-VO6RSFCO.js} +1 -1
- package/dist/m/c-VU4WEWPH.js +4 -0
- package/dist/m/c-W7LWCJHD.js +1 -0
- package/dist/m/c-WOJ2RTV4.js +1 -0
- package/dist/m/{c-T3P3AXEW.js → c-WOWGGNCJ.js} +1 -1
- package/dist/m/{c-6RGEQNVV.js → c-XCOQNISP.js} +1 -1
- package/dist/m/{c-SGGGCIVD.js → c-XS7G6U3Z.js} +1 -1
- package/dist/m/{c-4PO3YGDC.js → c-Y3J5LDKB.js} +1 -1
- package/dist/m/c-Y3MPVURA.js +1 -0
- package/dist/m/c-Y6HH7LTM.js +1 -0
- package/dist/m/{c-RO73MKKR.js → c-YRDKVEGR.js} +1 -1
- package/dist/m/{c-FEC6LGFF.js → c-YZGWSCMT.js} +1 -1
- package/dist/m/{c-7CM4GMTS.js → c-ZJ7726FO.js} +1 -1
- package/dist/m/{c-SKMIR5HQ.js → c-ZPI7M5U5.js} +1 -1
- package/dist/m/core/{admin-attrs-ONRWVXPK.js → admin-attrs-43JBA6JB.js} +1 -1
- package/dist/m/core/{autosave-TBKAAAFB.js → autosave-HD7Y7UNV.js} +1 -1
- package/dist/m/core/{conflict-notice-FOEE6I7E.js → conflict-notice-Y2NTMDGJ.js} +1 -1
- package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
- package/dist/m/core/{host-permission-X4BENZEU.js → host-permission-5BYP24GC.js} +1 -1
- package/dist/m/core/{is-edit-mode-FFHCQ5DR.js → is-edit-mode-73OXKK5H.js} +1 -1
- package/dist/m/core/{persist-6ILUR34S.js → persist-MX3VYVBN.js} +1 -1
- package/dist/m/core/{recovery-APWADU4K.js → recovery-VZF7RJZF.js} +1 -1
- package/dist/m/core/{save-6HT2LABE.js → save-JZ7R4ET2.js} +1 -1
- package/dist/m/core/{save-core-EVMNZQ4V.js → save-core-I4N6S4ED.js} +1 -1
- package/dist/m/core/{site-edits-G4BOZMV4.js → site-edits-TIJDPG6H.js} +1 -1
- package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-N5MKDGNI.js} +1 -1
- package/dist/m/core/unsaved-warning-BP6ZY3PG.js +1 -0
- package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
- package/dist/m/internals/index-3LAVREAU.js +1 -0
- package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
- package/dist/m/{loader-GLLVDUZA.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 +1608 -1498
- package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
- package/dist/m/page/{editor-KAZDJNND.js → editor-HS2CAV3W.js} +5 -5
- package/dist/m/plugins/{ai-edit-LABECCSN.js → ai-edit-WDV2GLDU.js} +2 -2
- package/dist/m/plugins/{cms-T2HLI7JG.js → cms-4KET6RZT.js} +9 -9
- package/dist/m/plugins/{demo-T7B4RIKL.js → demo-LMCMBILC.js} +1 -1
- package/dist/m/plugins/{derive-6LPJOCAV.js → derive-R4USHZ7P.js} +1 -1
- package/dist/m/plugins/{indicator-J4ABFARX.js → indicator-OLPZ4WFI.js} +1 -1
- package/dist/m/plugins/{live-PGBIEMGB.js → live-CO4VJS46.js} +1 -1
- package/dist/m/plugins/{menu-YZL3X2AJ.js → menu-ESEFQARM.js} +1 -1
- package/dist/m/plugins/{page-WOCNX2IN.js → page-L3VKIQU2.js} +2 -2
- package/dist/m/plugins/{page-view-SJODOOGE.js → page-view-6KNTXABH.js} +1 -1
- package/dist/m/plugins/{people-C4TCWFOQ.js → people-EBWE337I.js} +1 -1
- package/dist/m/plugins/quickcrop-VNRXGRPC.js +26 -0
- package/dist/m/plugins/{richclay-Y2NV44NA.js → richclay-V6WKJTUF.js} +17 -17
- package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
- package/dist/m/plugins/{source-YK7MO23W.js → source-OYHDXKTB.js} +1 -1
- package/dist/m/plugins/undo-WUK2NLBA.js +1 -0
- package/dist/m/plugins/{upgrade-NNTJBSK2.js → upgrade-SZQJYEQQ.js} +1 -1
- package/dist/m/plugins/{upload-ZKKJV76X.js → upload-NZET3DNQ.js} +1 -1
- package/dist/m/plugins/visitor-drafts-ZE6PCAFP.js +20 -0
- package/dist/m/plugins/{visitor-drafts-owner-LHUHFU3L.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
- package/dist/m/plugins/{visitor-menu-FY4KM2SW.js → visitor-menu-5XY57JQA.js} +1 -1
- package/dist/m/plugins/wire-S5ATC4QI.js +1 -0
- package/dist/m/reader-core-AHOAP5DH.js +77 -0
- package/dist/m/sync/live-sync-DX4K5GDB.js +1 -0
- package/dist/m/ui/index-G7ZJXNMG.js +1 -0
- package/dist/m/vendor/sap-core.vendor-5TWOJYVM.js +7 -0
- package/dist/offline-manifest.json +844 -784
- 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 +27 -37
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +6 -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/draft-mode.js +5 -0
- package/src/core/save-core.js +10 -10
- package/src/core/save.js +10 -10
- package/src/core/snapshot.js +56 -43
- package/src/core/stale-host-notice.js +20 -7
- package/src/lib/dirty-gate.js +19 -0
- 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/menu/visitor.js +33 -1
- package/src/plugins/quickcrop.js +128 -69
- package/src/plugins/sortable.js +25 -9
- package/src/plugins/visitor-drafts/commit.js +50 -3
- package/src/plugins/visitor-drafts/idb.js +8 -2
- 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 +61 -13
- package/src/plugins/visitor-drafts/store.js +44 -8
- package/src/plugins/visitor-drafts.js +34 -5
- 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/sync/live-sync.js +1 -1
- package/src/sync/wire-profile.js +9 -4
- 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 +6 -5
- package/src/vendor/sap-ownership.vendor.js +181 -11
- package/tool-generation/context.md +69 -0
- package/tool-generation/system.md +2 -2
- package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
- package/dist/m/c-277QP5ET.js +0 -1
- package/dist/m/c-3UW5EJ77.js +0 -1
- package/dist/m/c-42ZNV7LI.js +0 -7
- package/dist/m/c-6Q6RGFBF.js +0 -1
- package/dist/m/c-APGHGDZM.js +0 -1
- package/dist/m/c-BGN7RCTJ.js +0 -1
- package/dist/m/c-CJ7QYEWI.js +0 -1
- package/dist/m/c-CVSWLX3X.js +0 -1
- package/dist/m/c-DXPOFLEJ.js +0 -1
- package/dist/m/c-EILLN2H2.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-FYDKYW6E.js +0 -1
- package/dist/m/c-HKSLOVU4.js +0 -1
- package/dist/m/c-HTLYXCUH.js +0 -1
- package/dist/m/c-IUL7PU4P.js +0 -1
- package/dist/m/c-JQF2EUCG.js +0 -1
- package/dist/m/c-MFOSMAVM.js +0 -3
- package/dist/m/c-MQYEDF3Q.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-OYHI67B6.js +0 -1
- package/dist/m/c-PWJSJEJU.js +0 -1
- package/dist/m/c-QENKN7QI.js +0 -1
- package/dist/m/c-R23UHMXG.js +0 -1
- package/dist/m/c-SGH3UTF3.js +0 -1
- package/dist/m/c-SXNSSHPP.js +0 -1
- package/dist/m/c-TFLRT5JX.js +0 -1
- package/dist/m/c-U4NTUVDZ.js +0 -1
- package/dist/m/c-V2U3F4QX.js +0 -1
- package/dist/m/c-VG5XIN4T.js +0 -1
- package/dist/m/c-VHSMPITY.js +0 -1
- package/dist/m/c-Z4G2UK6T.js +0 -4
- package/dist/m/core/draft-mode-4MCL3LYI.js +0 -1
- package/dist/m/core/unsaved-warning-ITZDIZ2I.js +0 -1
- package/dist/m/internals/index-HS3CQDFW.js +0 -1
- package/dist/m/manifest-50FF1C45.js +0 -1
- package/dist/m/manifest-edit-BE1A621D.js +0 -1
- package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
- package/dist/m/plugins/sortable-XCHQG65Z.js +0 -1
- package/dist/m/plugins/undo-VLRBQNPN.js +0 -1
- package/dist/m/plugins/visitor-drafts-RTZNONHT.js +0 -20
- package/dist/m/plugins/wire-M2NR354P.js +0 -1
- package/dist/m/reader-core-T2S3JC4E.js +0 -77
- package/dist/m/sync/live-sync-Z64YREWQ.js +0 -1
- package/dist/m/ui/index-MGTWOW2C.js +0 -1
- package/dist/m/vendor/sap-core.vendor-QAEPMETX.js +0 -6
- /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-Y2NV44NA.js.LEGAL.txt → richclay-V6WKJTUF.js.LEGAL.txt} +0 -0
|
@@ -138,7 +138,7 @@ function wrap(db, keyRange) {
|
|
|
138
138
|
// `key`, hands them to `build` inside the transaction, and writes the `puts` and
|
|
139
139
|
// `deletes` it returns before the transaction can complete. A `build` or a write that
|
|
140
140
|
// throws aborts it, so nothing lands. Resolves with `build`'s `result` on `complete`.
|
|
141
|
-
function change(key, build, { timeout = 0 } = {}) {
|
|
141
|
+
function change(key, build, { timeout = 0, recovery = false } = {}) {
|
|
142
142
|
return new Promise((resolve, reject) => {
|
|
143
143
|
let tx;
|
|
144
144
|
try {
|
|
@@ -167,9 +167,14 @@ function wrap(db, keyRange) {
|
|
|
167
167
|
const store = tx.objectStore(RECORDS);
|
|
168
168
|
const rows = keyRange.bound(key + ":op:", key + ":op:\uffff");
|
|
169
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
|
+
}
|
|
170
175
|
// Requests in one transaction answer in the order they were made: the last one in
|
|
171
176
|
// means every read is.
|
|
172
|
-
reads.values.onsuccess = () => {
|
|
177
|
+
(reads.copyValues ?? reads.values).onsuccess = () => {
|
|
173
178
|
if (settled) return;
|
|
174
179
|
try {
|
|
175
180
|
const raw = {
|
|
@@ -177,6 +182,7 @@ function wrap(db, keyRange) {
|
|
|
177
182
|
meta: reads.meta.result ?? null,
|
|
178
183
|
v1base: reads.v1base.result ?? null,
|
|
179
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]]) }),
|
|
180
186
|
};
|
|
181
187
|
const planned = build(raw);
|
|
182
188
|
for (const [name, value] of planned.puts) store.put(value, name);
|
|
@@ -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
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { openDrafts } from "./idb.js";
|
|
23
|
-
import { emptyMeta, foreignMark, isForeign, markerFor, markerGen, recoveryKey, recoveryMark, rowKey, settled, transact } from "./commit.js";
|
|
23
|
+
import { emptyMeta, foreignMark, isChain, isForeign, markerFor, markerGen, recoveryKey, recoveryMark, recoveryRecord, rowKey, settled, transact } from "./commit.js";
|
|
24
24
|
import { setDraftStoreFailed } from "../../core/draft-mode.js";
|
|
25
25
|
|
|
26
26
|
export const FULL_COPY_LIMIT = 256 * 1024;
|
|
@@ -91,6 +91,7 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
91
91
|
let sent = null;
|
|
92
92
|
let carried = 0;
|
|
93
93
|
let retiring = false;
|
|
94
|
+
let restoringCopy = false;
|
|
94
95
|
let shownId = store.held()?.id;
|
|
95
96
|
// The latest full cache copy this tab saw at the key, and the text of another writer's
|
|
96
97
|
// record a storage event says replaced it: such a record merges from that copy.
|
|
@@ -127,6 +128,7 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
127
128
|
seed: store.seed(),
|
|
128
129
|
newId: store.newId,
|
|
129
130
|
merge3: store.merge3,
|
|
131
|
+
same: store.same,
|
|
130
132
|
unitMark: store.unitMark,
|
|
131
133
|
now: now(),
|
|
132
134
|
marker: held,
|
|
@@ -139,12 +141,12 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
// Another tab's slot is removed once nothing more can come of it and its tab has gone quiet.
|
|
142
|
-
function sweep(meta) {
|
|
144
|
+
function sweep(meta, removedMark) {
|
|
143
145
|
const at = now();
|
|
144
146
|
for (const name of store.journalSlots()) {
|
|
145
147
|
if (name === store.slotKey) continue;
|
|
146
148
|
const chain = parse(read(name));
|
|
147
|
-
if (settled(chain, meta) && at - (chain?.at ?? 0) > SLOT_AGE_MS) remove(name);
|
|
149
|
+
if ((removedMark && isChain(chain) && recoveryMark(chain) === removedMark) || (settled(chain, meta) && at - (chain?.at ?? 0) > SLOT_AGE_MS)) remove(name);
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -169,20 +171,26 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
169
171
|
try { storage.setItem(key, headOf(record, w)); } catch {}
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
async function stopWithPending() {
|
|
175
|
+
store.stop({ keepPending: true });
|
|
176
|
+
if (store.unsafe()) {
|
|
177
|
+
try { await drafts.putRow(rowKey(key, store.tab), JSON.stringify(store.chain())); }
|
|
178
|
+
catch (error) { throw store.failed(error); }
|
|
179
|
+
store.acknowledge(store.chain().ops.at(-1)?.seq ?? 0);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
172
183
|
// Operations captured after the transaction read stay journaled for the next load.
|
|
173
184
|
async function ended(result) {
|
|
185
|
+
try { store.captureLate(); }
|
|
186
|
+
catch (error) { throw store.failed(error); }
|
|
174
187
|
retiring = true;
|
|
175
188
|
const chain = store.chain();
|
|
176
189
|
const mark = recoveryMark(chain);
|
|
177
190
|
const kept = chain.ops.some((op) => op.seq > carried) || result.meta.recovery?.includes(mark)
|
|
178
191
|
? mark : result.meta.recovery?.includes(shownId) ? shownId : null;
|
|
179
192
|
store.acknowledge(carried);
|
|
180
|
-
|
|
181
|
-
if (store.unsafe()) {
|
|
182
|
-
try { await drafts.putRow(rowKey(key, store.tab), JSON.stringify(store.chain())); }
|
|
183
|
-
catch (error) { throw store.failed(error); }
|
|
184
|
-
store.acknowledge(store.chain().ops.at(-1)?.seq ?? 0);
|
|
185
|
-
}
|
|
193
|
+
await stopWithPending();
|
|
186
194
|
cache(result);
|
|
187
195
|
if (kept) rememberRecovery(kept);
|
|
188
196
|
const held = store.held();
|
|
@@ -277,13 +285,14 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
277
285
|
gen = meta.gen;
|
|
278
286
|
rememberGeneration(meta.reset);
|
|
279
287
|
const saved = store.acknowledge(meta.tabs[store.tab]?.seq ?? 0);
|
|
280
|
-
sweep(meta);
|
|
288
|
+
sweep(meta, result.removedMark);
|
|
281
289
|
cache(result);
|
|
282
290
|
if (store.pending()) return result;
|
|
283
291
|
if (saved) {
|
|
284
292
|
setDraftStoreFailed(false);
|
|
285
293
|
doc.dispatchEvent(new CustomEvent("clay:drafts-saved", { detail: { data: record.data } }));
|
|
286
294
|
}
|
|
295
|
+
if (restoringCopy) return result;
|
|
287
296
|
if (!store.shown()) {
|
|
288
297
|
rev = meta.rev;
|
|
289
298
|
return result;
|
|
@@ -303,8 +312,15 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
303
312
|
// so it carries every operation made before then.
|
|
304
313
|
let active = null;
|
|
305
314
|
let waiting = null;
|
|
306
|
-
function run(options) {
|
|
307
|
-
|
|
315
|
+
function run(options, recovery = null) {
|
|
316
|
+
const given = () => {
|
|
317
|
+
if (recovery?.type === "restore") {
|
|
318
|
+
if (store.stopped()) throw new Error("This page has started over. Reload it before restoring a draft.");
|
|
319
|
+
store.captureNow();
|
|
320
|
+
}
|
|
321
|
+
return { ...input(), recovery };
|
|
322
|
+
};
|
|
323
|
+
active = transact(drafts, key, given, options).then(took, (error) => {
|
|
308
324
|
throw store.pending() ? store.failed(error) : error;
|
|
309
325
|
}).finally(() => { active = null; });
|
|
310
326
|
return active;
|
|
@@ -318,6 +334,38 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
318
334
|
});
|
|
319
335
|
return waiting;
|
|
320
336
|
}
|
|
337
|
+
async function changeRecovery(recovery) {
|
|
338
|
+
while (active || waiting) await (waiting ?? active).catch(() => {});
|
|
339
|
+
return run({ recovery: true }, recovery);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function restoreRecovery(mark) {
|
|
343
|
+
if (restoringCopy) throw new Error("A draft is already being restored.");
|
|
344
|
+
restoringCopy = true;
|
|
345
|
+
try {
|
|
346
|
+
if (store.stopped()) throw new Error("This page has started over. Reload it before restoring a draft.");
|
|
347
|
+
store.captureNow();
|
|
348
|
+
let result;
|
|
349
|
+
do {
|
|
350
|
+
result = await changeRecovery({ type: "restore", mark, reset: store.generation() });
|
|
351
|
+
if (!result.restoredCopy || store.stopped()) throw new Error("This page has started over. Reload it before restoring a draft.");
|
|
352
|
+
} while (store.pending());
|
|
353
|
+
// This tab's pre-restore page is retired without ending the other tabs' generation.
|
|
354
|
+
try { store.captureLate("restored:" + store.tab); }
|
|
355
|
+
catch (error) { throw store.failed(error); }
|
|
356
|
+
await stopWithPending();
|
|
357
|
+
reload();
|
|
358
|
+
return result;
|
|
359
|
+
} finally { restoringCopy = false; }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function listRecovery() {
|
|
363
|
+
await first.catch(() => {});
|
|
364
|
+
const prefix = key + ":recovery:";
|
|
365
|
+
const rows = await drafts.readAll(key);
|
|
366
|
+
return [...rows].filter(([name]) => name.startsWith(prefix)).map(([name, raw]) => ({ mark: name.slice(prefix.length), copy: parse(raw) }));
|
|
367
|
+
}
|
|
368
|
+
|
|
321
369
|
const quietly = () => { sync().catch(() => {}); };
|
|
322
370
|
|
|
323
371
|
// At a hide, an operation too large for the journal slot goes into the database blind, with
|
|
@@ -341,5 +389,5 @@ export async function openSession({ store, storage, doc = document, now = () =>
|
|
|
341
389
|
|
|
342
390
|
const first = run({ timeout: FIRST_TRANSACTION_TIMEOUT_MS });
|
|
343
391
|
first.catch(() => {});
|
|
344
|
-
return { first, sync, hide, startOver };
|
|
392
|
+
return { first, sync, hide, startOver, listRecovery, restoreRecovery, recoveryRecord: (copy) => recoveryRecord(copy, store.seed()), removeRecovery: (mark) => changeRecovery({ type: "remove", mark }) };
|
|
345
393
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractData, applyData, engine } from "../../vendor/hyper-html-api-data.vendor.js";
|
|
2
|
-
import { draftSlot, setDraftStoreFailed } from "../../core/draft-mode.js";
|
|
2
|
+
import { draftGate, draftSlot, setDraftStoreFailed } from "../../core/draft-mode.js";
|
|
3
3
|
|
|
4
4
|
export const KEY_PREFIX = "clay:visitor-drafts:";
|
|
5
5
|
export const RECORD_VERSION = 1;
|
|
@@ -73,6 +73,7 @@ export function same(a, b) {
|
|
|
73
73
|
|
|
74
74
|
const STORE_SEL = 'script[type="application/json"][merge][clay~="live"]';
|
|
75
75
|
const ARTICLE_SEL = "article[data-blockeditor]";
|
|
76
|
+
const HANDLER_SEL = "[onbeforesave], [onbeforesnapshot]";
|
|
76
77
|
const isPagePart = (name) => name === "blockeditor" || /^blockeditor\.\d+$/.test(name);
|
|
77
78
|
const canonical = (live) => JSON.stringify(Object.keys(live).sort().map((name) => [name, live[name]]));
|
|
78
79
|
|
|
@@ -269,7 +270,7 @@ function withoutSiteKey(value) {
|
|
|
269
270
|
return rest;
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
export function createStore({ doc = document, storage = safeStorage(), now = () => new Date(), path = draftPath(), fallback = null, slotLimit = SLOT_LIMIT, pendingLimit = PENDING_LIMIT } = {}) {
|
|
273
|
+
export function createStore({ doc = document, storage = safeStorage(), gate = draftGate, now = () => new Date(), path = draftPath(), fallback = null, slotLimit = SLOT_LIMIT, pendingLimit = PENDING_LIMIT } = {}) {
|
|
273
274
|
const slot = draftSlot(doc);
|
|
274
275
|
const key = draftKey(slot, path);
|
|
275
276
|
const rules = withoutSiteKey(engine.findRulesIn(doc, "api").rules);
|
|
@@ -610,16 +611,46 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
610
611
|
// to `localStorage` after it made some.
|
|
611
612
|
let before = null;
|
|
612
613
|
|
|
613
|
-
|
|
614
|
+
// The dirty gate's token for the snapshot `base` was last compared with: taken before
|
|
615
|
+
// that snapshot, in its task. Null while that is not known.
|
|
616
|
+
let capturedAt = null;
|
|
617
|
+
|
|
618
|
+
const pageOf = (html) => (html === undefined ? doc : new DOMParser().parseFromString(html, "text/html"));
|
|
619
|
+
|
|
620
|
+
// `at` is the gate token of the snapshot `html` was serialized from.
|
|
621
|
+
function capture(html, { reset = tab.reset, at = null } = {}) {
|
|
622
|
+
if (stopped) return null;
|
|
623
|
+
const parsed = pageOf(html);
|
|
624
|
+
const op = captureData(extractData(parsed, rules), recordable(readUnit(parsed)), reset);
|
|
625
|
+
capturedAt = at;
|
|
626
|
+
return op;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// The page as it is now.
|
|
630
|
+
function captureNow(reset = tab.reset) {
|
|
631
|
+
if (stopped) return null;
|
|
632
|
+
const at = gate()?.token() ?? null;
|
|
633
|
+
return capture(window.clay.getHTML(), { reset, at });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// The last capture of a generation that has ended. A capture runs the page's save
|
|
637
|
+
// handlers, and a handler that writes a changing value makes a field differ with no
|
|
638
|
+
// edit behind it. So on a page with a save handler that has recorded no edit since
|
|
639
|
+
// `base` was captured, the fields are what `base` holds, and only the page editor's
|
|
640
|
+
// part, which no handler writes, is compared.
|
|
641
|
+
function captureLate(reset = tab.reset) {
|
|
614
642
|
if (stopped) return null;
|
|
643
|
+
const settled = capturedAt !== null && doc.querySelector(HANDLER_SEL) !== null && gate()?.unchangedSince(capturedAt) === true;
|
|
644
|
+
const parsed = pageOf(window.clay?.getHTML?.());
|
|
645
|
+
return captureData(settled ? base : extractData(parsed, rules), recordable(readUnit(parsed)), reset);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function captureData(mine, mineUnit, reset = tab.reset) {
|
|
615
649
|
if (unparked != null && park(unparked, { keepSlot: true }) === null) {
|
|
616
650
|
throw storageFailure(doc, new Error("an earlier saved copy could not be kept aside"));
|
|
617
651
|
}
|
|
618
652
|
// Too much is waiting: nothing more is taken until it drains, and the page keeps it.
|
|
619
653
|
if (tab.sizes.reduce((sum, size) => sum + size, 0) >= pendingLimit) throw storageFailure(doc, new Error("too many changes are waiting to be saved"));
|
|
620
|
-
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
621
|
-
const mine = extractData(parsed, rules);
|
|
622
|
-
const mineUnit = recordable(readUnit(parsed));
|
|
623
654
|
const unitChanged = !sameUnit(mineUnit, editorUnit);
|
|
624
655
|
const keys = {};
|
|
625
656
|
for (const name of Object.keys(mine)) {
|
|
@@ -644,6 +675,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
644
675
|
}
|
|
645
676
|
if (unitChanged) editorUnit = seenUnit = mineUnit;
|
|
646
677
|
fresh = false;
|
|
678
|
+
tab.reset = reset;
|
|
647
679
|
tab.ops.push(op);
|
|
648
680
|
tab.sizes.push(JSON.stringify(op).length);
|
|
649
681
|
journal();
|
|
@@ -655,7 +687,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
655
687
|
function keep() {
|
|
656
688
|
if (stopped) return true;
|
|
657
689
|
try {
|
|
658
|
-
if (changed())
|
|
690
|
+
if (changed()) captureNow();
|
|
659
691
|
} catch { return false; }
|
|
660
692
|
return tab.journaled === tab.seq;
|
|
661
693
|
}
|
|
@@ -680,6 +712,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
680
712
|
// unsaved changes, for `write` to save.
|
|
681
713
|
function release() {
|
|
682
714
|
if (tab.ops.length) ({ base, editorUnit, seenUnit } = before);
|
|
715
|
+
capturedAt = null;
|
|
683
716
|
drop();
|
|
684
717
|
}
|
|
685
718
|
|
|
@@ -815,8 +848,11 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
|
|
|
815
848
|
|
|
816
849
|
return {
|
|
817
850
|
slot, key, rules, read, restore, write, writeLive, flush, changed, adoptExternal, externalUnit, adoptedUnit, park, clear, snapshot,
|
|
818
|
-
tab: tab.id, slotKey, chain, capture, keep, acknowledge, release, take, stop, journalSlots,
|
|
851
|
+
tab: tab.id, slotKey, chain, capture, captureNow, captureLate, keep, acknowledge, release, take, stop, journalSlots,
|
|
819
852
|
seed: seedData,
|
|
853
|
+
servedUnit: () => { readServed(); return served; },
|
|
854
|
+
fitsInto,
|
|
855
|
+
same,
|
|
820
856
|
// For the session, which is fetched after the boot and imports nothing from here.
|
|
821
857
|
merge3,
|
|
822
858
|
unitMark,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isDraftMode, isEditMode, isOwner } from "../core/is-edit-mode.js";
|
|
2
2
|
import { hasSaveToken } from "../core/host-attrs.js";
|
|
3
|
-
import { draftSlot, draftStoreFailed, setDraftTransport, setDraftFlush, setDraftUnconfirmed } from "../core/draft-mode.js";
|
|
3
|
+
import { draftGate, draftSlot, draftStoreFailed, setDraftTransport, setDraftFlush, setDraftUnconfirmed } from "../core/draft-mode.js";
|
|
4
4
|
import { reloadAfterDiscard } from "../core/unsaved-warning.js";
|
|
5
5
|
import { createStore, adoptLegacy, draftPath, legacyKey, safeStorage, storageFailure } from "./visitor-drafts/store.js";
|
|
6
6
|
import { createUiMemory } from "./visitor-drafts/ui-memory.js";
|
|
@@ -72,13 +72,13 @@ if (isDraftMode && slot && rulesTag) {
|
|
|
72
72
|
.then((m) => m.openSession({ store, storage: local, reload: () => reloadAfterDiscard({ isCurrent: () => true, reload: () => window.location.reload() }) }))
|
|
73
73
|
.then((opened) => { session = opened; }, () => { direct = true; });
|
|
74
74
|
|
|
75
|
-
setDraftTransport((html) => {
|
|
75
|
+
setDraftTransport((html, at = null) => {
|
|
76
76
|
const recapture = !restoredDetail;
|
|
77
77
|
return restoring.then(() => {
|
|
78
78
|
if (refused) throw storageFailure(document, new Error("the draft database did not open"));
|
|
79
|
-
if (recapture) html = window.clay.getHTML();
|
|
79
|
+
if (recapture) [at, html] = [draftGate()?.token() ?? null, window.clay.getHTML()];
|
|
80
80
|
if (direct) return void store.write(html);
|
|
81
|
-
store.capture(html);
|
|
81
|
+
store.capture(html, { at });
|
|
82
82
|
return opening.then(() => {
|
|
83
83
|
if (session) return session.sync().then(() => {});
|
|
84
84
|
store.write(html);
|
|
@@ -131,6 +131,26 @@ if (isDraftMode && slot && rulesTag) {
|
|
|
131
131
|
};
|
|
132
132
|
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") keep(); });
|
|
133
133
|
window.addEventListener("pagehide", keep);
|
|
134
|
+
// A link marked clay-wait-drafts (the host's download and host actions) waits for this
|
|
135
|
+
// page's last changes to reach the download page before it navigates, like the prompt's
|
|
136
|
+
// Download button. A new tab or a modified click leaves this page open, so it does not wait.
|
|
137
|
+
let leaving = false;
|
|
138
|
+
document.addEventListener("click", async (event) => {
|
|
139
|
+
// composedPath reaches into open shadow roots such as Hyperclay's trial bar.
|
|
140
|
+
const link = event.composedPath().find((node) => node instanceof Element && node.matches("a[clay-wait-drafts][href]"));
|
|
141
|
+
if (!link || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
142
|
+
if (link.target && link.target !== "_self") return;
|
|
143
|
+
if (leaving) { event.preventDefault(); return; }
|
|
144
|
+
const waiting = settle();
|
|
145
|
+
if (!waiting) return;
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
leaving = true;
|
|
148
|
+
let timer;
|
|
149
|
+
await Promise.race([waiting, new Promise((resolve) => { timer = setTimeout(resolve, DOWNLOAD_WAIT_MS); })]);
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
leaving = false;
|
|
152
|
+
window.location.assign(link.href);
|
|
153
|
+
}, true);
|
|
134
154
|
|
|
135
155
|
// Another tab's save reaches a page on the database through its session. With
|
|
136
156
|
// `localStorage` as the record it arrives as that key's storage event, once the page
|
|
@@ -197,7 +217,7 @@ export const visitorDrafts = store ? {
|
|
|
197
217
|
saveNow() {
|
|
198
218
|
if (refused || !restoredDetail) return false;
|
|
199
219
|
if (direct) return store.writeLive();
|
|
200
|
-
try { store.
|
|
220
|
+
try { store.captureNow(); } catch { return false; }
|
|
201
221
|
session?.sync().catch(() => {});
|
|
202
222
|
return !store.unsafe();
|
|
203
223
|
},
|
|
@@ -232,3 +252,12 @@ export const visitorDrafts = store ? {
|
|
|
232
252
|
}),
|
|
233
253
|
source,
|
|
234
254
|
} : inertVisitorDrafts();
|
|
255
|
+
|
|
256
|
+
// Internal to the visitor menu; owner saving and the public visitorDrafts API stay separate.
|
|
257
|
+
export async function recoveryDrafts() {
|
|
258
|
+
await restoring;
|
|
259
|
+
await opening;
|
|
260
|
+
if (!store || !session || refused) return null;
|
|
261
|
+
const { createRecovery } = await import("./visitor-drafts/recovery.js");
|
|
262
|
+
return createRecovery({ store, session });
|
|
263
|
+
}
|
|
@@ -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;
|