regular-layout 0.2.1 → 0.2.2

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.
@@ -17,8 +17,8 @@ import type { Layout } from "./types.ts";
17
17
  * type: "split-panel",
18
18
  * orientation: "horizontal",
19
19
  * children: [
20
- * { type: "child-panel", child: "sidebar" },
21
- * { type: "child-panel", child: "main" }
20
+ * { type: "child-panel", tabs: "sidebar" },
21
+ * { type: "child-panel", tabs: "main" }
22
22
  * ],
23
23
  * sizes: [0.25, 0.75]
24
24
  * };
@@ -1,2 +1,2 @@
1
1
  import type { LayoutPath } from "./types";
2
- export declare function updateOverlaySheet(slot: string, box: DOMRect, style: CSSStyleDeclaration, drag_target: LayoutPath<undefined> | null, physics?: import("./constants").Physics): string;
2
+ export declare function updateOverlaySheet(slot: string, box: DOMRect, style: CSSStyleDeclaration, drag_target: LayoutPath | null, physics?: import("./constants").Physics): string;
@@ -16,4 +16,4 @@ import type { Layout } from "./types.ts";
16
16
  * @returns A new layout tree with updated sizes (original is not mutated).
17
17
  * ```
18
18
  */
19
- export declare function redistribute_panel_sizes(panel: Layout, path: number[], delta: number, physics?: import("./constants.ts").Physics): Layout;
19
+ export declare function redistribute_panel_sizes(panel: Layout, path: number[], delta: number | undefined, physics?: import("./constants.ts").Physics): Layout;
@@ -42,7 +42,7 @@ export interface SplitLayout {
42
42
  */
43
43
  export interface TabLayout {
44
44
  type: "child-panel";
45
- child: string[];
45
+ tabs: string[];
46
46
  selected?: number;
47
47
  }
48
48
  /**
@@ -57,12 +57,8 @@ export interface LayoutDivider {
57
57
  }
58
58
  /**
59
59
  * Represents a panel location result from hit detection.
60
- *
61
- * Contains both the panel identifier and its grid position in relative units.
62
- * The generic parameter `T` allows DOM-only properties (e.g. `DOMRect`) to be
63
- * shared in this cross-platform module.
64
60
  */
65
- export interface LayoutPath<T = undefined> {
61
+ export interface LayoutPath {
66
62
  type: "layout-path";
67
63
  slot: string;
68
64
  path: number[];
@@ -73,7 +69,7 @@ export interface LayoutPath<T = undefined> {
73
69
  row_offset: number;
74
70
  orientation: Orientation;
75
71
  is_edge: boolean;
76
- layout: T;
72
+ layout: Layout;
77
73
  }
78
74
  /**
79
75
  * An empty `Layout` with no panels.
@@ -28,15 +28,22 @@ export declare class RegularLayoutFrame extends HTMLElement {
28
28
  private _container_sheet;
29
29
  private _layout;
30
30
  private _header;
31
- private _drag_state;
32
- private _drag_moved;
31
+ private _drag;
33
32
  private _tab_to_index_map;
34
- constructor();
33
+ /**
34
+ * Initializes this elements. Override this method and
35
+ * `disconnectedCallback` to modify how this subclass renders the Shadow
36
+ * DOM and registers events.
37
+ */
35
38
  connectedCallback(): void;
39
+ /**
40
+ * Destroys this element.
41
+ */
36
42
  disconnectedCallback(): void;
37
43
  private onPointerDown;
38
44
  private onPointerMove;
39
45
  private onPointerUp;
46
+ private onPointerCancel;
40
47
  private onPointerLost;
41
48
  private drawTabs;
42
49
  }
@@ -1,5 +1,14 @@
1
1
  import type { LayoutPath, Layout, TabLayout, OverlayMode, Orientation } from "./layout/types.ts";
2
2
  import { type PhysicsUpdate, type Physics } from "./layout/constants.ts";
3
+ /**
4
+ * An interface which models the fields of `PointerEvent` that
5
+ * `<regular-layour>` actually uses, making it easier to sub out with an
6
+ * JavaScript object lieral when you don't have a `PointerEvent` handy.
7
+ */
8
+ export interface PointerEventCoordinates {
9
+ clientX: number;
10
+ clientY: number;
11
+ }
3
12
  /**
4
13
  * A Web Component that provides a resizable panel layout system.
5
14
  * Panels are arranged using CSS Grid and can be resized by dragging dividers.
@@ -32,6 +41,22 @@ import { type PhysicsUpdate, type Physics } from "./layout/constants.ts";
32
41
  * layout.restore(state);
33
42
  * ```
