@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { servedStaleToken } from "./host-attrs.js";
|
|
2
2
|
import { isEditMode } from "./is-edit-mode.js";
|
|
3
3
|
import onDomReady from "../lib/dom-ready.js";
|
|
4
|
-
import { set, make } from "../lib/hostile-css.js";
|
|
4
|
+
import { set, style, make } from "../lib/hostile-css.js";
|
|
5
5
|
|
|
6
6
|
// The page's only visible word on a host too old to save to.
|
|
7
7
|
//
|
|
@@ -47,12 +47,13 @@ function dismiss() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function build() {
|
|
50
|
+
const theme = ui.resolveTheme();
|
|
50
51
|
root = ui.bevelSurface("div", [
|
|
51
52
|
"position:fixed", "left:50%", "transform:translateX(-50%)",
|
|
52
53
|
"z-index:2147483001", "display:flex", "align-items:center", "gap:10px 12px",
|
|
53
54
|
"width:max-content", "max-width:calc(100vw - 24px)", "flex-wrap:wrap",
|
|
54
55
|
"padding:8px 8px 8px 14px", "text-align:left", `color-scheme:${ui.pageScheme()}`,
|
|
55
|
-
]);
|
|
56
|
+
], { theme, parts: ["notice.root", "notice.panel", "staleHost.root", "staleHost.panel"] });
|
|
56
57
|
// Three markers, and each is load-bearing on a page that CAN save: this element is
|
|
57
58
|
// injected, so it is in no document on disk and must never reach one, never wake the
|
|
58
59
|
// watcher, and never ride out in a snapshot to somebody else's browser.
|
|
@@ -62,13 +63,23 @@ function build() {
|
|
|
62
63
|
|
|
63
64
|
const dot = make("span", ["all:initial", "color-scheme:inherit", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${ui.TOKENS.ox}`]);
|
|
64
65
|
dot.setAttribute("clay", ui.RUNTIME_ONLY);
|
|
65
|
-
const
|
|
66
|
+
const dotNames = ["notice.dot", "staleHost.dot"];
|
|
67
|
+
if (ui.themed(theme)) {
|
|
68
|
+
ui.markParts(dot, theme, dotNames);
|
|
69
|
+
style(dot, ui.partRules(theme, dotNames, {}));
|
|
70
|
+
}
|
|
71
|
+
const close = ui.bevelButton("Dismiss", {
|
|
72
|
+
small: true, variant: "quiet", onClick: dismiss, theme,
|
|
73
|
+
parts: ["notice.button", "staleHost.button"],
|
|
74
|
+
labelParts: ["notice.buttonLabel", "staleHost.buttonLabel"],
|
|
75
|
+
});
|
|
66
76
|
close.setAttribute("aria-label", "Dismiss this message");
|
|
67
|
-
root.append(dot, ui.bevelText("span", [], MESSAGE), close);
|
|
77
|
+
root.append(dot, ui.bevelText("span", [], MESSAGE, { theme, parts: ["notice.text", "staleHost.text"] }), close);
|
|
68
78
|
|
|
69
79
|
document.body.appendChild(root);
|
|
70
80
|
place();
|
|
71
|
-
ui.
|
|
81
|
+
if (ui.themed(theme)) set(root, "color-scheme", theme.scheme === "light dark" ? "light" : theme.scheme);
|
|
82
|
+
else ui.paintLook(root, { background: ui.TOKENS.surface });
|
|
72
83
|
window.visualViewport?.addEventListener("resize", place);
|
|
73
84
|
window.visualViewport?.addEventListener("scroll", place);
|
|
74
85
|
}
|
|
@@ -81,8 +92,10 @@ async function init() {
|
|
|
81
92
|
// told its reader the opposite. The message is only true while editing is actually
|
|
82
93
|
// off, so it is shown only then.
|
|
83
94
|
if (isEditMode) return;
|
|
84
|
-
const [controls, bevel] = await Promise.all([
|
|
85
|
-
|
|
95
|
+
const [controls, bevel, themeConfig, parts] = await Promise.all([
|
|
96
|
+
import("../ui/bevel-controls.js"), import("../ui/bevel.js"), import("../ui/theme.js"), import("../ui/theme-parts.js"),
|
|
97
|
+
]);
|
|
98
|
+
ui = { ...controls, ...parts, TOKENS: bevel.TOKENS, resolveTheme: themeConfig.resolveTheme };
|
|
86
99
|
build();
|
|
87
100
|
}
|
|
88
101
|
|
package/src/lib/dirty-gate.js
CHANGED
|
@@ -29,6 +29,7 @@ import Mutation from './mutation.js';
|
|
|
29
29
|
import { isEditMode } from '../core/is-edit-mode.js';
|
|
30
30
|
import { STRIP_FROM_DIRTY_CHECK, SNAPSHOT_REMOVE_SELECTOR } from './region-policy.js';
|
|
31
31
|
import { PERSIST } from './attr-aliases.js';
|
|
32
|
+
import { setDraftGate } from '../core/draft-mode.js';
|
|
32
33
|
|
|
33
34
|
let changes = 0;
|
|
34
35
|
let clearedAt = 0;
|
|
@@ -121,6 +122,7 @@ export function startDirtyGate() {
|
|
|
121
122
|
);
|
|
122
123
|
document.addEventListener('input', onUserInput, true);
|
|
123
124
|
document.addEventListener('change', onUserInput, true);
|
|
125
|
+
setDraftGate({ token: gateTokenNow, unchangedSince: gateUnchangedSince });
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
/** The morph-apply window: nothing that happens inside it is a user edit. */
|
|
@@ -210,6 +212,23 @@ export function gateCaptureToken() {
|
|
|
210
212
|
return { gen: changes, probe };
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
/** A token for the page as it is now: what the browser has queued is counted first. */
|
|
216
|
+
export function gateTokenNow() {
|
|
217
|
+
Mutation._deliverBeforePause();
|
|
218
|
+
return gateCaptureToken();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* True when nothing was recorded since the token was taken and every [persist] control
|
|
223
|
+
* still holds what it held then. Records the browser has queued are delivered first, so
|
|
224
|
+
* the answer holds in the task that made a change too.
|
|
225
|
+
*/
|
|
226
|
+
export function gateUnchangedSince(token) {
|
|
227
|
+
if (!token) return false;
|
|
228
|
+
Mutation._deliverBeforePause();
|
|
229
|
+
return changes === token.gen && token.probe.every(([el, sig]) => controlSignature(el) === sig);
|
|
230
|
+
}
|
|
231
|
+
|
|
213
232
|
/** A change nothing in the DOM recorded: the page holds text a save has not written. */
|
|
214
233
|
export function gateMarkDirty() {
|
|
215
234
|
changes++;
|
package/src/page/editor.js
CHANGED
|
@@ -707,6 +707,11 @@ function checkboxClick(view, event) {
|
|
|
707
707
|
return true;
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
+
function pageTopInset(doc, least) {
|
|
711
|
+
const padding = parseFloat(doc.defaultView?.getComputedStyle(doc.documentElement).scrollPaddingTop);
|
|
712
|
+
return Number.isFinite(padding) ? Math.max(least, padding) : least;
|
|
713
|
+
}
|
|
714
|
+
|
|
710
715
|
// Mounts the editor in `mount`, inside a .blockeditor-shell wrapper (the surface a
|
|
711
716
|
// theme styles). `onChange(record)` gets the block record (blockMap) after
|
|
712
717
|
// every transaction that changed the document.
|
|
@@ -743,10 +748,11 @@ export function createPageEditor(mount, { blocks = null, onChange = null, upload
|
|
|
743
748
|
view = new pmView.EditorView(shell, {
|
|
744
749
|
state: blockeditorState(blocks, { upload, highlight, doc: initialDoc, history: useHistory, undo, redo, onAiTool, mediaNote, toolHost, features, commandHost }),
|
|
745
750
|
attributes: { class: "blockeditor-editor" },
|
|
746
|
-
// The phone keeps a fixed bar and the plus and grip at the bottom
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
751
|
+
// The phone keeps a fixed bar and the plus and grip at the bottom, and a page with a
|
|
752
|
+
// sticky header declares its height as scroll-padding-top: a caret scrolled into view
|
|
753
|
+
// stops clear of both.
|
|
754
|
+
scrollMargin: { get top() { return pageTopInset(doc, 16); }, bottom: 96, left: 5, right: 5 },
|
|
755
|
+
scrollThreshold: { get top() { return pageTopInset(doc, 16); }, bottom: 96, left: 0, right: 0 },
|
|
750
756
|
editable: () => editable,
|
|
751
757
|
nodeViews,
|
|
752
758
|
handleDOMEvents: {
|
|
@@ -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": {
|
|
@@ -72,17 +72,49 @@ function copyUrl(loc = window.location) {
|
|
|
72
72
|
return `/_/copy${path === "/" ? "" : path}`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Hyperclay's trial bar, once the visitor closes it, leaves its Host action and a way back to the
|
|
76
|
+
// page (hyperclay server-lib/drop/trial-bar.js): `window.hyperclayBar` holds its state, set before
|
|
77
|
+
// this menu loads, and `hyperclay:bar` reports each change. Download copy is already in the menu.
|
|
78
|
+
function attachBar({ addItem, removeItem }) {
|
|
79
|
+
const draw = (bar) => {
|
|
80
|
+
if (bar?.kind !== "trial" || !bar.dismissed) {
|
|
81
|
+
removeItem("clay.bar.host");
|
|
82
|
+
removeItem("clay.bar.show");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const host = Array.isArray(bar.actions) ? bar.actions.find((action) => action?.id === "host") : null;
|
|
86
|
+
if (typeof host?.href === "string") addItem({ id: "clay.bar.host", label: "Host and share online", href: host.href });
|
|
87
|
+
if (typeof bar.show === "function") addItem({ id: "clay.bar.show", label: "Show draft bar", run: () => bar.show() });
|
|
88
|
+
};
|
|
89
|
+
document.addEventListener("hyperclay:bar", (event) => draw(event.detail));
|
|
90
|
+
draw(window.hyperclayBar);
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
// A visitor's menu holds exactly Undo, Redo, Download copy and Make your own copy (David, Nov 11):
|
|
76
|
-
// no theme row, no page items, no Start over.
|
|
94
|
+
// no theme row, no page items, no Start over. While Hyperclay's trial bar is closed it also holds
|
|
95
|
+
// Host and share online and Show draft bar (David, Nov 11, sheet hosts review), and Earlier
|
|
96
|
+
// drafts when this page has recovery copies (David, Nov 13).
|
|
77
97
|
export function start() {
|
|
78
98
|
if (document.querySelector('clay-editor-ui[data-clay-ui="menu"]')) return;
|
|
79
99
|
const shell = createShell({ sections: [["clay.undo"], null], icon: GEAR, label: "Settings" });
|
|
80
100
|
const { embedded, skin, addItem, removeItem, open, close, note, goo, beforeOpen } = shell;
|
|
81
101
|
shell.mount(embedded ? [MENU_SAFETY, SHELL_BASE, skin ?? MENU_SKIN, MENU_TOUCH] : [MENU_SAFETY, SHELL_BASE, SHELL_FLOATING, MENU_SKIN, MENU_TOUCH]);
|
|
102
|
+
let recoveryMenu;
|
|
103
|
+
beforeOpen.push(() => {
|
|
104
|
+
if (typeof globalThis.indexedDB?.open !== "function" || !globalThis.IDBKeyRange) return;
|
|
105
|
+
recoveryMenu ??= import("../visitor-drafts/recovery-menu.js").then(async ({ attachRecoveryMenu }) => {
|
|
106
|
+
const { recoveryDrafts } = await import("../visitor-drafts.js");
|
|
107
|
+
attachRecoveryMenu(shell, recoveryDrafts);
|
|
108
|
+
}).catch((error) => {
|
|
109
|
+
recoveryMenu = null;
|
|
110
|
+
console.warn("visitor-menu: earlier drafts did not load", error);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
82
113
|
publishLooks();
|
|
83
114
|
attachUndo({ addItem, beforeOpen });
|
|
84
115
|
addItem({ id: "clay.download", label: "Download copy", run: () => window.clay.visitorDrafts.download() });
|
|
85
116
|
addItem({ id: "clay.copy", label: "Make your own copy", href: copyUrl() });
|
|
117
|
+
attachBar({ addItem, removeItem });
|
|
86
118
|
window.clay.menu = { addItem, removeItem, open, close, note, embedded, button: goo };
|
|
87
119
|
document.dispatchEvent(new CustomEvent("clay:menu-ready"));
|
|
88
120
|
}
|
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.
|
|
@@ -76,6 +76,22 @@ export function applyKeys(data, keys, merge3) {
|
|
|
76
76
|
export const isChain = (value) => isPlain(value) && typeof value.tab === "string" && typeof value.reset === "string" && Array.isArray(value.ops);
|
|
77
77
|
export const recoveryMark = (copy) => isChain(copy) ? `op:${copy.tab}:${copy.ops.at(-1)?.seq ?? copy.acked ?? 0}` : foreignMark(copy);
|
|
78
78
|
|
|
79
|
+
// Ended journals carry only the fields they changed; other fields use the served page.
|
|
80
|
+
export function recoveryRecord(copy, seed) {
|
|
81
|
+
if (isPlain(copy) && copy.v === 1 && isPlain(copy.data)) return clone(copy);
|
|
82
|
+
if (!isChain(copy) || !copy.ops.length) return null;
|
|
83
|
+
const data = clone(seed);
|
|
84
|
+
let unit = null;
|
|
85
|
+
for (const op of copy.ops) {
|
|
86
|
+
for (const [name, pair] of Object.entries(op.keys ?? {})) {
|
|
87
|
+
if ("m" in pair) Object.defineProperty(data, name, { value: clone(pair.m), enumerable: true, writable: true, configurable: true });
|
|
88
|
+
else delete data[name];
|
|
89
|
+
}
|
|
90
|
+
if (op.unit !== undefined) unit = op.unit.to;
|
|
91
|
+
}
|
|
92
|
+
return { v: 1, savedAt: copy.ops.at(-1).savedAt, data, ...(unit && { live: clone(unit.live), blockeditor: clone(unit.blockeditor) }) };
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
// Cleanup needs proof for this exact chain, even when its generation has ended.
|
|
80
96
|
export function settled(chain, meta) {
|
|
81
97
|
if (isPlain(chain) && meta.recovery?.includes(recoveryMark(chain))) return true;
|
|
@@ -123,7 +139,7 @@ function foreignBase(line, { record, seen, direct, v1base, meta }) {
|
|
|
123
139
|
* @param {?Object} [input.seen] - the latest cache copy the caller saw at the draft's key
|
|
124
140
|
* @param {boolean} [input.direct] - a storage event says `local` replaced exactly that copy
|
|
125
141
|
*/
|
|
126
|
-
export function commit(stored, { seed, newId, now, merge3, unitMark, marker = null, resets = [], chains = [], local = null, seen = null, direct = false }) {
|
|
142
|
+
export function commit(stored, { seed, newId, now, merge3, same, unitMark, marker = null, resets = [], chains = [], local = null, seen = null, direct = false, recovery = null, copies = [] }) {
|
|
127
143
|
const meta = stored.meta ? clone(stored.meta) : emptyMeta(marker);
|
|
128
144
|
// A database the browser cleared counts on from this ClayJS's cache, so the cache never
|
|
129
145
|
// reads as newer than the record.
|
|
@@ -260,6 +276,36 @@ export function commit(stored, { seed, newId, now, merge3, unitMark, marker = nu
|
|
|
260
276
|
out.applied[chain.tab] = at;
|
|
261
277
|
}
|
|
262
278
|
|
|
279
|
+
if (recovery) {
|
|
280
|
+
const found = copies.find(([mark]) => mark === recovery.mark);
|
|
281
|
+
if (!found) throw new Error("That draft is no longer available. Open the list again.");
|
|
282
|
+
if (recovery.type === "remove") {
|
|
283
|
+
meta.recovery = (meta.recovery ?? []).filter((mark) => mark !== recovery.mark);
|
|
284
|
+
out.dropped.push(recovery.mark);
|
|
285
|
+
out.removedMark = recovery.mark;
|
|
286
|
+
out.changed = true;
|
|
287
|
+
} else if (recovery.type === "restore" && recovery.reset === meta.reset) {
|
|
288
|
+
const chosen = recoveryRecord(found[1], seed);
|
|
289
|
+
if (!chosen) throw new Error("This draft cannot be restored by this version.");
|
|
290
|
+
if (meta.recovery?.includes(recovery.mark)) meta.recovery = [recovery.mark, ...meta.recovery.filter((mark) => mark !== recovery.mark)];
|
|
291
|
+
const alreadyKept = record && copies.some(([mark, copy]) => {
|
|
292
|
+
if (!meta.recovery?.includes(mark)) return false;
|
|
293
|
+
const kept = recoveryRecord(copy, seed);
|
|
294
|
+
return kept && same(record.data, kept.data) && same(unitOf(record), unitOf(kept));
|
|
295
|
+
});
|
|
296
|
+
if (record && !alreadyKept) recover(record);
|
|
297
|
+
record = {
|
|
298
|
+
v: 1, savedAt: new Date(now).toISOString(), id: newId(),
|
|
299
|
+
ancestry: lineage([], record, meta), data: chosen.data,
|
|
300
|
+
...(chosen.blockeditor && { live: chosen.live, blockeditor: chosen.blockeditor }),
|
|
301
|
+
};
|
|
302
|
+
meta.unit = unitMark(unitOf(record));
|
|
303
|
+
meta.rev++;
|
|
304
|
+
out.changed = true;
|
|
305
|
+
out.restoredCopy = true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
263
309
|
for (const [tab, entry] of Object.entries(meta.tabs)) {
|
|
264
310
|
if (now - (entry.at ?? 0) > TAB_TTL_MS) {
|
|
265
311
|
delete meta.tabs[tab];
|
|
@@ -285,7 +331,8 @@ export function transact(drafts, key, input, options) {
|
|
|
285
331
|
const stored = { meta: parse(raw.meta), record: parse(raw.record), v1base: parse(raw.v1base) };
|
|
286
332
|
const rows = raw.rows.map(([name, text]) => [name, parse(text)]);
|
|
287
333
|
const given = input(stored);
|
|
288
|
-
const
|
|
334
|
+
const copies = (raw.recovery ?? []).map(([name, text]) => [name.slice((key + ":recovery:").length), parse(text)]);
|
|
335
|
+
const result = commit(stored, { ...given, copies, chains: [...rows.map(([, chain]) => chain), ...(given.chains ?? [])] });
|
|
289
336
|
const puts = [];
|
|
290
337
|
const deletes = [];
|
|
291
338
|
if (result.changed) {
|
|
@@ -297,7 +344,7 @@ export function transact(drafts, key, input, options) {
|
|
|
297
344
|
if (result.conflicts.length) puts.push([conflictKey(key), JSON.stringify(result.conflicts[result.conflicts.length - 1])]);
|
|
298
345
|
for (const copy of result.recovered) puts.push([recoveryKey(key, recoveryMark(copy)), JSON.stringify(copy)]);
|
|
299
346
|
for (const mark of result.dropped) deletes.push(recoveryKey(key, mark));
|
|
300
|
-
for (const [name, chain] of rows) if (settled(chain, result.meta)) deletes.push(name);
|
|
347
|
+
for (const [name, chain] of rows) if (settled(chain, result.meta) || (result.removedMark && isChain(chain) && recoveryMark(chain) === result.removedMark)) deletes.push(name);
|
|
301
348
|
return { puts, deletes, result };
|
|
302
349
|
}, options);
|
|
303
350
|
}
|