@panphora/clayjs 0.7.3 → 1.0.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 +78 -87
- package/THIRD-PARTY-NOTICES.md +2 -4
- package/{all.js → entries/all.js} +7 -1
- package/{clay-dom.js → entries/clay-dom.js} +7 -1
- package/{clay-events.js → entries/clay-events.js} +7 -1
- package/{clay-internals.js → entries/clay-internals.js} +7 -1
- package/{clay-options.js → entries/clay-options.js} +7 -1
- package/{clay-ui.js → entries/clay-ui.js} +7 -1
- package/{clay-utils.js → entries/clay-utils.js} +7 -1
- package/{clay.js → entries/clay.js} +15 -1
- package/package.json +38 -14
- package/src/core/autosave.js +3 -2
- package/src/core/persist.js +3 -2
- package/src/lib/attr-aliases.js +13 -0
- package/src/lib/dirty-gate.js +2 -1
- package/src/plugins/demo.js +2 -2
- package/src/vendor/richclay.vendor.js +15 -15
- package/_headers +0 -14
- /package/{clay-data.js → entries/clay-data.js} +0 -0
- /package/{sap.js → entries/sap.js} +0 -0
package/README.md
CHANGED
|
@@ -4,39 +4,63 @@
|
|
|
4
4
|
|
|
5
5
|
Self-saving, malleable HTML in one classic `<script>`. Load `clay.js` on a page and it
|
|
6
6
|
becomes editable in place, snapshots its own DOM, and saves that DOM back to the file it
|
|
7
|
-
came from. No build step, no framework.
|
|
7
|
+
came from. No build step, no framework, no account.
|
|
8
|
+
|
|
9
|
+
The document is the app *and* the database. State lives in the DOM, and a save is one POST
|
|
10
|
+
of the entire serialized document. There is no data layer, no JSON on a server, no schema.
|
|
11
|
+
Open the saved file in a text editor and your edit is right there in the HTML.
|
|
12
|
+
|
|
13
|
+
## See it save itself
|
|
14
|
+
|
|
15
|
+
[`examples/notes.html`](https://github.com/panphora/clayjs/blob/main/examples/notes.html) is a complete self-saving document. Two
|
|
16
|
+
attributes do the work: `autosave` on `<html>`, and `editable` on the parts you type into.
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<!DOCTYPE html>
|
|
20
|
+
<html lang="en" autosave>
|
|
21
|
+
<head><meta charset="utf-8"><title>Notes</title></head>
|
|
22
|
+
<body>
|
|
23
|
+
<h1 editable>My notes</h1>
|
|
24
|
+
<div editable><p>Type anything here.</p></div>
|
|
25
|
+
<script src="https://clayjs.com/v1/clay.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Type into it, then open the file in a text editor: your words are in the HTML.
|
|
31
|
+
|
|
32
|
+
**Something has to write the bytes.** clayjs runs in the browser; it makes the document
|
|
33
|
+
and posts it, and a host puts it on disk. The shortest path is
|
|
34
|
+
[HTML Clay](https://htmlclay.com), a desktop app: rename the file to `.htmlclay` and
|
|
35
|
+
double-click it, and it serves the file at `http://127.0.0.1` and writes every save back.
|
|
36
|
+
[hyperclay.com](https://hyperclay.com) hosts the same file online. Your own server needs
|
|
37
|
+
one route, about twenty lines: see [the endpoint spec](https://clayjs.com/docs#endpoint).
|
|
38
|
+
Full walkthrough: [the tutorial](https://clayjs.com/get-started) and
|
|
39
|
+
[`examples/`](https://github.com/panphora/clayjs/tree/main/examples).
|
|
8
40
|
|
|
9
41
|
## Use
|
|
10
42
|
|
|
11
43
|
```html
|
|
12
|
-
<script src="https://clayjs.com/clay.js"></script>
|
|
44
|
+
<script src="https://clayjs.com/v1/clay.js"></script>
|
|
13
45
|
```
|
|
14
46
|
|
|
15
47
|
The loader detects edit vs view mode and pulls only the modules it needs. Tune it with
|
|
16
48
|
query params on the script URL:
|
|
17
49
|
|
|
18
50
|
```html
|
|
19
|
-
<script src="/clay.js?plugins=sync,cms&exclude=indicator"></script>
|
|
51
|
+
<script src="https://clayjs.com/v1/clay.js?plugins=sync,cms&exclude=indicator"></script>
|
|
20
52
|
```
|
|
21
53
|
|
|
22
54
|
- `?plugins=` — add optional plugins: `sync`, `cms`, `undo`, `sortable`, `indicator`, `quickcrop`, `upload`, `wire`, `demo`.
|
|
23
55
|
Only `richclay` loads by default, and only in edit mode. `cms` brings `quickcrop` with it, because
|
|
24
56
|
the CMS uses it for `data-hcms-crop` image fields.
|
|
25
57
|
- `?exclude=` — drop a plugin that would otherwise load (a default, or one another plugin pulled in).
|
|
26
|
-
- `?editmode=false` — force view mode (URL param wins over everything
|
|
27
|
-
|
|
28
|
-
## Host attributes
|
|
58
|
+
- `?editmode=false` — force view mode (URL param wins over everything else).
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `savetoken="…"` — the per-document save credential. clayjs posts to
|
|
35
|
-
`/_/save/{token}` and sends no cookies, because the token is the credential.
|
|
36
|
-
A token also implies edit mode, which is the only such signal a sandboxed
|
|
37
|
-
document can see. `htmlclaytoken` is the older spelling of the same thing.
|
|
38
|
-
Edit mode is decided in this order: the `?editmode` param, then
|
|
39
|
-
`window.clayEditMode`, then a save token, then the platform's owner cookie.
|
|
60
|
+
Edit mode is decided in this order: the `?editmode` param, then `window.clayEditMode`,
|
|
61
|
+
then a save token stamped on `<html>` by the host, then the platform's owner cookie. Hosts
|
|
62
|
+
and save tokens are covered in [the reference](https://github.com/panphora/clayjs/blob/main/docs/reference.md) and on
|
|
63
|
+
[clayjs.com/docs](https://clayjs.com/docs#editmode).
|
|
40
64
|
|
|
41
65
|
## Readiness
|
|
42
66
|
|
|
@@ -48,12 +72,6 @@ await clay.ready; // or: document.addEventListener("clay:ready", ...)
|
|
|
48
72
|
clay.save();
|
|
49
73
|
```
|
|
50
74
|
|
|
51
|
-
Edit mode exposes `clay.save()` (+ `clay.save.force()`), `clay.getHTML()`, `clay.addDocumentTransform(fn)`,
|
|
52
|
-
`clay.onSnapshot(fn)`, `clay.toggleEditMode()`, `clay.isEditMode`, `clay.isOwner`, `clay.Mutation`,
|
|
53
|
-
`clay.region`, `clay.cacheBust(el)`, plus `clay.undo` / `clay.cms` / `clay.morph` / `clay.RichClay` /
|
|
54
|
-
`clay.quickcrop` when those plugins load. View mode keeps only the always-available members (`toggleEditMode`, `isEditMode`, `isOwner`,
|
|
55
|
-
`Mutation`, `region`, `ready`); edit-only members are simply absent.
|
|
56
|
-
|
|
57
75
|
## API
|
|
58
76
|
|
|
59
77
|
Three tiers. The first two are a promise; the third is not.
|
|
@@ -64,55 +82,23 @@ Three tiers. The first two are a promise; the third is not.
|
|
|
64
82
|
| `clay.*` from a satellite (`clay-ui`, `clay-utils`, `clay-dom`, `clay-events`, `clay-options`, `clay-internals`) | opt-in, one script tag each | stable |
|
|
65
83
|
| anything else under `src/` | reachable by direct import, because `src/` ships | **may change in any release** |
|
|
66
84
|
|
|
67
|
-
The contract starts at **0.
|
|
85
|
+
The contract starts at **1.0.0**: no name below changes without a major version.
|
|
68
86
|
|
|
69
87
|
**`clay.js`** — `ready`, `save()`, `save.force()`, `getHTML()`, `addDocumentTransform(fn)`, `onSnapshot(fn)`,
|
|
70
88
|
`toggleEditMode()`, `isEditMode`, `isOwner`, `Mutation`, `region`, `cacheBust(el)`, plus `undo` / `cms` / `morph` /
|
|
71
|
-
`RichClay` / `quickcrop` when those plugins load. View mode keeps only the always-available members
|
|
72
|
-
|
|
73
|
-
`addDocumentTransform(fn)` runs your callback over a detached clone whenever the page
|
|
74
|
-
prepares to save AND whenever it checks whether anything changed. Keep it pure and
|
|
75
|
-
repeatable: it is a transform, not a "a save is happening" event.
|
|
76
|
-
|
|
77
|
-
`quickcrop(file, options)` opens a crop modal over a File or Blob and resolves
|
|
78
|
-
`{ blob, dataURL, width, height }`, or `null` if the person cancels. `aspect` (a number,
|
|
79
|
-
or null for freeform), `type`, `quality`, `maxWidth`/`maxHeight` and `labels.confirm` are
|
|
80
|
-
the options worth knowing. Its modal and injected stylesheet carry `save-remove`, so they
|
|
81
|
-
never reach the saved file.
|
|
82
|
-
|
|
83
|
-
`region` is the region-policy model: `resolveRegionPolicy`, `isInert`, `skipForPolicy`,
|
|
84
|
-
`strictestPolicy`, the `PERSIST` and `REGION_ATTRS` token constants, and the
|
|
85
|
-
`STRIP_FROM_SAVE`, `FREEZE_SELECTOR` and `STRIP_FROM_COMPARISON` selectors.
|
|
86
|
-
`clay.internals.region` is a different shape, narrower in aim, built for snapshot work.
|
|
89
|
+
`RichClay` / `quickcrop` when those plugins load. View mode keeps only the always-available members
|
|
90
|
+
(`toggleEditMode`, `isEditMode`, `isOwner`, `Mutation`, `region`, `ready`); edit-only members are simply absent.
|
|
87
91
|
|
|
88
92
|
**Satellites** — one script tag each, and each resolves its own `clay.loaded.*` promise. `clay-ui` adds
|
|
89
93
|
`toast`, `toastPersistent`, `ask`, `confirm`, `tell`, `snippet`, `modal`; `clay-utils` adds `clay.utils`
|
|
90
94
|
(`throttle`, `debounce`, `cookie`, `slugify`, `copyToClipboard`); `clay-internals` adds `clay.internals`,
|
|
91
|
-
|
|
95
|
+
the low-level surface for code that needs to sit *inside* the save lifecycle rather than call it.
|
|
96
|
+
`clay-events`, `clay-dom`, `clay-options` and `all.js` add HTML attributes and DOM helpers rather
|
|
92
97
|
than members on `clay`.
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```html
|
|
99
|
-
<script src="https://clayjs.com/clay-internals.js"></script>
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
- `captureSnapshot()`, `captureForSave()` — the snapshot pipeline, read side.
|
|
103
|
-
`captureSnapshot` gives you the clone before any stripping; `captureForSave` gives you the
|
|
104
|
-
bytes a save would send.
|
|
105
|
-
- `addDocumentTransform(fn)` — the same registry as `clay.addDocumentTransform`. Core only
|
|
106
|
-
publishes that name in edit mode, so reach for this one from a view-mode page or from the
|
|
107
|
-
satellite with no core loaded.
|
|
108
|
-
- `region.addRegionToken(el, token)`, `region.resolveRegionPolicy(node)`, `region.isInert(node)`,
|
|
109
|
-
`region.isSnapshotRemoved(el)`, `region.PERSIST`, `region.REGION_ATTRS`, and
|
|
110
|
-
`region.selectors.stripFromSave` / `.stripFromComparison` / `.stripFromDirtyCheck` /
|
|
111
|
-
`.noTriggerAutosave` / `.noDirty` / `.snapshotRemove` / `.freeze` — write your own attribute
|
|
112
|
-
without hardcoding our selectors.
|
|
113
|
-
- `save.saveHtml(html, cb, opts)`, `save.replacePageWith(url, cb)`, `save.isSaveInProgress()` — the save
|
|
114
|
-
lane under `clay.save`. **`saveHtml` writes the bytes you hand it straight to the file**, bypassing the
|
|
115
|
-
snapshot pipeline entirely; check `isSaveInProgress()` first.
|
|
99
|
+
Every member, every attribute, and the endpoint spec: **[docs/reference.md](https://github.com/panphora/clayjs/blob/main/docs/reference.md)**,
|
|
100
|
+
also served at [clayjs.com/llms.txt](https://clayjs.com/llms.txt) and rendered as
|
|
101
|
+
[clayjs.com/docs](https://clayjs.com/docs).
|
|
116
102
|
|
|
117
103
|
## Regions
|
|
118
104
|
|
|
@@ -133,41 +119,46 @@ and live sync will not overwrite it, while a `no-dirty` region renders itself fr
|
|
|
133
119
|
so it never warns and an incoming sync frame may replace it. Use `no-dirty` for filter bars,
|
|
134
120
|
projections and drag previews; use `no-trigger-autosave` for a heavy editor you save by hand.
|
|
135
121
|
|
|
122
|
+
## Repo map
|
|
123
|
+
|
|
124
|
+
**Everything in `entries/` is served under a version prefix on clayjs.com.**
|
|
125
|
+
`entries/clay.js` is `https://clayjs.com/v1/clay.js`, which rolls forward within major
|
|
126
|
+
version 1, and `https://clayjs.com/1.0.0/clay.js`, which never changes. Same for each
|
|
127
|
+
satellite. A saved document hardcodes its script URL and has no update channel, which is
|
|
128
|
+
why the version is in the URL and why a served path, once published, is permanent. Repo
|
|
129
|
+
paths are not: `build.js` flattens `entries/` into each prefix on the way out. `clay.js`
|
|
130
|
+
derives its own base URL from its script tag and imports `<base>/src/loader.js`, so `src/`
|
|
131
|
+
is public surface too and ships inside every prefix as its sibling.
|
|
132
|
+
|
|
133
|
+
| | |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `entries/clay.js` | the bootstrap: everything starts here |
|
|
136
|
+
| `entries/clay-ui.js`, `-dom`, `-events`, `-options`, `-utils`, `-internals` | satellites, one script tag each |
|
|
137
|
+
| `entries/all.js` | `All(selector)`, a chainable `querySelectorAll` wrapper |
|
|
138
|
+
| `entries/sap.js` | reactive templating, **generated** from the `sapjs` repo |
|
|
139
|
+
| `entries/clay-data.js` | the HTML data API, **generated** from the `hyper-html-api` repo |
|
|
140
|
+
| `src/` | the implementation, and public surface by direct import |
|
|
141
|
+
| `conformance/` | the byte-exact gate: real browser, pinned Chromium, goldens from the spec |
|
|
142
|
+
| `tests/` | jest unit suite, fixtures, and a stub save server |
|
|
143
|
+
| `website/` | the source of clayjs.com |
|
|
144
|
+
| `docs/reference.md` | the full reference, served as `/llms.txt` |
|
|
145
|
+
| `build.js`, `wrangler.jsonc` | assemble and deploy `public/`, which is a gitignored build output |
|
|
146
|
+
|
|
136
147
|
## Develop
|
|
137
148
|
|
|
138
149
|
```bash
|
|
139
150
|
npm test # jest unit suite
|
|
140
151
|
npm run test:conformance # byte-for-byte fixture gate, in a real browser
|
|
141
152
|
npm run dev # stub save server on :4601 for the tests/fixtures pages
|
|
153
|
+
npm run build # rebuild public/, the deploy output
|
|
142
154
|
```
|
|
143
155
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`public/` is what wrangler serves to clayjs.com. It is a build output: gitignored,
|
|
147
|
-
disposable, and rebuilt from scratch every time.
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
npm run build # rebuild public/ from source
|
|
151
|
-
npm run deploy # rebuild, then wrangler deploy
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Never edit `public/` by hand, and never copy a file into it. Its contents are
|
|
155
|
-
**derived**:
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
public/ = package.json "files" (minus THIRD-PARTY-NOTICES.md, which nothing requests)
|
|
159
|
-
+ website/* (flattened to the root, so /docs.html works)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
That derivation is the point. `files` is the list npm publishes, so it is already
|
|
163
|
-
the line you edit to ship a new satellite, and deriving from it means a file cannot
|
|
164
|
-
reach npm and miss the site. Before this script existed the mirror was hand-copied,
|
|
165
|
-
and it drifted exactly that way: `src/core/host-attrs.js` never made it across, and
|
|
166
|
-
since `loader.js` imports `is-edit-mode.js`, which imports it, the deployed
|
|
167
|
-
`clay.js` could not boot at all.
|
|
156
|
+
Setup, the pinned-browser requirement, and the things about this repo that will surprise
|
|
157
|
+
you are in [CONTRIBUTING.md](https://github.com/panphora/clayjs/blob/main/CONTRIBUTING.md).
|
|
168
158
|
|
|
169
159
|
## License
|
|
170
160
|
|
|
171
161
|
Our code is MIT-0 (MIT No Attribution): use it, remix it, ship it, no attribution
|
|
172
162
|
needed. Vendored third-party files keep their original permissive licenses; see
|
|
173
|
-
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
|
|
163
|
+
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md). Security reports go through
|
|
164
|
+
[the security policy](https://github.com/panphora/clayjs/blob/main/.github/SECURITY.md).
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -12,15 +12,13 @@ or carry third-party code, and keep their original licenses.
|
|
|
12
12
|
|
|
13
13
|
## Squire 2.4.8 (MIT)
|
|
14
14
|
|
|
15
|
-
- Carried inside src/vendor/richclay.vendor.js
|
|
16
|
-
(richclay bundles Squire)
|
|
15
|
+
- Carried inside src/vendor/richclay.vendor.js (richclay bundles Squire)
|
|
17
16
|
- Copyright (c) 2011-2023 by Neil Jenkins
|
|
18
17
|
- https://github.com/fastmail/Squire
|
|
19
18
|
|
|
20
19
|
## DOMPurify 3.4.11 (Apache-2.0 OR MPL-2.0)
|
|
21
20
|
|
|
22
|
-
- Carried inside src/vendor/richclay.vendor.js
|
|
23
|
-
(richclay bundles DOMPurify)
|
|
21
|
+
- Carried inside src/vendor/richclay.vendor.js (richclay bundles DOMPurify)
|
|
24
22
|
- (c) Cure53 and other contributors
|
|
25
23
|
- https://github.com/cure53/DOMPurify
|
|
26
24
|
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
clay.loaded.all = import(base + "/src/dom/all.js")
|
|
15
21
|
.then(function (m) {
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
clay.loaded.dom = import(base + "/src/dom/dom-helpers.js")
|
|
15
21
|
.catch(function (err) { console.error("clay-dom failed to load:", err); throw err; });
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
// The mutation-backed attrs subscribe to a hub that observes document.body, so
|
|
15
21
|
// wait for DOM ready before importing (same gate as clay.js).
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
// No DOM gate: nothing here touches document.body at import time, unlike clay-ui.
|
|
15
21
|
clay.loaded.internals = import(base + "/src/internals/index.js")
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
// Subscribes to the mutation hub (observes document.body); wait for DOM ready.
|
|
15
21
|
function domReady() {
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
// Toasts and dialogs append to document.body, so `loaded.ui` must not resolve
|
|
15
21
|
// before the DOM is ready (same gate as clay-events).
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
var url = new URL(script.src, location.href);
|
|
12
|
-
|
|
12
|
+
// Query and fragment off first, then step out of the tarball's entries/ directory
|
|
13
|
+
// so package-path CDNs resolve src/. See clay.js for the full reason on both.
|
|
14
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
15
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
16
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
17
|
+
base = base.slice(0, -8);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
clay.loaded.utils = import(base + "/src/utils/index.js")
|
|
15
21
|
.catch(function (err) { console.error("clay-utils failed to load:", err); throw err; });
|
|
@@ -31,7 +31,21 @@
|
|
|
31
31
|
clay.__booted = true;
|
|
32
32
|
|
|
33
33
|
var url = new URL(script.src, location.href);
|
|
34
|
-
|
|
34
|
+
// The query and the fragment come off before the last slash is found. Leaving them
|
|
35
|
+
// on split the base inside them, so clay.js?next=/a/b imported ".../clay.js?next=/a/src/loader.js"
|
|
36
|
+
// and the library never booted.
|
|
37
|
+
var path = url.href.split("#")[0].split("?")[0];
|
|
38
|
+
var base = path.slice(0, path.lastIndexOf("/"));
|
|
39
|
+
// In the npm tarball the entry scripts sit in entries/ while src/ stays beside it
|
|
40
|
+
// at the package root, so a CDN that serves package paths literally (jsDelivr,
|
|
41
|
+
// unpkg) would look for src/ one directory too deep and load nothing. Stepping out
|
|
42
|
+
// of an "entries" segment makes those URLs resolve. clayjs.com never has the
|
|
43
|
+
// segment: build.js flattens entries/ into each version prefix, so this is a no-op
|
|
44
|
+
// there. The question is asked of the pathname, because asking it of the whole URL
|
|
45
|
+
// matched a host merely named "entries" and sent the import to another origin.
|
|
46
|
+
if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
|
|
47
|
+
base = base.slice(0, -8);
|
|
48
|
+
}
|
|
35
49
|
import(base + "/src/loader.js")
|
|
36
50
|
.then(function (m) { return m.boot(base, url.searchParams, clay.__readyResolve); })
|
|
37
51
|
.catch(function (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -9,18 +9,32 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/panphora/clayjs.git"
|
|
11
11
|
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/panphora/clayjs/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"html",
|
|
17
|
+
"self-saving",
|
|
18
|
+
"malleable",
|
|
19
|
+
"local-first",
|
|
20
|
+
"no-build",
|
|
21
|
+
"contenteditable",
|
|
22
|
+
"dom",
|
|
23
|
+
"autosave",
|
|
24
|
+
"single-file",
|
|
25
|
+
"static-site",
|
|
26
|
+
"vanilla-javascript",
|
|
27
|
+
"html-editor"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
"./THIRD-PARTY-NOTICES.md": "./THIRD-PARTY-NOTICES.md",
|
|
32
|
+
"./src/*": "./src/*",
|
|
33
|
+
"./entries/*": "./entries/*",
|
|
34
|
+
"./*": "./entries/*"
|
|
35
|
+
},
|
|
12
36
|
"files": [
|
|
13
|
-
"
|
|
14
|
-
"clay-ui.js",
|
|
15
|
-
"clay-internals.js",
|
|
16
|
-
"clay-events.js",
|
|
17
|
-
"clay-options.js",
|
|
18
|
-
"clay-dom.js",
|
|
19
|
-
"all.js",
|
|
20
|
-
"clay-utils.js",
|
|
21
|
-
"sap.js",
|
|
22
|
-
"clay-data.js",
|
|
23
|
-
"_headers",
|
|
37
|
+
"entries/",
|
|
24
38
|
"THIRD-PARTY-NOTICES.md",
|
|
25
39
|
"src/"
|
|
26
40
|
],
|
|
@@ -29,8 +43,8 @@
|
|
|
29
43
|
},
|
|
30
44
|
"scripts": {
|
|
31
45
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
32
|
-
"test:conformance": "UPDATE_GOLDENS=0 web-test-runner",
|
|
33
|
-
"conformance:update": "UPDATE_GOLDENS=1 web-test-runner",
|
|
46
|
+
"test:conformance": "UPDATE_GOLDENS=0 web-test-runner --config conformance/wtr.config.mjs",
|
|
47
|
+
"conformance:update": "UPDATE_GOLDENS=1 web-test-runner --config conformance/wtr.config.mjs",
|
|
34
48
|
"prepublishOnly": "npm test && npm run test:conformance",
|
|
35
49
|
"dev": "node tests/stub-server.mjs",
|
|
36
50
|
"build": "node build.js",
|
|
@@ -49,5 +63,15 @@
|
|
|
49
63
|
"kind": "lib",
|
|
50
64
|
"status": "active",
|
|
51
65
|
"url": "https://clayjs.com"
|
|
66
|
+
},
|
|
67
|
+
"jest": {
|
|
68
|
+
"testEnvironment": "jsdom",
|
|
69
|
+
"testMatch": [
|
|
70
|
+
"**/tests/unit/**/*.test.js"
|
|
71
|
+
],
|
|
72
|
+
"transform": {},
|
|
73
|
+
"setupFiles": [
|
|
74
|
+
"<rootDir>/tests/jest.setup.js"
|
|
75
|
+
]
|
|
52
76
|
}
|
|
53
77
|
}
|
package/src/core/autosave.js
CHANGED
|
@@ -15,6 +15,7 @@ import { isEditMode } from "./is-edit-mode.js";
|
|
|
15
15
|
import { savePageThrottled } from "./save.js";
|
|
16
16
|
import { markUserDriven } from "../lib/user-gesture.js";
|
|
17
17
|
import { resolveRegionPolicy, skipForPolicy } from "../lib/region-policy.js";
|
|
18
|
+
import { PERSIST, AUTOSAVE } from "../lib/attr-aliases.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Initialize auto-save on DOM changes
|
|
@@ -47,7 +48,7 @@ function initSavePageOnChange() {
|
|
|
47
48
|
let inputSaveTimer = null;
|
|
48
49
|
function initSaveOnPersistInput() {
|
|
49
50
|
document.addEventListener('input', (e) => {
|
|
50
|
-
if (!e.target.closest(
|
|
51
|
+
if (!e.target.closest(PERSIST)) return;
|
|
51
52
|
// A trusted input on a [persist] field is itself a user-driven change the
|
|
52
53
|
// Mutation hub can't see (form values aren't DOM mutations). Attribute it,
|
|
53
54
|
// but only when the field is inside the dirty domain: a [persist] control in
|
|
@@ -62,7 +63,7 @@ function initSaveOnPersistInput() {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function init() {
|
|
65
|
-
if (!document.documentElement.
|
|
66
|
+
if (!document.documentElement.matches(AUTOSAVE)) return;
|
|
66
67
|
if (!isEditMode) return;
|
|
67
68
|
// initUserGesture moved to save.js's init: gesture provenance belongs to every
|
|
68
69
|
// editable page, not only the ones with <html autosave>.
|
package/src/core/persist.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { onSnapshot } from './snapshot.js';
|
|
2
2
|
import { serializeControlToAttributes, finalizeControlForSave } from '../vendor/control-serialize.vendor.js';
|
|
3
|
+
import { PERSIST } from '../lib/attr-aliases.js';
|
|
3
4
|
|
|
4
5
|
// Persistent Form Input Values
|
|
5
6
|
//
|
|
@@ -51,7 +52,7 @@ import { serializeControlToAttributes, finalizeControlForSave } from '../vendor/
|
|
|
51
52
|
// When event-attrs is loaded, its cloneNode() intercept (onclone.js) patches
|
|
52
53
|
// data-value into textContent on cloned textareas automatically.
|
|
53
54
|
|
|
54
|
-
export default function enablePersistentFormInputValues(filterBySelector =
|
|
55
|
+
export default function enablePersistentFormInputValues(filterBySelector = PERSIST) {
|
|
55
56
|
const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
|
|
56
57
|
const textareaSelector = `textarea${filterBySelector}`;
|
|
57
58
|
const selectSelector = `select${filterBySelector}`;
|
|
@@ -106,7 +107,7 @@ export default function enablePersistentFormInputValues(filterBySelector = "[per
|
|
|
106
107
|
|
|
107
108
|
// Auto-initialize with default selector
|
|
108
109
|
export function init() {
|
|
109
|
-
enablePersistentFormInputValues(
|
|
110
|
+
enablePersistentFormInputValues(PERSIST);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// Auto-init when module is imported
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// clayjs spells its attributes without a prefix, which is the whole pitch:
|
|
2
|
+
// <h1 editable> reads like HTML rather than like a framework. No attribute in the
|
|
3
|
+
// HTML standard uses these names and no proposal does either, so the risk of the
|
|
4
|
+
// spelling being taken is small. It is not zero, and a saved document hardcodes
|
|
5
|
+
// the attribute and can never be reached to migrate it.
|
|
6
|
+
//
|
|
7
|
+
// These clay- spellings are read everywhere the bare name is read, and are left
|
|
8
|
+
// out of the documentation on purpose. Their only job is to already exist in every
|
|
9
|
+
// 1.x build: if a bare name ever stops being ours, a file written today can be
|
|
10
|
+
// repaired by adding one attribute instead of needing a migration that cannot
|
|
11
|
+
// reach it. An escape hatch added after the collision would be worthless.
|
|
12
|
+
export const PERSIST = ":is([persist], [clay-persist])";
|
|
13
|
+
export const AUTOSAVE = ":is([autosave], [clay-autosave])";
|
package/src/lib/dirty-gate.js
CHANGED
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
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
|
+
import { PERSIST } from './attr-aliases.js';
|
|
31
32
|
|
|
32
33
|
let changes = 0;
|
|
33
34
|
let clearedAt = 0;
|
|
34
35
|
let paused = false;
|
|
35
36
|
let started = false;
|
|
36
37
|
|
|
37
|
-
const PERSIST_CONTROLS =
|
|
38
|
+
const PERSIST_CONTROLS = `input${PERSIST}, textarea${PERSIST}, select${PERSIST}`;
|
|
38
39
|
|
|
39
40
|
// Regions the loss oracle never sees. The hub feed already skips them, through
|
|
40
41
|
// `require: 'dirty'`, and the input feed has to skip them for the same reason:
|
package/src/plugins/demo.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// <html autosave demo-key="my-page-v1"> <!-- both attributes optional -->
|
|
5
5
|
// <script>window.clayEditMode = true;</script> <!-- before the clay.js tag -->
|
|
6
|
-
// <script src="https://clayjs.com/clay.js?plugins=demo"></script>
|
|
6
|
+
// <script src="https://clayjs.com/v1/clay.js?plugins=demo"></script>
|
|
7
7
|
//
|
|
8
8
|
// The fetch shim answers POST /_/save the way a clayjs server would, so the
|
|
9
9
|
// whole real pipeline (savestatus, events, autosave, ⌘S) runs unchanged. The
|
|
@@ -112,7 +112,7 @@ async function restore() {
|
|
|
112
112
|
// attribute, never the element: [contenteditable] cannot join CHROME_SELECTOR,
|
|
113
113
|
// whose matches are removed outright, and doing that deletes real content.
|
|
114
114
|
if (!isEditMode) {
|
|
115
|
-
for (const el of doc.querySelectorAll("[editable][contenteditable]")) {
|
|
115
|
+
for (const el of doc.querySelectorAll(":is([editable], [clay-editable])[contenteditable]")) {
|
|
116
116
|
el.removeAttribute("contenteditable");
|
|
117
117
|
}
|
|
118
118
|
}
|