regular-layout 0.3.0 → 0.5.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/README.md +110 -22
- package/dist/{layout → core}/types.d.ts +6 -0
- package/dist/extensions.d.ts +13 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +4 -4
- package/dist/layout/calculate_edge.d.ts +2 -2
- package/dist/layout/calculate_intersect.d.ts +1 -1
- package/dist/layout/calculate_path.d.ts +1 -1
- package/dist/layout/calculate_presize_paths.d.ts +10 -0
- package/dist/layout/flatten.d.ts +1 -1
- package/dist/layout/generate_grid.d.ts +11 -2
- package/dist/layout/generate_overlay.d.ts +18 -2
- package/dist/layout/insert_child.d.ts +1 -1
- package/dist/layout/redistribute_panel_sizes.d.ts +2 -2
- package/dist/layout/remove_child.d.ts +1 -1
- package/dist/model/overlay_controller.d.ts +34 -0
- package/dist/model/presize_queue.d.ts +17 -0
- package/dist/regular-layout-frame.d.ts +21 -0
- package/dist/regular-layout-tab.d.ts +1 -1
- package/dist/regular-layout.d.ts +76 -9
- package/package.json +5 -4
- package/src/{layout → core}/constants.ts +2 -2
- package/src/{layout → core}/types.ts +7 -0
- package/src/extensions.ts +25 -1
- package/src/index.ts +3 -1
- package/src/layout/calculate_edge.ts +2 -2
- package/src/layout/calculate_intersect.ts +5 -2
- package/src/layout/calculate_path.ts +1 -1
- package/src/layout/calculate_presize_paths.ts +93 -0
- package/src/layout/flatten.ts +1 -1
- package/src/layout/generate_grid.ts +20 -2
- package/src/layout/generate_overlay.ts +48 -16
- package/src/layout/insert_child.ts +1 -1
- package/src/layout/redistribute_panel_sizes.ts +2 -2
- package/src/layout/remove_child.ts +2 -2
- package/src/model/overlay_controller.ts +161 -0
- package/src/model/presize_queue.ts +79 -0
- package/src/regular-layout-frame.ts +58 -3
- package/src/regular-layout-tab.ts +20 -22
- package/src/regular-layout.ts +252 -132
- package/themes/borland.css +103 -0
- package/themes/chicago.css +55 -49
- package/themes/fluxbox.css +64 -60
- package/themes/gibson.css +174 -164
- package/themes/hotdog.css +53 -47
- package/themes/lorax.css +82 -75
- /package/dist/{layout → core}/constants.d.ts +0 -0
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import { DEFAULT_PHYSICS, type Physics } from "
|
|
13
|
-
import type { Layout } from "
|
|
12
|
+
import { DEFAULT_PHYSICS, type Physics } from "../core/constants.ts";
|
|
13
|
+
import type { Layout } from "../core/types.ts";
|
|
14
14
|
|
|
15
15
|
interface GridCell {
|
|
16
16
|
child: string;
|
|
@@ -264,3 +264,21 @@ export function create_css_grid_layout(
|
|
|
264
264
|
|
|
265
265
|
return css.join("\n");
|
|
266
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generates CSS Grid styles that render a single panel maximized to fill the
|
|
270
|
+
* entire layout, hiding all other slotted children.
|
|
271
|
+
*
|
|
272
|
+
* @param name - The name of the panel to maximize.
|
|
273
|
+
* @param physics - Instance constants (defaults to {@link DEFAULT_PHYSICS}).
|
|
274
|
+
* @returns CSS string showing only `name`, full-size.
|
|
275
|
+
*/
|
|
276
|
+
export function create_css_maximize_layout(
|
|
277
|
+
name: string,
|
|
278
|
+
physics: Physics = DEFAULT_PHYSICS,
|
|
279
|
+
): string {
|
|
280
|
+
return [
|
|
281
|
+
host_template("100%", "100%"),
|
|
282
|
+
child_template(physics, name, "1", "1"),
|
|
283
|
+
].join("\n");
|
|
284
|
+
}
|
|
@@ -9,8 +9,46 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import { DEFAULT_PHYSICS } from "
|
|
13
|
-
import type { LayoutPath } from "
|
|
12
|
+
import { DEFAULT_PHYSICS } from "../core/constants";
|
|
13
|
+
import type { LayoutPath, ViewWindow } from "../core/types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a {@link ViewWindow} to element-relative pixel coordinates,
|
|
17
|
+
* accounting for padding and optionally CSS `gap` and child `margin`.
|
|
18
|
+
*
|
|
19
|
+
* @param window - The view window in normalized 0–1 coordinates.
|
|
20
|
+
* @param box - The element's bounding client rect.
|
|
21
|
+
* @param style - The element's computed style (for padding).
|
|
22
|
+
* @param margin - Optional child element's computed style (for margin inset).
|
|
23
|
+
* @returns Pixel coordinates relative to the element's border-box origin.
|
|
24
|
+
*/
|
|
25
|
+
export function viewWindowToLocalRect(
|
|
26
|
+
window: ViewWindow,
|
|
27
|
+
box: DOMRect,
|
|
28
|
+
style: CSSStyleDeclaration,
|
|
29
|
+
margin?: CSSStyleDeclaration,
|
|
30
|
+
): { x: number; y: number; width: number; height: number } {
|
|
31
|
+
const paddingLeft = parseFloat(style.paddingLeft);
|
|
32
|
+
const paddingTop = parseFloat(style.paddingTop);
|
|
33
|
+
const contentWidth = box.width - paddingLeft - parseFloat(style.paddingRight);
|
|
34
|
+
const contentHeight =
|
|
35
|
+
box.height - paddingTop - parseFloat(style.paddingBottom);
|
|
36
|
+
|
|
37
|
+
const x = paddingLeft + window.col_start * contentWidth;
|
|
38
|
+
const y = paddingTop + window.row_start * contentHeight;
|
|
39
|
+
let width = (window.col_end - window.col_start) * contentWidth;
|
|
40
|
+
let height = (window.row_end - window.row_start) * contentHeight;
|
|
41
|
+
if (margin) {
|
|
42
|
+
const marginTop = parseFloat(margin.marginTop);
|
|
43
|
+
const marginRight = parseFloat(margin.marginRight);
|
|
44
|
+
const marginBottom = parseFloat(margin.marginBottom);
|
|
45
|
+
const marginLeft = parseFloat(margin.marginLeft);
|
|
46
|
+
width -= marginLeft + marginRight;
|
|
47
|
+
height -= marginTop + marginBottom;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { x, y, width, height };
|
|
51
|
+
}
|
|
14
52
|
|
|
15
53
|
export function updateOverlaySheet(
|
|
16
54
|
slot: string,
|
|
@@ -18,25 +56,19 @@ export function updateOverlaySheet(
|
|
|
18
56
|
style: CSSStyleDeclaration,
|
|
19
57
|
drag_target: LayoutPath | null,
|
|
20
58
|
physics = DEFAULT_PHYSICS,
|
|
59
|
+
margin?: CSSStyleDeclaration,
|
|
21
60
|
) {
|
|
22
61
|
if (!drag_target) {
|
|
23
62
|
return `:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){display:none;}`;
|
|
24
63
|
}
|
|
25
64
|
|
|
26
|
-
const
|
|
27
|
-
view_window
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const box_width =
|
|
34
|
-
box.width - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
65
|
+
const local = viewWindowToLocalRect(
|
|
66
|
+
drag_target.view_window,
|
|
67
|
+
box,
|
|
68
|
+
style,
|
|
69
|
+
margin,
|
|
70
|
+
);
|
|
35
71
|
|
|
36
|
-
const
|
|
37
|
-
const left = col_start * box_width + parseFloat(style.paddingLeft);
|
|
38
|
-
const height = (row_end - row_start) * box_height;
|
|
39
|
-
const width = (col_end - col_start) * box_width;
|
|
40
|
-
const css = `display:flex;position:absolute!important;z-index:1;top:${top}px;left:${left}px;height:${height}px;width:${width}px;`;
|
|
72
|
+
const css = `display:flex;position:absolute!important;z-index:1;top:${local.y}px;left:${local.x}px;height:${local.height}px;width:${local.width}px;`;
|
|
41
73
|
return `::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){${css}}`;
|
|
42
74
|
}
|
|
@@ -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, LayoutPathTraversal } from "
|
|
12
|
+
import type { Layout, LayoutPathTraversal } from "../core/types.ts";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Inserts a new child panel into the layout tree at a specified location.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import { DEFAULT_PHYSICS } from "
|
|
13
|
-
import type { Layout, LayoutPathTraversal } from "
|
|
12
|
+
import { DEFAULT_PHYSICS } from "../core/constants.ts";
|
|
13
|
+
import type { Layout, LayoutPathTraversal } from "../core/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 "../core/types.ts";
|
|
13
|
+
import { EMPTY_PANEL } from "../core/types.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Removes a child panel from the layout tree by its name.
|
|
@@ -0,0 +1,161 @@
|
|
|
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 { Layout, LayoutPath, OverlayMode } from "../core/types.ts";
|
|
13
|
+
import type { Physics } from "../core/constants.ts";
|
|
14
|
+
import type { PresizeQueue } from "./presize_queue.ts";
|
|
15
|
+
import { calculate_intersection } from "../layout/calculate_intersect.ts";
|
|
16
|
+
import { calculate_edge } from "../layout/calculate_edge.ts";
|
|
17
|
+
import { create_css_grid_layout } from "../layout/generate_grid.ts";
|
|
18
|
+
import { updateOverlaySheet } from "../layout/generate_overlay.ts";
|
|
19
|
+
import { remove_child } from "../layout/remove_child.ts";
|
|
20
|
+
import { insert_child } from "../layout/insert_child.ts";
|
|
21
|
+
|
|
22
|
+
export interface OverlayHost {
|
|
23
|
+
readonly panel: Layout;
|
|
24
|
+
readonly physics: Physics;
|
|
25
|
+
readonly stylesheet: CSSStyleSheet;
|
|
26
|
+
readonly presizeQueue: PresizeQueue;
|
|
27
|
+
relativeCoordinates(
|
|
28
|
+
event: { clientX: number; clientY: number },
|
|
29
|
+
recalculate: boolean,
|
|
30
|
+
): [number, number, DOMRect, CSSStyleDeclaration];
|
|
31
|
+
restore(layout: Layout, isFlattened?: boolean): Promise<void>;
|
|
32
|
+
querySelector(selectors: string): Element | null;
|
|
33
|
+
dispatchEvent(event: Event): boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Manages overlay state during drag-and-drop panel rearrangement.
|
|
38
|
+
*
|
|
39
|
+
* Handles rendering a preview of where a dragged panel would land,
|
|
40
|
+
* and committing or cancelling the placement when the drag ends.
|
|
41
|
+
*/
|
|
42
|
+
export class OverlayController {
|
|
43
|
+
constructor(private _host: OverlayHost) {}
|
|
44
|
+
|
|
45
|
+
async set(
|
|
46
|
+
event: { clientX: number; clientY: number },
|
|
47
|
+
{ slot }: LayoutPath,
|
|
48
|
+
className: string = this._host.physics.OVERLAY_CLASSNAME,
|
|
49
|
+
mode: OverlayMode = this._host.physics.OVERLAY_DEFAULT,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const host = this._host;
|
|
52
|
+
const panel = remove_child(host.panel, slot);
|
|
53
|
+
const query = `:scope > [${host.physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
|
|
54
|
+
let drag_element = host.querySelector(query);
|
|
55
|
+
if (drag_element) {
|
|
56
|
+
drag_element.classList.add(className);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [col, row, box, style] = host.relativeCoordinates(event, true);
|
|
60
|
+
let drop_target = calculate_intersection(col, row, panel);
|
|
61
|
+
if (drop_target) {
|
|
62
|
+
drop_target = calculate_edge(
|
|
63
|
+
col,
|
|
64
|
+
row,
|
|
65
|
+
panel,
|
|
66
|
+
slot,
|
|
67
|
+
drop_target,
|
|
68
|
+
box,
|
|
69
|
+
host.physics,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await host.presizeQueue.run(panel, () => {
|
|
74
|
+
if (mode === "grid" && drop_target) {
|
|
75
|
+
const path: [string, string] = [slot, drop_target?.slot];
|
|
76
|
+
const css = create_css_grid_layout(panel, path, host.physics);
|
|
77
|
+
host.stylesheet.replaceSync(css);
|
|
78
|
+
} else if (mode === "absolute") {
|
|
79
|
+
const grid_css = create_css_grid_layout(panel, undefined, host.physics);
|
|
80
|
+
|
|
81
|
+
while (drag_element?.tagName === "SLOT" && drag_element) {
|
|
82
|
+
drag_element = (
|
|
83
|
+
drag_element as HTMLSlotElement
|
|
84
|
+
).assignedElements()[0];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const margin = drag_element
|
|
88
|
+
? getComputedStyle(drag_element)
|
|
89
|
+
: undefined;
|
|
90
|
+
|
|
91
|
+
const overlay_css = updateOverlaySheet(
|
|
92
|
+
slot,
|
|
93
|
+
box,
|
|
94
|
+
style,
|
|
95
|
+
drop_target,
|
|
96
|
+
host.physics,
|
|
97
|
+
margin,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const event_name = `${host.physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
|
|
105
|
+
const custom_event = new CustomEvent<Layout>(event_name, {
|
|
106
|
+
detail: panel,
|
|
107
|
+
});
|
|
108
|
+
host.dispatchEvent(custom_event);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async clear(
|
|
112
|
+
event: { clientX: number; clientY: number } | null,
|
|
113
|
+
{ slot, layout }: LayoutPath,
|
|
114
|
+
className: string = this._host.physics.OVERLAY_CLASSNAME,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const host = this._host;
|
|
117
|
+
const panel = remove_child(host.panel, slot);
|
|
118
|
+
const query = `:scope > [${host.physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
|
|
119
|
+
const drag_element = host.querySelector(query);
|
|
120
|
+
|
|
121
|
+
if (event === null) {
|
|
122
|
+
await host.restore(layout);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const [col, row, box] = host.relativeCoordinates(event, false);
|
|
127
|
+
let drop_target = calculate_intersection(col, row, panel);
|
|
128
|
+
if (drop_target) {
|
|
129
|
+
drop_target = calculate_edge(
|
|
130
|
+
col,
|
|
131
|
+
row,
|
|
132
|
+
panel,
|
|
133
|
+
slot,
|
|
134
|
+
drop_target,
|
|
135
|
+
box,
|
|
136
|
+
host.physics,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (drop_target) {
|
|
141
|
+
const orientation = drop_target?.is_edge
|
|
142
|
+
? drop_target.orientation
|
|
143
|
+
: undefined;
|
|
144
|
+
|
|
145
|
+
const new_layout = insert_child(
|
|
146
|
+
panel,
|
|
147
|
+
slot,
|
|
148
|
+
drop_target.path,
|
|
149
|
+
orientation,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await host.restore(new_layout);
|
|
153
|
+
} else {
|
|
154
|
+
await host.restore(layout);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (drag_element) {
|
|
158
|
+
drag_element.classList.remove(className);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { Layout, PresizeDetail } from "../core/types.ts";
|
|
13
|
+
import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manages cancelable pre-resize gating for layout updates.
|
|
17
|
+
*
|
|
18
|
+
* Before each layout change, a cancelable `resize-before` event is dispatched
|
|
19
|
+
* on the target. If the event is cancelled via `preventDefault()`, the update
|
|
20
|
+
* is suspended until {@link resume} is called. Concurrent updates are queued
|
|
21
|
+
* and processed sequentially.
|
|
22
|
+
*/
|
|
23
|
+
export class PresizeQueue {
|
|
24
|
+
#resizing = false;
|
|
25
|
+
#queued: { layout: Layout; fn: () => void } | null = null;
|
|
26
|
+
#pending: (() => void) | null = null;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private _target: EventTarget,
|
|
30
|
+
private _eventName: string,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
async run(layout: Layout, fn: () => void): Promise<void> {
|
|
34
|
+
if (this.#resizing) {
|
|
35
|
+
this.#queued = { layout, fn };
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.#resizing = true;
|
|
40
|
+
try {
|
|
41
|
+
await this.#dispatchAndMaybeWait(layout, fn);
|
|
42
|
+
while (this.#queued) {
|
|
43
|
+
const { layout: nextLayout, fn: nextFn } = this.#queued;
|
|
44
|
+
this.#queued = null;
|
|
45
|
+
await this.#dispatchAndMaybeWait(nextLayout, nextFn);
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
this.#resizing = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resume(): void {
|
|
53
|
+
if (this.#pending) {
|
|
54
|
+
const resolve = this.#pending;
|
|
55
|
+
this.#pending = null;
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async #dispatchAndMaybeWait(layout: Layout, fn: () => void): Promise<void> {
|
|
61
|
+
const detail: PresizeDetail = {
|
|
62
|
+
calculatePresizePaths: () => calculate_presize_paths(layout),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const event = new CustomEvent(this._eventName, {
|
|
66
|
+
cancelable: true,
|
|
67
|
+
detail,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const proceed = this._target.dispatchEvent(event);
|
|
71
|
+
if (!proceed) {
|
|
72
|
+
await new Promise<void>((resolve) => {
|
|
73
|
+
this.#pending = resolve;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -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 { LayoutPath } from "./
|
|
12
|
+
import type { LayoutPath } from "./core/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";
|
|
@@ -26,11 +26,25 @@ const CSS = `
|
|
|
26
26
|
|
|
27
27
|
const HTML_TEMPLATE = `
|
|
28
28
|
<div part="titlebar"></div>
|
|
29
|
-
<
|
|
29
|
+
<div part="container"><slot></slot></div>
|
|
30
30
|
`;
|
|
31
31
|
|
|
32
32
|
type DragState = { moved?: boolean; path: LayoutPath };
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Escapes a string for safe use as a CSS `<string>` token (the `content`
|
|
36
|
+
* fallback), handling backslashes, double-quotes, and newlines.
|
|
37
|
+
*/
|
|
38
|
+
const css_string = (value: string): string =>
|
|
39
|
+
`"${value.replace(/[\\"\n]/g, (c) => (c === "\n" ? "\\A " : `\\${c}`))}"`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The per-slot CSS custom property a consumer sets to override a tab's label,
|
|
43
|
+
* e.g. `regular-layout { --regular-layout-my-panel--title: "My Panel"; }`.
|
|
44
|
+
*/
|
|
45
|
+
const title_variable = (slot: string): string =>
|
|
46
|
+
`--regular-layout-${globalThis.CSS.escape(slot)}--title`;
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* A custom element that represents a draggable panel within a
|
|
36
50
|
* `<regular-layout>`.
|
|
@@ -47,8 +61,17 @@ type DragState = { moved?: boolean; path: LayoutPath };
|
|
|
47
61
|
* [named `slot`s](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
|
|
48
62
|
* for wholesale replacement of the underlying Shadow DOM.
|
|
49
63
|
*
|
|
64
|
+
* The `name` attribute identifies the panel within the layout. The tab label
|
|
65
|
+
* defaults to `name`, but can be overridden with pure CSS by setting the
|
|
66
|
+
* `--regular-layout-<name>--title` custom property to a CSS string - the label
|
|
67
|
+
* is rendered via the tab title's `::before` `content`, so the override
|
|
68
|
+
* applies reactively with no JavaScript.
|
|
69
|
+
*
|
|
50
70
|
* @example
|
|
51
71
|
* ```html
|
|
72
|
+
* <style>
|
|
73
|
+
* regular-layout { --regular-layout-panel-1--title: "Panel 1"; }
|
|
74
|
+
* </style>
|
|
52
75
|
* <regular-layout>
|
|
53
76
|
* <regular-layout-frame name="panel-1">
|
|
54
77
|
* <!-- Panel content here -->
|
|
@@ -59,6 +82,7 @@ type DragState = { moved?: boolean; path: LayoutPath };
|
|
|
59
82
|
export class RegularLayoutFrame extends HTMLElement {
|
|
60
83
|
private _shadowRoot!: ShadowRoot;
|
|
61
84
|
private _container_sheet!: CSSStyleSheet;
|
|
85
|
+
private _title_sheet!: CSSStyleSheet;
|
|
62
86
|
private _layout!: RegularLayout;
|
|
63
87
|
private _header!: HTMLElement;
|
|
64
88
|
private _drag: DragState | null = null;
|
|
@@ -72,8 +96,12 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
72
96
|
connectedCallback() {
|
|
73
97
|
this._container_sheet ??= new CSSStyleSheet();
|
|
74
98
|
this._container_sheet.replaceSync(CSS);
|
|
99
|
+
this._title_sheet ??= new CSSStyleSheet();
|
|
75
100
|
this._shadowRoot ??= this.attachShadow({ mode: "open" });
|
|
76
|
-
this._shadowRoot.adoptedStyleSheets = [
|
|
101
|
+
this._shadowRoot.adoptedStyleSheets = [
|
|
102
|
+
this._container_sheet,
|
|
103
|
+
this._title_sheet,
|
|
104
|
+
];
|
|
77
105
|
this._shadowRoot.innerHTML = HTML_TEMPLATE;
|
|
78
106
|
this._layout = this.parentElement as RegularLayout;
|
|
79
107
|
this._header = this._shadowRoot.children[0] as HTMLElement;
|
|
@@ -106,6 +134,10 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
private onPointerDown = (event: PointerEvent): void => {
|
|
137
|
+
if (event.button !== 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
const elem = event.target as RegularLayoutTab;
|
|
110
142
|
if (elem.part.contains("tab")) {
|
|
111
143
|
const path = this._layout.calculateIntersect(event);
|
|
@@ -184,5 +216,28 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
184
216
|
for (let j = this._header.children.length - 1; j >= last_index; j--) {
|
|
185
217
|
this._header.removeChild(this._header.children[j]);
|
|
186
218
|
}
|
|
219
|
+
|
|
220
|
+
this.drawTitles(attr, new_tab_panel.tabs);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Regenerates this frame's title stylesheet, mapping each tab (by its slot
|
|
225
|
+
* `name`) to its label. The label is rendered via the title's `::before`
|
|
226
|
+
* `content`, reading the slot's `--regular-layout-<slot>--title` override
|
|
227
|
+
* variable with the slot name as the fallback - so a consumer can relabel a
|
|
228
|
+
* tab purely in CSS and the change applies reactively.
|
|
229
|
+
*
|
|
230
|
+
* @param attr - The child name attribute (`CHILD_ATTRIBUTE_NAME`).
|
|
231
|
+
* @param tabs - The slot names rendered in this frame's titlebar.
|
|
232
|
+
*/
|
|
233
|
+
private drawTitles = (attr: string, tabs: string[]) => {
|
|
234
|
+
const css = tabs
|
|
235
|
+
.map(
|
|
236
|
+
(slot) =>
|
|
237
|
+
`regular-layout-tab[${attr}="${slot}"] [part~="title"]::before{content:var(${title_variable(slot)}, ${css_string(slot)})}`,
|
|
238
|
+
)
|
|
239
|
+
.join("\n");
|
|
240
|
+
|
|
241
|
+
this._title_sheet.replaceSync(css);
|
|
187
242
|
};
|
|
188
243
|
}
|
|
@@ -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 { TabLayout } from "./
|
|
12
|
+
import type { TabLayout } from "./core/types.ts";
|
|
13
13
|
import type { RegularLayout } from "./regular-layout.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -34,6 +34,14 @@ export class RegularLayoutTab extends HTMLElement {
|
|
|
34
34
|
* @param index - The index of this tab within the tab panel.
|
|
35
35
|
*/
|
|
36
36
|
populate = (layout: RegularLayout, tab_panel: TabLayout, index: number) => {
|
|
37
|
+
// Stamp the slot onto the tab so its title can be styled by name (see
|
|
38
|
+
// `RegularLayoutFrame#drawTitles`). Tag-qualified selectors keep this
|
|
39
|
+
// distinct from the panel `name` on `<regular-layout-frame>`.
|
|
40
|
+
this.setAttribute(
|
|
41
|
+
layout.savePhysics().CHILD_ATTRIBUTE_NAME,
|
|
42
|
+
tab_panel.tabs[index],
|
|
43
|
+
);
|
|
44
|
+
|
|
37
45
|
if (this._tab_panel) {
|
|
38
46
|
const tab_changed =
|
|
39
47
|
(index === tab_panel.selected) !==
|
|
@@ -44,9 +52,6 @@ export class RegularLayoutTab extends HTMLElement {
|
|
|
44
52
|
|
|
45
53
|
if (index_changed) {
|
|
46
54
|
const selected = tab_panel.selected === index;
|
|
47
|
-
const slot = tab_panel.tabs[index];
|
|
48
|
-
this.children[0].textContent = slot;
|
|
49
|
-
|
|
50
55
|
if (selected) {
|
|
51
56
|
this.children[1].part.add("active-close");
|
|
52
57
|
this.part.add("active-tab");
|
|
@@ -56,7 +61,6 @@ export class RegularLayoutTab extends HTMLElement {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
} else {
|
|
59
|
-
const slot = tab_panel.tabs[index];
|
|
60
64
|
const selected = tab_panel.selected === index;
|
|
61
65
|
const parts = selected ? "active-close close" : "close";
|
|
62
66
|
this.innerHTML = `<div part="title"></div><button part="${parts}"></button>`;
|
|
@@ -67,7 +71,6 @@ export class RegularLayoutTab extends HTMLElement {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
this.addEventListener("pointerdown", this.onTabClick);
|
|
70
|
-
this.children[0].textContent = slot;
|
|
71
74
|
this.children[1].addEventListener("pointerdown", this.onTabClose);
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -76,28 +79,23 @@ export class RegularLayoutTab extends HTMLElement {
|
|
|
76
79
|
this._index = index;
|
|
77
80
|
};
|
|
78
81
|
|
|
79
|
-
private onTabClose = (
|
|
82
|
+
private onTabClose = (event: Event) => {
|
|
83
|
+
if (event instanceof PointerEvent && event?.button !== 0) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
if (this._tab_panel !== undefined && this._index !== undefined) {
|
|
81
88
|
this._layout?.removePanel(this._tab_panel.tabs[this._index]);
|
|
82
89
|
}
|
|
83
90
|
};
|
|
84
91
|
|
|
85
|
-
private onTabClick = (
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
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.tabs[this._index],
|
|
94
|
-
new_layout,
|
|
95
|
-
);
|
|
92
|
+
private onTabClick = (event: PointerEvent) => {
|
|
93
|
+
if (event.button !== 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this._layout?.restore(new_layout);
|
|
100
|
-
}
|
|
97
|
+
if (this._tab_panel !== undefined && this._index !== undefined) {
|
|
98
|
+
this._layout?.select(this._tab_panel.tabs[this._index]);
|
|
101
99
|
}
|
|
102
100
|
};
|
|
103
101
|
}
|