regular-layout 0.0.1 → 0.1.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 +26 -7
- package/dist/common/calculate_edge.d.ts +15 -0
- package/dist/common/constants.d.ts +29 -0
- package/dist/common/generate_grid.d.ts +1 -1
- package/dist/common/generate_overlay.d.ts +1 -1
- package/dist/common/insert_child.d.ts +1 -1
- package/dist/common/layout_config.d.ts +8 -15
- package/dist/common/redistribute_panel_sizes.d.ts +1 -1
- package/dist/extensions.d.ts +14 -0
- package/dist/index.js +9 -9
- package/dist/index.js.map +4 -4
- package/dist/regular-layout-frame.d.ts +7 -0
- package/dist/regular-layout.d.ts +47 -17
- package/package.json +2 -1
- package/src/common/calculate_edge.ts +104 -0
- package/src/common/calculate_intersect.ts +22 -9
- package/src/common/constants.ts +46 -0
- package/src/common/flatten.ts +5 -0
- package/src/common/generate_grid.ts +7 -4
- package/src/common/generate_overlay.ts +8 -5
- package/src/common/insert_child.ts +22 -15
- package/src/common/layout_config.ts +9 -18
- package/src/common/redistribute_panel_sizes.ts +7 -5
- package/src/common/remove_child.ts +38 -13
- package/src/extensions.ts +30 -2
- package/src/regular-layout-frame.ts +146 -23
- package/src/regular-layout.ts +163 -91
- package/dist/common/calculate_split.d.ts +0 -2
- package/src/common/calculate_split.ts +0 -53
|
@@ -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 "./layout_config.ts";
|
|
12
|
+
import type { Layout, TabLayout } from "./layout_config.ts";
|
|
13
13
|
import { EMPTY_PANEL } from "./layout_config.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -24,10 +24,20 @@ import { EMPTY_PANEL } from "./layout_config.ts";
|
|
|
24
24
|
* Returns `EMPTY_PANEL` if the last panel is removed.
|
|
25
25
|
*/
|
|
26
26
|
export function remove_child(panel: Layout, child: string): Layout {
|
|
27
|
-
// If this is a child panel
|
|
28
|
-
// The caller should handle this case
|
|
27
|
+
// If this is a child panel, handle tab removal
|
|
29
28
|
if (panel.type === "child-panel") {
|
|
30
|
-
|
|
29
|
+
if (panel.child.includes(child)) {
|
|
30
|
+
const newChild = panel.child.filter((c) => c !== child);
|
|
31
|
+
if (newChild.length === 0) {
|
|
32
|
+
return structuredClone(EMPTY_PANEL);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
type: "child-panel",
|
|
36
|
+
child: newChild,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return structuredClone(panel);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
// Clone the panel structure
|
|
@@ -36,23 +46,36 @@ export function remove_child(panel: Layout, child: string): Layout {
|
|
|
36
46
|
// Try to remove the child from this split panel's children
|
|
37
47
|
const index = result.children.findIndex((p) => {
|
|
38
48
|
if (p.type === "child-panel") {
|
|
39
|
-
return p.child
|
|
49
|
+
return p.child.includes(child);
|
|
40
50
|
}
|
|
51
|
+
|
|
41
52
|
return false;
|
|
42
53
|
});
|
|
43
54
|
|
|
44
55
|
if (index !== -1) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const tab_layout = result.children[index] as TabLayout;
|
|
57
|
+
if (tab_layout.child.length === 1) {
|
|
58
|
+
// Found the child at this level - remove it
|
|
59
|
+
const newChildren = result.children.filter((_, i) => i !== index);
|
|
60
|
+
const newSizes = remove_and_redistribute(result.sizes, index);
|
|
61
|
+
|
|
62
|
+
// If only one child remains, collapse the split panel
|
|
63
|
+
if (newChildren.length === 1) {
|
|
64
|
+
return newChildren[0];
|
|
65
|
+
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
result.children = newChildren;
|
|
68
|
+
result.sizes = newSizes;
|
|
69
|
+
} else {
|
|
70
|
+
tab_layout.child.splice(tab_layout.child.indexOf(child), 1);
|
|
71
|
+
if (
|
|
72
|
+
tab_layout.selected &&
|
|
73
|
+
tab_layout.selected >= tab_layout.child.length
|
|
74
|
+
) {
|
|
75
|
+
tab_layout.selected--;
|
|
76
|
+
}
|
|
52
77
|
}
|
|
53
78
|
|
|
54
|
-
result.children = newChildren;
|
|
55
|
-
result.sizes = newSizes;
|
|
56
79
|
return result;
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -64,8 +87,10 @@ export function remove_child(panel: Layout, child: string): Layout {
|
|
|
64
87
|
if (updated !== p) {
|
|
65
88
|
modified = true;
|
|
66
89
|
}
|
|
90
|
+
|
|
67
91
|
return updated;
|
|
68
92
|
}
|
|
93
|
+
|
|
69
94
|
return p;
|
|
70
95
|
});
|
|
71
96
|
|
package/src/extensions.ts
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { RegularLayout } from "./regular-layout.ts";
|
|
13
13
|
import { RegularLayoutFrame } from "./regular-layout-frame.ts";
|
|
14
|
-
|
|
15
|
-
customElements.define("regular-layout-frame", RegularLayoutFrame);
|
|
14
|
+
import type { Layout } from "./common/layout_config.ts";
|
|
16
15
|
|
|
17
16
|
customElements.define("regular-layout", RegularLayout);
|
|
17
|
+
customElements.define("regular-layout-frame", RegularLayoutFrame);
|
|
18
18
|
|
|
19
19
|
declare global {
|
|
20
20
|
interface Document {
|
|
@@ -37,4 +37,32 @@ declare global {
|
|
|
37
37
|
get(tagName: "regular-layout"): typeof RegularLayout;
|
|
38
38
|
get(tagName: "regular-layout-frame"): typeof RegularLayoutFrame;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
interface HTMLElement {
|
|
42
|
+
addEventListener(
|
|
43
|
+
name: "regular-layout-update",
|
|
44
|
+
cb: (e: RegularLayoutEvent) => void,
|
|
45
|
+
options?: { signal: AbortSignal },
|
|
46
|
+
): void;
|
|
47
|
+
|
|
48
|
+
addEventListener(
|
|
49
|
+
name: "regular-layout-before-update",
|
|
50
|
+
cb: (e: RegularLayoutEvent) => void,
|
|
51
|
+
options?: { signal: AbortSignal },
|
|
52
|
+
): void;
|
|
53
|
+
|
|
54
|
+
removeEventListener(
|
|
55
|
+
name: "regular-layout-update",
|
|
56
|
+
cb: (e: RegularLayoutEvent) => void,
|
|
57
|
+
): void;
|
|
58
|
+
|
|
59
|
+
removeEventListener(
|
|
60
|
+
name: "regular-layout-before-update",
|
|
61
|
+
cb: (e: RegularLayoutEvent) => void,
|
|
62
|
+
): void;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface RegularLayoutEvent extends CustomEvent {
|
|
67
|
+
detail: Layout;
|
|
40
68
|
}
|
|
@@ -9,17 +9,21 @@
|
|
|
9
9
|
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
|
|
10
10
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import { MIN_DRAG_DISTANCE, OVERLAY_CLASSNAME } from "./common/constants.ts";
|
|
13
|
+
import type { LayoutPath, TabLayout } from "./common/layout_config.ts";
|
|
14
|
+
import type { RegularLayoutEvent } from "./extensions.ts";
|
|
13
15
|
import type { RegularLayout } from "./regular-layout.ts";
|
|
14
16
|
|
|
15
|
-
const CSS = `
|
|
17
|
+
const CSS = (className: string) => `
|
|
16
18
|
:host{--titlebar--height:24px;box-sizing:border-box}
|
|
17
|
-
:host(
|
|
18
|
-
:host(
|
|
19
|
-
:host(
|
|
20
|
-
:host(
|
|
19
|
+
:host(:not(.${className})){margin-top:calc(var(--titlebar--height) + 3px)!important;}
|
|
20
|
+
:host(:not(.${className}))::part(container){position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;background-color:inherit;border-radius:inherit}
|
|
21
|
+
:host(:not(.${className}))::part(titlebar){height:var(--titlebar--height);margin-top:calc(0px - var(--titlebar--height));user-select: none;}
|
|
22
|
+
:host(:not(.${className}))::part(body){flex:1 1 auto;}
|
|
21
23
|
`;
|
|
22
24
|
|
|
25
|
+
const HTML_TEMPLATE = `<slot part="container"><slot part="titlebar"></slot><slot part="body"><slot></slot></slot></slot>`;
|
|
26
|
+
|
|
23
27
|
/**
|
|
24
28
|
* A custom element that represents a draggable panel within a
|
|
25
29
|
* `<regular-layout>`.
|
|
@@ -51,64 +55,183 @@ export class RegularLayoutFrame extends HTMLElement {
|
|
|
51
55
|
private _layout!: RegularLayout;
|
|
52
56
|
private _header!: HTMLElement;
|
|
53
57
|
private _drag_state: LayoutPath<DOMRect> | null = null;
|
|
58
|
+
private _drag_moved: boolean = false;
|
|
59
|
+
private _tab_to_index_map: WeakMap<HTMLDivElement, number> = new WeakMap();
|
|
60
|
+
private _tab_panel_state: TabLayout | null = null;
|
|
54
61
|
constructor() {
|
|
55
62
|
super();
|
|
56
63
|
this._container_sheet = new CSSStyleSheet();
|
|
57
|
-
this._container_sheet.replaceSync(CSS);
|
|
64
|
+
this._container_sheet.replaceSync(CSS(OVERLAY_CLASSNAME));
|
|
58
65
|
this._shadowRoot = this.attachShadow({ mode: "open" });
|
|
59
66
|
this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
|
|
60
|
-
this._shadowRoot.innerHTML = `<slot part="container"><slot part="titlebar">header</slot><slot part="body"><slot></slot></slot></slot>`;
|
|
61
|
-
this._layout = this.parentElement as RegularLayout;
|
|
62
|
-
this._header = this._shadowRoot.children[0].children[0] as HTMLElement;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
connectedCallback() {
|
|
70
|
+
this._shadowRoot.innerHTML = HTML_TEMPLATE;
|
|
71
|
+
this._layout = this.parentElement as RegularLayout;
|
|
72
|
+
this._header = this._shadowRoot.children[0].children[0] as HTMLElement;
|
|
66
73
|
this._header.addEventListener("pointerdown", this.onPointerDown);
|
|
67
74
|
this._header.addEventListener("pointermove", this.onPointerMove);
|
|
68
75
|
this._header.addEventListener("pointerup", this.onPointerUp);
|
|
76
|
+
this._header.addEventListener("lostpointercapture", this.onPointerLost);
|
|
77
|
+
this._layout.addEventListener("regular-layout-update", this.drawTabs);
|
|
78
|
+
this._layout.addEventListener(
|
|
79
|
+
"regular-layout-before-update",
|
|
80
|
+
this.drawTabs,
|
|
81
|
+
);
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
disconnectedCallback() {
|
|
72
85
|
this._header.removeEventListener("pointerdown", this.onPointerDown);
|
|
73
86
|
this._header.removeEventListener("pointermove", this.onPointerMove);
|
|
74
87
|
this._header.removeEventListener("pointerup", this.onPointerUp);
|
|
88
|
+
this._header.removeEventListener("lostpointercapture", this.onPointerLost);
|
|
89
|
+
this._layout.removeEventListener("regular-layout-update", this.drawTabs);
|
|
90
|
+
this._layout.removeEventListener(
|
|
91
|
+
"regular-layout-before-update",
|
|
92
|
+
this.drawTabs,
|
|
93
|
+
);
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
private onPointerDown = (event: PointerEvent): void => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
const elem = event.target as HTMLDivElement;
|
|
98
|
+
if (elem.part.contains("tab")) {
|
|
99
|
+
this._drag_state = this._layout.calculateIntersect(
|
|
100
|
+
event.clientX,
|
|
101
|
+
event.clientY,
|
|
102
|
+
);
|
|
82
103
|
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
if (this._drag_state) {
|
|
105
|
+
this._header.setPointerCapture(event.pointerId);
|
|
106
|
+
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
|
+
}
|
|
85
113
|
}
|
|
86
|
-
|
|
87
|
-
this._header.setPointerCapture(event.pointerId);
|
|
88
|
-
event.preventDefault();
|
|
89
|
-
event.stopImmediatePropagation();
|
|
90
114
|
};
|
|
91
115
|
|
|
92
116
|
private onPointerMove = (event: PointerEvent): void => {
|
|
93
117
|
if (this._drag_state) {
|
|
118
|
+
// Only initiate a drag if the cursor has moved sufficiently.
|
|
119
|
+
if (!this._drag_moved) {
|
|
120
|
+
const [current_col, current_row, box] =
|
|
121
|
+
this._layout.relativeCoordinates(event.clientX, event.clientY);
|
|
122
|
+
|
|
123
|
+
const dx = (current_col - this._drag_state.column) * box.width;
|
|
124
|
+
const dy = (current_row - this._drag_state.row) * box.height;
|
|
125
|
+
if (Math.sqrt(dx * dx + dy * dy) <= MIN_DRAG_DISTANCE) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this._drag_moved = true;
|
|
94
131
|
this._layout.setOverlayState(
|
|
95
132
|
event.clientX,
|
|
96
133
|
event.clientY,
|
|
97
134
|
this._drag_state,
|
|
135
|
+
OVERLAY_CLASSNAME,
|
|
98
136
|
);
|
|
99
137
|
}
|
|
100
138
|
};
|
|
101
139
|
|
|
102
140
|
private onPointerUp = (event: PointerEvent): void => {
|
|
103
|
-
if (this._drag_state) {
|
|
141
|
+
if (this._drag_state && this._drag_moved) {
|
|
104
142
|
this._layout.clearOverlayState(
|
|
105
143
|
event.clientX,
|
|
106
144
|
event.clientY,
|
|
107
145
|
this._drag_state,
|
|
146
|
+
OVERLAY_CLASSNAME,
|
|
108
147
|
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// TODO This may be handled by `onPointerLost`, not sure if this is
|
|
151
|
+
// browser-specific behavior ...
|
|
152
|
+
this._header.releasePointerCapture(event.pointerId);
|
|
153
|
+
this._drag_state = null;
|
|
154
|
+
this._drag_moved = false;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
private onPointerLost = (event: PointerEvent): void => {
|
|
158
|
+
if (this._drag_state) {
|
|
159
|
+
this._layout.clearOverlayState(-1, -1, this._drag_state);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this._header.releasePointerCapture(event.pointerId);
|
|
163
|
+
this._drag_state = null;
|
|
164
|
+
this._drag_moved = false;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
private drawTabs = (event: RegularLayoutEvent) => {
|
|
168
|
+
const slot = this.assignedSlot;
|
|
169
|
+
if (!slot) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const new_panel = event.detail;
|
|
174
|
+
const new_tab_panel = this._layout.getPanel(slot.name, new_panel);
|
|
175
|
+
if (!new_tab_panel) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < new_tab_panel.child.length; i++) {
|
|
180
|
+
if (i >= this._header.children.length) {
|
|
181
|
+
const new_tab = this.createTab(new_tab_panel, i);
|
|
182
|
+
this._header.appendChild(new_tab);
|
|
183
|
+
} else {
|
|
184
|
+
const tab_changed =
|
|
185
|
+
(i === new_tab_panel.selected) !==
|
|
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
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const last_index = new_tab_panel.child.length;
|
|
201
|
+
for (let j = this._header.children.length - 1; j >= last_index; j--) {
|
|
202
|
+
this._header.removeChild(this._header.children[j]);
|
|
203
|
+
}
|
|
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
|
+
);
|
|
109
231
|
|
|
110
|
-
|
|
111
|
-
|
|
232
|
+
if (new_tab_panel) {
|
|
233
|
+
new_tab_panel.selected = index;
|
|
234
|
+
this._layout.restore(new_layout);
|
|
112
235
|
}
|
|
113
236
|
};
|
|
114
237
|
}
|