@jxsuite/studio 0.33.0 → 0.34.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/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +10754 -11060
- package/dist/studio.js.map +83 -72
- package/package.json +7 -7
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +2 -1
- package/src/page-params.ts +383 -0
- package/src/panels/ai-panel.ts +5 -7
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +0 -2
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +15 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +90 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +2 -74
- package/src/types.ts +14 -18
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +5 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
|
@@ -10,6 +10,19 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
|
10
10
|
const mediaTags = new Set(["img", "video", "source", "iframe", "audio"]);
|
|
11
11
|
const TRANSPARENT_PX =
|
|
12
12
|
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
13
|
+
// A neutral, visible gray placeholder for image-like bindings in edit mode (e.g. a component `image`
|
|
14
|
+
// Prop bound to ${...}). The component renders <img src={prop}> internally, so a transparent pixel
|
|
15
|
+
// Would collapse to nothing — a gray SVG box reads as an intentional placeholder instead.
|
|
16
|
+
const PLACEHOLDER_IMG =
|
|
17
|
+
"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='32'%20height='32'%3E%3Crect%20width='32'%20height='32'%20fill='%23d0d0d0'/%3E%3C/svg%3E";
|
|
18
|
+
|
|
19
|
+
// Prop/attribute names that feed an <img>/background, so a ${...} binding would otherwise render a
|
|
20
|
+
// Broken image in edit mode. Matched by exact name or common camelCase suffix (featuredImage, heroBg).
|
|
21
|
+
const IMAGE_KEY_RE =
|
|
22
|
+
/(src|poster|image|bg|background|icon|logo|photo|avatar|thumbnail|cover|banner)$/i;
|
|
23
|
+
function isImageUrlKey(key: string): boolean {
|
|
24
|
+
return IMAGE_KEY_RE.test(key);
|
|
25
|
+
}
|
|
13
26
|
|
|
14
27
|
const textTags = new Set([
|
|
15
28
|
"p",
|
|
@@ -197,8 +210,15 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
197
210
|
const propsOut: Record<string, unknown> = {};
|
|
198
211
|
for (const [pk, pv] of Object.entries(v)) {
|
|
199
212
|
if (typeof pv === "string" && pv.includes("${")) {
|
|
200
|
-
|
|
201
|
-
|
|
213
|
+
// Image-like prop → neutral placeholder (the component renders it as an <img src>); link
|
|
214
|
+
// Targets → inert ""; everything else → the readable ❪ expr ❫ binding text.
|
|
215
|
+
if (isImageUrlKey(pk)) {
|
|
216
|
+
propsOut[pk] = PLACEHOLDER_IMG;
|
|
217
|
+
} else if (pk === "href" || pk === "action") {
|
|
218
|
+
propsOut[pk] = "";
|
|
219
|
+
} else {
|
|
220
|
+
propsOut[pk] = templateToEditDisplay(pv);
|
|
221
|
+
}
|
|
202
222
|
} else if (pv && typeof pv === "object" && (pv as Record<string, unknown>).$ref) {
|
|
203
223
|
const ref = (pv as Record<string, unknown>).$ref as string;
|
|
204
224
|
const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
|
|
@@ -238,7 +258,7 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
238
258
|
border: "1px dashed color-mix(in srgb, var(--danger) 40%, transparent)",
|
|
239
259
|
borderRadius: "4px",
|
|
240
260
|
color: "var(--danger)",
|
|
241
|
-
fontFamily: "'SF Mono', 'Fira Code', monospace",
|
|
261
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
|
|
242
262
|
fontSize: "11px",
|
|
243
263
|
fontStyle: "italic",
|
|
244
264
|
padding: "6px 10px",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Geometry read funnel — the single place the studio reads element bounds and does point hit-tests.
|
|
4
|
+
*
|
|
5
|
+
* Centralizing these reads is a seam for the iframe-canvas migration: today every call delegates
|
|
6
|
+
* straight to the DOM, but routing them through one module lets later phases redirect
|
|
7
|
+
* CANVAS-element geometry to rects posted out of the iframe (which the parent can't measure
|
|
8
|
+
* directly) without touching dozens of call sites. A test (`geometry.test.ts`) asserts no raw
|
|
9
|
+
* `getBoundingClientRect`/`elementFromPoint`/`elementsFromPoint` calls exist elsewhere in `src`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Bounding rect of an element, in viewport coordinates. */
|
|
13
|
+
export function rectOf(el: Element): DOMRect {
|
|
14
|
+
return el.getBoundingClientRect();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bounding rect of a Range, in viewport coordinates. Lives here (the one allowlisted home for
|
|
19
|
+
* `getBoundingClientRect`) so the `geometry.test.ts` textual guard passes — the iframe-side
|
|
20
|
+
* selection snapshot funnels its caret/selection geometry through this, never calling
|
|
21
|
+
* `range.getBoundingClientRect()` directly.
|
|
22
|
+
*/
|
|
23
|
+
export function rectOfRange(r: Range): DOMRect {
|
|
24
|
+
return r.getBoundingClientRect();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Topmost element at a viewport point, in `root` (defaults to the global document). */
|
|
28
|
+
export function elementAtPoint(
|
|
29
|
+
x: number,
|
|
30
|
+
y: number,
|
|
31
|
+
root: Document | ShadowRoot = document,
|
|
32
|
+
): Element | null {
|
|
33
|
+
return root.elementFromPoint(x, y);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** All elements at a viewport point, front-to-back, in `root` (defaults to the global document). */
|
|
37
|
+
export function elementsAtPoint(
|
|
38
|
+
x: number,
|
|
39
|
+
y: number,
|
|
40
|
+
root: Document | ShadowRoot = document,
|
|
41
|
+
): Element[] {
|
|
42
|
+
return root.elementsFromPoint(x, y);
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link-target.ts — pure, dependency-free helpers for classifying and composing anchor `href`
|
|
3
|
+
* targets in the properties panel's Link section.
|
|
4
|
+
*
|
|
5
|
+
* `classifyHref` splits a raw href string into a discriminated `{ kind, value }` pair so the UI can
|
|
6
|
+
* offer a target-kind selector (Internal page / External URL / #anchor / mailto: / tel:).
|
|
7
|
+
* `composeHref` is its inverse — the two round-trip for every kind so editing through the UI never
|
|
8
|
+
* corrupts an href.
|
|
9
|
+
*
|
|
10
|
+
* Both functions are total (never throw) and have no DOM or platform dependencies, so they are
|
|
11
|
+
* fully unit-testable in isolation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The distinct target kinds an anchor href can address. */
|
|
15
|
+
export type LinkKind = "internal" | "external" | "anchor" | "mailto" | "tel";
|
|
16
|
+
|
|
17
|
+
/** A classified href: its {@link LinkKind} plus the "bare" value for that kind's input. */
|
|
18
|
+
export interface LinkTarget {
|
|
19
|
+
kind: LinkKind;
|
|
20
|
+
/**
|
|
21
|
+
* The user-facing value for the kind's input: - internal → the path (e.g. `/about/`) - external →
|
|
22
|
+
* the full href (e.g. `https://x.com`) - anchor → the fragment WITHOUT the leading `#` (e.g.
|
|
23
|
+
* `section`) - mailto → the email address WITHOUT the `mailto:` scheme - tel → the phone number
|
|
24
|
+
* WITHOUT the `tel:` scheme
|
|
25
|
+
*/
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Matches an absolute URL scheme like `http://`, `https://`, `ftp://`, or protocol-relative `//`. */
|
|
30
|
+
const ABSOLUTE_SCHEME = /^([a-z][a-z\d+.-]*:)?\/\//i;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Classify a raw `href` string into a {@link LinkTarget}.
|
|
34
|
+
*
|
|
35
|
+
* Rules (checked in order): 1. `mailto:…` → `{ kind: "mailto", value: address }` 2. `tel:…` → `{
|
|
36
|
+
* kind: "tel", value: number }` 3. `#…` → `{ kind: "anchor", value: rest }` (leading `#` stripped)
|
|
37
|
+
* 4. absolute (`http(s)://…`) or protocol-relative (`//…`) → `{ kind: "external", value: href }` 5.
|
|
38
|
+
* `/…` → `{ kind: "internal", value: path }` 6. anything else → `{ kind: "external", value: href
|
|
39
|
+
* }`
|
|
40
|
+
*
|
|
41
|
+
* @param href The raw attribute value (may be empty)
|
|
42
|
+
*/
|
|
43
|
+
export function classifyHref(href = ""): LinkTarget {
|
|
44
|
+
const value = href;
|
|
45
|
+
const lower = value.toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (lower.startsWith("mailto:")) {
|
|
48
|
+
return { kind: "mailto", value: value.slice("mailto:".length) };
|
|
49
|
+
}
|
|
50
|
+
if (lower.startsWith("tel:")) {
|
|
51
|
+
return { kind: "tel", value: value.slice("tel:".length) };
|
|
52
|
+
}
|
|
53
|
+
if (value.startsWith("#")) {
|
|
54
|
+
return { kind: "anchor", value: value.slice(1) };
|
|
55
|
+
}
|
|
56
|
+
if (ABSOLUTE_SCHEME.test(value)) {
|
|
57
|
+
return { kind: "external", value };
|
|
58
|
+
}
|
|
59
|
+
if (value.startsWith("/")) {
|
|
60
|
+
return { kind: "internal", value };
|
|
61
|
+
}
|
|
62
|
+
return { kind: "external", value };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compose an `href` string from a {@link LinkKind} and its bare value — the inverse of
|
|
67
|
+
* {@link classifyHref}. An empty (or whitespace-only) value yields `""` for every kind so callers
|
|
68
|
+
* can treat "cleared" uniformly. Round-trips with `classifyHref` for every kind.
|
|
69
|
+
*
|
|
70
|
+
* @param kind The target kind
|
|
71
|
+
* @param value The bare value for that kind (address/number/fragment/path/href)
|
|
72
|
+
*/
|
|
73
|
+
export function composeHref(kind: LinkKind, value: string): string {
|
|
74
|
+
const bare = (value ?? "").trim();
|
|
75
|
+
if (!bare) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
switch (kind) {
|
|
79
|
+
case "mailto": {
|
|
80
|
+
return `mailto:${bare}`;
|
|
81
|
+
}
|
|
82
|
+
case "tel": {
|
|
83
|
+
return `tel:${bare}`;
|
|
84
|
+
}
|
|
85
|
+
case "anchor": {
|
|
86
|
+
return bare.startsWith("#") ? bare : `#${bare}`;
|
|
87
|
+
}
|
|
88
|
+
// Internal / external — the bare value IS the href.
|
|
89
|
+
default: {
|
|
90
|
+
return bare;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip `on*` event-handler bindings from a Jx document tree (returns a shallow-rebuilt copy). Used
|
|
3
|
+
* to neutralize handlers before edit/design rendering so the canvas never wires live behavior, and
|
|
4
|
+
* to drop `timing: "server"` state entries the canvas can't run.
|
|
5
|
+
*
|
|
6
|
+
* Lives in its own dependency-light module (it pulls only the pure `@jxsuite/schema/guards`
|
|
7
|
+
* predicate) so the slim canvas-iframe subtree renderer can import it without dragging in the
|
|
8
|
+
* editor's reactive `store.ts`; `store.ts` re-exports it for the legacy in-realm patcher.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isEventBinding } from "@jxsuite/schema/guards";
|
|
12
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
13
|
+
|
|
14
|
+
/** Strip all `on*` event-handler properties (and server-timed state) from a document tree. */
|
|
15
|
+
export function stripEventHandlers(node: JxMutableNode): JxMutableNode {
|
|
16
|
+
if (!node || typeof node !== "object") {
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
// Arrays of nodes round-trip element-wise; the array itself is not a node.
|
|
21
|
+
return node.map((n: JxMutableNode) => stripEventHandlers(n)) as unknown as JxMutableNode;
|
|
22
|
+
}
|
|
23
|
+
const out: Record<string, unknown> = {};
|
|
24
|
+
for (const [k, v] of Object.entries(node)) {
|
|
25
|
+
if (k.startsWith("on") && isEventBinding(v)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (k === "children") {
|
|
29
|
+
out.children = Array.isArray(v)
|
|
30
|
+
? v.map((c: JxMutableNode) => stripEventHandlers(c))
|
|
31
|
+
: stripEventHandlers(v as JxMutableNode);
|
|
32
|
+
} else if (k === "cases" && typeof v === "object") {
|
|
33
|
+
const cases: Record<string, unknown> = {};
|
|
34
|
+
for (const [ck, cv] of Object.entries(v as Record<string, unknown>)) {
|
|
35
|
+
cases[ck] = stripEventHandlers(cv as JxMutableNode);
|
|
36
|
+
}
|
|
37
|
+
out.cases = cases;
|
|
38
|
+
} else if (k === "state" && typeof v === "object" && v !== null) {
|
|
39
|
+
const state: Record<string, unknown> = {};
|
|
40
|
+
for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
|
|
41
|
+
if (sv && typeof sv === "object" && (sv as Record<string, unknown>).timing === "server") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
state[sk] = sv;
|
|
45
|
+
}
|
|
46
|
+
out.state = state;
|
|
47
|
+
} else if (k === "style" || k === "attributes" || k === "$media") {
|
|
48
|
+
out[k] = v;
|
|
49
|
+
} else {
|
|
50
|
+
out[k] = v;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
package/src/view.ts
CHANGED
|
@@ -28,15 +28,6 @@ interface ViewState {
|
|
|
28
28
|
_editingTarget?: string | null;
|
|
29
29
|
})
|
|
30
30
|
| null;
|
|
31
|
-
componentInlineEdit: {
|
|
32
|
-
el: HTMLElement;
|
|
33
|
-
path: (string | number)[];
|
|
34
|
-
originalText: string;
|
|
35
|
-
mediaName: string | null;
|
|
36
|
-
_outsideHandler?: ((e: MouseEvent) => void) | null;
|
|
37
|
-
[k: string]: unknown;
|
|
38
|
-
} | null;
|
|
39
|
-
inlineEditCleanup: (() => void) | null;
|
|
40
31
|
blockActionBarEl: HTMLElement | null;
|
|
41
32
|
selDragCleanup: (() => void) | null;
|
|
42
33
|
dndCleanups: (() => void)[];
|
|
@@ -46,16 +37,9 @@ interface ViewState {
|
|
|
46
37
|
forcedAttrEl: HTMLElement | null;
|
|
47
38
|
elementsCollapsed: Set<string>;
|
|
48
39
|
elementsFilter: string;
|
|
49
|
-
lastDragInput: {
|
|
50
|
-
clientX: number;
|
|
51
|
-
clientY: number;
|
|
52
|
-
[k: string]: unknown;
|
|
53
|
-
} | null;
|
|
54
40
|
_currentDropTargetRow: HTMLElement | null;
|
|
55
41
|
layerDragSourceHeight: number;
|
|
56
|
-
savedRange: Range | null;
|
|
57
42
|
_completionRegistered: boolean;
|
|
58
|
-
stylebookElToTag: WeakMap<Element, string>;
|
|
59
43
|
showAddBreakpointForm: boolean;
|
|
60
44
|
addBreakpointPreview: string;
|
|
61
45
|
layoutSelection: unknown;
|
|
@@ -81,10 +65,6 @@ export const view: ViewState = {
|
|
|
81
65
|
monacoEditor: null,
|
|
82
66
|
functionEditor: null,
|
|
83
67
|
|
|
84
|
-
// Inline editing
|
|
85
|
-
componentInlineEdit: null,
|
|
86
|
-
inlineEditCleanup: null,
|
|
87
|
-
|
|
88
68
|
// Floating UI containers
|
|
89
69
|
blockActionBarEl: null,
|
|
90
70
|
|
|
@@ -105,16 +85,13 @@ export const view: ViewState = {
|
|
|
105
85
|
elementsFilter: "",
|
|
106
86
|
|
|
107
87
|
// Drag interaction
|
|
108
|
-
lastDragInput: null,
|
|
109
88
|
_currentDropTargetRow: null,
|
|
110
89
|
layerDragSourceHeight: 0,
|
|
111
90
|
|
|
112
91
|
// Editor state
|
|
113
|
-
savedRange: null,
|
|
114
92
|
_completionRegistered: false,
|
|
115
93
|
|
|
116
94
|
// Canvas / stylebook
|
|
117
|
-
stylebookElToTag: new WeakMap(),
|
|
118
95
|
|
|
119
96
|
// Responsive breakpoints UI
|
|
120
97
|
showAddBreakpointForm: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, reactive } from "../reactivity";
|
|
1
|
+
import { computed, reactive, toRaw } from "../reactivity";
|
|
2
2
|
import { createTab, disposeTab } from "../tabs/tab";
|
|
3
3
|
import type { Tab } from "../tabs/tab";
|
|
4
4
|
|
|
@@ -56,6 +56,19 @@ export const activeTab = computed(() =>
|
|
|
56
56
|
workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
|
|
57
57
|
) as unknown as ComputedRef<Tab | null>;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Whether `tab` is the active tab. Identity check survives reactive-proxy wrapping (activeTab.value
|
|
61
|
+
* is a proxy; callers may hold either the raw tab or a proxy of it).
|
|
62
|
+
*
|
|
63
|
+
* @param {Tab | null} tab
|
|
64
|
+
*/
|
|
65
|
+
export function isTabActive(tab: Tab | null): boolean {
|
|
66
|
+
const active = activeTab.value;
|
|
67
|
+
return (
|
|
68
|
+
tab !== null && active !== null && toRaw(active as object) === toRaw(tab as unknown as object)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
/**
|
|
60
73
|
* Open a new tab and make it active.
|
|
61
74
|
*
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
/// <reference lib="dom.iterable" />
|
|
3
|
-
/**
|
|
4
|
-
* Isolated subtree rendering for surgical canvas patches. Re-renders a single document node into
|
|
5
|
-
* fresh DOM using the same edit-mode transform, runtime renderer, scope, and path mapper as the
|
|
6
|
-
* panel's last full render — so the patched DOM is indistinguishable from a full re-render.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { elToRenderScope, elToScope, getNodeAtPath, stripEventHandlers } from "../store";
|
|
10
|
-
import { prepareForEditMode } from "../utils/edit-display";
|
|
11
|
-
import { renderNode as runtimeRenderNode } from "@jxsuite/runtime";
|
|
12
|
-
import { effectScope } from "../reactivity";
|
|
13
|
-
import { canvasPerf } from "./canvas-perf";
|
|
14
|
-
|
|
15
|
-
import type { JxPath } from "../state";
|
|
16
|
-
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
17
|
-
import type { CanvasPanel, PanelLiveCtx } from "../types";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Render the document node at docPath into a detached DOM subtree for the given panel.
|
|
21
|
-
*
|
|
22
|
-
* The scope comes from the parent element's recorded render scope (elToScope), falling back to the
|
|
23
|
-
* panel's root scope — so $-bindings, $media, and state references resolve exactly as they did in
|
|
24
|
-
* the full render. Throws when the panel has no live render context.
|
|
25
|
-
*
|
|
26
|
-
* @param {CanvasPanel} panel
|
|
27
|
-
* @param {JxMutableNode} doc Raw (non-reactive) document root
|
|
28
|
-
* @param {JxPath} docPath
|
|
29
|
-
* @param {Element} parentEl The rendered parent element (scope source)
|
|
30
|
-
*/
|
|
31
|
-
export function renderSubtree(
|
|
32
|
-
panel: CanvasPanel,
|
|
33
|
-
doc: JxMutableNode,
|
|
34
|
-
docPath: JxPath,
|
|
35
|
-
parentEl: Element,
|
|
36
|
-
): HTMLElement | Text {
|
|
37
|
-
const { liveCtx } = panel;
|
|
38
|
-
if (!liveCtx) {
|
|
39
|
-
throw new Error("panel-missing-live-ctx");
|
|
40
|
-
}
|
|
41
|
-
const node = getNodeAtPath(doc, docPath) as JxMutableNode | string | undefined;
|
|
42
|
-
if (node === undefined) {
|
|
43
|
-
throw new Error(`node-not-found:${docPath.join("/")}`);
|
|
44
|
-
}
|
|
45
|
-
const def = typeof node === "string" ? node : prepareForEditMode(stripEventHandlers(node));
|
|
46
|
-
const scope = elToScope.get(parentEl) ?? liveCtx.scope;
|
|
47
|
-
|
|
48
|
-
// Render inside an effect scope parented to the panel's render scope: disposable on its own
|
|
49
|
-
// When the subtree is later replaced/removed, and disposed with the panel on full renders.
|
|
50
|
-
const render = () => {
|
|
51
|
-
const subScope = effectScope();
|
|
52
|
-
const rendered = subScope.run(() =>
|
|
53
|
-
runtimeRenderNode(def, scope, {
|
|
54
|
-
_path: docPathToRenderPath(docPath, liveCtx),
|
|
55
|
-
onNodeCreated: liveCtx.pathMapper,
|
|
56
|
-
}),
|
|
57
|
-
)!;
|
|
58
|
-
if (rendered instanceof HTMLElement) {
|
|
59
|
-
elToRenderScope.set(rendered, subScope);
|
|
60
|
-
}
|
|
61
|
-
return rendered;
|
|
62
|
-
};
|
|
63
|
-
const el = panel.renderScope ? panel.renderScope.run(render)! : render();
|
|
64
|
-
canvasPerf.subtreeRenders += 1;
|
|
65
|
-
|
|
66
|
-
// Match full-render edit display: canvas content never receives pointer events directly.
|
|
67
|
-
if (el instanceof HTMLElement) {
|
|
68
|
-
el.style.pointerEvents = "none";
|
|
69
|
-
for (const child of el.querySelectorAll("*")) {
|
|
70
|
-
(child as HTMLElement).style.pointerEvents = "none";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return el;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Inverse of makePathMapper's layout-prefix strip: document paths are relative to the page doc,
|
|
78
|
-
* render paths to the layout-merged doc. ($map/$switch paths never reach here — the patcher
|
|
79
|
-
* escalates them.)
|
|
80
|
-
*
|
|
81
|
-
* @param {JxPath} docPath
|
|
82
|
-
* @param {PanelLiveCtx} liveCtx
|
|
83
|
-
*/
|
|
84
|
-
function docPathToRenderPath(docPath: JxPath, liveCtx: PanelLiveCtx): (string | number)[] {
|
|
85
|
-
if (liveCtx.layoutWrapped && liveCtx.pageContentPrefix && docPath[0] === "children") {
|
|
86
|
-
// Re-apply the slot-container offset stripped by makePathMapper: page child index N renders at
|
|
87
|
-
// Container index N + offset (offset = leading layout siblings before the <slot>).
|
|
88
|
-
const [, idx] = docPath;
|
|
89
|
-
return typeof idx === "number"
|
|
90
|
-
? [...liveCtx.pageContentPrefix, idx + (liveCtx.pageContentOffset ?? 0), ...docPath.slice(2)]
|
|
91
|
-
: [...liveCtx.pageContentPrefix, ...docPath.slice(1)];
|
|
92
|
-
}
|
|
93
|
-
return docPath;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The DOM node currently occupying children[index] of the rendered parent, or null when appending
|
|
98
|
-
* at the end. The runtime renders an optional leading text node (from textContent) followed by one
|
|
99
|
-
* DOM node per children entry, in order.
|
|
100
|
-
*
|
|
101
|
-
* @param {Element} parentEl
|
|
102
|
-
* @param {JxMutableNode} parentNode
|
|
103
|
-
* @param {number} index
|
|
104
|
-
*/
|
|
105
|
-
export function domChildReference(
|
|
106
|
-
parentEl: Element,
|
|
107
|
-
parentNode: JxMutableNode,
|
|
108
|
-
index: number,
|
|
109
|
-
): ChildNode | null {
|
|
110
|
-
const text = parentNode.textContent;
|
|
111
|
-
const textOffset = text != null && String(text) !== "" ? 1 : 0;
|
|
112
|
-
return parentEl.childNodes[textOffset + index] ?? null;
|
|
113
|
-
}
|