@panphora/clayjs 1.18.1 → 1.20.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/README.md +1 -1
- 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 +8773 -8444
- package/dist/m/attrs/{refetch-on-save-ZUE7PSCI.js → refetch-on-save-DPGJJMGN.js} +1 -1
- package/dist/m/attrs/save-freeze-MTFVVYV5.js +1 -0
- package/dist/m/{c-KP64LT2A.js → c-2VB555FG.js} +1 -1
- package/dist/m/{c-KGHVFRR3.js → c-3AXJDL2P.js} +1 -1
- package/dist/m/{c-372N6H3B.js → c-3GJ4XARY.js} +1 -1
- package/dist/m/c-3TWCKFOP.js +1 -0
- package/dist/m/{c-U7SHNWDH.js → c-3YH6JSIL.js} +1 -1
- package/dist/m/{c-AMU3BPPR.js → c-44B5ODN7.js} +2 -2
- package/dist/m/{c-S2U6NJU4.js → c-62HZW5DH.js} +1 -1
- package/dist/m/{c-ZK4GIZH2.js → c-67ILSNPR.js} +1 -1
- package/dist/m/c-67LGZ52V.js +20 -0
- package/dist/m/{c-W6DQSHXI.js → c-7SXYQCN4.js} +1 -1
- package/dist/m/{c-5OOVFCJM.js → c-AL4RBJF2.js} +1 -1
- package/dist/m/{c-L42OLXWM.js → c-ANNFZFLN.js} +1 -1
- package/dist/m/{c-QO4HNGCQ.js → c-B4VYNXTQ.js} +1 -1
- package/dist/m/{c-7PRJCA3E.js → c-BJPZCWAV.js} +1 -1
- package/dist/m/{c-SW4YEG2D.js → c-BTLFLLML.js} +1 -1
- package/dist/m/c-C4IIQZGJ.js +1 -0
- package/dist/m/{c-5T6NZHNM.js → c-CAWNFER6.js} +1 -1
- package/dist/m/c-CSNE5EIF.js +1 -0
- package/dist/m/{c-DVYGFTSK.js → c-DDDOKPI6.js} +2 -2
- package/dist/m/{c-6DB2OLRJ.js → c-DHXPDLW6.js} +1 -1
- package/dist/m/{c-WCXLG3MV.js → c-E4IBSAQM.js} +1 -1
- package/dist/m/{c-IVTRVENZ.js → c-EDSPKYMA.js} +1 -1
- package/dist/m/{c-UORXLSD3.js → c-EFD6QNTE.js} +1 -1
- package/dist/m/{c-7MG5RU63.js → c-EGTIAL6P.js} +1 -1
- package/dist/m/{c-SPZPGD3R.js → c-EOCKLMKX.js} +1 -1
- package/dist/m/{c-HMV6P4RX.js → c-EOCZ2YVL.js} +1 -1
- package/dist/m/{c-B5UJQT7W.js → c-EZ7B5AZT.js} +1 -1
- package/dist/m/{c-UPD33ZPE.js → c-FDAXZ2PM.js} +1 -1
- package/dist/m/{c-RND3G2S2.js → c-FNVQAACK.js} +1 -1
- package/dist/m/c-FSWRGBVQ.js +1 -0
- package/dist/m/c-FTWGYI5E.js +1 -0
- package/dist/m/{c-CCQ25GFY.js → c-GKEATME6.js} +1 -1
- package/dist/m/{c-YBKHUAK7.js → c-GPGYKER7.js} +1 -1
- package/dist/m/{c-FWKHM4E7.js → c-GSRLXYWL.js} +1 -1
- package/dist/m/{c-B7J4K2AE.js → c-GVPNDIKJ.js} +2 -2
- package/dist/m/{c-AMYEFA2E.js → c-I2IFKJX2.js} +1 -1
- package/dist/m/c-ICMAOOGK.js +1 -0
- package/dist/m/{c-BUAYFEV5.js → c-J3BXFTZS.js} +1 -1
- package/dist/m/{c-BIJALSRD.js → c-JDZDF2QM.js} +1 -1
- package/dist/m/{c-UDHIVLYH.js → c-JH5FF4NL.js} +1 -1
- package/dist/m/{c-OYRZ7XG2.js → c-JHJR5BQX.js} +1 -1
- package/dist/m/{c-V44KWI2L.js → c-JOH753EJ.js} +2 -2
- package/dist/m/{c-P3OFYUMA.js → c-JUE7B4OX.js} +1 -1
- package/dist/m/c-K76AUL4I.js +7 -0
- package/dist/m/{c-ZWIOJGMT.js → c-LA2BW7UY.js} +1 -1
- package/dist/m/{c-ICJYHABF.js → c-LDUNOR57.js} +1 -1
- package/dist/m/{c-C6IPRG34.js → c-LIBE4HPD.js} +1 -1
- package/dist/m/c-LPUJXRXW.js +1 -0
- package/dist/m/{c-TTB5EK4S.js → c-LQU7TXIH.js} +1 -1
- package/dist/m/{c-5ZQV7KC4.js → c-LV5MABYD.js} +1 -1
- package/dist/m/{c-URSMSFCB.js → c-M4FSPNQ2.js} +1 -1
- package/dist/m/{c-2KIEGNC4.js → c-MEWUGR4D.js} +1 -1
- package/dist/m/c-MIL4B2JQ.js +1 -0
- package/dist/m/{c-IVMD3FEM.js → c-N7Y52HJH.js} +1 -1
- package/dist/m/{c-PLN76GF3.js → c-NFR673TL.js} +2 -2
- package/dist/m/{c-T637T7SN.js → c-NLZ5AN4L.js} +1 -1
- package/dist/m/{c-OPLUZR3A.js → c-OABVRYOT.js} +1 -1
- package/dist/m/c-PIVIYLTY.js +1 -0
- package/dist/m/{c-L7JL5TL3.js → c-QY7F6EN6.js} +1 -1
- package/dist/m/{c-UY4QX42B.js → c-RG6XUZFA.js} +1 -1
- package/dist/m/c-RHMPKZZ7.js +1 -0
- package/dist/m/{c-TJAXGIRV.js → c-RQRMPZXD.js} +1 -1
- package/dist/m/{c-GY422N3Q.js → c-RS77ODLY.js} +1 -1
- package/dist/m/{c-IMZCWYHR.js → c-RT3C74GY.js} +1 -1
- package/dist/m/{c-CAS3NND4.js → c-S5IEIZCN.js} +1 -1
- package/dist/m/{c-K3ADYNLU.js → c-SBSMJIPC.js} +1 -1
- package/dist/m/c-SFKI6OLM.js +2 -0
- package/dist/m/{c-4TXNLX4K.js → c-SJPD46ZO.js} +4 -4
- package/dist/m/{c-HHGJBU4D.js → c-SO63UGPQ.js} +1 -1
- package/dist/m/{c-PLP4JKFC.js → c-TUGVINJR.js} +1 -1
- package/dist/m/{c-Q26MME6N.js → c-TWCXBWS3.js} +1 -1
- package/dist/m/{c-GNDL3TKB.js → c-U5X57T4L.js} +1 -1
- package/dist/m/{c-4YU3BR4D.js → c-UXEVJ63W.js} +1 -1
- package/dist/m/{c-KJJPV77R.js → c-V2J5C5MT.js} +1 -1
- package/dist/m/{c-6UGWFMQC.js → c-VBLFAQD2.js} +1 -1
- package/dist/m/{c-ERQSNR6K.js → c-VQCEOGKH.js} +2 -2
- package/dist/m/{c-YVZ2L7CE.js → c-VSPAFOKU.js} +1 -1
- package/dist/m/{c-5ONTYIUI.js → c-VY6XRBOW.js} +1 -1
- package/dist/m/{c-2KL3BD52.js → c-W23KVGUO.js} +1 -1
- package/dist/m/{c-COPTL5WB.js → c-W3K23S72.js} +1 -1
- package/dist/m/c-WFMRX5RZ.js +1 -0
- package/dist/m/{c-BWFVSBDF.js → c-WN3MXOLT.js} +1 -1
- package/dist/m/{c-X5TSXZYJ.js → c-XD5MOROJ.js} +1 -1
- package/dist/m/{c-MWXFLW4J.js → c-XQSXKIKM.js} +1 -1
- package/dist/m/{c-OTMKN2OO.js → c-Y33LUUIQ.js} +1 -1
- package/dist/m/{c-JNLX3RJB.js → c-YQUJ74R3.js} +1 -1
- package/dist/m/{c-2GRUM77Q.js → c-YSB6JKV2.js} +2 -2
- package/dist/m/{c-WQWIRWO3.js → c-Z32V4MHK.js} +1 -1
- package/dist/m/{c-BHZYSHW4.js → c-ZJLU56DV.js} +1 -1
- package/dist/m/{c-DKLS74XB.js → c-ZQXNHZZT.js} +2 -2
- package/dist/m/{c-TNEM3RNM.js → c-ZSCBEB7I.js} +1 -1
- package/dist/m/core/{admin-attrs-YY7T5FGU.js → admin-attrs-ZG7R5V3E.js} +1 -1
- package/dist/m/core/{autosave-S6OMNDCK.js → autosave-VPORYJSE.js} +1 -1
- package/dist/m/core/{conflict-notice-IWHMHH43.js → conflict-notice-JMAZLVRX.js} +1 -1
- package/dist/m/core/{persist-UKXV5KLA.js → persist-3OPWJKZS.js} +1 -1
- package/dist/m/core/{save-LYY7K6U7.js → save-AOJBDSOK.js} +1 -1
- package/dist/m/core/{save-core-MKRPSEEO.js → save-core-IOCMXO5L.js} +1 -1
- package/dist/m/core/{snapshot-NU3YM6GR.js → snapshot-O7II2AGG.js} +1 -1
- package/dist/m/core/unsaved-warning-U5VUJFOE.js +1 -0
- package/dist/m/events/{index-RN2JNGBV.js → index-B6TGDYMG.js} +1 -1
- package/dist/m/internals/{index-TGLQINN3.js → index-HDRANDWE.js} +1 -1
- package/dist/m/lib/{cache-bust-MUMFXZK6.js → cache-bust-NH7VBWHO.js} +1 -1
- package/dist/m/lib/{region-policy-SB2AWVG5.js → region-policy-KWBPROWF.js} +1 -1
- package/dist/m/loader-DZZLUBVB.js +1 -0
- package/dist/m/manifest-3AEF5191.js +1 -0
- package/dist/m/manifest-edit-29A26F22.js +1 -0
- package/dist/m/manifest.json +1336 -1281
- package/dist/m/options/{options-EJMS3K24.js → options-55LQHSJ4.js} +1 -1
- package/dist/m/page/{editor-6LRB3GE4.js → editor-EY2WAAGB.js} +2 -2
- package/dist/m/plugins/{ai-edit-UTV7YO2I.js → ai-edit-J5VLSW5I.js} +1 -1
- package/dist/m/plugins/{demo-OYO5GCAO.js → demo-42BSYQ7T.js} +1 -1
- package/dist/m/plugins/like-EV7MTZ56.js +1 -0
- package/dist/m/plugins/{live-AD4H6XSL.js → live-5EZN2EYP.js} +1 -1
- package/dist/m/plugins/menu-KGWKT6CS.js +1 -0
- package/dist/m/plugins/{page-MD3T2ZX3.js → page-LVW5DWA4.js} +2 -2
- package/dist/m/plugins/{page-view-PAWVZNFY.js → page-view-5KQ76IJB.js} +1 -1
- package/dist/m/plugins/{sortable-4S6SQC3D.js → sortable-WJZZBM7R.js} +1 -1
- package/dist/m/plugins/{source-TJXOIOYY.js → source-CCEUM3OR.js} +1 -1
- package/dist/m/plugins/{undo-NLPGCJQY.js → undo-3JEYK5QY.js} +1 -1
- package/dist/m/plugins/{upgrade-HSUNDNJD.js → upgrade-NW556WJD.js} +1 -1
- package/dist/m/plugins/{upload-NHGJXZK2.js → upload-TTCQRIM5.js} +1 -1
- package/dist/m/plugins/{visitor-drafts-BW43DGY3.js → visitor-drafts-2AWNT23K.js} +2 -2
- package/dist/m/plugins/{visitor-drafts-owner-7ODQG7JX.js → visitor-drafts-owner-MZP45XSG.js} +1 -1
- package/dist/m/plugins/{wire-VBWXEXU2.js → wire-VRYPY6EJ.js} +1 -1
- package/dist/m/{reader-core-4W2KYEWQ.js → reader-core-2GXY7SHY.js} +2 -2
- package/dist/m/sync/{live-sync-MO5DH33W.js → live-sync-SKPMFYOO.js} +1 -1
- package/dist/m/ui/{index-IVWN2QC4.js → index-QNIPDHXQ.js} +1 -1
- package/dist/offline-manifest.json +652 -616
- package/docs/guide.md +25 -8
- package/package.json +4 -2
- package/src/core/conflict-download.js +8 -7
- package/src/core/conflict-notice-ui.js +4 -4
- package/src/core/copy-content.js +103 -0
- package/src/core/download.js +14 -2
- package/src/core/save-size-notice.js +2 -2
- package/src/core/upgrade-run.js +5 -1
- package/src/lib/region-policy.js +1 -1
- package/src/loader-logic.js +5 -1
- package/src/loader.js +2 -0
- package/src/page/features/export-html.js +4 -4
- package/src/page/features/export-markdown.js +2 -2
- package/src/page/recovery-ui.js +2 -2
- package/src/page/save-states.js +4 -4
- package/src/plugins/like.js +254 -0
- package/src/plugins/menu/actions.js +1 -1
- package/src/plugins/page.js +4 -4
- package/src/plugins/visitor-drafts/prompt.js +12 -12
- package/src/plugins/wire.js +3 -1
- package/dist/m/attrs/save-freeze-LWGF6XUN.js +0 -1
- package/dist/m/c-D5IKEKXW.js +0 -7
- package/dist/m/c-DCFSEJZG.js +0 -1
- package/dist/m/c-EVYAISP3.js +0 -1
- package/dist/m/c-GRXHHFPO.js +0 -1
- package/dist/m/c-HT2DKBV2.js +0 -1
- package/dist/m/c-MCOOHCSG.js +0 -1
- package/dist/m/c-VILENZTD.js +0 -1
- package/dist/m/c-VR2BJ4AN.js +0 -1
- package/dist/m/c-WGW764WX.js +0 -1
- package/dist/m/c-WNTYKCDG.js +0 -1
- package/dist/m/c-YBSS55KU.js +0 -20
- package/dist/m/core/unsaved-warning-7QTKEN7Z.js +0 -1
- package/dist/m/loader-3BBNZZGM.js +0 -1
- package/dist/m/manifest-26C55578.js +0 -1
- package/dist/m/manifest-edit-4396A009.js +0 -1
- package/dist/m/plugins/menu-M5WW7DBF.js +0 -1
- /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
- /package/dist/m/plugins/{people-JCHVPF7P.js → people-C7TVBBMR.js} +0 -0
package/docs/guide.md
CHANGED
|
@@ -4,7 +4,7 @@ This is the complete reference for building malleable documents with ClayJS: one
|
|
|
4
4
|
|
|
5
5
|
It is written for a developer or an LLM building full apps with inline editing and inline controls: kanban boards with card detail, block writers, finance trackers, CRMs, flashcards, trackers. It covers the CMS plugin only in passing ([cms: content-managed pages](https://clayjs.com/docs/guide/components/#cms-content-managed-pages)), because an app whose product is editing should not route editing through a form sidebar.
|
|
6
6
|
|
|
7
|
-
Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/
|
|
7
|
+
Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/hyperspacesystems/clayjs/tree/main/tests/unit/guide-examples) boots each one in jsdom with the real loader and checks what it saves, how it merges a peer's edit, and what a viewer receives. Open any of them and view source, or download one and open it in HTML Clay to edit it:
|
|
8
8
|
|
|
9
9
|
| File | What it shows |
|
|
10
10
|
|---|---|
|
|
@@ -314,6 +314,7 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
314
314
|
| `cms` | off | yes | `clay.cms`; implies `quickcrop` and `upload` |
|
|
315
315
|
| `wire` | off | yes | `clay.wire`, the channel to a local agent process |
|
|
316
316
|
| `message` | off | yes | `message-form`: a visitor's contact form to the owner, `clay.message` ([message: contact forms](https://clayjs.com/docs/guide/components/#message-contact-forms)) |
|
|
317
|
+
| `like` | off | yes | `like`: a like button on any element, counted by the host, `clay.like` ([like](https://clayjs.com/docs/plugins/like/)) |
|
|
317
318
|
| `ai-edit` | off | no | select text, ask an agent to rewrite it; implies `wire` |
|
|
318
319
|
| `demo` | off | yes | fakes saving into `localStorage` for public demos |
|
|
319
320
|
| `visitor-drafts` | off | yes | `clay.visitorDrafts`; in draft mode, the draft store, download prompt and intro area; outside it, only the owner chip, fetched when the owner edits ([Free documents for visitors](https://clayjs.com/docs/guide/free-documents/)) |
|
|
@@ -891,7 +892,23 @@ Traps in this table:
|
|
|
891
892
|
- **Fill a scratchpad after `clay.ready`, and insert new ones empty.** The contents are captured when the element first appears. Content written before boot, or present in a cloned element when inserted, becomes its saved contents. Empty the scratchpads in a copied record before inserting it.
|
|
892
893
|
- **Create `editor-ui` roots at runtime from an unmarked `<template>` on every load.** An `editor-ui` root disappears from the saved file, so it cannot be authored in the file. Do not mark the blueprint inside the `<template>` as `editor-ui`, or the first save empties the template and leaves no chrome to clone on the next load. Leave the blueprint unmarked and set `clay="editor-ui"` on each clone as you insert it.
|
|
893
894
|
|
|
894
|
-
### 4.3 `
|
|
895
|
+
### 4.3 Copy policy: `no-copy` and `no-copy-unless:paid-forms`
|
|
896
|
+
|
|
897
|
+
Save and sync tokens govern the working document; copy-policy tokens on `clay` govern what travels into an explicit copy:
|
|
898
|
+
|
|
899
|
+
```html
|
|
900
|
+
<script src="/analytics.js" defer clay="no-copy"></script>
|
|
901
|
+
<header class="intro" clay="no-copy">…</header>
|
|
902
|
+
<form action="/_/message" method="post" clay="no-copy-unless:paid-forms">…</form>
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
- **Normal saves keep the original.** Ordinary saves, live sync, versions, and automatic recovery retain `no-copy` and conditional regions.
|
|
906
|
+
- **Explicit copies strip `no-copy` content, even for the owner.** Copies, duplicates, forks, HTML downloads, and ZIP exports remove `clay="no-copy"` elements (including inside `<template>` contents). User-requested recovery downloads filter visible HTML and embedded earlier HTML pages while preserving recovery records and edits.
|
|
907
|
+
- **Authored loaders vs runtime scripts.** An authored loader marked `clay="no-copy"` stays in original saves and disappears from copies. Any SDK `<script>` added at runtime uses `clay="no-save no-snapshot no-watch no-copy"` so it is not persisted. ClayJS core knows only generic copy policy, not any analytics provider.
|
|
908
|
+
- **Conditional paid forms.** `clay="no-copy-unless:paid-forms"` aliases legacy `data-paid-only="forms"` (still supported). Hosted copy/create checks the destination's existing `paid-forms` (`forms`) entitlement; offline downloads have no destination entitlement and strip optional conditional content. Reusable templates retain conditional regions for creation-time filtering, while unconditional `no-copy` regions are removed before owned template outputs. Exact case-sensitive matching fails closed, so an unknown conditional spelling cannot disguise a required paid feature.
|
|
909
|
+
- **Supporting hosts and raw file limits.** Enforced in ClayJS client downloads and recovery exports, Hyperclay hosted copy/create/download flows, and Hyperclay Local / HTML Clay copies and exports. Finder, `cp`, and browser Save Page As bypass app hooks and copy raw file bytes unchanged.
|
|
910
|
+
|
|
911
|
+
### 4.4 `persist`: form controls
|
|
895
912
|
|
|
896
913
|
Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belongs to the record. A control's typed value is a property, not an attribute, and serialization only reads attributes. Without `persist`, a typed value is lost on save and the file keeps the authored `value`.
|
|
897
914
|
|
|
@@ -912,7 +929,7 @@ Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belon
|
|
|
912
929
|
|
|
913
930
|
**After undo, redo, or a merge, ClayJS puts each control's display back in step with its attributes.** A control the user has touched shows its live property, not its attribute. A merge and the undo plugin copy `checked`, `value`, and `selected` back onto the property (undo does this for `persist` controls outside `no-undo` regions). A merge leaves the focused text field alone, while undo restores it too.
|
|
914
931
|
|
|
915
|
-
### 4.
|
|
932
|
+
### 4.5 Where to keep data that is not visible text
|
|
916
933
|
|
|
917
934
|
In order of preference:
|
|
918
935
|
|
|
@@ -934,7 +951,7 @@ In order of preference:
|
|
|
934
951
|
4. **A live JSON script (`clay="live"`)** for state that changes faster than a save: game boards, scores, cursors ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)).
|
|
935
952
|
5. **`<template>` content: never for mutable data.** It is saved, but edits there never trigger autosave.
|
|
936
953
|
|
|
937
|
-
### 4.
|
|
954
|
+
### 4.6 Element identity: `data-id` and `id`
|
|
938
955
|
|
|
939
956
|
Live sync and undo pair elements across versions by **`data-id`, then `id`**, used only when the value is unique on its side and the tag names match. Without an id, elements pair by content, which works for distinct text and fails for identical new items.
|
|
940
957
|
|
|
@@ -955,11 +972,11 @@ Live sync and undo pair elements across versions by **`data-id`, then `id`**, us
|
|
|
955
972
|
- **A `data-id` is also what presence names.** A record target `{ kind: "record", id }` carries it, so a stable id is what lets a peer's badge find the row again after a reload or a merge ([Presence](https://clayjs.com/docs/guide/live-sync/#presence)).
|
|
956
973
|
- **Use ids to find records in commands:** `` board.querySelector(`.card[data-id="${CSS.escape(id)}"]`) ``. Never find a record by position.
|
|
957
974
|
|
|
958
|
-
### 4.
|
|
975
|
+
### 4.7 Assets
|
|
959
976
|
|
|
960
977
|
ClayJS saves an image through its `src`. Upload the file and write the returned URL ([upload and quickcrop](https://clayjs.com/docs/guide/components/#upload-and-quickcrop)). Every host returns a host path such as `/_/uploads/assets-<name>/photo-ab12.png`, which keeps working on that host when the document is renamed or moved. A document opened straight from disk shows those images broken; Export as zip takes a document with its files. Where the host cannot store files, embed a `data:` URL. Data URLs work anywhere, but every save carries the bytes, so keep embedded images small.
|
|
961
978
|
|
|
962
|
-
### 4.
|
|
979
|
+
### 4.8 Ephemeral state: per-tab state on saved elements
|
|
963
980
|
|
|
964
981
|
Use `ephemeral` for per-tab state on a saved element, such as a selected card, a row being dragged, or an open section. Toggling a normal class or attribute is an edit like any other: it marks the page unsaved, starts an autosave, adds an undo step, reaches every peer, and lands in the file. Mark it `ephemeral` instead:
|
|
965
982
|
|
|
@@ -981,7 +998,7 @@ clay.ephemeral.toggle(card, "selected"); // add, remove, toggle(el, word, forc
|
|
|
981
998
|
|
|
982
999
|
Use `ephemeral` for state on one element. State that comes from the URL hash and spans many records (the open card, a filter) is still painted with a runtime stylesheet ([Per-tab state](https://clayjs.com/docs/guide/architecture/#per-tab-state)).
|
|
983
1000
|
|
|
984
|
-
### 4.
|
|
1001
|
+
### 4.9 A template is an ordinary record with a tag
|
|
985
1002
|
|
|
986
1003
|
A reusable template is a record like any other, marked with a tag. It stays editable, searchable, exported, synced and in a visitor's draft, and it needs no second store and no new format. The app reads the tag off the body it already has:
|
|
987
1004
|
|
|
@@ -991,7 +1008,7 @@ const templates = open => Entries.live().filter(a => a !== open && summary({ bod
|
|
|
991
1008
|
|
|
992
1009
|
The tag is matched outside code spans and fences, so a template entry that shows a `#template` example does not become a template itself. Using one copies its text into the page, and the new record is written by the same save as everything else.
|
|
993
1010
|
|
|
994
|
-
### 4.
|
|
1011
|
+
### 4.10 Keep a helper that reads the text beside the editor, in an `editor-ui` row
|
|
995
1012
|
|
|
996
1013
|
A helper that reacts to the text being written belongs in a row a script builds beside the editor, marked `clay="editor-ui"`, and refreshed on a debounce. Nothing it draws is saved, printed or synced, and no keystroke pays for it beyond the debounce:
|
|
997
1014
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable documents. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -66,11 +66,13 @@
|
|
|
66
66
|
"scripts": {
|
|
67
67
|
"test": "node scripts/test-guard.mjs",
|
|
68
68
|
"measure:load": "node scripts/measure-load.mjs",
|
|
69
|
+
"test:perf": "node scripts/test-guard.mjs --runInBand --testMatch '**/tests/perf/**/*.test.js' tests/perf",
|
|
69
70
|
"test:conformance": "UPDATE_GOLDENS=0 web-test-runner --config conformance/wtr.config.mjs",
|
|
70
71
|
"conformance:update": "UPDATE_GOLDENS=1 web-test-runner --config conformance/wtr.config.mjs",
|
|
71
72
|
"build:standalone": "node build.js --standalone",
|
|
72
73
|
"prepack": "node build.js --standalone",
|
|
73
74
|
"test:standalone": "node build.js --standalone && web-test-runner --config tests/browser/wtr.config.mjs",
|
|
75
|
+
"browser:install": "node scripts/chrome-for-testing/browser-runtime.cjs install",
|
|
74
76
|
"prepublishOnly": "testbox run",
|
|
75
77
|
"dev": "node tests/stub-server.mjs",
|
|
76
78
|
"build": "npm run build:bevel && npm run build:skins && node build.js",
|
|
@@ -110,7 +112,7 @@
|
|
|
110
112
|
"marked": "18.0.14",
|
|
111
113
|
"overtype": "2.6.1",
|
|
112
114
|
"parse5": "^7.3.0",
|
|
113
|
-
"playwright": "1.
|
|
115
|
+
"playwright": "1.64.1",
|
|
114
116
|
"postcss": "^8.5.28",
|
|
115
117
|
"prosemirror-commands": "1.7.2",
|
|
116
118
|
"prosemirror-dropcursor": "1.8.4",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serializeRecoveryClone, captureRecoveryPage } from "./snapshot.js";
|
|
2
2
|
import { rowOf } from "./conflict-presentation.js";
|
|
3
3
|
import { documentName, downloadHtml } from "./download.js";
|
|
4
|
+
import { stripCopyContent } from "./copy-content.js";
|
|
4
5
|
|
|
5
6
|
export { documentName, downloadHtml };
|
|
6
7
|
|
|
@@ -31,12 +32,12 @@ export function buildRecovery(records, { refused = false } = {}) {
|
|
|
31
32
|
if (apply && !seen.has(apply.id)) { seen.add(apply.id); applies.push(apply); }
|
|
32
33
|
}
|
|
33
34
|
applies.sort((a, b) => (a.ticket || 0) - (b.ticket || 0));
|
|
34
|
-
const pageOf = (a) => serializeRecoveryClone(a.recoveryRoot ?? a.root, { prepared: a.domain === "save" });
|
|
35
|
+
const pageOf = (a) => stripCopyContent(serializeRecoveryClone(a.recoveryRoot ?? a.root, { prepared: a.domain === "save" }));
|
|
35
36
|
|
|
36
37
|
let html;
|
|
37
38
|
let older;
|
|
38
39
|
if (refused || !applies.length) {
|
|
39
|
-
html = captureRecoveryPage();
|
|
40
|
+
html = stripCopyContent(captureRecoveryPage());
|
|
40
41
|
older = applies;
|
|
41
42
|
} else {
|
|
42
43
|
html = pageOf(applies[applies.length - 1]);
|
|
@@ -66,9 +67,9 @@ export function buildRecovery(records, { refused = false } = {}) {
|
|
|
66
67
|
// The review panel's own copy: the page a reload kept, when there is one, with every
|
|
67
68
|
// stored row it drew listed beside it as JSON. The rows are already plain, so nothing
|
|
68
69
|
// here reads the ledger the tab no longer has.
|
|
69
|
-
export function downloadStored({ html = null, edits = [] } = {}) {
|
|
70
|
+
export async function downloadStored({ html = null, edits = [] } = {}) {
|
|
70
71
|
try {
|
|
71
|
-
const page = typeof html === "string" ? html : captureRecoveryPage();
|
|
72
|
+
const page = stripCopyContent(typeof html === "string" ? html : captureRecoveryPage());
|
|
72
73
|
const data = {
|
|
73
74
|
format: "clay-lost-edits",
|
|
74
75
|
version: 1,
|
|
@@ -78,17 +79,17 @@ export function downloadStored({ html = null, edits = [] } = {}) {
|
|
|
78
79
|
edits: Array.isArray(edits) ? edits : [],
|
|
79
80
|
earlierPages: [],
|
|
80
81
|
};
|
|
81
|
-
return downloadHtml({ html: embed(page, data), name: `${documentName()}-my-copy-${stamp()}.html` });
|
|
82
|
+
return await downloadHtml({ html: embed(page, data), name: `${documentName()}-my-copy-${stamp()}.html` });
|
|
82
83
|
} catch (err) {
|
|
83
84
|
console.error("clayjs: could not build the download", err);
|
|
84
85
|
return false;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export function downloadRecovery(records, { refused = false } = {}) {
|
|
89
|
+
export async function downloadRecovery(records, { refused = false } = {}) {
|
|
89
90
|
try {
|
|
90
91
|
const { html, name } = buildRecovery(records, { refused });
|
|
91
|
-
return downloadHtml({ html, name });
|
|
92
|
+
return await downloadHtml({ html, name });
|
|
92
93
|
} catch (err) {
|
|
93
94
|
console.error("clayjs: could not build the download", err);
|
|
94
95
|
return false;
|
|
@@ -707,12 +707,12 @@ async function onRevert() {
|
|
|
707
707
|
let downloadedTimer = null;
|
|
708
708
|
|
|
709
709
|
// Download my copy. Nothing here acknowledges or releases a record.
|
|
710
|
-
function onDownload() {
|
|
710
|
+
async function onDownload() {
|
|
711
711
|
const l = ledger();
|
|
712
712
|
// A refusal ends in a reload that takes every record with it, so its copy carries
|
|
713
713
|
// all of them; a replaced notice covers the rows it drew.
|
|
714
714
|
const records = refused ? (l?.list() ?? []) : renderedIds.map((id) => l?.get(id)).filter(Boolean);
|
|
715
|
-
const ok = downloadRecovery(records, { refused: !!refused });
|
|
715
|
+
const ok = await downloadRecovery(records, { refused: !!refused });
|
|
716
716
|
downloadFailed = !ok;
|
|
717
717
|
downloaded = ok;
|
|
718
718
|
clearTimeout(downloadedTimer);
|
|
@@ -801,14 +801,14 @@ async function onCopyStored(entry) {
|
|
|
801
801
|
|
|
802
802
|
// Download my copy, from the stored root: the page as this tab had it, with the
|
|
803
803
|
// losing text in it. Nothing here acknowledges or releases a record.
|
|
804
|
-
function onDownloadRecovered() {
|
|
804
|
+
async function onDownloadRecovered() {
|
|
805
805
|
const record = recovered.find((item) => item.root);
|
|
806
806
|
const edits = [];
|
|
807
807
|
for (const item of recovered) {
|
|
808
808
|
for (const entry of item.conflicts) edits.push({ id: entry.id, kind: entry.kind, name: storedName(entry), yours: entry.yours, now: entry.now, sentence: entry.sentence });
|
|
809
809
|
}
|
|
810
810
|
for (const entry of recoveredUndo) edits.push({ id: entry.id, kind: entry.kind, name: entry.name, yours: entry.yours, now: entry.now, sentence: entry.sentence });
|
|
811
|
-
const ok = downloadStored({ html: record?.root ?? null, edits });
|
|
811
|
+
const ok = await downloadStored({ html: record?.root ?? null, edits });
|
|
812
812
|
downloadFailed = !ok;
|
|
813
813
|
downloaded = ok;
|
|
814
814
|
clearTimeout(downloadedTimer);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copy-only content: marked regions that stay in the file, in normal saves, in snapshots
|
|
2
|
+
// and in recovery storage, and are removed only from the bytes of a copy someone asks for.
|
|
3
|
+
// The removal is a splice on the original string using parse5's source offsets, so every
|
|
4
|
+
// byte outside a removed range survives, templates and implicit end tags included.
|
|
5
|
+
|
|
6
|
+
import { parse } from "../vendor/parse5.vendor.js";
|
|
7
|
+
|
|
8
|
+
const CLAY = "clay";
|
|
9
|
+
const PAID_ONLY = "data-paid-only";
|
|
10
|
+
const NO_COPY = "no-copy";
|
|
11
|
+
const CONDITIONAL_FORMS = "no-copy-unless:paid-forms";
|
|
12
|
+
const CLAY_TOKENS = /[\t\n\f\r ]+/;
|
|
13
|
+
|
|
14
|
+
function attributeOf(node, name) {
|
|
15
|
+
return node.attrs?.find((attr) => attr.name === name)?.value ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function childrenOf(node) {
|
|
19
|
+
return node.content ? [...(node.childNodes || []), node.content] : (node.childNodes || []);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function eachElement(node, visit) {
|
|
23
|
+
visit(node);
|
|
24
|
+
for (const child of childrenOf(node)) eachElement(child, visit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clayTokensOf(node) {
|
|
28
|
+
return (attributeOf(node, CLAY) ?? "").split(CLAY_TOKENS);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The marker is compared to a feature key exactly: no trim, no lowercase, so a typo, an
|
|
32
|
+
// empty value or `FORMS` strips nothing and the content stays, which is the direction
|
|
33
|
+
// that fails closed. `clay="no-copy-unless:paid-forms"` is the approved spelling of the
|
|
34
|
+
// same condition.
|
|
35
|
+
function paidMarkersOf(node) {
|
|
36
|
+
const markers = [];
|
|
37
|
+
const value = attributeOf(node, PAID_ONLY);
|
|
38
|
+
if (value !== null) markers.push(value);
|
|
39
|
+
if (clayTokensOf(node).includes(CONDITIONAL_FORMS)) markers.push("forms");
|
|
40
|
+
return [...new Set(markers)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lastDescendantEnd(node) {
|
|
44
|
+
let end = null;
|
|
45
|
+
eachElement(node, (entry) => {
|
|
46
|
+
const entryEnd = entry.sourceCodeLocation?.endOffset;
|
|
47
|
+
if (typeof entryEnd === "number" && entryEnd >= 0 && (end === null || entryEnd > end)) end = entryEnd;
|
|
48
|
+
});
|
|
49
|
+
return end;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function endOffsetFor(node) {
|
|
53
|
+
const location = node.sourceCodeLocation;
|
|
54
|
+
if (typeof location?.endOffset === "number" && location.endOffset >= 0) return location.endOffset;
|
|
55
|
+
const fallback = lastDescendantEnd(node) ?? location?.startTag?.endOffset ?? null;
|
|
56
|
+
if (fallback === null) return null;
|
|
57
|
+
console.warn(`clayjs: <${node.tagName}> has no end offset; removing through ${fallback}`);
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markedRanges(html, features) {
|
|
62
|
+
const ranges = [];
|
|
63
|
+
eachElement(parse(html, { sourceCodeLocationInfo: true }), (node) => {
|
|
64
|
+
const markers = paidMarkersOf(node);
|
|
65
|
+
const unconditional = clayTokensOf(node).includes(NO_COPY);
|
|
66
|
+
const conditional = features === null ? markers.length > 0 : markers.some((marker) => features.includes(marker));
|
|
67
|
+
if (!unconditional && !conditional) return;
|
|
68
|
+
const start = node.sourceCodeLocation?.startOffset;
|
|
69
|
+
const end = typeof start === "number" ? endOffsetFor(node) : null;
|
|
70
|
+
if (typeof start === "number" && start >= 0 && end !== null && end > start) ranges.push({ start, end });
|
|
71
|
+
});
|
|
72
|
+
return ranges.filter((range, index) => !ranges.some((other, otherIndex) =>
|
|
73
|
+
otherIndex !== index && other.start <= range.start && range.end <= other.end));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function leadingRunStart(html, start) {
|
|
77
|
+
const newline = html.lastIndexOf("\n", start - 1);
|
|
78
|
+
if (newline < 0 || /\S/.test(html.slice(newline + 1, start))) return start;
|
|
79
|
+
return html[newline - 1] === "\r" ? newline - 1 : newline;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function spliceRanges(html, ranges) {
|
|
83
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
84
|
+
let stripped = html;
|
|
85
|
+
let limit = html.length;
|
|
86
|
+
for (let index = sorted.length - 1; index >= 0; index -= 1) {
|
|
87
|
+
const { start, end } = sorted[index];
|
|
88
|
+
if (end > limit) continue;
|
|
89
|
+
const from = leadingRunStart(html, start);
|
|
90
|
+
if (from >= end) continue;
|
|
91
|
+
stripped = stripped.slice(0, from) + stripped.slice(end);
|
|
92
|
+
limit = from;
|
|
93
|
+
}
|
|
94
|
+
return stripped;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `features` decides the conditional regions: null (the default, and the offline
|
|
98
|
+
// destination) strips every one of them, while a list strips only the missing features
|
|
99
|
+
// it names. Unconditional `no-copy` content always goes.
|
|
100
|
+
export function stripCopyContent(html, features = null) {
|
|
101
|
+
if (typeof html !== "string" || !/clay|data-paid-only/i.test(html)) return html;
|
|
102
|
+
return spliceRanges(html, markedRanges(html, features));
|
|
103
|
+
}
|
package/src/core/download.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Saving a page as a file the browser downloads. Kept apart from the conflict copy
|
|
2
2
|
// builders so an editor that only offers "download this page" loads none of them.
|
|
3
3
|
|
|
4
|
+
const HTML_TYPES = ["text/html", "application/xhtml+xml"];
|
|
5
|
+
|
|
6
|
+
function isHtml(type) {
|
|
7
|
+
if (typeof type !== "string") return false;
|
|
8
|
+
return HTML_TYPES.includes(type.split(";")[0].trim().toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export function documentName(pathname = location.pathname) {
|
|
5
12
|
const last = pathname.split("/").filter(Boolean).pop();
|
|
6
13
|
if (!last) return pathname.endsWith("/") || pathname === "" ? "index" : "page";
|
|
@@ -9,10 +16,15 @@ export function documentName(pathname = location.pathname) {
|
|
|
9
16
|
return name.replace(/\.(html?|htmlclay)$/i, "") || "page";
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
export function downloadHtml({ html, name, type = "text/html" }) {
|
|
19
|
+
export async function downloadHtml({ html, name, type = "text/html" }) {
|
|
13
20
|
let url = null;
|
|
14
21
|
try {
|
|
15
|
-
|
|
22
|
+
let contents = html;
|
|
23
|
+
if (isHtml(type) && typeof html === "string" && /clay|data-paid-only/i.test(html)) {
|
|
24
|
+
const { stripCopyContent } = await import("./copy-content.js");
|
|
25
|
+
contents = stripCopyContent(html);
|
|
26
|
+
}
|
|
27
|
+
url = URL.createObjectURL(new Blob([contents], { type }));
|
|
16
28
|
const a = document.createElement("a");
|
|
17
29
|
a.setAttribute("clay", "no-save no-watch no-snapshot");
|
|
18
30
|
a.href = url;
|
|
@@ -57,8 +57,8 @@ function place() {
|
|
|
57
57
|
// The safe copy the conflict notice offers, for the same reason: a file the person can
|
|
58
58
|
// open has to exist outside a host that will not store these bytes. It already reports
|
|
59
59
|
// its own failure.
|
|
60
|
-
function download() {
|
|
61
|
-
downloadStored({});
|
|
60
|
+
async function download() {
|
|
61
|
+
await downloadStored({});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function build() {
|
package/src/core/upgrade-run.js
CHANGED
|
@@ -348,7 +348,11 @@ export async function pageBytes(clay, { loc = location, fetchFn = (url, init) =>
|
|
|
348
348
|
export async function downloadCopy({ clay, doc = document, loc = location, fetchFn } = {}) {
|
|
349
349
|
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return false;
|
|
350
350
|
try {
|
|
351
|
-
|
|
351
|
+
let bytes = await pageBytes(clay, { loc, fetchFn });
|
|
352
|
+
if (typeof bytes === "string" && /clay|data-paid-only/i.test(bytes)) {
|
|
353
|
+
const { stripCopyContent } = await import("./copy-content.js");
|
|
354
|
+
bytes = stripCopyContent(bytes);
|
|
355
|
+
}
|
|
352
356
|
const url = URL.createObjectURL(new Blob([bytes], { type: "text/html" }));
|
|
353
357
|
const a = doc.createElement("a");
|
|
354
358
|
a.setAttribute("clay", "editor-ui");
|
package/src/lib/region-policy.js
CHANGED
|
@@ -54,7 +54,7 @@ export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-u
|
|
|
54
54
|
// Every canonical token spellable inside the space-separated `clay` attribute.
|
|
55
55
|
// Exported: this list used to exist as a private CLAY_TOKENS that nothing read,
|
|
56
56
|
// while callers hardcoded their own copies.
|
|
57
|
-
export const TOKENS = [...POLICY_TOKENS];
|
|
57
|
+
export const TOKENS = [...POLICY_TOKENS, 'no-copy', 'no-copy-unless:paid-forms'];
|
|
58
58
|
|
|
59
59
|
// True when a region marker is present, whether spelled as a `clay` token
|
|
60
60
|
// (whitespace-token semantics, matching [clay~=token]) or a legacy bare attribute.
|
package/src/loader-logic.js
CHANGED
|
@@ -62,6 +62,9 @@ export const PLUGIN_PATHS = {
|
|
|
62
62
|
// A visitor's contact form to the document's owner. Not editOnly: visitors are
|
|
63
63
|
// who send messages.
|
|
64
64
|
message: { path: "plugins/message.js", editOnly: false, default: false },
|
|
65
|
+
// A like button on any element, counted by the host. Not editOnly: visitors are
|
|
66
|
+
// who like.
|
|
67
|
+
like: { path: "plugins/like.js", editOnly: false, default: false },
|
|
65
68
|
// Edit mode only: the AI comment box is an editing gesture. Opt in with
|
|
66
69
|
// `plugins=ai-edit`; it stays dormant on any host that does not list a ready
|
|
67
70
|
// `ai-edit` helper.
|
|
@@ -139,6 +142,7 @@ export const MODULES = {
|
|
|
139
142
|
"plugins/menu.js": () => import("./plugins/menu.js"),
|
|
140
143
|
"plugins/wire.js": () => import("./plugins/wire.js"),
|
|
141
144
|
"plugins/message.js": () => import("./plugins/message.js"),
|
|
145
|
+
"plugins/like.js": () => import("./plugins/like.js"),
|
|
142
146
|
"plugins/ai-edit.js": () => import("./plugins/ai-edit.js"),
|
|
143
147
|
"plugins/demo.js": () => import("./plugins/demo.js"),
|
|
144
148
|
"plugins/visitor-drafts.js": () => import("./plugins/visitor-drafts.js"),
|
|
@@ -151,7 +155,7 @@ export const MODULES = {
|
|
|
151
155
|
|
|
152
156
|
// `source` is last on purpose: its install captures a save clone, so it wants every
|
|
153
157
|
// plugin that registers a document transform to have registered it first.
|
|
154
|
-
const PLUGIN_ORDER = ["visitor-drafts", "richclay", "indicator", "sortable", "undo", "unsent", "quickcrop", "upload", "people", "menu", "upgrade", "cms", "page-view", "sync", "live", "page", "message", "wire", "ai-edit", "demo", "derive", "bind-text", "source"];
|
|
158
|
+
const PLUGIN_ORDER = ["visitor-drafts", "richclay", "indicator", "sortable", "undo", "unsent", "quickcrop", "upload", "people", "menu", "upgrade", "cms", "page-view", "sync", "live", "page", "message", "like", "wire", "ai-edit", "demo", "derive", "bind-text", "source"];
|
|
155
159
|
|
|
156
160
|
// A plugin that cannot do its whole job alone. hypercms reads the cropper through
|
|
157
161
|
// a capability lookup (`clay.quickcrop`) and silently uploads the raw file when it
|
package/src/loader.js
CHANGED
|
@@ -371,6 +371,8 @@ function attachPluginMember(path, mod, namespaces = loaded) {
|
|
|
371
371
|
Object.defineProperty(clay, "me", { get: mod.currentMe, configurable: true, enumerable: true });
|
|
372
372
|
} else if (path === "plugins/message.js") {
|
|
373
373
|
clay.message = mod.message;
|
|
374
|
+
} else if (path === "plugins/like.js") {
|
|
375
|
+
clay.like = mod.like;
|
|
374
376
|
} else if (path === "plugins/wire.js") {
|
|
375
377
|
clay.wire = mod.wire || mod.default;
|
|
376
378
|
} else if (path === "plugins/ai-edit.js") {
|
|
@@ -112,12 +112,12 @@ export function createFeature({ view = null, download = downloadHtml, fetch: fet
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function decide(s) {
|
|
115
|
-
listen(s, s.panel, "click", (event) => {
|
|
115
|
+
listen(s, s.panel, "click", async (event) => {
|
|
116
116
|
const b = event.target.closest?.("[data-act]");
|
|
117
117
|
if (!b) return;
|
|
118
118
|
event.preventDefault();
|
|
119
119
|
if (b.dataset.act === "close") close();
|
|
120
|
-
else if (b.dataset.act === "download") save(s);
|
|
120
|
+
else if (b.dataset.act === "download") await save(s);
|
|
121
121
|
else if (b.dataset.act === "retry") run().catch((error) => console.error("clayjs: the reading copy could not be made", error));
|
|
122
122
|
});
|
|
123
123
|
listen(s, doc, "keydown", (event) => {
|
|
@@ -130,9 +130,9 @@ export function createFeature({ view = null, download = downloadHtml, fetch: fet
|
|
|
130
130
|
|
|
131
131
|
// The one place a reading copy becomes a file. The download is handed the bytes that
|
|
132
132
|
// were shown, never a second render, so what a person saw is what they get.
|
|
133
|
-
function save(s) {
|
|
133
|
+
async function save(s) {
|
|
134
134
|
if (!s.ready || disposed) return { ok: false, code: "not-editing" };
|
|
135
|
-
const saved = download({ html: s.ready.html, name: s.ready.name });
|
|
135
|
+
const saved = await download({ html: s.ready.html, name: s.ready.name });
|
|
136
136
|
close();
|
|
137
137
|
return { ok: saved === true, downloaded: saved === true };
|
|
138
138
|
}
|
|
@@ -83,10 +83,10 @@ export function createFeature({ view = null, download = downloadHtml } = {}) {
|
|
|
83
83
|
s.panel.replaceChildren(...parts);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function save() {
|
|
86
|
+
async function save() {
|
|
87
87
|
const s = session;
|
|
88
88
|
if (!s) return { ok: false, code: "no-export" };
|
|
89
|
-
if (download({ html: s.report.text, name: s.name, type: "text/markdown" })) {
|
|
89
|
+
if (await download({ html: s.report.text, name: s.name, type: "text/markdown" })) {
|
|
90
90
|
close();
|
|
91
91
|
view.focus?.();
|
|
92
92
|
return { ok: true, downloaded: true };
|
package/src/page/recovery-ui.js
CHANGED
|
@@ -228,12 +228,12 @@ export function createRecoveryReview({
|
|
|
228
228
|
button.setAttribute("aria-expanded", "false");
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
const download = () => {
|
|
231
|
+
const download = async () => {
|
|
232
232
|
const edits = [];
|
|
233
233
|
for (const row of rows) edits.push({ id: row.id, kind: "recovered-page", name: row.message, yours: row.text, now: "", sentence: row.message });
|
|
234
234
|
const composing = "Text typed while composing never reached the page.";
|
|
235
235
|
for (const row of journaled) edits.push({ id: row.id, kind: "composing", name: "Typed text", yours: row.text, now: "", sentence: composing });
|
|
236
|
-
if (!downloadStored({ html: null, edits })) failure = "That download could not be made.";
|
|
236
|
+
if (!(await downloadStored({ html: null, edits }))) failure = "That download could not be made.";
|
|
237
237
|
draw();
|
|
238
238
|
};
|
|
239
239
|
|
package/src/page/save-states.js
CHANGED
|
@@ -67,14 +67,14 @@ function withoutPageTag(html) {
|
|
|
67
67
|
// One complete, openable file: the capture the save would have sent, with the
|
|
68
68
|
// page projection already applied by the live store's snapshot listener, and
|
|
69
69
|
// without the page editor's merge state, which no saved file or download carries.
|
|
70
|
-
function downloadPageCopy() {
|
|
70
|
+
async function downloadPageCopy() {
|
|
71
71
|
const { forSave } = captureForSaveAndComparison({ emitForSync: false });
|
|
72
|
-
return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
72
|
+
return await downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// A copy of the page captured earlier (captureForSaveAndComparison().forSave), as a download.
|
|
76
|
-
function downloadSnapshot(forSave) {
|
|
77
|
-
return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
76
|
+
async function downloadSnapshot(forSave) {
|
|
77
|
+
return await downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// The article's own plain text, never the editor's chrome: the capture carries
|