34
43
  *
44
+ * @remarks
45
+ *
46
+ * Why does this implementation use a `<slot>` at all? We must use
47
+ * `<slot>` and the Shadow DOM to scope the grid CSS rules to each
48
+ * instance of `<regular-layout>` (without e.g. giving them unique
49
+ * `"id"` and injecting into `document,head`), and we can only select
50
+ * `::slotted` light DOM children from `adoptedStyleSheets` on the
51
+ * `ShadowRoot`.
52
+ *
53
+ * Why does this implementation use a single `<slot>` and the child
54
+ * `"name"` attribute, as opposed to a named `<slot name="my_slot">`
55
+ * and the built-in `"slot"` child attribute? Children with a `"slot"`
56
+ * attribute don't fallback to the un-named `<slot>`, so using the
57
+ * latter implementation would require synchronizing the light DOM
58
+ * and shadow DOM slots/slotted children continuously.
59
+ *
35
60
  */
36
61
  export declare class RegularLayout extends HTMLElement {
37
62
  private _shadowRoot;
@@ -48,17 +73,17 @@ export declare class RegularLayout extends HTMLElement {
48
73
  /**
49
74
  * Determines which panel is at a given screen coordinate.
50
75
  *
51
- * @param column - X coordinate in screen pixels.
52
- * @param row - Y coordinate in screen pixels.
76
+ * @param coordinates - `PointerEvent`, `MouseEvent`, or just X and Y
77
+ * coordinates in screen pixels.
53
78
  * @returns Panel information if a panel is at that position, null otherwise.
54
79
  */
55
- calculateIntersect: (x: number, y: number, check_dividers?: boolean) => LayoutPath<Layout> | null;
80
+ calculateIntersect: (coordinates: PointerEventCoordinates) => LayoutPath | null;
56
81
  /**
57
82
  * Sets the visual overlay state during drag-and-drop operations.
58
83
  * Displays a preview of where a panel would be placed at the given coordinates.
59
84
  *
60
- * @param x - X coordinate in screen pixels.
61
- * @param y - Y coordinate in screen pixels.
85
+ * @param event - `PointerEvent`, `MouseEvent`, or just X and Y
86
+ * coordinates in screen pixels.
62
87
  * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
63
88
  * which points to the drag element in the current layout.
64
89
  * @param className - The CSS class name to use for the overlay panel
@@ -67,12 +92,12 @@ export declare class RegularLayout extends HTMLElement {
67
92
  * the target, "absolute" positions the panel absolutely. Defaults to
68
93
  * "absolute".
69
94
  */
70
- setOverlayState: (x: number, y: number, { slot }: LayoutPath<unknown>, className?: string, mode?: OverlayMode) => void;
95
+ setOverlayState: (event: PointerEventCoordinates, { slot }: LayoutPath, className?: string, mode?: OverlayMode) => void;
71
96
  /**
72
97
  * Clears the overlay state and commits the panel placement.
73
98
  *
74
- * @param x - X coordinate in screen pixels.
75
- * @param y - Y coordinate in screen pixels.
99
+ * @param event - `PointerEvent`, `MouseEvent`, or just X and Y
100
+ * coordinates in screen pixels.
76
101
  * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
77
102
  * which points to the drag element in the current layout.
78
103
  * @param className - The CSS class name to use for the overlay panel
@@ -80,7 +105,7 @@ export declare class RegularLayout extends HTMLElement {
80
105
  * @param mode - Overlay rendering mode that was used, must match the mode
81
106
  * passed to `setOverlayState`. Defaults to "absolute".
82
107
  */
83
- clearOverlayState: (x: number, y: number, drag_target: LayoutPath<Layout>, className?: string) => void;
108
+ clearOverlayState: (event: PointerEventCoordinates | null, { slot, layout }: LayoutPath, className?: string) => void;
84
109
  /**
85
110
  * Inserts a new panel into the layout at a specified path.
86
111
  *
@@ -155,14 +180,26 @@ export declare class RegularLayout extends HTMLElement {
155
180
  * Transforms absolute pixel positions into normalized coordinates (0-1 range)
156
181
  * relative to the layout's bounding box.
157
182
  *
158
- * @param clientX - X coordinate in screen pixels (client space).
159
- * @param clientY - Y coordinate in screen pixels (client space).
183
+ * @param coordinates - `PointerEvent`, `MouseEvent`, or just X and Y
184
+ * coordinates in screen pixels.
160
185
  * @returns A tuple containing:
161
186
  * - col: Normalized X coordinate (0 = left edge, 1 = right edge)
162
187
  * - row: Normalized Y coordinate (0 = top edge, 1 = bottom edge)
163
188
  * - box: The layout element's bounding rectangle
164
189
  */
165
- relativeCoordinates: (clientX: number, clientY: number, recalculate_bounds?: boolean) => [number, number, DOMRect, CSSStyleDeclaration];
190
+ relativeCoordinates: (event: PointerEventCoordinates, recalculate_bounds?: boolean) => [number, number, DOMRect, CSSStyleDeclaration];
191
+ /**
192
+ * Calculates the Euclidean distance in pixels between the current pointer
193
+ * coordinates and a drag target's position within the layout.
194
+ *
195
+ * @param coordinates - The current pointer event coordinates.
196
+ * @param drag_target - The layout path representing the drag target
197
+ * position.
198
+ * @returns The distance in pixels between the coordinates and the drag
199
+ * target.
200
+ */
201
+ diffCoordinates: (event: PointerEventCoordinates, drag_target: LayoutPath) => number;
202
+ private onDblClick;
166
203
  private onPointerDown;
167
204
  private onPointerMove;
168
205
  private onPointerUp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "regular-layout",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A regular CSS `grid` container",
5
5
  "keywords": [],
6
6
  "license": "Apache-2.0",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "browser": "dist/index.js",
12
12
  "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
13
14
  "type": "module",
14
15
  "files": [
15
16
  "dist/**/*",
@@ -20,7 +21,8 @@
20
21
  "build": "tsx build.ts",
21
22
  "build:watch": "tsx build.ts --watch",
22
23
  "clean": "rm -rf dist",
23
- "test": "playwright test",
24
+ "test": "playwright test tests/integration tests/unit",
25
+ "test:perf": "playwright test --workers=1 ./benchmarks",
24
26
  "example": "tsx serve.ts",
25
27
  "deploy": "tsx deploy.ts",
26
28
  "lint": "biome lint src tests",
@@ -87,7 +87,9 @@ export function calculate_edge(
87
87
  (box?.height || 1) *
88
88
  (drop_target.view_window.row_end - drop_target.view_window.row_start);
89
89
 
90
- const use_column = col_distance * col_scale > row_distance * row_scale;
90
+ const use_column =
91
+ col_scale / 2 - col_distance * col_scale <
92
+ row_scale / 2 - row_distance * row_scale;
91
93
 
92
94
  return insert_axis(
93
95
  panel,
@@ -80,8 +80,8 @@ function calculate_intersection_recursive(
80
80
  const row_height = view_window.row_end - view_window.row_start;
81
81
  return {
82
82
  type: "layout-path",
83
- layout: undefined,
84
- slot: panel.child[selected],
83
+ layout: panel,
84
+ slot: panel.tabs[selected],
85
85
  path,
86
86
  view_window,
87
87
  is_edge: false,
@@ -104,7 +104,7 @@ function build_cells(
104
104
  const selected = panel.selected ?? 0;
105
105
  return [
106
106
  {
107
- child: panel.child[selected],
107
+ child: panel.tabs[selected],
108
108
  colStart: find_track_index(physics, colPositions, colStart),
109
109
  colEnd: find_track_index(physics, colPositions, colEnd),
110
110
  rowStart: find_track_index(physics, rowPositions, rowStart),
@@ -182,8 +182,8 @@ const child_template = (
182
182
  * type: "split-panel",
183
183
  * orientation: "horizontal",
184
184
  * children: [
185
- * { type: "child-panel", child: "sidebar" },
186
- * { type: "child-panel", child: "main" }
185
+ * { type: "child-panel", tabs: "sidebar" },
186
+ * { type: "child-panel", tabs: "main" }
187
187
  * ],
188
188
  * sizes: [0.25, 0.75]
189
189
  * };
@@ -204,7 +204,7 @@ export function create_css_grid_layout(
204
204
  const selected = layout.selected ?? 0;
205
205
  return [
206
206
  host_template("100%", "100%"),
207
- child_template(physics, layout.child[selected], "1", "1"),
207
+ child_template(physics, layout.tabs[selected], "1", "1"),
208
208
  ].join("\n");
209
209
  }
210
210
 
@@ -16,7 +16,7 @@ export function updateOverlaySheet(
16
16
  slot: string,
17
17
  box: DOMRect,
18
18
  style: CSSStyleDeclaration,
19
- drag_target: LayoutPath<undefined> | null,
19
+ drag_target: LayoutPath | null,
20
20
  physics = DEFAULT_PHYSICS,
21
21
  ) {
22
22
  if (!drag_target) {
@@ -33,7 +33,7 @@ export function insert_child(
33
33
  ): Layout {
34
34
  const createChildPanel = (childId: string): Layout => ({
35
35
  type: "child-panel",
36
- child: [childId],
36
+ tabs: [childId],
37
37
  });
38
38
 
39
39
  if (path.length === 0) {
@@ -42,7 +42,7 @@ export function insert_child(
42
42
  // Add to existing child-panel as a tab
43
43
  return {
44
44
  type: "child-panel",
45
- child: [child, ...panel.child],
45
+ tabs: [child, ...panel.tabs],
46
46
  };
47
47
  } else if (orientation) {
48
48
  // When inserting at edge of root, wrap the entire panel in a new split
@@ -105,13 +105,13 @@ export function insert_child(
105
105
  restPath.length === 0 &&
106
106
  orientation === undefined &&
107
107
  index >= 0 &&
108
- index <= panel.child.length
108
+ index <= panel.tabs.length
109
109
  ) {
110
- const newChild = [...panel.child];
110
+ const newChild = [...panel.tabs];
111
111
  newChild.splice(index, 0, child);
112
112
  return {
113
113
  ...panel,
114
- child: newChild,
114
+ tabs: newChild,
115
115
  };
116
116
  }
117
117
 
@@ -32,7 +32,7 @@ import type { Layout } from "./types.ts";
32
32
  export function redistribute_panel_sizes(
33
33
  panel: Layout,
34
34
  path: number[],
35
- delta: number,
35
+ delta: number | undefined,
36
36
  physics = DEFAULT_PHYSICS,
37
37
  ): Layout {
38
38
  // Clone the entire panel structure
@@ -41,7 +41,7 @@ export function redistribute_panel_sizes(
41
41
  // Find the orientation of the insertion panel,
42
42
  // and scale the delta on the respective axis if aligned.
43
43
  let current: Layout = result;
44
- const deltas = { horizontal: delta, vertical: delta };
44
+ const deltas = { horizontal: delta || 0, vertical: delta || 0 };
45
45
  for (let i = 0; i < path.length - 1; i++) {
46
46
  if (current.type === "split-panel") {
47
47
  deltas[current.orientation] /= current.sizes[path[i]];
@@ -51,17 +51,21 @@ export function redistribute_panel_sizes(
51
51
 
52
52
  // Apply the redistribution at the final path index
53
53
  if (current.type === "split-panel") {
54
- const delta = deltas[current.orientation];
55
- const index = path[path.length - 1];
54
+ if (delta === undefined) {
55
+ current.sizes = current.sizes.map((_) => 1 / current.sizes.length);
56
+ } else {
57
+ const delta = deltas[current.orientation];
58
+ const index = path[path.length - 1];
56
59
 
57
- // It would be fun to remove this condition.
58
- if (index < current.sizes.length - 1) {
59
- current.sizes = add_and_redistribute(
60
- physics,
61
- current.sizes,
62
- index,
63
- delta,
64
- );
60
+ // It would be fun to remove this condition.
61
+ if (index < current.sizes.length - 1) {
62
+ current.sizes = add_and_redistribute(
63
+ physics,
64
+ current.sizes,
65
+ index,
66
+ delta,
67
+ );
68
+ }
65
69
  }
66
70
  }
67
71
 
@@ -26,14 +26,14 @@ import { EMPTY_PANEL } from "./types.ts";
26
26
  export function remove_child(panel: Layout, child: string): Layout {
27
27
  // If this is a child panel, handle tab removal
28
28
  if (panel.type === "child-panel") {
29
- if (panel.child.includes(child)) {
30
- const newChild = panel.child.filter((c) => c !== child);
29
+ if (panel.tabs.includes(child)) {
30
+ const newChild = panel.tabs.filter((c) => c !== child);
31
31
  if (newChild.length === 0) {
32
32
  return structuredClone(EMPTY_PANEL);
33
33
  }
34
34
  return {
35
35
  type: "child-panel",
36
- child: newChild,
36
+ tabs: newChild,
37
37
  };
38
38
  }
39
39
 
@@ -46,7 +46,7 @@ export function remove_child(panel: Layout, child: string): Layout {
46
46
  // Try to remove the child from this split panel's children
47
47
  const index = result.children.findIndex((p) => {
48
48
  if (p.type === "child-panel") {
49
- return p.child.includes(child);
49
+ return p.tabs.includes(child);
50
50
  }
51
51
 
52
52
  return false;
@@ -54,7 +54,7 @@ export function remove_child(panel: Layout, child: string): Layout {
54
54
 
55
55
  if (index !== -1) {
56
56
  const tab_layout = result.children[index] as TabLayout;
57
- if (tab_layout.child.length === 1) {
57
+ if (tab_layout.tabs.length === 1) {
58
58
  // Found the child at this level - remove it
59
59
  const newChildren = result.children.filter((_, i) => i !== index);
60
60
  const newSizes = remove_and_redistribute(result.sizes, index);
@@ -67,10 +67,10 @@ export function remove_child(panel: Layout, child: string): Layout {
67
67
  result.children = newChildren;
68
68
  result.sizes = newSizes;
69
69
  } else {
70
- tab_layout.child.splice(tab_layout.child.indexOf(child), 1);
70
+ tab_layout.tabs.splice(tab_layout.tabs.indexOf(child), 1);
71
71
  if (
72
72
  tab_layout.selected &&
73
- tab_layout.selected >= tab_layout.child.length
73
+ tab_layout.selected >= tab_layout.tabs.length
74
74
  ) {
75
75
  tab_layout.selected--;
76
76
  }
@@ -34,7 +34,6 @@ export interface ViewWindow {
34
34
  col_end: number;
35
35
  }
36
36
 
37
-
38
37
  /**
39
38
  * A split panel that divides space among multiple child layouts
40
39
  * .
@@ -59,7 +58,7 @@ export interface SplitLayout {
59
58
  */
60
59
  export interface TabLayout {
61
60
  type: "child-panel";
62
- child: string[];
61
+ tabs: string[];
63
62
  selected?: number;
64
63
  }
65
64
 
@@ -76,12 +75,8 @@ export interface LayoutDivider {
76
75
 
77
76
  /**
78
77
  * Represents a panel location result from hit detection.
79
- *
80
- * Contains both the panel identifier and its grid position in relative units.
81
- * The generic parameter `T` allows DOM-only properties (e.g. `DOMRect`) to be
82
- * shared in this cross-platform module.
83
78
  */
84
- export interface LayoutPath<T = undefined> {
79
+ export interface LayoutPath {
85
80
  type: "layout-path";
86
81
  slot: string;
87
82
  path: number[];
@@ -92,7 +87,7 @@ export interface LayoutPath<T = undefined> {
92
87
  row_offset: number;
93
88
  orientation: Orientation;
94
89
  is_edge: boolean;
95
- layout: T;
90
+ layout: Layout;
96
91
  }
97
92
 
98
93
  /**
@@ -9,7 +9,7 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import type { Layout, LayoutPath } from "./layout/types.ts";
12
+ import type { LayoutPath } from "./layout/types.ts";
13
13
  import type { RegularLayoutEvent } from "./extensions.ts";
14
14
  import type { RegularLayout } from "./regular-layout.ts";
15
15
  import type { RegularLayoutTab } from "./regular-layout-tab.ts";
@@ -29,6 +29,8 @@ const HTML_TEMPLATE = `
29
29
  <slot part="container"></slot>
30
30
  `;
31
31
 
32
+ type DragState = { moved?: boolean; path: LayoutPath };
33
+
32
34
  /**
33
35
  * A custom element that represents a draggable panel within a
34
36
  * `<regular-layout>`.
@@ -55,29 +57,31 @@ const HTML_TEMPLATE = `
55
57
  * ```
56
58
  */
57
59
  export class RegularLayoutFrame extends HTMLElement {
58
- private _shadowRoot: ShadowRoot;
59
- private _container_sheet: CSSStyleSheet;
60
+ private _shadowRoot!: ShadowRoot;
61
+ private _container_sheet!: CSSStyleSheet;
60
62
  private _layout!: RegularLayout;
61
63
  private _header!: HTMLElement;
62
- private _drag_state: LayoutPath<Layout> | null = null;
63
- private _drag_moved: boolean = false;
64
+ private _drag: DragState | null = null;
64
65
  private _tab_to_index_map: WeakMap<RegularLayoutTab, number> = new WeakMap();
65
- constructor() {
66
- super();
67
- this._container_sheet = new CSSStyleSheet();
68
- this._container_sheet.replaceSync(CSS);
69
- this._shadowRoot = this.attachShadow({ mode: "open" });
70
- this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
71
- }
72
66
 
67
+ /**
68
+ * Initializes this elements. Override this method and
69
+ * `disconnectedCallback` to modify how this subclass renders the Shadow
70
+ * DOM and registers events.
71
+ */
73
72
  connectedCallback() {
73
+ this._container_sheet ??= new CSSStyleSheet();
74
+ this._container_sheet.replaceSync(CSS);
75
+ this._shadowRoot ??= this.attachShadow({ mode: "open" });
76
+ this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
74
77
  this._shadowRoot.innerHTML = HTML_TEMPLATE;
75
78
  this._layout = this.parentElement as RegularLayout;
76
79
  this._header = this._shadowRoot.children[0] as HTMLElement;
77
80
  this._header.addEventListener("pointerdown", this.onPointerDown);
78
- this._header.addEventListener("pointermove", this.onPointerMove);
79
- this._header.addEventListener("pointerup", this.onPointerUp);
80
- this._header.addEventListener("lostpointercapture", this.onPointerLost);
81
+ this.addEventListener("pointermove", this.onPointerMove);
82
+ this.addEventListener("pointerup", this.onPointerUp);
83
+ this.addEventListener("pointercancel", this.onPointerCancel);
84
+ this.addEventListener("lostpointercapture", this.onPointerLost);
81
85
  this._layout.addEventListener("regular-layout-update", this.drawTabs);
82
86
  this._layout.addEventListener(
83
87
  "regular-layout-before-update",
@@ -85,11 +89,15 @@ export class RegularLayoutFrame extends HTMLElement {
85
89
  );
86
90
  }
87
91
 
92
+ /**
93
+ * Destroys this element.
94
+ */
88
95
  disconnectedCallback() {
89
96
  this._header.removeEventListener("pointerdown", this.onPointerDown);
90
- this._header.removeEventListener("pointermove", this.onPointerMove);
91
- this._header.removeEventListener("pointerup", this.onPointerUp);
92
- this._header.removeEventListener("lostpointercapture", this.onPointerLost);
97
+ this.removeEventListener("pointermove", this.onPointerMove);
98
+ this.removeEventListener("pointerup", this.onPointerUp);
99
+ this.removeEventListener("pointercancel", this.onPointerUp);
100
+ this.removeEventListener("lostpointercapture", this.onPointerLost);
93
101
  this._layout.removeEventListener("regular-layout-update", this.drawTabs);
94
102
  this._layout.removeEventListener(
95
103
  "regular-layout-before-update",
@@ -100,75 +108,52 @@ export class RegularLayoutFrame extends HTMLElement {
100
108
  private onPointerDown = (event: PointerEvent): void => {
101
109
  const elem = event.target as RegularLayoutTab;
102
110
  if (elem.part.contains("tab")) {
103
- this._drag_state = this._layout.calculateIntersect(
104
- event.clientX,
105
- event.clientY,
106
- );
107
-
108
- if (this._drag_state) {
109
- this._header.setPointerCapture(event.pointerId);
111
+ const path = this._layout.calculateIntersect(event);
112
+ if (path) {
113
+ this._drag = { path };
114
+ this.setPointerCapture(event.pointerId);
110
115
  event.preventDefault();
116
+ } else {
117
+ this._drag = null;
111
118
  }
112
119
  }
113
120
  };
114
121
 
115
122
  private onPointerMove = (event: PointerEvent): void => {
116
- if (this._drag_state) {
123
+ if (this._drag) {
117
124
  const physics = this._layout.savePhysics();
118
-
119
- // Only initiate a drag if the cursor has moved sufficiently.
120
- if (!this._drag_moved) {
121
- const [current_col, current_row, box] =
122
- this._layout.relativeCoordinates(event.clientX, event.clientY);
123
-
124
- const dx = (current_col - this._drag_state.column) * box.width;
125
- const dy = (current_row - this._drag_state.row) * box.height;
126
- if (Math.sqrt(dx * dx + dy * dy) <= physics.MIN_DRAG_DISTANCE) {
125
+ if (!this._drag.moved) {
126
+ const diff = this._layout.diffCoordinates(event, this._drag.path);
127
+ if (diff <= physics.MIN_DRAG_DISTANCE) {
127
128
  return;
128
129
  }
129
130
  }
130
131
 
131
- this._drag_moved = true;
132
- this._layout.setOverlayState(
133
- event.clientX,
134
- event.clientY,
135
- this._drag_state,
136
- physics.OVERLAY_CLASSNAME,
137
- );
132
+ this._drag.moved = true;
133
+ this._layout.setOverlayState(event, this._drag.path);
138
134
  }
139
135
  };
140
136
 
141
137
  private onPointerUp = (event: PointerEvent): void => {
142
- if (this._drag_state && this._drag_moved) {
143
- this._layout.clearOverlayState(
144
- event.clientX,
145
- event.clientY,
146
- this._drag_state,
147
- );
138
+ if (this._drag?.moved) {
139
+ this._layout.clearOverlayState(event, this._drag.path);
148
140
  }
149
-
150
- // TODO This may be handled by `onPointerLost`, not sure if this is
151
- // browser-specific behavior ...
152
- this._header.releasePointerCapture(event.pointerId);
153
- this._drag_state = null;
154
- this._drag_moved = false;
155
141
  };
156
142
 
157
- private onPointerLost = (event: PointerEvent): void => {
158
- if (this._drag_state) {
159
- this._layout.clearOverlayState(-1, -1, this._drag_state);
143
+ private onPointerCancel = (_: PointerEvent): void => {
144
+ if (this._drag?.moved) {
145
+ this._layout.clearOverlayState(null, this._drag.path);
160
146
  }
147
+ };
161
148
 
162
- this._header.releasePointerCapture(event.pointerId);
163
- this._drag_state = null;
164
- this._drag_moved = false;
149
+ private onPointerLost = (event: PointerEvent): void => {
150
+ this.releasePointerCapture(event.pointerId);
151
+ this._drag = null;
165
152
  };
166
153
 
167
154
  private drawTabs = (event: RegularLayoutEvent) => {
168
- const slot = this.getAttribute(
169
- this._layout.savePhysics().CHILD_ATTRIBUTE_NAME,
170
- );
171
-
155
+ const attr = this._layout.savePhysics().CHILD_ATTRIBUTE_NAME;
156
+ const slot = this.getAttribute(attr);
172
157
  if (!slot) {
173
158
  return;
174
159
  }
@@ -178,12 +163,12 @@ export class RegularLayoutFrame extends HTMLElement {
178
163
  if (!new_tab_panel) {
179
164
  new_tab_panel = {
180
165
  type: "child-panel",
181
- child: [slot],
166
+ tabs: [slot],
182
167
  selected: 0,
183
168
  };
184
169
  }
185
170
 
186
- for (let i = 0; i < new_tab_panel.child.length; i++) {
171
+ for (let i = 0; i < new_tab_panel.tabs.length; i++) {
187
172
  if (i >= this._header.children.length) {
188
173
  const new_tab = document.createElement("regular-layout-tab");
189
174
  new_tab.populate(this._layout, new_tab_panel, i);
@@ -195,7 +180,7 @@ export class RegularLayoutFrame extends HTMLElement {
195
180
  }
196
181
  }
197
182
 
198
- const last_index = new_tab_panel.child.length;
183
+ const last_index = new_tab_panel.tabs.length;
199
184
  for (let j = this._header.children.length - 1; j >= last_index; j--) {
200
185
  this._header.removeChild(this._header.children[j]);
201
186
  }