@nuforge/editor 0.3.0 → 0.3.1
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/dist/index.cjs +2 -113
- package/dist/index.d.ts +0 -151
- package/dist/index.mjs +2 -113
- package/dist/react.cjs +14 -137
- package/dist/react.d.ts +0 -164
- package/dist/react.mjs +14 -137
- package/package.json +9 -9
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -3,11 +3,9 @@ import { Nu } from '@nuforge/core';
|
|
|
3
3
|
import { reactive } from '@nuforge/reactivity';
|
|
4
4
|
import { Parser } from '@nuforge/parser';
|
|
5
5
|
|
|
6
|
-
/** A literal expression (`"x"`, `5`, `true`). */
|
|
7
6
|
function lit(value) {
|
|
8
7
|
return t.literal({ value });
|
|
9
8
|
}
|
|
10
|
-
/** A text node: `<text text={value} />`, rendered as its string value. */
|
|
11
9
|
function text(value) {
|
|
12
10
|
return t.tagTemplate({
|
|
13
11
|
tag: 'text',
|
|
@@ -15,10 +13,6 @@ function text(value) {
|
|
|
15
13
|
children: [],
|
|
16
14
|
});
|
|
17
15
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Concise element builder — `el('div', { class: lit('box') }, [text('hi')])`
|
|
20
|
-
* instead of the verbose `t.tagTemplate({ tag, props, children })`.
|
|
21
|
-
*/
|
|
22
16
|
function el(tag, props = {}, children = []) {
|
|
23
17
|
return t.tagTemplate({ tag, props, children });
|
|
24
18
|
}
|
|
@@ -51,7 +45,6 @@ class BlockRegistry {
|
|
|
51
45
|
return out;
|
|
52
46
|
}
|
|
53
47
|
}
|
|
54
|
-
/** The built-in palette — the common HTML primitives. */
|
|
55
48
|
const DEFAULT_BLOCKS = [
|
|
56
49
|
{ id: 'box', label: 'Box', icon: 'square', create: () => el('div') },
|
|
57
50
|
{ id: 'text', label: 'Text', icon: 'type', create: () => text('Text') },
|
|
@@ -81,27 +74,11 @@ const DEFAULT_BLOCKS = [
|
|
|
81
74
|
},
|
|
82
75
|
];
|
|
83
76
|
|
|
84
|
-
/**
|
|
85
|
-
* Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
|
|
86
|
-
* navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
|
|
87
|
-
* interaction can't cross as direct object references the way `IframeCanvas`'s
|
|
88
|
-
* portal allows — everything here is a structured-clone-safe plain message.
|
|
89
|
-
*
|
|
90
|
-
* Deliberately generic: only the shapes the editor primitive itself needs
|
|
91
|
-
* (state sync, ready handshake, the core interaction events) are typed. Host
|
|
92
|
-
* concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
|
|
93
|
-
* opaque `payload` — this package relays it, never interprets it.
|
|
94
|
-
*/
|
|
95
77
|
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
96
78
|
const CANVAS_PROTOCOL_VERSION = 1;
|
|
97
|
-
/** Namespace the `BroadcastChannel` per canvas instance (derive from the
|
|
98
|
-
* iframe's own `src`, so a host never has to invent/track a separate id). */
|
|
99
79
|
function canvasChannelName(src) {
|
|
100
80
|
return `${CANVAS_CHANNEL}:${src}`;
|
|
101
81
|
}
|
|
102
|
-
/** Narrow an arbitrary `BroadcastChannel` payload to a message from this
|
|
103
|
-
* protocol — guards against unrelated traffic on the same channel name and
|
|
104
|
-
* against a version mismatch across a stale cached bundle. */
|
|
105
82
|
function isCanvasMessage(data) {
|
|
106
83
|
return (!!data &&
|
|
107
84
|
typeof data === 'object' &&
|
|
@@ -109,47 +86,18 @@ function isCanvasMessage(data) {
|
|
|
109
86
|
data.v === CANVAS_PROTOCOL_VERSION);
|
|
110
87
|
}
|
|
111
88
|
|
|
112
|
-
/**
|
|
113
|
-
* Canvas-side receiver for a genuinely-navigated `/canvas/[id]`-style route:
|
|
114
|
-
* a `Nu` instance kept in sync with the host's one-way, via `BroadcastChannel`
|
|
115
|
-
* + `t.unflatten`/`t.merge` (the same primitives `@nuforge/collaboration`'s
|
|
116
|
-
* `LoroSyncProvider` uses for its state<->doc binding, without the CRDT layer
|
|
117
|
-
* — there's exactly one writer here, so convergence machinery is unneeded).
|
|
118
|
-
* No `createEditor()`: this instance is never meant to originate an edit the
|
|
119
|
-
* host doesn't already know about, so it only needs `Nu`, not the mutation SDK.
|
|
120
|
-
*/
|
|
121
89
|
function createCanvasReceiver(opts) {
|
|
122
90
|
const nu = Nu.create({ externals: opts.externals });
|
|
123
91
|
const volatile = reactive({ activeComponent: null, loadGeneration: 0 });
|
|
124
92
|
const channel = new BroadcastChannel(canvasChannelName(opts.src));
|
|
125
93
|
const uiListeners = new Set();
|
|
126
|
-
/** Did the incoming flattened state add or remove any node id (a
|
|
127
|
-
* structural change), vs. only edit fields on already-known nodes? */
|
|
128
|
-
const isStructuralChange = (incoming) => {
|
|
129
|
-
if (!nu.loaded) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
const currentKeys = Object.keys(t.flatten(nu.state).types);
|
|
133
|
-
const incomingKeys = Object.keys(incoming);
|
|
134
|
-
if (currentKeys.length !== incomingKeys.length) {
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
const incomingSet = new Set(incomingKeys);
|
|
138
|
-
return currentKeys.some((key) => !incomingSet.has(key));
|
|
139
|
-
};
|
|
140
94
|
const applyState = (message) => {
|
|
141
|
-
|
|
142
|
-
// see RemoteCanvasHost's `previewMode`/`activeComponent`-change doc), OR
|
|
143
|
-
// a `patch` that structurally changed the tree. See `volatile.loadGeneration`'s
|
|
144
|
-
// doc for why structural patches are folded into this same path instead
|
|
145
|
-
// of merging in place.
|
|
146
|
-
const needsFullReload = message.type === 'init' || isStructuralChange(message.flat.types);
|
|
147
|
-
if (needsFullReload) {
|
|
95
|
+
if (message.type === 'init') {
|
|
148
96
|
nu.load(t.unflatten(message.flat));
|
|
149
97
|
volatile.loadGeneration++;
|
|
150
98
|
}
|
|
151
99
|
else {
|
|
152
|
-
nu.change(() => t.merge(nu.state, t.unflatten(message.flat)));
|
|
100
|
+
nu.change(() => t.merge(nu.state, t.unflatten(message.flat), { arrayReconcile: 'id' }));
|
|
153
101
|
}
|
|
154
102
|
volatile.activeComponent = message.activeComponent;
|
|
155
103
|
};
|
|
@@ -218,13 +166,6 @@ class CommandRegistry {
|
|
|
218
166
|
}
|
|
219
167
|
}
|
|
220
168
|
|
|
221
|
-
/**
|
|
222
|
-
* Compute a drop position from the pointer's Y against a target's bounding rect.
|
|
223
|
-
* When `allowInside` is set, the middle band is `inside` (drop as a child) and
|
|
224
|
-
* the outer bands are `before`/`after`; otherwise it's a simple before/after
|
|
225
|
-
* split at the midpoint. Pure — use it from an `onDragOver`/`onPointerMove`
|
|
226
|
-
* handler to drive a drop indicator and the eventual `moveNode`/`pasteNode`.
|
|
227
|
-
*/
|
|
228
169
|
function dropPositionFromPointer(clientY, rect, opts = {}) {
|
|
229
170
|
const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
|
|
230
171
|
if (opts.allowInside) {
|
|
@@ -236,7 +177,6 @@ function dropPositionFromPointer(clientY, rect, opts = {}) {
|
|
|
236
177
|
}
|
|
237
178
|
return ratio < 0.5 ? 'before' : 'after';
|
|
238
179
|
}
|
|
239
|
-
/** A human label for a template node (tag name, component name, slot, …). */
|
|
240
180
|
function templateLabel(node) {
|
|
241
181
|
if (node instanceof t.TagTemplate) {
|
|
242
182
|
return node.tag;
|
|
@@ -252,15 +192,12 @@ function templateLabel(node) {
|
|
|
252
192
|
}
|
|
253
193
|
return 'node';
|
|
254
194
|
}
|
|
255
|
-
/** Child templates of a node (empty for leaves / text nodes). */
|
|
256
195
|
function templateChildren(node) {
|
|
257
196
|
return (node.children ?? []).filter((child) => child instanceof t.Type);
|
|
258
197
|
}
|
|
259
|
-
/** Is this a text node (`<text text=…>`), rendered as its string value? */
|
|
260
198
|
function isTextTemplate(node) {
|
|
261
199
|
return node instanceof t.TagTemplate && node.tag === 'text';
|
|
262
200
|
}
|
|
263
|
-
/** Preview the content of a text node, e.g. for a layers tree. */
|
|
264
201
|
function textPreview(node) {
|
|
265
202
|
if (!isTextTemplate(node)) {
|
|
266
203
|
return null;
|
|
@@ -271,7 +208,6 @@ function textPreview(node) {
|
|
|
271
208
|
}
|
|
272
209
|
return '{…}';
|
|
273
210
|
}
|
|
274
|
-
/** Can this node hold appended children? */
|
|
275
211
|
function canHaveChildren(node) {
|
|
276
212
|
if (!node || isTextTemplate(node)) {
|
|
277
213
|
return false;
|
|
@@ -280,7 +216,6 @@ function canHaveChildren(node) {
|
|
|
280
216
|
node instanceof t.ComponentTemplate ||
|
|
281
217
|
t.is(node, t.FragmentTemplate));
|
|
282
218
|
}
|
|
283
|
-
/** Is `ancestor` somewhere above `node` in the tree? */
|
|
284
219
|
function isAncestorOf(nu, ancestor, node) {
|
|
285
220
|
let parent = nu.getParentNode(node);
|
|
286
221
|
while (parent) {
|
|
@@ -292,11 +227,6 @@ function isAncestorOf(nu, ancestor, node) {
|
|
|
292
227
|
return false;
|
|
293
228
|
}
|
|
294
229
|
|
|
295
|
-
/**
|
|
296
|
-
* A high-level facade over a {@link Nu} runtime for building visual page
|
|
297
|
-
* editors. Every mutation goes through `nu.change`, so it is transactional and
|
|
298
|
-
* undoable. Reach for these instead of hand-splicing the AST.
|
|
299
|
-
*/
|
|
300
230
|
class Editor {
|
|
301
231
|
nu;
|
|
302
232
|
blocks;
|
|
@@ -319,8 +249,6 @@ class Editor {
|
|
|
319
249
|
run: (e) => e.nu.redo(),
|
|
320
250
|
});
|
|
321
251
|
}
|
|
322
|
-
// --- mutation -------------------------------------------------------------
|
|
323
|
-
/** Append (or insert at `index`) `child` into `parent`'s children. */
|
|
324
252
|
insertNode(parent, child, index) {
|
|
325
253
|
const kids = parent.children;
|
|
326
254
|
if (!Array.isArray(kids)) {
|
|
@@ -335,7 +263,6 @@ class Editor {
|
|
|
335
263
|
}
|
|
336
264
|
});
|
|
337
265
|
}
|
|
338
|
-
/** Remove a node from its parent. Returns false if it has no parent. */
|
|
339
266
|
removeNode(node) {
|
|
340
267
|
let ok = false;
|
|
341
268
|
this.nu.change(() => {
|
|
@@ -343,7 +270,6 @@ class Editor {
|
|
|
343
270
|
});
|
|
344
271
|
return ok;
|
|
345
272
|
}
|
|
346
|
-
/** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
|
|
347
273
|
moveNode(dragged, target, position) {
|
|
348
274
|
if (dragged === target || isAncestorOf(this.nu, dragged, target)) {
|
|
349
275
|
return false;
|
|
@@ -354,40 +280,26 @@ class Editor {
|
|
|
354
280
|
});
|
|
355
281
|
return ok;
|
|
356
282
|
}
|
|
357
|
-
// --- clipboard ------------------------------------------------------------
|
|
358
283
|
clipboardNode = null;
|
|
359
|
-
/** Put a node on the editor clipboard (a clone is made on paste). */
|
|
360
284
|
copyNode(node) {
|
|
361
285
|
this.clipboardNode = node;
|
|
362
286
|
}
|
|
363
|
-
/** Copy a node, then remove it. */
|
|
364
287
|
cutNode(node) {
|
|
365
288
|
this.copyNode(node);
|
|
366
289
|
return this.removeNode(node);
|
|
367
290
|
}
|
|
368
|
-
/** Whether there is something on the clipboard to paste. */
|
|
369
291
|
canPaste() {
|
|
370
292
|
return this.clipboardNode !== null;
|
|
371
293
|
}
|
|
372
|
-
/**
|
|
373
|
-
* Paste a fresh copy (new ids) of the clipboard node before/after/inside
|
|
374
|
-
* `target`. Returns the inserted node, or null if nothing was pasted.
|
|
375
|
-
*/
|
|
376
294
|
pasteNode(target, position = 'after') {
|
|
377
295
|
if (!this.clipboardNode) {
|
|
378
296
|
return null;
|
|
379
297
|
}
|
|
380
298
|
return this.insertCloneRelativeTo(this.clipboardNode, target, position);
|
|
381
299
|
}
|
|
382
|
-
/** Duplicate a node in place (inserted right after it). Returns the copy. */
|
|
383
300
|
duplicateNode(node) {
|
|
384
301
|
return this.insertCloneRelativeTo(node, node, 'after');
|
|
385
302
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Insert a detached node before/after/inside a target (undoable). The core of
|
|
388
|
-
* canvas drag-and-drop: a drop computes a target + position, then calls this.
|
|
389
|
-
* Returns the inserted node.
|
|
390
|
-
*/
|
|
391
303
|
insertNodeAt(node, target, position) {
|
|
392
304
|
let ok = false;
|
|
393
305
|
this.nu.change(() => {
|
|
@@ -395,7 +307,6 @@ class Editor {
|
|
|
395
307
|
});
|
|
396
308
|
return ok ? (this.nu.getNodeFromId(node.id) ?? null) : null;
|
|
397
309
|
}
|
|
398
|
-
/** Create a block and insert it before/after/inside a target. */
|
|
399
310
|
insertBlockAt(blockId, target, position) {
|
|
400
311
|
const block = this.blocks.get(blockId);
|
|
401
312
|
if (!block) {
|
|
@@ -409,10 +320,8 @@ class Editor {
|
|
|
409
320
|
this.nu.change(() => {
|
|
410
321
|
ok = this.insertRelativeTo(clone, target, position);
|
|
411
322
|
});
|
|
412
|
-
// Return the reactive node from the tree (clone is wrapped on insert).
|
|
413
323
|
return ok ? (this.nu.getNodeFromId(clone.id) ?? null) : null;
|
|
414
324
|
}
|
|
415
|
-
/** Set several props at once. */
|
|
416
325
|
updateProps(node, props) {
|
|
417
326
|
this.nu.change(() => {
|
|
418
327
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -430,7 +339,6 @@ class Editor {
|
|
|
430
339
|
delete node.props[key];
|
|
431
340
|
});
|
|
432
341
|
}
|
|
433
|
-
/** Parse `source` as a nuforge expression and assign it to `key`. */
|
|
434
342
|
setExpression(node, key, source) {
|
|
435
343
|
let expr;
|
|
436
344
|
try {
|
|
@@ -442,7 +350,6 @@ class Editor {
|
|
|
442
350
|
this.setProp(node, key, expr);
|
|
443
351
|
return true;
|
|
444
352
|
}
|
|
445
|
-
/** Update a text node's literal value. */
|
|
446
353
|
setText(node, value) {
|
|
447
354
|
const expr = node.props?.text;
|
|
448
355
|
if (!(expr instanceof t.Literal)) {
|
|
@@ -475,7 +382,6 @@ class Editor {
|
|
|
475
382
|
tag.classList.properties[clean] = t.literal({ value: true });
|
|
476
383
|
});
|
|
477
384
|
}
|
|
478
|
-
/** Parse `source` and set the condition under which class `name` applies. */
|
|
479
385
|
setClassCondition(node, name, source) {
|
|
480
386
|
let expr;
|
|
481
387
|
try {
|
|
@@ -504,7 +410,6 @@ class Editor {
|
|
|
504
410
|
}
|
|
505
411
|
});
|
|
506
412
|
}
|
|
507
|
-
/** Set the `@if` condition from source. */
|
|
508
413
|
setCondition(node, source) {
|
|
509
414
|
let expr;
|
|
510
415
|
try {
|
|
@@ -523,8 +428,6 @@ class Editor {
|
|
|
523
428
|
node.if = null;
|
|
524
429
|
});
|
|
525
430
|
}
|
|
526
|
-
// --- loop (@each) ---------------------------------------------------------
|
|
527
|
-
/** Repeat a node over `iterator` (a list expression), binding each `alias`. */
|
|
528
431
|
setEach(node, iterator, alias = 'item', indexName) {
|
|
529
432
|
let expr;
|
|
530
433
|
try {
|
|
@@ -547,7 +450,6 @@ class Editor {
|
|
|
547
450
|
node.each = null;
|
|
548
451
|
});
|
|
549
452
|
}
|
|
550
|
-
// --- component state / props ----------------------------------------------
|
|
551
453
|
addState(component, name = 'value', init = '0') {
|
|
552
454
|
const taken = new Set(component.state.map((v) => v.name));
|
|
553
455
|
let unique = name;
|
|
@@ -632,8 +534,6 @@ class Editor {
|
|
|
632
534
|
});
|
|
633
535
|
return true;
|
|
634
536
|
}
|
|
635
|
-
// --- blocks ---------------------------------------------------------------
|
|
636
|
-
/** Create a block's template and insert it into `parent`. */
|
|
637
537
|
insertBlock(blockId, parent, index) {
|
|
638
538
|
const block = this.blocks.get(blockId);
|
|
639
539
|
if (!block || !canHaveChildren(parent)) {
|
|
@@ -643,7 +543,6 @@ class Editor {
|
|
|
643
543
|
this.insertNode(parent, created, index);
|
|
644
544
|
return created;
|
|
645
545
|
}
|
|
646
|
-
// --- components / pages ---------------------------------------------------
|
|
647
546
|
uniqueName(base) {
|
|
648
547
|
const taken = new Set(this.getComponents().map((c) => c.name));
|
|
649
548
|
if (!taken.has(base)) {
|
|
@@ -655,7 +554,6 @@ class Editor {
|
|
|
655
554
|
}
|
|
656
555
|
return `${base}${i}`;
|
|
657
556
|
}
|
|
658
|
-
/** Add a new component (a "page"). */
|
|
659
557
|
addComponent(name = 'Page') {
|
|
660
558
|
const unique = this.uniqueName(name);
|
|
661
559
|
const component = t.nuComponent({
|
|
@@ -669,7 +567,6 @@ class Editor {
|
|
|
669
567
|
});
|
|
670
568
|
return this.getComponent(unique);
|
|
671
569
|
}
|
|
672
|
-
/** Rename a component, updating `<Name/>` references across the program. */
|
|
673
570
|
renameComponent(component, name) {
|
|
674
571
|
const clean = name.trim();
|
|
675
572
|
if (!clean ||
|
|
@@ -719,7 +616,6 @@ class Editor {
|
|
|
719
616
|
this.walkTemplates(child, fn);
|
|
720
617
|
}
|
|
721
618
|
}
|
|
722
|
-
// --- query ----------------------------------------------------------------
|
|
723
619
|
getComponents() {
|
|
724
620
|
return this.nu.state.program.components.filter((c) => c instanceof t.NuComponent);
|
|
725
621
|
}
|
|
@@ -739,7 +635,6 @@ class Editor {
|
|
|
739
635
|
getNodeIndex(node) {
|
|
740
636
|
return this.getSiblings(node).indexOf(node);
|
|
741
637
|
}
|
|
742
|
-
/** Full path from the State root to this node (for selections/serialization). */
|
|
743
638
|
getNodePath(node) {
|
|
744
639
|
const out = [];
|
|
745
640
|
let current = node;
|
|
@@ -751,12 +646,9 @@ class Editor {
|
|
|
751
646
|
}
|
|
752
647
|
return out;
|
|
753
648
|
}
|
|
754
|
-
// --- change subscription --------------------------------------------------
|
|
755
|
-
/** Fires with the raw field-level writes of every change (incl. undo/redo). */
|
|
756
649
|
onChange(cb) {
|
|
757
650
|
return this.nu.listenToMutations(cb);
|
|
758
651
|
}
|
|
759
|
-
/** Fires only when the tree shape changes (a child array was mutated). */
|
|
760
652
|
onStructureChange(cb) {
|
|
761
653
|
return this.nu.listenToMutations((entries) => {
|
|
762
654
|
if (entries.some((e) => Array.isArray(e.target))) {
|
|
@@ -764,7 +656,6 @@ class Editor {
|
|
|
764
656
|
}
|
|
765
657
|
});
|
|
766
658
|
}
|
|
767
|
-
// --- internals (run inside nu.change) -------------------------------------
|
|
768
659
|
spliceFromParent(node) {
|
|
769
660
|
const parent = this.nu.getParentNode(node);
|
|
770
661
|
const kids = parent?.children;
|
|
@@ -812,7 +703,6 @@ class Editor {
|
|
|
812
703
|
destKids.splice(position === 'after' ? ti + 1 : ti, 0, dragged);
|
|
813
704
|
return true;
|
|
814
705
|
}
|
|
815
|
-
/** Insert an already-detached node relative to `target` (no removal). */
|
|
816
706
|
insertRelativeTo(node, target, position) {
|
|
817
707
|
if (position === 'inside') {
|
|
818
708
|
if (!canHaveChildren(target)) {
|
|
@@ -839,4 +729,3 @@ function createEditor(nu, opts) {
|
|
|
839
729
|
}
|
|
840
730
|
|
|
841
731
|
export { BlockRegistry, CANVAS_CHANNEL, CANVAS_PROTOCOL_VERSION, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, canvasChannelName, createCanvasReceiver, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isCanvasMessage, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
|
|
842
|
-
//# sourceMappingURL=index.mjs.map
|