@panphora/clayjs 1.5.2 → 1.6.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 +5 -3
- package/THIRD-PARTY-NOTICES.md +6 -0
- package/dist/clay.standalone.js +14984 -10685
- package/package.json +9 -4
- package/src/core/conflict-download.js +91 -0
- package/src/core/conflict-notice.js +720 -0
- package/src/core/conflict-presentation.js +301 -0
- package/src/core/save-conflict-notice.js +2 -192
- package/src/core/save-feedback.js +6 -0
- package/src/core/save.js +60 -1
- package/src/core/snapshot.js +21 -0
- package/src/core/stale-host-notice.js +21 -24
- package/src/core/unsaved-warning.js +35 -10
- package/src/lib/content-dom.js +4 -2
- package/src/lib/dirty-gate.js +9 -0
- package/src/lib/unsaved-state.js +55 -0
- package/src/lib/user-gesture.js +10 -0
- package/src/loader-logic.js +26 -15
- package/src/loader.js +4 -3
- package/src/plugins/ai-edit.js +489 -179
- package/src/plugins/cms.js +12 -0
- package/src/plugins/indicator.js +35 -16
- package/src/plugins/quickcrop.js +103 -0
- package/src/plugins/richclay.js +21 -0
- package/src/plugins/source.js +13 -6
- package/src/plugins/wire.js +4 -0
- package/src/sync/conflict-revert.js +1138 -0
- package/src/sync/conflicts.js +159 -0
- package/src/sync/live-sync.js +188 -116
- package/src/sync/presence.js +42 -36
- package/src/sync/section-notice.js +53 -28
- package/src/ui/bevel-controls.js +277 -0
- package/src/ui/bevel-dialog.js +112 -0
- package/src/ui/bevel.js +247 -0
- package/src/ui/dialogs.js +63 -86
- package/src/ui/index.js +22 -11
- package/src/ui/modal.js +80 -387
- package/src/ui/skins/cms-toggle.js +15 -0
- package/src/ui/skins/cms.js +4 -0
- package/src/ui/skins/quickcrop.js +4 -0
- package/src/ui/skins/richclay.js +4 -0
- package/src/ui/toast.js +116 -401
- package/src/ui/vendor-skin.js +65 -0
- package/src/vendor/hyper-morph.vendor.js +4 -4
- package/src/vendor/quickcrop.vendor.js +14 -8
- package/src/vendor/richclay.vendor.js +22 -41
|
@@ -19,17 +19,16 @@ import { set, make } from "../lib/hostile-css.js";
|
|
|
19
19
|
// One line, no choice to make: nothing on this page can fix it, so offering a button
|
|
20
20
|
// would be a lie. Dismissable, because after you have read it, it is only in the way.
|
|
21
21
|
|
|
22
|
-
const BG = "var(--clay-notice-bg,#222)";
|
|
23
|
-
const INK = "var(--clay-notice-ink,#fff)";
|
|
24
|
-
const EDGE = "var(--clay-notice-edge,rgba(255,255,255,.28))";
|
|
25
|
-
const FONT = "14px/1.45 system-ui,-apple-system,'Segoe UI',sans-serif";
|
|
26
|
-
|
|
27
22
|
const MESSAGE =
|
|
28
23
|
"This page can't be edited: the app serving it is out of date. " +
|
|
29
24
|
"Update HTML Clay to 1.9.0 or newer.";
|
|
30
25
|
|
|
31
26
|
let root = null;
|
|
32
27
|
|
|
28
|
+
// Bevel is fetched only when the warning is actually shown. This module loads on every
|
|
29
|
+
// page, view mode included, and almost none of them ever show it.
|
|
30
|
+
let ui = null;
|
|
31
|
+
|
|
33
32
|
// A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to the
|
|
34
33
|
// layout viewport, so a bottom-anchored bar parks itself behind the keyboard. Same fix
|
|
35
34
|
// as the conflict notice.
|
|
@@ -48,32 +47,24 @@ function dismiss() {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
function build() {
|
|
51
|
-
root =
|
|
50
|
+
root = ui.bevelSurface("div", [
|
|
52
51
|
"position:fixed", "left:50%", "transform:translateX(-50%)",
|
|
53
|
-
"z-index:2147483001", "display:flex", "align-items:center", "gap:10px",
|
|
54
|
-
"max-width:calc(100vw - 24px)", "flex-wrap:wrap",
|
|
55
|
-
"padding:
|
|
56
|
-
`background:${BG}`, `color:${INK}`, `border:1px solid ${EDGE}`,
|
|
57
|
-
"box-shadow:0 6px 24px rgba(0,0,0,.32),0 1px 2px rgba(0,0,0,.24)",
|
|
58
|
-
`font:${FONT}`, "text-align:left",
|
|
52
|
+
"z-index:2147483001", "display:flex", "align-items:center", "gap:10px 12px",
|
|
53
|
+
"width:max-content", "max-width:calc(100vw - 24px)", "flex-wrap:wrap",
|
|
54
|
+
"padding:8px 8px 8px 14px", "text-align:left", `color-scheme:${ui.pageScheme()}`,
|
|
59
55
|
]);
|
|
60
56
|
// Three markers, and each is load-bearing on a page that CAN save: this element is
|
|
61
57
|
// injected, so it is in no document on disk and must never reach one, never wake the
|
|
62
58
|
// watcher, and never ride out in a snapshot to somebody else's browser.
|
|
63
|
-
root.setAttribute("clay",
|
|
59
|
+
root.setAttribute("clay", ui.RUNTIME_ONLY);
|
|
64
60
|
root.setAttribute("data-clay-stale-host", "");
|
|
65
61
|
root.setAttribute("role", "alert");
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const close =
|
|
70
|
-
"all:initial", "box-sizing:border-box", "cursor:pointer", `font:${FONT}`,
|
|
71
|
-
"color:" + INK, "opacity:.72", "padding:2px 6px", "border-radius:6px", "flex:none",
|
|
72
|
-
], "Dismiss");
|
|
73
|
-
close.type = "button";
|
|
63
|
+
const dot = make("span", ["all:initial", "color-scheme:inherit", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${ui.TOKENS.ox}`]);
|
|
64
|
+
dot.setAttribute("clay", ui.RUNTIME_ONLY);
|
|
65
|
+
const close = ui.bevelButton("Dismiss", { small: true, variant: "quiet", onClick: dismiss });
|
|
74
66
|
close.setAttribute("aria-label", "Dismiss this message");
|
|
75
|
-
|
|
76
|
-
root.append(close);
|
|
67
|
+
root.append(dot, ui.bevelText("span", [], MESSAGE), close);
|
|
77
68
|
|
|
78
69
|
document.body.appendChild(root);
|
|
79
70
|
place();
|
|
@@ -81,7 +72,7 @@ function build() {
|
|
|
81
72
|
window.visualViewport?.addEventListener("scroll", place);
|
|
82
73
|
}
|
|
83
74
|
|
|
84
|
-
function init() {
|
|
75
|
+
async function init() {
|
|
85
76
|
if (!servedStaleToken()) return;
|
|
86
77
|
// ?editmode=true outranks the stale-host check by design: that is a person at the
|
|
87
78
|
// keyboard asking for editing on this load, and is-edit-mode.js gives it to them.
|
|
@@ -89,7 +80,13 @@ function init() {
|
|
|
89
80
|
// told its reader the opposite. The message is only true while editing is actually
|
|
90
81
|
// off, so it is shown only then.
|
|
91
82
|
if (isEditMode) return;
|
|
83
|
+
const [controls, bevel] = await Promise.all([import("../ui/bevel-controls.js"), import("../ui/bevel.js")]);
|
|
84
|
+
ui = { ...controls, TOKENS: bevel.TOKENS };
|
|
92
85
|
build();
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
|
|
88
|
+
let shown = Promise.resolve();
|
|
89
|
+
onDomReady(() => { shown = init(); });
|
|
90
|
+
|
|
91
|
+
// Settles once the warning is on the page, or straight away when there is none to show.
|
|
92
|
+
export const whenShown = () => shown;
|
|
@@ -19,6 +19,20 @@ import { isEditMode } from "./is-edit-mode.js";
|
|
|
19
19
|
import { captureForDirtyCheck } from "./snapshot.js";
|
|
20
20
|
import { getLastSavedDirty } from "./save.js";
|
|
21
21
|
import { logUnloadDiffSync, preloadIfEnabled } from "../lib/autosave-debug.js";
|
|
22
|
+
import { hasUnsavedState } from "../lib/unsaved-state.js";
|
|
23
|
+
|
|
24
|
+
// One beforeunload let through, for the reload a person confirmed with two presses
|
|
25
|
+
// in the conflict notice. Nothing else is switched off: the next close still warns.
|
|
26
|
+
let discardPermit = false;
|
|
27
|
+
export function reloadAfterDiscard({ isCurrent, reload }) {
|
|
28
|
+
if (!isCurrent()) return false;
|
|
29
|
+
discardPermit = true;
|
|
30
|
+
// Browsers fire beforeunload for a reload on their own schedule; a permit still
|
|
31
|
+
// unused a second later means the navigation did not happen.
|
|
32
|
+
setTimeout(() => { discardPermit = false; }, 1000);
|
|
33
|
+
reload();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
// Pre-load diff library if debug mode is on (so it's ready for unload)
|
|
24
38
|
preloadIfEnabled();
|
|
@@ -31,20 +45,31 @@ preloadIfEnabled();
|
|
|
31
45
|
// the person editing it deserves the warning.
|
|
32
46
|
window.addEventListener('beforeunload', (event) => {
|
|
33
47
|
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;
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
if (discardPermit) {
|
|
50
|
+
discardPermit = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Work outside the DOM first: it needs no capture, and a capture that throws
|
|
55
|
+
// must not hide it. The demo plugin does not save it either, so it warns there too.
|
|
56
|
+
const held = hasUnsavedState();
|
|
43
57
|
|
|
44
|
-
|
|
58
|
+
// The demo plugin saves the page's bytes into this browser's own storage, so
|
|
59
|
+
// leaving loses none of them.
|
|
60
|
+
let bytesDiffer = false;
|
|
61
|
+
if (!held && !window.clay?.demo) {
|
|
62
|
+
// The DIRTY domain, not the autosave domain. An edit inside a
|
|
63
|
+
// no-trigger-autosave region never starts a save by itself, which is exactly
|
|
64
|
+
// why closing the tab on one has to warn: nothing else is going to write it.
|
|
65
|
+
const currentForCompare = captureForDirtyCheck();
|
|
66
|
+
const lastSaved = getLastSavedDirty();
|
|
67
|
+
bytesDiffer = currentForCompare !== lastSaved;
|
|
45
68
|
// Debug: log what's different before showing the warning
|
|
46
|
-
logUnloadDiffSync(currentForCompare, lastSaved);
|
|
69
|
+
if (bytesDiffer) logUnloadDiffSync(currentForCompare, lastSaved);
|
|
70
|
+
}
|
|
47
71
|
|
|
72
|
+
if (held || bytesDiffer) {
|
|
48
73
|
event.preventDefault();
|
|
49
74
|
event.returnValue = '';
|
|
50
75
|
}
|
package/src/lib/content-dom.js
CHANGED
|
@@ -27,8 +27,10 @@ function matchesWithin(node, selector, boundary, inherit) {
|
|
|
27
27
|
|
|
28
28
|
function importTree(source, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps) {
|
|
29
29
|
if (source.nodeType === 1 && (
|
|
30
|
-
(inherit
|
|
31
|
-
|
|
30
|
+
(inherit
|
|
31
|
+
? hasCapability(source, capability)
|
|
32
|
+
: matchesWithin(source, capabilityMatch, boundary, false)) ||
|
|
33
|
+
(exclude && (inherit && source !== boundary ? source.matches(exclude) : matchesWithin(source, exclude, boundary, inherit)))
|
|
32
34
|
)) return null
|
|
33
35
|
const copy = targetDocument.importNode(source, false)
|
|
34
36
|
maps.cloneToLive.set(copy, source)
|
package/src/lib/dirty-gate.js
CHANGED
|
@@ -191,3 +191,12 @@ export function gateClearIfUnchanged(token) {
|
|
|
191
191
|
if (typeof document !== 'undefined' && isEditMode) {
|
|
192
192
|
startDirtyGate();
|
|
193
193
|
}
|
|
194
|
+
|
|
195
|
+
// Public for editors whose own surface is editor-ui (so neither gate feed sees it):
|
|
196
|
+
// a person's edit made while a live-sync frame waits on a resource would otherwise be
|
|
197
|
+
// recorded as saved. Set here, not in the loader, because the sync plugin is what
|
|
198
|
+
// loads this module.
|
|
199
|
+
if (typeof window !== 'undefined') {
|
|
200
|
+
window.clay = window.clay || {};
|
|
201
|
+
window.clay.markDirty = gateMarkDirty;
|
|
202
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unsaved-state.js: "this tab holds work its saved bytes cannot show".
|
|
3
|
+
*
|
|
4
|
+
* The close warning compares the page's bytes with the last save. Some work is
|
|
5
|
+
* not in those bytes: text a live-sync merge replaced, kept in memory until the
|
|
6
|
+
* person reviews it, or an editor's model ahead of the DOM it writes into. Its
|
|
7
|
+
* owner registers a synchronous check here, and the close warning fires while
|
|
8
|
+
* any check says pending. Checks run fresh each time, so a forgotten state can
|
|
9
|
+
* never outlive what it describes, and a save never clears one.
|
|
10
|
+
*/
|
|
11
|
+
const sources = new Map();
|
|
12
|
+
let nextKey = 0;
|
|
13
|
+
|
|
14
|
+
export function registerUnsavedState({ id = '', isPending }) {
|
|
15
|
+
const key = ++nextKey;
|
|
16
|
+
sources.set(key, { id, isPending });
|
|
17
|
+
let disposed = false;
|
|
18
|
+
return {
|
|
19
|
+
changed() {
|
|
20
|
+
if (disposed) return;
|
|
21
|
+
document.dispatchEvent(new CustomEvent('clay:unsaved-state-changed', {
|
|
22
|
+
detail: { pending: pendingIds() },
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
dispose() {
|
|
26
|
+
if (disposed) return;
|
|
27
|
+
disposed = true;
|
|
28
|
+
sources.delete(key);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A check that throws counts as pending: unknown is not clean.
|
|
34
|
+
function isPending(source) {
|
|
35
|
+
try {
|
|
36
|
+
return !!source.isPending();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(`[clay] unsaved-state check "${source.id}" threw`, err);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function pendingIds() {
|
|
44
|
+
return [...sources.values()].filter(isPending).map((s) => s.id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasUnsavedState() {
|
|
48
|
+
for (const source of sources.values()) if (isPending(source)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
window.clay = window.clay || {};
|
|
54
|
+
window.clay.registerUnsavedState = registerUnsavedState;
|
|
55
|
+
}
|
package/src/lib/user-gesture.js
CHANGED
|
@@ -44,6 +44,9 @@ const RECENT_GESTURE_MS = 500;
|
|
|
44
44
|
|
|
45
45
|
let gestureTaskActive = false;
|
|
46
46
|
let lastTrustedGestureTs = -Infinity;
|
|
47
|
+
// Set by the first trusted gesture and never cleared. Live sync reads it at the
|
|
48
|
+
// load-time settle: a page nobody has touched yet holds no one's edit.
|
|
49
|
+
let anyGesture = false;
|
|
47
50
|
let userDrivenSinceLastSave = false;
|
|
48
51
|
let explicitSaveIntent = false;
|
|
49
52
|
let installed = false;
|
|
@@ -55,6 +58,7 @@ function now() {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function markGestureTurn() {
|
|
61
|
+
anyGesture = true;
|
|
58
62
|
gestureTaskActive = true;
|
|
59
63
|
lastTrustedGestureTs = now();
|
|
60
64
|
// Clear the same-turn flag on the next macrotask. The MutationObserver
|
|
@@ -100,6 +104,11 @@ export function markUserDriven() {
|
|
|
100
104
|
userDrivenSinceLastSave = true;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/** True once any trusted gesture has reached the page. */
|
|
108
|
+
export function gestureSeen() {
|
|
109
|
+
return anyGesture;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
/**
|
|
104
113
|
* Read-and-reset the accumulated bit. Called at the ACTUAL save send (not on a
|
|
105
114
|
* save that never ships), so it survives the autosave debounce and coalescing.
|
|
@@ -144,6 +153,7 @@ export function _resetUserGesture() {
|
|
|
144
153
|
gestureTaskActive = false;
|
|
145
154
|
lastTrustedGestureTs = -Infinity;
|
|
146
155
|
userDrivenSinceLastSave = false;
|
|
156
|
+
anyGesture = false;
|
|
147
157
|
explicitSaveIntent = false;
|
|
148
158
|
}
|
|
149
159
|
|
package/src/loader-logic.js
CHANGED
|
@@ -16,7 +16,7 @@ export const CORE_WAVES = {
|
|
|
16
16
|
// transform registered after that made an untouched page read as unsaved.
|
|
17
17
|
"core/persist.js", "core/admin-attrs.js",
|
|
18
18
|
"core/save-core.js", "core/save.js",
|
|
19
|
-
"core/
|
|
19
|
+
"core/conflict-notice.js",
|
|
20
20
|
"core/unsaved-warning.js", "core/autosave.js",
|
|
21
21
|
"attrs/save-freeze.js", "attrs/onaftersave.js", "attrs/refetch-on-save.js",
|
|
22
22
|
"lib/cache-bust.js",
|
|
@@ -24,20 +24,21 @@ export const CORE_WAVES = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const PLUGIN_PATHS = {
|
|
27
|
-
richclay: { path: "
|
|
27
|
+
richclay: { path: "plugins/richclay.js", editOnly: true, default: true },
|
|
28
28
|
indicator: { path: "plugins/indicator.js", editOnly: true, default: false },
|
|
29
29
|
sync: { path: "sync/live-sync.js", editOnly: false, default: false },
|
|
30
30
|
sortable: { path: "plugins/sortable.js", editOnly: true, default: false },
|
|
31
31
|
undo: { path: "plugins/undo.js", editOnly: true, default: false },
|
|
32
|
-
cms: { path: "
|
|
33
|
-
quickcrop: { path: "
|
|
32
|
+
cms: { path: "plugins/cms.js", editOnly: false, default: false },
|
|
33
|
+
quickcrop: { path: "plugins/quickcrop.js", editOnly: false, default: false },
|
|
34
34
|
// editOnly, because a file picker only ever appears in edit mode: the cms
|
|
35
35
|
// injects its own editing toggle there and clayjs's edit-mode signal is a
|
|
36
36
|
// superset of the cms's, so the plugin is present exactly when it can be used.
|
|
37
37
|
upload: { path: "plugins/upload.js", editOnly: true, default: false },
|
|
38
38
|
wire: { path: "plugins/wire.js", editOnly: false, default: false },
|
|
39
|
-
// Edit mode only: the AI comment box is an editing gesture
|
|
40
|
-
// dormant
|
|
39
|
+
// Edit mode only: the AI comment box is an editing gesture. Opt in with
|
|
40
|
+
// `plugins=ai-edit`; it stays dormant on any host that does not list a ready
|
|
41
|
+
// `ai-edit` helper.
|
|
41
42
|
"ai-edit": { path: "plugins/ai-edit.js", editOnly: true, default: false },
|
|
42
43
|
demo: { path: "plugins/demo.js", editOnly: false, default: false },
|
|
43
44
|
// Saves the file's own bytes back instead of a fresh serialization of the DOM.
|
|
@@ -68,7 +69,7 @@ export const MODULES = {
|
|
|
68
69
|
"core/snapshot.js": () => import("./core/snapshot.js"),
|
|
69
70
|
"core/save-core.js": () => import("./core/save-core.js"),
|
|
70
71
|
"core/save.js": () => import("./core/save.js"),
|
|
71
|
-
"core/
|
|
72
|
+
"core/conflict-notice.js": () => import("./core/conflict-notice.js"),
|
|
72
73
|
"core/unsaved-warning.js": () => import("./core/unsaved-warning.js"),
|
|
73
74
|
"core/persist.js": () => import("./core/persist.js"),
|
|
74
75
|
"core/admin-attrs.js": () => import("./core/admin-attrs.js"),
|
|
@@ -78,13 +79,13 @@ export const MODULES = {
|
|
|
78
79
|
"attrs/refetch-on-save.js": () => import("./attrs/refetch-on-save.js"),
|
|
79
80
|
"lib/cache-bust.js": () => import("./lib/cache-bust.js"),
|
|
80
81
|
"vendor/hyper-morph.vendor.js": () => import("./vendor/hyper-morph.vendor.js"),
|
|
81
|
-
"
|
|
82
|
+
"plugins/richclay.js": () => import("./plugins/richclay.js"),
|
|
82
83
|
"plugins/indicator.js": () => import("./plugins/indicator.js"),
|
|
83
84
|
"sync/live-sync.js": () => import("./sync/live-sync.js"),
|
|
84
85
|
"plugins/sortable.js": () => import("./plugins/sortable.js"),
|
|
85
86
|
"plugins/undo.js": () => import("./plugins/undo.js"),
|
|
86
|
-
"
|
|
87
|
-
"
|
|
87
|
+
"plugins/cms.js": () => import("./plugins/cms.js"),
|
|
88
|
+
"plugins/quickcrop.js": () => import("./plugins/quickcrop.js"),
|
|
88
89
|
"plugins/upload.js": () => import("./plugins/upload.js"),
|
|
89
90
|
"plugins/wire.js": () => import("./plugins/wire.js"),
|
|
90
91
|
"plugins/ai-edit.js": () => import("./plugins/ai-edit.js"),
|
|
@@ -114,6 +115,8 @@ const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "quickcrop",
|
|
|
114
115
|
// enables. Reverting it is reverting this line.
|
|
115
116
|
// ai-edit reads `clay.wire.helpers()` and sends through `clay.wire.send`, so a page
|
|
116
117
|
// that asks for it has to have the wire. The wire loads first in the order above.
|
|
118
|
+
// It does not bring undo: hyper-undo takes Cmd+Z for the whole window, text fields
|
|
119
|
+
// included, which a page that never asked for it should not get by default.
|
|
117
120
|
const IMPLIES = { cms: ["quickcrop", "upload"], "ai-edit": ["wire"] };
|
|
118
121
|
|
|
119
122
|
function parseCsv(params, key, enabled, apply) {
|
|
@@ -134,17 +137,25 @@ export function resolveModules(params, isEditMode) {
|
|
|
134
137
|
const core = [...CORE_WAVES.always];
|
|
135
138
|
if (isEditMode) core.push(...CORE_WAVES.editOnly);
|
|
136
139
|
|
|
137
|
-
const
|
|
140
|
+
const explicit = new Set();
|
|
141
|
+
parseCsv(params, "plugins", explicit, (set, name) => set.add(name));
|
|
142
|
+
const excluded = new Set();
|
|
143
|
+
parseCsv(params, "exclude", excluded, (set, name) => set.add(name));
|
|
144
|
+
|
|
145
|
+
const enabled = new Set(explicit);
|
|
138
146
|
for (const [name, spec] of Object.entries(PLUGIN_PATHS)) {
|
|
139
147
|
if (spec.default) enabled.add(name);
|
|
140
148
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
149
|
+
// ai-edit cannot run without the wire, so excluding the wire excludes it too.
|
|
150
|
+
if (excluded.has("wire")) enabled.delete("ai-edit");
|
|
151
|
+
// A default-on plugin that this mode drops brings nothing with it. An explicitly
|
|
152
|
+
// asked for plugin still brings what it implies.
|
|
144
153
|
for (const name of [...enabled]) {
|
|
154
|
+
if (excluded.has(name)) continue;
|
|
155
|
+
if (!explicit.has(name) && PLUGIN_PATHS[name].editOnly && !isEditMode) continue;
|
|
145
156
|
for (const implied of IMPLIES[name] || []) enabled.add(implied);
|
|
146
157
|
}
|
|
147
|
-
|
|
158
|
+
for (const name of excluded) enabled.delete(name);
|
|
148
159
|
|
|
149
160
|
const plugins = [];
|
|
150
161
|
for (const name of PLUGIN_ORDER) {
|
package/src/loader.js
CHANGED
|
@@ -108,7 +108,8 @@ function attachPluginMember(path, mod) {
|
|
|
108
108
|
clay.undo = mod.undo || mod.default;
|
|
109
109
|
} else if (path === "sync/live-sync.js") {
|
|
110
110
|
clay.morph = mod.morph;
|
|
111
|
-
|
|
111
|
+
clay.conflicts = mod.conflicts;
|
|
112
|
+
} else if (path === "plugins/cms.js") {
|
|
112
113
|
clay.cms = mod.cms || mod.default;
|
|
113
114
|
} else if (path === "plugins/upload.js") {
|
|
114
115
|
clay.upload = mod.upload || mod.default;
|
|
@@ -118,9 +119,9 @@ function attachPluginMember(path, mod) {
|
|
|
118
119
|
clay.aiEdit = mod.aiEdit || mod.default;
|
|
119
120
|
} else if (path === "plugins/demo.js") {
|
|
120
121
|
clay.demo = mod.demo;
|
|
121
|
-
} else if (path === "
|
|
122
|
+
} else if (path === "plugins/richclay.js") {
|
|
122
123
|
clay.RichClay = mod.RichClay || mod.default;
|
|
123
|
-
} else if (path === "
|
|
124
|
+
} else if (path === "plugins/quickcrop.js") {
|
|
124
125
|
clay.quickcrop = mod.quickcrop || mod.default;
|
|
125
126
|
} else if (path === "plugins/source.js") {
|
|
126
127
|
clay.source = mod.source || mod.default;
|