regular-layout 0.0.1 → 0.1.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.
@@ -22,18 +22,17 @@ import type {
22
22
  LayoutPath,
23
23
  Layout,
24
24
  LayoutDivider,
25
+ TabLayout,
26
+ OverlayMode,
25
27
  } from "./common/layout_config.ts";
26
28
  import { calculate_intersection } from "./common/calculate_intersect.ts";
27
29
  import { remove_child } from "./common/remove_child.ts";
28
30
  import { insert_child } from "./common/insert_child.ts";
29
31
  import { redistribute_panel_sizes } from "./common/redistribute_panel_sizes.ts";
30
32
  import { updateOverlaySheet } from "./common/generate_overlay.ts";
31
- import { calculate_split } from "./common/calculate_split.ts";
33
+ import { calculate_edge } from "./common/calculate_edge.ts";
32
34
  import { flatten } from "./common/flatten.ts";
33
-
34
- export type OverlayMode = "grid" | "absolute" | "interactive";
35
-
36
- const OVERLAY_DEFAULT: OverlayMode = "absolute";
35
+ import { OVERLAY_CLASSNAME, OVERLAY_DEFAULT } from "./common/constants.ts";
37
36
 
38
37
  /**
39
38
  * A Web Component that provides a resizable panel layout system.
@@ -85,9 +84,6 @@ export class RegularLayout extends HTMLElement {
85
84
  this._shadowRoot.adoptedStyleSheets = [this._stylesheet];
86
85
  this._shadowRoot.appendChild(this._unslotted_slot);
87
86
  this._slots = new Map();
88
- this.onPointerDown = this.onPointerDown.bind(this);
89
- this.onPointerMove = this.onPointerMove.bind(this);
90
- this.onPointerUp = this.onPointerUp.bind(this);
91
87
  }
92
88
 
93
89
  connectedCallback() {
@@ -108,74 +104,85 @@ export class RegularLayout extends HTMLElement {
108
104
  *
109
105
  * @param x - X coordinate in screen pixels.
110
106
  * @param y - Y coordinate in screen pixels.
111
- * @param layoutPath - Layout path containing the slot identifier.
112
- * @param mode - Overlay rendering mode: "grid" highlights the target,
113
- * "absolute" positions the panel absolutely, "interactive" updates the
114
- * actual layout in real-time. Defaults to "absolute".
107
+ * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
108
+ * which points to the drag element in the current layout.
109
+ * @param className - The CSS class name to use for the overlay panel
110
+ * (defaults to "overlay").
111
+ * @param mode - Overlay rendering mode: "grid" uses CSS grid to position
112
+ * the target, "absolute" positions the panel absolutely. Defaults to
113
+ * "absolute".
115
114
  */
