@nuforge/editor 0.3.0 → 0.3.2
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 +5 -114
- package/dist/index.d.ts +0 -151
- package/dist/index.mjs +5 -114
- 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.cjs
CHANGED
|
@@ -24,11 +24,9 @@ function _interopNamespaceDefault(e) {
|
|
|
24
24
|
|
|
25
25
|
var t__namespace = /*#__PURE__*/_interopNamespaceDefault(t);
|
|
26
26
|
|
|
27
|
-
/** A literal expression (`"x"`, `5`, `true`). */
|
|
28
27
|
function lit(value) {
|
|
29
28
|
return t__namespace.literal({ value });
|
|
30
29
|
}
|
|
31
|
-
/** A text node: `<text text={value} />`, rendered as its string value. */
|
|
32
30
|
function text(value) {
|
|
33
31
|
return t__namespace.tagTemplate({
|
|
34
32
|
tag: 'text',
|
|
@@ -36,10 +34,6 @@ function text(value) {
|
|
|
36
34
|
children: [],
|
|
37
35
|
});
|
|
38
36
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Concise element builder — `el('div', { class: lit('box') }, [text('hi')])`
|
|
41
|
-
* instead of the verbose `t.tagTemplate({ tag, props, children })`.
|
|
42
|
-
*/
|
|
43
37
|
function el(tag, props = {}, children = []) {
|
|
44
38
|
return t__namespace.tagTemplate({ tag, props, children });
|
|
45
39
|
}
|
|
@@ -72,7 +66,6 @@ class BlockRegistry {
|
|
|
72
66
|
return out;
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
|
-
/** The built-in palette — the common HTML primitives. */
|
|
76
69
|
const DEFAULT_BLOCKS = [
|
|
77
70
|
{ id: 'box', label: 'Box', icon: 'square', create: () => el('div') },
|
|
78
71
|
{ id: 'text', label: 'Text', icon: 'type', create: () => text('Text') },
|
|
@@ -102,27 +95,11 @@ const DEFAULT_BLOCKS = [
|
|
|
102
95
|
},
|
|
103
96
|
];
|
|
104
97
|
|
|
105
|
-
/**
|
|
106
|
-
* Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
|
|
107
|
-
* navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
|
|
108
|
-
* interaction can't cross as direct object references the way `IframeCanvas`'s
|
|
109
|
-
* portal allows — everything here is a structured-clone-safe plain message.
|
|
110
|
-
*
|
|
111
|
-
* Deliberately generic: only the shapes the editor primitive itself needs
|
|
112
|
-
* (state sync, ready handshake, the core interaction events) are typed. Host
|
|
113
|
-
* concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
|
|
114
|
-
* opaque `payload` — this package relays it, never interprets it.
|
|
115
|
-
*/
|
|
116
98
|
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
117
99
|
const CANVAS_PROTOCOL_VERSION = 1;
|
|
118
|
-
/** Namespace the `BroadcastChannel` per canvas instance (derive from the
|
|
119
|
-
* iframe's own `src`, so a host never has to invent/track a separate id). */
|
|
120
100
|
function canvasChannelName(src) {
|
|
121
101
|
return `${CANVAS_CHANNEL}:${src}`;
|
|
122
102
|
}
|
|
123
|
-
/** Narrow an arbitrary `BroadcastChannel` payload to a message from this
|
|
124
|
-
* protocol — guards against unrelated traffic on the same channel name and
|
|
125
|
-
* against a version mismatch across a stale cached bundle. */
|
|
126
103
|
function isCanvasMessage(data) {
|
|
127
104
|
return (!!data &&
|
|
128
105
|
typeof data === 'object' &&
|
|
@@ -130,47 +107,18 @@ function isCanvasMessage(data) {
|
|
|
130
107
|
data.v === CANVAS_PROTOCOL_VERSION);
|
|
131
108
|
}
|
|
132
109
|
|
|
133
|
-
/**
|
|
134
|
-
* Canvas-side receiver for a genuinely-navigated `/canvas/[id]`-style route:
|
|
135
|
-
* a `Nu` instance kept in sync with the host's one-way, via `BroadcastChannel`
|
|
136
|
-
* + `t.unflatten`/`t.merge` (the same primitives `@nuforge/collaboration`'s
|
|
137
|
-
* `LoroSyncProvider` uses for its state<->doc binding, without the CRDT layer
|
|
138
|
-
* — there's exactly one writer here, so convergence machinery is unneeded).
|
|
139
|
-
* No `createEditor()`: this instance is never meant to originate an edit the
|
|
140
|
-
* host doesn't already know about, so it only needs `Nu`, not the mutation SDK.
|
|
141
|
-
*/
|
|
142
110
|
function createCanvasReceiver(opts) {
|
|
143
111
|
const nu = core.Nu.create({ externals: opts.externals });
|
|
144
112
|
const volatile = reactivity.reactive({ activeComponent: null, loadGeneration: 0 });
|
|
145
113
|
const channel = new BroadcastChannel(canvasChannelName(opts.src));
|
|
146
114
|
const uiListeners = new Set();
|
|
147
|
-
/** Did the incoming flattened state add or remove any node id (a
|
|
148
|
-
* structural change), vs. only edit fields on already-known nodes? */
|
|
149
|
-
const isStructuralChange = (incoming) => {
|
|
150
|
-
if (!nu.loaded) {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
const currentKeys = Object.keys(t__namespace.flatten(nu.state).types);
|
|
154
|
-
const incomingKeys = Object.keys(incoming);
|
|
155
|
-
if (currentKeys.length !== incomingKeys.length) {
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
const incomingSet = new Set(incomingKeys);
|
|
159
|
-
return currentKeys.some((key) => !incomingSet.has(key));
|
|
160
|
-
};
|
|
161
115
|
const applyState = (message) => {
|
|
162
|
-
|
|
163
|
-
// see RemoteCanvasHost's `previewMode`/`activeComponent`-change doc), OR
|
|
164
|
-
// a `patch` that structurally changed the tree. See `volatile.loadGeneration`'s
|
|
165
|
-
// doc for why structural patches are folded into this same path instead
|
|
166
|
-
// of merging in place.
|
|
167
|
-
const needsFullReload = message.type === 'init' || isStructuralChange(message.flat.types);
|
|
168
|
-
if (needsFullReload) {
|
|
116
|
+
if (message.type === 'init') {
|
|
169
117
|
nu.load(t__namespace.unflatten(message.flat));
|
|
170
118
|
volatile.loadGeneration++;
|
|
171
119
|
}
|
|
172
120
|
else {
|
|
173
|
-
nu.change(() => t__namespace.merge(nu.state, t__namespace.unflatten(message.flat)));
|
|
121
|
+
nu.change(() => t__namespace.merge(nu.state, t__namespace.unflatten(message.flat), { arrayReconcile: 'id' }));
|
|
174
122
|
}
|
|
175
123
|
volatile.activeComponent = message.activeComponent;
|
|
176
124
|
};
|
|
@@ -239,13 +187,6 @@ class CommandRegistry {
|
|
|
239
187
|
}
|
|
240
188
|
}
|
|
241
189
|
|
|
242
|
-
/**
|
|
243
|
-
* Compute a drop position from the pointer's Y against a target's bounding rect.
|
|
244
|
-
* When `allowInside` is set, the middle band is `inside` (drop as a child) and
|
|
245
|
-
* the outer bands are `before`/`after`; otherwise it's a simple before/after
|
|
246
|
-
* split at the midpoint. Pure — use it from an `onDragOver`/`onPointerMove`
|
|
247
|
-
* handler to drive a drop indicator and the eventual `moveNode`/`pasteNode`.
|
|
248
|
-
*/
|
|
249
190
|
function dropPositionFromPointer(clientY, rect, opts = {}) {
|
|
250
191
|
const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
|
|
251
192
|
if (opts.allowInside) {
|
|
@@ -257,7 +198,6 @@ function dropPositionFromPointer(clientY, rect, opts = {}) {
|
|
|
257
198
|
}
|
|
258
199
|
return ratio < 0.5 ? 'before' : 'after';
|
|
259
200
|
}
|
|
260
|
-
/** A human label for a template node (tag name, component name, slot, …). */
|
|
261
201
|
function templateLabel(node) {
|
|
262
202
|
if (node instanceof t__namespace.TagTemplate) {
|
|
263
203
|
return node.tag;
|
|
@@ -273,15 +213,12 @@ function templateLabel(node) {
|
|
|
273
213
|
}
|
|
274
214
|
return 'node';
|
|
275
215
|
}
|
|
276
|
-
/** Child templates of a node (empty for leaves / text nodes). */
|
|
277
216
|
function templateChildren(node) {
|
|
278
217
|
return (node.children ?? []).filter((child) => child instanceof t__namespace.Type);
|
|
279
218
|
}
|
|
280
|
-
/** Is this a text node (`<text text=…>`), rendered as its string value? */
|
|
281
219
|
function isTextTemplate(node) {
|
|
282
220
|
return node instanceof t__namespace.TagTemplate && node.tag === 'text';
|
|
283
221
|
}
|
|
284
|
-
/** Preview the content of a text node, e.g. for a layers tree. */
|
|
285
222
|
function textPreview(node) {
|
|
286
223
|
if (!isTextTemplate(node)) {
|
|
287
224
|
return null;
|
|
@@ -292,7 +229,6 @@ function textPreview(node) {
|
|
|
292
229
|
}
|
|
293
230
|
return '{…}';
|
|
294
231
|
}
|
|
295
|
-
/** Can this node hold appended children? */
|
|
296
232
|
function canHaveChildren(node) {
|
|
297
233
|
if (!node || isTextTemplate(node)) {
|
|
298
234
|
return false;
|
|
@@ -301,7 +237,6 @@ function canHaveChildren(node) {
|
|
|
301
237
|
node instanceof t__namespace.ComponentTemplate ||
|
|
302
238
|
t__namespace.is(node, t__namespace.FragmentTemplate));
|
|
303
239
|
}
|
|
304
|
-
/** Is `ancestor` somewhere above `node` in the tree? */
|
|
305
240
|
function isAncestorOf(nu, ancestor, node) {
|
|
306
241
|
let parent = nu.getParentNode(node);
|
|
307
242
|
while (parent) {
|
|
@@ -313,11 +248,6 @@ function isAncestorOf(nu, ancestor, node) {
|
|
|
313
248
|
return false;
|
|
314
249
|
}
|
|
315
250
|
|
|
316
|
-
/**
|
|
317
|
-
* A high-level facade over a {@link Nu} runtime for building visual page
|
|
318
|
-
* editors. Every mutation goes through `nu.change`, so it is transactional and
|
|
319
|
-
* undoable. Reach for these instead of hand-splicing the AST.
|
|
320
|
-
*/
|
|
321
251
|
class Editor {
|
|
322
252
|
nu;
|
|
323
253
|
blocks;
|
|
@@ -340,8 +270,6 @@ class Editor {
|
|
|
340
270
|
run: (e) => e.nu.redo(),
|
|
341
271
|
});
|
|
342
272
|
}
|
|
343
|
-
// --- mutation -------------------------------------------------------------
|
|
344
|
-
/** Append (or insert at `index`) `child` into `parent`'s children. */
|
|
345
273
|
insertNode(parent, child, index) {
|
|
346
274
|
const kids = parent.children;
|
|
347
275
|
if (!Array.isArray(kids)) {
|
|
@@ -356,7 +284,6 @@ class Editor {
|
|
|
356
284
|
}
|
|
357
285
|
});
|
|
358
286
|
}
|
|
359
|
-
/** Remove a node from its parent. Returns false if it has no parent. */
|
|
360
287
|
removeNode(node) {
|
|
361
288
|
let ok = false;
|
|
362
289
|
this.nu.change(() => {
|
|
@@ -364,7 +291,6 @@ class Editor {
|
|
|
364
291
|
});
|
|
365
292
|
return ok;
|
|
366
293
|
}
|
|
367
|
-
/** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
|
|
368
294
|
moveNode(dragged, target, position) {
|
|
369
295
|
if (dragged === target || isAncestorOf(this.nu, dragged, target)) {
|
|
370
296
|
return false;
|
|
@@ -375,40 +301,26 @@ class Editor {
|
|
|
375
301
|
});
|
|
376
302
|
return ok;
|
|
377
303
|
}
|
|
378
|
-
// --- clipboard ------------------------------------------------------------
|
|
379
304
|
clipboardNode = null;
|
|
380
|
-
/** Put a node on the editor clipboard (a clone is made on paste). */
|
|
381
305
|
copyNode(node) {
|
|
382
306
|
this.clipboardNode = node;
|
|
383
307
|
}
|
|
384
|
-
/** Copy a node, then remove it. */
|
|
385
308
|
cutNode(node) {
|
|
386
309
|
this.copyNode(node);
|
|
387
310
|
return this.removeNode(node);
|
|
388
311
|
}
|
|
389
|
-
/** Whether there is something on the clipboard to paste. */
|
|
390
312
|
canPaste() {
|
|
391
313
|
return this.clipboardNode !== null;
|
|
392
314
|
}
|
|
393
|
-
/**
|
|
394
|
-
* Paste a fresh copy (new ids) of the clipboard node before/after/inside
|
|
395
|
-
* `target`. Returns the inserted node, or null if nothing was pasted.
|
|
396
|
-
*/
|
|
397
315
|
pasteNode(target, position = 'after') {
|
|
398
316
|
if (!this.clipboardNode) {
|
|
399
317
|
return null;
|
|
400
318
|
}
|
|
401
319
|
return this.insertCloneRelativeTo(this.clipboardNode, target, position);
|
|
402
320
|
}
|
|
403
|
-
/** Duplicate a node in place (inserted right after it). Returns the copy. */
|
|
404
321
|
duplicateNode(node) {
|
|
405
322
|
return this.insertCloneRelativeTo(node, node, 'after');
|
|
406
323
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Insert a detached node before/after/inside a target (undoable). The core of
|
|
409
|
-
* canvas drag-and-drop: a drop computes a target + position, then calls this.
|
|
410
|
-
* Returns the inserted node.
|
|
411
|
-
*/
|
|
412
324
|
insertNodeAt(node, target, position) {
|
|
413
325
|
let ok = false;
|
|
414
326
|
this.nu.change(() => {
|
|
@@ -416,7 +328,6 @@ class Editor {
|
|
|
416
328
|
});
|
|
417
329
|
return ok ? (this.nu.getNodeFromId(node.id) ?? null) : null;
|
|
418
330
|
}
|
|
419
|
-
/** Create a block and insert it before/after/inside a target. */
|
|
420
331
|
insertBlockAt(blockId, target, position) {
|
|
421
332
|
const block = this.blocks.get(blockId);
|
|
422
333
|
if (!block) {
|
|
@@ -430,10 +341,8 @@ class Editor {
|
|
|
430
341
|
this.nu.change(() => {
|
|
431
342
|
ok = this.insertRelativeTo(clone, target, position);
|
|
432
343
|
});
|
|
433
|
-
// Return the reactive node from the tree (clone is wrapped on insert).
|
|
434
344
|
return ok ? (this.nu.getNodeFromId(clone.id) ?? null) : null;
|
|
435
345
|
}
|
|
436
|
-
/** Set several props at once. */
|
|
437
346
|
updateProps(node, props) {
|
|
438
347
|
this.nu.change(() => {
|
|
439
348
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -451,7 +360,6 @@ class Editor {
|
|
|
451
360
|
delete node.props[key];
|
|
452
361
|
});
|
|
453
362
|
}
|
|
454
|
-
/** Parse `source` as a nuforge expression and assign it to `key`. */
|
|
455
363
|
setExpression(node, key, source) {
|
|
456
364
|
let expr;
|
|
457
365
|
try {
|
|
@@ -463,7 +371,6 @@ class Editor {
|
|
|
463
371
|
this.setProp(node, key, expr);
|
|
464
372
|
return true;
|
|
465
373
|
}
|
|
466
|
-
/** Update a text node's literal value. */
|
|
467
374
|
setText(node, value) {
|
|
468
375
|
const expr = node.props?.text;
|
|
469
376
|
if (!(expr instanceof t__namespace.Literal)) {
|
|
@@ -496,7 +403,6 @@ class Editor {
|
|
|
496
403
|
tag.classList.properties[clean] = t__namespace.literal({ value: true });
|
|
497
404
|
});
|
|
498
405
|
}
|
|
499
|
-
/** Parse `source` and set the condition under which class `name` applies. */
|
|
500
406
|
setClassCondition(node, name, source) {
|
|
501
407
|
let expr;
|
|
502
408
|
try {
|
|
@@ -525,7 +431,6 @@ class Editor {
|
|
|
525
431
|
}
|
|
526
432
|
});
|
|
527
433
|
}
|
|
528
|
-
/** Set the `@if` condition from source. */
|
|
529
434
|
setCondition(node, source) {
|
|
530
435
|
let expr;
|
|
531
436
|
try {
|
|
@@ -544,8 +449,6 @@ class Editor {
|
|
|
544
449
|
node.if = null;
|
|
545
450
|
});
|
|
546
451
|
}
|
|
547
|
-
// --- loop (@each) ---------------------------------------------------------
|
|
548
|
-
/** Repeat a node over `iterator` (a list expression), binding each `alias`. */
|
|
549
452
|
setEach(node, iterator, alias = 'item', indexName) {
|
|
550
453
|
let expr;
|
|
551
454
|
try {
|
|
@@ -568,7 +471,6 @@ class Editor {
|
|
|
568
471
|
node.each = null;
|
|
569
472
|
});
|
|
570
473
|
}
|
|
571
|
-
// --- component state / props ----------------------------------------------
|
|
572
474
|
addState(component, name = 'value', init = '0') {
|
|
573
475
|
const taken = new Set(component.state.map((v) => v.name));
|
|
574
476
|
let unique = name;
|
|
@@ -653,8 +555,6 @@ class Editor {
|
|
|
653
555
|
});
|
|
654
556
|
return true;
|
|
655
557
|
}
|
|
656
|
-
// --- blocks ---------------------------------------------------------------
|
|
657
|
-
/** Create a block's template and insert it into `parent`. */
|
|
658
558
|
insertBlock(blockId, parent, index) {
|
|
659
559
|
const block = this.blocks.get(blockId);
|
|
660
560
|
if (!block || !canHaveChildren(parent)) {
|
|
@@ -664,7 +564,6 @@ class Editor {
|
|
|
664
564
|
this.insertNode(parent, created, index);
|
|
665
565
|
return created;
|
|
666
566
|
}
|
|
667
|
-
// --- components / pages ---------------------------------------------------
|
|
668
567
|
uniqueName(base) {
|
|
669
568
|
const taken = new Set(this.getComponents().map((c) => c.name));
|
|
670
569
|
if (!taken.has(base)) {
|
|
@@ -676,7 +575,6 @@ class Editor {
|
|
|
676
575
|
}
|
|
677
576
|
return `${base}${i}`;
|
|
678
577
|
}
|
|
679
|
-
/** Add a new component (a "page"). */
|
|
680
578
|
addComponent(name = 'Page') {
|
|
681
579
|
const unique = this.uniqueName(name);
|
|
682
580
|
const component = t__namespace.nuComponent({
|
|
@@ -690,7 +588,6 @@ class Editor {
|
|
|
690
588
|
});
|
|
691
589
|
return this.getComponent(unique);
|
|
692
590
|
}
|
|
693
|
-
/** Rename a component, updating `<Name/>` references across the program. */
|
|
694
591
|
renameComponent(component, name) {
|
|
695
592
|
const clean = name.trim();
|
|
696
593
|
if (!clean ||
|
|
@@ -702,7 +599,9 @@ class Editor {
|
|
|
702
599
|
component.name = clean;
|
|
703
600
|
for (const c of this.getComponents()) {
|
|
704
601
|
this.walkTemplates(c.template, (tpl) => {
|
|
705
|
-
if (tpl instanceof t__namespace.ComponentTemplate &&
|
|
602
|
+
if (tpl instanceof t__namespace.ComponentTemplate &&
|
|
603
|
+
!tpl.component.external &&
|
|
604
|
+
tpl.component.name === old) {
|
|
706
605
|
tpl.component.name = clean;
|
|
707
606
|
}
|
|
708
607
|
});
|
|
@@ -740,7 +639,6 @@ class Editor {
|
|
|
740
639
|
this.walkTemplates(child, fn);
|
|
741
640
|
}
|
|
742
641
|
}
|
|
743
|
-
// --- query ----------------------------------------------------------------
|
|
744
642
|
getComponents() {
|
|
745
643
|
return this.nu.state.program.components.filter((c) => c instanceof t__namespace.NuComponent);
|
|
746
644
|
}
|
|
@@ -760,7 +658,6 @@ class Editor {
|
|
|
760
658
|
getNodeIndex(node) {
|
|
761
659
|
return this.getSiblings(node).indexOf(node);
|
|
762
660
|
}
|
|
763
|
-
/** Full path from the State root to this node (for selections/serialization). */
|
|
764
661
|
getNodePath(node) {
|
|
765
662
|
const out = [];
|
|
766
663
|
let current = node;
|
|
@@ -772,12 +669,9 @@ class Editor {
|
|
|
772
669
|
}
|
|
773
670
|
return out;
|
|
774
671
|
}
|
|
775
|
-
// --- change subscription --------------------------------------------------
|
|
776
|
-
/** Fires with the raw field-level writes of every change (incl. undo/redo). */
|
|
777
672
|
onChange(cb) {
|
|
778
673
|
return this.nu.listenToMutations(cb);
|
|
779
674
|
}
|
|
780
|
-
/** Fires only when the tree shape changes (a child array was mutated). */
|
|
781
675
|
onStructureChange(cb) {
|
|
782
676
|
return this.nu.listenToMutations((entries) => {
|
|
783
677
|
if (entries.some((e) => Array.isArray(e.target))) {
|
|
@@ -785,7 +679,6 @@ class Editor {
|
|
|
785
679
|
}
|
|
786
680
|
});
|
|
787
681
|
}
|
|
788
|
-
// --- internals (run inside nu.change) -------------------------------------
|
|
789
682
|
spliceFromParent(node) {
|
|
790
683
|
const parent = this.nu.getParentNode(node);
|
|
791
684
|
const kids = parent?.children;
|
|
@@ -833,7 +726,6 @@ class Editor {
|
|
|
833
726
|
destKids.splice(position === 'after' ? ti + 1 : ti, 0, dragged);
|
|
834
727
|
return true;
|
|
835
728
|
}
|
|
836
|
-
/** Insert an already-detached node relative to `target` (no removal). */
|
|
837
729
|
insertRelativeTo(node, target, position) {
|
|
838
730
|
if (position === 'inside') {
|
|
839
731
|
if (!canHaveChildren(target)) {
|
|
@@ -880,4 +772,3 @@ exports.templateChildren = templateChildren;
|
|
|
880
772
|
exports.templateLabel = templateLabel;
|
|
881
773
|
exports.text = text;
|
|
882
774
|
exports.textPreview = textPreview;
|
|
883
|
-
//# sourceMappingURL=index.cjs.map
|