@netless/app-slide 0.2.103 → 0.2.105-beta.0

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.103",
3
+ "version": "0.2.105-beta.0",
4
4
  "main": "dist/main.cjs.js",
5
5
  "module": "dist/main.es.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  "types": "node scripts/build-types.mjs",
9
9
  "build": "vite build && npm run types",
10
10
  "build:dev": "vite build --mode development && npm run types",
11
- "cleanup": "rimraf ./dist"
11
+ "cleanup": "rimraf ./dist",
12
+ "test": "rimraf test/.cache && esbuild test/ResizableContainer.test.ts --bundle --platform=node --format=cjs --outfile=test/.cache/ResizableContainer.test.cjs && node test/.cache/ResizableContainer.test.cjs"
12
13
  },
13
14
  "files": [
14
15
  "src",
@@ -1,4 +1,4 @@
1
- import { Slide } from "@netless/slide";
1
+ import type { Slide } from "@netless/slide";
2
2
  import { ScrollBar } from "./ScrollBar";
3
3
  import type { AppContext, ReadonlyTeleBox } from "@netless/window-manager";
4
4
  import type { Attributes, MagixEvents } from "../typings";
@@ -8,8 +8,10 @@ export class ResizableContainer {
8
8
  private root: HTMLDivElement;
9
9
  public container: HTMLDivElement;
10
10
  private scrollContainer: HTMLDivElement;
11
+ /** @deprecated Use addScaleChangedListener() for multiple observers. */
11
12
  public onScaleChanged: ((scale: number) => void) | null = null;
12
13
  public onLayoutUpdated: (() => void) | null = null;
14
+ private scaleChangedListeners = new Set<(scale: number) => void>();
13
15
 
14
16
  private parent: HTMLElement;
15
17
  private whiteboardContainer: HTMLDivElement | null = null;
@@ -18,6 +20,7 @@ export class ResizableContainer {
18
20
  private slideWidth = 1;
19
21
  private slideHeight = 1;
20
22
  private scrollBar: ScrollBar | null = null;
23
+ private layoutFrameId: number | undefined;
21
24
  private context: AppContext<Attributes, MagixEvents, AppOptions>;
22
25
 
23
26
  // 每次 scale 后, 重置为居中
@@ -30,7 +33,7 @@ export class ResizableContainer {
30
33
  parent: HTMLElement,
31
34
  context: AppContext<Attributes, MagixEvents, AppOptions>,
32
35
  enableResize: boolean,
33
- _box?: ReadonlyTeleBox,
36
+ _box?: ReadonlyTeleBox
34
37
  ) {
35
38
  this.enableResize = enableResize;
36
39
  this.parent = parent;
@@ -56,7 +59,7 @@ export class ResizableContainer {
56
59
  // 初始化滚动条
57
60
  this.scrollBar = new ScrollBar(this.root, this);
58
61
  }
59
- };
62
+ }
60
63
 
61
64
  public getTranslate(): { x: number; y: number } {
62
65
  return { x: this.translateX, y: this.translateY };
@@ -66,6 +69,13 @@ export class ResizableContainer {
66
69
  return this.scale;
67
70
  }
68
71
 
72
+ public addScaleChangedListener(listener: (scale: number) => void): () => void {
73
+ this.scaleChangedListeners.add(listener);
74
+ return () => {
75
+ this.scaleChangedListeners.delete(listener);
76
+ };
77
+ }
78
+
69
79
  private renderScrollBar(
70
80
  width: number,
71
81
  overflowWidth: number,
@@ -97,6 +107,7 @@ export class ResizableContainer {
97
107
  };
98
108
 
99
109
  public updateResizableContainer() {
110
+ this.cancelScheduledLayoutUpdate();
100
111
  const parentBounds = this.scrollContainer.getBoundingClientRect();
101
112
  this.container.style.width = `${parentBounds.width * this.scale}px`;
102
113
  this.container.style.height = `${parentBounds.height * this.scale}px`;
@@ -183,13 +194,26 @@ export class ResizableContainer {
183
194
  this.scrollContainer.style.height = "100%";
184
195
  }
185
196
  applyScale = this.enableResize ? applyScale : 1;
186
- if (this.onScaleChanged && this.enableResize) {
187
- this.onScaleChanged(applyScale);
188
- }
189
197
  this.scale = applyScale;
190
- setTimeout(() => {
191
- this.updateResizableContainer();
192
- });
198
+ if (this.enableResize) {
199
+ this.onScaleChanged?.(this.scale);
200
+ for (const listener of this.scaleChangedListeners) {
201
+ listener(this.scale);
202
+ }
203
+ }
204
+ if (this.layoutFrameId === undefined) {
205
+ this.layoutFrameId = requestAnimationFrame(() => {
206
+ this.layoutFrameId = undefined;
207
+ this.updateResizableContainer();
208
+ });
209
+ }
210
+ }
211
+
212
+ private cancelScheduledLayoutUpdate(): void {
213
+ if (this.layoutFrameId !== undefined) {
214
+ cancelAnimationFrame(this.layoutFrameId);
215
+ this.layoutFrameId = undefined;
216
+ }
193
217
  }
194
218
 
195
219
  public addSlideContainer(slideContainer: HTMLDivElement) {
@@ -206,7 +230,9 @@ export class ResizableContainer {
206
230
  }
207
231
 
208
232
  public destroy(_box?: ReadonlyTeleBox): void {
233
+ this.cancelScheduledLayoutUpdate();
209
234
  this.onLayoutUpdated = null;
235
+ this.scaleChangedListeners.clear();
210
236
  this.scrollBar?.destroy();
211
237
  }
212
238
  }
@@ -33,6 +33,7 @@ export const EmptyAttributes: Attributes = {
33
33
  slideScale: 1,
34
34
  translateX: 0.5,
35
35
  translateY: 0.5,
36
+ originSize: null,
36
37
  };
37
38
 
38
39
  export interface SlideControllerOptions {
@@ -486,24 +486,32 @@ export class SlideDocsViewer {
486
486
 
487
487
  protected scaleDocsToFit = () => {
488
488
  if (this.slideController) {
489
- const { width, height } = this.slideController.slide;
490
- if (width && height) {
491
- this.whiteboardView.moveCameraToContain({
492
- originX: -width / 2,
493
- originY: -height / 2,
494
- width,
495
- height,
496
- animationMode: "immediately" as AnimationMode.Immediately,
497
- });
498
- this.whiteboardView.setCameraBound({
499
- damping: 1,
500
- maxContentMode: () => this.whiteboardView.camera.scale,
501
- minContentMode: () => this.whiteboardView.camera.scale,
502
- centerX: 0,
503
- centerY: 0,
504
- width,
505
- height,
506
- });
489
+ const { width: slideWidth, height: slideHeight } = this.slideController.slide;
490
+ if (slideWidth && slideHeight) {
491
+ const originSize = this.context.storage.state.originSize;
492
+ const { width, height } = originSize && originSize.width > 0 && originSize.height > 0
493
+ ? originSize
494
+ : { width: slideWidth, height: slideHeight };
495
+ if (width > 0 && height > 0) {
496
+ this.whiteboardView.moveCameraToContain({
497
+ originX: -width / 2,
498
+ originY: -height / 2,
499
+ width,
500
+ height,
501
+ animationMode: "immediately" as AnimationMode.Immediately,
502
+ });
503
+
504
+ const scale = this.whiteboardView.camera.scale;
505
+ this.whiteboardView.setCameraBound({
506
+ damping: 1,
507
+ maxContentMode: () => scale,
508
+ minContentMode: () => scale,
509
+ centerX: 0,
510
+ centerY: 0,
511
+ width: slideWidth,
512
+ height: slideHeight,
513
+ });
514
+ }
507
515
  if (!this.isViewMounted) {
508
516
  this.isViewMounted = true;
509
517
  console.log("[Slide] mount whiteboard view");
package/src/index.ts CHANGED
@@ -107,13 +107,15 @@ export interface AppResult {
107
107
  jumpToPage: (page: number) => boolean;
108
108
  setSildeReadonly: (bol: boolean) => void;
109
109
  setSyncEventQueuePolicy: (policy: NonNullable<ISlideConfig["syncEventQueuePolicy"]>) => void;
110
- onScaleChanged: (cb: (scale: number) => void) => void;
110
+ onScaleChanged: (cb: (scale: number) => void) => () => void;
111
111
  scaleView: (to: number) => void;
112
112
  getViewScale: () => number | undefined;
113
113
  translateView: (offsetX: number, offsetY: number) => void;
114
114
  }
115
115
 
116
- const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
116
+ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
117
+ teardown(context: import("@netless/window-manager").AppContext): void;
118
+ } = {
117
119
  kind: "Slide",
118
120
  setup(context) {
119
121
  console.log("[Slide] setup @ " + version);
@@ -247,7 +249,14 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
247
249
  });
248
250
  }
249
251
 
250
- context.emitter.on("destroy", () => {
252
+ let disposed = false;
253
+ let offDestroy: (() => void) | undefined;
254
+ const teardown = () => {
255
+ if (disposed) return;
256
+ disposed = true;
257
+ const removeDestroy = offDestroy;
258
+ offDestroy = undefined;
259
+ removeDestroy?.();
251
260
  log("[Slide] destroy", context.appId);
252
261
  if (useFreezer) apps.delete(context.appId);
253
262
  sideEffect.flushAll();
@@ -255,32 +264,33 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
255
264
  docsViewer.destroy();
256
265
  docsViewer = null;
257
266
  }
258
- });
267
+ };
268
+ offDestroy = context.emitter.on("destroy", teardown);
259
269
 
260
270
  docsViewer.mount();
261
271
 
272
+ (SlideApp as any).__teardownByContext ||= new WeakMap<object, () => void>();
273
+ (SlideApp as any).__teardownByContext.set(context, teardown);
274
+
262
275
  return {
263
276
  onScaleChanged: (cb: (scale: number) => void) => {
264
277
  if (!docsViewer) {
265
- return;
278
+ return () => void 0;
266
279
  }
267
- docsViewer.resizableContainer.onScaleChanged = cb;
280
+ return docsViewer.resizableContainer.addScaleChangedListener(cb);
268
281
  },
269
282
  scaleView: (to: number) => {
270
- let applyScale = Number(to);
271
- if (Number.isNaN(applyScale)) {
272
- applyScale = 1;
273
- }
274
- if (applyScale < 1.0) {
275
- applyScale = 1.0;
276
- }
277
- if (applyScale > 4.0) {
278
- applyScale = 4.0;
279
- }
280
-
281
- context.storage.setState({ slideScale: applyScale });
282
- context.storage.setState({ translateX: 0.5, translateY: 0.5 });
283
- docsViewer?.resizableContainer.scaleContainer(applyScale);
283
+ const applyScale = Number(to);
284
+ if (!Number.isFinite(applyScale) || applyScale <= 0) return;
285
+ const container = docsViewer?.resizableContainer;
286
+ if (!container || Math.abs(container.getScale() - applyScale) < 0.001) return;
287
+
288
+ context.storage.setState({
289
+ slideScale: applyScale,
290
+ translateX: 0.5,
291
+ translateY: 0.5,
292
+ });
293
+ container.scaleContainer(applyScale);
284
294
  },
285
295
  getViewScale: () => {
286
296
  return docsViewer?.resizableContainer.getScale();
@@ -387,6 +397,10 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
387
397
  },
388
398
  };
389
399
  },
400
+ teardown(context) {
401
+ (SlideApp as any).__teardownByContext?.get(context)?.();
402
+ (SlideApp as any).__teardownByContext?.delete(context);
403
+ },
390
404
  };
391
405
 
392
406
  export default SlideApp;
package/src/typings.ts CHANGED
@@ -15,6 +15,10 @@ export interface Attributes {
15
15
  slideScale: number;
16
16
  translateX: number;
17
17
  translateY: number;
18
+ originSize: {
19
+ width: number;
20
+ height: number;
21
+ } | null;
18
22
  }
19
23
 
20
24
  export type MagixPayload = {