返回 DeepSeek-Reasonix
composerMenuViewport.ts
根目录 / desktop / frontend / src / lib / composerMenuViewport.ts
1 const COMPOSER_MENU_GAP = 6;
2 const VIEWPORT_EDGE_GAP = 8;
3
4 export const COMPOSER_MENU_AVAILABLE_HEIGHT = "--composer-menu-available-height";
5
6 export function composerMenuAvailableHeight(anchorTop: number): number {
7 if (!Number.isFinite(anchorTop)) return 0;
8 return Math.max(0, Math.floor(anchorTop - COMPOSER_MENU_GAP - VIEWPORT_EDGE_GAP));
9 }
10
11 function updateAvailableHeight(anchor: HTMLElement): void {
12 const next = `${composerMenuAvailableHeight(anchor.getBoundingClientRect().top)}px`;
13 if (anchor.style.getPropertyValue(COMPOSER_MENU_AVAILABLE_HEIGHT) !== next) {
14 anchor.style.setProperty(COMPOSER_MENU_AVAILABLE_HEIGHT, next);
15 }
16 }
17
18 // The composer menu is absolutely positioned above its anchor, so viewport
19 // height alone cannot describe the space available to it. Measure the anchor's
20 // real top edge and observe the surrounding layout: terminal resizing moves the
21 // composer without necessarily resizing the composer itself.
22 export function observeComposerMenuViewport(anchor: HTMLElement): () => void {
23 let live = true;
24 let frame: number | null = null;
25
26 const update = () => {
27 if (!live || !anchor.isConnected) return;
28 updateAvailableHeight(anchor);
29 };
30 const scheduleUpdate = () => {
31 if (!live || frame !== null) return;
32 frame = window.requestAnimationFrame(() => {
33 frame = null;
34 update();
35 });
36 };
37
38 // useLayoutEffect calls this before paint, so measure synchronously to avoid
39 // one frame at the 50vh CSS fallback when the terminal leaves less room.
40 update();
41
42 const observer = typeof ResizeObserver === "undefined"
43 ? null
44 : new ResizeObserver(scheduleUpdate);
45 if (observer) {
46 const observed = new Set<Element>();
47 let current: HTMLElement | null = anchor;
48 while (current) {
49 if (!observed.has(current)) {
50 observer.observe(current);
51 observed.add(current);
52 }
53 // Sibling size changes can move the anchor without changing its own box
54 // (the terminal drawer is the important example), so observe each
55 // ancestor's direct children as well as the ancestor itself.
56 for (const child of Array.from(current.children)) {
57 if (child instanceof HTMLElement && !observed.has(child)) {
58 observer.observe(child);
59 observed.add(child);
60 }
61 }
62 current = current.parentElement;
63 }
64 }
65
66 window.addEventListener("resize", scheduleUpdate);
67 window.addEventListener("scroll", scheduleUpdate, true);
68 window.visualViewport?.addEventListener("resize", scheduleUpdate);
69 window.visualViewport?.addEventListener("scroll", scheduleUpdate);
70
71 return () => {
72 live = false;
73 if (frame !== null) window.cancelAnimationFrame(frame);
74 observer?.disconnect();
75 window.removeEventListener("resize", scheduleUpdate);
76 window.removeEventListener("scroll", scheduleUpdate, true);
77 window.visualViewport?.removeEventListener("resize", scheduleUpdate);
78 window.visualViewport?.removeEventListener("scroll", scheduleUpdate);
79 anchor.style.removeProperty(COMPOSER_MENU_AVAILABLE_HEIGHT);
80 };
81 }
82
82 lines TYPESCRIPT