116
- setOverlayState<T>(
115
+ setOverlayState = (
117
116
  x: number,
118
117
  y: number,
119
- { slot }: LayoutPath<T>,
120
- mode: "grid" | "absolute" | "interactive" = OVERLAY_DEFAULT,
121
- ) {
118
+ { slot }: LayoutPath<unknown>,
119
+ className: string = OVERLAY_CLASSNAME,
120
+ mode: OverlayMode = OVERLAY_DEFAULT,
121
+ ) => {
122
122
  let panel = this._panel;
123
123
  if (mode === "absolute") {
124
124
  panel = remove_child(panel, slot);
125
- this._slots.get(slot)?.assignedElements()[0]?.removeAttribute("slot");
125
+ this.updateSlots(panel, slot);
126
+ this._slots.get(slot)?.assignedElements()[0]?.classList.add(className);
126
127
  }
127
128
 
128
129
  const [col, row, box] = this.relativeCoordinates(x, y);
129
130
  let drop_target = calculate_intersection(col, row, panel, false);
130
131
  if (drop_target) {
131
- drop_target = calculate_split(col, row, panel, slot, drop_target);
132
- }
133
-
134
- if (drop_target) {
135
- if (mode === "interactive") {
136
- let new_panel = remove_child(this._panel, slot);
137
- new_panel = flatten(insert_child(new_panel, slot, drop_target.path));
138
- const css = create_css_grid_layout(new_panel);
139
- this._stylesheet.replaceSync(css);
140
- } else if (mode === "grid") {
132
+ drop_target = calculate_edge(col, row, panel, slot, drop_target);
133
+ if (mode === "grid") {
141
134
  const path: [string, string] = [slot, drop_target.slot];
142
135
  const css = create_css_grid_layout(this._panel, false, path);
143
136
  this._stylesheet.replaceSync(css);
144
137
  } else if (mode === "absolute") {
145
- const css = `${create_css_grid_layout(panel)}\n${updateOverlaySheet({ ...drop_target, box })}`;
146
- this._stylesheet.replaceSync(css);
138
+ const grid_css = create_css_grid_layout(panel);
139
+ const overlay_css = updateOverlaySheet(slot, { ...drop_target, box });
140
+ this._stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
147
141
  }
142
+ } else {
143
+ const css = `${create_css_grid_layout(panel)}}`;
144
+ this._stylesheet.replaceSync(css);
148
145
  }
149
- }
146
+
147
+ const event = new CustomEvent("regular-layout-before-update", {
148
+ detail: panel,
149
+ });
150
+
151
+ this.dispatchEvent(event);
152
+ };
150
153
 
151
154
  /**
152
155
  * Clears the overlay state and commits the panel placement.
153
156
  *
154
157
  * @param x - X coordinate in screen pixels.
155
158
  * @param y - Y coordinate in screen pixels.
156
- * @param layout_path - Layout path containing the slot identifier.
159
+ * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
160
+ * which points to the drag element in the current layout.
161
+ * @param className - The CSS class name to use for the overlay panel
162
+ * (defaults to "overlay").
157
163
  * @param mode - Overlay rendering mode that was used, must match the mode
158
- * passed to `setOverlayState`. Defaults to "absolute".
164
+ * passed to `setOverlayState`. Defaults to "absolute".
159
165
  */
160
- clearOverlayState<T>(
166
+ clearOverlayState = (
161
167
  x: number,
162
168
  y: number,
163
- drag_target: LayoutPath<T>,
164
- mode: "grid" | "absolute" | "interactive" = OVERLAY_DEFAULT,
165
- ) {
169
+ drag_target: LayoutPath<unknown>,
170
+ className: string = OVERLAY_CLASSNAME,
171
+ mode: OverlayMode = OVERLAY_DEFAULT,
172
+ ) => {
166
173
  let panel = this._panel;
167
174
  if (mode === "absolute") {
168
175
  panel = remove_child(panel, drag_target.slot);
169
- this._unslotted_slot
170
- .assignedElements()[0]
171
- ?.setAttribute("slot", drag_target.slot);
176
+ this._slots
177
+ .get(drag_target.slot)
178
+ ?.assignedElements()[0]
179
+ ?.classList.remove(className);
172
180
  }
173
181
 
174
182
  const [col, row, _] = this.relativeCoordinates(x, y);
175
183
  let drop_target = calculate_intersection(col, row, panel, false);
176
184
  if (drop_target) {
177
- // TODO I think I only need the new path here?
178
- drop_target = calculate_split(
185
+ drop_target = calculate_edge(
179
186
  col,
180
187
  row,
181
188
  panel,
@@ -184,10 +191,17 @@ export class RegularLayout extends HTMLElement {
184
191
  );
185
192
  }
186
193
 
187
- const { path } = drop_target ? drop_target : drag_target;
188
- this.removePanel(drag_target.slot);
189
- this.insertPanel(drag_target.slot, path);
190
- }
194
+ const { path, orientation } = drop_target ? drop_target : drag_target;
195
+ this.restore(
196
+ insert_child(
197
+ panel,
198
+ drag_target.slot,
199
+ path,
200
+ orientation,
201
+ !drop_target?.is_edge,
202
+ ),
203
+ );
204
+ };
191
205
 
192
206
  /**
193
207
  * Inserts a new panel into the layout at a specified path.
@@ -195,32 +209,56 @@ export class RegularLayout extends HTMLElement {
195
209
  * @param name - Unique identifier for the new panel.
196
210
  * @param path - Index path defining where to insert.
197
211
  */
198
- insertPanel(name: string, path: number[] = []) {
212
+ insertPanel = (name: string, path: number[] = []) => {
199
213
  this.restore(insert_child(this._panel, name, path));
200
- }
214
+ };
201
215
 
202
216
  /**
203
217
  * Removes a panel from the layout by name.
204
218
  *
205
219
  * @param name - Name of the panel to remove
206
220
  */
207
- removePanel(name: string) {
221
+ removePanel = (name: string) => {
208
222
  this.restore(remove_child(this._panel, name));
209
- }
223
+ };
224
+
225
+ /**
226
+ * Retrieves a panel by name from the layout tree.
227
+ *
228
+ * @param name - Name of the panel to find.
229
+ * @param layout - Optional layout tree to search in (defaults to current layout).
230
+ * @returns The TabLayout containing the panel if found, null otherwise.
231
+ */
232
+ getPanel = (name: string, layout: Layout = this._panel): TabLayout | null => {
233
+ if (layout.type === "child-panel") {
234
+ if (layout.child.includes(name)) {
235
+ return layout;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ for (const child of layout.children) {
241
+ const found = this.getPanel(name, child);
242
+ if (found) {
243
+ return found;
244
+ }
245
+ }
246
+
247
+ return null;
248
+ };
210
249
 
211
250
  /**
212
251
  * Determines which panel is at a given screen coordinate.
213
- * Useful for drag-and-drop operations or custom interactions.
214
252
  *
215
253
  * @param column - X coordinate in screen pixels.
216
254
  * @param row - Y coordinate in screen pixels.
217
255
  * @returns Panel information if a panel is at that position, null otherwise.
218
256
  */
219
- calculateIntersect(
257
+ calculateIntersect = (
220
258
  x: number,
221
259
  y: number,
222
260
  check_dividers: boolean = false,
223
- ): LayoutPath<DOMRect> | null {
261
+ ): LayoutPath<DOMRect> | null => {
224
262
  const [col, row, box] = this.relativeCoordinates(x, y);
225
263
  const panel = calculate_intersection(col, row, this._panel, check_dividers);
226
264
  if (panel?.type === "layout-path") {
@@ -228,7 +266,14 @@ export class RegularLayout extends HTMLElement {
228
266
  }
229
267
 
230
268
  return null;
231
- }
269
+ };
270
+
271
+ /**
272
+ * Clears the entire layout, unslotting all panels.
273
+ */
274
+ clear = () => {
275
+ this.restore(EMPTY_PANEL);
276
+ };
232
277
 
233
278
  /**
234
279
  * Restores the layout from a saved state.
@@ -242,29 +287,17 @@ export class RegularLayout extends HTMLElement {
242
287
  * layout.restore(savedState);
243
288
  * ```
244
289
  */
245
- restore(layout: Layout, _is_flattened: boolean = false) {
290
+ restore = (layout: Layout, _is_flattened: boolean = false) => {
246
291
  this._panel = !_is_flattened ? flatten(layout) : layout;
247
- const css = create_css_grid_layout(layout);
292
+ const css = create_css_grid_layout(this._panel);
248
293
  this._stylesheet.replaceSync(css);
249
- const old = new Set(this._slots.keys());
250
- for (const name of iter_panel_children(layout)) {
251
- old.delete(name);
252
- if (!this._slots.has(name)) {
253
- const slot = document.createElement("slot");
254
- slot.setAttribute("name", name);
255
- this._shadowRoot.appendChild(slot);
256
- this._slots.set(name, slot);
257
- }
258
- }
294
+ this.updateSlots(this._panel);
295
+ const event = new CustomEvent("regular-layout-update", {
296
+ detail: this._panel,
297
+ });
259
298
 
260
- for (const key of old) {
261
- const child = this._slots.get(key);
262
- if (child) {
263
- this._shadowRoot.removeChild(child);
264
- this._slots.delete(key);
265
- }
266
- }
267
- }
299
+ this.dispatchEvent(event);
300
+ };
268
301
 
269
302
  /**
270
303
  * Serializes the current layout state, which can be restored via `restore`.
@@ -278,32 +311,71 @@ export class RegularLayout extends HTMLElement {
278
311
  * localStorage.setItem('layout', JSON.stringify(state));
279
312
  * ```
280
313
  */
281
- save(): Layout {
314
+ save = (): Layout => {
282
315
  return structuredClone(this._panel);
283
- }
316
+ };
284
317
 
285
- private relativeCoordinates(
318
+ /**
319
+ * Converts screen coordinates to relative layout coordinates.
320
+ *
321
+ * Transforms absolute pixel positions into normalized coordinates (0-1 range)
322
+ * relative to the layout's bounding box.
323
+ *
324
+ * @param clientX - X coordinate in screen pixels (client space).
325
+ * @param clientY - Y coordinate in screen pixels (client space).
326
+ * @returns A tuple containing:
327
+ * - col: Normalized X coordinate (0 = left edge, 1 = right edge)
328
+ * - row: Normalized Y coordinate (0 = top edge, 1 = bottom edge)
329
+ * - box: The layout element's bounding rectangle
330
+ */
331
+ relativeCoordinates = (
286
332
  clientX: number,
287
333
  clientY: number,
288
- ): [number, number, DOMRect] {
334
+ ): [number, number, DOMRect] => {
289
335
  const box = this.getBoundingClientRect();
290
336
  const col = (clientX - box.left) / (box.right - box.left);
291
337
  const row = (clientY - box.top) / (box.bottom - box.top);
292
338
  return [col, row, box];
293
- }
339
+ };
294
340
 
295
- private onPointerDown(event: PointerEvent) {
296
- const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
297
- const hit = calculate_intersection(col, row, this._panel);
298
- if (hit && hit.type !== "layout-path") {
299
- this._dragPath = [hit, col, row];
300
- this.setPointerCapture(event.pointerId);
301
- event.preventDefault();
302
- event.stopImmediatePropagation();
341
+ private updateSlots = (layout: Layout, overlay?: string) => {
342
+ const old = new Set(this._slots.keys());
343
+ if (overlay) {
344
+ old.delete(overlay);
303
345
  }
304
- }
305
346
 
306
- private onPointerMove(event: PointerEvent) {
347
+ for (const name of iter_panel_children(layout)) {
348
+ old.delete(name);
349
+ if (!this._slots.has(name)) {
350
+ const slot = document.createElement("slot");
351
+ slot.setAttribute("name", name);
352
+ this._shadowRoot.appendChild(slot);
353
+ this._slots.set(name, slot);
354
+ }
355
+ }
356
+
357
+ for (const key of old) {
358
+ const child = this._slots.get(key);
359
+ if (child) {
360
+ this._shadowRoot.removeChild(child);
361
+ this._slots.delete(key);
362
+ }
363
+ }
364
+ };
365
+
366
+ private onPointerDown = (event: PointerEvent) => {
367
+ if (event.target === this) {
368
+ const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
369
+ const hit = calculate_intersection(col, row, this._panel);
370
+ if (hit && hit.type !== "layout-path") {
371
+ this._dragPath = [hit, col, row];
372
+ this.setPointerCapture(event.pointerId);
373
+ event.preventDefault();
374
+ }
375
+ }
376
+ };
377
+
378
+ private onPointerMove = (event: PointerEvent) => {
307
379
  if (this._dragPath) {
308
380
  const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
309
381
  const old_panel = this._panel;
@@ -312,9 +384,9 @@ export class RegularLayout extends HTMLElement {
312
384
  const panel = redistribute_panel_sizes(old_panel, path, offset);
313
385
  this._stylesheet.replaceSync(create_css_grid_layout(panel));
314
386
  }
315
- }
387
+ };
316
388
 
317
- private onPointerUp(event: PointerEvent) {
389
+ private onPointerUp = (event: PointerEvent) => {
318
390
  if (this._dragPath) {
319
391
  this.releasePointerCapture(event.pointerId);
320
392
  const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
@@ -330,5 +402,5 @@ export class RegularLayout extends HTMLElement {
330
402
 
331
403
  this._dragPath = undefined;
332
404
  }
333
- }
405
+ };
334
406
  }
@@ -1,2 +0,0 @@
1
- import { type Layout, type LayoutPath } from "./layout_config";
2
- export declare function calculate_split(col: number, row: number, panel: Layout, slot: string, config: LayoutPath): LayoutPath;
@@ -1,53 +0,0 @@
1
- import { calculate_intersection } from "./calculate_intersect";
2
- import { insert_child } from "./insert_child";
3
- import {
4
- SPLIT_EDGE_TOLERANCE,
5
- type Layout,
6
- type LayoutPath,
7
- } from "./layout_config";
8
-
9
- export function calculate_split(
10
- col: number,
11
- row: number,
12
- panel: Layout,
13
- slot: string,
14
- config: LayoutPath,
15
- ): LayoutPath {
16
- if (
17
- config.column_offset < SPLIT_EDGE_TOLERANCE ||
18
- config.column_offset > 1 - SPLIT_EDGE_TOLERANCE
19
- ) {
20
- if (config.orientation === "vertical") {
21
- const new_panel = insert_child(panel, slot, [
22
- ...config.path,
23
- config.column_offset < SPLIT_EDGE_TOLERANCE ? 0 : 1,
24
- ]);
25
-
26
- config = calculate_intersection(col, row, new_panel, false);
27
- } else {
28
- const new_panel = insert_child(panel, slot, config.path);
29
- config = calculate_intersection(col, row, new_panel, false);
30
- }
31
- }
32
-
33
- if (
34
- (config.row_offset < SPLIT_EDGE_TOLERANCE &&
35
- config.row_offset < config.column_offset) ||
36
- (config.row_offset > 1 - SPLIT_EDGE_TOLERANCE &&
37
- config.row_offset > config.column_offset)
38
- ) {
39
- if (config.orientation === "horizontal") {
40
- const new_panel = insert_child(panel, slot, [
41
- ...config.path,
42
- config.row_offset < SPLIT_EDGE_TOLERANCE ? 0 : 1,
43
- ]);
44
-
45
- config = calculate_intersection(col, row, new_panel, false);
46
- } else {
47
- const new_panel = insert_child(panel, slot, config.path);
48
- config = calculate_intersection(col, row, new_panel, false);
49
- }
50
- }
51
-
52
- return config;
53
- }