Skip to content

Commit 5fc38be

Browse files
TripleTreclaude
andauthored
ResizableContainer (#56)
* feat: resizable slide * feat: add ResizableContainer with zoom and drag support - Implement ResizableContainer class for slide and whiteboard containers - Support touchpad pinch-to-zoom (0.5x-2x range) with smooth scaling - Support Ctrl/Cmd + wheel zoom and Shift + wheel drag - Add separate handling for slide (transform scale) and whiteboard (width/height) - Ensure zoom center follows mouse position for both containers - Implement synchronized movement to maintain perfect overlap - Add proper destroy methods for cleanup - Support large interaction area within parent element * feat: enhance ResizableContainer touch and interaction handling - Add support for two-finger drag movement when scaled - Implement intelligent touch gesture detection (scale vs drag) - Add Cmd/Ctrl+M mouse drag functionality - Extend scale range to 0.1x - 2.0x for better flexibility - Improve movement boundary calculations - Add comprehensive keyboard and mouse event handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor: simplify ResizableContainer and add scale method - Remove all interactive scaling logic (touch, mouse, keyboard) - Add scaleContainer method for programmatic scaling - Merge scaleContainer and updateContainers into single function - Implement proper whiteboard centering logic - Add storage listener for slideScale changes in SlideDocsViewer - Add triggerSlideResizeObserver to handle transform-based scaling - Support whiteboard and slide synchronization * fix: correct slideScale listener and clean up ResizableContainer - Fix storage state listener to handle diff.newValue format - Clean up ResizableContainer by removing duplicate logic - Remove unnecessary whiteboardTranslateX/Y properties - Simplify scaleContainer and updateContainers methods - Add debug logging for scaleView functionality - Optimize ResizeObserver setup and cleanup --------- Co-authored-by: Claude <[email protected]>
1 parent d077ec1 commit 5fc38be

File tree

6 files changed

+658
-6
lines changed

6 files changed

+658
-6
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import {Slide} from "@netless/slide";
2+
import {ScrollBar} from "./ScrollBar";
3+
import type {AppContext} from "@netless/window-manager";
4+
import type {Attributes, MagixEvents} from "../typings";
5+
import type {AppOptions} from "../index";
6+
7+
export class ResizableContainer {
8+
9+
private root: HTMLDivElement;
10+
public container: HTMLDivElement;
11+
private scrollContainer: HTMLDivElement;
12+
public onScaleChanged: ((scale: number) => void) | null = null;
13+
14+
private parent: HTMLElement;
15+
private whiteboardContainer: HTMLDivElement | null = null;
16+
private slide: Slide | null = null;
17+
private scale = 1;
18+
private resizeObserver: ResizeObserver | null = null;
19+
private slideWidth = 1;
20+
private slideHeight = 1;
21+
private scrollBar: ScrollBar;
22+
private context: AppContext<Attributes, MagixEvents, AppOptions>;
23+
24+
// 每次 scale 后, 重置为居中
25+
private translateX = 0.5;
26+
private translateY = 0.5;
27+
28+
private lastTriggerTime = 0;
29+
private enableResize: boolean;
30+
31+
constructor(
32+
parent: HTMLElement,
33+
context: AppContext<Attributes, MagixEvents, AppOptions>,
34+
enableResize: boolean
35+
) {
36+
this.enableResize = enableResize || true;
37+
this.parent = parent;
38+
this.context = context;
39+
this.root = document.createElement('div');
40+
this.root.style.width = '100%';
41+
this.root.style.height = '100%';
42+
this.root.style.overflow = 'hidden';
43+
this.parent.appendChild(this.root);
44+
this.scrollContainer = document.createElement('div');
45+
this.scrollContainer.setAttribute("data-resizable-scroll", "true");
46+
this.scrollContainer.style.width = '100%';
47+
this.scrollContainer.style.height = '100%';
48+
this.scrollContainer.style.position = 'relative';
49+
this.scrollContainer.style.overflow = 'hidden';
50+
this.container = document.createElement('div');
51+
this.container.style.position = 'relative';
52+
this.container.setAttribute("data-resizable-container", "true");
53+
this.scrollContainer.appendChild(this.container);
54+
this.root.appendChild(this.scrollContainer);
55+
56+
// 初始化滚动条
57+
this.scrollBar = new ScrollBar(this.root, this);
58+
59+
this.resizeObserver = new ResizeObserver(() => {
60+
this.updateResizableContainer();
61+
});
62+
this.resizeObserver.observe(this.scrollContainer);
63+
}
64+
65+
public getTranslate(): {x: number, y: number} {
66+
return { x: this.translateX, y: this.translateY };
67+
}
68+
69+
public getScale(): number {
70+
return this.scale;
71+
}
72+
73+
private renderScrollBar(width: number, overflowWidth: number, height: number, overflowHeight: number): void {
74+
if (this.scrollBar) {
75+
this.scrollBar.render(width, height, overflowWidth, overflowHeight);
76+
}
77+
}
78+
79+
public setSlideObject(slide: Slide) {
80+
this.slide = slide;
81+
this.slide.on("renderEnd", this.updateSlideSize);
82+
}
83+
84+
private updateSlideSize = () => {
85+
if (this.slide) {
86+
let updateContainer = false;
87+
if (this.slideWidth !== this.slide.width || this.slideHeight !== this.slide.height) {
88+
this.slideWidth = this.slide.width;
89+
this.slideHeight = this.slide.height;
90+
updateContainer = true;
91+
}
92+
if (updateContainer) {
93+
this.updateResizableContainer();
94+
}
95+
}
96+
};
97+
98+
public updateResizableContainer() {
99+
if (Date.now() - this.lastTriggerTime < 50) {
100+
return;
101+
}
102+
this.lastTriggerTime = Date.now();
103+
const parentBounds = this.scrollContainer.getBoundingClientRect();
104+
this.container.style.width = `${parentBounds.width * this.scale}px`;
105+
this.container.style.height = `${parentBounds.height * this.scale}px`;
106+
107+
if (this.whiteboardContainer) {
108+
const whiteboardBounds = this.whiteboardContainer.getBoundingClientRect();
109+
if (whiteboardBounds.width / whiteboardBounds.height > this.slideWidth / this.slideHeight) {
110+
// 裁剪两边
111+
const renderWidth = (whiteboardBounds.height * this.slideWidth / this.slideHeight);
112+
const padding = (whiteboardBounds.width - renderWidth) / 2;
113+
this.whiteboardContainer.style.clipPath = `inset(0px ${padding}px 0px ${padding}px)`;
114+
} else if (whiteboardBounds.width / whiteboardBounds.height < this.slideWidth / this.slideHeight) {
115+
// 裁剪上下
116+
const renderHeight = (whiteboardBounds.width * this.slideHeight / this.slideWidth);
117+
const padding = (whiteboardBounds.height - renderHeight) / 2;
118+
this.whiteboardContainer.style.clipPath = `inset(${padding}px 0px ${padding}px 0px)`;
119+
}
120+
}
121+
122+
this.translateX = 0.5;
123+
this.translateY = 0.5;
124+
125+
this.renderScrollBar(parentBounds.width, parentBounds.width * this.scale, parentBounds.height, parentBounds.height * this.scale);
126+
this.handleNormalizeTranslate(this.translateX, this.translateY, {
127+
triggerScrollBar: true,
128+
triggerSync: true,
129+
});
130+
}
131+
132+
// x, y 范围 0 ~ 1
133+
public handleNormalizeTranslate(x: number, y: number, options: {
134+
triggerScrollBar: boolean,
135+
triggerSync: boolean,
136+
}) {
137+
if (Math.abs(x - this.translateX) < 0.001 && Math.abs(y - this.translateY) < 0.001 && !options.triggerSync) {
138+
return;
139+
}
140+
const parentBounds = this.scrollContainer.getBoundingClientRect();
141+
const selfBounds = this.container.getBoundingClientRect();
142+
const translateX = -x * (selfBounds.width - parentBounds.width);
143+
const translateY = -y * (selfBounds.height - parentBounds.height);
144+
this.translateX = x;
145+
this.translateY = y;
146+
this.container.style.transform = `translate(${translateX}px, ${translateY}px)`;
147+
if (options.triggerScrollBar) {
148+
this.scrollBar.handleNormalizeTranslate(x, y);
149+
}
150+
if (options.triggerSync) {
151+
this.context.storage.setState({ translateX: this.translateX, translateY: this.translateY });
152+
}
153+
}
154+
155+
public scaleContainer(applyScale: number) {
156+
if (Math.abs(this.scale - applyScale) < 0.001) {
157+
return;
158+
}
159+
if (applyScale > 1.0) {
160+
this.scrollContainer.style.width = 'calc(100% - 6px)';
161+
this.scrollContainer.style.height = 'calc(100% - 6px)';
162+
} else {
163+
this.scrollContainer.style.width = '100%';
164+
this.scrollContainer.style.height = '100%';
165+
}
166+
applyScale = this.enableResize ? applyScale : 1;
167+
if (this.onScaleChanged && this.enableResize) {
168+
this.onScaleChanged(applyScale);
169+
}
170+
this.scale = applyScale;
171+
setTimeout(() => {
172+
this.updateResizableContainer();
173+
});
174+
}
175+
176+
public addSlideContainer(slideContainer: HTMLDivElement) {
177+
this.container.appendChild(slideContainer);
178+
}
179+
180+
public addWhiteboardContainer(whiteboardContainer: HTMLDivElement) {
181+
this.container.appendChild(whiteboardContainer);
182+
this.whiteboardContainer = whiteboardContainer;
183+
}
184+
185+
public getCurrentScale(): number {
186+
return this.scale;
187+
}
188+
189+
public destroy(): void {
190+
this.resizeObserver?.disconnect();
191+
this.scrollBar?.destroy();
192+
}
193+
}

0 commit comments

Comments
 (0)