@jxsuite/studio 0.10.1 → 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.
@@ -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 {any} panel
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 {any} */ (el).style.pointerEvents = "auto";
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 {any} */ (el).style.pointerEvents = "none";
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 isVoid = VOID_ELEMENTS.has((node?.tagName || "div").toLowerCase());
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(/** @type {any} */ (srcPath), elPath)) return false;
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: isVoid };
70
- },
71
- onDragEnter() {
72
- showCanvasDropIndicator(el, elPath, isVoid, panel);
95
+ return { path: elPath, _isVoid: isLeaf };
73
96
  },
74
- onDrag() {
75
- showCanvasDropIndicator(el, elPath, isVoid, panel);
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
- onDragLeave() {
78
- dropLine.style.display = "none";
79
- el.classList.remove("canvas-drop-target");
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
- const instruction = getCanvasDropInstruction(el, elPath, isVoid);
85
- if (!instruction) return;
86
- applyDropInstruction(instruction, source.data, elPath);
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 {any} el
95
- * @param {any} elPath
96
- * @param {any} isVoid
127
+ * @param {HTMLElement} el
128
+ * @param {JxPath} elPath
129
+ * @param {boolean} isLeaf
130
+ * @returns {DropResult}
97
131
  */
98
- function getCanvasDropInstruction(el, elPath, isVoid) {
99
- const rect = el.getBoundingClientRect();
100
- if (!view.lastDragInput) return null;
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 (elPath.length === 0) return { type: "make-child" };
105
- if (isVoid) return relY < 0.5 ? { type: "reorder-above" } : { type: "reorder-below" };
106
- if (relY < 0.25) return { type: "reorder-above" };
107
- if (relY > 0.75) return { type: "reorder-below" };
108
- return { type: "make-child" };
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
- * @param {any} el
113
- * @param {any} elPath
114
- * @param {any} isVoid
115
- * @param {any} panel
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 showCanvasDropIndicator(el, elPath, isVoid, panel) {
118
- const instruction = getCanvasDropInstruction(el, elPath, isVoid);
119
- const { dropLine, viewport } = panel;
120
- if (!instruction) {
121
- dropLine.style.display = "none";
122
- return;
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 elRect = el.getBoundingClientRect();
128
- const left = (elRect.left - wrapRect.left + viewport.scrollLeft) / scale;
129
- const width = elRect.width / scale;
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 = `${(elRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
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 = `${elRect.height / scale}px`;
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
- ? (elRect.top - wrapRect.top + viewport.scrollTop) / scale
146
- : (elRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
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 = "2px";
234
+ dropLine.style.height = "";
153
235
  dropLine.className = "canvas-drop-indicator line";
154
236
  }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * When editing project.json: shows Class Imports, Dependencies (add/remove packages), and
5
5
  * per-package component toggles for cherry-picking individual elements. When editing a
6
- * page/layout/component/collection: shows Component Imports ($ref picker) and per-package component
7
- * toggles.
6
+ * page/layout/component/content type: shows Component Imports ($ref picker) and per-package
7
+ * component toggles.
8
8
  */
9
9
 
10
10
  import { html, nothing } from "lit-html";
@@ -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) boxes.push(overlayBoxDescriptor(el, "hover", p));
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=${b.cls}
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
- ></div>
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);