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.
- package/LICENSE.md +1 -1
- package/README.md +6 -6
- package/dist/extensions.d.ts +5 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +15 -10
- package/dist/index.js.map +4 -4
- package/dist/{common → layout}/calculate_edge.d.ts +2 -2
- package/dist/{common → layout}/calculate_intersect.d.ts +7 -9
- package/dist/{common → layout}/constants.d.ts +15 -1
- package/dist/{common → layout}/flatten.d.ts +1 -1
- package/dist/{common → layout}/generate_grid.d.ts +3 -3
- package/dist/layout/generate_overlay.d.ts +2 -0
- package/dist/{common → layout}/insert_child.d.ts +3 -2
- package/dist/{common → layout}/redistribute_panel_sizes.d.ts +1 -1
- package/dist/{common → layout}/remove_child.d.ts +1 -1
- package/dist/{common/layout_config.d.ts → layout/types.d.ts} +6 -10
- package/dist/regular-layout-frame.d.ts +1 -4
- package/dist/regular-layout-tab.d.ts +26 -0
- package/dist/regular-layout.d.ts +23 -18
- package/package.json +9 -7
- package/src/extensions.ts +10 -4
- package/src/index.ts +3 -7
- package/src/layout/calculate_edge.ts +209 -0
- package/src/{common → layout}/calculate_intersect.ts +59 -101
- package/src/{common → layout}/constants.ts +18 -1
- package/src/{common → layout}/flatten.ts +1 -1
- package/src/{common → layout}/generate_grid.ts +76 -106
- package/src/{common → layout}/generate_overlay.ts +24 -12
- package/src/{common → layout}/insert_child.ts +105 -51
- package/src/{common → layout}/redistribute_panel_sizes.ts +1 -1
- package/src/{common → layout}/remove_child.ts +2 -2
- package/src/{common/layout_config.ts → layout/types.ts} +6 -19
- package/src/regular-layout-frame.ts +34 -71
- package/src/regular-layout-tab.ts +103 -0
- package/src/regular-layout.ts +190 -141
- package/themes/chicago.css +89 -0
- package/themes/fluxbox.css +110 -0
- package/themes/gibson.css +264 -0
- package/themes/hotdog.css +88 -0
- package/themes/lorax.css +129 -0
- package/dist/common/generate_overlay.d.ts +0 -2
- 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 "./
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
66
|
-
//
|
|
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
|
|
126
|
+
return insert_child(newPanel, child, path, orientation);
|
|
75
127
|
}
|
|
76
128
|
|
|
77
129
|
if (restPath.length === 0 || index === panel.children.length) {
|
|
78
|
-
if (
|
|
79
|
-
panel
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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 "./
|
|
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 "./
|
|
13
|
-
import { EMPTY_PANEL } from "./
|
|
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
|
-
|
|
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 "./
|
|
13
|
-
import type {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
:host
|
|
20
|
-
:host
|
|
21
|
-
:host
|
|
22
|
-
:host
|
|
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 =
|
|
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
|
|
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<
|
|
63
|
+
private _drag_state: LayoutPath<Layout> | null = null;
|
|
58
64
|
private _drag_moved: boolean = false;
|
|
59
|
-
private _tab_to_index_map: 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
|
|
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]
|
|
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
|
|
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.
|
|
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
|
-
|
|
174
|
+
let new_tab_panel = this._layout.getPanel(slot, new_panel);
|
|
175
175
|
if (!new_tab_panel) {
|
|
176
|
-
|
|
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 =
|
|
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
|
|
185
|
-
|
|
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
|
+
}
|