Skip to content

Commit 5865e00

Browse files
authored
Merge pull request #16 from texodus/presize-ext
Extend presize functionality
2 parents 59a7ed4 + 9018a34 commit 5865e00

11 files changed

Lines changed: 394 additions & 63 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "regular-layout",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "A regular CSS `grid` container",
55
"keywords": [],
66
"license": "Apache-2.0",

src/core/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ export interface LayoutPath {
100100
*/
101101
export interface PresizeDetail {
102102
calculatePresizePaths(): Record<string, LayoutPath>;
103+
104+
/**
105+
* When the transition is a drag preview, the dragged panel's name and the
106+
* {@link LayoutPath} of its preview box. The dragged panel is removed from
107+
* the layout tree during a drag (so it is absent from
108+
* {@link PresizeDetail.calculatePresizePaths}), but `path.view_window` here
109+
* is exactly where the overlay renders it, letting consumers presize the
110+
* dragged panel like any other. `null` when the pointer is outside every
111+
* drop target (the dragged panel is hidden), `undefined` for non-drag
112+
* transitions.
113+
*/
114+
overlay?: { slot: string; path: LayoutPath } | null;
103115
}
104116

105117
/**

src/model/overlay_controller.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -70,36 +70,49 @@ export class OverlayController {
7070
);
7171
}
7272

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];
73+
// The dragged panel is removed from `panel` above, so it is absent from
74+
// the presize paths - `drop_target` carries its preview box instead
75+
// (null when the pointer is outside every drop target and the dragged
76+
// panel is hidden).
77+
const overlay = drop_target ? { slot, path: drop_target } : null;
78+
await host.presizeQueue.run(
79+
panel,
80+
() => {
81+
if (mode === "grid" && drop_target) {
82+
const path: [string, string] = [slot, drop_target?.slot];
83+
const css = create_css_grid_layout(panel, path, host.physics);
84+
host.stylesheet.replaceSync(css);
85+
} else if (mode === "absolute") {
86+
const grid_css = create_css_grid_layout(
87+
panel,
88+
undefined,
89+
host.physics,
90+
);
91+
92+
while (drag_element?.tagName === "SLOT" && drag_element) {
93+
drag_element = (
94+
drag_element as HTMLSlotElement
95+
).assignedElements()[0];
96+
}
97+
98+
const margin = drag_element
99+
? getComputedStyle(drag_element)
100+
: undefined;
101+
102+
const overlay_css = updateOverlaySheet(
103+
slot,
104+
box,
105+
style,
106+
drop_target,
107+
host.physics,
108+
margin,
109+
);
110+
111+
host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
85112
}
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-
});
113+
},
114+
overlay,
115+
);
103116

104117
const event_name = `${host.physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
105118
const custom_event = new CustomEvent<Layout>(event_name, {

src/model/presize_queue.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@
99
// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
1010
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1111

12-
import type { Layout, PresizeDetail } from "../core/types.ts";
12+
import type { Layout, LayoutPath, PresizeDetail } from "../core/types.ts";
1313
import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
1414

15+
/**
16+
* The dragged panel's preview geometry during a drag transition - see
17+
* {@link PresizeDetail.overlay}.
18+
*/
19+
export type PresizeOverlay = { slot: string; path: LayoutPath } | null;
20+
1521
/**
1622
* Manages cancelable pre-resize gating for layout updates.
1723
*
@@ -22,27 +28,40 @@ import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
2228
*/
2329
export class PresizeQueue {
2430
#resizing = false;
25-
#queued: { layout: Layout; fn: () => void } | null = null;
31+
#queued: {
32+
layout: Layout;
33+
fn: () => void;
34+
overlay?: PresizeOverlay;
35+
} | null = null;
2636
#pending: (() => void) | null = null;
2737

2838
constructor(
2939
private _target: EventTarget,
3040
private _eventName: string,
41+
private _onDispatch?: () => void,
3142
) {}
3243

33-
async run(layout: Layout, fn: () => void): Promise<void> {
44+
async run(
45+
layout: Layout,
46+
fn: () => void,
47+
overlay?: PresizeOverlay,
48+
): Promise<void> {
3449
if (this.#resizing) {
35-
this.#queued = { layout, fn };
50+
this.#queued = { layout, fn, overlay };
3651
return;
3752
}
3853

3954
this.#resizing = true;
4055
try {
41-
await this.#dispatchAndMaybeWait(layout, fn);
56+
await this.#dispatchAndMaybeWait(layout, fn, overlay);
4257
while (this.#queued) {
43-
const { layout: nextLayout, fn: nextFn } = this.#queued;
58+
const {
59+
layout: nextLayout,
60+
fn: nextFn,
61+
overlay: nextOverlay,
62+
} = this.#queued;
4463
this.#queued = null;
45-
await this.#dispatchAndMaybeWait(nextLayout, nextFn);
64+
await this.#dispatchAndMaybeWait(nextLayout, nextFn, nextOverlay);
4665
}
4766
} finally {
4867
this.#resizing = false;
@@ -57,9 +76,15 @@ export class PresizeQueue {
5776
}
5877
}
5978

60-
async #dispatchAndMaybeWait(layout: Layout, fn: () => void): Promise<void> {
79+
async #dispatchAndMaybeWait(
80+
layout: Layout,
81+
fn: () => void,
82+
overlay?: PresizeOverlay,
83+
): Promise<void> {
84+
this._onDispatch?.();
6185
const detail: PresizeDetail = {
6286
calculatePresizePaths: () => calculate_presize_paths(layout),
87+
overlay,
6388
};
6489

6590
const event = new CustomEvent(this._eventName, {

src/regular-layout-frame.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const CSS = `
2727
`;
2828

2929
const HTML_TEMPLATE = `
30-
<div part="titlebar"><div class="tabs"><slot name="tab"><regular-layout-tab></regular-layout-tab></slot></div></div>
30+
<div part="titlebar"><div class="tabs" part="titlebar-track"><slot name="tab"><regular-layout-tab></regular-layout-tab></slot></div></div>
3131
<div part="container"><slot></slot></div>
3232
`;
3333

src/regular-layout.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export class RegularLayout extends HTMLElement {
130130
private _presizeQueue: PresizeQueue;
131131
private _overlayController: OverlayController;
132132
private _maximized?: string;
133+
private _resize_observer?: ResizeObserver;
133134

134135
constructor() {
135136
super();
@@ -141,7 +142,13 @@ export class RegularLayout extends HTMLElement {
141142
this._cursor_stylesheet = new CSSStyleSheet();
142143
this._cursor_override = false;
143144
const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-resize`;
144-
this._presizeQueue = new PresizeQueue(this, event_name);
145+
// Refresh the bounds cache once per layout transition, so consumers
146+
// reading coordinates from a `before-resize` handler always see the
147+
// element's current position - a `ResizeObserver` alone can't catch
148+
// position-only moves of the host.
149+
this._presizeQueue = new PresizeQueue(this, event_name, () => {
150+
this._dimensions = undefined;
151+
});
145152
this._overlayController = new OverlayController(this.create_overlay_host());
146153
this._shadowRoot.adoptedStyleSheets = [
147154
this._stylesheet,
@@ -150,13 +157,19 @@ export class RegularLayout extends HTMLElement {
150157
}
151158

152159
connectedCallback() {
160+
this._resize_observer ??= new ResizeObserver(() => {
161+
this._dimensions = undefined;
162+
});
163+
164+
this._resize_observer.observe(this);
153165
this.addEventListener("dblclick", this.onDblClick);
154166
this.addEventListener("pointerdown", this.onPointerDown);
155167
this.addEventListener("pointerup", this.onPointerUp);
156168
this.addEventListener("pointermove", this.onPointerMove);
157169
}
158170

159171
disconnectedCallback() {
172+
this._resize_observer?.disconnect();
160173
this.removeEventListener("dblclick", this.onDblClick);
161174
this.removeEventListener("pointerdown", this.onPointerDown);
162175
this.removeEventListener("pointerup", this.onPointerUp);
@@ -334,34 +347,54 @@ export class RegularLayout extends HTMLElement {
334347
* {@link save}, and any subsequent {@link restore} resets the layout to the
335348
* minimized (normal, multi-panel) view.
336349
*
350+
* Before applying, dispatches a cancelable `regular-layout-before-resize`
351+
* event (as {@link restore} does) whose paths describe the post-maximize
352+
* geometry - a single full-window entry for `name`. If the event is
353+
* cancelled via `preventDefault()`, the update is suspended until
354+
* {@link resumeResize} is called.
355+
*
337356
* Has no effect if `name` is not present in the current layout.
338357
*
339358
* @param name - The name of the panel to maximize.
340359
*/
341-
maximize = (name: string) => {
360+
maximize = async (name: string) => {
342361
if (!this.getPanel(name)) {
343362
return;
344363
}
345364

346-
this._maximized = name;
347-
this._stylesheet.replaceSync(
348-
create_css_maximize_layout(name, this._physics),
349-
);
365+
// The post-maximize geometry as a `Layout`: `name` alone, full-window.
366+
// Panels hidden by the maximize are absent from the presize paths,
367+
// consistent with `calculate_presize_paths`' visible-panels-only
368+
// semantics.
369+
const layout: Layout = { type: "tab-layout", tabs: [name], selected: 0 };
370+
await this._presizeQueue.run(layout, () => {
371+
this._maximized = name;
372+
this._stylesheet.replaceSync(
373+
create_css_maximize_layout(name, this._physics),
374+
);
375+
});
350376
};
351377

352378
/**
353379
* Restores the normal multi-panel view after a {@link maximize}, without
354380
* altering the layout tree. No-op if no panel is currently maximized.
381+
*
382+
* Before applying, dispatches a cancelable `regular-layout-before-resize`
383+
* event (as {@link restore} does) with paths for the restored multi-panel
384+
* geometry. If the event is cancelled via `preventDefault()`, the update
385+
* is suspended until {@link resumeResize} is called.
355386
*/
356-
minimize = () => {
387+
minimize = async () => {
357388
if (this._maximized === undefined) {
358389
return;
359390
}
360391

361-
this._maximized = undefined;
362-
this._stylesheet.replaceSync(
363-
create_css_grid_layout(this._panel, undefined, this._physics),
364-
);
392+
await this._presizeQueue.run(this._panel, () => {
393+
this._maximized = undefined;
394+
this._stylesheet.replaceSync(
395+
create_css_grid_layout(this._panel, undefined, this._physics),
396+
);
397+
});
365398
};
366399

367400
/**
@@ -371,6 +404,7 @@ export class RegularLayout extends HTMLElement {
371404
* @param layout - The layout tree to restore
372405
*/
373406
restoreSync = (layout: Layout, _is_flattened: boolean = false) => {
407+
this._dimensions = undefined;
374408
this._maximized = undefined;
375409
this._panel = !_is_flattened ? flatten(layout) : layout;
376410
const css = create_css_grid_layout(this._panel, undefined, this._physics);

0 commit comments

Comments
 (0)