@panphora/clayjs 1.2.0 → 1.4.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 +7 -3
- package/THIRD-PARTY-NOTICES.md +10 -0
- package/dist/clay.standalone.js +22539 -14260
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +1 -1
- package/package.json +7 -2
- package/packed-contract.json +17 -0
- package/src/attrs/save-freeze.js +10 -18
- package/src/core/admin-contenteditable.js +8 -6
- package/src/core/admin-inputs.js +23 -13
- package/src/core/admin-onclick.js +5 -0
- package/src/core/is-edit-mode.js +7 -2
- package/src/core/persist.js +5 -10
- package/src/core/save-core.js +35 -0
- package/src/core/save.js +13 -1
- package/src/core/snapshot.js +186 -14
- package/src/core/source-map.js +1025 -0
- package/src/core/unsaved-warning.js +3 -0
- package/src/dom/dom-helpers.js +5 -1
- package/src/lib/content-dom.js +108 -0
- package/src/lib/mutation.js +26 -3
- package/src/lib/region-capabilities.js +69 -0
- package/src/lib/region-policy.js +18 -13
- package/src/loader-logic.js +27 -5
- package/src/loader.js +6 -0
- package/src/plugins/ai-edit.js +625 -0
- package/src/plugins/demo.js +3 -0
- package/src/plugins/sortable.js +6 -1
- package/src/plugins/source.js +410 -0
- package/src/plugins/wire.js +248 -47
- package/src/sync/live-sync.js +106 -42
- package/src/sync/presence.js +303 -0
- package/src/sync/section-notice.js +230 -0
- package/src/sync/splice-merge.js +7 -10
- package/src/sync/stream.js +190 -0
- package/src/vendor/control-serialize.vendor.js +10 -6
- package/src/vendor/hyper-morph.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +438 -45
- package/src/vendor/parse5.vendor.js +3 -0
- package/src/vendor/quickcrop.vendor.js +1 -1
- package/src/vendor/richclay.vendor.js +22 -15
|
@@ -31,6 +31,9 @@ preloadIfEnabled();
|
|
|
31
31
|
// the person editing it deserves the warning.
|
|
32
32
|
window.addEventListener('beforeunload', (event) => {
|
|
33
33
|
if (!isEditMode) return;
|
|
34
|
+
// The demo plugin saves into this browser's own storage, so leaving the page
|
|
35
|
+
// loses nothing a prompt could protect.
|
|
36
|
+
if (window.clay?.demo) return;
|
|
34
37
|
|
|
35
38
|
// The DIRTY domain, not the autosave domain. An edit inside a
|
|
36
39
|
// no-trigger-autosave region never starts a save by itself, which is exactly
|
package/src/dom/dom-helpers.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import nearest from "./nearest.js";
|
|
2
2
|
import pipe from "./pipe.js";
|
|
3
|
+
import { createContentView, cleanContentClone } from '../lib/content-dom.js';
|
|
3
4
|
|
|
4
5
|
function init () {
|
|
5
6
|
|
|
7
|
+
window.clay = window.clay || {};
|
|
8
|
+
window.clay.dom = Object.assign(window.clay.dom || {}, { createContentView, cleanContentClone });
|
|
9
|
+
|
|
6
10
|
// Bail if already initialized
|
|
7
11
|
if (HTMLElement.prototype.hasOwnProperty('nearest')) {
|
|
8
12
|
return;
|
|
@@ -214,5 +218,5 @@ function init () {
|
|
|
214
218
|
// Auto-init when module is imported
|
|
215
219
|
init();
|
|
216
220
|
|
|
217
|
-
export { init };
|
|
221
|
+
export { init, createContentView, cleanContentClone };
|
|
218
222
|
export default init;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { capabilitySelector, hasCapability } from './region-capabilities.js'
|
|
2
|
+
|
|
3
|
+
const UNSUPPORTED_SELECTOR = /:(?:focus(?:-within|-visible)?|hover|active|visited|defined)\b/i
|
|
4
|
+
|
|
5
|
+
function copyControlState(source, copy) {
|
|
6
|
+
if (!/^(INPUT|TEXTAREA|SELECT|OPTION)$/.test(source.tagName || '')) return
|
|
7
|
+
const type = (source.getAttribute?.('type') || '').toLowerCase()
|
|
8
|
+
if ('value' in source && 'value' in copy && source.tagName !== 'OPTION' && type !== 'checkbox' && type !== 'radio') copy.value = source.value
|
|
9
|
+
if ('checked' in source && 'checked' in copy) copy.checked = source.checked
|
|
10
|
+
if ('selected' in source && 'selected' in copy) copy.selected = source.selected
|
|
11
|
+
if (source.tagName === 'SELECT') {
|
|
12
|
+
for (let i = 0; i < source.options.length; i++) copy.options[i].selected = source.options[i].selected
|
|
13
|
+
}
|
|
14
|
+
if ('indeterminate' in source && 'indeterminate' in copy) copy.indeterminate = source.indeterminate
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function matchesWithin(node, selector, boundary, inherit) {
|
|
18
|
+
if (inherit) return !!node.closest?.(selector)
|
|
19
|
+
let current = node
|
|
20
|
+
while (current?.nodeType === 1) {
|
|
21
|
+
if (current.matches(selector)) return true
|
|
22
|
+
if (current === boundary) break
|
|
23
|
+
current = current.parentElement
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function importTree(source, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps) {
|
|
29
|
+
if (source.nodeType === 1 && (
|
|
30
|
+
(inherit ? hasCapability(source, capability) : matchesWithin(source, capabilityMatch, boundary, false)) ||
|
|
31
|
+
(exclude && matchesWithin(source, exclude, boundary, inherit))
|
|
32
|
+
)) return null
|
|
33
|
+
const copy = targetDocument.importNode(source, false)
|
|
34
|
+
maps.cloneToLive.set(copy, source)
|
|
35
|
+
maps.liveToClone.set(source, copy)
|
|
36
|
+
const sourceChildren = source.nodeType === 1 && source.tagName === 'TEMPLATE' ? source.content : source
|
|
37
|
+
const copyChildren = copy.nodeType === 1 && copy.tagName === 'TEMPLATE' ? copy.content : copy
|
|
38
|
+
if (sourceChildren !== source) {
|
|
39
|
+
maps.cloneToLive.set(copyChildren, sourceChildren)
|
|
40
|
+
maps.liveToClone.set(sourceChildren, copyChildren)
|
|
41
|
+
}
|
|
42
|
+
for (const child of sourceChildren.childNodes || []) {
|
|
43
|
+
const childCopy = importTree(child, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps)
|
|
44
|
+
if (childCopy) {
|
|
45
|
+
copyChildren.appendChild(childCopy)
|
|
46
|
+
if (child.nodeType === 1) copyControlState(child, childCopy)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (source.nodeType === 1) copyControlState(source, copy)
|
|
50
|
+
return copy
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createContentView(context, { capability = 'data', exclude = null, inherit = true } = {}) {
|
|
54
|
+
if (!context) throw new TypeError('createContentView requires a DOM context')
|
|
55
|
+
const sourceDocument = context.nodeType === 9 ? context : context.ownerDocument
|
|
56
|
+
if (!sourceDocument?.implementation?.createHTMLDocument) {
|
|
57
|
+
throw new TypeError('createContentView requires an HTML DOM implementation')
|
|
58
|
+
}
|
|
59
|
+
const inertDocument = sourceDocument.implementation.createHTMLDocument('')
|
|
60
|
+
const cloneToLive = new WeakMap()
|
|
61
|
+
const liveToClone = new WeakMap()
|
|
62
|
+
const sourceRoot = context.nodeType === 9 ? context.documentElement : context
|
|
63
|
+
if (exclude) inertDocument.documentElement.matches(exclude)
|
|
64
|
+
const capabilityMatch = capabilitySelector(capability)
|
|
65
|
+
const root = importTree(sourceRoot, inertDocument, capability, capabilityMatch, exclude, sourceRoot, inherit, { cloneToLive, liveToClone })
|
|
66
|
+
if (context.nodeType === 9 && root) {
|
|
67
|
+
inertDocument.replaceChild(root, inertDocument.documentElement)
|
|
68
|
+
liveToClone.set(context, inertDocument)
|
|
69
|
+
cloneToLive.set(inertDocument, context)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const assertSelector = selector => {
|
|
73
|
+
if (UNSUPPORTED_SELECTOR.test(selector)) {
|
|
74
|
+
throw new Error(`Filtered content queries do not support stateful selector: ${selector}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
root,
|
|
80
|
+
document: inertDocument,
|
|
81
|
+
capability,
|
|
82
|
+
selector: capabilitySelector(capability),
|
|
83
|
+
cloneToLive,
|
|
84
|
+
liveToClone,
|
|
85
|
+
original(node) { return cloneToLive.get(node) || null },
|
|
86
|
+
cloneOf(node) { return liveToClone.get(node) || null },
|
|
87
|
+
query(selector, queryRoot = root) {
|
|
88
|
+
assertSelector(selector)
|
|
89
|
+
return Array.from(queryRoot.querySelectorAll(selector), node => cloneToLive.get(node) || node)
|
|
90
|
+
},
|
|
91
|
+
text(node = root) {
|
|
92
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
93
|
+
return projected?.textContent || ''
|
|
94
|
+
},
|
|
95
|
+
html(node = root) {
|
|
96
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
97
|
+
return projected?.innerHTML ?? ''
|
|
98
|
+
},
|
|
99
|
+
clone(node = root) {
|
|
100
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
101
|
+
return projected ? inertDocument.importNode(projected, true) : null
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function cleanContentClone(node, options) {
|
|
107
|
+
return createContentView(node, options).root
|
|
108
|
+
}
|
package/src/lib/mutation.js
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
import { EXTENSION_ATTR_PATTERN } from './extension-noise.js';
|
|
70
70
|
import { resolveRegionPolicy, isInert, strictestPolicy, skipForPolicy } from './region-policy.js';
|
|
71
|
+
import { isPolicyAttribute } from './region-capabilities.js';
|
|
71
72
|
import { ROOT_LIBRARY_ATTRS } from './root-attrs.js';
|
|
72
73
|
|
|
73
74
|
import { isUserDrivenNow, markUserDriven } from './user-gesture.js';
|
|
@@ -186,7 +187,12 @@ const localMutation = {
|
|
|
186
187
|
// doesn't participate in (no-save / no-trigger-autosave / no-undo / freeze
|
|
187
188
|
// and their legacy equivalents), resolved against the callback's `require`.
|
|
188
189
|
let filteredChanges = changes.filter(
|
|
189
|
-
change =>
|
|
190
|
+
change => {
|
|
191
|
+
const currentSkipped = skipForPolicy(this._policyForChange(change), require, skip);
|
|
192
|
+
if (!change.policyTransition) return !currentSkipped;
|
|
193
|
+
const previousSkipped = skipForPolicy(change.previousPolicy, require, skip);
|
|
194
|
+
return !(currentSkipped && previousSkipped);
|
|
195
|
+
}
|
|
190
196
|
);
|
|
191
197
|
if (!filteredChanges.length) {
|
|
192
198
|
this._log('No changes passed the region policy, skipping callback');
|
|
@@ -478,7 +484,8 @@ const localMutation = {
|
|
|
478
484
|
// Intake drop: a no-watch / mutations-ignore subtree (or extension noise)
|
|
479
485
|
// is invisible to every consumer, so skip it without walking. All other
|
|
480
486
|
// region attributes are resolved per-consumer in _notify.
|
|
481
|
-
|
|
487
|
+
const policyTransition = mutation.type === 'attributes' && isPolicyAttribute(mutation.attributeName);
|
|
488
|
+
if (!policyTransition && isInert(mutation.target)) {
|
|
482
489
|
continue;
|
|
483
490
|
}
|
|
484
491
|
|
|
@@ -586,7 +593,9 @@ const localMutation = {
|
|
|
586
593
|
element: mutation.target,
|
|
587
594
|
attribute: mutation.attributeName,
|
|
588
595
|
oldValue: mutation.oldValue,
|
|
589
|
-
newValue: mutation.target.getAttribute(mutation.attributeName)
|
|
596
|
+
newValue: mutation.target.getAttribute(mutation.attributeName),
|
|
597
|
+
policyTransition,
|
|
598
|
+
previousPolicy: policyTransition ? policyBeforeMutation(mutation) : null,
|
|
590
599
|
};
|
|
591
600
|
changes.push(change);
|
|
592
601
|
changesByType.attribute.push(change);
|
|
@@ -783,3 +792,17 @@ if (typeof window !== 'undefined' && !existingHub) {
|
|
|
783
792
|
}
|
|
784
793
|
|
|
785
794
|
export default Mutation;
|
|
795
|
+
function policyBeforeMutation(record) {
|
|
796
|
+
let source = record.target;
|
|
797
|
+
let clone = source.cloneNode(false);
|
|
798
|
+
const leaf = clone;
|
|
799
|
+
while (source.parentElement) {
|
|
800
|
+
source = source.parentElement;
|
|
801
|
+
const parent = source.cloneNode(false);
|
|
802
|
+
parent.appendChild(clone);
|
|
803
|
+
clone = parent;
|
|
804
|
+
}
|
|
805
|
+
if (record.oldValue == null) leaf.removeAttribute(record.attributeName);
|
|
806
|
+
else leaf.setAttribute(record.attributeName, record.oldValue);
|
|
807
|
+
return resolveRegionPolicy(leaf);
|
|
808
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const CAPABILITIES = Object.freeze({
|
|
2
|
+
data: Object.freeze({ tokens: ['no-data'], bundles: ['editor-ui'] }),
|
|
3
|
+
save: Object.freeze({ tokens: ['no-save'], bundles: ['editor-ui'] }),
|
|
4
|
+
snapshot: Object.freeze({ tokens: ['no-snapshot'], bundles: ['editor-ui'] }),
|
|
5
|
+
watch: Object.freeze({ tokens: ['no-watch'], bundles: ['editor-ui'] }),
|
|
6
|
+
undo: Object.freeze({ tokens: ['no-undo'], bundles: ['editor-ui'] }),
|
|
7
|
+
history: Object.freeze({ tokens: [], bundles: ['editor-ui'] }),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const BUNDLES = Object.freeze({
|
|
11
|
+
'editor-ui': Object.freeze(['no-data', 'no-save', 'no-snapshot', 'no-watch', 'no-undo']),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const POLICY_TOKENS = Object.freeze([
|
|
15
|
+
'no-save',
|
|
16
|
+
'no-snapshot',
|
|
17
|
+
'no-trigger-autosave',
|
|
18
|
+
'no-dirty',
|
|
19
|
+
'no-watch',
|
|
20
|
+
'no-undo',
|
|
21
|
+
'no-data',
|
|
22
|
+
'freeze',
|
|
23
|
+
'editor-ui',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
export function hasPolicyToken(element, token) {
|
|
27
|
+
if (!element || element.nodeType !== 1) return false
|
|
28
|
+
const clay = element.getAttribute?.('clay')
|
|
29
|
+
return !!(
|
|
30
|
+
(clay && clay.split(/\s+/).includes(token)) ||
|
|
31
|
+
element.hasAttribute?.(token)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function expandsTo(element, token) {
|
|
36
|
+
if (hasPolicyToken(element, token)) return true
|
|
37
|
+
for (const [bundle, members] of Object.entries(BUNDLES)) {
|
|
38
|
+
if (members.includes(token) && hasPolicyToken(element, bundle)) return true
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function capabilitySelector(capability) {
|
|
44
|
+
const definition = CAPABILITIES[capability]
|
|
45
|
+
if (!definition) throw new Error(`Unknown region capability: ${capability}`)
|
|
46
|
+
return [...definition.tokens, ...definition.bundles]
|
|
47
|
+
.flatMap(token => [`[clay~="${token}"]`, `[${token}]`])
|
|
48
|
+
.join(', ')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function closestWithCapability(node, capability) {
|
|
52
|
+
let element = node && node.nodeType === 1 ? node : node?.parentElement
|
|
53
|
+
while (element && element.nodeType === 1) {
|
|
54
|
+
const definition = CAPABILITIES[capability]
|
|
55
|
+
if (!definition) throw new Error(`Unknown region capability: ${capability}`)
|
|
56
|
+
if (definition.tokens.some(token => expandsTo(element, token)) ||
|
|
57
|
+
definition.bundles.some(bundle => hasPolicyToken(element, bundle))) return element
|
|
58
|
+
element = element.parentElement
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function hasCapability(node, capability) {
|
|
64
|
+
return !!closestWithCapability(node, capability)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isPolicyAttribute(name) {
|
|
68
|
+
return name === 'clay' || POLICY_TOKENS.includes(name)
|
|
69
|
+
}
|
package/src/lib/region-policy.js
CHANGED
|
@@ -44,24 +44,21 @@
|
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
import { EXTENSION_NODE_SELECTOR } from './extension-noise.js';
|
|
47
|
+
import { capabilitySelector, expandsTo, hasCapability, hasPolicyToken, POLICY_TOKENS } from './region-capabilities.js';
|
|
47
48
|
|
|
48
49
|
export const PERSIST = { FULL: 'full', FROZEN: 'frozen', NONE: 'none' };
|
|
49
50
|
|
|
50
51
|
// The canonical region tokens (spelled in the `clay` attribute or as bare attrs).
|
|
51
|
-
export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-undo', 'no-watch', 'freeze'];
|
|
52
|
+
export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-undo', 'no-watch', 'no-data', 'freeze', 'editor-ui'];
|
|
52
53
|
|
|
53
54
|
// Every canonical token spellable inside the space-separated `clay` attribute.
|
|
54
55
|
// Exported: this list used to exist as a private CLAY_TOKENS that nothing read,
|
|
55
56
|
// while callers hardcoded their own copies.
|
|
56
|
-
export const TOKENS = [
|
|
57
|
+
export const TOKENS = [...POLICY_TOKENS];
|
|
57
58
|
|
|
58
59
|
// True when a region marker is present, whether spelled as a `clay` token
|
|
59
60
|
// (whitespace-token semantics, matching [clay~=token]) or a legacy bare attribute.
|
|
60
|
-
|
|
61
|
-
const clay = el.getAttribute?.("clay");
|
|
62
|
-
if (clay && clay.split(/\s+/).includes(token)) return true; // whitespace-token semantics, matches [clay~=…]
|
|
63
|
-
return !!el.hasAttribute?.(token); // legacy bare attribute
|
|
64
|
-
}
|
|
61
|
+
const hasRegionToken = hasPolicyToken;
|
|
65
62
|
|
|
66
63
|
/**
|
|
67
64
|
* Add a canonical region token to an element's `clay` attribute, preserving any
|
|
@@ -79,7 +76,7 @@ export function addRegionToken(el, token) {
|
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
// Serializer selectors (recognize the clay-token spelling FIRST, then new + legacy bare).
|
|
82
|
-
export const STRIP_FROM_SAVE = '
|
|
79
|
+
export const STRIP_FROM_SAVE = `${capabilitySelector('save')}, [save-remove]`;
|
|
83
80
|
export const FREEZE_SELECTOR = '[clay~="freeze"], [freeze], [save-freeze]';
|
|
84
81
|
// forComparison additionally strips every region whose autosave-trigger is off,
|
|
85
82
|
// so their churn never marks the page dirty — including the no-watch /
|
|
@@ -117,7 +114,8 @@ export const STRIP_FROM_DIRTY_CHECK =
|
|
|
117
114
|
// dirty-comparison) in snapshot.js. `no-snapshot` is the consistent alias for the
|
|
118
115
|
// original `snapshot-remove`; hyper-morph treats both as sync-ignored so a
|
|
119
116
|
// live-sync receiver keeps its own local copy instead of deleting it.
|
|
120
|
-
export const SNAPSHOT_REMOVE_SELECTOR = '
|
|
117
|
+
export const SNAPSHOT_REMOVE_SELECTOR = `${capabilitySelector('snapshot')}, [snapshot-remove]`;
|
|
118
|
+
export const NO_DATA_SELECTOR = capabilitySelector('data');
|
|
121
119
|
|
|
122
120
|
// Ancestor-aware, because the strip removes a marked element together with its
|
|
123
121
|
// whole subtree: a child of a no-snapshot region is just as absent from every
|
|
@@ -128,6 +126,10 @@ export function isSnapshotRemoved(node) {
|
|
|
128
126
|
return !!(element && element.closest && element.closest(SNAPSHOT_REMOVE_SELECTOR));
|
|
129
127
|
}
|
|
130
128
|
|
|
129
|
+
export function isDataExcluded(node) {
|
|
130
|
+
return hasCapability(node, 'data');
|
|
131
|
+
}
|
|
132
|
+
|
|
131
133
|
const PERSIST_RANK = { full: 0, frozen: 1, none: 2 };
|
|
132
134
|
const RANK_PERSIST = ['full', 'frozen', 'none'];
|
|
133
135
|
|
|
@@ -158,11 +160,11 @@ export function resolveRegionPolicy(node) {
|
|
|
158
160
|
while (element && element.nodeType === 1) {
|
|
159
161
|
if (element.hasAttribute) {
|
|
160
162
|
// new naked attributes
|
|
161
|
-
if (
|
|
163
|
+
if (expandsTo(element, 'no-watch')) watched = false;
|
|
162
164
|
if (hasRegionToken(element, 'no-trigger-autosave')) autosaveOff = true;
|
|
163
165
|
if (hasRegionToken(element, 'no-dirty')) { autosaveOff = true; dirtyOff = true; }
|
|
164
|
-
if (
|
|
165
|
-
if (
|
|
166
|
+
if (expandsTo(element, 'no-undo')) undoable = false;
|
|
167
|
+
if (expandsTo(element, 'no-save')) persistRank = Math.max(persistRank, PERSIST_RANK.none);
|
|
166
168
|
if (hasRegionToken(element, 'freeze')) persistRank = Math.max(persistRank, PERSIST_RANK.frozen);
|
|
167
169
|
// legacy markers -> bundles
|
|
168
170
|
if (hasRegionToken(element, 'mutations-ignore')) watched = false;
|
|
@@ -209,7 +211,7 @@ export function isInert(node) {
|
|
|
209
211
|
if (element && element.closest && element.closest(EXTENSION_NODE_SELECTOR)) return true;
|
|
210
212
|
while (element && element.nodeType === 1) {
|
|
211
213
|
if (element.hasAttribute &&
|
|
212
|
-
(
|
|
214
|
+
(expandsTo(element, 'no-watch') || hasRegionToken(element, 'mutations-ignore'))) {
|
|
213
215
|
return true;
|
|
214
216
|
}
|
|
215
217
|
element = element.parentElement;
|
|
@@ -283,6 +285,7 @@ export const regionShape = {
|
|
|
283
285
|
resolveRegionPolicy,
|
|
284
286
|
isInert,
|
|
285
287
|
isSnapshotRemoved,
|
|
288
|
+
isDataExcluded,
|
|
286
289
|
skipForPolicy,
|
|
287
290
|
strictestPolicy,
|
|
288
291
|
addRegionToken,
|
|
@@ -297,6 +300,7 @@ export const regionShape = {
|
|
|
297
300
|
NO_TRIGGER_AUTOSAVE_SELECTOR,
|
|
298
301
|
NO_DIRTY_SELECTOR,
|
|
299
302
|
SNAPSHOT_REMOVE_SELECTOR,
|
|
303
|
+
NO_DATA_SELECTOR,
|
|
300
304
|
FREEZE_SELECTOR,
|
|
301
305
|
|
|
302
306
|
// Nested camelCase: what clay.internals.region has always published.
|
|
@@ -307,6 +311,7 @@ export const regionShape = {
|
|
|
307
311
|
noTriggerAutosave: NO_TRIGGER_AUTOSAVE_SELECTOR,
|
|
308
312
|
noDirty: NO_DIRTY_SELECTOR,
|
|
309
313
|
snapshotRemove: SNAPSHOT_REMOVE_SELECTOR,
|
|
314
|
+
noData: NO_DATA_SELECTOR,
|
|
310
315
|
freeze: FREEZE_SELECTOR,
|
|
311
316
|
},
|
|
312
317
|
};
|
package/src/loader-logic.js
CHANGED
|
@@ -10,10 +10,14 @@ export const CORE_WAVES = {
|
|
|
10
10
|
"core/stale-host-notice.js",
|
|
11
11
|
],
|
|
12
12
|
editOnly: [
|
|
13
|
-
"
|
|
13
|
+
"vendor/hyper-morph.vendor.js",
|
|
14
|
+
"core/snapshot.js",
|
|
15
|
+
// Before save.js: it takes the boot baseline as it evaluates, and a snapshot
|
|
16
|
+
// transform registered after that made an untouched page read as unsaved.
|
|
17
|
+
"core/persist.js", "core/admin-attrs.js",
|
|
18
|
+
"core/save-core.js", "core/save.js",
|
|
14
19
|
"core/save-conflict-notice.js",
|
|
15
|
-
"core/unsaved-warning.js", "core/
|
|
16
|
-
"core/admin-attrs.js", "core/autosave.js",
|
|
20
|
+
"core/unsaved-warning.js", "core/autosave.js",
|
|
17
21
|
"attrs/save-freeze.js", "attrs/onaftersave.js", "attrs/refetch-on-save.js",
|
|
18
22
|
"lib/cache-bust.js",
|
|
19
23
|
],
|
|
@@ -32,7 +36,18 @@ export const PLUGIN_PATHS = {
|
|
|
32
36
|
// superset of the cms's, so the plugin is present exactly when it can be used.
|
|
33
37
|
upload: { path: "plugins/upload.js", editOnly: true, default: false },
|
|
34
38
|
wire: { path: "plugins/wire.js", editOnly: false, default: false },
|
|
39
|
+
// Edit mode only: the AI comment box is an editing gesture, and the plugin is
|
|
40
|
+
// dormant anyway on any host that does not list a ready `ai-edit` helper.
|
|
41
|
+
"ai-edit": { path: "plugins/ai-edit.js", editOnly: true, default: false },
|
|
35
42
|
demo: { path: "plugins/demo.js", editOnly: false, default: false },
|
|
43
|
+
// Saves the file's own bytes back instead of a fresh serialization of the DOM.
|
|
44
|
+
// editOnly because a page that cannot save has nothing to preserve. On by default
|
|
45
|
+
// because the fallback rate it is judged on can only be collected from real pages,
|
|
46
|
+
// and a plugin nobody enables produces no number to judge. It costs a 48 KB parser
|
|
47
|
+
// and one extra request at boot, both in edit mode only, and every save it cannot
|
|
48
|
+
// verify is sent as the ordinary full serialization, so the floor is the behaviour
|
|
49
|
+
// it replaces. `exclude=source` turns it off.
|
|
50
|
+
source: { path: "plugins/source.js", editOnly: true, default: true },
|
|
36
51
|
};
|
|
37
52
|
|
|
38
53
|
// One literal import per module the loader can ask for, so a bundler can see the
|
|
@@ -62,6 +77,7 @@ export const MODULES = {
|
|
|
62
77
|
"attrs/onaftersave.js": () => import("./attrs/onaftersave.js"),
|
|
63
78
|
"attrs/refetch-on-save.js": () => import("./attrs/refetch-on-save.js"),
|
|
64
79
|
"lib/cache-bust.js": () => import("./lib/cache-bust.js"),
|
|
80
|
+
"vendor/hyper-morph.vendor.js": () => import("./vendor/hyper-morph.vendor.js"),
|
|
65
81
|
"vendor/richclay.vendor.js": () => import("./vendor/richclay.vendor.js"),
|
|
66
82
|
"plugins/indicator.js": () => import("./plugins/indicator.js"),
|
|
67
83
|
"sync/live-sync.js": () => import("./sync/live-sync.js"),
|
|
@@ -71,10 +87,14 @@ export const MODULES = {
|
|
|
71
87
|
"vendor/quickcrop.vendor.js": () => import("./vendor/quickcrop.vendor.js"),
|
|
72
88
|
"plugins/upload.js": () => import("./plugins/upload.js"),
|
|
73
89
|
"plugins/wire.js": () => import("./plugins/wire.js"),
|
|
90
|
+
"plugins/ai-edit.js": () => import("./plugins/ai-edit.js"),
|
|
74
91
|
"plugins/demo.js": () => import("./plugins/demo.js"),
|
|
92
|
+
"plugins/source.js": () => import("./plugins/source.js"),
|
|
75
93
|
};
|
|
76
94
|
|
|
77
|
-
|
|
95
|
+
// `source` is last on purpose: its install captures a save clone, so it wants every
|
|
96
|
+
// plugin that registers a document transform to have registered it first.
|
|
97
|
+
const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "quickcrop", "upload", "cms", "sync", "wire", "ai-edit", "demo", "source"];
|
|
78
98
|
|
|
79
99
|
// A plugin that cannot do its whole job alone. hypercms reads the cropper through
|
|
80
100
|
// a capability lookup (`clay.quickcrop`) and silently uploads the raw file when it
|
|
@@ -92,7 +112,9 @@ const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "quickcrop",
|
|
|
92
112
|
// This is the only line in the capability that changes how an already-published
|
|
93
113
|
// page behaves, which is why it shipped alone, one release after the plugin it
|
|
94
114
|
// enables. Reverting it is reverting this line.
|
|
95
|
-
|
|
115
|
+
// ai-edit reads `clay.wire.helpers()` and sends through `clay.wire.send`, so a page
|
|
116
|
+
// that asks for it has to have the wire. The wire loads first in the order above.
|
|
117
|
+
const IMPLIES = { cms: ["quickcrop", "upload"], "ai-edit": ["wire"] };
|
|
96
118
|
|
|
97
119
|
function parseCsv(params, key, enabled, apply) {
|
|
98
120
|
const raw = params.get(key);
|
package/src/loader.js
CHANGED
|
@@ -82,6 +82,7 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
|
82
82
|
const snapshot = loaded["core/snapshot.js"];
|
|
83
83
|
const save = loaded["core/save.js"];
|
|
84
84
|
const cacheBustMod = loaded["lib/cache-bust.js"];
|
|
85
|
+
const hyperMorph = loaded["vendor/hyper-morph.vendor.js"];
|
|
85
86
|
|
|
86
87
|
if (save) {
|
|
87
88
|
const saveFn = save.savePage || save.default;
|
|
@@ -97,6 +98,7 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
|
97
98
|
if (cacheBustMod) {
|
|
98
99
|
clay.cacheBust = cacheBustMod.default;
|
|
99
100
|
}
|
|
101
|
+
if (hyperMorph) clay.morph = hyperMorph.morph;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
function attachPluginMember(path, mod) {
|
|
@@ -112,12 +114,16 @@ function attachPluginMember(path, mod) {
|
|
|
112
114
|
clay.upload = mod.upload || mod.default;
|
|
113
115
|
} else if (path === "plugins/wire.js") {
|
|
114
116
|
clay.wire = mod.wire || mod.default;
|
|
117
|
+
} else if (path === "plugins/ai-edit.js") {
|
|
118
|
+
clay.aiEdit = mod.aiEdit || mod.default;
|
|
115
119
|
} else if (path === "plugins/demo.js") {
|
|
116
120
|
clay.demo = mod.demo;
|
|
117
121
|
} else if (path === "vendor/richclay.vendor.js") {
|
|
118
122
|
clay.RichClay = mod.RichClay || mod.default;
|
|
119
123
|
} else if (path === "vendor/quickcrop.vendor.js") {
|
|
120
124
|
clay.quickcrop = mod.quickcrop || mod.default;
|
|
125
|
+
} else if (path === "plugins/source.js") {
|
|
126
|
+
clay.source = mod.source || mod.default;
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
|