@jxsuite/studio 0.10.2 → 0.11.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/README.md +82 -0
- package/dist/studio.js +1001 -147
- package/dist/studio.js.map +28 -18
- package/package.json +3 -2
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-live-render.js +133 -15
- package/src/canvas/canvas-render.js +2 -1
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/overlays.js +15 -3
- package/src/panels/panel-events.js +10 -0
- package/src/panels/properties-panel.js +299 -5
- package/src/panels/stylebook-panel.js +31 -2
- package/src/settings/content-types-editor.js +22 -1
- package/src/settings/schema-field-ui.js +57 -4
- package/src/site-context.js +105 -0
- package/src/store.js +9 -1
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.10.
|
|
33
|
+
"@jxsuite/runtime": "^0.10.2",
|
|
34
34
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
35
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
36
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"@spectrum-web-components/tabs": "^1.12.1",
|
|
59
59
|
"@spectrum-web-components/textfield": "^1.12.1",
|
|
60
60
|
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
|
+
"@spectrum-web-components/toast": "^1.12.1",
|
|
61
62
|
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
62
63
|
"lit-html": "^3.3.3",
|
|
63
64
|
"monaco-editor": "^0.55.1",
|
package/src/browse/browse.js
CHANGED
|
@@ -95,11 +95,14 @@ async function collectFiles(dir, platform) {
|
|
|
95
95
|
results.push(...sub);
|
|
96
96
|
} else {
|
|
97
97
|
const ext = extOf(entry.name);
|
|
98
|
+
const category = categoryFor(entry.path, ext);
|
|
99
|
+
const type =
|
|
100
|
+
category === "Content" ? contentTypeFor(entry.path) || ext || "file" : ext || "file";
|
|
98
101
|
results.push({
|
|
99
102
|
name: entry.name,
|
|
100
103
|
path: entry.path,
|
|
101
|
-
type
|
|
102
|
-
category
|
|
104
|
+
type,
|
|
105
|
+
category,
|
|
103
106
|
ext,
|
|
104
107
|
});
|
|
105
108
|
}
|
|
@@ -110,6 +113,27 @@ async function collectFiles(dir, platform) {
|
|
|
110
113
|
return results;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Match a file path against project contentTypes source globs to find its content type name.
|
|
118
|
+
* Returns the content type name (capitalized) or null if no match.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} filePath
|
|
121
|
+
* @returns {string | null}
|
|
122
|
+
*/
|
|
123
|
+
function contentTypeFor(filePath) {
|
|
124
|
+
const config = projectState?.projectConfig;
|
|
125
|
+
if (!config?.contentTypes) return null;
|
|
126
|
+
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
127
|
+
const d = /** @type {any} */ (def);
|
|
128
|
+
if (!d.source) continue;
|
|
129
|
+
const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
|
|
130
|
+
if (filePath.startsWith(prefix + "/") || filePath === prefix) {
|
|
131
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
// ─── Data loading ────────────────────────────────────────────────────────────
|
|
114
138
|
|
|
115
139
|
async function loadFiles() {
|
|
@@ -376,7 +400,7 @@ export async function renderBrowse(container, ctx) {
|
|
|
376
400
|
>
|
|
377
401
|
<sp-table-cell class="browse-name-cell">${f.name}</sp-table-cell>
|
|
378
402
|
<sp-table-cell>${f.category}</sp-table-cell>
|
|
379
|
-
<sp-table-cell>${f.
|
|
403
|
+
<sp-table-cell>${f.type}</sp-table-cell>
|
|
380
404
|
<sp-table-cell class="browse-path-cell">${f.path}</sp-table-cell>
|
|
381
405
|
</sp-table-row>
|
|
382
406
|
`,
|
|
@@ -6,12 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { elToPath, stripEventHandlers, projectState } from "../store.js";
|
|
8
8
|
import { view } from "../view.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
renderNode as runtimeRenderNode,
|
|
11
|
+
buildScope,
|
|
12
|
+
defineElement,
|
|
13
|
+
setSkipServerFunctions,
|
|
14
|
+
} from "@jxsuite/runtime";
|
|
10
15
|
import {
|
|
11
16
|
getEffectiveElements,
|
|
12
17
|
getEffectiveImports,
|
|
13
18
|
getEffectiveMedia,
|
|
14
19
|
getEffectiveHead,
|
|
20
|
+
getEffectiveLayoutPath,
|
|
21
|
+
resolveLayoutDoc,
|
|
22
|
+
distributePageIntoLayout,
|
|
15
23
|
} from "../site-context.js";
|
|
16
24
|
import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
17
25
|
import { prepareForEditMode } from "../utils/edit-display.js";
|
|
@@ -20,6 +28,56 @@ import { getActiveElement } from "../editor/inline-edit.js";
|
|
|
20
28
|
/** @type {any} */
|
|
21
29
|
let _ctx = null;
|
|
22
30
|
|
|
31
|
+
/** Set of DOM elements that originated from the layout (not page content). */
|
|
32
|
+
export const layoutElements = new WeakSet();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk the merged document tree to find the path prefix where page children were distributed into
|
|
36
|
+
* the layout slot. Returns the path to the container whose children are the page content (first
|
|
37
|
+
* non-$__layout children array).
|
|
38
|
+
*
|
|
39
|
+
* @param {any} node
|
|
40
|
+
* @param {any[]} [path]
|
|
41
|
+
* @returns {any[] | null}
|
|
42
|
+
*/
|
|
43
|
+
function findPageContentPrefix(node, path = []) {
|
|
44
|
+
if (!node || typeof node !== "object") return null;
|
|
45
|
+
if (Array.isArray(node.children)) {
|
|
46
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
47
|
+
const child = node.children[i];
|
|
48
|
+
if (child && typeof child === "object" && !child.$__layout) {
|
|
49
|
+
return [...path, "children"];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
53
|
+
const child = node.children[i];
|
|
54
|
+
if (child && typeof child === "object" && child.$__layout) {
|
|
55
|
+
const found = findPageContentPrefix(child, [...path, "children", i]);
|
|
56
|
+
if (found) return found;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The path of the currently active layout file, or null. */
|
|
64
|
+
export let activeLayoutPath = /** @type {string | null} */ (null);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recursively mark all nodes in a layout doc tree with $__layout: true so we can identify which
|
|
68
|
+
* rendered DOM elements came from the layout vs page content.
|
|
69
|
+
*/
|
|
70
|
+
function markLayoutNodes(/** @type {any} */ node) {
|
|
71
|
+
if (!node || typeof node !== "object") return;
|
|
72
|
+
node.$__layout = true;
|
|
73
|
+
if (Array.isArray(node.children)) {
|
|
74
|
+
for (const child of node.children) markLayoutNodes(child);
|
|
75
|
+
}
|
|
76
|
+
if (node.$elements) {
|
|
77
|
+
for (const el of node.$elements) markLayoutNodes(el);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
23
81
|
/**
|
|
24
82
|
* Initialize the canvas live render module.
|
|
25
83
|
*
|
|
@@ -45,9 +103,46 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
45
103
|
const S = _ctx.getState();
|
|
46
104
|
const canvasMode = _ctx.getCanvasMode();
|
|
47
105
|
|
|
48
|
-
|
|
106
|
+
// Suppress server function resolution in non-preview modes to avoid
|
|
107
|
+
// failed proxy calls and infinite reactive retries (also covers
|
|
108
|
+
// async custom element connectedCallbacks that run after this function returns)
|
|
109
|
+
setSkipServerFunctions(canvasMode !== "preview");
|
|
110
|
+
|
|
111
|
+
let renderDoc =
|
|
49
112
|
canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
|
|
50
113
|
|
|
114
|
+
// ─── Layout wrapping ────────────────────────────────────────────────────
|
|
115
|
+
// For page documents, resolve the layout and wrap content in the layout shell.
|
|
116
|
+
// Layout-originated nodes are marked with $__layout so we can distinguish them.
|
|
117
|
+
let layoutWrapped = false;
|
|
118
|
+
activeLayoutPath = null;
|
|
119
|
+
|
|
120
|
+
const isPage =
|
|
121
|
+
S.documentPath &&
|
|
122
|
+
projectState?.isSiteProject &&
|
|
123
|
+
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/"));
|
|
124
|
+
|
|
125
|
+
/** @type {any[] | null} Path prefix in merged doc where page children live */
|
|
126
|
+
let pageContentPrefix = null;
|
|
127
|
+
|
|
128
|
+
if (isPage) {
|
|
129
|
+
const layoutPath = getEffectiveLayoutPath(doc.$layout);
|
|
130
|
+
if (layoutPath) {
|
|
131
|
+
const layoutDoc = await resolveLayoutDoc(layoutPath);
|
|
132
|
+
if (layoutDoc) {
|
|
133
|
+
if (gen !== view.renderGeneration) return null;
|
|
134
|
+
activeLayoutPath = layoutPath.replace(/^\.\//, "");
|
|
135
|
+
markLayoutNodes(layoutDoc);
|
|
136
|
+
const pageForSlots = canvasMode === "preview" ? structuredClone(doc) : renderDoc;
|
|
137
|
+
const merged = distributePageIntoLayout(layoutDoc, pageForSlots);
|
|
138
|
+
renderDoc =
|
|
139
|
+
canvasMode === "preview" ? merged : prepareForEditMode(stripEventHandlers(merged));
|
|
140
|
+
layoutWrapped = true;
|
|
141
|
+
pageContentPrefix = findPageContentPrefix(merged);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
51
146
|
// In edit mode, collect paths where $map templates were inlined as children[0]
|
|
52
147
|
// so we can remap runtime paths (children,0,...) → (children,map,...)
|
|
53
148
|
const mapParentPaths = new Set();
|
|
@@ -224,26 +319,49 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
224
319
|
if (gen !== view.renderGeneration) return null;
|
|
225
320
|
const el = /** @type {HTMLElement} */ (
|
|
226
321
|
runtimeRenderNode(renderDoc, $defs, {
|
|
227
|
-
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
322
|
+
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path, /** @type {any} */ def) {
|
|
323
|
+
// Track layout-originated elements — don't store in elToPath to avoid
|
|
324
|
+
// path collisions with remapped page content paths
|
|
325
|
+
if (layoutWrapped && def?.$__layout) {
|
|
326
|
+
layoutElements.add(el);
|
|
327
|
+
if (el.setAttribute) el.setAttribute("data-jx-layout", "");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Remap layout-wrapped paths: strip the layout prefix so paths are
|
|
332
|
+
// relative to the original page document (which is what S.document holds)
|
|
333
|
+
let mappedPath = path;
|
|
334
|
+
if (layoutWrapped && pageContentPrefix) {
|
|
335
|
+
const pfx = pageContentPrefix;
|
|
336
|
+
if (
|
|
337
|
+
path.length >= pfx.length &&
|
|
338
|
+
pfx.every((/** @type {any} */ seg, /** @type {number} */ i) => path[i] === seg)
|
|
339
|
+
) {
|
|
340
|
+
mappedPath = ["children", ...path.slice(pfx.length)];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
228
344
|
// Remap $map paths: wrapper and template children → real document paths
|
|
229
345
|
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
230
346
|
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
231
|
-
let mappedPath = path;
|
|
232
347
|
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
233
|
-
for (let i = 0; i <
|
|
234
|
-
if (
|
|
235
|
-
const parentKey =
|
|
348
|
+
for (let i = 0; i < mappedPath.length - 1; i++) {
|
|
349
|
+
if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
|
|
350
|
+
const parentKey = mappedPath.slice(0, i).join("/");
|
|
236
351
|
if (mapParentPaths.has(parentKey)) {
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
mappedPath = path.slice(0, i + 1);
|
|
352
|
+
if (mappedPath.length === i + 2) {
|
|
353
|
+
mappedPath = mappedPath.slice(0, i + 1);
|
|
240
354
|
} else if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
355
|
+
mappedPath.length >= i + 4 &&
|
|
356
|
+
mappedPath[i + 2] === "children" &&
|
|
357
|
+
mappedPath[i + 3] === 0
|
|
244
358
|
) {
|
|
245
|
-
|
|
246
|
-
|
|
359
|
+
mappedPath = [
|
|
360
|
+
...mappedPath.slice(0, i),
|
|
361
|
+
"children",
|
|
362
|
+
"map",
|
|
363
|
+
...mappedPath.slice(i + 4),
|
|
364
|
+
];
|
|
247
365
|
}
|
|
248
366
|
break;
|
|
249
367
|
}
|
|
@@ -249,10 +249,11 @@ export function renderCanvas() {
|
|
|
249
249
|
resetZoomIndicator();
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
252
253
|
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
253
254
|
const editTpl = html`
|
|
254
255
|
<div class="content-edit-canvas">
|
|
255
|
-
<div class="content-edit-column">${panelTpl}</div>
|
|
256
|
+
<div class="content-edit-column" style="max-width:${baseWidth}px">${panelTpl}</div>
|
|
256
257
|
</div>
|
|
257
258
|
`;
|
|
258
259
|
litRender(editTpl, canvasWrap);
|
package/src/panels/canvas-dnd.js
CHANGED
|
@@ -20,19 +20,39 @@ import { view } from "../view.js";
|
|
|
20
20
|
import { applyDropInstruction } from "../panels/dnd.js";
|
|
21
21
|
import { effectiveZoom } from "../canvas/canvas-helpers.js";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* canvas: HTMLElement;
|
|
26
|
+
* overlayClk: HTMLElement;
|
|
27
|
+
* overlay: HTMLElement;
|
|
28
|
+
* viewport: HTMLElement;
|
|
29
|
+
* dropLine: HTMLElement;
|
|
30
|
+
* }} CanvasPanel
|
|
31
|
+
*
|
|
32
|
+
* @typedef {(string | number)[]} JxPath
|
|
33
|
+
*
|
|
34
|
+
* @typedef {{ type: "reorder-above" | "reorder-below" | "make-child" }} DropInstruction
|
|
35
|
+
*
|
|
36
|
+
* @typedef {{ instruction: DropInstruction; referenceEl: HTMLElement; targetPath: JxPath }} DropResult
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** @type {HTMLElement | null} */
|
|
40
|
+
let _activeDropEl = null;
|
|
41
|
+
|
|
23
42
|
/**
|
|
24
43
|
* Register all canvas elements in a panel as DnD drop targets.
|
|
25
44
|
*
|
|
26
|
-
* @param {
|
|
45
|
+
* @param {CanvasPanel} panel
|
|
27
46
|
*/
|
|
28
47
|
export function registerPanelDnD(panel) {
|
|
29
48
|
const { canvas, dropLine } = panel;
|
|
30
49
|
const allEls = canvas.querySelectorAll("*");
|
|
31
50
|
|
|
32
51
|
const monitorCleanup = monitorForElements({
|
|
33
|
-
onDragStart() {
|
|
52
|
+
onDragStart({ location }) {
|
|
53
|
+
view.lastDragInput = location.current.input;
|
|
34
54
|
for (const el of canvas.querySelectorAll("*")) {
|
|
35
|
-
/** @type {
|
|
55
|
+
/** @type {HTMLElement} */ (el).style.pointerEvents = "auto";
|
|
36
56
|
}
|
|
37
57
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
|
|
38
58
|
},
|
|
@@ -40,10 +60,12 @@ export function registerPanelDnD(panel) {
|
|
|
40
60
|
view.lastDragInput = location.current.input;
|
|
41
61
|
},
|
|
42
62
|
onDrop() {
|
|
63
|
+
_activeDropEl?.classList.remove("canvas-drop-target");
|
|
64
|
+
_activeDropEl = null;
|
|
43
65
|
for (const p of canvasPanels) p.dropLine.style.display = "none";
|
|
44
66
|
view.lastDragInput = null;
|
|
45
67
|
for (const el of canvas.querySelectorAll("*")) {
|
|
46
|
-
/** @type {
|
|
68
|
+
/** @type {HTMLElement} */ (el).style.pointerEvents = "none";
|
|
47
69
|
}
|
|
48
70
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
|
|
49
71
|
},
|
|
@@ -56,34 +78,45 @@ export function registerPanelDnD(panel) {
|
|
|
56
78
|
if (!elPath) continue;
|
|
57
79
|
|
|
58
80
|
const node = getNodeAtPath(S.document, elPath);
|
|
59
|
-
const
|
|
81
|
+
const tag = (node?.tagName || "div").toLowerCase();
|
|
82
|
+
const hasElementChildren = node?.children?.some(
|
|
83
|
+
(/** @type {unknown} */ c) => c != null && typeof c === "object",
|
|
84
|
+
);
|
|
85
|
+
const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
|
|
60
86
|
|
|
61
87
|
const cleanup = dropTargetForElements({
|
|
62
|
-
element: el,
|
|
88
|
+
element: /** @type {HTMLElement} */ (el),
|
|
63
89
|
canDrop({ source }) {
|
|
64
|
-
const srcPath = source.data.path;
|
|
65
|
-
if (srcPath && isAncestor(
|
|
90
|
+
const srcPath = /** @type {JxPath | undefined} */ (source.data.path);
|
|
91
|
+
if (srcPath && isAncestor(srcPath, elPath)) return false;
|
|
66
92
|
return true;
|
|
67
93
|
},
|
|
68
94
|
getData() {
|
|
69
|
-
return { path: elPath, _isVoid:
|
|
70
|
-
},
|
|
71
|
-
onDragEnter() {
|
|
72
|
-
showCanvasDropIndicator(el, elPath, isVoid, panel);
|
|
95
|
+
return { path: elPath, _isVoid: isLeaf };
|
|
73
96
|
},
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
onDragEnter({ location }) {
|
|
98
|
+
view.lastDragInput = location.current.input;
|
|
99
|
+
if (_activeDropEl && _activeDropEl !== el) {
|
|
100
|
+
_activeDropEl.classList.remove("canvas-drop-target");
|
|
101
|
+
}
|
|
102
|
+
_activeDropEl = /** @type {HTMLElement} */ (el);
|
|
103
|
+
showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
|
|
76
104
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
el
|
|
105
|
+
onDrag({ location }) {
|
|
106
|
+
view.lastDragInput = location.current.input;
|
|
107
|
+
showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
|
|
80
108
|
},
|
|
109
|
+
onDragLeave() {},
|
|
81
110
|
onDrop({ source }) {
|
|
82
111
|
dropLine.style.display = "none";
|
|
83
|
-
el.classList.remove("canvas-drop-target");
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
/** @type {HTMLElement} */ (el).classList.remove("canvas-drop-target");
|
|
113
|
+
_activeDropEl = null;
|
|
114
|
+
const { instruction, targetPath } = getCanvasDropResult(
|
|
115
|
+
/** @type {HTMLElement} */ (el),
|
|
116
|
+
elPath,
|
|
117
|
+
isLeaf,
|
|
118
|
+
);
|
|
119
|
+
applyDropInstruction(instruction, source.data, targetPath);
|
|
87
120
|
},
|
|
88
121
|
});
|
|
89
122
|
view.canvasDndCleanups.push(cleanup);
|
|
@@ -91,49 +124,98 @@ export function registerPanelDnD(panel) {
|
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
/**
|
|
94
|
-
* @param {
|
|
95
|
-
* @param {
|
|
96
|
-
* @param {
|
|
127
|
+
* @param {HTMLElement} el
|
|
128
|
+
* @param {JxPath} elPath
|
|
129
|
+
* @param {boolean} isLeaf
|
|
130
|
+
* @returns {DropResult}
|
|
97
131
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
function getCanvasDropResult(el, elPath, isLeaf) {
|
|
133
|
+
if (!view.lastDragInput)
|
|
134
|
+
return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
|
|
101
135
|
const y = view.lastDragInput.clientY;
|
|
136
|
+
|
|
137
|
+
if (elPath.length === 0) {
|
|
138
|
+
const children = /** @type {HTMLElement[]} */ (Array.from(el.children));
|
|
139
|
+
if (children.length === 0)
|
|
140
|
+
return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
|
|
141
|
+
return nearestChildEdge(children, y, elPath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rect = el.getBoundingClientRect();
|
|
102
145
|
const relY = (y - rect.top) / rect.height;
|
|
103
146
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
if (isLeaf) {
|
|
148
|
+
const instruction =
|
|
149
|
+
relY < 0.5
|
|
150
|
+
? { type: /** @type {const} */ ("reorder-above") }
|
|
151
|
+
: { type: /** @type {const} */ ("reorder-below") };
|
|
152
|
+
return { instruction, referenceEl: el, targetPath: elPath };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (relY < 0.25)
|
|
156
|
+
return { instruction: { type: "reorder-above" }, referenceEl: el, targetPath: elPath };
|
|
157
|
+
if (relY > 0.75)
|
|
158
|
+
return { instruction: { type: "reorder-below" }, referenceEl: el, targetPath: elPath };
|
|
159
|
+
return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
|
|
109
160
|
}
|
|
110
161
|
|
|
111
162
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* @param {
|
|
163
|
+
* Find the nearest child edge to the cursor and return the appropriate instruction along with the
|
|
164
|
+
* reference child element and its path.
|
|
165
|
+
*
|
|
166
|
+
* @param {HTMLElement[]} children
|
|
167
|
+
* @param {number} cursorY
|
|
168
|
+
* @param {JxPath} parentPath
|
|
169
|
+
* @returns {DropResult}
|
|
116
170
|
*/
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
function nearestChildEdge(children, cursorY, parentPath) {
|
|
172
|
+
let closestDist = Infinity;
|
|
173
|
+
let instruction = /** @type {DropInstruction} */ ({ type: "reorder-below" });
|
|
174
|
+
let closestIdx = children.length - 1;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < children.length; i++) {
|
|
177
|
+
const rect = children[i].getBoundingClientRect();
|
|
178
|
+
const topDist = Math.abs(cursorY - rect.top);
|
|
179
|
+
const bottomDist = Math.abs(cursorY - rect.bottom);
|
|
180
|
+
|
|
181
|
+
if (topDist < closestDist) {
|
|
182
|
+
closestDist = topDist;
|
|
183
|
+
instruction = { type: "reorder-above" };
|
|
184
|
+
closestIdx = i;
|
|
185
|
+
}
|
|
186
|
+
if (bottomDist < closestDist) {
|
|
187
|
+
closestDist = bottomDist;
|
|
188
|
+
instruction = { type: "reorder-below" };
|
|
189
|
+
closestIdx = i;
|
|
190
|
+
}
|
|
123
191
|
}
|
|
124
192
|
|
|
193
|
+
const childPath = [...parentPath, "children", closestIdx];
|
|
194
|
+
return { instruction, referenceEl: children[closestIdx], targetPath: childPath };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {HTMLElement} el
|
|
199
|
+
* @param {JxPath} elPath
|
|
200
|
+
* @param {boolean} isLeaf
|
|
201
|
+
* @param {CanvasPanel} panel
|
|
202
|
+
*/
|
|
203
|
+
function showCanvasDropIndicator(el, elPath, isLeaf, panel) {
|
|
204
|
+
const { instruction, referenceEl } = getCanvasDropResult(el, elPath, isLeaf);
|
|
205
|
+
const { dropLine, viewport } = panel;
|
|
206
|
+
|
|
125
207
|
const scale = effectiveZoom();
|
|
126
208
|
const wrapRect = viewport.getBoundingClientRect();
|
|
127
|
-
const
|
|
128
|
-
const left = (
|
|
129
|
-
const width =
|
|
209
|
+
const refRect = referenceEl.getBoundingClientRect();
|
|
210
|
+
const left = (refRect.left - wrapRect.left + viewport.scrollLeft) / scale;
|
|
211
|
+
const width = refRect.width / scale;
|
|
130
212
|
|
|
131
213
|
if (instruction.type === "make-child") {
|
|
132
214
|
dropLine.style.display = "block";
|
|
133
|
-
dropLine.style.top = `${(
|
|
215
|
+
dropLine.style.top = `${(refRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
|
|
134
216
|
dropLine.style.left = `${left}px`;
|
|
135
217
|
dropLine.style.width = `${width}px`;
|
|
136
|
-
dropLine.style.height = `${
|
|
218
|
+
dropLine.style.height = `${refRect.height / scale}px`;
|
|
137
219
|
dropLine.className = "canvas-drop-indicator inside";
|
|
138
220
|
el.classList.add("canvas-drop-target");
|
|
139
221
|
return;
|
|
@@ -142,13 +224,13 @@ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
|
|
|
142
224
|
el.classList.remove("canvas-drop-target");
|
|
143
225
|
const top =
|
|
144
226
|
instruction.type === "reorder-above"
|
|
145
|
-
? (
|
|
146
|
-
: (
|
|
227
|
+
? (refRect.top - wrapRect.top + viewport.scrollTop) / scale
|
|
228
|
+
: (refRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
|
|
147
229
|
|
|
148
230
|
dropLine.style.display = "block";
|
|
149
231
|
dropLine.style.top = `${top}px`;
|
|
150
232
|
dropLine.style.left = `${left}px`;
|
|
151
233
|
dropLine.style.width = `${width}px`;
|
|
152
|
-
dropLine.style.height = "
|
|
234
|
+
dropLine.style.height = "";
|
|
153
235
|
dropLine.className = "canvas-drop-indicator line";
|
|
154
236
|
}
|
package/src/panels/overlays.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getActivePanel,
|
|
12
12
|
overlayBoxDescriptor,
|
|
13
13
|
} from "../canvas/canvas-helpers.js";
|
|
14
|
+
import { layoutElements } from "../canvas/canvas-live-render.js";
|
|
14
15
|
|
|
15
16
|
/** @type {any} */
|
|
16
17
|
let _ctx = null;
|
|
@@ -85,7 +86,11 @@ export function render() {
|
|
|
85
86
|
|
|
86
87
|
if (S.hover && !pathsEqual(S.hover, S.selection)) {
|
|
87
88
|
const el = findCanvasElement(S.hover, p.canvas);
|
|
88
|
-
if (el)
|
|
89
|
+
if (el) {
|
|
90
|
+
const desc = overlayBoxDescriptor(el, "hover", p);
|
|
91
|
+
if (layoutElements.has(el)) /** @type {any} */ (desc).isLayout = true;
|
|
92
|
+
boxes.push(desc);
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
if (S.selection && p === getActivePanel()) {
|
|
@@ -93,6 +98,7 @@ export function render() {
|
|
|
93
98
|
if (el) {
|
|
94
99
|
const desc = overlayBoxDescriptor(el, "selection", p);
|
|
95
100
|
if (view.componentInlineEdit || _ctx.isEditing()) /** @type {any} */ (desc).border = "none";
|
|
101
|
+
if (layoutElements.has(el)) /** @type {any} */ (desc).isLayout = true;
|
|
96
102
|
boxes.push(desc);
|
|
97
103
|
}
|
|
98
104
|
}
|
|
@@ -103,11 +109,17 @@ export function render() {
|
|
|
103
109
|
${boxes.map(
|
|
104
110
|
(b) => html`
|
|
105
111
|
<div
|
|
106
|
-
class
|
|
112
|
+
class="${b.cls}${/** @type {any} */ (b).isLayout ? " overlay-layout" : ""}"
|
|
107
113
|
style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}${b.border
|
|
108
114
|
? `;border:${b.border}`
|
|
109
115
|
: ""}"
|
|
110
|
-
|
|
116
|
+
>
|
|
117
|
+
${
|
|
118
|
+
/** @type {any} */ (b).isLayout
|
|
119
|
+
? html`<span class="overlay-layout-badge">Layout</span>`
|
|
120
|
+
: nothing
|
|
121
|
+
}
|
|
122
|
+
</div>
|
|
111
123
|
`,
|
|
112
124
|
)}
|
|
113
125
|
`,
|
|
@@ -23,6 +23,7 @@ import { showContextMenu } from "../editor/context-menu.js";
|
|
|
23
23
|
import * as insertionHelper from "../editor/insertion-helper.js";
|
|
24
24
|
import { defaultDef } from "../panels/shared.js";
|
|
25
25
|
import { bubbleInlinePath, findCanvasElement, effectiveZoom } from "../canvas/canvas-helpers.js";
|
|
26
|
+
import { layoutElements, activeLayoutPath } from "../canvas/canvas-live-render.js";
|
|
26
27
|
|
|
27
28
|
/** @type {any} */
|
|
28
29
|
let _ctx = null;
|
|
@@ -87,6 +88,15 @@ export function registerPanelEvents(panel) {
|
|
|
87
88
|
|
|
88
89
|
for (const el of elements) {
|
|
89
90
|
if (canvas.contains(el) && el !== canvas) {
|
|
91
|
+
// Layout element clicked — show layout info instead of selecting in page doc
|
|
92
|
+
if (layoutElements.has(el)) {
|
|
93
|
+
view.layoutSelection = { el, layoutPath: activeLayoutPath };
|
|
94
|
+
update(selectNode(S, null));
|
|
95
|
+
renderOnly("rightPanel");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
view.layoutSelection = null;
|
|
99
|
+
|
|
90
100
|
const originalPath = elToPath.get(el);
|
|
91
101
|
if (originalPath) {
|
|
92
102
|
let path = bubbleInlinePath(S.document, originalPath);
|