regular-layout 0.4.0 → 0.6.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/dist/extensions.d.ts +7 -0
- package/dist/index.js +19 -14
- package/dist/index.js.map +3 -3
- package/dist/layout/generate_grid.d.ts +9 -0
- package/dist/regular-layout-frame.d.ts +24 -1
- package/dist/regular-layout.d.ts +39 -0
- package/package.json +1 -1
- package/src/extensions.ts +12 -0
- package/src/layout/generate_grid.ts +57 -13
- package/src/layout/generate_overlay.ts +1 -1
- package/src/model/overlay_controller.ts +0 -1
- package/src/regular-layout-frame.ts +92 -35
- package/src/regular-layout-tab.ts +19 -21
- package/src/regular-layout.ts +86 -1
- package/themes/borland.css +6 -4
- package/themes/chicago.css +12 -4
- package/themes/fluxbox.css +6 -3
- package/themes/gibson.css +8 -22
- package/themes/hotdog.css +13 -4
- package/themes/lorax.css +40 -43
|
@@ -31,3 +31,12 @@ import type { Layout } from "../core/types.ts";
|
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
export declare function create_css_grid_layout(layout: Layout, overlay?: [string, string], physics?: Physics): string;
|
|
34
|
+
/**
|
|
35
|
+
* Generates CSS Grid styles that render a single panel maximized to fill the
|
|
36
|
+
* entire layout, hiding all other slotted children.
|
|
37
|
+
*
|
|
38
|
+
* @param name - The name of the panel to maximize.
|
|
39
|
+
* @param physics - Instance constants (defaults to {@link DEFAULT_PHYSICS}).
|
|
40
|
+
* @returns CSS string showing only `name`, full-size.
|
|
41
|
+
*/
|
|
42
|
+
export declare function create_css_maximize_layout(name: string, physics?: Physics): string;
|
|
@@ -14,8 +14,17 @@
|
|
|
14
14
|
* [named `slot`s](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
|
|
15
15
|
* for wholesale replacement of the underlying Shadow DOM.
|
|
16
16
|
*
|
|
17
|
+
* The `name` attribute identifies the panel within the layout. The tab label
|
|
18
|
+
* defaults to `name`, but can be overridden with pure CSS by setting the
|
|
19
|
+
* `--regular-layout-<name>--title` custom property to a CSS string - the label
|
|
20
|
+
* is rendered via the tab title's `::before` `content`, so the override
|
|
21
|
+
* applies reactively with no JavaScript.
|
|
22
|
+
*
|
|
17
23
|
* @example
|
|
18
24
|
* ```html
|
|
25
|
+
* <style>
|
|
26
|
+
* regular-layout { --regular-layout-panel-1--title: "Panel 1"; }
|
|
27
|
+
* </style>
|
|
19
28
|
* <regular-layout>
|
|
20
29
|
* <regular-layout-frame name="panel-1">
|
|
21
30
|
* <!-- Panel content here -->
|
|
@@ -26,10 +35,11 @@
|
|
|
26
35
|
export declare class RegularLayoutFrame extends HTMLElement {
|
|
27
36
|
private _shadowRoot;
|
|
28
37
|
private _container_sheet;
|
|
38
|
+
private _tab_sheet;
|
|
29
39
|
private _layout;
|
|
30
40
|
private _header;
|
|
41
|
+
private _default_tab;
|
|
31
42
|
private _drag;
|
|
32
|
-
private _tab_to_index_map;
|
|
33
43
|
/**
|
|
34
44
|
* Initializes this elements. Override this method and
|
|
35
45
|
* `disconnectedCallback` to modify how this subclass renders the Shadow
|
|
@@ -46,4 +56,17 @@ export declare class RegularLayoutFrame extends HTMLElement {
|
|
|
46
56
|
private onPointerCancel;
|
|
47
57
|
private onPointerLost;
|
|
48
58
|
private drawTabs;
|
|
59
|
+
/**
|
|
60
|
+
* Regenerates this frame's tab stylesheet: the shared titlebar grid template
|
|
61
|
+
* (so every frame in the stack aligns), the column this frame's tab occupies,
|
|
62
|
+
* and the tab label. The label renders via the title's `::before` `content`,
|
|
63
|
+
* reading the slot's `--regular-layout-<slot>--title` override variable with
|
|
64
|
+
* the slot name as the fallback - so a consumer can relabel a tab purely in
|
|
65
|
+
* CSS and the change applies reactively.
|
|
66
|
+
*
|
|
67
|
+
* @param slot - This frame's panel name.
|
|
68
|
+
* @param index - This frame's column (zero-based) within the stack.
|
|
69
|
+
* @param count - The number of tabs in the stack.
|
|
70
|
+
*/
|
|
71
|
+
private drawTab;
|
|
49
72
|
}
|
package/dist/regular-layout.d.ts
CHANGED
|
@@ -57,6 +57,14 @@ export interface PointerEventCoordinates {
|
|
|
57
57
|
* latter implementation would require synchronizing the light DOM
|
|
58
58
|
* and shadow DOM slots/slotted children continuously.
|
|
59
59
|
*
|
|
60
|
+
* Note that `::slotted()` only matches *directly* slotted nodes - it
|
|
61
|
+
* does not pierce a slotted `<slot>`. A consumer that wants to forward
|
|
62
|
+
* its own light-DOM content through `<regular-layout>` therefore cannot
|
|
63
|
+
* place a bare `<slot name="X">` as a layout child and expect it to be
|
|
64
|
+
* grid-positioned; instead, wrap the forwarding slot in a concrete
|
|
65
|
+
* `<regular-layout-frame name="X">` (which *is* directly slotted and so
|
|
66
|
+
* matched by the generated `::slotted([name="X"])` rules).
|
|
67
|
+
*
|
|
60
68
|
*/
|
|
61
69
|
export declare class RegularLayout extends HTMLElement {
|
|
62
70
|
private _shadowRoot;
|
|
@@ -69,6 +77,7 @@ export declare class RegularLayout extends HTMLElement {
|
|
|
69
77
|
private _physics;
|
|
70
78
|
private _presizeQueue;
|
|
71
79
|
private _overlayController;
|
|
80
|
+
private _maximized?;
|
|
72
81
|
constructor();
|
|
73
82
|
connectedCallback(): void;
|
|
74
83
|
disconnectedCallback(): void;
|
|
@@ -133,6 +142,20 @@ export declare class RegularLayout extends HTMLElement {
|
|
|
133
142
|
* @param name - Name of the panel to remove
|
|
134
143
|
*/
|
|
135
144
|
removePanel: (name: string) => Promise<void>;
|
|
145
|
+
/**
|
|
146
|
+
* Selects a panel by `name`, making it the front-most tab within its
|
|
147
|
+
* containing stack, then dispatches a `<prefix>-select` event with
|
|
148
|
+
* `detail: { name }`.
|
|
149
|
+
*
|
|
150
|
+
* The event fires whenever a tab is selected - including re-selecting the
|
|
151
|
+
* already-active tab, or selecting the sole tab of a single-element stack -
|
|
152
|
+
* so consumers can always map a tab interaction to an "active panel". When
|
|
153
|
+
* the selection actually changes, a `<prefix>-update` event is dispatched
|
|
154
|
+
* first (via {@link restore}).
|
|
155
|
+
*
|
|
156
|
+
* @param name - The name of the panel to select.
|
|
157
|
+
*/
|
|
158
|
+
select: (name: string) => Promise<void>;
|
|
136
159
|
/**
|
|
137
160
|
* Retrieves a panel by name from the layout tree.
|
|
138
161
|
*
|
|
@@ -145,6 +168,22 @@ export declare class RegularLayout extends HTMLElement {
|
|
|
145
168
|
* Clears the entire layout, unslotting all panels.
|
|
146
169
|
*/
|
|
147
170
|
clear: () => Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* Maximizes a panel by `name`, rendering it full-size and hiding every
|
|
173
|
+
* other panel. This is transient display state - it is **not** persisted by
|
|
174
|
+
* {@link save}, and any subsequent {@link restore} resets the layout to the
|
|
175
|
+
* minimized (normal, multi-panel) view.
|
|
176
|
+
*
|
|
177
|
+
* Has no effect if `name` is not present in the current layout.
|
|
178
|
+
*
|
|
179
|
+
* @param name - The name of the panel to maximize.
|
|
180
|
+
*/
|
|
181
|
+
maximize: (name: string) => void;
|
|
182
|
+
/**
|
|
183
|
+
* Restores the normal multi-panel view after a {@link maximize}, without
|
|
184
|
+
* altering the layout tree. No-op if no panel is currently maximized.
|
|
185
|
+
*/
|
|
186
|
+
minimize: () => void;
|
|
148
187
|
/**
|
|
149
188
|
* Restores the layout from a saved state synchronously, without
|
|
150
189
|
* dispatching the `regular-layout-before-resize` event.
|
package/package.json
CHANGED
package/src/extensions.ts
CHANGED
|
@@ -65,6 +65,12 @@ declare global {
|
|
|
65
65
|
options?: { signal: AbortSignal },
|
|
66
66
|
): void;
|
|
67
67
|
|
|
68
|
+
addEventListener(
|
|
69
|
+
name: "regular-layout-select",
|
|
70
|
+
cb: (e: RegularLayoutSelectEvent) => void,
|
|
71
|
+
options?: { signal: AbortSignal },
|
|
72
|
+
): void;
|
|
73
|
+
|
|
68
74
|
removeEventListener(
|
|
69
75
|
name: "regular-layout-update",
|
|
70
76
|
cb: (e: RegularLayoutEvent) => void,
|
|
@@ -79,8 +85,14 @@ declare global {
|
|
|
79
85
|
name: "regular-layout-before-resize",
|
|
80
86
|
cb: (e: RegularLayoutPresizeEvent) => void,
|
|
81
87
|
): void;
|
|
88
|
+
|
|
89
|
+
removeEventListener(
|
|
90
|
+
name: "regular-layout-select",
|
|
91
|
+
cb: (e: RegularLayoutSelectEvent) => void,
|
|
92
|
+
): void;
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
export type RegularLayoutEvent = CustomEvent<Layout>;
|
|
86
97
|
export type RegularLayoutPresizeEvent = CustomEvent<PresizeDetail>;
|
|
98
|
+
export type RegularLayoutSelectEvent = CustomEvent<{ name: string }>;
|
|
@@ -18,6 +18,7 @@ interface GridCell {
|
|
|
18
18
|
colEnd: number;
|
|
19
19
|
rowStart: number;
|
|
20
20
|
rowEnd: number;
|
|
21
|
+
zIndex?: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function dedupe_sort(physics: Physics, result: number[], pos: number) {
|
|
@@ -102,15 +103,26 @@ function build_cells(
|
|
|
102
103
|
): GridCell[] {
|
|
103
104
|
if (panel.type === "tab-layout") {
|
|
104
105
|
const selected = panel.selected ?? 0;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
const col_start = find_track_index(physics, colPositions, colStart);
|
|
107
|
+
const col_end = find_track_index(physics, colPositions, colEnd);
|
|
108
|
+
const row_start = find_track_index(physics, rowPositions, rowStart);
|
|
109
|
+
const row_end = find_track_index(physics, rowPositions, rowEnd);
|
|
110
|
+
|
|
111
|
+
// Stacked tabs all occupy the same cell, overlapping. Only the selected
|
|
112
|
+
// frame is lifted (`z-index:1`) so its content sits on top; the rest
|
|
113
|
+
// self-hide their content and just contribute their tab. Keeping the
|
|
114
|
+
// max frame `z-index` at 1 lets the drag overlay sit above any stack
|
|
115
|
+
// with a small constant (2). A single-tab stack
|
|
116
|
+
// emits one placement with no `z-index`, so non-stacked output is
|
|
117
|
+
// unchanged.
|
|
118
|
+
return panel.tabs.map((child, i) => ({
|
|
119
|
+
child,
|
|
120
|
+
colStart: col_start,
|
|
121
|
+
colEnd: col_end,
|
|
122
|
+
rowStart: row_start,
|
|
123
|
+
rowEnd: row_end,
|
|
124
|
+
zIndex: panel.tabs.length > 1 && i === selected ? 1 : undefined,
|
|
125
|
+
}));
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
const { children, sizes, orientation } = panel;
|
|
@@ -162,8 +174,11 @@ const child_template = (
|
|
|
162
174
|
slot: string,
|
|
163
175
|
rowPart: string,
|
|
164
176
|
colPart: string,
|
|
177
|
+
zIndex?: number,
|
|
165
178
|
) =>
|
|
166
|
-
`:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){display:flex;grid-column:${colPart};grid-row:${rowPart}
|
|
179
|
+
`:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){display:flex;grid-column:${colPart};grid-row:${rowPart}${
|
|
180
|
+
zIndex === undefined ? "" : `;z-index:${zIndex}`
|
|
181
|
+
}}`;
|
|
167
182
|
|
|
168
183
|
/**
|
|
169
184
|
* Generates CSS Grid styles to render a layout tree.
|
|
@@ -202,9 +217,18 @@ export function create_css_grid_layout(
|
|
|
202
217
|
): string {
|
|
203
218
|
if (layout.type === "tab-layout") {
|
|
204
219
|
const selected = layout.selected ?? 0;
|
|
220
|
+
const stacked = layout.tabs.length > 1;
|
|
205
221
|
return [
|
|
206
222
|
host_template("100%", "100%"),
|
|
207
|
-
|
|
223
|
+
...layout.tabs.map((tab, i) =>
|
|
224
|
+
child_template(
|
|
225
|
+
physics,
|
|
226
|
+
tab,
|
|
227
|
+
"1",
|
|
228
|
+
"1",
|
|
229
|
+
stacked && i === selected ? 1 : undefined,
|
|
230
|
+
),
|
|
231
|
+
),
|
|
208
232
|
].join("\n");
|
|
209
233
|
}
|
|
210
234
|
|
|
@@ -253,14 +277,34 @@ export function create_css_grid_layout(
|
|
|
253
277
|
for (const cell of cells) {
|
|
254
278
|
const colPart = formatGridLine(cell.colStart, cell.colEnd);
|
|
255
279
|
const rowPart = formatGridLine(cell.rowStart, cell.rowEnd);
|
|
256
|
-
css.push(
|
|
280
|
+
css.push(
|
|
281
|
+
child_template(physics, cell.child, rowPart, colPart, cell.zIndex),
|
|
282
|
+
);
|
|
257
283
|
if (cell.child === overlay?.[1]) {
|
|
258
284
|
css.push(child_template(physics, overlay[0], rowPart, colPart));
|
|
259
285
|
css.push(
|
|
260
|
-
`:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}=${overlay[0]}]){z-index:
|
|
286
|
+
`:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}=${overlay[0]}]){z-index:2}`,
|
|
261
287
|
);
|
|
262
288
|
}
|
|
263
289
|
}
|
|
264
290
|
|
|
265
291
|
return css.join("\n");
|
|
266
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Generates CSS Grid styles that render a single panel maximized to fill the
|
|
296
|
+
* entire layout, hiding all other slotted children.
|
|
297
|
+
*
|
|
298
|
+
* @param name - The name of the panel to maximize.
|
|
299
|
+
* @param physics - Instance constants (defaults to {@link DEFAULT_PHYSICS}).
|
|
300
|
+
* @returns CSS string showing only `name`, full-size.
|
|
301
|
+
*/
|
|
302
|
+
export function create_css_maximize_layout(
|
|
303
|
+
name: string,
|
|
304
|
+
physics: Physics = DEFAULT_PHYSICS,
|
|
305
|
+
): string {
|
|
306
|
+
return [
|
|
307
|
+
host_template("100%", "100%"),
|
|
308
|
+
child_template(physics, name, "1", "1"),
|
|
309
|
+
].join("\n");
|
|
310
|
+
}
|
|
@@ -69,6 +69,6 @@ export function updateOverlaySheet(
|
|
|
69
69
|
margin,
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
-
const css = `display:flex;position:absolute!important;z-index:
|
|
72
|
+
const css = `display:flex;position:absolute!important;z-index:2;top:${local.y}px;left:${local.x}px;height:${local.height}px;width:${local.width}px;`;
|
|
73
73
|
return `::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){${css}}`;
|
|
74
74
|
}
|
|
@@ -58,7 +58,6 @@ export class OverlayController {
|
|
|
58
58
|
|
|
59
59
|
const [col, row, box, style] = host.relativeCoordinates(event, true);
|
|
60
60
|
let drop_target = calculate_intersection(col, row, panel);
|
|
61
|
-
console.log(row, col, drop_target);
|
|
62
61
|
if (drop_target) {
|
|
63
62
|
drop_target = calculate_edge(
|
|
64
63
|
col,
|
|
@@ -15,22 +15,38 @@ import type { RegularLayout } from "./regular-layout.ts";
|
|
|
15
15
|
import type { RegularLayoutTab } from "./regular-layout-tab.ts";
|
|
16
16
|
|
|
17
17
|
const CSS = `
|
|
18
|
-
:host{box-sizing:border-box;flex-direction:column}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
:host
|
|
24
|
-
:
|
|
18
|
+
:host{box-sizing:border-box;flex-direction:column;pointer-events:none}
|
|
19
|
+
[part~="titlebar"]{height:24px;user-select:none;overflow:hidden}
|
|
20
|
+
[part~="container"]{flex:1 1 auto;pointer-events:auto}
|
|
21
|
+
[part~="title"]{flex:1 1 auto;pointer-events:none}
|
|
22
|
+
[part~="close"]{align-self:stretch}
|
|
23
|
+
:host([inactive]) [part~="container"]{display:none}
|
|
24
|
+
.tabs{display:grid;width:100%;height:100%}
|
|
25
|
+
.tabs slot[name="tab"]{display:grid;grid-row:1;pointer-events:auto}
|
|
26
|
+
.tabs regular-layout-tab{display:flex;overflow:hidden}
|
|
25
27
|
`;
|
|
26
28
|
|
|
27
29
|
const HTML_TEMPLATE = `
|
|
28
|
-
<div part="titlebar"></div>
|
|
30
|
+
<div part="titlebar"><div class="tabs"><slot name="tab"><regular-layout-tab></regular-layout-tab></slot></div></div>
|
|
29
31
|
<div part="container"><slot></slot></div>
|
|
30
32
|
`;
|
|
31
33
|
|
|
32
34
|
type DragState = { moved?: boolean; path: LayoutPath };
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Escapes a string for safe use as a CSS `<string>` token (the `content`
|
|
38
|
+
* fallback), handling backslashes, double-quotes, and newlines.
|
|
39
|
+
*/
|
|
40
|
+
const css_string = (value: string): string =>
|
|
41
|
+
`"${value.replace(/[\\"\n]/g, (c) => (c === "\n" ? "\\A " : `\\${c}`))}"`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The per-slot CSS custom property a consumer sets to override a tab's label,
|
|
45
|
+
* e.g. `regular-layout { --regular-layout-my-panel--title: "My Panel"; }`.
|
|
46
|
+
*/
|
|
47
|
+
const title_variable = (slot: string): string =>
|
|
48
|
+
`--regular-layout-${globalThis.CSS.escape(slot)}--title`;
|
|
49
|
+
|
|
34
50
|
/**
|
|
35
51
|
* A custom element that represents a draggable panel within a
|
|
36
52
|
* `<regular-layout>`.
|
|
@@ -47,8 +63,17 @@ type DragState = { moved?: boolean; path: LayoutPath };
|
|
|
47
63
|
* [named `slot`s](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
|
|
48
64
|
* for wholesale replacement of the underlying Shadow DOM.
|
|
49
65
|
*
|
|
66
|
+
* The `name` attribute identifies the panel within the layout. The tab label
|
|
67
|
+
* defaults to `name`, but can be overridden with pure CSS by setting the
|
|
68
|
+
* `--regular-layout-<name>--title` custom property to a CSS string - the label
|
|
69
|
+
* is rendered via the tab title's `::before` `content`, so the override
|
|
70
|
+
* applies reactively with no JavaScript.
|
|
71
|
+
*
|
|
50
72
|
* @example
|
|
51
73
|
* ```html
|
|
74
|
+
* <style>
|
|
75
|
+
* regular-layout { --regular-layout-panel-1--title: "Panel 1"; }
|
|
76
|
+
* </style>
|
|
52
77
|
* <regular-layout>
|
|
53
78
|
* <regular-layout-frame name="panel-1">
|
|
54
79
|
* <!-- Panel content here -->
|
|
@@ -59,10 +84,11 @@ type DragState = { moved?: boolean; path: LayoutPath };
|
|
|
59
84
|
export class RegularLayoutFrame extends HTMLElement {
|
|
60
85
|
private _shadowRoot!: ShadowRoot;
|
|
61
86
|
private _container_sheet!: CSSStyleSheet;
|
|
87
|
+
private _tab_sheet!: CSSStyleSheet;
|
|
62
88
|
private _layout!: RegularLayout;
|
|
63
89
|
private _header!: HTMLElement;
|
|
90
|
+
private _default_tab!: RegularLayoutTab;
|
|
64
91
|
private _drag: DragState | null = null;
|
|
65
|
-
private _tab_to_index_map: WeakMap<RegularLayoutTab, number> = new WeakMap();
|
|
66
92
|
|
|
67
93
|
/**
|
|
68
94
|
* Initializes this elements. Override this method and
|
|
@@ -72,11 +98,20 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
72
98
|
connectedCallback() {
|
|
73
99
|
this._container_sheet ??= new CSSStyleSheet();
|
|
74
100
|
this._container_sheet.replaceSync(CSS);
|
|
101
|
+
this._tab_sheet ??= new CSSStyleSheet();
|
|
75
102
|
this._shadowRoot ??= this.attachShadow({ mode: "open" });
|
|
76
|
-
this._shadowRoot.adoptedStyleSheets = [
|
|
103
|
+
this._shadowRoot.adoptedStyleSheets = [
|
|
104
|
+
this._container_sheet,
|
|
105
|
+
this._tab_sheet,
|
|
106
|
+
];
|
|
77
107
|
this._shadowRoot.innerHTML = HTML_TEMPLATE;
|
|
78
108
|
this._layout = this.parentElement as RegularLayout;
|
|
79
109
|
this._header = this._shadowRoot.children[0] as HTMLElement;
|
|
110
|
+
// The built-in tab is the `slot="tab"` fallback; a consumer-supplied
|
|
111
|
+
// `slot="tab"` child replaces it.
|
|
112
|
+
this._default_tab = this._header.querySelector(
|
|
113
|
+
"regular-layout-tab",
|
|
114
|
+
) as RegularLayoutTab;
|
|
80
115
|
this._header.addEventListener("pointerdown", this.onPointerDown);
|
|
81
116
|
this.addEventListener("pointermove", this.onPointerMove);
|
|
82
117
|
this.addEventListener("pointerup", this.onPointerUp);
|
|
@@ -106,11 +141,23 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
private onPointerDown = (event: PointerEvent): void => {
|
|
144
|
+
if (event.button !== 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
109
148
|
const elem = event.target as RegularLayoutTab;
|
|
110
149
|
if (elem.part.contains("tab")) {
|
|
150
|
+
const slot = this.getAttribute(
|
|
151
|
+
this._layout.savePhysics().CHILD_ATTRIBUTE_NAME,
|
|
152
|
+
);
|
|
153
|
+
|
|
111
154
|
const path = this._layout.calculateIntersect(event);
|
|
112
|
-
if (path) {
|
|
113
|
-
this
|
|
155
|
+
if (path && slot) {
|
|
156
|
+
// Drag *this frame's* panel, not whichever panel is front-most at
|
|
157
|
+
// the pointer (which is what `calculateIntersect` resolves to in a
|
|
158
|
+
// stack). The overlapping stack shares geometry, so only the slot
|
|
159
|
+
// needs overriding.
|
|
160
|
+
this._drag = { path: { ...path, slot } };
|
|
114
161
|
this.setPointerCapture(event.pointerId);
|
|
115
162
|
event.preventDefault();
|
|
116
163
|
} else {
|
|
@@ -158,31 +205,41 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
158
205
|
return;
|
|
159
206
|
}
|
|
160
207
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
new_tab_panel = {
|
|
165
|
-
type: "tab-layout",
|
|
166
|
-
tabs: [slot],
|
|
167
|
-
selected: 0,
|
|
168
|
-
};
|
|
208
|
+
let tab_panel = this._layout.getPanel(slot, event.detail);
|
|
209
|
+
if (!tab_panel) {
|
|
210
|
+
tab_panel = { type: "tab-layout", tabs: [slot], selected: 0 };
|
|
169
211
|
}
|
|
170
212
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
213
|
+
// Each frame renders only its *own* tab. Frames in a stack overlap (see
|
|
214
|
+
// `create_css_grid_layout`); the tabs tile because every frame derives
|
|
215
|
+
// the same column template and places its tab in its own column. Only
|
|
216
|
+
// the selected frame shows its content; the rest keep just their tab.
|
|
217
|
+
const index = tab_panel.tabs.indexOf(slot);
|
|
218
|
+
const selected = (tab_panel.selected ?? 0) === index;
|
|
219
|
+
this.toggleAttribute("inactive", !selected);
|
|
220
|
+
this._default_tab.populate(this._layout, tab_panel, index);
|
|
221
|
+
this.drawTab(slot, index, tab_panel.tabs.length);
|
|
222
|
+
};
|
|
182
223
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Regenerates this frame's tab stylesheet: the shared titlebar grid template
|
|
226
|
+
* (so every frame in the stack aligns), the column this frame's tab occupies,
|
|
227
|
+
* and the tab label. The label renders via the title's `::before` `content`,
|
|
228
|
+
* reading the slot's `--regular-layout-<slot>--title` override variable with
|
|
229
|
+
* the slot name as the fallback - so a consumer can relabel a tab purely in
|
|
230
|
+
* CSS and the change applies reactively.
|
|
231
|
+
*
|
|
232
|
+
* @param slot - This frame's panel name.
|
|
233
|
+
* @param index - This frame's column (zero-based) within the stack.
|
|
234
|
+
* @param count - The number of tabs in the stack.
|
|
235
|
+
*/
|
|
236
|
+
private drawTab = (slot: string, index: number, count: number) => {
|
|
237
|
+
this._tab_sheet.replaceSync(
|
|
238
|
+
[
|
|
239
|
+
`.tabs{grid-template-columns:repeat(${count},var(--rl-tab-width,1fr))}`,
|
|
240
|
+
`.tabs slot[name="tab"]{grid-column:${index + 1}}`,
|
|
241
|
+
`[part~="title"]::before{content:var(${title_variable(slot)}, ${css_string(slot)})}`,
|
|
242
|
+
].join("\n"),
|
|
243
|
+
);
|
|
187
244
|
};
|
|
188
245
|
}
|
|
@@ -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
|
}
|