regular-layout 0.1.0 → 0.2.1
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 +3 -2
- package/dist/{common → layout}/calculate_intersect.d.ts +13 -9
- package/dist/layout/constants.d.ts +81 -0
- package/dist/{common → layout}/flatten.d.ts +1 -1
- package/dist/{common → layout}/generate_grid.d.ts +5 -4
- 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 +2 -2
- 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 +37 -18
- package/package.json +9 -7
- package/src/extensions.ts +10 -4
- package/src/index.ts +3 -7
- package/src/layout/calculate_edge.ts +217 -0
- package/src/{common → layout}/calculate_intersect.ts +61 -101
- package/src/layout/constants.ts +119 -0
- package/src/{common → layout}/flatten.ts +1 -1
- package/src/{common → layout}/generate_grid.ts +120 -106
- package/src/{common → layout}/generate_overlay.ts +26 -12
- package/src/{common → layout}/insert_child.ts +105 -51
- package/src/{common → layout}/redistribute_panel_sizes.ts +11 -4
- package/src/{common → layout}/remove_child.ts +2 -2
- package/src/{common/layout_config.ts → layout/types.ts} +7 -19
- package/src/regular-layout-frame.ts +40 -74
- package/src/regular-layout-tab.ts +103 -0
- package/src/regular-layout.ts +260 -148
- 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 +130 -0
- package/dist/common/constants.d.ts +0 -29
- package/dist/common/generate_overlay.d.ts +0 -2
- package/src/common/calculate_edge.ts +0 -104
- package/src/common/constants.ts +0 -46
|
@@ -34,12 +34,18 @@ export interface ViewWindow {
|
|
|
34
34
|
col_end: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
|
|
37
38
|
/**
|
|
38
39
|
* A split panel that divides space among multiple child layouts
|
|
39
40
|
* .
|
|
40
41
|
* Child panels are arranged either horizontally (side by side) or vertically
|
|
41
42
|
* (stacked), via the `orientation` property `"horizzontal"` and `"vertical"`
|
|
42
43
|
* (respectively).
|
|
44
|
+
*
|
|
45
|
+
* While the type structure of `SplitLayout` allows nesting levels with the same
|
|
46
|
+
* `orientation`, calling `RegularLayout.restore` with such a `Layout` will be
|
|
47
|
+
* flattened to the equivalent layout with every child guaranteed to have the
|
|
48
|
+
* opposite `orientation` as its parent.
|
|
43
49
|
*/
|
|
44
50
|
export interface SplitLayout {
|
|
45
51
|
type: "split-panel";
|
|
@@ -78,7 +84,6 @@ export interface LayoutDivider {
|
|
|
78
84
|
export interface LayoutPath<T = undefined> {
|
|
79
85
|
type: "layout-path";
|
|
80
86
|
slot: string;
|
|
81
|
-
panel: TabLayout;
|
|
82
87
|
path: number[];
|
|
83
88
|
view_window: ViewWindow;
|
|
84
89
|
column: number;
|
|
@@ -87,24 +92,7 @@ export interface LayoutPath<T = undefined> {
|
|
|
87
92
|
row_offset: number;
|
|
88
93
|
orientation: Orientation;
|
|
89
94
|
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
|
-
}
|
|
95
|
+
layout: T;
|
|
108
96
|
}
|
|
109
97
|
|
|
110
98
|
/**
|
|
@@ -9,20 +9,25 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import type { LayoutPath, TabLayout } from "./common/layout_config.ts";
|
|
12
|
+
import type { Layout, LayoutPath } from "./layout/types.ts";
|
|
14
13
|
import type { RegularLayoutEvent } from "./extensions.ts";
|
|
15
14
|
import type { RegularLayout } from "./regular-layout.ts";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
:host
|
|
20
|
-
:host
|
|
21
|
-
:host
|
|
22
|
-
:host
|
|
15
|
+
import type { RegularLayoutTab } from "./regular-layout-tab.ts";
|
|
16
|
+
|
|
17
|
+
const CSS = `
|
|
18
|
+
:host{box-sizing:border-box;flex-direction:column}
|
|
19
|
+
:host::part(titlebar){display:flex;height:24px;user-select:none;overflow:hidden}
|
|
20
|
+
:host::part(container){flex:1 1 auto}
|
|
21
|
+
:host::part(title){flex:1 1 auto;pointer-events:none}
|
|
22
|
+
:host::part(close){align-self:stretch}
|
|
23
|
+
:host::slotted{flex:1 1 auto;}
|
|
24
|
+
:host regular-layout-tab{width:0px;}
|
|
23
25
|
`;
|
|
24
26
|
|
|
25
|
-
const HTML_TEMPLATE =
|
|
27
|
+
const HTML_TEMPLATE = `
|
|
28
|
+
<div part="titlebar"></div>
|
|
29
|
+
<slot part="container"></slot>
|
|
30
|
+
`;
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* A custom element that represents a draggable panel within a
|
|
@@ -43,7 +48,7 @@ const HTML_TEMPLATE = `<slot part="container"><slot part="titlebar"></slot><slot
|
|
|
43
48
|
* @example
|
|
44
49
|
* ```html
|
|
45
50
|
* <regular-layout>
|
|
46
|
-
* <regular-layout-frame
|
|
51
|
+
* <regular-layout-frame name="panel-1">
|
|
47
52
|
* <!-- Panel content here -->
|
|
48
53
|
* </regular-layout-frame>
|
|
49
54
|
* </regular-layout>
|
|
@@ -54,14 +59,13 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
54
59
|
private _container_sheet: CSSStyleSheet;
|
|
55
60
|
private _layout!: RegularLayout;
|
|
56
61
|
private _header!: HTMLElement;
|
|
57
|
-
private _drag_state: LayoutPath<
|
|
62
|
+
private _drag_state: LayoutPath<Layout> | null = null;
|
|
58
63
|
private _drag_moved: boolean = false;
|
|
59
|
-
private _tab_to_index_map: WeakMap<
|
|
60
|
-
private _tab_panel_state: TabLayout | null = null;
|
|
64
|
+
private _tab_to_index_map: WeakMap<RegularLayoutTab, number> = new WeakMap();
|
|
61
65
|
constructor() {
|
|
62
66
|
super();
|
|
63
67
|
this._container_sheet = new CSSStyleSheet();
|
|
64
|
-
this._container_sheet.replaceSync(CSS
|
|
68
|
+
this._container_sheet.replaceSync(CSS);
|
|
65
69
|
this._shadowRoot = this.attachShadow({ mode: "open" });
|
|
66
70
|
this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
|
|
67
71
|
}
|
|
@@ -69,7 +73,7 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
69
73
|
connectedCallback() {
|
|
70
74
|
this._shadowRoot.innerHTML = HTML_TEMPLATE;
|
|
71
75
|
this._layout = this.parentElement as RegularLayout;
|
|
72
|
-
this._header = this._shadowRoot.children[0]
|
|
76
|
+
this._header = this._shadowRoot.children[0] as HTMLElement;
|
|
73
77
|
this._header.addEventListener("pointerdown", this.onPointerDown);
|
|
74
78
|
this._header.addEventListener("pointermove", this.onPointerMove);
|
|
75
79
|
this._header.addEventListener("pointerup", this.onPointerUp);
|
|
@@ -94,7 +98,7 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
private onPointerDown = (event: PointerEvent): void => {
|
|
97
|
-
const elem = event.target as
|
|
101
|
+
const elem = event.target as RegularLayoutTab;
|
|
98
102
|
if (elem.part.contains("tab")) {
|
|
99
103
|
this._drag_state = this._layout.calculateIntersect(
|
|
100
104
|
event.clientX,
|
|
@@ -104,17 +108,14 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
104
108
|
if (this._drag_state) {
|
|
105
109
|
this._header.setPointerCapture(event.pointerId);
|
|
106
110
|
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
111
|
}
|
|
113
112
|
}
|
|
114
113
|
};
|
|
115
114
|
|
|
116
115
|
private onPointerMove = (event: PointerEvent): void => {
|
|
117
116
|
if (this._drag_state) {
|
|
117
|
+
const physics = this._layout.savePhysics();
|
|
118
|
+
|
|
118
119
|
// Only initiate a drag if the cursor has moved sufficiently.
|
|
119
120
|
if (!this._drag_moved) {
|
|
120
121
|
const [current_col, current_row, box] =
|
|
@@ -122,7 +123,7 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
122
123
|
|
|
123
124
|
const dx = (current_col - this._drag_state.column) * box.width;
|
|
124
125
|
const dy = (current_row - this._drag_state.row) * box.height;
|
|
125
|
-
if (Math.sqrt(dx * dx + dy * dy) <= MIN_DRAG_DISTANCE) {
|
|
126
|
+
if (Math.sqrt(dx * dx + dy * dy) <= physics.MIN_DRAG_DISTANCE) {
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
129
|
}
|
|
@@ -132,7 +133,7 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
132
133
|
event.clientX,
|
|
133
134
|
event.clientY,
|
|
134
135
|
this._drag_state,
|
|
135
|
-
OVERLAY_CLASSNAME,
|
|
136
|
+
physics.OVERLAY_CLASSNAME,
|
|
136
137
|
);
|
|
137
138
|
}
|
|
138
139
|
};
|
|
@@ -143,7 +144,6 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
143
144
|
event.clientX,
|
|
144
145
|
event.clientY,
|
|
145
146
|
this._drag_state,
|
|
146
|
-
OVERLAY_CLASSNAME,
|
|
147
147
|
);
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -165,35 +165,33 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
165
165
|
};
|
|
166
166
|
|
|
167
167
|
private drawTabs = (event: RegularLayoutEvent) => {
|
|
168
|
-
const slot = this.
|
|
168
|
+
const slot = this.getAttribute(
|
|
169
|
+
this._layout.savePhysics().CHILD_ATTRIBUTE_NAME,
|
|
170
|
+
);
|
|
171
|
+
|
|
169
172
|
if (!slot) {
|
|
170
173
|
return;
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
const new_panel = event.detail;
|
|
174
|
-
|
|
177
|
+
let new_tab_panel = this._layout.getPanel(slot, new_panel);
|
|
175
178
|
if (!new_tab_panel) {
|
|
176
|
-
|
|
179
|
+
new_tab_panel = {
|
|
180
|
+
type: "child-panel",
|
|
181
|
+
child: [slot],
|
|
182
|
+
selected: 0,
|
|
183
|
+
};
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
for (let i = 0; i < new_tab_panel.child.length; i++) {
|
|
180
187
|
if (i >= this._header.children.length) {
|
|
181
|
-
const new_tab =
|
|
188
|
+
const new_tab = document.createElement("regular-layout-tab");
|
|
189
|
+
new_tab.populate(this._layout, new_tab_panel, i);
|
|
182
190
|
this._header.appendChild(new_tab);
|
|
191
|
+
this._tab_to_index_map.set(new_tab, i);
|
|
183
192
|
} 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
|
-
}
|
|
193
|
+
const tab = this._header.children[i] as RegularLayoutTab;
|
|
194
|
+
tab.populate(this._layout, new_tab_panel, i);
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
197
|
|
|
@@ -201,37 +199,5 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
201
199
|
for (let j = this._header.children.length - 1; j >= last_index; j--) {
|
|
202
200
|
this._header.removeChild(this._header.children[j]);
|
|
203
201
|
}
|
|
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
202
|
};
|
|
237
203
|
}
|
|
@@ -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
|
+
}
|