@netless/app-slide 0.2.85 → 0.2.86

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/app-slide",
3
- "version": "0.2.85",
3
+ "version": "0.2.86",
4
4
  "main": "dist/main.cjs.js",
5
5
  "module": "dist/main.es.js",
6
6
  "types": "dist/index.d.ts",
@@ -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,0 +1,327 @@
1
+ import {ResizableContainer} from "./ResizableContainer";
2
+
3
+ export class ScrollBar {
4
+ private container: HTMLElement;
5
+ private horizontalScrollBar: HTMLDivElement | null = null;
6
+ private verticalScrollBar: HTMLDivElement | null = null;
7
+ private resizableContainer: ResizableContainer;
8
+
9
+ // 跟踪拖动状态 - 分别跟踪水平和垂直滚动条
10
+ private isDraggingHorizontal = false;
11
+ private isDraggingVertical = false;
12
+
13
+ // 跟踪事件监听器以便清理
14
+ private eventListeners: Array<{
15
+ element: HTMLElement | Document;
16
+ type: string;
17
+ listener: (e: Event) => void;
18
+ }> = [];
19
+
20
+ constructor(container: HTMLElement, resizableContainer: ResizableContainer) {
21
+ this.container = container;
22
+ this.resizableContainer = resizableContainer;
23
+ }
24
+
25
+ /**
26
+ * 添加事件监听器并跟踪引用
27
+ */
28
+ private addTrackedListener<T extends HTMLElement | Document>(
29
+ element: T,
30
+ type: string,
31
+ listener: (e: any) => void
32
+ ): void {
33
+ element.addEventListener(type, listener);
34
+ this.eventListeners.push({ element, type, listener });
35
+ }
36
+
37
+ /**
38
+ * 移除所有跟踪的事件监听器
39
+ */
40
+ private removeAllListeners(): void {
41
+ this.eventListeners.forEach(({ element, type, listener }) => {
42
+ element.removeEventListener(type, listener);
43
+ });
44
+ this.eventListeners = [];
45
+ }
46
+
47
+
48
+
49
+ /**
50
+ * 渲染滚动条
51
+ * @param containerWidth 容器宽度
52
+ * @param containerHeight 容器高度
53
+ * @param contentWidth 内容宽度
54
+ * @param contentHeight 内容高度
55
+ */
56
+ public render(
57
+ containerWidth: number,
58
+ containerHeight: number,
59
+ contentWidth: number,
60
+ contentHeight: number
61
+ ): void {
62
+ this.clearScrollBars();
63
+
64
+ // 水平滚动条
65
+ if (contentWidth > containerWidth) {
66
+ this.createHorizontalScrollBar(containerWidth, contentWidth);
67
+ }
68
+
69
+ // 垂直滚动条
70
+ if (contentHeight > containerHeight) {
71
+ this.createVerticalScrollBar(containerHeight, contentHeight);
72
+ }
73
+ }
74
+
75
+ public handleNormalizeTranslate(x: number, y: number) {
76
+ // 确保坐标在有效范围内
77
+ const clampedX = Math.max(0, Math.min(1, x));
78
+ const clampedY = Math.max(0, Math.min(1, y));
79
+
80
+ // 更新水平滚动条 thumb 位置
81
+ if (this.horizontalScrollBar) {
82
+ const thumb = this.horizontalScrollBar.querySelector('.horizontal-thumb') as HTMLElement;
83
+ if (thumb) {
84
+ const trackWidth = this.horizontalScrollBar.offsetWidth;
85
+ const thumbWidth = thumb.offsetWidth;
86
+ const maxPosition = trackWidth - thumbWidth - 2; // 减去边距
87
+ if (maxPosition > 0) {
88
+ const newPosition = 1 + clampedX * maxPosition; // 1 是左边距
89
+ thumb.style.left = `${newPosition}px`;
90
+ }
91
+ }
92
+ }
93
+
94
+ // 更新垂直滚动条 thumb 位置
95
+ if (this.verticalScrollBar) {
96
+ const thumb = this.verticalScrollBar.querySelector('.vertical-thumb') as HTMLElement;
97
+ if (thumb) {
98
+ const trackHeight = this.verticalScrollBar.offsetHeight;
99
+ const thumbHeight = thumb.offsetHeight;
100
+ const maxPosition = trackHeight - thumbHeight - 2; // 减去边距
101
+ if (maxPosition > 0) {
102
+ const newPosition = 1 + clampedY * maxPosition; // 1 是上边距
103
+ thumb.style.top = `${newPosition}px`;
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+
110
+ private clearScrollBars(): void {
111
+ // 清理所有事件监听器
112
+ this.removeAllListeners();
113
+
114
+ if (this.horizontalScrollBar) {
115
+ this.horizontalScrollBar.remove();
116
+ this.horizontalScrollBar = null;
117
+ }
118
+ if (this.verticalScrollBar) {
119
+ this.verticalScrollBar.remove();
120
+ this.verticalScrollBar = null;
121
+ }
122
+ }
123
+
124
+ private createHorizontalScrollBar(
125
+ containerWidth: number,
126
+ contentWidth: number
127
+ ): void {
128
+ const scrollBar = document.createElement('div');
129
+ scrollBar.className = 'scroll-bar horizontal';
130
+
131
+ // 计算滚动条thumb宽度
132
+ const availableWidth = containerWidth;
133
+ const trackWidth = availableWidth;
134
+ const thumbWidth = Math.max(30, Math.min(trackWidth, (trackWidth * availableWidth) / contentWidth));
135
+
136
+ // 滚动条轨道
137
+ scrollBar.style.cssText = `
138
+ position: absolute;
139
+ bottom: 0;
140
+ left: 0;
141
+ width: ${trackWidth}px;
142
+ height: 6px;
143
+ z-index: 1000;
144
+ cursor: pointer;
145
+ opacity: 1;
146
+ transition: opacity 0.2s ease;
147
+ `;
148
+
149
+ // 滚动条始终显示,无需 hover 效果
150
+
151
+ // 滚动条thumb
152
+ const thumb = document.createElement('div');
153
+ thumb.className = 'scroll-bar-thumb horizontal-thumb';
154
+ thumb.style.cssText = `
155
+ position: absolute;
156
+ bottom: 0px;
157
+ left: 0px;
158
+ width: ${thumbWidth}px;
159
+ height: 6px;
160
+ background: #9E9E9E;
161
+ border-radius: 4px;
162
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
163
+ cursor: grab;
164
+ `;
165
+
166
+ // 添加拖动事件
167
+ this.addThumbDragListener(thumb, 'horizontal', thumbWidth, trackWidth);
168
+
169
+ scrollBar.appendChild(thumb);
170
+ this.container.appendChild(scrollBar);
171
+ this.horizontalScrollBar = scrollBar;
172
+ }
173
+
174
+ private createVerticalScrollBar(
175
+ containerHeight: number,
176
+ contentHeight: number
177
+ ): void {
178
+ const scrollBar = document.createElement('div');
179
+ scrollBar.className = 'scroll-bar vertical';
180
+
181
+ // 计算滚动条thumb高度
182
+ const availableHeight = containerHeight;
183
+ const trackHeight = availableHeight;
184
+ const thumbHeight = Math.max(30, Math.min(trackHeight, (trackHeight * availableHeight) / contentHeight));
185
+
186
+ // 滚动条轨道
187
+ scrollBar.style.cssText = `
188
+ position: absolute;
189
+ top: 0;
190
+ right: 0;
191
+ width: 6px;
192
+ height: ${trackHeight}px;
193
+ z-index: 1000;
194
+ cursor: pointer;
195
+ opacity: 1;
196
+ transition: opacity 0.2s ease;
197
+ `;
198
+
199
+ // 滚动条始终显示,无需 hover 效果
200
+
201
+ // 滚动条thumb
202
+ const thumb = document.createElement('div');
203
+ thumb.className = 'scroll-bar-thumb vertical-thumb';
204
+ thumb.style.cssText = `
205
+ position: absolute;
206
+ top: 1px;
207
+ left: 0px;
208
+ width: 6px;
209
+ height: ${thumbHeight}px;
210
+ background: #9E9E9E;
211
+ border-radius: 4px;
212
+ box-shadow: 1px 0 2px rgba(0, 0, 0, 0.2);
213
+ cursor: grab;
214
+ `;
215
+
216
+ // 添加拖动事件
217
+ this.addThumbDragListener(thumb, 'vertical', thumbHeight, trackHeight);
218
+
219
+ scrollBar.appendChild(thumb);
220
+ this.container.appendChild(scrollBar);
221
+ this.verticalScrollBar = scrollBar;
222
+ }
223
+
224
+ /**
225
+ * 为thumb添加拖动事件监听器
226
+ */
227
+ private addThumbDragListener(
228
+ thumb: HTMLElement,
229
+ orientation: 'horizontal' | 'vertical',
230
+ thumbSize: number,
231
+ trackSize: number
232
+ ): void {
233
+ let startPos = 0;
234
+ let startThumbPos = 0;
235
+
236
+ const handleStart = (clientX: number, clientY: number) => {
237
+ if (orientation === 'horizontal') {
238
+ this.isDraggingHorizontal = true;
239
+ } else {
240
+ this.isDraggingVertical = true;
241
+ }
242
+ startPos = orientation === 'horizontal' ? clientX : clientY;
243
+ startThumbPos = orientation === 'horizontal'
244
+ ? parseFloat(thumb.style.left) || 0
245
+ : parseFloat(thumb.style.top) || 0;
246
+
247
+ thumb.style.cursor = 'grabbing';
248
+ };
249
+
250
+ const handleMove = (clientX: number, clientY: number) => {
251
+ const isDragging = orientation === 'horizontal' ? this.isDraggingHorizontal : this.isDraggingVertical;
252
+ if (!isDragging) return;
253
+
254
+ const currentPos = orientation === 'horizontal' ? clientX : clientY;
255
+ const delta = currentPos - startPos;
256
+
257
+ // 计算新的thumb位置
258
+ let newThumbPos = startThumbPos + delta;
259
+ newThumbPos = Math.max(1, Math.min(trackSize - thumbSize - 1, newThumbPos));
260
+
261
+ // 更新thumb位置
262
+ if (orientation === 'horizontal') {
263
+ thumb.style.left = `${newThumbPos}px`;
264
+ } else {
265
+ thumb.style.top = `${newThumbPos}px`;
266
+ }
267
+
268
+ // 计算滚动比例 (0 到 1)
269
+ const maxScroll = trackSize - thumbSize - 2;
270
+ let scrollRatio = 0;
271
+ if (maxScroll > 0) {
272
+ scrollRatio = (newThumbPos - 1) / maxScroll;
273
+ scrollRatio = Math.max(0, Math.min(1, scrollRatio)); // 确保在 0-1 范围内
274
+ }
275
+
276
+ // 调用ResizableContainer的handleNormalizeTranslate方法,传入 false 避免循环调用
277
+ if (orientation === 'horizontal') {
278
+ this.resizableContainer.handleNormalizeTranslate(scrollRatio, this.resizableContainer['translateY'] ?? 0.5, {
279
+ triggerScrollBar: false,
280
+ triggerSync: true,
281
+ });
282
+ } else {
283
+ this.resizableContainer.handleNormalizeTranslate(this.resizableContainer['translateX'] ?? 0.5, scrollRatio, {
284
+ triggerScrollBar: false,
285
+ triggerSync: true,
286
+ });
287
+ }
288
+ };
289
+
290
+ const handleEnd = () => {
291
+ if (orientation === 'horizontal') {
292
+ this.isDraggingHorizontal = false;
293
+ } else {
294
+ this.isDraggingVertical = false;
295
+ }
296
+ thumb.style.cursor = 'grab';
297
+ };
298
+
299
+ // 鼠标事件
300
+ const handleMouseDown = (e: MouseEvent) => {
301
+ handleStart(e.clientX, e.clientY);
302
+ e.preventDefault();
303
+ };
304
+
305
+ const handleMouseMove = (e: MouseEvent) => {
306
+ handleMove(e.clientX, e.clientY);
307
+ };
308
+
309
+ const handleMouseUp = () => {
310
+ handleEnd();
311
+ // 滚动条始终显示,无需隐藏逻辑
312
+ };
313
+
314
+ // 添加事件监听器
315
+ this.addTrackedListener(thumb, 'mousedown', handleMouseDown);
316
+ this.addTrackedListener(document, 'mousemove', handleMouseMove);
317
+ this.addTrackedListener(document, 'mouseup', handleMouseUp);
318
+ }
319
+
320
+ /**
321
+ * 销毁滚动条
322
+ */
323
+ public destroy(): void {
324
+ this.clearScrollBars();
325
+ }
326
+ }
327
+
@@ -17,7 +17,7 @@ import { SideEffectManager } from "side-effect-manager";
17
17
  import { Slide, SLIDE_EVENTS } from "@netless/slide";
18
18
  import { clamp } from "../utils/helpers";
19
19
  import { cachedGetBgColor } from "../utils/bgcolor";
20
- import { logger, log, verbose, setRoomLogger } from "../utils/logger";
20
+ import { log, verbose, setRoomLogger } from "../utils/logger";
21
21
  import { getRoomTracker } from "../utils/tracker";
22
22
  export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
23
23
 
@@ -30,6 +30,9 @@ export const EmptyAttributes: Attributes = {
30
30
  resourceList: [],
31
31
  previewList: [],
32
32
  customLinks: [],
33
+ slideScale: 1,
34
+ translateX: 0.5,
35
+ translateY: 0.5,
33
36
  };
34
37
 
35
38
  export interface SlideControllerOptions {
@@ -289,7 +292,7 @@ export class SlideControllerBase {
289
292
  anchor,
290
293
  interactive: true,
291
294
  mode: "interactive",
292
- controller: logger.enable,
295
+ controller: false,
293
296
  enableGlobalClick: options.enableGlobalClick ?? true,
294
297
  renderOptions: {
295
298
  minFPS: options.minFPS || 25,
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { ReadonlyTeleBox, AnimationMode, View, AppContext } from "@netless/window-manager";
2
+ import type { ReadonlyTeleBox, AnimationMode, View, AppContext, StorageStateChangedListener } from "@netless/window-manager";
3
3
  import type { SlideController, SlideControllerOptions } from "../SlideController";
4
4
 
5
5
  import { SideEffectManager } from "side-effect-manager";
6
6
  import { createDocsViewerPages } from "../SlideController";
7
7
  import { DocsViewer, type DocsViewerPage } from "../DocsViewer";
8
+ import { ResizableContainer } from "../DocsViewer/ResizableContainer";
8
9
  import { logger } from "../utils/logger";
9
10
  import { isEditable } from "../utils/helpers";
10
11
  import type { Attributes, MagixEvents } from "../typings";
@@ -29,6 +30,7 @@ export interface SlideDocsViewerConfig {
29
30
  baseScenePath: string;
30
31
  appId: string;
31
32
  urlInterrupter?: (url: string) => Promise<string>;
33
+ enableScale?: boolean;
32
34
  onPagesReady?: (pages: DocsViewerPage[]) => void;
33
35
  onNavigate?: (index: number, origin?: string) => void;
34
36
  }
@@ -52,6 +54,7 @@ export class SlideDocsViewer {
52
54
  protected readonly appId: string;
53
55
  protected isViewMounted = false;
54
56
  protected justSildeReadonly = false;
57
+ private enableScale: boolean;
55
58
 
56
59
  public constructor({
57
60
  context,
@@ -62,6 +65,7 @@ export class SlideDocsViewer {
62
65
  baseScenePath,
63
66
  appId,
64
67
  urlInterrupter,
68
+ enableScale,
65
69
  onPagesReady,
66
70
  onNavigate,
67
71
  }: SlideDocsViewerConfig) {
@@ -72,6 +76,7 @@ export class SlideDocsViewer {
72
76
  this.mountWhiteboard = mountWhiteboard;
73
77
  this.onNavigate = onNavigate || noop;
74
78
  this.baseScenePath = baseScenePath;
79
+ this.enableScale = enableScale ?? false;
75
80
  this.appId = appId;
76
81
  this.viewer = new DocsViewer({
77
82
  readonly: box.readonly,
@@ -101,11 +106,60 @@ export class SlideDocsViewer {
101
106
  });
102
107
 
103
108
  this.render();
109
+
110
+ // 在 render() 之后设置监听器,确保 ResizableContainer 已经创建
111
+ this.sideEffect.add(() => {
112
+ const applyScale = (scale: number) => {
113
+ if (this.resizableContainer) {
114
+ this.resizableContainer.scaleContainer(scale);
115
+ }
116
+ };
117
+
118
+ // 记录初始状态
119
+ console.log('[SlideDocsViewer] Initial storage state:', this.context.storage.state);
120
+ console.log('[SlideDocsViewer] Initial slideScale:', this.context.storage.state.slideScale);
121
+
122
+ // 应用初始的 slideScale 值(现在 ResizableContainer 应该已经创建)
123
+ if (this.context.storage.state.slideScale !== undefined) {
124
+ console.log('[SlideDocsViewer] Applying initial slideScale:', this.context.storage.state.slideScale);
125
+ applyScale(this.context.storage.state.slideScale);
126
+ } else {
127
+ console.log('[SlideDocsViewer] No initial slideScale found');
128
+ }
129
+
130
+ const handler: StorageStateChangedListener<Attributes> = (diff) => {
131
+ if (diff.slideScale !== undefined) {
132
+ // slideScale 是一个包含 newValue 和 oldValue 的对象
133
+ const newScale = diff.slideScale.newValue;
134
+ applyScale(newScale ?? 1);
135
+ }
136
+ if (diff.translateX !== undefined || diff.translateY !== undefined) {
137
+ const currentTranslate = this.resizableContainer.getTranslate();
138
+ const translateX = diff.translateX ? (diff.translateX.newValue ?? 0.5) : currentTranslate.x;
139
+ const translateY = diff.translateY ? (diff.translateY.newValue ?? 0.5) : currentTranslate.y;
140
+ if (this.resizableContainer) {
141
+ this.resizableContainer.handleNormalizeTranslate(translateX, translateY, {
142
+ triggerScrollBar: true,
143
+ triggerSync: false,
144
+ });
145
+ }
146
+ }
147
+ };
148
+
149
+ console.log('[SlideDocsViewer] Adding storage state listener');
150
+ this.context.storage.onStateChanged.addListener(handler);
151
+
152
+ return () => {
153
+ console.log('[SlideDocsViewer] Removing storage state listener');
154
+ this.context.storage.onStateChanged.removeListener(handler);
155
+ };
156
+ });
104
157
  }
105
158
 
106
159
  public $slide!: HTMLDivElement;
107
160
  public $whiteboardView!: HTMLDivElement;
108
161
  public $overlay!: HTMLDivElement;
162
+ public resizableContainer!: ResizableContainer;
109
163
 
110
164
  public setJustSildeReadonly(justSildeReadonly: boolean) {
111
165
  this.justSildeReadonly = justSildeReadonly;
@@ -113,9 +167,20 @@ export class SlideDocsViewer {
113
167
  }
114
168
 
115
169
  public render() {
116
- this.viewer.$content.appendChild(this.renderSlideContainer());
117
- this.viewer.$content.appendChild(this.renderWhiteboardView());
118
- this.viewer.$content.appendChild(this.renderOverlay());
170
+ // 创建 ResizableContainer 来管理 slide 和 whiteboardView
171
+ if (!this.resizableContainer) {
172
+ this.resizableContainer = new ResizableContainer(this.viewer.$content, this.context, this.enableScale);
173
+ }
174
+
175
+ // 创建元素
176
+ this.renderSlideContainer();
177
+ this.renderWhiteboardView();
178
+ this.renderOverlay();
179
+
180
+ // 分别添加 slide 和 whiteboardView 到 ResizableContainer
181
+ this.resizableContainer.addSlideContainer(this.$slide);
182
+ this.resizableContainer.addWhiteboardContainer(this.$whiteboardView);
183
+ this.viewer.$content.appendChild(this.$overlay);
119
184
  this.sideEffect.addEventListener(window, "keydown", ev => {
120
185
  if (this.justSildeReadonly) {
121
186
  return;
@@ -184,6 +249,7 @@ export class SlideDocsViewer {
184
249
  onError: this.onError,
185
250
  });
186
251
 
252
+ this.resizableContainer.setSlideObject(this.slideController.slide);
187
253
  this.scaleDocsToFit();
188
254
  this.sideEffect.add(() => {
189
255
  this.whiteboardView.callbacks.on("onSizeUpdated", this.scaleDocsToFit);
@@ -248,6 +314,7 @@ export class SlideDocsViewer {
248
314
  this.slideController = null;
249
315
  }
250
316
  this.viewer.unmount();
317
+ this.resizableContainer.destroy();
251
318
  return this;
252
319
  }
253
320