@panphora/clayjs 0.1.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 +21 -0
- package/README.md +57 -0
- package/clay.js +21 -0
- package/package.json +27 -0
- package/src/attrs/onaftersave.js +38 -0
- package/src/attrs/refetch-on-save.js +36 -0
- package/src/attrs/save-freeze.js +102 -0
- package/src/core/admin-attrs.js +22 -0
- package/src/core/admin-contenteditable.js +47 -0
- package/src/core/admin-inputs.js +48 -0
- package/src/core/admin-onclick.js +50 -0
- package/src/core/admin-resources.js +49 -0
- package/src/core/autosave.js +61 -0
- package/src/core/edit-mode.js +38 -0
- package/src/core/is-edit-mode.js +30 -0
- package/src/core/persist.js +103 -0
- package/src/core/save-core.js +385 -0
- package/src/core/save.js +475 -0
- package/src/core/snapshot.js +282 -0
- package/src/core/unsaved-warning.js +38 -0
- package/src/lib/autosave-debug.js +223 -0
- package/src/lib/cache-bust.js +12 -0
- package/src/lib/cookie.js +37 -0
- package/src/lib/dom-ready.js +9 -0
- package/src/lib/extension-noise.js +63 -0
- package/src/lib/load-vendor-script.js +57 -0
- package/src/lib/mutation.js +719 -0
- package/src/lib/query.js +3 -0
- package/src/lib/region-policy.js +220 -0
- package/src/lib/throttle.js +41 -0
- package/src/lib/user-gesture.js +126 -0
- package/src/loader-logic.js +62 -0
- package/src/loader.js +123 -0
- package/src/plugins/indicator.js +51 -0
- package/src/plugins/sortable.js +119 -0
- package/src/plugins/undo.js +23 -0
- package/src/sync/live-sync.js +752 -0
- package/src/vendor/Sortable.vendor.js +2 -0
- package/src/vendor/control-serialize.vendor.js +88 -0
- package/src/vendor/hyper-morph.vendor.js +22 -0
- package/src/vendor/hyper-undo.vendor.js +11 -0
- package/src/vendor/hypercms.vendor.js +1751 -0
- package/src/vendor/richclay.vendor.js +456 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Miranda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# clayjs
|
|
2
|
+
|
|
3
|
+
Self-saving, malleable HTML in one classic `<script>`. Load `clay.js` on a page and it
|
|
4
|
+
becomes editable in place, snapshots its own DOM, and saves that DOM back to the file it
|
|
5
|
+
came from. No build step, no framework.
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script src="https://clayjs.com/clay.js"></script>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The loader detects edit vs view mode and pulls only the modules it needs. Tune it with
|
|
14
|
+
query params on the script URL:
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<script src="/clay.js?plugins=sync,cms&exclude=indicator"></script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `?plugins=` — add optional plugins: `sync`, `cms` (richclay, indicator, sortable, undo load by default in edit mode).
|
|
21
|
+
- `?exclude=` — drop a default plugin.
|
|
22
|
+
- `?editmode=false` — force view mode (URL param wins over cookies/globals).
|
|
23
|
+
|
|
24
|
+
## Readiness
|
|
25
|
+
|
|
26
|
+
The bootstrap creates `window.clay` synchronously with just a `ready` promise, then augments
|
|
27
|
+
that same object once boot finishes. Wait for it before using any other member:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
await clay.ready; // or: document.addEventListener("clay:ready", ...)
|
|
31
|
+
clay.save();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Edit mode exposes `clay.save()` (+ `clay.save.force()`), `clay.getHTML()`, `clay.beforeSave(fn)`,
|
|
35
|
+
`clay.onSnapshot(fn)`, `clay.toggleEditMode()`, `clay.isEditMode`, `clay.isOwner`, `clay.Mutation`,
|
|
36
|
+
`clay.cacheBust(el)`, plus `clay.undo` / `clay.cms` / `clay.morph` when those plugins load. View
|
|
37
|
+
mode keeps only the always-available members (`toggleEditMode`, `isEditMode`, `isOwner`,
|
|
38
|
+
`Mutation`, `ready`); edit-only members are simply absent.
|
|
39
|
+
|
|
40
|
+
## Regions
|
|
41
|
+
|
|
42
|
+
Mark parts of the DOM the save/sync engine should treat specially with one space-separated
|
|
43
|
+
attribute:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<div clay="no-save no-snapshot freeze">…</div>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Tokens: `no-save`, `no-snapshot`, `no-trigger-autosave`, `no-watch`, `no-undo`, `freeze`.
|
|
50
|
+
Add `autosave` to `<html>` to save automatically on change.
|
|
51
|
+
|
|
52
|
+
## Develop
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm test # jest unit suite
|
|
56
|
+
npm run dev # stub save server on :4601 for the tests/fixtures pages
|
|
57
|
+
```
|
package/clay.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* clayjs bootstrap. https://clayjs.com */
|
|
2
|
+
(function () {
|
|
3
|
+
// Suppress every vendored bundle's window auto-export; the loader assembles
|
|
4
|
+
// window.clay (and a scoped compat shim) explicitly.
|
|
5
|
+
window.__hyperclayNoAutoExport = true;
|
|
6
|
+
|
|
7
|
+
var readyResolve;
|
|
8
|
+
var clay = { ready: new Promise(function (r) { readyResolve = r; }) };
|
|
9
|
+
window.clay = clay;
|
|
10
|
+
|
|
11
|
+
var script = document.currentScript;
|
|
12
|
+
if (!script || !script.src) {
|
|
13
|
+
console.error("clayjs: could not determine my own URL (load me with a classic <script src=...>, not type=\"module\")");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
var url = new URL(script.src, location.href);
|
|
17
|
+
var base = url.href.slice(0, url.href.lastIndexOf("/"));
|
|
18
|
+
import(base + "/src/loader.js")
|
|
19
|
+
.then(function (m) { return m.boot(base, url.searchParams, readyResolve); })
|
|
20
|
+
.catch(function (err) { console.error("clayjs failed to load:", err); });
|
|
21
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@panphora/clayjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://clayjs.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/panphora/clayjs.git"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"clay.js",
|
|
14
|
+
"src/"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
21
|
+
"dev": "node tests/stub-server.mjs"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"jest": "^29.7.0",
|
|
25
|
+
"jest-environment-jsdom": "^29.7.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [onaftersave] Custom Attribute
|
|
3
|
+
*
|
|
4
|
+
* Runs inline JavaScript after a successful save.
|
|
5
|
+
* Only fires on 'clay:save-saved' events (not on error/offline).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <span onaftersave="this.innerText = event.detail.msg"></span>
|
|
9
|
+
* <link href="styles.css" onaftersave="cacheBust(this)">
|
|
10
|
+
*
|
|
11
|
+
* The event.detail object contains:
|
|
12
|
+
* - status: 'saved'
|
|
13
|
+
* - msg: string (e.g., 'Saved')
|
|
14
|
+
* - timestamp: number (Date.now())
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function broadcast(e) {
|
|
18
|
+
const status = e.type.replace('clay:save-', '');
|
|
19
|
+
const detail = { ...e.detail, status };
|
|
20
|
+
|
|
21
|
+
document.querySelectorAll('[onaftersave]').forEach(el => {
|
|
22
|
+
try {
|
|
23
|
+
const event = new CustomEvent('aftersave', { detail });
|
|
24
|
+
const handler = new Function('event', el.getAttribute('onaftersave'));
|
|
25
|
+
handler.call(el, event);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('[onaftersave] Error in handler:', err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function init() {
|
|
33
|
+
document.addEventListener('clay:save-saved', broadcast);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
init();
|
|
37
|
+
|
|
38
|
+
export default init;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function swapElement(el) {
|
|
2
|
+
const attr = el.hasAttribute('href') ? 'href' : el.hasAttribute('src') ? 'src' : null;
|
|
3
|
+
if (!attr) return;
|
|
4
|
+
|
|
5
|
+
const oldValue = el.getAttribute(attr);
|
|
6
|
+
const url = new URL(oldValue, location.href);
|
|
7
|
+
url.searchParams.set('v', Date.now());
|
|
8
|
+
const isSameOrigin = url.origin === location.origin;
|
|
9
|
+
|
|
10
|
+
const newEl = document.createElement(el.tagName);
|
|
11
|
+
for (const { name, value } of el.attributes) {
|
|
12
|
+
newEl.setAttribute(name, value);
|
|
13
|
+
}
|
|
14
|
+
newEl.setAttribute(attr, isSameOrigin ? url.pathname + url.search : url.href);
|
|
15
|
+
const tokens = new Set((newEl.getAttribute("clay") || "").trim().split(/\s+/).filter(Boolean));
|
|
16
|
+
tokens.add("no-trigger-autosave");
|
|
17
|
+
tokens.add("no-undo");
|
|
18
|
+
newEl.setAttribute("clay", Array.from(tokens).join(" "));
|
|
19
|
+
|
|
20
|
+
el.insertAdjacentElement('afterend', newEl);
|
|
21
|
+
|
|
22
|
+
newEl.onload = () => el.remove();
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
if (el.parentNode) el.remove();
|
|
25
|
+
}, 2000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function init() {
|
|
29
|
+
document.addEventListener('clay:save-saved', () => {
|
|
30
|
+
document.querySelectorAll('[refetch-on-save]').forEach(swapElement);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
init();
|
|
35
|
+
|
|
36
|
+
export default init;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [freeze] / [save-freeze] Custom Attribute
|
|
3
|
+
*
|
|
4
|
+
* Freezes an element's innerHTML for save purposes.
|
|
5
|
+
* The live DOM can change freely, but the saved HTML always
|
|
6
|
+
* contains the original content captured when the element first appeared.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <div clay="freeze">Content that JS will modify at runtime</div>
|
|
10
|
+
*
|
|
11
|
+
* The original innerHTML is captured:
|
|
12
|
+
* - On page load, for all existing freeze elements
|
|
13
|
+
* - On DOM insertion, for dynamically added freeze elements
|
|
14
|
+
*
|
|
15
|
+
* At save time, the clone's innerHTML is replaced with the stored original.
|
|
16
|
+
* Changes inside freeze elements do not trigger autosave dirty checks.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { onSnapshot, onPrepareForSave } from "../core/snapshot.js";
|
|
20
|
+
import { isEditMode } from "../core/is-edit-mode.js";
|
|
21
|
+
import { FREEZE_SELECTOR } from "../lib/region-policy.js";
|
|
22
|
+
import Mutation from "../lib/mutation.js";
|
|
23
|
+
|
|
24
|
+
const originals = new WeakMap();
|
|
25
|
+
|
|
26
|
+
const saveFreeze = { debug: false };
|
|
27
|
+
|
|
28
|
+
function log(...args) {
|
|
29
|
+
if (saveFreeze.debug) console.log('[save-freeze]', ...args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function capture(el) {
|
|
33
|
+
if (!originals.has(el)) {
|
|
34
|
+
originals.set(el, el.innerHTML);
|
|
35
|
+
log('captured original', el.innerHTML.substring(0, 80) + '...');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function captureAll() {
|
|
40
|
+
const els = document.querySelectorAll(FREEZE_SELECTOR);
|
|
41
|
+
log('captureAll found', els.length, 'elements');
|
|
42
|
+
for (const el of els) {
|
|
43
|
+
capture(el);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function freezeClone(clone) {
|
|
48
|
+
const liveElements = document.querySelectorAll(FREEZE_SELECTOR);
|
|
49
|
+
const cloneElements = clone.querySelectorAll(FREEZE_SELECTOR);
|
|
50
|
+
|
|
51
|
+
log('freezing clone — live:', liveElements.length, 'clone:', cloneElements.length);
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < cloneElements.length; i++) {
|
|
54
|
+
const liveEl = liveElements[i];
|
|
55
|
+
const hasOriginal = liveEl && originals.has(liveEl);
|
|
56
|
+
if (hasOriginal) {
|
|
57
|
+
const original = originals.get(liveEl);
|
|
58
|
+
const current = cloneElements[i].innerHTML;
|
|
59
|
+
if (original !== current) {
|
|
60
|
+
log('element', i, '— restoring original');
|
|
61
|
+
cloneElements[i].innerHTML = original;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function init() {
|
|
68
|
+
log('init, isEditMode:', isEditMode);
|
|
69
|
+
if (!isEditMode) return;
|
|
70
|
+
|
|
71
|
+
captureAll();
|
|
72
|
+
|
|
73
|
+
// Capture the original innerHTML of dynamically added [freeze]/[save-freeze]
|
|
74
|
+
// regions. Folded onto the single Mutation observer (Phase 2 — was its own).
|
|
75
|
+
// debounce:0 so we snapshot the authored content before any runtime edit
|
|
76
|
+
// mutates it; the callback is a pure read into a WeakMap (no DOM writes).
|
|
77
|
+
// require:'observed' (skip only no-watch); pausable:false so a morphed-in
|
|
78
|
+
// freeze region is still captured. NOTE: the old observer watched
|
|
79
|
+
// document.documentElement; Mutation watches document.body, so a [freeze] added
|
|
80
|
+
// directly to <head> at runtime is no longer captured (authored <head> freeze
|
|
81
|
+
// regions are still handled by captureAll() on load) — an accepted edge.
|
|
82
|
+
Mutation.onAddElement({
|
|
83
|
+
selectorFilter: FREEZE_SELECTOR,
|
|
84
|
+
require: 'observed',
|
|
85
|
+
pausable: false,
|
|
86
|
+
debounce: 0
|
|
87
|
+
}, (changes) => {
|
|
88
|
+
changes.forEach(({ element }) => capture(element));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Phase 2: Freeze in snapshot — before snapshot-ready fires.
|
|
92
|
+
// This prevents live-sync from writing unfrozen content to disk.
|
|
93
|
+
onSnapshot(freezeClone);
|
|
94
|
+
|
|
95
|
+
// Phase 3a: Freeze again in prepare — belt-and-suspenders.
|
|
96
|
+
// Catches any modifications made between phase 2 and 3a (e.g., onbeforesave handlers).
|
|
97
|
+
onPrepareForSave(freezeClone);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
init();
|
|
101
|
+
|
|
102
|
+
export default saveFreeze;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Admin system - combines all admin features (edit-only).
|
|
2
|
+
import { init as initContenteditable } from './admin-contenteditable.js';
|
|
3
|
+
import { init as initInputs } from './admin-inputs.js';
|
|
4
|
+
import { init as initOnClick } from './admin-onclick.js';
|
|
5
|
+
import { init as initResources } from './admin-resources.js';
|
|
6
|
+
|
|
7
|
+
let initialized = false;
|
|
8
|
+
|
|
9
|
+
function init() {
|
|
10
|
+
if (initialized) return;
|
|
11
|
+
initialized = true;
|
|
12
|
+
initContenteditable();
|
|
13
|
+
initInputs();
|
|
14
|
+
initOnClick();
|
|
15
|
+
initResources();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Auto-init when module is imported
|
|
19
|
+
init();
|
|
20
|
+
|
|
21
|
+
export { init };
|
|
22
|
+
export default init;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isEditMode, isOwner } from "./is-edit-mode.js";
|
|
2
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
3
|
+
import { beforeSave } from "./snapshot.js";
|
|
4
|
+
|
|
5
|
+
export const SELECTOR = '[editmode\\:contenteditable]';
|
|
6
|
+
|
|
7
|
+
export function disableContentEditableBeforeSave () {
|
|
8
|
+
beforeSave(docElem => {
|
|
9
|
+
docElem.querySelectorAll(SELECTOR).forEach(resource => {
|
|
10
|
+
const originalValue = resource.getAttribute("contenteditable");
|
|
11
|
+
resource.setAttribute("inert-contenteditable", originalValue);
|
|
12
|
+
resource.removeAttribute("contenteditable");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function enableContentEditableForAdminOnPageLoad () {
|
|
18
|
+
if (!isEditMode) return;
|
|
19
|
+
|
|
20
|
+
onDomReady(() => {
|
|
21
|
+
enableContentEditable();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runtime toggle functions
|
|
26
|
+
export function enableContentEditable() {
|
|
27
|
+
document.querySelectorAll(SELECTOR).forEach(el => {
|
|
28
|
+
let val = el.getAttribute("inert-contenteditable");
|
|
29
|
+
if (!["false", "plaintext-only"].includes(val)) val = "true";
|
|
30
|
+
el.setAttribute("contenteditable", val);
|
|
31
|
+
el.removeAttribute("inert-contenteditable");
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function disableContentEditable() {
|
|
36
|
+
document.querySelectorAll(SELECTOR).forEach(el => {
|
|
37
|
+
const val = el.getAttribute("contenteditable") || "true";
|
|
38
|
+
el.setAttribute("inert-contenteditable", val);
|
|
39
|
+
el.removeAttribute("contenteditable");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Auto-initialize
|
|
44
|
+
export function init() {
|
|
45
|
+
disableContentEditableBeforeSave();
|
|
46
|
+
enableContentEditableForAdminOnPageLoad();
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isEditMode, isOwner } from "./is-edit-mode.js";
|
|
2
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
3
|
+
import { beforeSave } from "./snapshot.js";
|
|
4
|
+
|
|
5
|
+
export const SELECTOR_DISABLED = '[viewmode\\:disabled]';
|
|
6
|
+
export const SELECTOR_READONLY = '[viewmode\\:readonly]';
|
|
7
|
+
|
|
8
|
+
export function disableAdminInputsBeforeSave() {
|
|
9
|
+
beforeSave(docElem => {
|
|
10
|
+
docElem.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
|
|
11
|
+
input.setAttribute('disabled', '');
|
|
12
|
+
});
|
|
13
|
+
docElem.querySelectorAll(SELECTOR_READONLY).forEach(input => {
|
|
14
|
+
input.setAttribute('readonly', '');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function enableAdminInputsOnPageLoad() {
|
|
20
|
+
if (!isEditMode) return;
|
|
21
|
+
|
|
22
|
+
onDomReady(() => {
|
|
23
|
+
enableAdminInputs();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function enableAdminInputs() {
|
|
28
|
+
document.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
|
|
29
|
+
input.removeAttribute('disabled');
|
|
30
|
+
});
|
|
31
|
+
document.querySelectorAll(SELECTOR_READONLY).forEach(input => {
|
|
32
|
+
input.removeAttribute('readonly');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function disableAdminInputs() {
|
|
37
|
+
document.querySelectorAll(SELECTOR_DISABLED).forEach(input => {
|
|
38
|
+
input.setAttribute('disabled', '');
|
|
39
|
+
});
|
|
40
|
+
document.querySelectorAll(SELECTOR_READONLY).forEach(input => {
|
|
41
|
+
input.setAttribute('readonly', '');
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function init() {
|
|
46
|
+
disableAdminInputsBeforeSave();
|
|
47
|
+
enableAdminInputsOnPageLoad();
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isEditMode, isOwner } from "./is-edit-mode.js";
|
|
2
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
3
|
+
import { beforeSave } from "./snapshot.js";
|
|
4
|
+
|
|
5
|
+
export const SELECTOR = '[editmode\\:onclick]';
|
|
6
|
+
|
|
7
|
+
export function disableOnClickBeforeSave () {
|
|
8
|
+
beforeSave(docElem => {
|
|
9
|
+
docElem.querySelectorAll(SELECTOR).forEach(resource => {
|
|
10
|
+
const originalValue = resource.getAttribute("onclick");
|
|
11
|
+
resource.setAttribute("inert-onclick", originalValue);
|
|
12
|
+
resource.removeAttribute("onclick");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function enableOnClickForAdminOnPageLoad () {
|
|
18
|
+
if (!isEditMode) return;
|
|
19
|
+
|
|
20
|
+
onDomReady(() => {
|
|
21
|
+
enableOnClick();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runtime toggle functions
|
|
26
|
+
export function enableOnClick() {
|
|
27
|
+
document.querySelectorAll(SELECTOR).forEach(el => {
|
|
28
|
+
const val = el.getAttribute("inert-onclick");
|
|
29
|
+
if (val) {
|
|
30
|
+
el.setAttribute("onclick", val);
|
|
31
|
+
el.removeAttribute("inert-onclick");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function disableOnClick() {
|
|
37
|
+
document.querySelectorAll(SELECTOR).forEach(el => {
|
|
38
|
+
const val = el.getAttribute("onclick");
|
|
39
|
+
if (val) {
|
|
40
|
+
el.setAttribute("inert-onclick", val);
|
|
41
|
+
el.removeAttribute("onclick");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Auto-initialize
|
|
47
|
+
export function init() {
|
|
48
|
+
disableOnClickBeforeSave();
|
|
49
|
+
enableOnClickForAdminOnPageLoad();
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isEditMode, isOwner } from "./is-edit-mode.js";
|
|
2
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
3
|
+
import { beforeSave } from "./snapshot.js";
|
|
4
|
+
|
|
5
|
+
export const SELECTOR = '[editmode\\:resource]:is(style, link, script)';
|
|
6
|
+
export const SELECTOR_INERT = '[editmode\\:resource]:is(style, link, script)[type^="inert/"]';
|
|
7
|
+
|
|
8
|
+
export function disableAdminResourcesBeforeSave () {
|
|
9
|
+
beforeSave(docElem => {
|
|
10
|
+
docElem.querySelectorAll(SELECTOR).forEach(resource => {
|
|
11
|
+
const currentType = resource.getAttribute('type') || 'text/javascript';
|
|
12
|
+
if (!currentType.startsWith('inert/')) {
|
|
13
|
+
resource.setAttribute('type', `inert/${currentType}`);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function enableAdminResourcesOnPageLoad () {
|
|
20
|
+
if (!isEditMode) return;
|
|
21
|
+
|
|
22
|
+
onDomReady(() => {
|
|
23
|
+
enableAdminResources();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Runtime toggle functions
|
|
28
|
+
export function enableAdminResources() {
|
|
29
|
+
document.querySelectorAll(SELECTOR_INERT).forEach(resource => {
|
|
30
|
+
resource.type = resource.type.replace(/inert\//g, '');
|
|
31
|
+
resource.replaceWith(resource.cloneNode(true));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function disableAdminResources() {
|
|
36
|
+
document.querySelectorAll(SELECTOR).forEach(resource => {
|
|
37
|
+
const currentType = resource.getAttribute('type') || 'text/javascript';
|
|
38
|
+
if (!currentType.startsWith('inert/')) {
|
|
39
|
+
resource.setAttribute('type', `inert/${currentType}`);
|
|
40
|
+
resource.replaceWith(resource.cloneNode(true));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Auto-initialize
|
|
46
|
+
export function init() {
|
|
47
|
+
disableAdminResourcesBeforeSave();
|
|
48
|
+
enableAdminResourcesOnPageLoad();
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-save system for clayjs
|
|
3
|
+
*
|
|
4
|
+
* Automatically saves page on DOM changes with throttling.
|
|
5
|
+
* Gated by the <html autosave> attribute.
|
|
6
|
+
*
|
|
7
|
+
* Requires the 'save-system' module to be loaded first.
|
|
8
|
+
*
|
|
9
|
+
* Recommended companion modules:
|
|
10
|
+
* - 'unsaved-warning' - Warn before leaving with unsaved changes (required for beforeunload)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import Mutation from "../lib/mutation.js";
|
|
14
|
+
import { isEditMode } from "./is-edit-mode.js";
|
|
15
|
+
import { savePageThrottled } from "./save.js";
|
|
16
|
+
import { initUserGesture, markUserDriven } from "../lib/user-gesture.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize auto-save on DOM changes
|
|
20
|
+
* Uses debounced mutation observer
|
|
21
|
+
*/
|
|
22
|
+
function initSavePageOnChange() {
|
|
23
|
+
Mutation.onAnyChange({
|
|
24
|
+
debounce: 1500,
|
|
25
|
+
omitChangeDetails: true,
|
|
26
|
+
require: 'autosave'
|
|
27
|
+
}, () => {
|
|
28
|
+
savePageThrottled();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize auto-save on input events for [persist] elements
|
|
34
|
+
* Form input values don't trigger DOM mutations, so we listen for input events
|
|
35
|
+
*/
|
|
36
|
+
let inputSaveTimer = null;
|
|
37
|
+
function initSaveOnPersistInput() {
|
|
38
|
+
document.addEventListener('input', (e) => {
|
|
39
|
+
if (!e.target.closest('[persist]')) return;
|
|
40
|
+
// A trusted input on a [persist] field is itself a user-driven change the
|
|
41
|
+
// Mutation hub can't see (form values aren't DOM mutations). Attribute it.
|
|
42
|
+
if (e.isTrusted) markUserDriven();
|
|
43
|
+
clearTimeout(inputSaveTimer);
|
|
44
|
+
inputSaveTimer = setTimeout(savePageThrottled, 1500);
|
|
45
|
+
}, true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function init() {
|
|
49
|
+
if (!document.documentElement.hasAttribute("autosave")) return;
|
|
50
|
+
if (!isEditMode) return;
|
|
51
|
+
initUserGesture();
|
|
52
|
+
initSavePageOnChange();
|
|
53
|
+
initSaveOnPersistInput();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// No window exports - savePageThrottled is exported from save-system
|
|
57
|
+
|
|
58
|
+
// Auto-init when module is imported
|
|
59
|
+
init();
|
|
60
|
+
|
|
61
|
+
export default init;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Edit mode system - combines the editmode toggle with page type setting.
|
|
2
|
+
// Loads in BOTH view and edit mode: toggleEditMode must exist in view mode
|
|
3
|
+
// (it's the way IN), and setPageTypeOnPageLoad marks <html> in either mode.
|
|
4
|
+
import { isEditMode, isOwner } from "./is-edit-mode.js";
|
|
5
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
6
|
+
import { beforeSave } from "./snapshot.js";
|
|
7
|
+
|
|
8
|
+
export function toggleEditMode() {
|
|
9
|
+
const url = new URL(window.location.href);
|
|
10
|
+
const newMode = isEditMode ? "false" : "true";
|
|
11
|
+
url.searchParams.set('editmode', newMode);
|
|
12
|
+
window.location.href = url.toString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setViewerPageTypeBeforeSave () {
|
|
16
|
+
beforeSave(docElem => {
|
|
17
|
+
docElem.setAttribute("editmode", "false");
|
|
18
|
+
docElem.setAttribute("pageowner", "false");
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setPageTypeOnPageLoad () {
|
|
23
|
+
onDomReady(() => {
|
|
24
|
+
document.documentElement.setAttribute("editmode", isEditMode ? "true" : "false");
|
|
25
|
+
document.documentElement.setAttribute("pageowner", isOwner ? "true" : "false");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function init() {
|
|
30
|
+
setViewerPageTypeBeforeSave();
|
|
31
|
+
setPageTypeOnPageLoad();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Auto-init when module is imported
|
|
35
|
+
init();
|
|
36
|
+
|
|
37
|
+
export { init, isEditMode, isOwner };
|
|
38
|
+
export default { init, toggleEditMode, isEditMode, isOwner };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import cookie from "../lib/cookie.js";
|
|
2
|
+
import query from "../lib/query.js";
|
|
3
|
+
|
|
4
|
+
// Edit-mode precedence: an explicit ?editmode=true|false URL param wins, then an
|
|
5
|
+
// opt-in window.clayEditMode global (with the legacy window.__hyperclayEditMode
|
|
6
|
+
// still honored as a fallback — htmlclay injects it today), then the platform's
|
|
7
|
+
// isAdminOfCurrentResource cookie. The global is for standalone uses (demos,
|
|
8
|
+
// htmlclay, any self-saving file) that are always editable and have no owner
|
|
9
|
+
// cookie; setting it before clayjs loads turns on the edit-only modules.
|
|
10
|
+
let forcedEditMode = null;
|
|
11
|
+
if (typeof window !== "undefined") {
|
|
12
|
+
if (window.clayEditMode != null) {
|
|
13
|
+
forcedEditMode = Boolean(window.clayEditMode);
|
|
14
|
+
} else if (window.__hyperclayEditMode != null) {
|
|
15
|
+
forcedEditMode = Boolean(window.__hyperclayEditMode);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isEditMode = query.editmode
|
|
20
|
+
? query.editmode === "true" // takes precedence over the global and cookie
|
|
21
|
+
: forcedEditMode != null
|
|
22
|
+
? forcedEditMode
|
|
23
|
+
: Boolean(cookie.get("isAdminOfCurrentResource"));
|
|
24
|
+
|
|
25
|
+
const isOwner = Boolean(cookie.get("isAdminOfCurrentResource"));
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
isEditMode,
|
|
29
|
+
isOwner
|
|
30
|
+
}
|