@nuforge/editor 0.2.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 +79 -0
- package/dist/index.d.ts +101 -3
- package/dist/index.mjs +75 -1
- package/dist/react.cjs +215 -16
- package/dist/react.d.ts +41 -4
- package/dist/react.mjs +215 -17
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var t = require('@nuforge/types');
|
|
4
|
+
var core = require('@nuforge/core');
|
|
5
|
+
var reactivity = require('@nuforge/reactivity');
|
|
4
6
|
var parser = require('@nuforge/parser');
|
|
5
7
|
|
|
6
8
|
function _interopNamespaceDefault(e) {
|
|
@@ -93,6 +95,78 @@ const DEFAULT_BLOCKS = [
|
|
|
93
95
|
},
|
|
94
96
|
];
|
|
95
97
|
|
|
98
|
+
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
99
|
+
const CANVAS_PROTOCOL_VERSION = 1;
|
|
100
|
+
function canvasChannelName(src) {
|
|
101
|
+
return `${CANVAS_CHANNEL}:${src}`;
|
|
102
|
+
}
|
|
103
|
+
function isCanvasMessage(data) {
|
|
104
|
+
return (!!data &&
|
|
105
|
+
typeof data === 'object' &&
|
|
106
|
+
data.channel === CANVAS_CHANNEL &&
|
|
107
|
+
data.v === CANVAS_PROTOCOL_VERSION);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createCanvasReceiver(opts) {
|
|
111
|
+
const nu = core.Nu.create({ externals: opts.externals });
|
|
112
|
+
const volatile = reactivity.reactive({ activeComponent: null, loadGeneration: 0 });
|
|
113
|
+
const channel = new BroadcastChannel(canvasChannelName(opts.src));
|
|
114
|
+
const uiListeners = new Set();
|
|
115
|
+
const applyState = (message) => {
|
|
116
|
+
if (message.type === 'init') {
|
|
117
|
+
nu.load(t__namespace.unflatten(message.flat));
|
|
118
|
+
volatile.loadGeneration++;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
nu.change(() => t__namespace.merge(nu.state, t__namespace.unflatten(message.flat), { arrayReconcile: 'id' }));
|
|
122
|
+
}
|
|
123
|
+
volatile.activeComponent = message.activeComponent;
|
|
124
|
+
};
|
|
125
|
+
const onMessage = (event) => {
|
|
126
|
+
if (!isCanvasMessage(event.data)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const message = event.data;
|
|
130
|
+
if (message.type === 'init' || message.type === 'patch') {
|
|
131
|
+
applyState(message);
|
|
132
|
+
}
|
|
133
|
+
else if (message.type === 'ui') {
|
|
134
|
+
for (const cb of uiListeners) {
|
|
135
|
+
cb(message.payload);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
channel.addEventListener('message', onMessage);
|
|
140
|
+
return {
|
|
141
|
+
nu,
|
|
142
|
+
volatile,
|
|
143
|
+
postReady() {
|
|
144
|
+
channel.postMessage({
|
|
145
|
+
channel: CANVAS_CHANNEL,
|
|
146
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
147
|
+
type: 'ready',
|
|
148
|
+
mountGen: Math.floor(Math.random() * 1e9),
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
onUi(cb) {
|
|
152
|
+
uiListeners.add(cb);
|
|
153
|
+
return () => uiListeners.delete(cb);
|
|
154
|
+
},
|
|
155
|
+
postEvent(message) {
|
|
156
|
+
channel.postMessage({
|
|
157
|
+
channel: CANVAS_CHANNEL,
|
|
158
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
159
|
+
...message,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
dispose() {
|
|
163
|
+
channel.removeEventListener('message', onMessage);
|
|
164
|
+
channel.close();
|
|
165
|
+
uiListeners.clear();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
96
170
|
class CommandRegistry {
|
|
97
171
|
editor;
|
|
98
172
|
commands = new Map();
|
|
@@ -676,15 +750,20 @@ function createEditor(nu, opts) {
|
|
|
676
750
|
}
|
|
677
751
|
|
|
678
752
|
exports.BlockRegistry = BlockRegistry;
|
|
753
|
+
exports.CANVAS_CHANNEL = CANVAS_CHANNEL;
|
|
754
|
+
exports.CANVAS_PROTOCOL_VERSION = CANVAS_PROTOCOL_VERSION;
|
|
679
755
|
exports.CommandRegistry = CommandRegistry;
|
|
680
756
|
exports.DEFAULT_BLOCKS = DEFAULT_BLOCKS;
|
|
681
757
|
exports.Editor = Editor;
|
|
682
758
|
exports.canHaveChildren = canHaveChildren;
|
|
759
|
+
exports.canvasChannelName = canvasChannelName;
|
|
760
|
+
exports.createCanvasReceiver = createCanvasReceiver;
|
|
683
761
|
exports.createEditor = createEditor;
|
|
684
762
|
exports.defineBlock = defineBlock;
|
|
685
763
|
exports.dropPositionFromPointer = dropPositionFromPointer;
|
|
686
764
|
exports.el = el;
|
|
687
765
|
exports.isAncestorOf = isAncestorOf;
|
|
766
|
+
exports.isCanvasMessage = isCanvasMessage;
|
|
688
767
|
exports.isTextTemplate = isTextTemplate;
|
|
689
768
|
exports.lit = lit;
|
|
690
769
|
exports.templateChildren = templateChildren;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from '@nuforge/types';
|
|
2
|
-
import { Nu } from '@nuforge/core';
|
|
2
|
+
import { Nu, NuExternalsFactory } from '@nuforge/core';
|
|
3
3
|
import { MutationEntry } from '@nuforge/reactivity';
|
|
4
4
|
|
|
5
5
|
interface PropSchemaField {
|
|
@@ -46,6 +46,104 @@ declare function textPreview(node: t.Template): string | null;
|
|
|
46
46
|
declare function canHaveChildren(node: t.Template | null): boolean;
|
|
47
47
|
declare function isAncestorOf(nu: Nu, ancestor: t.Template, node: t.Template): boolean;
|
|
48
48
|
|
|
49
|
+
declare const CANVAS_CHANNEL = "nuforge-canvas";
|
|
50
|
+
declare const CANVAS_PROTOCOL_VERSION = 1;
|
|
51
|
+
declare function canvasChannelName(src: string): string;
|
|
52
|
+
interface FlattenedState {
|
|
53
|
+
root: {
|
|
54
|
+
$$typeId: string;
|
|
55
|
+
};
|
|
56
|
+
types: Record<string, {
|
|
57
|
+
type: string;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
interface CanvasInitMessage {
|
|
62
|
+
channel: typeof CANVAS_CHANNEL;
|
|
63
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
64
|
+
type: 'init';
|
|
65
|
+
activeComponent: string;
|
|
66
|
+
flat: FlattenedState;
|
|
67
|
+
}
|
|
68
|
+
interface CanvasPatchMessage {
|
|
69
|
+
channel: typeof CANVAS_CHANNEL;
|
|
70
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
71
|
+
type: 'patch';
|
|
72
|
+
activeComponent: string;
|
|
73
|
+
flat: FlattenedState;
|
|
74
|
+
rev: number;
|
|
75
|
+
}
|
|
76
|
+
interface CanvasUiMessage {
|
|
77
|
+
channel: typeof CANVAS_CHANNEL;
|
|
78
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
79
|
+
type: 'ui';
|
|
80
|
+
payload: unknown;
|
|
81
|
+
}
|
|
82
|
+
type CanvasHostMessage = CanvasInitMessage | CanvasPatchMessage | CanvasUiMessage;
|
|
83
|
+
interface CanvasReadyMessage {
|
|
84
|
+
channel: typeof CANVAS_CHANNEL;
|
|
85
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
86
|
+
type: 'ready';
|
|
87
|
+
mountGen: number;
|
|
88
|
+
}
|
|
89
|
+
interface CanvasSelectMessage {
|
|
90
|
+
channel: typeof CANVAS_CHANNEL;
|
|
91
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
92
|
+
type: 'select';
|
|
93
|
+
nodeId: string | null;
|
|
94
|
+
}
|
|
95
|
+
interface CanvasHoverMessage {
|
|
96
|
+
channel: typeof CANVAS_CHANNEL;
|
|
97
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
98
|
+
type: 'hover';
|
|
99
|
+
nodeId: string | null;
|
|
100
|
+
}
|
|
101
|
+
interface CanvasDropMessage {
|
|
102
|
+
channel: typeof CANVAS_CHANNEL;
|
|
103
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
104
|
+
type: 'drop';
|
|
105
|
+
targetId: string;
|
|
106
|
+
position: DropPosition;
|
|
107
|
+
}
|
|
108
|
+
interface CanvasKeydownMessage {
|
|
109
|
+
channel: typeof CANVAS_CHANNEL;
|
|
110
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
111
|
+
type: 'keydown';
|
|
112
|
+
key: string;
|
|
113
|
+
code: string;
|
|
114
|
+
ctrlKey: boolean;
|
|
115
|
+
metaKey: boolean;
|
|
116
|
+
shiftKey: boolean;
|
|
117
|
+
altKey: boolean;
|
|
118
|
+
}
|
|
119
|
+
interface CanvasLinkClickMessage {
|
|
120
|
+
channel: typeof CANVAS_CHANNEL;
|
|
121
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
122
|
+
type: 'link-click';
|
|
123
|
+
href: string;
|
|
124
|
+
}
|
|
125
|
+
type CanvasClientMessage = CanvasReadyMessage | CanvasSelectMessage | CanvasHoverMessage | CanvasDropMessage | CanvasKeydownMessage | CanvasLinkClickMessage;
|
|
126
|
+
type CanvasMessage = CanvasHostMessage | CanvasClientMessage;
|
|
127
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
128
|
+
declare function isCanvasMessage(data: unknown): data is CanvasMessage;
|
|
129
|
+
|
|
130
|
+
interface CanvasReceiverOpts {
|
|
131
|
+
src: string;
|
|
132
|
+
externals?: NuExternalsFactory;
|
|
133
|
+
}
|
|
134
|
+
interface CanvasReceiver {
|
|
135
|
+
nu: Nu;
|
|
136
|
+
volatile: {
|
|
137
|
+
activeComponent: string | null;
|
|
138
|
+
loadGeneration: number;
|
|
139
|
+
};
|
|
140
|
+
postReady(): void;
|
|
141
|
+
onUi(cb: (payload: unknown) => void): () => void;
|
|
142
|
+
postEvent(message: DistributiveOmit<CanvasClientMessage, 'channel' | 'v'>): void;
|
|
143
|
+
dispose(): void;
|
|
144
|
+
}
|
|
145
|
+
declare function createCanvasReceiver(opts: CanvasReceiverOpts): CanvasReceiver;
|
|
146
|
+
|
|
49
147
|
interface CreateEditorOptions {
|
|
50
148
|
blocks?: Block[];
|
|
51
149
|
defaultBlocks?: boolean;
|
|
@@ -124,5 +222,5 @@ declare class CommandRegistry {
|
|
|
124
222
|
run(id: string): void;
|
|
125
223
|
}
|
|
126
224
|
|
|
127
|
-
export { BlockRegistry, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
|
|
128
|
-
export type { Block, Command, CreateEditorOptions, DropPosition, PropSchemaField };
|
|
225
|
+
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 };
|
|
226
|
+
export type { Block, CanvasClientMessage, CanvasDropMessage, CanvasHostMessage, CanvasHoverMessage, CanvasInitMessage, CanvasKeydownMessage, CanvasLinkClickMessage, CanvasMessage, CanvasPatchMessage, CanvasReadyMessage, CanvasReceiver, CanvasReceiverOpts, CanvasSelectMessage, CanvasUiMessage, Command, CreateEditorOptions, DistributiveOmit, DropPosition, FlattenedState, PropSchemaField };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as t from '@nuforge/types';
|
|
2
|
+
import { Nu } from '@nuforge/core';
|
|
3
|
+
import { reactive } from '@nuforge/reactivity';
|
|
2
4
|
import { Parser } from '@nuforge/parser';
|
|
3
5
|
|
|
4
6
|
function lit(value) {
|
|
@@ -72,6 +74,78 @@ const DEFAULT_BLOCKS = [
|
|
|
72
74
|
},
|
|
73
75
|
];
|
|
74
76
|
|
|
77
|
+
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
78
|
+
const CANVAS_PROTOCOL_VERSION = 1;
|
|
79
|
+
function canvasChannelName(src) {
|
|
80
|
+
return `${CANVAS_CHANNEL}:${src}`;
|
|
81
|
+
}
|
|
82
|
+
function isCanvasMessage(data) {
|
|
83
|
+
return (!!data &&
|
|
84
|
+
typeof data === 'object' &&
|
|
85
|
+
data.channel === CANVAS_CHANNEL &&
|
|
86
|
+
data.v === CANVAS_PROTOCOL_VERSION);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createCanvasReceiver(opts) {
|
|
90
|
+
const nu = Nu.create({ externals: opts.externals });
|
|
91
|
+
const volatile = reactive({ activeComponent: null, loadGeneration: 0 });
|
|
92
|
+
const channel = new BroadcastChannel(canvasChannelName(opts.src));
|
|
93
|
+
const uiListeners = new Set();
|
|
94
|
+
const applyState = (message) => {
|
|
95
|
+
if (message.type === 'init') {
|
|
96
|
+
nu.load(t.unflatten(message.flat));
|
|
97
|
+
volatile.loadGeneration++;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
nu.change(() => t.merge(nu.state, t.unflatten(message.flat), { arrayReconcile: 'id' }));
|
|
101
|
+
}
|
|
102
|
+
volatile.activeComponent = message.activeComponent;
|
|
103
|
+
};
|
|
104
|
+
const onMessage = (event) => {
|
|
105
|
+
if (!isCanvasMessage(event.data)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const message = event.data;
|
|
109
|
+
if (message.type === 'init' || message.type === 'patch') {
|
|
110
|
+
applyState(message);
|
|
111
|
+
}
|
|
112
|
+
else if (message.type === 'ui') {
|
|
113
|
+
for (const cb of uiListeners) {
|
|
114
|
+
cb(message.payload);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
channel.addEventListener('message', onMessage);
|
|
119
|
+
return {
|
|
120
|
+
nu,
|
|
121
|
+
volatile,
|
|
122
|
+
postReady() {
|
|
123
|
+
channel.postMessage({
|
|
124
|
+
channel: CANVAS_CHANNEL,
|
|
125
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
126
|
+
type: 'ready',
|
|
127
|
+
mountGen: Math.floor(Math.random() * 1e9),
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
onUi(cb) {
|
|
131
|
+
uiListeners.add(cb);
|
|
132
|
+
return () => uiListeners.delete(cb);
|
|
133
|
+
},
|
|
134
|
+
postEvent(message) {
|
|
135
|
+
channel.postMessage({
|
|
136
|
+
channel: CANVAS_CHANNEL,
|
|
137
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
138
|
+
...message,
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
dispose() {
|
|
142
|
+
channel.removeEventListener('message', onMessage);
|
|
143
|
+
channel.close();
|
|
144
|
+
uiListeners.clear();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
75
149
|
class CommandRegistry {
|
|
76
150
|
editor;
|
|
77
151
|
commands = new Map();
|
|
@@ -654,4 +728,4 @@ function createEditor(nu, opts) {
|
|
|
654
728
|
return new Editor(nu, opts);
|
|
655
729
|
}
|
|
656
730
|
|
|
657
|
-
export { BlockRegistry, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
|
|
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 };
|
package/dist/react.cjs
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var react$1 = require('@nuforge/react');
|
|
5
|
+
var t = require('@nuforge/types');
|
|
5
6
|
var react = require('react');
|
|
6
7
|
var reactDom = require('react-dom');
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
10
|
+
const CANVAS_PROTOCOL_VERSION = 1;
|
|
11
|
+
function canvasChannelName(src) {
|
|
12
|
+
return `${CANVAS_CHANNEL}:${src}`;
|
|
13
|
+
}
|
|
14
|
+
function isCanvasMessage(data) {
|
|
15
|
+
return (!!data &&
|
|
16
|
+
typeof data === 'object' &&
|
|
17
|
+
data.channel === CANVAS_CHANNEL &&
|
|
18
|
+
data.v === CANVAS_PROTOCOL_VERSION);
|
|
19
|
+
}
|
|
8
20
|
|
|
9
21
|
function dropPositionFromPointer(clientY, rect, opts = {}) {
|
|
10
22
|
const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
|
|
@@ -174,24 +186,14 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
174
186
|
setDrop(null);
|
|
175
187
|
}
|
|
176
188
|
}, [previewMode]);
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
const el = doc.querySelector(`[data-nu-id="${id}"]`);
|
|
189
|
+
const elementFor = (id) => doc && id ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
|
|
190
|
+
const rectOfEl = (el) => {
|
|
182
191
|
if (!el) {
|
|
183
192
|
return null;
|
|
184
193
|
}
|
|
185
194
|
const r = el.getBoundingClientRect();
|
|
186
195
|
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
187
196
|
};
|
|
188
|
-
const labelOf = (id) => {
|
|
189
|
-
if (!doc || !id) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
const el = doc.querySelector(`[data-nu-id="${id}"]`);
|
|
193
|
-
return el ? el.tagName.toLowerCase() : null;
|
|
194
|
-
};
|
|
195
197
|
react.useLayoutEffect(() => {
|
|
196
198
|
if (previewMode) {
|
|
197
199
|
setSelected(null);
|
|
@@ -199,9 +201,10 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
199
201
|
setSelectedLabel(null);
|
|
200
202
|
return;
|
|
201
203
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
const selectedEl = elementFor(selectedId);
|
|
205
|
+
setSelected(rectOfEl(selectedEl));
|
|
206
|
+
setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
|
|
207
|
+
setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
|
|
205
208
|
}, [previewMode, selectedId, hoveredId, revision, tick, doc]);
|
|
206
209
|
return react.createElement('div', { className: containerClassName, style: { position: 'relative' } }, react.createElement('iframe', { ref: iframeRef, title: 'nuforge preview', className, srcDoc }, doc?.body
|
|
207
210
|
? reactDom.createPortal(react.createElement(CanvasWindowContext.Provider, { value: { doc, window: doc.defaultView } }, react.createElement(EditorFrame, { frame })), doc.body)
|
|
@@ -216,6 +219,201 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
216
219
|
}, renderBox({ selected, hovered, selectedLabel, drop }))
|
|
217
220
|
: null);
|
|
218
221
|
}
|
|
222
|
+
function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hoveredId = null, revision = 0, previewMode = false, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }) {
|
|
223
|
+
const iframeRef = react.useRef(null);
|
|
224
|
+
const [doc, setDoc] = react.useState(null);
|
|
225
|
+
const [tick, setTick] = react.useState(0);
|
|
226
|
+
const [selected, setSelected] = react.useState(null);
|
|
227
|
+
const [hovered, setHovered] = react.useState(null);
|
|
228
|
+
const [selectedLabel, setSelectedLabel] = react.useState(null);
|
|
229
|
+
const props = react.useRef({
|
|
230
|
+
nu,
|
|
231
|
+
activeComponent,
|
|
232
|
+
uiPayload,
|
|
233
|
+
onSelect,
|
|
234
|
+
onHover,
|
|
235
|
+
onDropOnNode,
|
|
236
|
+
onKeydownRelay,
|
|
237
|
+
onLinkClick,
|
|
238
|
+
});
|
|
239
|
+
props.current = {
|
|
240
|
+
nu,
|
|
241
|
+
activeComponent,
|
|
242
|
+
uiPayload,
|
|
243
|
+
onSelect,
|
|
244
|
+
onHover,
|
|
245
|
+
onDropOnNode,
|
|
246
|
+
onKeydownRelay,
|
|
247
|
+
onLinkClick,
|
|
248
|
+
};
|
|
249
|
+
const connectionRef = react.useRef(null);
|
|
250
|
+
react.useEffect(() => {
|
|
251
|
+
const iframe = iframeRef.current;
|
|
252
|
+
if (!iframe) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const onLoad = () => setDoc(iframe.contentDocument);
|
|
256
|
+
iframe.addEventListener('load', onLoad);
|
|
257
|
+
return () => iframe.removeEventListener('load', onLoad);
|
|
258
|
+
}, [src]);
|
|
259
|
+
react.useEffect(() => {
|
|
260
|
+
if (!doc) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const onScroll = () => setTick((x) => x + 1);
|
|
264
|
+
doc.addEventListener('scroll', onScroll, true);
|
|
265
|
+
return () => doc.removeEventListener('scroll', onScroll, true);
|
|
266
|
+
}, [doc]);
|
|
267
|
+
react.useEffect(() => {
|
|
268
|
+
const iframe = iframeRef.current;
|
|
269
|
+
if (!iframe) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const observer = new ResizeObserver(() => setTick((x) => x + 1));
|
|
273
|
+
observer.observe(iframe);
|
|
274
|
+
if (doc?.documentElement) {
|
|
275
|
+
observer.observe(doc.documentElement);
|
|
276
|
+
}
|
|
277
|
+
return () => observer.disconnect();
|
|
278
|
+
}, [doc]);
|
|
279
|
+
react.useEffect(() => {
|
|
280
|
+
const channel = new BroadcastChannel(canvasChannelName(src));
|
|
281
|
+
let ready = false;
|
|
282
|
+
let mountGen = null;
|
|
283
|
+
let rafId = null;
|
|
284
|
+
let rev = 0;
|
|
285
|
+
const sendInit = () => {
|
|
286
|
+
channel.postMessage({
|
|
287
|
+
channel: CANVAS_CHANNEL,
|
|
288
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
289
|
+
type: 'init',
|
|
290
|
+
activeComponent: props.current.activeComponent,
|
|
291
|
+
flat: t.flatten(props.current.nu.state),
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
const requestPatch = () => {
|
|
295
|
+
if (!ready || rafId !== null) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
rafId = requestAnimationFrame(() => {
|
|
299
|
+
rafId = null;
|
|
300
|
+
channel.postMessage({
|
|
301
|
+
channel: CANVAS_CHANNEL,
|
|
302
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
303
|
+
type: 'patch',
|
|
304
|
+
activeComponent: props.current.activeComponent,
|
|
305
|
+
flat: t.flatten(props.current.nu.state),
|
|
306
|
+
rev: ++rev,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const postUi = (payload) => {
|
|
311
|
+
channel.postMessage({
|
|
312
|
+
channel: CANVAS_CHANNEL,
|
|
313
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
314
|
+
type: 'ui',
|
|
315
|
+
payload,
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
connectionRef.current = { sendInit, requestPatch, postUi };
|
|
319
|
+
const onMessage = (event) => {
|
|
320
|
+
if (!isCanvasMessage(event.data)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const message = event.data;
|
|
324
|
+
const p = props.current;
|
|
325
|
+
switch (message.type) {
|
|
326
|
+
case 'ready':
|
|
327
|
+
if (mountGen !== null && message.mountGen <= mountGen) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
mountGen = message.mountGen;
|
|
331
|
+
ready = true;
|
|
332
|
+
sendInit();
|
|
333
|
+
postUi(props.current.uiPayload);
|
|
334
|
+
break;
|
|
335
|
+
case 'select':
|
|
336
|
+
p.onSelect?.(message.nodeId);
|
|
337
|
+
setTick((x) => x + 1);
|
|
338
|
+
break;
|
|
339
|
+
case 'hover':
|
|
340
|
+
p.onHover?.(message.nodeId);
|
|
341
|
+
setTick((x) => x + 1);
|
|
342
|
+
break;
|
|
343
|
+
case 'drop':
|
|
344
|
+
p.onDropOnNode?.(message.targetId, message.position);
|
|
345
|
+
break;
|
|
346
|
+
case 'keydown':
|
|
347
|
+
p.onKeydownRelay?.(message);
|
|
348
|
+
break;
|
|
349
|
+
case 'link-click':
|
|
350
|
+
p.onLinkClick?.(message.href);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
channel.addEventListener('message', onMessage);
|
|
355
|
+
const unlistenChangeset = nu.listenToChangeset(() => {
|
|
356
|
+
requestPatch();
|
|
357
|
+
setTick((x) => x + 1);
|
|
358
|
+
});
|
|
359
|
+
return () => {
|
|
360
|
+
channel.removeEventListener('message', onMessage);
|
|
361
|
+
channel.close();
|
|
362
|
+
unlistenChangeset();
|
|
363
|
+
if (rafId !== null) {
|
|
364
|
+
cancelAnimationFrame(rafId);
|
|
365
|
+
}
|
|
366
|
+
connectionRef.current = null;
|
|
367
|
+
};
|
|
368
|
+
}, [src, nu]);
|
|
369
|
+
const resyncKey = `${activeComponent}:${previewMode}`;
|
|
370
|
+
const lastResyncKey = react.useRef(null);
|
|
371
|
+
react.useEffect(() => {
|
|
372
|
+
if (lastResyncKey.current === resyncKey) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
lastResyncKey.current = resyncKey;
|
|
376
|
+
connectionRef.current?.sendInit();
|
|
377
|
+
}, [resyncKey]);
|
|
378
|
+
react.useEffect(() => {
|
|
379
|
+
connectionRef.current?.postUi(uiPayload);
|
|
380
|
+
}, [uiPayload]);
|
|
381
|
+
const elementFor = (id) => doc && id ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
|
|
382
|
+
const rectOfEl = (el) => {
|
|
383
|
+
if (!el) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const r = el.getBoundingClientRect();
|
|
387
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
388
|
+
};
|
|
389
|
+
react.useLayoutEffect(() => {
|
|
390
|
+
if (previewMode) {
|
|
391
|
+
setSelected(null);
|
|
392
|
+
setHovered(null);
|
|
393
|
+
setSelectedLabel(null);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const selectedEl = elementFor(selectedId);
|
|
397
|
+
setSelected(rectOfEl(selectedEl));
|
|
398
|
+
setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
|
|
399
|
+
setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
|
|
400
|
+
}, [previewMode, selectedId, hoveredId, revision, tick, doc]);
|
|
401
|
+
return react.createElement('div', { className: containerClassName, style: { position: 'relative' } }, react.createElement('iframe', {
|
|
402
|
+
ref: iframeRef,
|
|
403
|
+
title: 'nuforge canvas',
|
|
404
|
+
className,
|
|
405
|
+
src,
|
|
406
|
+
}), renderBox && !previewMode
|
|
407
|
+
? react.createElement('div', {
|
|
408
|
+
style: {
|
|
409
|
+
position: 'absolute',
|
|
410
|
+
inset: 0,
|
|
411
|
+
pointerEvents: 'none',
|
|
412
|
+
overflow: 'hidden',
|
|
413
|
+
},
|
|
414
|
+
}, renderBox({ selected, hovered, selectedLabel }))
|
|
415
|
+
: null);
|
|
416
|
+
}
|
|
219
417
|
function useStructureRevision(editor) {
|
|
220
418
|
const [rev, setRev] = react.useState(0);
|
|
221
419
|
react.useEffect(() => editor.onStructureChange(() => setRev((r) => r + 1)), [editor]);
|
|
@@ -278,6 +476,7 @@ exports.DEFAULT_CANVAS_CSS = DEFAULT_CANVAS_CSS;
|
|
|
278
476
|
exports.EditorFrame = EditorFrame;
|
|
279
477
|
exports.IframeCanvas = IframeCanvas;
|
|
280
478
|
exports.PropFields = PropFields;
|
|
479
|
+
exports.RemoteCanvasHost = RemoteCanvasHost;
|
|
281
480
|
exports.templateIdFromElement = templateIdFromElement;
|
|
282
481
|
exports.useCanvasWindow = useCanvasWindow;
|
|
283
482
|
exports.useEditorRevision = useEditorRevision;
|
package/dist/react.d.ts
CHANGED
|
@@ -27,6 +27,22 @@ declare class BlockRegistry {
|
|
|
27
27
|
byCategory(): Record<string, Block[]>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
type DropPosition = 'before' | 'after' | 'inside';
|
|
31
|
+
|
|
32
|
+
declare const CANVAS_CHANNEL = "nuforge-canvas";
|
|
33
|
+
declare const CANVAS_PROTOCOL_VERSION = 1;
|
|
34
|
+
interface CanvasKeydownMessage {
|
|
35
|
+
channel: typeof CANVAS_CHANNEL;
|
|
36
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
37
|
+
type: 'keydown';
|
|
38
|
+
key: string;
|
|
39
|
+
code: string;
|
|
40
|
+
ctrlKey: boolean;
|
|
41
|
+
metaKey: boolean;
|
|
42
|
+
shiftKey: boolean;
|
|
43
|
+
altKey: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
interface Command {
|
|
31
47
|
id: string;
|
|
32
48
|
label: string;
|
|
@@ -43,8 +59,6 @@ declare class CommandRegistry {
|
|
|
43
59
|
run(id: string): void;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
type DropPosition = 'before' | 'after' | 'inside';
|
|
47
|
-
|
|
48
62
|
interface CreateEditorOptions {
|
|
49
63
|
blocks?: Block[];
|
|
50
64
|
defaultBlocks?: boolean;
|
|
@@ -146,6 +160,29 @@ interface IframeCanvasProps {
|
|
|
146
160
|
}) => ReactNode;
|
|
147
161
|
}
|
|
148
162
|
declare function IframeCanvas({ frame, selectedId, hoveredId, revision, previewMode, onSelect, onHover, onDropOnNode, baseCss, srcDoc, className, containerClassName, renderBox, }: IframeCanvasProps): ReactNode;
|
|
163
|
+
interface RemoteCanvasHostProps {
|
|
164
|
+
nu: Nu;
|
|
165
|
+
activeComponent: string;
|
|
166
|
+
src: string;
|
|
167
|
+
selectedId?: string | null;
|
|
168
|
+
hoveredId?: string | null;
|
|
169
|
+
revision?: number;
|
|
170
|
+
previewMode?: boolean;
|
|
171
|
+
uiPayload?: unknown;
|
|
172
|
+
onSelect?: (id: string | null) => void;
|
|
173
|
+
onHover?: (id: string | null) => void;
|
|
174
|
+
onDropOnNode?: (targetId: string, position: DropPosition) => void;
|
|
175
|
+
onKeydownRelay?: (e: Omit<CanvasKeydownMessage, 'channel' | 'v' | 'type'>) => void;
|
|
176
|
+
onLinkClick?: (href: string) => void;
|
|
177
|
+
className?: string;
|
|
178
|
+
containerClassName?: string;
|
|
179
|
+
renderBox?: (rects: {
|
|
180
|
+
selected: Rect | null;
|
|
181
|
+
hovered: Rect | null;
|
|
182
|
+
selectedLabel: string | null;
|
|
183
|
+
}) => ReactNode;
|
|
184
|
+
}
|
|
185
|
+
declare function RemoteCanvasHost({ nu, activeComponent, src, selectedId, hoveredId, revision, previewMode, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }: RemoteCanvasHostProps): ReactNode;
|
|
149
186
|
declare function useStructureRevision(editor: Pick<Editor, 'onStructureChange'>): number;
|
|
150
187
|
declare function useEditorRevision(editor: Pick<Editor, 'onChange'>): number;
|
|
151
188
|
type PropFieldValue = string | number | boolean | undefined;
|
|
@@ -157,5 +194,5 @@ interface PropFieldsProps {
|
|
|
157
194
|
}
|
|
158
195
|
declare function PropFields({ fields, values, onChange, className, }: PropFieldsProps): ReactNode;
|
|
159
196
|
|
|
160
|
-
export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, templateIdFromElement, useCanvasWindow, useEditorRevision, useStructureRevision };
|
|
161
|
-
export type { CanvasWindow, IframeCanvasProps, PropFieldValue, PropFieldsProps, Rect };
|
|
197
|
+
export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, RemoteCanvasHost, templateIdFromElement, useCanvasWindow, useEditorRevision, useStructureRevision };
|
|
198
|
+
export type { CanvasWindow, IframeCanvasProps, PropFieldValue, PropFieldsProps, Rect, RemoteCanvasHostProps };
|
package/dist/react.mjs
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { createViewElement, observer } from '@nuforge/react';
|
|
3
|
+
import { flatten } from '@nuforge/types';
|
|
3
4
|
import { createContext, memo, createElement, Fragment, useRef, useState, useEffect, useLayoutEffect, useContext } from 'react';
|
|
4
5
|
import { createPortal } from 'react-dom';
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
8
|
+
const CANVAS_PROTOCOL_VERSION = 1;
|
|
9
|
+
function canvasChannelName(src) {
|
|
10
|
+
return `${CANVAS_CHANNEL}:${src}`;
|
|
11
|
+
}
|
|
12
|
+
function isCanvasMessage(data) {
|
|
13
|
+
return (!!data &&
|
|
14
|
+
typeof data === 'object' &&
|
|
15
|
+
data.channel === CANVAS_CHANNEL &&
|
|
16
|
+
data.v === CANVAS_PROTOCOL_VERSION);
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
function dropPositionFromPointer(clientY, rect, opts = {}) {
|
|
8
20
|
const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
|
|
@@ -172,24 +184,14 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
172
184
|
setDrop(null);
|
|
173
185
|
}
|
|
174
186
|
}, [previewMode]);
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
const el = doc.querySelector(`[data-nu-id="${id}"]`);
|
|
187
|
+
const elementFor = (id) => doc && id ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
|
|
188
|
+
const rectOfEl = (el) => {
|
|
180
189
|
if (!el) {
|
|
181
190
|
return null;
|
|
182
191
|
}
|
|
183
192
|
const r = el.getBoundingClientRect();
|
|
184
193
|
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
185
194
|
};
|
|
186
|
-
const labelOf = (id) => {
|
|
187
|
-
if (!doc || !id) {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
const el = doc.querySelector(`[data-nu-id="${id}"]`);
|
|
191
|
-
return el ? el.tagName.toLowerCase() : null;
|
|
192
|
-
};
|
|
193
195
|
useLayoutEffect(() => {
|
|
194
196
|
if (previewMode) {
|
|
195
197
|
setSelected(null);
|
|
@@ -197,9 +199,10 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
197
199
|
setSelectedLabel(null);
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
const selectedEl = elementFor(selectedId);
|
|
203
|
+
setSelected(rectOfEl(selectedEl));
|
|
204
|
+
setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
|
|
205
|
+
setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
|
|
203
206
|
}, [previewMode, selectedId, hoveredId, revision, tick, doc]);
|
|
204
207
|
return createElement('div', { className: containerClassName, style: { position: 'relative' } }, createElement('iframe', { ref: iframeRef, title: 'nuforge preview', className, srcDoc }, doc?.body
|
|
205
208
|
? createPortal(createElement(CanvasWindowContext.Provider, { value: { doc, window: doc.defaultView } }, createElement(EditorFrame, { frame })), doc.body)
|
|
@@ -214,6 +217,201 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
214
217
|
}, renderBox({ selected, hovered, selectedLabel, drop }))
|
|
215
218
|
: null);
|
|
216
219
|
}
|
|
220
|
+
function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hoveredId = null, revision = 0, previewMode = false, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }) {
|
|
221
|
+
const iframeRef = useRef(null);
|
|
222
|
+
const [doc, setDoc] = useState(null);
|
|
223
|
+
const [tick, setTick] = useState(0);
|
|
224
|
+
const [selected, setSelected] = useState(null);
|
|
225
|
+
const [hovered, setHovered] = useState(null);
|
|
226
|
+
const [selectedLabel, setSelectedLabel] = useState(null);
|
|
227
|
+
const props = useRef({
|
|
228
|
+
nu,
|
|
229
|
+
activeComponent,
|
|
230
|
+
uiPayload,
|
|
231
|
+
onSelect,
|
|
232
|
+
onHover,
|
|
233
|
+
onDropOnNode,
|
|
234
|
+
onKeydownRelay,
|
|
235
|
+
onLinkClick,
|
|
236
|
+
});
|
|
237
|
+
props.current = {
|
|
238
|
+
nu,
|
|
239
|
+
activeComponent,
|
|
240
|
+
uiPayload,
|
|
241
|
+
onSelect,
|
|
242
|
+
onHover,
|
|
243
|
+
onDropOnNode,
|
|
244
|
+
onKeydownRelay,
|
|
245
|
+
onLinkClick,
|
|
246
|
+
};
|
|
247
|
+
const connectionRef = useRef(null);
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const iframe = iframeRef.current;
|
|
250
|
+
if (!iframe) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const onLoad = () => setDoc(iframe.contentDocument);
|
|
254
|
+
iframe.addEventListener('load', onLoad);
|
|
255
|
+
return () => iframe.removeEventListener('load', onLoad);
|
|
256
|
+
}, [src]);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (!doc) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const onScroll = () => setTick((x) => x + 1);
|
|
262
|
+
doc.addEventListener('scroll', onScroll, true);
|
|
263
|
+
return () => doc.removeEventListener('scroll', onScroll, true);
|
|
264
|
+
}, [doc]);
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const iframe = iframeRef.current;
|
|
267
|
+
if (!iframe) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const observer = new ResizeObserver(() => setTick((x) => x + 1));
|
|
271
|
+
observer.observe(iframe);
|
|
272
|
+
if (doc?.documentElement) {
|
|
273
|
+
observer.observe(doc.documentElement);
|
|
274
|
+
}
|
|
275
|
+
return () => observer.disconnect();
|
|
276
|
+
}, [doc]);
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const channel = new BroadcastChannel(canvasChannelName(src));
|
|
279
|
+
let ready = false;
|
|
280
|
+
let mountGen = null;
|
|
281
|
+
let rafId = null;
|
|
282
|
+
let rev = 0;
|
|
283
|
+
const sendInit = () => {
|
|
284
|
+
channel.postMessage({
|
|
285
|
+
channel: CANVAS_CHANNEL,
|
|
286
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
287
|
+
type: 'init',
|
|
288
|
+
activeComponent: props.current.activeComponent,
|
|
289
|
+
flat: flatten(props.current.nu.state),
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
const requestPatch = () => {
|
|
293
|
+
if (!ready || rafId !== null) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
rafId = requestAnimationFrame(() => {
|
|
297
|
+
rafId = null;
|
|
298
|
+
channel.postMessage({
|
|
299
|
+
channel: CANVAS_CHANNEL,
|
|
300
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
301
|
+
type: 'patch',
|
|
302
|
+
activeComponent: props.current.activeComponent,
|
|
303
|
+
flat: flatten(props.current.nu.state),
|
|
304
|
+
rev: ++rev,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
const postUi = (payload) => {
|
|
309
|
+
channel.postMessage({
|
|
310
|
+
channel: CANVAS_CHANNEL,
|
|
311
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
312
|
+
type: 'ui',
|
|
313
|
+
payload,
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
connectionRef.current = { sendInit, requestPatch, postUi };
|
|
317
|
+
const onMessage = (event) => {
|
|
318
|
+
if (!isCanvasMessage(event.data)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const message = event.data;
|
|
322
|
+
const p = props.current;
|
|
323
|
+
switch (message.type) {
|
|
324
|
+
case 'ready':
|
|
325
|
+
if (mountGen !== null && message.mountGen <= mountGen) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
mountGen = message.mountGen;
|
|
329
|
+
ready = true;
|
|
330
|
+
sendInit();
|
|
331
|
+
postUi(props.current.uiPayload);
|
|
332
|
+
break;
|
|
333
|
+
case 'select':
|
|
334
|
+
p.onSelect?.(message.nodeId);
|
|
335
|
+
setTick((x) => x + 1);
|
|
336
|
+
break;
|
|
337
|
+
case 'hover':
|
|
338
|
+
p.onHover?.(message.nodeId);
|
|
339
|
+
setTick((x) => x + 1);
|
|
340
|
+
break;
|
|
341
|
+
case 'drop':
|
|
342
|
+
p.onDropOnNode?.(message.targetId, message.position);
|
|
343
|
+
break;
|
|
344
|
+
case 'keydown':
|
|
345
|
+
p.onKeydownRelay?.(message);
|
|
346
|
+
break;
|
|
347
|
+
case 'link-click':
|
|
348
|
+
p.onLinkClick?.(message.href);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
channel.addEventListener('message', onMessage);
|
|
353
|
+
const unlistenChangeset = nu.listenToChangeset(() => {
|
|
354
|
+
requestPatch();
|
|
355
|
+
setTick((x) => x + 1);
|
|
356
|
+
});
|
|
357
|
+
return () => {
|
|
358
|
+
channel.removeEventListener('message', onMessage);
|
|
359
|
+
channel.close();
|
|
360
|
+
unlistenChangeset();
|
|
361
|
+
if (rafId !== null) {
|
|
362
|
+
cancelAnimationFrame(rafId);
|
|
363
|
+
}
|
|
364
|
+
connectionRef.current = null;
|
|
365
|
+
};
|
|
366
|
+
}, [src, nu]);
|
|
367
|
+
const resyncKey = `${activeComponent}:${previewMode}`;
|
|
368
|
+
const lastResyncKey = useRef(null);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (lastResyncKey.current === resyncKey) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
lastResyncKey.current = resyncKey;
|
|
374
|
+
connectionRef.current?.sendInit();
|
|
375
|
+
}, [resyncKey]);
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
connectionRef.current?.postUi(uiPayload);
|
|
378
|
+
}, [uiPayload]);
|
|
379
|
+
const elementFor = (id) => doc && id ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
|
|
380
|
+
const rectOfEl = (el) => {
|
|
381
|
+
if (!el) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const r = el.getBoundingClientRect();
|
|
385
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
386
|
+
};
|
|
387
|
+
useLayoutEffect(() => {
|
|
388
|
+
if (previewMode) {
|
|
389
|
+
setSelected(null);
|
|
390
|
+
setHovered(null);
|
|
391
|
+
setSelectedLabel(null);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const selectedEl = elementFor(selectedId);
|
|
395
|
+
setSelected(rectOfEl(selectedEl));
|
|
396
|
+
setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
|
|
397
|
+
setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
|
|
398
|
+
}, [previewMode, selectedId, hoveredId, revision, tick, doc]);
|
|
399
|
+
return createElement('div', { className: containerClassName, style: { position: 'relative' } }, createElement('iframe', {
|
|
400
|
+
ref: iframeRef,
|
|
401
|
+
title: 'nuforge canvas',
|
|
402
|
+
className,
|
|
403
|
+
src,
|
|
404
|
+
}), renderBox && !previewMode
|
|
405
|
+
? createElement('div', {
|
|
406
|
+
style: {
|
|
407
|
+
position: 'absolute',
|
|
408
|
+
inset: 0,
|
|
409
|
+
pointerEvents: 'none',
|
|
410
|
+
overflow: 'hidden',
|
|
411
|
+
},
|
|
412
|
+
}, renderBox({ selected, hovered, selectedLabel }))
|
|
413
|
+
: null);
|
|
414
|
+
}
|
|
217
415
|
function useStructureRevision(editor) {
|
|
218
416
|
const [rev, setRev] = useState(0);
|
|
219
417
|
useEffect(() => editor.onStructureChange(() => setRev((r) => r + 1)), [editor]);
|
|
@@ -272,4 +470,4 @@ function PropFieldControl({ field, value, onChange, }) {
|
|
|
272
470
|
return createElement('label', { htmlFor: id, 'data-nu-field': field.name }, createElement('span', null, label), control);
|
|
273
471
|
}
|
|
274
472
|
|
|
275
|
-
export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, templateIdFromElement, useCanvasWindow, useEditorRevision, useStructureRevision };
|
|
473
|
+
export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, RemoteCanvasHost, templateIdFromElement, useCanvasWindow, useEditorRevision, useStructureRevision };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuforge/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "High-level SDK for building visual page editors on nuforge (mutations, blocks, commands, selection)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^18 || ^19",
|
|
31
31
|
"react-dom": "^18 || ^19",
|
|
32
|
-
"@nuforge/core": "0.1.
|
|
33
|
-
"@nuforge/
|
|
34
|
-
"@nuforge/
|
|
35
|
-
"@nuforge/
|
|
32
|
+
"@nuforge/core": "0.1.5",
|
|
33
|
+
"@nuforge/react": "0.1.5",
|
|
34
|
+
"@nuforge/types": "0.1.4",
|
|
35
|
+
"@nuforge/parser": "0.1.4"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@nuforge/react": {
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"@types/react-dom": "^19.2.0",
|
|
51
51
|
"react": "^19.2.0",
|
|
52
52
|
"react-dom": "^19.2.0",
|
|
53
|
-
"@nuforge/core": "0.1.
|
|
54
|
-
"@nuforge/
|
|
55
|
-
"@nuforge/
|
|
56
|
-
"@nuforge/types": "0.1.
|
|
53
|
+
"@nuforge/core": "0.1.5",
|
|
54
|
+
"@nuforge/react": "0.1.5",
|
|
55
|
+
"@nuforge/parser": "0.1.4",
|
|
56
|
+
"@nuforge/types": "0.1.4"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|