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.
@@ -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(-2px - var(--titlebar--height));user-select: none;}
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._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;
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 (!this._drag_state) {
84
- return;
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
- this._header.setPointerCapture(event.pointerId);
88
- event.preventDefault();
89
- event.stopImmediatePropagation();
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
- this._header.releasePointerCapture(event.pointerId);
111
- this._drag_state = null;
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
  }
@@ -16,12 +16,17 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
 
19
- import { EMPTY_PANEL, iter_panel_children } from "./common/layout_config.ts";
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
- this.removePanel(drag_target.slot);
189
- this.insertPanel(drag_target.slot, path);
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
- const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
297
- const hit = calculate_intersection(col, row, this._panel);
298
- if (hit && hit.type !== "layout-path") {
299
- this._dragPath = [hit, col, row];
300
- this.setPointerCapture(event.pointerId);
301
- event.preventDefault();
302
- event.stopImmediatePropagation();
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