@panphora/clayjs 0.6.1 → 0.7.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/LICENSE +12 -17
- package/README.md +21 -11
- package/THIRD-PARTY-NOTICES.md +40 -0
- package/package.json +3 -3
- package/src/attrs/onaftersave.js +23 -9
- package/src/attrs/refetch-on-save.js +38 -18
- package/src/core/autosave.js +11 -4
- package/src/core/host-attrs.js +1 -16
- package/src/core/save-core.js +26 -27
- package/src/core/save.js +105 -26
- package/src/core/snapshot.js +121 -26
- package/src/core/unsaved-warning.js +11 -7
- package/src/internals/index.js +3 -26
- package/src/lib/authored-url.js +96 -0
- package/src/lib/cache-bust.js +16 -12
- package/src/lib/dirty-gate.js +28 -10
- package/src/lib/mutation.js +5 -3
- package/src/lib/region-policy.js +98 -18
- package/src/lib/root-attrs.js +0 -5
- package/src/lib/user-gesture.js +33 -1
- package/src/loader-logic.js +11 -4
- package/src/plugins/demo.js +14 -8
- package/src/sync/live-sync.js +157 -12
- package/src/sync/splice-merge.js +21 -7
- package/src/vendor/quickcrop.vendor.js +1 -1
- package/NOTICE +0 -19
package/LICENSE
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
MIT
|
|
1
|
+
MIT No Attribution
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 David Miranda
|
|
4
4
|
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify,
|
|
8
|
+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
permit persons to whom the Software is furnished to do so.
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
12
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
13
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
14
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
15
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
16
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# clayjs
|
|
1
|
+
# clayjs™
|
|
2
2
|
|
|
3
3
|
**Site, tutorial, and docs: [clayjs.com](https://clayjs.com)** · hosted platform: [hyperclay.com](https://hyperclay.com)
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ query params on the script URL:
|
|
|
19
19
|
<script src="/clay.js?plugins=sync,cms&exclude=indicator"></script>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
- `?plugins=` — add optional plugins: `sync`, `cms`, `undo`, `sortable`, `indicator`, `quickcrop`, `wire`.
|
|
22
|
+
- `?plugins=` — add optional plugins: `sync`, `cms`, `undo`, `sortable`, `indicator`, `quickcrop`, `upload`, `wire`, `demo`.
|
|
23
23
|
Only `richclay` loads by default, and only in edit mode. `cms` brings `quickcrop` with it, because
|
|
24
24
|
the CMS uses it for `data-hcms-crop` image fields.
|
|
25
25
|
- `?exclude=` — drop a plugin that would otherwise load (a default, or one another plugin pulled in).
|
|
@@ -35,10 +35,6 @@ never reach the file on disk.
|
|
|
35
35
|
`/_/save/{token}` and sends no cookies, because the token is the credential.
|
|
36
36
|
A token also implies edit mode, which is the only such signal a sandboxed
|
|
37
37
|
document can see. `htmlclaytoken` is the older spelling of the same thing.
|
|
38
|
-
- `clay-save-transport="desktop-json-v1"` — send the save as
|
|
39
|
-
`{content, snapshotHtml, userDriven}` JSON instead of plain text. Only declare
|
|
40
|
-
it on a host whose save lane reads that envelope.
|
|
41
|
-
|
|
42
38
|
Edit mode is decided in this order: the `?editmode` param, then
|
|
43
39
|
`window.clayEditMode`, then a save token, then the platform's owner cookie.
|
|
44
40
|
|
|
@@ -111,8 +107,9 @@ lifecycle.
|
|
|
111
107
|
satellite with no core loaded.
|
|
112
108
|
- `region.addRegionToken(el, token)`, `region.resolveRegionPolicy(node)`, `region.isInert(node)`,
|
|
113
109
|
`region.isSnapshotRemoved(el)`, `region.PERSIST`, `region.REGION_ATTRS`, and
|
|
114
|
-
`region.selectors.stripFromSave` / `.stripFromComparison` / `.
|
|
115
|
-
|
|
110
|
+
`region.selectors.stripFromSave` / `.stripFromComparison` / `.stripFromDirtyCheck` /
|
|
111
|
+
`.noTriggerAutosave` / `.noDirty` / `.snapshotRemove` / `.freeze` — write your own attribute
|
|
112
|
+
without hardcoding our selectors.
|
|
116
113
|
- `save.saveHtml(html, cb, opts)`, `save.replacePageWith(url, cb)`, `save.isSaveInProgress()` — the save
|
|
117
114
|
lane under `clay.save`. **`saveHtml` writes the bytes you hand it straight to the file**, bypassing the
|
|
118
115
|
snapshot pipeline entirely; check `isSaveInProgress()` first.
|
|
@@ -126,8 +123,15 @@ attribute:
|
|
|
126
123
|
<div clay="no-save no-snapshot freeze">…</div>
|
|
127
124
|
```
|
|
128
125
|
|
|
129
|
-
Tokens: `no-save`, `no-snapshot`, `no-trigger-autosave`, `no-watch`, `no-undo`,
|
|
130
|
-
Add `autosave` to `<html>` to save automatically on change.
|
|
126
|
+
Tokens: `no-save`, `no-snapshot`, `no-trigger-autosave`, `no-dirty`, `no-watch`, `no-undo`,
|
|
127
|
+
`freeze`. Add `autosave` to `<html>` to save automatically on change.
|
|
128
|
+
|
|
129
|
+
`no-trigger-autosave` and `no-dirty` are the pair worth getting right. Both are saved in full and
|
|
130
|
+
neither starts an autosave. The difference is whether their content is *work*: a
|
|
131
|
+
`no-trigger-autosave` region holds real edits waiting for a manual save, so it warns you on close
|
|
132
|
+
and live sync will not overwrite it, while a `no-dirty` region renders itself from something else,
|
|
133
|
+
so it never warns and an incoming sync frame may replace it. Use `no-dirty` for filter bars,
|
|
134
|
+
projections and drag previews; use `no-trigger-autosave` for a heavy editor you save by hand.
|
|
131
135
|
|
|
132
136
|
## Develop
|
|
133
137
|
|
|
@@ -151,7 +155,7 @@ Never edit `public/` by hand, and never copy a file into it. Its contents are
|
|
|
151
155
|
**derived**:
|
|
152
156
|
|
|
153
157
|
```
|
|
154
|
-
public/ = package.json "files" (minus
|
|
158
|
+
public/ = package.json "files" (minus THIRD-PARTY-NOTICES.md, which nothing requests)
|
|
155
159
|
+ website/* (flattened to the root, so /docs.html works)
|
|
156
160
|
```
|
|
157
161
|
|
|
@@ -161,3 +165,9 @@ reach npm and miss the site. Before this script existed the mirror was hand-copi
|
|
|
161
165
|
and it drifted exactly that way: `src/core/host-attrs.js` never made it across, and
|
|
162
166
|
since `loader.js` imports `is-edit-mode.js`, which imports it, the deployed
|
|
163
167
|
`clay.js` could not boot at all.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
Our code is MIT-0 (MIT No Attribution): use it, remix it, ship it, no attribution
|
|
172
|
+
needed. Vendored third-party files keep their original permissive licenses; see
|
|
173
|
+
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Our own code in this repository is licensed under MIT-0 (see LICENSE): use it,
|
|
4
|
+
remix it, ship it, no attribution needed. The components below are third-party,
|
|
5
|
+
or carry third-party code, and keep their original licenses.
|
|
6
|
+
|
|
7
|
+
## SortableJS 1.15.6 (MIT)
|
|
8
|
+
|
|
9
|
+
- File: src/vendor/Sortable.vendor.js
|
|
10
|
+
- Copyright (c) SortableJS contributors
|
|
11
|
+
- https://github.com/SortableJS/Sortable
|
|
12
|
+
|
|
13
|
+
## Squire 2.4.8 (MIT)
|
|
14
|
+
|
|
15
|
+
- Carried inside src/vendor/richclay.vendor.js and website/vendor/richclay.min.js
|
|
16
|
+
(richclay bundles Squire)
|
|
17
|
+
- Copyright (c) 2011-2023 by Neil Jenkins
|
|
18
|
+
- https://github.com/fastmail/Squire
|
|
19
|
+
|
|
20
|
+
## DOMPurify 3.4.11 (Apache-2.0 OR MPL-2.0)
|
|
21
|
+
|
|
22
|
+
- Carried inside src/vendor/richclay.vendor.js and website/vendor/richclay.min.js
|
|
23
|
+
(richclay bundles DOMPurify)
|
|
24
|
+
- (c) Cure53 and other contributors
|
|
25
|
+
- https://github.com/cure53/DOMPurify
|
|
26
|
+
|
|
27
|
+
## MicroModal (MIT)
|
|
28
|
+
|
|
29
|
+
- The modal component in src/ui/modal.js derives its structure and class naming
|
|
30
|
+
from MicroModal.
|
|
31
|
+
- Copyright (c) 2017 Indrashish Ghosh
|
|
32
|
+
- https://github.com/ghosh/Micromodal
|
|
33
|
+
|
|
34
|
+
## First-party vendored modules
|
|
35
|
+
|
|
36
|
+
The remaining files in src/vendor/ (hypercms, hyper-morph, hyper-undo, quickcrop,
|
|
37
|
+
control-serialize) and the generated sap.js and clay-data.js are our own sibling
|
|
38
|
+
libraries. clayjs also derives from our own hyperclayjs library (MIT); as the
|
|
39
|
+
copyright holder of those portions we license them here under MIT-0 with the rest
|
|
40
|
+
of our code.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
|
|
6
|
-
"license": "MIT",
|
|
6
|
+
"license": "MIT-0",
|
|
7
7
|
"homepage": "https://clayjs.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"sap.js",
|
|
22
22
|
"clay-data.js",
|
|
23
23
|
"_headers",
|
|
24
|
-
"
|
|
24
|
+
"THIRD-PARTY-NOTICES.md",
|
|
25
25
|
"src/"
|
|
26
26
|
],
|
|
27
27
|
"publishConfig": {
|
package/src/attrs/onaftersave.js
CHANGED
|
@@ -5,16 +5,30 @@
|
|
|
5
5
|
* Only fires on 'clay:save-saved' events (not on error/offline).
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* <span clay="no-
|
|
9
|
-
* <link href="styles.css" onaftersave="cacheBust(this)">
|
|
8
|
+
* <span clay="no-save" onaftersave="this.innerText = event.detail.msg"></span>
|
|
9
|
+
* <link href="styles.css" onaftersave="clay.cacheBust(this)">
|
|
10
10
|
*
|
|
11
|
-
* MARK WHAT YOU MUTATE `no-
|
|
12
|
-
* These handlers run after the save baseline has been taken, so anything they
|
|
13
|
-
* to the live DOM reads as a change the
|
|
14
|
-
* dirty:
|
|
15
|
-
* the element
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* MARK WHAT YOU MUTATE `no-save` (or `freeze`).
|
|
12
|
+
* These handlers run after the save baseline has been taken, so anything they
|
|
13
|
+
* write to the live DOM reads as a change the person made, and the page stays
|
|
14
|
+
* dirty: the close-tab warning never clears, and on an autosave page it loops.
|
|
15
|
+
* `no-save` keeps the element out of the file entirely, which is what you want
|
|
16
|
+
* for a status chip or any other runtime-only chrome. `freeze` is the choice
|
|
17
|
+
* when the element must exist in the saved file, but as authored rather than as
|
|
18
|
+
* the handler left it.
|
|
19
|
+
*
|
|
20
|
+
* CHANGED IN 0.7: `no-trigger-autosave` is no longer enough on its own.
|
|
21
|
+
* It stops the handler's write from STARTING a save, and it still does that. It
|
|
22
|
+
* no longer hides the write from an explicit save or from the close warning,
|
|
23
|
+
* because those two now deliberately see batching regions — an edit you make
|
|
24
|
+
* inside one is real work that nothing else is going to write. A handler whose
|
|
25
|
+
* output is marked only `no-trigger-autosave` will leave the page reporting
|
|
26
|
+
* unsaved changes after every save. Change those markers to `no-save`, or to
|
|
27
|
+
* `freeze` if the element belongs in the file.
|
|
28
|
+
*
|
|
29
|
+
* `clay.cacheBust()` and `[refetch-on-save]` need no marker at all: they
|
|
30
|
+
* remember the authored URL and restore it on every snapshot (authored-url.js).
|
|
31
|
+
* A handler that mutates some OTHER element has to mark that element itself.
|
|
18
32
|
*
|
|
19
33
|
* (The alternative — re-reading the whole live DOM after handlers run — is what
|
|
20
34
|
* clayjs used to do, and it silently discarded anything typed during a save.)
|
|
@@ -1,28 +1,48 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Mutation from '../lib/mutation.js';
|
|
2
|
+
import { urlAttrFor, writeRuntimeUrl, markSuperseded, isSuperseded } from '../lib/authored-url.js';
|
|
3
|
+
|
|
4
|
+
/** Remove an element without letting the removal read as a user edit. */
|
|
5
|
+
function removeQuietly(el) {
|
|
6
|
+
if (!el.parentNode) return;
|
|
7
|
+
Mutation.pause();
|
|
8
|
+
try {
|
|
9
|
+
el.remove();
|
|
10
|
+
} finally {
|
|
11
|
+
Mutation.resume();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
function swapElement(el) {
|
|
4
|
-
const attr =
|
|
16
|
+
const attr = urlAttrFor(el);
|
|
5
17
|
if (!attr) return;
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
19
|
+
// A second save landing inside the two-second overlap would otherwise swap the
|
|
20
|
+
// element that is already on its way out, and clone the clone.
|
|
21
|
+
if (isSuperseded(el)) return;
|
|
22
|
+
|
|
23
|
+
const url = new URL(el.getAttribute(attr), location.href);
|
|
9
24
|
url.searchParams.set('v', Date.now());
|
|
10
25
|
const isSameOrigin = url.origin === location.origin;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
const runtimeValue = isSameOrigin ? url.pathname + url.search : url.href;
|
|
27
|
+
|
|
28
|
+
Mutation.pause();
|
|
29
|
+
try {
|
|
30
|
+
const newEl = document.createElement(el.tagName);
|
|
31
|
+
for (const { name, value } of el.attributes) {
|
|
32
|
+
newEl.setAttribute(name, value);
|
|
33
|
+
}
|
|
34
|
+
// Record-if-absent carries over any authored URL the copied attributes
|
|
35
|
+
// already held, so cacheBust and refetch on one element keep the original.
|
|
36
|
+
writeRuntimeUrl(newEl, attr, runtimeValue);
|
|
37
|
+
|
|
38
|
+
markSuperseded(el);
|
|
39
|
+
el.insertAdjacentElement('afterend', newEl);
|
|
40
|
+
|
|
41
|
+
newEl.onload = () => removeQuietly(el);
|
|
42
|
+
setTimeout(() => removeQuietly(el), 2000);
|
|
43
|
+
} finally {
|
|
44
|
+
Mutation.resume();
|
|
15
45
|
}
|
|
16
|
-
newEl.setAttribute(attr, isSameOrigin ? url.pathname + url.search : url.href);
|
|
17
|
-
addRegionToken(newEl, "no-trigger-autosave");
|
|
18
|
-
addRegionToken(newEl, "no-undo");
|
|
19
|
-
|
|
20
|
-
el.insertAdjacentElement('afterend', newEl);
|
|
21
|
-
|
|
22
|
-
newEl.onload = () => el.remove();
|
|
23
|
-
setTimeout(() => {
|
|
24
|
-
if (el.parentNode) el.remove();
|
|
25
|
-
}, 2000);
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
function init() {
|
package/src/core/autosave.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import Mutation from "../lib/mutation.js";
|
|
14
14
|
import { isEditMode } from "./is-edit-mode.js";
|
|
15
15
|
import { savePageThrottled } from "./save.js";
|
|
16
|
-
import {
|
|
16
|
+
import { markUserDriven } from "../lib/user-gesture.js";
|
|
17
|
+
import { resolveRegionPolicy, skipForPolicy } from "../lib/region-policy.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Initialize auto-save on DOM changes
|
|
@@ -48,8 +49,13 @@ function initSaveOnPersistInput() {
|
|
|
48
49
|
document.addEventListener('input', (e) => {
|
|
49
50
|
if (!e.target.closest('[persist]')) return;
|
|
50
51
|
// A trusted input on a [persist] field is itself a user-driven change the
|
|
51
|
-
// Mutation hub can't see (form values aren't DOM mutations). Attribute it
|
|
52
|
-
|
|
52
|
+
// Mutation hub can't see (form values aren't DOM mutations). Attribute it,
|
|
53
|
+
// but only when the field is inside the dirty domain: a [persist] control in
|
|
54
|
+
// a no-save or freeze region contributes nothing to the saved bytes, so
|
|
55
|
+
// typing in one must not arm provenance for a later background write.
|
|
56
|
+
if (e.isTrusted && !skipForPolicy(resolveRegionPolicy(e.target), 'dirty')) {
|
|
57
|
+
markUserDriven();
|
|
58
|
+
}
|
|
53
59
|
clearTimeout(inputSaveTimer);
|
|
54
60
|
inputSaveTimer = setTimeout(savePageThrottled, 1500);
|
|
55
61
|
}, true);
|
|
@@ -58,7 +64,8 @@ function initSaveOnPersistInput() {
|
|
|
58
64
|
function init() {
|
|
59
65
|
if (!document.documentElement.hasAttribute("autosave")) return;
|
|
60
66
|
if (!isEditMode) return;
|
|
61
|
-
initUserGesture
|
|
67
|
+
// initUserGesture moved to save.js's init: gesture provenance belongs to every
|
|
68
|
+
// editable page, not only the ones with <html autosave>.
|
|
62
69
|
initSavePageOnChange();
|
|
63
70
|
initSaveOnPersistInput();
|
|
64
71
|
}
|
package/src/core/host-attrs.js
CHANGED
|
@@ -7,13 +7,7 @@
|
|
|
7
7
|
* cannot drift between the edit-mode ladder and the save lane.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { HOST_TOKEN_ATTRS
|
|
11
|
-
|
|
12
|
-
// A host that wants the desktop JSON envelope on its save lane declares it on
|
|
13
|
-
// the root. This replaced a `location.hostname === 'localhost'` sniff, which sent
|
|
14
|
-
// the envelope to every host that happened to be local, including ones whose
|
|
15
|
-
// save lane takes text and answers 415.
|
|
16
|
-
export const DESKTOP_JSON = "desktop-json-v1";
|
|
10
|
+
import { HOST_TOKEN_ATTRS } from "../lib/root-attrs.js";
|
|
17
11
|
|
|
18
12
|
/**
|
|
19
13
|
* The per-document save token this response carries, or null.
|
|
@@ -35,12 +29,3 @@ export function saveToken() {
|
|
|
35
29
|
export function hasSaveToken() {
|
|
36
30
|
return saveToken() !== null;
|
|
37
31
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The save transport the served document declares, or null.
|
|
41
|
-
* @returns {?string}
|
|
42
|
-
*/
|
|
43
|
-
export function saveTransport() {
|
|
44
|
-
if (typeof document === "undefined") return null;
|
|
45
|
-
return document.documentElement.getAttribute(SAVE_TRANSPORT_ATTR);
|
|
46
|
-
}
|
package/src/core/save-core.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isEditMode } from "./is-edit-mode.js";
|
|
11
|
-
import { consumeUserDriven, markUserDriven } from "../lib/user-gesture.js";
|
|
12
|
-
import { saveToken
|
|
11
|
+
import { consumeUserDriven, consumeExplicitSave, markUserDriven } from "../lib/user-gesture.js";
|
|
12
|
+
import { saveToken } from "./host-attrs.js";
|
|
13
13
|
import {
|
|
14
14
|
getPageContents,
|
|
15
15
|
onSnapshot,
|
|
@@ -103,13 +103,19 @@ function skippedResult(msg) {
|
|
|
103
103
|
* the save — and the per-document token in its path — to an origin the document
|
|
104
104
|
* itself chose.
|
|
105
105
|
*
|
|
106
|
+
* The body is the document, as text, always. Spec §3: this route has exactly one
|
|
107
|
+
* body shape. Everything else about the save travels in a header, which is how
|
|
108
|
+
* §6's `conditional` capability added `If-Match` without touching the body, and
|
|
109
|
+
* how anything later will be added too. There used to be a JSON envelope here
|
|
110
|
+
* carrying the document alongside an unstripped snapshot; the snapshot's home is
|
|
111
|
+
* the §10 relay, which the live-sync plugin already posts it to.
|
|
112
|
+
*
|
|
106
113
|
* @param {string} content - HTML to save
|
|
107
|
-
* @param {?string} snapshotHtml - unstripped snapshot, for the desktop envelope
|
|
108
114
|
* @param {boolean} userDriven - Whether a human gesture is behind this save
|
|
109
115
|
* @param {AbortSignal} signal
|
|
110
116
|
* @returns {{url: string, options: Object}}
|
|
111
117
|
*/
|
|
112
|
-
function buildSaveRequest(content,
|
|
118
|
+
function buildSaveRequest(content, userDriven, signal) {
|
|
113
119
|
const token = saveToken();
|
|
114
120
|
const path = token ? `${SAVE_PATH}/${token}` : SAVE_PATH;
|
|
115
121
|
const options = {
|
|
@@ -120,23 +126,14 @@ function buildSaveRequest(content, snapshotHtml, userDriven, signal) {
|
|
|
120
126
|
'Document-URL': window.location.href,
|
|
121
127
|
// The older spelling of the same header, which htmlclay reads today.
|
|
122
128
|
'Page-URL': window.location.href,
|
|
123
|
-
'X-Hyperclay-User-Driven
|
|
124
|
-
|
|
129
|
+
// Spec §9's name for the provenance bit. `X-Hyperclay-User-Driven` was the
|
|
130
|
+
// pre-spec spelling; every host reads Save-Trigger first and falls back to
|
|
131
|
+
// it, so stored documents running an older client keep working.
|
|
132
|
+
'Save-Trigger': userDriven ? 'user' : 'auto'
|
|
133
|
+
},
|
|
134
|
+
body: content
|
|
125
135
|
};
|
|
126
136
|
|
|
127
|
-
// The desktop JSON envelope carries the unstripped snapshot alongside the
|
|
128
|
-
// document, so a sync engine can replay what the browser actually had. It goes
|
|
129
|
-
// out only when the served document DECLARES that transport, and it is passed in
|
|
130
|
-
// from the capture that produced `content` — it used to be left on a window
|
|
131
|
-
// global that was cleared only on success, so two captures without an intervening
|
|
132
|
-
// successful save would pair a stale snapshot with fresh content.
|
|
133
|
-
if (saveTransport() === DESKTOP_JSON && snapshotHtml) {
|
|
134
|
-
options.headers['Content-Type'] = 'application/json';
|
|
135
|
-
options.body = JSON.stringify({ content, snapshotHtml, userDriven });
|
|
136
|
-
} else {
|
|
137
|
-
options.body = content;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
137
|
return { url: new URL(path, window.location.origin).href, options };
|
|
141
138
|
}
|
|
142
139
|
|
|
@@ -153,18 +150,22 @@ function buildSaveRequest(content, snapshotHtml, userDriven, signal) {
|
|
|
153
150
|
* instead of the status.
|
|
154
151
|
*
|
|
155
152
|
* @param {string} content - HTML to save
|
|
156
|
-
* @param {?string} snapshotHtml
|
|
157
153
|
* @returns {Promise<Object>} The server's response body
|
|
158
154
|
*/
|
|
159
|
-
function sendSave(content
|
|
155
|
+
function sendSave(content) {
|
|
160
156
|
const controller = new AbortController();
|
|
161
157
|
const timeoutId = setTimeout(() => controller.abort(), SAVE_TIMEOUT_MS);
|
|
162
158
|
|
|
163
159
|
// Read-and-reset the data-guard provenance bit at the ACTUAL send (past the
|
|
164
160
|
// early returns in the callers), so it's never consumed on a save that never
|
|
165
161
|
// ships.
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
// Two independent ways a save is human: an edit made in a trusted turn, or a
|
|
163
|
+
// person pressing Save. Both are consumed here, at the one point past every
|
|
164
|
+
// early return, so neither can survive into an unrelated later save.
|
|
165
|
+
const gestureDriven = consumeUserDriven();
|
|
166
|
+
const explicitlyAsked = consumeExplicitSave();
|
|
167
|
+
const userDriven = gestureDriven || explicitlyAsked;
|
|
168
|
+
const { url, options } = buildSaveRequest(content, userDriven, controller.signal);
|
|
168
169
|
|
|
169
170
|
return fetch(url, options)
|
|
170
171
|
.then(res => res.text().then(text => {
|
|
@@ -208,15 +209,13 @@ function sendSave(content, snapshotHtml) {
|
|
|
208
209
|
*
|
|
209
210
|
* @param {string} html - HTML string to save
|
|
210
211
|
* @param {Function} callback - Called with the result on completion
|
|
211
|
-
* @param {Object} [options]
|
|
212
|
-
* @param {?string} [options.snapshotHtml] - unstripped snapshot from the same capture
|
|
213
212
|
* @returns {Promise<{msg: string, msgType: string, code: ?string, etag: ?string}>}
|
|
214
213
|
*
|
|
215
214
|
* @example
|
|
216
215
|
* const {msg, msgType} = await saveHtml(myHtml);
|
|
217
216
|
* if (msgType === 'error') console.error('Save failed:', msg);
|
|
218
217
|
*/
|
|
219
|
-
export function saveHtml(html, callback = () => {}
|
|
218
|
+
export function saveHtml(html, callback = () => {}) {
|
|
220
219
|
return new Promise((resolve) => {
|
|
221
220
|
const done = (result) => {
|
|
222
221
|
if (typeof callback === 'function') callback(result);
|
|
@@ -241,7 +240,7 @@ export function saveHtml(html, callback = () => {}, { snapshotHtml = null } = {}
|
|
|
241
240
|
return;
|
|
242
241
|
}
|
|
243
242
|
|
|
244
|
-
sendSave(html
|
|
243
|
+
sendSave(html)
|
|
245
244
|
.then(successResult, errorResult)
|
|
246
245
|
// Clear the flag BEFORE handing the result back, so a caller that queues a
|
|
247
246
|
// follow-up save can start it immediately rather than being told the lane
|