regular-layout 0.1.0 → 0.2.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.
Files changed (42) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +6 -6
  3. package/dist/extensions.d.ts +5 -4
  4. package/dist/index.d.ts +3 -3
  5. package/dist/index.js +15 -10
  6. package/dist/index.js.map +4 -4
  7. package/dist/{common → layout}/calculate_edge.d.ts +2 -2
  8. package/dist/{common → layout}/calculate_intersect.d.ts +7 -9
  9. package/dist/{common → layout}/constants.d.ts +15 -1
  10. package/dist/{common → layout}/flatten.d.ts +1 -1
  11. package/dist/{common → layout}/generate_grid.d.ts +3 -3
  12. package/dist/layout/generate_overlay.d.ts +2 -0
  13. package/dist/{common → layout}/insert_child.d.ts +3 -2
  14. package/dist/{common → layout}/redistribute_panel_sizes.d.ts +1 -1
  15. package/dist/{common → layout}/remove_child.d.ts +1 -1
  16. package/dist/{common/layout_config.d.ts → layout/types.d.ts} +6 -10
  17. package/dist/regular-layout-frame.d.ts +1 -4
  18. package/dist/regular-layout-tab.d.ts +26 -0
  19. package/dist/regular-layout.d.ts +23 -18
  20. package/package.json +9 -7
  21. package/src/extensions.ts +10 -4
  22. package/src/index.ts +3 -7
  23. package/src/layout/calculate_edge.ts +209 -0
  24. package/src/{common → layout}/calculate_intersect.ts +59 -101
  25. package/src/{common → layout}/constants.ts +18 -1
  26. package/src/{common → layout}/flatten.ts +1 -1
  27. package/src/{common → layout}/generate_grid.ts +76 -106
  28. package/src/{common → layout}/generate_overlay.ts +24 -12
  29. package/src/{common → layout}/insert_child.ts +105 -51
  30. package/src/{common → layout}/redistribute_panel_sizes.ts +1 -1
  31. package/src/{common → layout}/remove_child.ts +2 -2
  32. package/src/{common/layout_config.ts → layout/types.ts} +6 -19
  33. package/src/regular-layout-frame.ts +34 -71
  34. package/src/regular-layout-tab.ts +103 -0
  35. package/src/regular-layout.ts +190 -141
  36. package/themes/chicago.css +89 -0
  37. package/themes/fluxbox.css +110 -0
  38. package/themes/gibson.css +264 -0
  39. package/themes/hotdog.css +88 -0
  40. package/themes/lorax.css +129 -0
  41. package/dist/common/generate_overlay.d.ts +0 -2
  42. package/src/common/calculate_edge.ts +0 -104
@@ -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 } from "./layout_config.ts";
12
+ import type { Layout } from "./types.ts";
13
13
 
14
14
  /**
15
15
  * Inserts a new child panel into the layout tree at a specified location.
@@ -22,15 +22,20 @@ import type { Layout } from "./layout_config.ts";
22
22
  * at root level.
23
23
  * @param orientation - Orientation for newly created split panels. Defaults to
24
24
  * "horizontal".
25
+ * @param is_edge - If true, create the split at the parent level.
25
26
  * @returns A new layout tree with the child inserted (original is not mutated).
26
27
  */
