@nuforge/editor 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +161 -18
- package/dist/index.d.ts +62 -10
- package/dist/index.mjs +161 -18
- package/dist/react.cjs +82 -79
- package/dist/react.d.ts +7 -7
- package/dist/react.mjs +82 -79
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -96,22 +96,61 @@ const DEFAULT_BLOCKS = [
|
|
|
96
96
|
];
|
|
97
97
|
|
|
98
98
|
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
99
|
-
const CANVAS_PROTOCOL_VERSION =
|
|
100
|
-
function canvasChannelName(src) {
|
|
101
|
-
return `${CANVAS_CHANNEL}:${src}`;
|
|
102
|
-
}
|
|
99
|
+
const CANVAS_PROTOCOL_VERSION = 2;
|
|
103
100
|
function isCanvasMessage(data) {
|
|
104
101
|
return (!!data &&
|
|
105
102
|
typeof data === 'object' &&
|
|
106
103
|
data.channel === CANVAS_CHANNEL &&
|
|
107
104
|
data.v === CANVAS_PROTOCOL_VERSION);
|
|
108
105
|
}
|
|
106
|
+
function canvasNonce() {
|
|
107
|
+
const c = globalThis.crypto;
|
|
108
|
+
if (c?.randomUUID) {
|
|
109
|
+
return c.randomUUID();
|
|
110
|
+
}
|
|
111
|
+
return `${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
112
|
+
}
|
|
109
113
|
|
|
110
|
-
|
|
114
|
+
const READY_RETRY_MS = 100;
|
|
115
|
+
const READY_RETRY_MAX_MS = 2000;
|
|
116
|
+
function onNextFrame(cb) {
|
|
117
|
+
const raf = globalThis
|
|
118
|
+
.requestAnimationFrame;
|
|
119
|
+
if (raf) {
|
|
120
|
+
raf(cb);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
setTimeout(cb, 16);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function sameRect(a, b) {
|
|
127
|
+
if (!a || !b) {
|
|
128
|
+
return a === b;
|
|
129
|
+
}
|
|
130
|
+
return (a.top === b.top &&
|
|
131
|
+
a.left === b.left &&
|
|
132
|
+
a.width === b.width &&
|
|
133
|
+
a.height === b.height);
|
|
134
|
+
}
|
|
135
|
+
function sameRects(a, b) {
|
|
136
|
+
return (a.selectedLabel === b.selectedLabel &&
|
|
137
|
+
sameRect(a.selected, b.selected) &&
|
|
138
|
+
sameRect(a.hovered, b.hovered));
|
|
139
|
+
}
|
|
140
|
+
function createCanvasReceiver(opts = {}) {
|
|
111
141
|
const nu = core.Nu.create({ externals: opts.externals });
|
|
112
142
|
const volatile = reactivity.reactive({ activeComponent: null, loadGeneration: 0 });
|
|
113
|
-
const
|
|
143
|
+
const view = opts.view ?? globalThis.window ?? null;
|
|
144
|
+
const doc = opts.doc ?? globalThis.document ?? null;
|
|
114
145
|
const uiListeners = new Set();
|
|
146
|
+
const nonce = canvasNonce();
|
|
147
|
+
let port = null;
|
|
148
|
+
let retryTimer = null;
|
|
149
|
+
let retryDelay = READY_RETRY_MS;
|
|
150
|
+
let disposed = false;
|
|
151
|
+
const send = (message) => {
|
|
152
|
+
port?.postMessage(message);
|
|
153
|
+
};
|
|
115
154
|
const applyState = (message) => {
|
|
116
155
|
if (message.type === 'init') {
|
|
117
156
|
nu.load(t__namespace.unflatten(message.flat));
|
|
@@ -122,46 +161,150 @@ function createCanvasReceiver(opts) {
|
|
|
122
161
|
}
|
|
123
162
|
volatile.activeComponent = message.activeComponent;
|
|
124
163
|
};
|
|
125
|
-
|
|
164
|
+
let tracked = {
|
|
165
|
+
selectedId: null,
|
|
166
|
+
hoveredId: null,
|
|
167
|
+
};
|
|
168
|
+
let lastRects = null;
|
|
169
|
+
let measureScheduled = false;
|
|
170
|
+
let resizeObserver = null;
|
|
171
|
+
const elementFor = (id) => id && doc ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
|
|
172
|
+
const rectOf = (el) => {
|
|
173
|
+
if (!el) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const r = el.getBoundingClientRect();
|
|
177
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
178
|
+
};
|
|
179
|
+
const measure = () => {
|
|
180
|
+
const selectedEl = elementFor(tracked.selectedId);
|
|
181
|
+
const next = {
|
|
182
|
+
type: 'rects',
|
|
183
|
+
selected: rectOf(selectedEl),
|
|
184
|
+
hovered: tracked.hoveredId && tracked.hoveredId !== tracked.selectedId
|
|
185
|
+
? rectOf(elementFor(tracked.hoveredId))
|
|
186
|
+
: null,
|
|
187
|
+
selectedLabel: selectedEl ? selectedEl.tagName.toLowerCase() : null,
|
|
188
|
+
};
|
|
189
|
+
if (lastRects && sameRects(lastRects, next)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
lastRects = next;
|
|
193
|
+
send({ channel: CANVAS_CHANNEL, v: CANVAS_PROTOCOL_VERSION, ...next });
|
|
194
|
+
};
|
|
195
|
+
const scheduleMeasure = () => {
|
|
196
|
+
if (measureScheduled || !doc) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
measureScheduled = true;
|
|
200
|
+
onNextFrame(() => {
|
|
201
|
+
measureScheduled = false;
|
|
202
|
+
measure();
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
const onScroll = () => scheduleMeasure();
|
|
206
|
+
if (doc) {
|
|
207
|
+
doc.addEventListener('scroll', onScroll, true);
|
|
208
|
+
const RO = globalThis
|
|
209
|
+
.ResizeObserver;
|
|
210
|
+
if (RO && doc.documentElement) {
|
|
211
|
+
resizeObserver = new RO(scheduleMeasure);
|
|
212
|
+
resizeObserver.observe(doc.documentElement);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const onPortMessage = (event) => {
|
|
126
216
|
if (!isCanvasMessage(event.data)) {
|
|
127
217
|
return;
|
|
128
218
|
}
|
|
129
219
|
const message = event.data;
|
|
130
220
|
if (message.type === 'init' || message.type === 'patch') {
|
|
131
221
|
applyState(message);
|
|
222
|
+
scheduleMeasure();
|
|
132
223
|
}
|
|
133
224
|
else if (message.type === 'ui') {
|
|
134
225
|
for (const cb of uiListeners) {
|
|
135
226
|
cb(message.payload);
|
|
136
227
|
}
|
|
228
|
+
scheduleMeasure();
|
|
229
|
+
}
|
|
230
|
+
else if (message.type === 'track') {
|
|
231
|
+
tracked = { selectedId: message.selectedId, hoveredId: message.hoveredId };
|
|
232
|
+
lastRects = null;
|
|
233
|
+
scheduleMeasure();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const stopRetrying = () => {
|
|
237
|
+
if (retryTimer !== null) {
|
|
238
|
+
clearTimeout(retryTimer);
|
|
239
|
+
retryTimer = null;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const announce = () => {
|
|
243
|
+
view?.parent?.postMessage({
|
|
244
|
+
channel: CANVAS_CHANNEL,
|
|
245
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
246
|
+
type: 'ready',
|
|
247
|
+
nonce,
|
|
248
|
+
}, '*');
|
|
249
|
+
retryDelay = Math.min(retryDelay * 2, READY_RETRY_MAX_MS);
|
|
250
|
+
retryTimer = setTimeout(announce, retryDelay);
|
|
251
|
+
};
|
|
252
|
+
const onWindowMessage = (event) => {
|
|
253
|
+
if (disposed || !isCanvasMessage(event.data)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const message = event.data;
|
|
257
|
+
if (message.type !== 'connect' || message.nonce !== nonce) {
|
|
258
|
+
return;
|
|
137
259
|
}
|
|
260
|
+
const incoming = event.ports?.[0];
|
|
261
|
+
if (!incoming) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
port?.close();
|
|
265
|
+
port = incoming;
|
|
266
|
+
port.addEventListener('message', onPortMessage);
|
|
267
|
+
port.start();
|
|
268
|
+
stopRetrying();
|
|
269
|
+
send({
|
|
270
|
+
channel: CANVAS_CHANNEL,
|
|
271
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
272
|
+
type: 'hello',
|
|
273
|
+
nonce,
|
|
274
|
+
});
|
|
138
275
|
};
|
|
139
|
-
|
|
276
|
+
view?.addEventListener('message', onWindowMessage);
|
|
140
277
|
return {
|
|
141
278
|
nu,
|
|
142
279
|
volatile,
|
|
143
280
|
postReady() {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
281
|
+
if (disposed || port || retryTimer !== null) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
retryDelay = READY_RETRY_MS;
|
|
285
|
+
announce();
|
|
150
286
|
},
|
|
151
287
|
onUi(cb) {
|
|
152
288
|
uiListeners.add(cb);
|
|
153
289
|
return () => uiListeners.delete(cb);
|
|
154
290
|
},
|
|
155
291
|
postEvent(message) {
|
|
156
|
-
|
|
292
|
+
send({
|
|
157
293
|
channel: CANVAS_CHANNEL,
|
|
158
294
|
v: CANVAS_PROTOCOL_VERSION,
|
|
159
295
|
...message,
|
|
160
296
|
});
|
|
161
297
|
},
|
|
162
298
|
dispose() {
|
|
163
|
-
|
|
164
|
-
|
|
299
|
+
disposed = true;
|
|
300
|
+
stopRetrying();
|
|
301
|
+
doc?.removeEventListener('scroll', onScroll, true);
|
|
302
|
+
resizeObserver?.disconnect();
|
|
303
|
+
resizeObserver = null;
|
|
304
|
+
view?.removeEventListener('message', onWindowMessage);
|
|
305
|
+
port?.removeEventListener('message', onPortMessage);
|
|
306
|
+
port?.close();
|
|
307
|
+
port = null;
|
|
165
308
|
uiListeners.clear();
|
|
166
309
|
},
|
|
167
310
|
};
|
|
@@ -758,7 +901,7 @@ exports.CommandRegistry = CommandRegistry;
|
|
|
758
901
|
exports.DEFAULT_BLOCKS = DEFAULT_BLOCKS;
|
|
759
902
|
exports.Editor = Editor;
|
|
760
903
|
exports.canHaveChildren = canHaveChildren;
|
|
761
|
-
exports.
|
|
904
|
+
exports.canvasNonce = canvasNonce;
|
|
762
905
|
exports.createCanvasReceiver = createCanvasReceiver;
|
|
763
906
|
exports.createEditor = createEditor;
|
|
764
907
|
exports.defineBlock = defineBlock;
|
package/dist/index.d.ts
CHANGED
|
@@ -47,8 +47,13 @@ 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
49
|
declare const CANVAS_CHANNEL = "nuforge-canvas";
|
|
50
|
-
declare const CANVAS_PROTOCOL_VERSION =
|
|
51
|
-
|
|
50
|
+
declare const CANVAS_PROTOCOL_VERSION = 2;
|
|
51
|
+
interface Rect {
|
|
52
|
+
top: number;
|
|
53
|
+
left: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
}
|
|
52
57
|
interface FlattenedState {
|
|
53
58
|
root: {
|
|
54
59
|
$$typeId: string;
|
|
@@ -79,12 +84,31 @@ interface CanvasUiMessage {
|
|
|
79
84
|
type: 'ui';
|
|
80
85
|
payload: unknown;
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
interface CanvasConnectMessage {
|
|
88
|
+
channel: typeof CANVAS_CHANNEL;
|
|
89
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
90
|
+
type: 'connect';
|
|
91
|
+
nonce: string;
|
|
92
|
+
}
|
|
93
|
+
interface CanvasTrackMessage {
|
|
94
|
+
channel: typeof CANVAS_CHANNEL;
|
|
95
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
96
|
+
type: 'track';
|
|
97
|
+
selectedId: string | null;
|
|
98
|
+
hoveredId: string | null;
|
|
99
|
+
}
|
|
100
|
+
type CanvasHostMessage = CanvasInitMessage | CanvasPatchMessage | CanvasUiMessage | CanvasConnectMessage | CanvasTrackMessage;
|
|
83
101
|
interface CanvasReadyMessage {
|
|
84
102
|
channel: typeof CANVAS_CHANNEL;
|
|
85
103
|
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
86
104
|
type: 'ready';
|
|
87
|
-
|
|
105
|
+
nonce: string;
|
|
106
|
+
}
|
|
107
|
+
interface CanvasHelloMessage {
|
|
108
|
+
channel: typeof CANVAS_CHANNEL;
|
|
109
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
110
|
+
type: 'hello';
|
|
111
|
+
nonce: string;
|
|
88
112
|
}
|
|
89
113
|
interface CanvasSelectMessage {
|
|
90
114
|
channel: typeof CANVAS_CHANNEL;
|
|
@@ -122,14 +146,42 @@ interface CanvasLinkClickMessage {
|
|
|
122
146
|
type: 'link-click';
|
|
123
147
|
href: string;
|
|
124
148
|
}
|
|
125
|
-
|
|
149
|
+
interface CanvasRectsMessage {
|
|
150
|
+
channel: typeof CANVAS_CHANNEL;
|
|
151
|
+
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
152
|
+
type: 'rects';
|
|
153
|
+
selected: Rect | null;
|
|
154
|
+
hovered: Rect | null;
|
|
155
|
+
selectedLabel: string | null;
|
|
156
|
+
}
|
|
157
|
+
type CanvasEventMessage = CanvasSelectMessage | CanvasHoverMessage | CanvasDropMessage | CanvasKeydownMessage | CanvasLinkClickMessage;
|
|
158
|
+
type CanvasClientMessage = CanvasReadyMessage | CanvasHelloMessage | CanvasRectsMessage | CanvasEventMessage;
|
|
126
159
|
type CanvasMessage = CanvasHostMessage | CanvasClientMessage;
|
|
127
160
|
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
128
161
|
declare function isCanvasMessage(data: unknown): data is CanvasMessage;
|
|
162
|
+
declare function canvasNonce(): string;
|
|
129
163
|
|
|
164
|
+
interface CanvasView {
|
|
165
|
+
parent: {
|
|
166
|
+
postMessage(message: unknown, targetOrigin: string): void;
|
|
167
|
+
} | null;
|
|
168
|
+
addEventListener(type: 'message', cb: (e: MessageEvent) => void): void;
|
|
169
|
+
removeEventListener(type: 'message', cb: (e: MessageEvent) => void): void;
|
|
170
|
+
}
|
|
171
|
+
interface CanvasGeometrySource {
|
|
172
|
+
querySelector(selectors: string): CanvasMeasurable | null;
|
|
173
|
+
addEventListener(type: 'scroll', cb: () => void, capture: boolean): void;
|
|
174
|
+
removeEventListener(type: 'scroll', cb: () => void, capture: boolean): void;
|
|
175
|
+
documentElement?: Element | null;
|
|
176
|
+
}
|
|
177
|
+
interface CanvasMeasurable {
|
|
178
|
+
tagName: string;
|
|
179
|
+
getBoundingClientRect(): Rect;
|
|
180
|
+
}
|
|
130
181
|
interface CanvasReceiverOpts {
|
|
131
|
-
src: string;
|
|
132
182
|
externals?: NuExternalsFactory;
|
|
183
|
+
view?: CanvasView;
|
|
184
|
+
doc?: CanvasGeometrySource;
|
|
133
185
|
}
|
|
134
186
|
interface CanvasReceiver {
|
|
135
187
|
nu: Nu;
|
|
@@ -139,10 +191,10 @@ interface CanvasReceiver {
|
|
|
139
191
|
};
|
|
140
192
|
postReady(): void;
|
|
141
193
|
onUi(cb: (payload: unknown) => void): () => void;
|
|
142
|
-
postEvent(message: DistributiveOmit<
|
|
194
|
+
postEvent(message: DistributiveOmit<CanvasEventMessage, 'channel' | 'v'>): void;
|
|
143
195
|
dispose(): void;
|
|
144
196
|
}
|
|
145
|
-
declare function createCanvasReceiver(opts
|
|
197
|
+
declare function createCanvasReceiver(opts?: CanvasReceiverOpts): CanvasReceiver;
|
|
146
198
|
|
|
147
199
|
interface CreateEditorOptions {
|
|
148
200
|
blocks?: Block[];
|
|
@@ -222,5 +274,5 @@ declare class CommandRegistry {
|
|
|
222
274
|
run(id: string): void;
|
|
223
275
|
}
|
|
224
276
|
|
|
225
|
-
export { BlockRegistry, CANVAS_CHANNEL, CANVAS_PROTOCOL_VERSION, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren,
|
|
226
|
-
export type { Block, CanvasClientMessage, CanvasDropMessage, CanvasHostMessage, CanvasHoverMessage, CanvasInitMessage, CanvasKeydownMessage, CanvasLinkClickMessage, CanvasMessage, CanvasPatchMessage, CanvasReadyMessage, CanvasReceiver, CanvasReceiverOpts, CanvasSelectMessage, CanvasUiMessage, Command, CreateEditorOptions, DistributiveOmit, DropPosition, FlattenedState, PropSchemaField };
|
|
277
|
+
export { BlockRegistry, CANVAS_CHANNEL, CANVAS_PROTOCOL_VERSION, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, canvasNonce, createCanvasReceiver, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isCanvasMessage, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
|
|
278
|
+
export type { Block, CanvasClientMessage, CanvasConnectMessage, CanvasDropMessage, CanvasEventMessage, CanvasGeometrySource, CanvasHelloMessage, CanvasHostMessage, CanvasHoverMessage, CanvasInitMessage, CanvasKeydownMessage, CanvasLinkClickMessage, CanvasMeasurable, CanvasMessage, CanvasPatchMessage, CanvasReadyMessage, CanvasReceiver, CanvasReceiverOpts, CanvasRectsMessage, CanvasSelectMessage, CanvasTrackMessage, CanvasUiMessage, CanvasView, Command, CreateEditorOptions, DistributiveOmit, DropPosition, FlattenedState, PropSchemaField, Rect };
|
package/dist/index.mjs
CHANGED
|
@@ -75,22 +75,61 @@ const DEFAULT_BLOCKS = [
|
|
|
75
75
|
];
|
|
76
76
|
|
|
77
77
|
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
78
|
-
const CANVAS_PROTOCOL_VERSION =
|
|
79
|
-
function canvasChannelName(src) {
|
|
80
|
-
return `${CANVAS_CHANNEL}:${src}`;
|
|
81
|
-
}
|
|
78
|
+
const CANVAS_PROTOCOL_VERSION = 2;
|
|
82
79
|
function isCanvasMessage(data) {
|
|
83
80
|
return (!!data &&
|
|
84
81
|
typeof data === 'object' &&
|
|
85
82
|
data.channel === CANVAS_CHANNEL &&
|
|
86
83
|
data.v === CANVAS_PROTOCOL_VERSION);
|
|
87
84
|
}
|
|
85
|
+
function canvasNonce() {
|
|
86
|
+
const c = globalThis.crypto;
|
|
87
|
+
if (c?.randomUUID) {
|
|
88
|
+
return c.randomUUID();
|
|
89
|
+
}
|
|
90
|
+
return `${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
91
|
+
}
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
const READY_RETRY_MS = 100;
|
|
94
|
+
const READY_RETRY_MAX_MS = 2000;
|
|
95
|
+
function onNextFrame(cb) {
|
|
96
|
+
const raf = globalThis
|
|
97
|
+
.requestAnimationFrame;
|
|
98
|
+
if (raf) {
|
|
99
|
+
raf(cb);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
setTimeout(cb, 16);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function sameRect(a, b) {
|
|
106
|
+
if (!a || !b) {
|
|
107
|
+
return a === b;
|
|
108
|
+
}
|
|
109
|
+
return (a.top === b.top &&
|
|
110
|
+
a.left === b.left &&
|
|
111
|
+
a.width === b.width &&
|
|
112
|
+
a.height === b.height);
|
|
113
|
+
}
|
|
114
|
+
function sameRects(a, b) {
|
|
115
|
+
return (a.selectedLabel === b.selectedLabel &&
|
|
116
|
+
sameRect(a.selected, b.selected) &&
|
|
117
|
+
sameRect(a.hovered, b.hovered));
|
|
118
|
+
}
|
|
119
|
+
function createCanvasReceiver(opts = {}) {
|
|
90
120
|
const nu = Nu.create({ externals: opts.externals });
|
|
91
121
|
const volatile = reactive({ activeComponent: null, loadGeneration: 0 });
|
|
92
|
-
const
|
|
122
|
+
const view = opts.view ?? globalThis.window ?? null;
|
|
123
|
+
const doc = opts.doc ?? globalThis.document ?? null;
|
|
93
124
|
const uiListeners = new Set();
|
|
125
|
+
const nonce = canvasNonce();
|
|
126
|
+
let port = null;
|
|
127
|
+
let retryTimer = null;
|
|
128
|
+
let retryDelay = READY_RETRY_MS;
|
|
129
|
+
let disposed = false;
|
|
130
|
+
const send = (message) => {
|
|
131
|
+
port?.postMessage(message);
|
|
132
|
+
};
|
|
94
133
|
const applyState = (message) => {
|
|
95
134
|
if (message.type === 'init') {
|
|
96
135
|
nu.load(t.unflatten(message.flat));
|
|
@@ -101,46 +140,150 @@ function createCanvasReceiver(opts) {
|
|
|
101
140
|
}
|
|
102
141
|
volatile.activeComponent = message.activeComponent;
|
|
103
142
|
};
|
|
104
|
-
|
|
143
|
+
let tracked = {
|
|
144
|
+
selectedId: null,
|
|
145
|
+
hoveredId: null,
|
|
146
|
+
};
|
|
147
|
+
let lastRects = null;
|
|
148
|
+
let measureScheduled = false;
|
|
149
|
+
let resizeObserver = null;
|
|
150
|
+
const elementFor = (id) => id && doc ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
|
|
151
|
+
const rectOf = (el) => {
|
|
152
|
+
if (!el) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const r = el.getBoundingClientRect();
|
|
156
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
157
|
+
};
|
|
158
|
+
const measure = () => {
|
|
159
|
+
const selectedEl = elementFor(tracked.selectedId);
|
|
160
|
+
const next = {
|
|
161
|
+
type: 'rects',
|
|
162
|
+
selected: rectOf(selectedEl),
|
|
163
|
+
hovered: tracked.hoveredId && tracked.hoveredId !== tracked.selectedId
|
|
164
|
+
? rectOf(elementFor(tracked.hoveredId))
|
|
165
|
+
: null,
|
|
166
|
+
selectedLabel: selectedEl ? selectedEl.tagName.toLowerCase() : null,
|
|
167
|
+
};
|
|
168
|
+
if (lastRects && sameRects(lastRects, next)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
lastRects = next;
|
|
172
|
+
send({ channel: CANVAS_CHANNEL, v: CANVAS_PROTOCOL_VERSION, ...next });
|
|
173
|
+
};
|
|
174
|
+
const scheduleMeasure = () => {
|
|
175
|
+
if (measureScheduled || !doc) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
measureScheduled = true;
|
|
179
|
+
onNextFrame(() => {
|
|
180
|
+
measureScheduled = false;
|
|
181
|
+
measure();
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
const onScroll = () => scheduleMeasure();
|
|
185
|
+
if (doc) {
|
|
186
|
+
doc.addEventListener('scroll', onScroll, true);
|
|
187
|
+
const RO = globalThis
|
|
188
|
+
.ResizeObserver;
|
|
189
|
+
if (RO && doc.documentElement) {
|
|
190
|
+
resizeObserver = new RO(scheduleMeasure);
|
|
191
|
+
resizeObserver.observe(doc.documentElement);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const onPortMessage = (event) => {
|
|
105
195
|
if (!isCanvasMessage(event.data)) {
|
|
106
196
|
return;
|
|
107
197
|
}
|
|
108
198
|
const message = event.data;
|
|
109
199
|
if (message.type === 'init' || message.type === 'patch') {
|
|
110
200
|
applyState(message);
|
|
201
|
+
scheduleMeasure();
|
|
111
202
|
}
|
|
112
203
|
else if (message.type === 'ui') {
|
|
113
204
|
for (const cb of uiListeners) {
|
|
114
205
|
cb(message.payload);
|
|
115
206
|
}
|
|
207
|
+
scheduleMeasure();
|
|
208
|
+
}
|
|
209
|
+
else if (message.type === 'track') {
|
|
210
|
+
tracked = { selectedId: message.selectedId, hoveredId: message.hoveredId };
|
|
211
|
+
lastRects = null;
|
|
212
|
+
scheduleMeasure();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const stopRetrying = () => {
|
|
216
|
+
if (retryTimer !== null) {
|
|
217
|
+
clearTimeout(retryTimer);
|
|
218
|
+
retryTimer = null;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const announce = () => {
|
|
222
|
+
view?.parent?.postMessage({
|
|
223
|
+
channel: CANVAS_CHANNEL,
|
|
224
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
225
|
+
type: 'ready',
|
|
226
|
+
nonce,
|
|
227
|
+
}, '*');
|
|
228
|
+
retryDelay = Math.min(retryDelay * 2, READY_RETRY_MAX_MS);
|
|
229
|
+
retryTimer = setTimeout(announce, retryDelay);
|
|
230
|
+
};
|
|
231
|
+
const onWindowMessage = (event) => {
|
|
232
|
+
if (disposed || !isCanvasMessage(event.data)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const message = event.data;
|
|
236
|
+
if (message.type !== 'connect' || message.nonce !== nonce) {
|
|
237
|
+
return;
|
|
116
238
|
}
|
|
239
|
+
const incoming = event.ports?.[0];
|
|
240
|
+
if (!incoming) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
port?.close();
|
|
244
|
+
port = incoming;
|
|
245
|
+
port.addEventListener('message', onPortMessage);
|
|
246
|
+
port.start();
|
|
247
|
+
stopRetrying();
|
|
248
|
+
send({
|
|
249
|
+
channel: CANVAS_CHANNEL,
|
|
250
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
251
|
+
type: 'hello',
|
|
252
|
+
nonce,
|
|
253
|
+
});
|
|
117
254
|
};
|
|
118
|
-
|
|
255
|
+
view?.addEventListener('message', onWindowMessage);
|
|
119
256
|
return {
|
|
120
257
|
nu,
|
|
121
258
|
volatile,
|
|
122
259
|
postReady() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
260
|
+
if (disposed || port || retryTimer !== null) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
retryDelay = READY_RETRY_MS;
|
|
264
|
+
announce();
|
|
129
265
|
},
|
|
130
266
|
onUi(cb) {
|
|
131
267
|
uiListeners.add(cb);
|
|
132
268
|
return () => uiListeners.delete(cb);
|
|
133
269
|
},
|
|
134
270
|
postEvent(message) {
|
|
135
|
-
|
|
271
|
+
send({
|
|
136
272
|
channel: CANVAS_CHANNEL,
|
|
137
273
|
v: CANVAS_PROTOCOL_VERSION,
|
|
138
274
|
...message,
|
|
139
275
|
});
|
|
140
276
|
},
|
|
141
277
|
dispose() {
|
|
142
|
-
|
|
143
|
-
|
|
278
|
+
disposed = true;
|
|
279
|
+
stopRetrying();
|
|
280
|
+
doc?.removeEventListener('scroll', onScroll, true);
|
|
281
|
+
resizeObserver?.disconnect();
|
|
282
|
+
resizeObserver = null;
|
|
283
|
+
view?.removeEventListener('message', onWindowMessage);
|
|
284
|
+
port?.removeEventListener('message', onPortMessage);
|
|
285
|
+
port?.close();
|
|
286
|
+
port = null;
|
|
144
287
|
uiListeners.clear();
|
|
145
288
|
},
|
|
146
289
|
};
|
|
@@ -730,4 +873,4 @@ function createEditor(nu, opts) {
|
|
|
730
873
|
return new Editor(nu, opts);
|
|
731
874
|
}
|
|
732
875
|
|
|
733
|
-
export { BlockRegistry, CANVAS_CHANNEL, CANVAS_PROTOCOL_VERSION, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren,
|
|
876
|
+
export { BlockRegistry, CANVAS_CHANNEL, CANVAS_PROTOCOL_VERSION, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, canvasNonce, createCanvasReceiver, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isCanvasMessage, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
|
package/dist/react.cjs
CHANGED
|
@@ -7,10 +7,7 @@ var react = require('react');
|
|
|
7
7
|
var reactDom = require('react-dom');
|
|
8
8
|
|
|
9
9
|
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
10
|
-
const CANVAS_PROTOCOL_VERSION =
|
|
11
|
-
function canvasChannelName(src) {
|
|
12
|
-
return `${CANVAS_CHANNEL}:${src}`;
|
|
13
|
-
}
|
|
10
|
+
const CANVAS_PROTOCOL_VERSION = 2;
|
|
14
11
|
function isCanvasMessage(data) {
|
|
15
12
|
return (!!data &&
|
|
16
13
|
typeof data === 'object' &&
|
|
@@ -219,13 +216,14 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
219
216
|
}, renderBox({ selected, hovered, selectedLabel, drop }))
|
|
220
217
|
: null);
|
|
221
218
|
}
|
|
219
|
+
const NO_BOXES = {
|
|
220
|
+
selected: null,
|
|
221
|
+
hovered: null,
|
|
222
|
+
selectedLabel: null,
|
|
223
|
+
};
|
|
222
224
|
function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hoveredId = null, revision = 0, previewMode = false, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }) {
|
|
223
225
|
const iframeRef = react.useRef(null);
|
|
224
|
-
const [
|
|
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);
|
|
226
|
+
const [boxes, setBoxes] = react.useState(NO_BOXES);
|
|
229
227
|
const props = react.useRef({
|
|
230
228
|
nu,
|
|
231
229
|
activeComponent,
|
|
@@ -248,42 +246,14 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
248
246
|
};
|
|
249
247
|
const connectionRef = react.useRef(null);
|
|
250
248
|
react.useEffect(() => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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;
|
|
249
|
+
let port = null;
|
|
250
|
+
let pendingNonce = null;
|
|
251
|
+
let connectedNonce = null;
|
|
283
252
|
let rafId = null;
|
|
284
253
|
let rev = 0;
|
|
254
|
+
const post = (message) => port?.postMessage(message);
|
|
285
255
|
const sendInit = () => {
|
|
286
|
-
|
|
256
|
+
post({
|
|
287
257
|
channel: CANVAS_CHANNEL,
|
|
288
258
|
v: CANVAS_PROTOCOL_VERSION,
|
|
289
259
|
type: 'init',
|
|
@@ -292,12 +262,12 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
292
262
|
});
|
|
293
263
|
};
|
|
294
264
|
const requestPatch = () => {
|
|
295
|
-
if (
|
|
265
|
+
if (connectedNonce === null || rafId !== null) {
|
|
296
266
|
return;
|
|
297
267
|
}
|
|
298
268
|
rafId = requestAnimationFrame(() => {
|
|
299
269
|
rafId = null;
|
|
300
|
-
|
|
270
|
+
post({
|
|
301
271
|
channel: CANVAS_CHANNEL,
|
|
302
272
|
v: CANVAS_PROTOCOL_VERSION,
|
|
303
273
|
type: 'patch',
|
|
@@ -308,37 +278,51 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
308
278
|
});
|
|
309
279
|
};
|
|
310
280
|
const postUi = (payload) => {
|
|
311
|
-
|
|
281
|
+
post({
|
|
312
282
|
channel: CANVAS_CHANNEL,
|
|
313
283
|
v: CANVAS_PROTOCOL_VERSION,
|
|
314
284
|
type: 'ui',
|
|
315
285
|
payload,
|
|
316
286
|
});
|
|
317
287
|
};
|
|
318
|
-
|
|
319
|
-
|
|
288
|
+
const postTrack = (selectedId, hoveredId) => {
|
|
289
|
+
post({
|
|
290
|
+
channel: CANVAS_CHANNEL,
|
|
291
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
292
|
+
type: 'track',
|
|
293
|
+
selectedId,
|
|
294
|
+
hoveredId,
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
connectionRef.current = { sendInit, requestPatch, postUi, postTrack };
|
|
298
|
+
const onPortMessage = (event) => {
|
|
320
299
|
if (!isCanvasMessage(event.data)) {
|
|
321
300
|
return;
|
|
322
301
|
}
|
|
323
302
|
const message = event.data;
|
|
324
303
|
const p = props.current;
|
|
325
304
|
switch (message.type) {
|
|
326
|
-
case '
|
|
327
|
-
if (
|
|
305
|
+
case 'hello':
|
|
306
|
+
if (message.nonce !== pendingNonce) {
|
|
328
307
|
return;
|
|
329
308
|
}
|
|
330
|
-
|
|
331
|
-
ready = true;
|
|
309
|
+
connectedNonce = message.nonce;
|
|
332
310
|
sendInit();
|
|
311
|
+
postTrack(trackRef.current.selectedId, trackRef.current.hoveredId);
|
|
333
312
|
postUi(props.current.uiPayload);
|
|
334
313
|
break;
|
|
314
|
+
case 'rects':
|
|
315
|
+
setBoxes({
|
|
316
|
+
selected: message.selected,
|
|
317
|
+
hovered: message.hovered,
|
|
318
|
+
selectedLabel: message.selectedLabel,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
335
321
|
case 'select':
|
|
336
322
|
p.onSelect?.(message.nodeId);
|
|
337
|
-
setTick((x) => x + 1);
|
|
338
323
|
break;
|
|
339
324
|
case 'hover':
|
|
340
325
|
p.onHover?.(message.nodeId);
|
|
341
|
-
setTick((x) => x + 1);
|
|
342
326
|
break;
|
|
343
327
|
case 'drop':
|
|
344
328
|
p.onDropOnNode?.(message.targetId, message.position);
|
|
@@ -351,14 +335,39 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
351
335
|
break;
|
|
352
336
|
}
|
|
353
337
|
};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
338
|
+
const onWindowMessage = (event) => {
|
|
339
|
+
const frame = iframeRef.current;
|
|
340
|
+
if (!frame || event.source !== frame.contentWindow) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!isCanvasMessage(event.data) || event.data.type !== 'ready') {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const { nonce } = event.data;
|
|
347
|
+
if (connectedNonce === nonce) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
port?.close();
|
|
351
|
+
const channel = new MessageChannel();
|
|
352
|
+
port = channel.port1;
|
|
353
|
+
pendingNonce = nonce;
|
|
354
|
+
connectedNonce = null;
|
|
355
|
+
port.addEventListener('message', onPortMessage);
|
|
356
|
+
port.start();
|
|
357
|
+
event.source.postMessage({
|
|
358
|
+
channel: CANVAS_CHANNEL,
|
|
359
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
360
|
+
type: 'connect',
|
|
361
|
+
nonce,
|
|
362
|
+
}, '*', [channel.port2]);
|
|
363
|
+
};
|
|
364
|
+
window.addEventListener('message', onWindowMessage);
|
|
365
|
+
const unlistenChangeset = nu.listenToChangeset(requestPatch);
|
|
359
366
|
return () => {
|
|
360
|
-
|
|
361
|
-
|
|
367
|
+
window.removeEventListener('message', onWindowMessage);
|
|
368
|
+
port?.removeEventListener('message', onPortMessage);
|
|
369
|
+
port?.close();
|
|
370
|
+
port = null;
|
|
362
371
|
unlistenChangeset();
|
|
363
372
|
if (rafId !== null) {
|
|
364
373
|
cancelAnimationFrame(rafId);
|
|
@@ -378,26 +387,20 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
378
387
|
react.useEffect(() => {
|
|
379
388
|
connectionRef.current?.postUi(uiPayload);
|
|
380
389
|
}, [uiPayload]);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
+
const trackRef = react.useRef({
|
|
391
|
+
selectedId: null,
|
|
392
|
+
hoveredId: null,
|
|
393
|
+
});
|
|
394
|
+
react.useEffect(() => {
|
|
395
|
+
const next = previewMode
|
|
396
|
+
? { selectedId: null, hoveredId: null }
|
|
397
|
+
: { selectedId, hoveredId };
|
|
398
|
+
trackRef.current = next;
|
|
390
399
|
if (previewMode) {
|
|
391
|
-
|
|
392
|
-
setHovered(null);
|
|
393
|
-
setSelectedLabel(null);
|
|
394
|
-
return;
|
|
400
|
+
setBoxes(NO_BOXES);
|
|
395
401
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
|
|
399
|
-
setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
|
|
400
|
-
}, [previewMode, selectedId, hoveredId, revision, tick, doc]);
|
|
402
|
+
connectionRef.current?.postTrack(next.selectedId, next.hoveredId);
|
|
403
|
+
}, [previewMode, selectedId, hoveredId, revision]);
|
|
401
404
|
return react.createElement('div', { className: containerClassName, style: { position: 'relative' } }, react.createElement('iframe', {
|
|
402
405
|
ref: iframeRef,
|
|
403
406
|
title: 'nuforge canvas',
|
|
@@ -411,7 +414,7 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
411
414
|
pointerEvents: 'none',
|
|
412
415
|
overflow: 'hidden',
|
|
413
416
|
},
|
|
414
|
-
}, renderBox(
|
|
417
|
+
}, renderBox(boxes))
|
|
415
418
|
: null);
|
|
416
419
|
}
|
|
417
420
|
function useStructureRevision(editor) {
|
package/dist/react.d.ts
CHANGED
|
@@ -30,7 +30,13 @@ declare class BlockRegistry {
|
|
|
30
30
|
type DropPosition = 'before' | 'after' | 'inside';
|
|
31
31
|
|
|
32
32
|
declare const CANVAS_CHANNEL = "nuforge-canvas";
|
|
33
|
-
declare const CANVAS_PROTOCOL_VERSION =
|
|
33
|
+
declare const CANVAS_PROTOCOL_VERSION = 2;
|
|
34
|
+
interface Rect {
|
|
35
|
+
top: number;
|
|
36
|
+
left: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}
|
|
34
40
|
interface CanvasKeydownMessage {
|
|
35
41
|
channel: typeof CANVAS_CHANNEL;
|
|
36
42
|
v: typeof CANVAS_PROTOCOL_VERSION;
|
|
@@ -120,12 +126,6 @@ declare class Editor {
|
|
|
120
126
|
private insertRelativeTo;
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
interface Rect {
|
|
124
|
-
top: number;
|
|
125
|
-
left: number;
|
|
126
|
-
width: number;
|
|
127
|
-
height: number;
|
|
128
|
-
}
|
|
129
129
|
interface CanvasWindow {
|
|
130
130
|
doc: Document | null;
|
|
131
131
|
window: Window | null;
|
package/dist/react.mjs
CHANGED
|
@@ -5,10 +5,7 @@ import { createContext, memo, createElement, Fragment, useRef, useState, useEffe
|
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
|
|
7
7
|
const CANVAS_CHANNEL = 'nuforge-canvas';
|
|
8
|
-
const CANVAS_PROTOCOL_VERSION =
|
|
9
|
-
function canvasChannelName(src) {
|
|
10
|
-
return `${CANVAS_CHANNEL}:${src}`;
|
|
11
|
-
}
|
|
8
|
+
const CANVAS_PROTOCOL_VERSION = 2;
|
|
12
9
|
function isCanvasMessage(data) {
|
|
13
10
|
return (!!data &&
|
|
14
11
|
typeof data === 'object' &&
|
|
@@ -217,13 +214,14 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
|
|
|
217
214
|
}, renderBox({ selected, hovered, selectedLabel, drop }))
|
|
218
215
|
: null);
|
|
219
216
|
}
|
|
217
|
+
const NO_BOXES = {
|
|
218
|
+
selected: null,
|
|
219
|
+
hovered: null,
|
|
220
|
+
selectedLabel: null,
|
|
221
|
+
};
|
|
220
222
|
function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hoveredId = null, revision = 0, previewMode = false, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }) {
|
|
221
223
|
const iframeRef = useRef(null);
|
|
222
|
-
const [
|
|
223
|
-
const [tick, setTick] = useState(0);
|
|
224
|
-
const [selected, setSelected] = useState(null);
|
|
225
|
-
const [hovered, setHovered] = useState(null);
|
|
226
|
-
const [selectedLabel, setSelectedLabel] = useState(null);
|
|
224
|
+
const [boxes, setBoxes] = useState(NO_BOXES);
|
|
227
225
|
const props = useRef({
|
|
228
226
|
nu,
|
|
229
227
|
activeComponent,
|
|
@@ -246,42 +244,14 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
246
244
|
};
|
|
247
245
|
const connectionRef = useRef(null);
|
|
248
246
|
useEffect(() => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
247
|
+
let port = null;
|
|
248
|
+
let pendingNonce = null;
|
|
249
|
+
let connectedNonce = null;
|
|
281
250
|
let rafId = null;
|
|
282
251
|
let rev = 0;
|
|
252
|
+
const post = (message) => port?.postMessage(message);
|
|
283
253
|
const sendInit = () => {
|
|
284
|
-
|
|
254
|
+
post({
|
|
285
255
|
channel: CANVAS_CHANNEL,
|
|
286
256
|
v: CANVAS_PROTOCOL_VERSION,
|
|
287
257
|
type: 'init',
|
|
@@ -290,12 +260,12 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
290
260
|
});
|
|
291
261
|
};
|
|
292
262
|
const requestPatch = () => {
|
|
293
|
-
if (
|
|
263
|
+
if (connectedNonce === null || rafId !== null) {
|
|
294
264
|
return;
|
|
295
265
|
}
|
|
296
266
|
rafId = requestAnimationFrame(() => {
|
|
297
267
|
rafId = null;
|
|
298
|
-
|
|
268
|
+
post({
|
|
299
269
|
channel: CANVAS_CHANNEL,
|
|
300
270
|
v: CANVAS_PROTOCOL_VERSION,
|
|
301
271
|
type: 'patch',
|
|
@@ -306,37 +276,51 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
306
276
|
});
|
|
307
277
|
};
|
|
308
278
|
const postUi = (payload) => {
|
|
309
|
-
|
|
279
|
+
post({
|
|
310
280
|
channel: CANVAS_CHANNEL,
|
|
311
281
|
v: CANVAS_PROTOCOL_VERSION,
|
|
312
282
|
type: 'ui',
|
|
313
283
|
payload,
|
|
314
284
|
});
|
|
315
285
|
};
|
|
316
|
-
|
|
317
|
-
|
|
286
|
+
const postTrack = (selectedId, hoveredId) => {
|
|
287
|
+
post({
|
|
288
|
+
channel: CANVAS_CHANNEL,
|
|
289
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
290
|
+
type: 'track',
|
|
291
|
+
selectedId,
|
|
292
|
+
hoveredId,
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
connectionRef.current = { sendInit, requestPatch, postUi, postTrack };
|
|
296
|
+
const onPortMessage = (event) => {
|
|
318
297
|
if (!isCanvasMessage(event.data)) {
|
|
319
298
|
return;
|
|
320
299
|
}
|
|
321
300
|
const message = event.data;
|
|
322
301
|
const p = props.current;
|
|
323
302
|
switch (message.type) {
|
|
324
|
-
case '
|
|
325
|
-
if (
|
|
303
|
+
case 'hello':
|
|
304
|
+
if (message.nonce !== pendingNonce) {
|
|
326
305
|
return;
|
|
327
306
|
}
|
|
328
|
-
|
|
329
|
-
ready = true;
|
|
307
|
+
connectedNonce = message.nonce;
|
|
330
308
|
sendInit();
|
|
309
|
+
postTrack(trackRef.current.selectedId, trackRef.current.hoveredId);
|
|
331
310
|
postUi(props.current.uiPayload);
|
|
332
311
|
break;
|
|
312
|
+
case 'rects':
|
|
313
|
+
setBoxes({
|
|
314
|
+
selected: message.selected,
|
|
315
|
+
hovered: message.hovered,
|
|
316
|
+
selectedLabel: message.selectedLabel,
|
|
317
|
+
});
|
|
318
|
+
break;
|
|
333
319
|
case 'select':
|
|
334
320
|
p.onSelect?.(message.nodeId);
|
|
335
|
-
setTick((x) => x + 1);
|
|
336
321
|
break;
|
|
337
322
|
case 'hover':
|
|
338
323
|
p.onHover?.(message.nodeId);
|
|
339
|
-
setTick((x) => x + 1);
|
|
340
324
|
break;
|
|
341
325
|
case 'drop':
|
|
342
326
|
p.onDropOnNode?.(message.targetId, message.position);
|
|
@@ -349,14 +333,39 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
349
333
|
break;
|
|
350
334
|
}
|
|
351
335
|
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
336
|
+
const onWindowMessage = (event) => {
|
|
337
|
+
const frame = iframeRef.current;
|
|
338
|
+
if (!frame || event.source !== frame.contentWindow) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!isCanvasMessage(event.data) || event.data.type !== 'ready') {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const { nonce } = event.data;
|
|
345
|
+
if (connectedNonce === nonce) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
port?.close();
|
|
349
|
+
const channel = new MessageChannel();
|
|
350
|
+
port = channel.port1;
|
|
351
|
+
pendingNonce = nonce;
|
|
352
|
+
connectedNonce = null;
|
|
353
|
+
port.addEventListener('message', onPortMessage);
|
|
354
|
+
port.start();
|
|
355
|
+
event.source.postMessage({
|
|
356
|
+
channel: CANVAS_CHANNEL,
|
|
357
|
+
v: CANVAS_PROTOCOL_VERSION,
|
|
358
|
+
type: 'connect',
|
|
359
|
+
nonce,
|
|
360
|
+
}, '*', [channel.port2]);
|
|
361
|
+
};
|
|
362
|
+
window.addEventListener('message', onWindowMessage);
|
|
363
|
+
const unlistenChangeset = nu.listenToChangeset(requestPatch);
|
|
357
364
|
return () => {
|
|
358
|
-
|
|
359
|
-
|
|
365
|
+
window.removeEventListener('message', onWindowMessage);
|
|
366
|
+
port?.removeEventListener('message', onPortMessage);
|
|
367
|
+
port?.close();
|
|
368
|
+
port = null;
|
|
360
369
|
unlistenChangeset();
|
|
361
370
|
if (rafId !== null) {
|
|
362
371
|
cancelAnimationFrame(rafId);
|
|
@@ -376,26 +385,20 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
376
385
|
useEffect(() => {
|
|
377
386
|
connectionRef.current?.postUi(uiPayload);
|
|
378
387
|
}, [uiPayload]);
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
+
const trackRef = useRef({
|
|
389
|
+
selectedId: null,
|
|
390
|
+
hoveredId: null,
|
|
391
|
+
});
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
const next = previewMode
|
|
394
|
+
? { selectedId: null, hoveredId: null }
|
|
395
|
+
: { selectedId, hoveredId };
|
|
396
|
+
trackRef.current = next;
|
|
388
397
|
if (previewMode) {
|
|
389
|
-
|
|
390
|
-
setHovered(null);
|
|
391
|
-
setSelectedLabel(null);
|
|
392
|
-
return;
|
|
398
|
+
setBoxes(NO_BOXES);
|
|
393
399
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
|
|
397
|
-
setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
|
|
398
|
-
}, [previewMode, selectedId, hoveredId, revision, tick, doc]);
|
|
400
|
+
connectionRef.current?.postTrack(next.selectedId, next.hoveredId);
|
|
401
|
+
}, [previewMode, selectedId, hoveredId, revision]);
|
|
399
402
|
return createElement('div', { className: containerClassName, style: { position: 'relative' } }, createElement('iframe', {
|
|
400
403
|
ref: iframeRef,
|
|
401
404
|
title: 'nuforge canvas',
|
|
@@ -409,7 +412,7 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
|
|
|
409
412
|
pointerEvents: 'none',
|
|
410
413
|
overflow: 'hidden',
|
|
411
414
|
},
|
|
412
|
-
}, renderBox(
|
|
415
|
+
}, renderBox(boxes))
|
|
413
416
|
: null);
|
|
414
417
|
}
|
|
415
418
|
function useStructureRevision(editor) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuforge/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"react": "^18 || ^19",
|
|
31
31
|
"react-dom": "^18 || ^19",
|
|
32
32
|
"@nuforge/core": "0.1.5",
|
|
33
|
-
"@nuforge/react": "0.1.5",
|
|
34
33
|
"@nuforge/parser": "0.1.4",
|
|
34
|
+
"@nuforge/react": "0.1.5",
|
|
35
35
|
"@nuforge/types": "0.1.4"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
@@ -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/
|
|
53
|
+
"@nuforge/types": "0.1.4",
|
|
54
54
|
"@nuforge/parser": "0.1.4",
|
|
55
55
|
"@nuforge/react": "0.1.5",
|
|
56
|
-
"@nuforge/
|
|
56
|
+
"@nuforge/core": "0.1.5"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|