@panphora/clayjs 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +5570 -4173
- package/dist/m/attrs/{save-freeze-MH3AOD56.js → save-freeze-PUYX32AK.js} +1 -1
- package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
- package/dist/m/c-2EPHRF47.js +1 -0
- package/dist/m/{c-JD2VFAYG.js → c-2SUFMUZX.js} +2 -2
- package/dist/m/{c-CITLTVQ4.js → c-2TJCFPX4.js} +1 -1
- package/dist/m/c-33SS5A4Y.js +1 -0
- package/dist/m/{c-V2FPBRO6.js → c-34J5HC7N.js} +1 -1
- package/dist/m/c-3F3AADKG.js +1 -0
- package/dist/m/{c-TCFVG3I4.js → c-3S3647J6.js} +1 -1
- package/dist/m/{c-GY4BREDT.js → c-3S5TNRZA.js} +1 -1
- package/dist/m/{c-GDNZEOOZ.js → c-3WX5FABT.js} +1 -1
- package/dist/m/{c-AI3H6ETU.js → c-3XGLJAS7.js} +2 -2
- package/dist/m/c-42WBOKFM.js +1 -0
- package/dist/m/c-47M4QHCK.js +1 -0
- package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
- package/dist/m/{c-GEAG5XAK.js → c-4TP3UXBT.js} +1 -1
- package/dist/m/{c-IESDKFO6.js → c-5ELELG2F.js} +1 -1
- package/dist/m/{c-X3V4RWMA.js → c-5ZYLTN3Q.js} +1 -1
- package/dist/m/{c-5PZYDWTW.js → c-67TBU56I.js} +1 -1
- package/dist/m/{c-WNOLMHI2.js → c-6BGJAJ67.js} +1 -1
- package/dist/m/{c-4Q6QIODW.js → c-755ZOHOM.js} +1 -1
- package/dist/m/c-7Q4XYDF4.js +1 -0
- package/dist/m/c-7RHOMAMR.js +1 -0
- package/dist/m/c-A5LASUEZ.js +1 -0
- package/dist/m/{c-IL5IVTOG.js → c-ABOBD7F3.js} +1 -1
- package/dist/m/c-AQVQKFKB.js +1 -0
- package/dist/m/{c-QDVG4NAZ.js → c-ARO6CPMA.js} +1 -1
- package/dist/m/{c-ORLPQFNU.js → c-AZM34SPO.js} +1 -1
- package/dist/m/{c-XUZMRQ7O.js → c-BCS3AZQZ.js} +1 -1
- package/dist/m/c-BOS7GONO.js +1 -0
- package/dist/m/{c-SUAC5RSG.js → c-C5UGNOC2.js} +1 -1
- package/dist/m/c-C6DXZIXD.js +1 -0
- package/dist/m/{c-WSTUCBHV.js → c-CPRGR2II.js} +1 -1
- package/dist/m/{c-NICICT4P.js → c-DEYHCYJB.js} +1 -1
- package/dist/m/{c-WGQVSKKC.js → c-DYTBSJBI.js} +1 -1
- package/dist/m/{c-DT7E76Z2.js → c-DZT6W2NG.js} +1 -1
- package/dist/m/{c-O2QZDVZ2.js → c-EEWPNUM2.js} +1 -1
- package/dist/m/{c-MFB74XVN.js → c-EHAR4RJJ.js} +2 -2
- package/dist/m/c-EHJWIGWN.js +1 -0
- package/dist/m/c-EIDZHQHZ.js +1 -0
- package/dist/m/{c-V2V5FKXX.js → c-EIUKME3W.js} +1 -1
- package/dist/m/c-ELXMSEN5.js +1 -0
- package/dist/m/c-EXZOA6XY.js +7 -0
- package/dist/m/{c-FKTALXNV.js → c-EYFMAR3M.js} +1 -1
- package/dist/m/c-FC5RUFPR.js +1 -0
- package/dist/m/{c-FBOO2UY3.js → c-FUN6AKFE.js} +1 -1
- package/dist/m/{c-V4B3RUWB.js → c-GFASNR55.js} +1 -1
- package/dist/m/{c-XMIDHEKF.js → c-GY36NO7C.js} +1 -1
- package/dist/m/{c-UD4VDBGJ.js → c-H3M6UDNV.js} +1 -1
- package/dist/m/{c-SFZRY6RT.js → c-H47C6PJZ.js} +1 -1
- package/dist/m/{c-MUM53J5J.js → c-H4M2T4A6.js} +1 -1
- package/dist/m/{c-U7F66I64.js → c-HYNM5U6B.js} +1 -1
- package/dist/m/c-J2YYLEVM.js +1 -0
- package/dist/m/c-J2ZW4HMR.js +1 -0
- package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
- package/dist/m/c-K2ZHQMPF.js +1 -0
- package/dist/m/c-KD2ME5UN.js +2 -0
- package/dist/m/{c-Z2OZFX3N.js → c-L6KVXPBO.js} +1 -1
- package/dist/m/{c-GO635CRU.js → c-LAKBBJOU.js} +2 -2
- package/dist/m/{c-4AE2A2LK.js → c-LETPPG2F.js} +1 -1
- package/dist/m/{c-SFYTKRU7.js → c-MKAUC6DB.js} +1 -1
- package/dist/m/{c-WDFQ7PBU.js → c-MMEVDN6D.js} +1 -1
- package/dist/m/{c-LS7IVQVW.js → c-MMV5JULA.js} +1 -1
- package/dist/m/{c-NEQRFC4O.js → c-MVVEIJUU.js} +1 -1
- package/dist/m/{c-SXZYVXD6.js → c-N362HNGO.js} +1 -1
- package/dist/m/{c-7K4SCQB7.js → c-ND3W7D5U.js} +1 -1
- package/dist/m/{c-3E2IK5HU.js → c-NNHNXD6M.js} +1 -1
- package/dist/m/{c-D62HRSZK.js → c-NYJ577NL.js} +1 -1
- package/dist/m/{c-YFO6ZJLQ.js → c-O3DPFZCE.js} +1 -1
- package/dist/m/c-OZOACVUD.js +1 -0
- package/dist/m/{c-MV7TEUTD.js → c-OZVK3TFI.js} +1 -1
- package/dist/m/{c-ZEA3DCG3.js → c-PWILUN4T.js} +1 -1
- package/dist/m/{c-BCWYEG6P.js → c-QA3Q3MDI.js} +1 -1
- package/dist/m/{c-XHZIKRCV.js → c-QKOKMQXA.js} +1 -1
- package/dist/m/{c-TGIMBAWE.js → c-RBWVF4N5.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-RGAALCDU.js → c-SPOEDIMP.js} +1 -1
- package/dist/m/{c-YX2MC7YL.js → c-SRW5OBCF.js} +1 -1
- package/dist/m/{c-C6YZNU6E.js → c-SYOA4T7V.js} +1 -1
- package/dist/m/c-SYVF6ZMF.js +1 -0
- package/dist/m/{c-NADLYVAH.js → c-TJ5PFOJ2.js} +1 -1
- package/dist/m/{c-KYTVZT4D.js → c-TKV273YU.js} +1 -1
- package/dist/m/c-U35T5NAY.js +1 -0
- package/dist/m/{c-PRZNJUYZ.js → c-UEKJWXCB.js} +2 -2
- package/dist/m/c-UW6MMZ4V.js +1 -0
- package/dist/m/{c-XQTERKQA.js → c-V4DBV5ZV.js} +1 -1
- package/dist/m/c-VGG3UR7I.js +1 -0
- package/dist/m/c-VU4WEWPH.js +4 -0
- package/dist/m/{c-5Q5XBN56.js → c-W7LWCJHD.js} +1 -1
- package/dist/m/c-WHMZCRQD.js +6 -0
- package/dist/m/{c-WMRJKQ2V.js → c-WHQUZM2V.js} +1 -1
- package/dist/m/c-WOJ2RTV4.js +1 -0
- package/dist/m/{c-ZJG6C2WL.js → c-WOWGGNCJ.js} +1 -1
- package/dist/m/{c-ZORVIXUR.js → c-XHD7XJTE.js} +1 -1
- package/dist/m/c-XONFK3WC.js +1 -0
- package/dist/m/{c-HGG7M2S3.js → c-Y3J5LDKB.js} +1 -1
- package/dist/m/{c-JB5YU57S.js → c-Y3MPVURA.js} +1 -1
- package/dist/m/c-Y6HH7LTM.js +1 -0
- package/dist/m/{c-52OC3OH3.js → c-YRDKVEGR.js} +1 -1
- package/dist/m/c-YYIN4NAF.js +30 -0
- package/dist/m/{c-WVCT3ITX.js → c-YZGWSCMT.js} +1 -1
- package/dist/m/c-ZFAPQQIB.js +1 -0
- package/dist/m/core/{admin-attrs-ZI6K6UGM.js → admin-attrs-43JBA6JB.js} +1 -1
- package/dist/m/core/{autosave-BSM2ABJL.js → autosave-HD7Y7UNV.js} +1 -1
- package/dist/m/core/{conflict-notice-7EQLDYPA.js → conflict-notice-UNDOD4BL.js} +1 -1
- package/dist/m/core/{persist-2SZO3PCE.js → persist-MX3VYVBN.js} +1 -1
- package/dist/m/core/{save-K6Q47J2X.js → save-JZ7R4ET2.js} +1 -1
- package/dist/m/core/{save-core-U3Z44XBO.js → save-core-I4N6S4ED.js} +1 -1
- package/dist/m/core/site-edits-C22O53GS.js +1 -0
- package/dist/m/core/{snapshot-PWCHYFL6.js → snapshot-N5MKDGNI.js} +1 -1
- package/dist/m/core/{unsaved-warning-2WRZLJH5.js → unsaved-warning-BP6ZY3PG.js} +1 -1
- package/dist/m/internals/{index-FSLC4M7J.js → index-3LAVREAU.js} +1 -1
- package/dist/m/loader-DCKCZKY6.js +1 -0
- package/dist/m/manifest-7954CBE3.js +1 -0
- package/dist/m/manifest-edit-93AA79D7.js +1 -0
- package/dist/m/manifest.json +1240 -1179
- package/dist/m/page/{editor-PKSPJ2XJ.js → editor-UF3CIA6V.js} +5 -5
- package/dist/m/plugins/{ai-edit-3LG6MGS3.js → ai-edit-WDV2GLDU.js} +2 -2
- package/dist/m/plugins/{cms-T2HLI7JG.js → cms-KG6TCTR3.js} +9 -9
- package/dist/m/plugins/{derive-7DR5TUDH.js → derive-R4USHZ7P.js} +1 -1
- package/dist/m/plugins/{indicator-CTKX4EY7.js → indicator-OLPZ4WFI.js} +1 -1
- package/dist/m/plugins/{live-IFLL6YN5.js → live-DF5YS5AH.js} +1 -1
- package/dist/m/plugins/{menu-YJZ3JWBC.js → menu-ESEFQARM.js} +1 -1
- package/dist/m/plugins/page-GZSU77JS.js +2 -0
- package/dist/m/plugins/{page-view-NMCCU6MF.js → page-view-7MM3FEAR.js} +1 -1
- package/dist/m/plugins/{people-6WIWCZEG.js → people-EBWE337I.js} +1 -1
- package/dist/m/plugins/quickcrop-UHURKA2F.js +26 -0
- package/dist/m/plugins/richclay-MXH2GFAZ.js +549 -0
- package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
- package/dist/m/plugins/{source-7LSGOV46.js → source-OYHDXKTB.js} +1 -1
- package/dist/m/plugins/undo-KLHAGV75.js +1 -0
- package/dist/m/plugins/{upgrade-MMPZUCDM.js → upgrade-SZQJYEQQ.js} +1 -1
- package/dist/m/plugins/{visitor-drafts-V5YB7XDH.js → visitor-drafts-ZE6PCAFP.js} +2 -2
- package/dist/m/plugins/{visitor-menu-QDV2MYJN.js → visitor-menu-5XY57JQA.js} +1 -1
- package/dist/m/plugins/{wire-3YGUO3GE.js → wire-S5ATC4QI.js} +1 -1
- package/dist/m/reader-core-AHOAP5DH.js +77 -0
- package/dist/m/sync/{live-sync-XMXXZS3G.js → live-sync-RZQSDMZW.js} +1 -1
- package/dist/m/ui/index-G7ZJXNMG.js +1 -0
- package/dist/m/vendor/{sap-core.vendor-JANUQULT.js → sap-core.vendor-5TWOJYVM.js} +4 -4
- package/dist/offline-manifest.json +780 -750
- package/dist/server/core.cjs +4245 -2614
- package/dist/server/markdown.cjs +1 -0
- package/dist/server/markdown.mjs +7 -0
- package/docs/guide.md +26 -37
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +5 -5
- package/package.json +6 -2
- package/packed-contract.json +3 -0
- package/src/attrs/save-freeze.js +1 -1
- package/src/core/admin-contenteditable.js +1 -1
- package/src/core/admin-inputs.js +1 -1
- package/src/core/admin-onclick.js +1 -1
- package/src/core/admin-resources.js +5 -2
- package/src/core/conflict-notice-ui.js +59 -34
- package/src/core/save.js +9 -9
- package/src/core/site-edits.js +1 -1
- package/src/core/snapshot.js +56 -43
- package/src/core/stale-host-notice.js +20 -7
- package/src/loader-logic.js +0 -13
- package/src/page/boot-pack.js +1 -0
- package/src/page/editor.js +10 -4
- package/src/page/history-intent.js +111 -4
- package/src/page/import-markdown.js +22 -8
- package/src/page/save-projection.js +1 -1
- package/src/page/ybind/bridge.js +14 -7
- package/src/page/ybind/moves.js +437 -3
- package/src/page/ybind/origins.js +1 -1
- package/src/page/ybind/recovery.js +1 -1
- package/src/plugins/page.js +48 -4
- package/src/plugins/quickcrop.js +128 -69
- package/src/plugins/richclay.js +136 -0
- package/src/plugins/sortable.js +25 -9
- package/src/plugins/undo.js +169 -7
- package/src/recovery/undo.js +14 -7
- 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 +24 -0
- 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/ui/vendor-skin.js +9 -4
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +7 -7
- package/src/vendor/richclay.vendor.js +16 -16
- package/src/vendor/sap-core.vendor.js +4 -4
- package/tool-generation/context.md +69 -0
- package/tool-generation/system.md +1 -1
- package/dist/m/c-277QP5ET.js +0 -1
- package/dist/m/c-5SZ6KFR5.js +0 -1
- package/dist/m/c-CU33V6RW.js +0 -4
- package/dist/m/c-CVSWLX3X.js +0 -1
- package/dist/m/c-DXPOFLEJ.js +0 -1
- package/dist/m/c-EODTTNVA.js +0 -1
- package/dist/m/c-ESK4S67I.js +0 -1
- package/dist/m/c-FDWR3FL7.js +0 -1
- package/dist/m/c-FJDTDGDA.js +0 -1
- package/dist/m/c-FYDKYW6E.js +0 -1
- package/dist/m/c-HKSLOVU4.js +0 -1
- package/dist/m/c-IJHF4IO6.js +0 -1
- package/dist/m/c-IUL7PU4P.js +0 -1
- package/dist/m/c-JWW6BHWD.js +0 -6
- package/dist/m/c-K4M76VPM.js +0 -7
- package/dist/m/c-M7ITYKMF.js +0 -1
- package/dist/m/c-MR75YPRT.js +0 -1
- package/dist/m/c-NFQFV7OP.js +0 -2
- package/dist/m/c-NHXOBD6A.js +0 -1
- package/dist/m/c-NVU3DORS.js +0 -1
- package/dist/m/c-PNZIJFG7.js +0 -1
- package/dist/m/c-QWB53KRW.js +0 -1
- package/dist/m/c-RHPHMTF2.js +0 -2
- package/dist/m/c-RWJ3OSCP.js +0 -1
- package/dist/m/c-SEVOS4UE.js +0 -1
- package/dist/m/c-SGG6MUXW.js +0 -1
- package/dist/m/c-SXNSSHPP.js +0 -1
- package/dist/m/c-TFLRT5JX.js +0 -1
- package/dist/m/c-TLQINES7.js +0 -30
- package/dist/m/c-U65PREMC.js +0 -1
- package/dist/m/c-V5L6HWVL.js +0 -1
- package/dist/m/c-VMXCX52Q.js +0 -1
- package/dist/m/c-YYGUHSZR.js +0 -1
- package/dist/m/core/site-edits-CFMF6GHD.js +0 -1
- package/dist/m/loader-RC54UV7H.js +0 -1
- package/dist/m/manifest-3B84C829.js +0 -1
- package/dist/m/manifest-edit-1CB79AD4.js +0 -1
- package/dist/m/plugins/page-GTDPYGE4.js +0 -2
- package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
- package/dist/m/plugins/richclay-L3BZMX4I.js +0 -549
- package/dist/m/plugins/sortable-365SRRL3.js +0 -1
- package/dist/m/plugins/undo-UES4KS7B.js +0 -1
- package/dist/m/reader-core-VR7EL4WV.js +0 -77
- package/dist/m/ui/index-MGTWOW2C.js +0 -1
- /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
- /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-UHURKA2F.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-L3BZMX4I.js.LEGAL.txt → richclay-MXH2GFAZ.js.LEGAL.txt} +0 -0
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: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Typing joins one undo item the
|
|
2
|
-
//
|
|
1
|
+
// Typing joins one undo item per sentence, the same as every other field on the page:
|
|
2
|
+
// Mod-z removes the last sentence typed, not the last letter or word. Only the editor's own input path marks a
|
|
3
3
|
// transaction; a command or a programmatic dispatch stays one undo item by itself.
|
|
4
4
|
import { state } from "../vendor/prosemirror.vendor.js";
|
|
5
5
|
import { STEP } from "./blocks.js";
|
|
@@ -14,15 +14,28 @@ const blockIdAt = (doc, pos) => {
|
|
|
14
14
|
|
|
15
15
|
const markKey = (st) => (st.storedMarks ?? st.selection.$from.marks()).map((m) => `${m.type.name}:${JSON.stringify(m.attrs)}`).sort().join(",");
|
|
16
16
|
|
|
17
|
+
// A sentence ends at a space typed right after ., ? or ! (closing quotes and brackets
|
|
18
|
+
// allowed between). The space belongs to the sentence it ends, so the step closes after it.
|
|
19
|
+
// A second space, or a space after a space, is not a new sentence. The rule is hyper-undo's
|
|
20
|
+
// `endsSentence`, kept here so the block editor does not load the engine.
|
|
21
|
+
const SENTENCE_END = /[.?!]["'\u201D\u2019)\]]*$/;
|
|
22
|
+
|
|
23
|
+
const endsSentence = (st, from, text) => {
|
|
24
|
+
if (!/\s$/.test(text)) return false;
|
|
25
|
+
const $from = st.doc.resolve(from);
|
|
26
|
+
const head = $from.parent.textBetween(0, $from.parentOffset) + text.replace(/\s+$/, "");
|
|
27
|
+
return !/\s$/.test(head) && SENTENCE_END.test(head);
|
|
28
|
+
};
|
|
29
|
+
|
|
17
30
|
export function typingIntent(st, from, to, text) {
|
|
18
|
-
return { kind: "insert", blockId: blockIdAt(st.doc, from), from, end: from + text.length, replaced: from !== to, text, marks: markKey(st), at: Date.now() };
|
|
31
|
+
return { kind: "insert", blockId: blockIdAt(st.doc, from), from, end: from + text.length, replaced: from !== to, text, marks: markKey(st), breakAfter: endsSentence(st, from, text), at: Date.now() };
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export function continuesGroup(prev, next) {
|
|
22
35
|
if (!prev || !next || prev.kind !== next.kind) return false;
|
|
23
36
|
if (next.kind === "compose") return prev.id === next.id;
|
|
24
37
|
if (prev.blockId !== next.blockId || next.at - prev.at > GROUP_MS) return false;
|
|
25
|
-
if (next.kind === "insert") return !next.replaced && next.from === prev.end && next.marks === prev.marks
|
|
38
|
+
if (next.kind === "insert") return !prev.breakAfter && !next.replaced && next.from === prev.end && next.marks === prev.marks;
|
|
26
39
|
return next.direction === prev.direction && (next.direction < 0 ? next.to === prev.from : next.from === prev.from);
|
|
27
40
|
}
|
|
28
41
|
|
|
@@ -76,3 +89,97 @@ export function historyIntentPlugin() {
|
|
|
76
89
|
},
|
|
77
90
|
});
|
|
78
91
|
}
|
|
92
|
+
|
|
93
|
+
// Each page entry owns one current Yjs StackItem, replaced by its inverse after replay.
|
|
94
|
+
export function createHistoryBridge({ um, undo, redo, article, focus }) {
|
|
95
|
+
const page = () => window.clay?.undo
|
|
96
|
+
const epoch = Math.random().toString(36).slice(2)
|
|
97
|
+
let next = 0
|
|
98
|
+
let running = false
|
|
99
|
+
let recording = false
|
|
100
|
+
let disposed = false
|
|
101
|
+
|
|
102
|
+
const replay = (entry, direction) => {
|
|
103
|
+
const fromKey = direction === 'undo' ? 'undoStack' : 'redoStack'
|
|
104
|
+
const toKey = direction === 'undo' ? 'redoStack' : 'undoStack'
|
|
105
|
+
const source = um[fromKey], destination = um[toKey]
|
|
106
|
+
const index = source.indexOf(entry.item)
|
|
107
|
+
if (index < 0) throw new Error('blockeditor: history item is no longer available')
|
|
108
|
+
const remaining = source.filter((_, at) => at !== index)
|
|
109
|
+
// Yjs scans BOTH stacks' deletion sets when checking map conflicts. Keep those
|
|
110
|
+
// sets visible on the opposite stack while limiting the pop loop to one item.
|
|
111
|
+
const temporary = [...destination, ...remaining]
|
|
112
|
+
const boundary = temporary.length
|
|
113
|
+
um[fromKey] = [entry.item]
|
|
114
|
+
um[toKey] = temporary
|
|
115
|
+
running = true
|
|
116
|
+
let changed
|
|
117
|
+
let inverse
|
|
118
|
+
try {
|
|
119
|
+
changed = direction === 'undo' ? undo() : redo()
|
|
120
|
+
} finally {
|
|
121
|
+
inverse = temporary.slice(boundary)
|
|
122
|
+
um[fromKey] = remaining
|
|
123
|
+
um[toKey] = [...destination, ...inverse]
|
|
124
|
+
running = false
|
|
125
|
+
um.stopCapturing()
|
|
126
|
+
}
|
|
127
|
+
if (!changed || !inverse.length) {
|
|
128
|
+
entry.item = null
|
|
129
|
+
throw new Error('blockeditor: history item was emptied by another edit')
|
|
130
|
+
}
|
|
131
|
+
entry.item = inverse[0]
|
|
132
|
+
entry.applied = direction === 'redo'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const onAdded = (event) => {
|
|
136
|
+
if (running || event.origin === um || event.type !== 'undo' || page()?.pageHistory !== true) return
|
|
137
|
+
const entry = { item: event.stackItem, applied: true }
|
|
138
|
+
const id = `${epoch}:${++next}`
|
|
139
|
+
const oldValue = `${id}:undone`, newValue = `${id}:applied`
|
|
140
|
+
const primitive = {
|
|
141
|
+
kind: 'value',
|
|
142
|
+
target: article,
|
|
143
|
+
prop: `blockeditor-step:${id}`,
|
|
144
|
+
oldValue,
|
|
145
|
+
newValue,
|
|
146
|
+
read: () => !disposed && entry.item && (entry.applied ? um.undoStack : um.redoStack).includes(entry.item)
|
|
147
|
+
? entry.applied ? newValue : oldValue
|
|
148
|
+
: null,
|
|
149
|
+
write: (target, wanted) => {
|
|
150
|
+
const typing = !!document.activeElement?.closest?.('[contenteditable]:not([contenteditable="false"])')
|
|
151
|
+
const before = article.textContent
|
|
152
|
+
replay(entry, wanted === oldValue ? 'undo' : 'redo')
|
|
153
|
+
if (typing && article.textContent !== before && typeof focus === 'function') focus()
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
recording = true
|
|
157
|
+
try {
|
|
158
|
+
try { page().recordStep('Edit', primitive) } catch { page().record(primitive) }
|
|
159
|
+
} finally {
|
|
160
|
+
recording = false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const onCommit = () => { if (!recording) um.stopCapturing() }
|
|
165
|
+
um.on('stack-item-added', onAdded)
|
|
166
|
+
let subscribed = null
|
|
167
|
+
const subscribe = () => {
|
|
168
|
+
const current = page()?.pageHistory === true ? page() : null
|
|
169
|
+
if (current === subscribed) return
|
|
170
|
+
subscribed?.off?.('commit', onCommit)
|
|
171
|
+
subscribed = current
|
|
172
|
+
subscribed?.on?.('commit', onCommit)
|
|
173
|
+
}
|
|
174
|
+
subscribe()
|
|
175
|
+
document.addEventListener('clay:undo-session', subscribe)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
dispose() {
|
|
179
|
+
disposed = true
|
|
180
|
+
um.off('stack-item-added', onAdded)
|
|
181
|
+
subscribed?.off?.('commit', onCommit)
|
|
182
|
+
document.removeEventListener('clay:undo-session', subscribe)
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -17,8 +17,8 @@ import { safeHref, safeSrc } from "./address.js";
|
|
|
17
17
|
const EMBEDS = "iframe, frame, frameset, object, embed, applet, meta, base, link, form, style, animate, set, animateMotion, animateTransform, portal";
|
|
18
18
|
const LOADERS = /^(srcdoc|data|formaction|codebase|archive|ping|background|poster|lowsrc|dynsrc)$/i;
|
|
19
19
|
const SCRIPTED = /^\s*(javascript|vbscript|data):/i;
|
|
20
|
-
function cleanImported(html) {
|
|
21
|
-
const t =
|
|
20
|
+
function cleanImported(html, doc) {
|
|
21
|
+
const t = doc.createElement("template");
|
|
22
22
|
t.innerHTML = html;
|
|
23
23
|
for (const el of t.content.querySelectorAll(EMBEDS)) el.remove();
|
|
24
24
|
for (const el of t.content.querySelectorAll("*")) {
|
|
@@ -57,7 +57,19 @@ function added(marks, name, attrs) {
|
|
|
57
57
|
// One text block's inline content as the record keeps it: c, and m as runs (one
|
|
58
58
|
// key per UTF-16 unit, packed), exactly as flatten and runsOf write it; `parts`
|
|
59
59
|
// splits around an image, which the schema only carries as a block of its own.
|
|
60
|
-
|
|
60
|
+
// A link or image written as a relative path (`other.md`, `setup.sh`, `shot.png`) names a
|
|
61
|
+
// file beside the Markdown it came from. It never becomes a web address: `setup.sh` read
|
|
62
|
+
// as a host name is someone else's site, and an image there would make every reader's
|
|
63
|
+
// browser ask that site for it. The host may map it (`resolveLink`, `resolveImage`).
|
|
64
|
+
const RELATIVE = /^(?![a-z][a-z0-9+.-]*:|[/#?])\S/i;
|
|
65
|
+
|
|
66
|
+
function linkHref(raw, resolveLink) {
|
|
67
|
+
const resolved = typeof resolveLink === "function" ? resolveLink(raw) : null;
|
|
68
|
+
if (typeof resolved === "string" && resolved) return safeHref(resolved);
|
|
69
|
+
return RELATIVE.test(raw.trim()) ? null : safeHref(raw);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inlineOf(tokens, { images = true, refused = [], unsupported = [], resolveLink = null } = {}) {
|
|
61
73
|
const parts = [];
|
|
62
74
|
let c = "";
|
|
63
75
|
let m = [];
|
|
@@ -91,7 +103,7 @@ function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}
|
|
|
91
103
|
case "codespan": text(token.text, added(marks, "code")); break;
|
|
92
104
|
case "br": atom(BREAK); break;
|
|
93
105
|
case "link": {
|
|
94
|
-
const href =
|
|
106
|
+
const href = linkHref(String(token.href ?? ""), resolveLink);
|
|
95
107
|
if (href) walk(token.tokens, added(marks, "link", { href }));
|
|
96
108
|
else {
|
|
97
109
|
refused.push({ type: "link", href: String(token.href ?? "") });
|
|
@@ -121,7 +133,7 @@ function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}
|
|
|
121
133
|
return { parts };
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
export function importMarkdownTokens(tokens) {
|
|
136
|
+
export function importMarkdownTokens(tokens, { document: doc = globalThis.document, resolveImage = null, resolveLink = null } = {}) {
|
|
125
137
|
if (!Array.isArray(tokens)) throw new MarkdownImportError("malformed-input", "Markdown import needs the lexer's token list.");
|
|
126
138
|
let size = 0;
|
|
127
139
|
for (const token of tokens) {
|
|
@@ -155,7 +167,9 @@ export function importMarkdownTokens(tokens) {
|
|
|
155
167
|
return value;
|
|
156
168
|
};
|
|
157
169
|
const imageOf = (image) => {
|
|
158
|
-
const
|
|
170
|
+
const resolved = typeof resolveImage === "function" ? resolveImage(image.src) : null;
|
|
171
|
+
const mapped = typeof resolved === "string" && resolved ? resolved : null;
|
|
172
|
+
const src = mapped ? safeSrc(mapped) : RELATIVE.test(String(image.src).trim()) ? null : safeSrc(image.src);
|
|
159
173
|
if (src) return { t: "image", src, ...(image.alt ? { alt: image.alt } : {}), ...(image.title ? { caption: image.title } : {}) };
|
|
160
174
|
refused.push({ type: "image", href: image.src });
|
|
161
175
|
return image.alt ? block("paragraph", {}, { c: image.alt, m: [] }) : null;
|
|
@@ -168,7 +182,7 @@ export function importMarkdownTokens(tokens) {
|
|
|
168
182
|
}
|
|
169
183
|
};
|
|
170
184
|
const paragraph = (inline, ctx) => textBlocks(ctx.quote ? "quote" : "paragraph", {}, inline);
|
|
171
|
-
const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, ...options });
|
|
185
|
+
const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, resolveLink, ...options });
|
|
172
186
|
const literal = (token) => {
|
|
173
187
|
const raw = String(token.raw ?? token.text ?? "");
|
|
174
188
|
unsupported.push({ type: token.type, text: raw });
|
|
@@ -193,7 +207,7 @@ export function importMarkdownTokens(tokens) {
|
|
|
193
207
|
case "html": {
|
|
194
208
|
const raw = String(token.text ?? token.raw ?? "");
|
|
195
209
|
unsupported.push({ type: "HTML block cleaned", text: raw });
|
|
196
|
-
push({ t: "preserved", html: cleanImported(fragmentCleaner(
|
|
210
|
+
push({ t: "preserved", html: cleanImported(fragmentCleaner(doc).fallback(raw), doc) });
|
|
197
211
|
break;
|
|
198
212
|
}
|
|
199
213
|
case "blockquote": {
|
package/src/page/ybind/bridge.js
CHANGED
|
@@ -10,7 +10,7 @@ import { state as pmState } from "../../vendor/prosemirror.vendor.js";
|
|
|
10
10
|
import { repairSiblings, isKey } from "../model.js";
|
|
11
11
|
import { createOriginIndex, installUndoOrigins } from "./origins.js";
|
|
12
12
|
import { reduceRecovery, reduceRecoverable, canonicalRevision, restoreKey, physicalKeyOf, authorKey, RECOVERED_PREFIX, parsePhysicalKey, parseGoneIntents } from "./recovery.js";
|
|
13
|
-
import { moveBlocks } from "./moves.js";
|
|
13
|
+
import { moveBlocks, createMoveCarry } from "./moves.js";
|
|
14
14
|
import { createBoardCommands } from "./board-commands.js";
|
|
15
15
|
import { continuesGroup, intentOf } from "../history-intent.js";
|
|
16
16
|
import { MOVE, BlockSelection } from "../blocks.js";
|
|
@@ -320,7 +320,9 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
320
320
|
if (!d) { adopt(view.state.doc); return; }
|
|
321
321
|
const tr = view.state.tr.replace(d.start, d.endA, derived.slice(d.start, d.endB)).setMeta(REMOTE_META, true).setMeta("addToHistory", false);
|
|
322
322
|
if (own) tr.setMeta("yundo", true);
|
|
323
|
-
if (
|
|
323
|
+
if (own && view.state.doc.textContent !== derived.textContent && document.activeElement?.closest?.('[contenteditable]:not([contenteditable="false"])')) {
|
|
324
|
+
tr.setSelection(pmState.Selection.near(tr.doc.resolve(Math.min(d.endB, tr.doc.content.size)), -1));
|
|
325
|
+
} else if (keep) {
|
|
324
326
|
const hiddenUnit = (y) => y >= 0 && w.holes.some((hole) => y >= yBefore(w.holes, hole.at) && y < yAfter(w.holes, hole.at));
|
|
325
327
|
const placeIn = (id, offset) => {
|
|
326
328
|
let found = null;
|
|
@@ -359,12 +361,14 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
359
361
|
return true;
|
|
360
362
|
};
|
|
361
363
|
const stale = (t) => { began.set(t, docVersion); docVersion++; };
|
|
364
|
+
const carry = createMoveCarry({ pd, origins, um, walked: () => currentWalk() ?? walk(pd, origins) });
|
|
362
365
|
const onTransaction = (t) => {
|
|
363
366
|
if (unchanged(t)) {
|
|
364
367
|
idle.add(t);
|
|
365
368
|
if (walkedAt === began.get(t) && docVersion === walkedAt + 1) walkedAt = docVersion;
|
|
366
369
|
return;
|
|
367
370
|
}
|
|
371
|
+
if (t.origin === um) carry.note(t);
|
|
368
372
|
deltas++;
|
|
369
373
|
delta = deltas === 1 ? captureDelta(pd, t, w) : null;
|
|
370
374
|
};
|
|
@@ -385,7 +389,9 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
385
389
|
saved = { anchor: end(sel.$anchor), head: end(sel.$head) };
|
|
386
390
|
};
|
|
387
391
|
const onAfter = (t) => {
|
|
388
|
-
|
|
392
|
+
// A transaction that wrote nothing (Yjs's cleanup after an undo) is no peer's edit, so
|
|
393
|
+
// it leaves an open typing run open.
|
|
394
|
+
if (t.origin !== LOCAL && !unchanged(t)) { lastIntent = null; groupOpen = false; }
|
|
389
395
|
};
|
|
390
396
|
// One render per batch Yjs settles (a transaction and those its observers queued), after
|
|
391
397
|
// its garbage collection and merges, so the walk it leaves is current for the next keystroke.
|
|
@@ -398,9 +404,10 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
398
404
|
else if (t.origin === um) own = true;
|
|
399
405
|
}
|
|
400
406
|
if (!changed) { saved = null; return; }
|
|
401
|
-
if (!view) { bumpRecovery(); return; }
|
|
407
|
+
if (!view) { bumpRecovery(); carry.received(transactions); return; }
|
|
402
408
|
if (local) { saved = null; return; }
|
|
403
409
|
render(own);
|
|
410
|
+
carry.received(transactions);
|
|
404
411
|
};
|
|
405
412
|
const portMove = (tr, move) => {
|
|
406
413
|
const result = moveBlocks({ pd, view, um, origins, render, walked: currentWalk() }, move);
|
|
@@ -486,7 +493,7 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
486
493
|
registerAuthor = null;
|
|
487
494
|
const intent = history ? intentOf(local, view) : null;
|
|
488
495
|
const join = groupOpen && continuesGroup(lastIntent, intent);
|
|
489
|
-
lastIntent = intent;
|
|
496
|
+
lastIntent = !join && intent?.breakAfter && !intent.text.trim() ? { ...intent, breakAfter: false } : intent;
|
|
490
497
|
joining = join;
|
|
491
498
|
try {
|
|
492
499
|
localGroup(pd, local, w, stats, um, history, register, { join, open: intent !== null });
|
|
@@ -546,7 +553,7 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
|
|
|
546
553
|
moveBlocks(args) {
|
|
547
554
|
return moveBlocks({ pd, view, um, origins, render, walked: currentWalk() }, args);
|
|
548
555
|
},
|
|
549
|
-
undo() { return
|
|
550
|
-
redo() { return
|
|
556
|
+
undo() { return carry.replay("undo"); },
|
|
557
|
+
redo() { return carry.replay("redo"); },
|
|
551
558
|
};
|
|
552
559
|
}
|