27
28
  export function insert_child(
28
29
  panel: Layout,
29
30
  child: string,
30
31
  path: number[],
31
- orientation: "horizontal" | "vertical" = "horizontal",
32
- is_edge?: boolean,
32
+ orientation?: "horizontal" | "vertical",
33
33
  ): Layout {
34
+ const createChildPanel = (childId: string): Layout => ({
35
+ type: "child-panel",
36
+ child: [childId],
37
+ });
38
+
34
39
  if (path.length === 0) {
35
40
  // Insert at root level
36
41
  if (panel.type === "child-panel") {
@@ -39,92 +44,136 @@ export function insert_child(
39
44
  type: "child-panel",
40
45
  child: [child, ...panel.child],
41
46
  };
47
+ } else if (orientation) {
48
+ // When inserting at edge of root, wrap the entire panel in a new split
49
+ return {
50
+ type: "split-panel",
51
+ orientation: orientation,
52
+ children: [createChildPanel(child), panel],
53
+ sizes: [0.5, 0.5],
54
+ };
42
55
  } else {
43
56
  // Append to existing split-panel
44
- const newChildren = [
45
- ...panel.children,
46
- {
47
- type: "child-panel",
48
- child: [child],
49
- } as Layout,
50
- ];
51
-
52
- const numChildren = newChildren.length;
53
- const newSizes = Array(numChildren).fill(1 / numChildren);
57
+ const newChildren = [...panel.children, createChildPanel(child)];
58
+ const newSizes = [...panel.sizes, 1 / (newChildren.length - 1)];
54
59
  return {
55
60
  ...panel,
56
61
  children: newChildren,
57
- sizes: newSizes,
62
+ sizes: redistribute(newSizes),
58
63
  };
59
64
  }
60
65
  }
61
66
 
62
67
  // Navigate down the path
63
68
  const [index, ...restPath] = path;
69
+
70
+ // Special case: when orientation is provided and restPath is empty, handle edge insertion
71
+ if (orientation && restPath.length === 0) {
72
+ // If panel is a split-panel with the same orientation, insert into its children
73
+ if (panel.type === "split-panel" && panel.orientation === orientation) {
74
+ const newChildren = [...panel.children];
75
+ newChildren.splice(index, 0, createChildPanel(child));
76
+ const newSizes = [...panel.sizes];
77
+ newSizes.splice(index, 0, 1 / (newChildren.length - 1));
78
+ return {
79
+ ...panel,
80
+ children: newChildren,
81
+ sizes: redistribute(newSizes),
82
+ };
83
+ }
84
+
85
+ // Otherwise, wrap the entire panel in a new split at the edge
86
+ const children =
87
+ index === 0
88
+ ? [createChildPanel(child), panel]
89
+ : [panel, createChildPanel(child)];
90
+
91
+ return {
92
+ type: "split-panel",
93
+ orientation: orientation,
94
+ children,
95
+ sizes: [0.5, 0.5],
96
+ };
97
+ }
98
+
64
99
  if (panel.type === "child-panel") {
65
- // This shouldn't happen if path.length > 0, but handle it gracefully
66
- // We need to split this child-panel
100
+ // Stack into child array only when ALL of these conditions are met:
101
+ // 1. Path has exactly one element (restPath is empty)
102
+ // 2. Orientation was NOT explicitly provided (orientation is undefined)
103
+ // 3. Index is within the valid stacking range [0, child.length]
104
+ if (
105
+ restPath.length === 0 &&
106
+ orientation === undefined &&
107
+ index >= 0 &&
108
+ index <= panel.child.length
109
+ ) {
110
+ const newChild = [...panel.child];
111
+ newChild.splice(index, 0, child);
112
+ return {
113
+ ...panel,
114
+ child: newChild,
115
+ };
116
+ }
117
+
118
+ // Otherwise, wrap in a split panel and recurse
67
119
  const newPanel: Layout = {
68
120
  type: "split-panel",
69
- orientation,
121
+ orientation: orientation || "horizontal",
70
122
  children: [panel],
71
123
  sizes: [1],
72
124
  };
73
125
 
74
- return insert_child(newPanel, child, path, orientation, is_edge);
126
+ return insert_child(newPanel, child, path, orientation);
75
127
  }
76
128
 
77
129
  if (restPath.length === 0 || index === panel.children.length) {
78
- if (is_edge && panel.children[index]?.type === "child-panel") {
79
- panel.children[index].child.unshift(child);
80
- panel.children[index].selected = 0;
81
- return panel;
130
+ if (orientation && panel.children[index]) {
131
+ // When inserting at an edge, create a split panel with the new child and existing child
132
+ const newSplitPanel: Layout = {
133
+ type: "split-panel",
134
+ orientation: orientation,
135
+ children: [createChildPanel(child), panel.children[index]],
136
+ sizes: [0.5, 0.5],
137
+ };
138
+
139
+ const newChildren = [...panel.children];
140
+ newChildren[index] = newSplitPanel;
141
+ return {
142
+ ...panel,
143
+ children: newChildren,
144
+ sizes: redistribute(panel.sizes),
145
+ };
82
146
  }
83
147
 
84
148
  // Insert at this level at the specified index
85
149
  const newChildren = [...panel.children];
86
- newChildren.splice(index, 0, {
87
- type: "child-panel",
88
- child: [child],
89
- });
90
-
91
- const numChildren = newChildren.length;
92
- const newSizes = Array(numChildren).fill(1 / numChildren);
150
+ newChildren.splice(index, 0, createChildPanel(child));
151
+ const newSizes = [...panel.sizes];
152
+ newSizes.splice(index, 0, 1 / (newChildren.length - 1));
93
153
  return {
94
154
  ...panel,
95
155
  children: newChildren,
96
- sizes: newSizes,
156
+ sizes: redistribute(newSizes),
97
157
  };
98
158
  }
99
159
 
100
160
  const targetChild = panel.children[index];
101
- if (targetChild.type === "child-panel" && restPath.length > 0) {
102
- // Need to split this child-panel
103
- const oppositeOrientation =
104
- panel.orientation === "horizontal" ? "vertical" : "horizontal";
105
-
106
- const newSplitPanel = insert_child(
107
- targetChild,
108
- child,
109
- restPath,
110
- oppositeOrientation,
111
- is_edge,
112
- );
113
161
 
114
- const newChildren = [...panel.children];
115
- newChildren[index] = newSplitPanel;
116
- return {
117
- ...panel,
118
- children: newChildren,
119
- };
120
- }
162
+ // Determine the orientation to pass down when navigating into a child-panel
163
+ const childOrientation =
164
+ targetChild.type === "child-panel" &&
165
+ restPath.length > 0 &&
166
+ orientation !== undefined
167
+ ? panel.orientation === "horizontal"
168
+ ? "vertical"
169
+ : "horizontal"
170
+ : orientation;
121
171
 
122
172
  const updatedChild = insert_child(
123
173
  targetChild,
124
174
  child,
125
175
  restPath,
126
- orientation,
127
- is_edge,
176
+ childOrientation,
128
177
  );
129
178
 
130
179
  const newChildren = [...panel.children];
@@ -134,3 +183,8 @@ export function insert_child(
134
183
  children: newChildren,
135
184
  };
136
185
  }
186
+
187
+ function redistribute(arr: number[]): number[] {
188
+ const total = arr.reduce((sum, val) => sum + val, 0);
189
+ return arr.map((val) => val / total);
190
+ }
@@ -10,7 +10,7 @@
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
12
  import { MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD } from "./constants.ts";
13
- import type { Layout } from "./layout_config.ts";
13
+ import type { Layout } from "./types.ts";
14
14
 
15
15
  /**
16
16
  * Adjusts panel sizes during a drag operation on a divider.
@@ -9,8 +9,8 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import type { Layout, TabLayout } from "./layout_config.ts";
13
- import { EMPTY_PANEL } from "./layout_config.ts";
12
+ import type { Layout, TabLayout } from "./types.ts";
13
+ import { EMPTY_PANEL } from "./types.ts";
14
14
 
15
15
  /**
16
16
  * Removes a child panel from the layout tree by its name.
@@ -40,6 +40,11 @@ export interface ViewWindow {
40
40
  * Child panels are arranged either horizontally (side by side) or vertically
41
41
  * (stacked), via the `orientation` property `"horizzontal"` and `"vertical"`
42
42
  * (respectively).
43
+ *
44
+ * While the type structure of `SplitLayout` allows nesting levels with the same
45
+ * `orientation`, calling `RegularLayout.restore` with such a `Layout` will be
46
+ * flattened to the equivalent layout with every child guaranteed to have the
47
+ * opposite `orientation` as its parent.
43
48
  */
44
49
  export interface SplitLayout {
45
50
  type: "split-panel";
@@ -78,7 +83,6 @@ export interface LayoutDivider {
78
83
  export interface LayoutPath<T = undefined> {
79
84
  type: "layout-path";
80
85
  slot: string;
81
- panel: TabLayout;
82
86
  path: number[];
83
87
  view_window: ViewWindow;
84
88
  column: number;
@@ -87,24 +91,7 @@ export interface LayoutPath<T = undefined> {
87
91
  row_offset: number;
88
92
  orientation: Orientation;
89
93
  is_edge: boolean;
90
- box: T;
91
- }
92
-
93
- /**
94
- * Recursively iterates over all child panel names in the layout tree, yielding
95
- * panel names in depth-first order.
96
- *
97
- * @param panel - The layout tree to iterate over
98
- * @returns Generator yielding child panel names
99
- */
100
- export function* iter_panel_children(panel: Layout): Generator<string> {
101
- if (panel.type === "split-panel") {
102
- for (const child of panel.children) {
103
- yield* iter_panel_children(child);
104
- }
105
- } else {
106
- yield panel.child[panel.selected || 0];
107
- }
94
+ layout: T;
108
95
  }
109
96
 
110
97
  /**
@@ -9,20 +9,26 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import { MIN_DRAG_DISTANCE, OVERLAY_CLASSNAME } from "./common/constants.ts";
13
- import type { LayoutPath, TabLayout } from "./common/layout_config.ts";
12
+ import { MIN_DRAG_DISTANCE, OVERLAY_CLASSNAME } from "./layout/constants.ts";
13
+ import type { Layout, LayoutPath } from "./layout/types.ts";
14
14
  import type { RegularLayoutEvent } from "./extensions.ts";
15
15
  import type { RegularLayout } from "./regular-layout.ts";
16
-
17
- const CSS = (className: string) => `
18
- :host{--titlebar--height:24px;box-sizing:border-box}
19
- :host(:not(.${className})){margin-top:calc(var(--titlebar--height) + 3px)!important;}
20
- :host(:not(.${className}))::part(container){position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;background-color:inherit;border-radius:inherit}
21
- :host(:not(.${className}))::part(titlebar){height:var(--titlebar--height);margin-top:calc(0px - var(--titlebar--height));user-select: none;}
22
- :host(:not(.${className}))::part(body){flex:1 1 auto;}
16
+ import type { RegularLayoutTab } from "./regular-layout-tab.ts";
17
+
18
+ const CSS = `
19
+ :host{box-sizing:border-box;flex-direction:column}
20
+ :host::part(titlebar){display:flex;height:24px;user-select:none;overflow:hidden}
21
+ :host::part(container){flex:1 1 auto}
22
+ :host::part(title){flex:1 1 auto;pointer-events:none}
23
+ :host::part(close){align-self:stretch}
24
+ :host::slotted{flex:1 1 auto;}
25
+ :host regular-layout-tab{width:0px;}
23
26
  `;
24
27
 
25
- const HTML_TEMPLATE = `<slot part="container"><slot part="titlebar"></slot><slot part="body"><slot></slot></slot></slot>`;
28
+ const HTML_TEMPLATE = `
29
+ <div part="titlebar"></div>
30
+ <slot part="container"></slot>
31
+ `;
26
32
 
27
33
  /**
28
34
  * A custom element that represents a draggable panel within a
@@ -43,7 +49,7 @@ const HTML_TEMPLATE = `<slot part="container"><slot part="titlebar"></slot><slot
43
49
  * @example
44
50
  * ```html
45
51
  * <regular-layout>
46
- * <regular-layout-frame slot="panel-1">
52
+ * <regular-layout-frame name="panel-1">
47
53
  * <!-- Panel content here -->
48
54
  * </regular-layout-frame>
49
55
  * </regular-layout>
@@ -54,14 +60,13 @@ export class RegularLayoutFrame extends HTMLElement {
54
60
  private _container_sheet: CSSStyleSheet;
55
61
  private _layout!: RegularLayout;
56
62
  private _header!: HTMLElement;
57
- private _drag_state: LayoutPath<DOMRect> | null = null;
63
+ private _drag_state: LayoutPath<Layout> | null = null;
58
64
  private _drag_moved: boolean = false;
59
- private _tab_to_index_map: WeakMap<HTMLDivElement, number> = new WeakMap();
60
- private _tab_panel_state: TabLayout | null = null;
65
+ private _tab_to_index_map: WeakMap<RegularLayoutTab, number> = new WeakMap();
61
66
  constructor() {
62
67
  super();
63
68
  this._container_sheet = new CSSStyleSheet();
64
- this._container_sheet.replaceSync(CSS(OVERLAY_CLASSNAME));
69
+ this._container_sheet.replaceSync(CSS);
65
70
  this._shadowRoot = this.attachShadow({ mode: "open" });
66
71
  this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
67
72
  }
@@ -69,7 +74,7 @@ export class RegularLayoutFrame extends HTMLElement {
69
74
  connectedCallback() {
70
75
  this._shadowRoot.innerHTML = HTML_TEMPLATE;
71
76
  this._layout = this.parentElement as RegularLayout;
72
- this._header = this._shadowRoot.children[0].children[0] as HTMLElement;
77
+ this._header = this._shadowRoot.children[0] as HTMLElement;
73
78
  this._header.addEventListener("pointerdown", this.onPointerDown);
74
79
  this._header.addEventListener("pointermove", this.onPointerMove);
75
80
  this._header.addEventListener("pointerup", this.onPointerUp);
@@ -94,7 +99,7 @@ export class RegularLayoutFrame extends HTMLElement {
94
99
  }
95
100
 
96
101
  private onPointerDown = (event: PointerEvent): void => {
97
- const elem = event.target as HTMLDivElement;
102
+ const elem = event.target as RegularLayoutTab;
98
103
  if (elem.part.contains("tab")) {
99
104
  this._drag_state = this._layout.calculateIntersect(
100
105
  event.clientX,
@@ -104,11 +109,6 @@ export class RegularLayoutFrame extends HTMLElement {
104
109
  if (this._drag_state) {
105
110
  this._header.setPointerCapture(event.pointerId);
106
111
  event.preventDefault();
107
- const last_index = this._drag_state.path.length - 1;
108
- const selected = this._tab_to_index_map.get(elem);
109
- if (selected) {
110
- this._drag_state.path[last_index] = selected;
111
- }
112
112
  }
113
113
  }
114
114
  };
@@ -165,35 +165,30 @@ export class RegularLayoutFrame extends HTMLElement {
165
165
  };
166
166
 
167
167
  private drawTabs = (event: RegularLayoutEvent) => {
168
- const slot = this.assignedSlot;
168
+ const slot = this.getAttribute("name");
169
169
  if (!slot) {
170
170
  return;
171
171
  }
172
172
 
173
173
  const new_panel = event.detail;
174
- const new_tab_panel = this._layout.getPanel(slot.name, new_panel);
174
+ let new_tab_panel = this._layout.getPanel(slot, new_panel);
175
175
  if (!new_tab_panel) {
176
- return;
176
+ new_tab_panel = {
177
+ type: "child-panel",
178
+ child: [slot],
179
+ selected: 0,
180
+ };
177
181
  }
178
182
 
179
183
  for (let i = 0; i < new_tab_panel.child.length; i++) {
180
184
  if (i >= this._header.children.length) {
181
- const new_tab = this.createTab(new_tab_panel, i);
185
+ const new_tab = document.createElement("regular-layout-tab");
186
+ new_tab.populate(this._layout, new_tab_panel, i);
182
187
  this._header.appendChild(new_tab);
188
+ this._tab_to_index_map.set(new_tab, i);
183
189
  } else {
184
- const tab_changed =
185
- (i === new_tab_panel.selected) !==
186
- (i === this._tab_panel_state?.selected);
187
-
188
- const tab = this._header.children[i] as HTMLDivElement;
189
- const index_changed =
190
- tab_changed ||
191
- this._tab_panel_state?.child[i] !== new_tab_panel.child[i];
192
-
193
- if (index_changed) {
194
- const new_tab = this.createTab(new_tab_panel, i);
195
- this._header.replaceChild(new_tab, tab);
196
- }
190
+ const tab = this._header.children[i] as RegularLayoutTab;
191
+ tab.populate(this._layout, new_tab_panel, i);
197
192
  }
198
193
  }
199
194
 
@@ -201,37 +196,5 @@ export class RegularLayoutFrame extends HTMLElement {
201
196
  for (let j = this._header.children.length - 1; j >= last_index; j--) {
202
197
  this._header.removeChild(this._header.children[j]);
203
198
  }
204
-
205
- this._tab_panel_state = new_tab_panel;
206
- };
207
-
208
- private createTab = (tab_panel: TabLayout, index: number): HTMLDivElement => {
209
- const selected = tab_panel.selected || 0;
210
- const tab = document.createElement("div");
211
- this._tab_to_index_map.set(tab, index);
212
- tab.textContent = tab_panel.child[index] || "";
213
- if (index === selected) {
214
- tab.setAttribute("part", "tab active-tab");
215
- } else {
216
- tab.setAttribute("part", "tab");
217
- tab.addEventListener("pointerdown", (_) =>
218
- this.onTabClick(tab_panel, index),
219
- );
220
- }
221
-
222
- return tab;
223
- };
224
-
225
- private onTabClick = (tab_panel: TabLayout, index: number) => {
226
- const new_layout = this._layout.save();
227
- const new_tab_panel = this._layout.getPanel(
228
- tab_panel.child[index],
229
- new_layout,
230
- );
231
-
232
- if (new_tab_panel) {
233
- new_tab_panel.selected = index;
234
- this._layout.restore(new_layout);
235
- }
236
199
  };
237
200
  }
@@ -0,0 +1,103 @@
1
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ // ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ // ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ // ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ // ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ // ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+
12
+ import type { TabLayout } from "./layout/types.ts";
13
+ import type { RegularLayout } from "./regular-layout.ts";
14
+
15
+ /**
16
+ * A custom HTML element representing an individual tab in a tab panel.
17
+ *
18
+ * This element manages the visual representation and interactions for a single tab,
19
+ * including selection state, close functionality, and content display.
20
+ */
21
+ export class RegularLayoutTab extends HTMLElement {
22
+ private _layout?: RegularLayout;
23
+ private _tab_panel?: TabLayout;
24
+ private _index?: number;
25
+
26
+ /**
27
+ * Populates or updates the tab with layout information.
28
+ *
29
+ * This method initializes the tab's content and event listeners on first call,
30
+ * and efficiently updates only the changed properties on subsequent calls.
31
+ *
32
+ * @param layout - The parent RegularLayout instance managing this tab.
33
+ * @param tab_panel - The tab panel layout containing this tab.
34
+ * @param index - The index of this tab within the tab panel.
35
+ */
36
+ populate = (layout: RegularLayout, tab_panel: TabLayout, index: number) => {
37
+ if (this._tab_panel) {
38
+ const tab_changed =
39
+ (index === tab_panel.selected) !==
40
+ (index === this._tab_panel?.selected);
41
+
42
+ const index_changed =
43
+ tab_changed || this._tab_panel?.child[index] !== tab_panel.child[index];
44
+
45
+ if (index_changed) {
46
+ const selected = tab_panel.selected === index;
47
+ const slot = tab_panel.child[index];
48
+ this.children[0].textContent = slot;
49
+
50
+ if (selected) {
51
+ this.children[1].part.add("active-close");
52
+ this.part.add("active-tab");
53
+ } else {
54
+ this.children[1].part.remove("active-close");
55
+ this.part.remove("active-tab");
56
+ }
57
+ }
58
+ } else {
59
+ const slot = tab_panel.child[index];
60
+ const selected = tab_panel.selected === index;
61
+ const parts = selected ? "active-close close" : "close";
62
+ this.innerHTML = `<div part="title"></div><button part="${parts}"></button>`;
63
+ if (selected) {
64
+ this.part.add("tab", "active-tab");
65
+ } else {
66
+ this.part.add("tab");
67
+ }
68
+
69
+ this.addEventListener("pointerdown", this.onTabClick);
70
+ this.children[0].textContent = slot;
71
+ this.children[1].addEventListener("pointerdown", this.onTabClose);
72
+ }
73
+
74
+ this._tab_panel = tab_panel;
75
+ this._layout = layout;
76
+ this._index = index;
77
+ };
78
+
79
+ private onTabClose = (_: Event) => {
80
+ if (this._tab_panel !== undefined && this._index !== undefined) {
81
+ this._layout?.removePanel(this._tab_panel.child[this._index]);
82
+ }
83
+ };
84
+
85
+ private onTabClick = (_: PointerEvent) => {
86
+ if (
87
+ this._tab_panel !== undefined &&
88
+ this._index !== undefined &&
89
+ this._index !== this._tab_panel.selected
90
+ ) {
91
+ const new_layout = this._layout?.save();
92
+ const new_tab_panel = this._layout?.getPanel(
93
+ this._tab_panel.child[this._index],
94
+ new_layout,
95
+ );
96
+
97
+ if (new_tab_panel && new_layout) {
98
+ new_tab_panel.selected = this._index;
99
+ this._layout?.restore(new_layout);
100
+ }
101
+ }
102
+ };
103
+ }