regular-layout 0.0.1 → 0.0.2
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 +9 -4
- package/dist/common/calculate_split.d.ts +14 -1
- package/dist/common/insert_child.d.ts +1 -1
- package/dist/common/layout_config.d.ts +14 -3
- package/dist/extensions.d.ts +16 -0
- package/dist/index.js +6 -6
- package/dist/index.js.map +3 -3
- package/dist/regular-layout-frame.d.ts +4 -0
- package/dist/regular-layout.d.ts +3 -2
- package/package.json +1 -1
- package/src/common/calculate_intersect.ts +19 -9
- package/src/common/calculate_split.ts +160 -28
- package/src/common/flatten.ts +4 -0
- package/src/common/generate_grid.ts +5 -3
- package/src/common/insert_child.ts +21 -15
- package/src/common/layout_config.ts +17 -4
- package/src/common/redistribute_panel_sizes.ts +5 -1
- package/src/common/remove_child.ts +37 -13
- package/src/extensions.ts +26 -2
- package/src/regular-layout-frame.ts +84 -13
- package/src/regular-layout.ts +57 -18
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
12
|
import type { LayoutPath } from "./common/layout_config.ts";
|
|
13
|
+
import type { RegularLayoutEvent } from "./extensions.ts";
|
|
13
14
|
import type { RegularLayout } from "./regular-layout.ts";
|
|
14
15
|
|
|
15
16
|
const CSS = `
|
|
16
17
|
:host{--titlebar--height:24px;box-sizing:border-box}
|
|
17
18
|
:host([slot]){margin-top:calc(var(--titlebar--height) + 3px)!important;}
|
|
18
19
|
:host([slot])::part(container){position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;background-color:inherit;border-radius:inherit}
|
|
19
|
-
:host([slot])::part(titlebar){height:var(--titlebar--height);margin-top:calc(
|
|
20
|
+
:host([slot])::part(titlebar){height:var(--titlebar--height);margin-top:calc(0px - var(--titlebar--height));user-select: none;}
|
|
20
21
|
:host([slot])::part(body){flex:1 1 auto;}
|
|
21
22
|
`;
|
|
22
23
|
|
|
24
|
+
const HTML_TEMPLATE = `<slot part="container"><slot part="titlebar"></slot><slot part="body"><slot></slot></slot></slot>`;
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* A custom element that represents a draggable panel within a
|
|
25
28
|
* `<regular-layout>`.
|
|
@@ -51,27 +54,74 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
51
54
|
private _layout!: RegularLayout;
|
|
52
55
|
private _header!: HTMLElement;
|
|
53
56
|
private _drag_state: LayoutPath<DOMRect> | null = null;
|
|
57
|
+
private _drag_moved: boolean = false;
|
|
58
|
+
private _tab_to_index_map: WeakMap<HTMLDivElement, number> = new WeakMap();
|
|
54
59
|
constructor() {
|
|
55
60
|
super();
|
|
56
61
|
this._container_sheet = new CSSStyleSheet();
|
|
57
62
|
this._container_sheet.replaceSync(CSS);
|
|
58
63
|
this._shadowRoot = this.attachShadow({ mode: "open" });
|
|
59
64
|
this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
65
|
+
this.drawTabs = this.drawTabs.bind(this);
|
|
66
|
+
this.onPointerDown = this.onPointerDown.bind(this);
|
|
67
|
+
this.onPointerMove = this.onPointerMove.bind(this);
|
|
68
|
+
this.onPointerUp = this.onPointerUp.bind(this);
|
|
69
|
+
this.onPointerLost = this.onPointerLost.bind(this);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
connectedCallback() {
|
|
73
|
+
this._shadowRoot.innerHTML = HTML_TEMPLATE;
|
|
74
|
+
this._layout = this.parentElement as RegularLayout;
|
|
75
|
+
this._header = this._shadowRoot.children[0].children[0] as HTMLElement;
|
|
66
76
|
this._header.addEventListener("pointerdown", this.onPointerDown);
|
|
67
77
|
this._header.addEventListener("pointermove", this.onPointerMove);
|
|
68
78
|
this._header.addEventListener("pointerup", this.onPointerUp);
|
|
79
|
+
this._header.addEventListener("lostpointercapture", this.onPointerLost);
|
|
80
|
+
this._layout.addEventListener("regular-layout-update", this.drawTabs);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
disconnectedCallback() {
|
|
72
84
|
this._header.removeEventListener("pointerdown", this.onPointerDown);
|
|
73
85
|
this._header.removeEventListener("pointermove", this.onPointerMove);
|
|
74
86
|
this._header.removeEventListener("pointerup", this.onPointerUp);
|
|
87
|
+
this._header.removeEventListener("lostpointercapture", this.onPointerLost);
|
|
88
|
+
this._layout.removeEventListener("regular-layout-update", this.drawTabs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private drawTabs(event: RegularLayoutEvent) {
|
|
92
|
+
const slot = this.getAttribute("slot");
|
|
93
|
+
if (slot) {
|
|
94
|
+
const result = this._layout.getPanel(slot, event.detail);
|
|
95
|
+
this._header.textContent = "";
|
|
96
|
+
if (!result) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let e = 0; e < (result?.child?.length || 0); e++) {
|
|
101
|
+
const elem = result?.child[e];
|
|
102
|
+
const div = document.createElement("div");
|
|
103
|
+
this._tab_to_index_map.set(div, e);
|
|
104
|
+
// div.dataset.index = `${e}`;
|
|
105
|
+
div.textContent = elem || "";
|
|
106
|
+
div.setAttribute(
|
|
107
|
+
"part",
|
|
108
|
+
e === (result?.selected || 0) ? "tab active-tab" : "tab",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const x = e;
|
|
112
|
+
if (e !== (result?.selected || 0)) {
|
|
113
|
+
div.addEventListener("pointerdown", (pointerEvent: PointerEvent) => {
|
|
114
|
+
result.selected = x;
|
|
115
|
+
this._layout.restore(event.detail);
|
|
116
|
+
pointerEvent.preventDefault();
|
|
117
|
+
pointerEvent.stopImmediatePropagation();
|
|
118
|
+
pointerEvent.stopPropagation();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this._header.appendChild(div);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
75
125
|
}
|
|
76
126
|
|
|
77
127
|
private onPointerDown = (event: PointerEvent): void => {
|
|
@@ -80,17 +130,25 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
80
130
|
event.clientY,
|
|
81
131
|
);
|
|
82
132
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
133
|
+
if (this._drag_state) {
|
|
134
|
+
const elem = event.target as HTMLDivElement;
|
|
135
|
+
if (elem.part.contains("tab")) {
|
|
136
|
+
const last_index = this._drag_state.path.length - 1;
|
|
137
|
+
const selected = this._tab_to_index_map.get(elem);
|
|
138
|
+
if (selected) {
|
|
139
|
+
this._drag_state.path[last_index] = selected;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
86
142
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
this._header.setPointerCapture(event.pointerId);
|
|
144
|
+
// event.preventDefault();
|
|
145
|
+
// event.stopImmediatePropagation();
|
|
146
|
+
}
|
|
90
147
|
};
|
|
91
148
|
|
|
92
149
|
private onPointerMove = (event: PointerEvent): void => {
|
|
93
150
|
if (this._drag_state) {
|
|
151
|
+
this._drag_moved = true;
|
|
94
152
|
this._layout.setOverlayState(
|
|
95
153
|
event.clientX,
|
|
96
154
|
event.clientY,
|
|
@@ -100,15 +158,28 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
100
158
|
};
|
|
101
159
|
|
|
102
160
|
private onPointerUp = (event: PointerEvent): void => {
|
|
103
|
-
if (this._drag_state) {
|
|
161
|
+
if (this._drag_state && this._drag_moved) {
|
|
104
162
|
this._layout.clearOverlayState(
|
|
105
163
|
event.clientX,
|
|
106
164
|
event.clientY,
|
|
107
165
|
this._drag_state,
|
|
108
166
|
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// TODO This may be handled by `onPointerLost`, not sure if this is
|
|
170
|
+
// browser-specific behavior ...
|
|
171
|
+
this._header.releasePointerCapture(event.pointerId);
|
|
172
|
+
this._drag_state = null;
|
|
173
|
+
this._drag_moved = false;
|
|
174
|
+
};
|
|
109
175
|
|
|
110
|
-
|
|
111
|
-
|
|
176
|
+
private onPointerLost = (event: PointerEvent): void => {
|
|
177
|
+
if (this._drag_state) {
|
|
178
|
+
this._layout.clearOverlayState(-1, -1, this._drag_state);
|
|
112
179
|
}
|
|
180
|
+
|
|
181
|
+
this._header.releasePointerCapture(event.pointerId);
|
|
182
|
+
this._drag_state = null;
|
|
183
|
+
this._drag_moved = false;
|
|
113
184
|
};
|
|
114
185
|
}
|
package/src/regular-layout.ts
CHANGED
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
EMPTY_PANEL,
|
|
21
|
+
iter_panel_children,
|
|
22
|
+
OVERLAY_DEFAULT,
|
|
23
|
+
} from "./common/layout_config.ts";
|
|
20
24
|
import { create_css_grid_layout } from "./common/generate_grid.ts";
|
|
21
25
|
import type {
|
|
22
26
|
LayoutPath,
|
|
23
27
|
Layout,
|
|
24
28
|
LayoutDivider,
|
|
29
|
+
TabLayout,
|
|
25
30
|
} from "./common/layout_config.ts";
|
|
26
31
|
import { calculate_intersection } from "./common/calculate_intersect.ts";
|
|
27
32
|
import { remove_child } from "./common/remove_child.ts";
|
|
@@ -31,10 +36,6 @@ import { updateOverlaySheet } from "./common/generate_overlay.ts";
|
|
|
31
36
|
import { calculate_split } from "./common/calculate_split.ts";
|
|
32
37
|
import { flatten } from "./common/flatten.ts";
|
|
33
38
|
|
|
34
|
-
export type OverlayMode = "grid" | "absolute" | "interactive";
|
|
35
|
-
|
|
36
|
-
const OVERLAY_DEFAULT: OverlayMode = "absolute";
|
|
37
|
-
|
|
38
39
|
/**
|
|
39
40
|
* A Web Component that provides a resizable panel layout system.
|
|
40
41
|
* Panels are arranged using CSS Grid and can be resized by dragging dividers.
|
|
@@ -129,9 +130,6 @@ export class RegularLayout extends HTMLElement {
|
|
|
129
130
|
let drop_target = calculate_intersection(col, row, panel, false);
|
|
130
131
|
if (drop_target) {
|
|
131
132
|
drop_target = calculate_split(col, row, panel, slot, drop_target);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (drop_target) {
|
|
135
133
|
if (mode === "interactive") {
|
|
136
134
|
let new_panel = remove_child(this._panel, slot);
|
|
137
135
|
new_panel = flatten(insert_child(new_panel, slot, drop_target.path));
|
|
@@ -145,7 +143,13 @@ export class RegularLayout extends HTMLElement {
|
|
|
145
143
|
const css = `${create_css_grid_layout(panel)}\n${updateOverlaySheet({ ...drop_target, box })}`;
|
|
146
144
|
this._stylesheet.replaceSync(css);
|
|
147
145
|
}
|
|
146
|
+
} else {
|
|
147
|
+
const css = `${create_css_grid_layout(panel)}}`;
|
|
148
|
+
this._stylesheet.replaceSync(css);
|
|
148
149
|
}
|
|
150
|
+
|
|
151
|
+
const event = new CustomEvent("regular-layout-update", { detail: panel });
|
|
152
|
+
this.dispatchEvent(event);
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -184,9 +188,17 @@ export class RegularLayout extends HTMLElement {
|
|
|
184
188
|
);
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
const { path } = drop_target ? drop_target : drag_target;
|
|
188
|
-
|
|
189
|
-
this.
|
|
191
|
+
const { path, orientation } = drop_target ? drop_target : drag_target;
|
|
192
|
+
|
|
193
|
+
this.restore(
|
|
194
|
+
insert_child(
|
|
195
|
+
panel,
|
|
196
|
+
drag_target.slot,
|
|
197
|
+
path,
|
|
198
|
+
orientation,
|
|
199
|
+
!drop_target?.is_edge,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
190
202
|
}
|
|
191
203
|
|
|
192
204
|
/**
|
|
@@ -208,6 +220,24 @@ export class RegularLayout extends HTMLElement {
|
|
|
208
220
|
this.restore(remove_child(this._panel, name));
|
|
209
221
|
}
|
|
210
222
|
|
|
223
|
+
getPanel(name: string, layout: Layout = this._panel): TabLayout | null {
|
|
224
|
+
if (layout.type === "child-panel") {
|
|
225
|
+
if (layout.child.includes(name)) {
|
|
226
|
+
return layout;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const child of layout.children) {
|
|
232
|
+
const found = this.getPanel(name, child);
|
|
233
|
+
if (found) {
|
|
234
|
+
return found;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
211
241
|
/**
|
|
212
242
|
* Determines which panel is at a given screen coordinate.
|
|
213
243
|
* Useful for drag-and-drop operations or custom interactions.
|
|
@@ -230,6 +260,10 @@ export class RegularLayout extends HTMLElement {
|
|
|
230
260
|
return null;
|
|
231
261
|
}
|
|
232
262
|
|
|
263
|
+
clear() {
|
|
264
|
+
this.restore(EMPTY_PANEL);
|
|
265
|
+
}
|
|
266
|
+
|
|
233
267
|
/**
|
|
234
268
|
* Restores the layout from a saved state.
|
|
235
269
|
*
|
|
@@ -264,6 +298,9 @@ export class RegularLayout extends HTMLElement {
|
|
|
264
298
|
this._slots.delete(key);
|
|
265
299
|
}
|
|
266
300
|
}
|
|
301
|
+
|
|
302
|
+
const event = new CustomEvent("regular-layout-update", { detail: layout });
|
|
303
|
+
this.dispatchEvent(event);
|
|
267
304
|
}
|
|
268
305
|
|
|
269
306
|
/**
|
|
@@ -293,13 +330,15 @@ export class RegularLayout extends HTMLElement {
|
|
|
293
330
|
}
|
|
294
331
|
|
|
295
332
|
private onPointerDown(event: PointerEvent) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
333
|
+
if (event.target === this) {
|
|
334
|
+
const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
|
|
335
|
+
const hit = calculate_intersection(col, row, this._panel);
|
|
336
|
+
if (hit && hit.type !== "layout-path") {
|
|
337
|
+
this._dragPath = [hit, col, row];
|
|
338
|
+
this.setPointerCapture(event.pointerId);
|
|
339
|
+
// event.preventDefault();
|
|
340
|
+
// event.stopImmediatePropagation();
|
|
341
|
+
}
|
|
303
342
|
}
|
|
304
343
|
}
|
|
305
344
|
|