@netless/app-slide 0.2.99 → 0.2.100-alpha.1

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,19 +1,27 @@
1
1
  {
2
2
  "name": "@netless/app-slide",
3
- "version": "0.2.99",
3
+ "version": "0.2.100-alpha.1",
4
4
  "main": "dist/main.cjs.js",
5
5
  "module": "dist/main.es.js",
6
6
  "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "test": "node __tests__/recovery.test.js",
9
+ "types": "node scripts/build-types.mjs",
10
+ "build": "vite build && npm run types",
11
+ "build:dev": "vite build --mode development && npm run types",
12
+ "cleanup": "rimraf ./dist"
13
+ },
7
14
  "files": [
8
15
  "src",
9
16
  "dist",
10
17
  "README-zh.md"
11
18
  ],
12
19
  "devDependencies": {
13
- "@netless/slide": "1.4.54",
20
+ "@netless/slide": "1.4.55-alpha.4",
14
21
  "@types/color-string": "^1.5.2",
15
22
  "color-string": "^1.9.1",
16
23
  "jspdf": "2.5.1",
24
+ "@juggle/resize-observer": "^3.3.1",
17
25
  "side-effect-manager": "^1.2.1",
18
26
  "vanilla-lazyload": "^17.8.3"
19
27
  },
@@ -24,11 +32,5 @@
24
32
  "jspdf": {
25
33
  "optional": true
26
34
  }
27
- },
28
- "scripts": {
29
- "types": "node scripts/build-types.mjs",
30
- "build": "vite build && npm run types",
31
- "build:dev": "vite build --mode development && npm run types",
32
- "cleanup": "rimraf ./dist"
33
35
  }
34
- }
36
+ }
@@ -1,6 +1,6 @@
1
1
  import { Slide } from "@netless/slide";
2
2
  import { ScrollBar } from "./ScrollBar";
3
- import type { AppContext } from "@netless/window-manager";
3
+ import type { AppContext, ReadonlyTeleBox } from "@netless/window-manager";
4
4
  import type { Attributes, MagixEvents } from "../typings";
5
5
  import type { AppOptions } from "../index";
6
6
 
@@ -14,7 +14,6 @@ export class ResizableContainer {
14
14
  private whiteboardContainer: HTMLDivElement | null = null;
15
15
  private slide: Slide | null = null;
16
16
  private scale = 1;
17
- private resizeObserver: ResizeObserver | null = null;
18
17
  private slideWidth = 1;
19
18
  private slideHeight = 1;
20
19
  private scrollBar: ScrollBar | null = null;
@@ -29,7 +28,8 @@ export class ResizableContainer {
29
28
  constructor(
30
29
  parent: HTMLElement,
31
30
  context: AppContext<Attributes, MagixEvents, AppOptions>,
32
- enableResize: boolean
31
+ enableResize: boolean,
32
+ _box?: ReadonlyTeleBox,
33
33
  ) {
34
34
  this.enableResize = enableResize;
35
35
  this.parent = parent;
@@ -55,13 +55,7 @@ export class ResizableContainer {
55
55
  // 初始化滚动条
56
56
  this.scrollBar = new ScrollBar(this.root, this);
57
57
  }
58
-
59
- this.resizeObserver = new ResizeObserver(() => {
60
- this.updateResizableContainer();
61
- });
62
- this.resizeObserver.observe(this.scrollContainer);
63
-
64
- }
58
+ };
65
59
 
66
60
  public getTranslate(): { x: number; y: number } {
67
61
  return { x: this.translateX, y: this.translateY };
@@ -204,8 +198,7 @@ export class ResizableContainer {
204
198
  return this.scale;
205
199
  }
206
200
 
207
- public destroy(): void {
208
- this.resizeObserver?.disconnect();
201
+ public destroy(_box?: ReadonlyTeleBox): void {
209
202
  this.scrollBar?.destroy();
210
203
  }
211
204
  }
@@ -19,6 +19,7 @@ import { clamp } from "../utils/helpers";
19
19
  import { cachedGetBgColor } from "../utils/bgcolor";
20
20
  import { log, verbose, setRoomLogger } from "../utils/logger";
21
21
  import { getRoomTracker } from "../utils/tracker";
22
+ import { releaseAndRestoreSlide } from "./recovery";
22
23
  export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
23
24
 
24
25
  export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
@@ -315,6 +316,7 @@ export class SlideControllerBase {
315
316
  anchor,
316
317
  interactive: true,
317
318
  mode: "interactive",
319
+ syncEventQueuePolicy: options.syncEventQueuePolicy ?? "fifo",
318
320
  controller: false,
319
321
  enableGlobalClick: options.enableGlobalClick ?? true,
320
322
  renderOptions: {
@@ -342,6 +344,8 @@ export class SlideControllerBase {
342
344
  skipActionWhenFrozen: options.skipActionWhenFrozen ?? true,
343
345
  resourceMaxRetries: options.resourceMaxRetries,
344
346
  onResourceMaxRetries: options.onResourceMaxRetries,
347
+ // Slide 内部 ResizeObserver 关掉,改由 app-slide 的 observer + boxSizeChange 一起驱动
348
+ disableFrameResizeObserver: true,
345
349
  });
346
350
  if (import.meta.env.DEV) {
347
351
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -392,20 +396,20 @@ export class SlideControllerBase {
392
396
  if (!this.visible) return;
393
397
  this.isFrozen = false;
394
398
  if (this.ready) {
395
- let isNeedSyncState = false;
396
399
  log("[Slide] unfreeze", this.context.appId);
397
- if (this.invisibleBehavior === "frozen") {
398
- this.slide.release();
399
- isNeedSyncState = true;
400
- } else {
401
- this.slide.resume();
402
- }
403
- if (isNeedSyncState) {
404
- const state = this.context.storage.state.state;
405
- if (state) {
406
- log("[Slide] sync storage", JSON.stringify(state));
407
- this.slide.setSlideState(state);
400
+ try {
401
+ if (this.invisibleBehavior !== "frozen") {
402
+ this.slide.resume();
403
+ return;
408
404
  }
405
+
406
+ await releaseAndRestoreSlide(
407
+ this.slide,
408
+ () => this.context.storage.state.state,
409
+ state => log("[Slide] sync storage", JSON.stringify(state))
410
+ );
411
+ } catch (error) {
412
+ log("[Slide] unfreeze failed", error);
409
413
  }
410
414
  } else {
411
415
  this._toFreeze = -1;
@@ -0,0 +1,17 @@
1
+ export interface RecoverableSlide<State> {
2
+ release: (callback: () => void) => void;
3
+ setSlideState: (state: State) => Promise<void>;
4
+ }
5
+
6
+ export async function releaseAndRestoreSlide<State>(
7
+ slide: RecoverableSlide<State>,
8
+ getLatestState: () => State | null | undefined,
9
+ onRestore?: (state: State) => void
10
+ ): Promise<void> {
11
+ await new Promise<void>(resolve => slide.release(resolve));
12
+ const state = getLatestState();
13
+ if (state) {
14
+ onRestore?.(state);
15
+ await slide.setSlideState(state);
16
+ }
17
+ }
@@ -16,6 +16,24 @@ import { logger } from "../utils/logger";
16
16
  import { isEditable } from "../utils/helpers";
17
17
  import type { Attributes, MagixEvents } from "../typings";
18
18
  import type { AppOptions } from "..";
19
+ import type { SyncEventQueuePolicy } from "@netless/slide";
20
+ import { ResizeObserver as ResizeObserverPolyfill } from "@juggle/resize-observer";
21
+
22
+ type BoxSize = { width: number; height: number };
23
+
24
+ const ResizeObserverImpl = window.ResizeObserver || ResizeObserverPolyfill;
25
+ const BOX_SIZE_EPSILON = 0.5;
26
+
27
+ const formatBoxSize = ({ width, height }: BoxSize): string =>
28
+ `${width.toFixed(1)} x ${height.toFixed(1)} px`;
29
+
30
+ const sizesEqual = (left?: BoxSize, right?: BoxSize): boolean =>
31
+ Boolean(
32
+ left &&
33
+ right &&
34
+ Math.abs(left.width - right.width) <= BOX_SIZE_EPSILON &&
35
+ Math.abs(left.height - right.height) <= BOX_SIZE_EPSILON
36
+ );
19
37
 
20
38
  export const ClickThroughAppliances = new Set(["clicker"]);
21
39
 
@@ -60,7 +78,15 @@ export class SlideDocsViewer {
60
78
  protected readonly appId: string;
61
79
  protected isViewMounted = false;
62
80
  protected justSildeReadonly = false;
81
+ protected syncEventQueuePolicy: SyncEventQueuePolicy = "fifo";
63
82
  private enableScale: boolean;
83
+ private boxSizeEventCount = 0;
84
+ private latestBoxSize: BoxSize | undefined;
85
+ private lastObserverSize: BoxSize | undefined;
86
+ private lastAppliedSize: BoxSize | undefined;
87
+ private lastAppliedSource: "observer" | "event" | undefined;
88
+ private offBoxSizeChange: (() => void) | undefined;
89
+ private contentResizeObserver: ResizeObserver | undefined;
64
90
 
65
91
  public constructor({
66
92
  context,
@@ -112,6 +138,7 @@ export class SlideDocsViewer {
112
138
  });
113
139
 
114
140
  this.render();
141
+ this._registerBoxSizeChange();
115
142
 
116
143
  // 在 render() 之后设置监听器,确保 ResizableContainer 已经创建
117
144
  this.sideEffect.add(() => {
@@ -172,7 +199,19 @@ export class SlideDocsViewer {
172
199
 
173
200
  public setJustSildeReadonly(justSildeReadonly: boolean) {
174
201
  this.justSildeReadonly = justSildeReadonly;
175
- this.slideController?.slide.setInteractive(!this.justSildeReadonly);
202
+ this.applyInteractionState();
203
+ }
204
+
205
+ public setSyncEventQueuePolicy(policy: SyncEventQueuePolicy) {
206
+ this.syncEventQueuePolicy = policy;
207
+ this.applyInteractionState();
208
+ }
209
+
210
+ protected applyInteractionState() {
211
+ const slide = this.slideController?.slide;
212
+ if (!slide) return;
213
+ slide.setInteractive(!this.justSildeReadonly);
214
+ slide.setSyncEventQueuePolicy(this.syncEventQueuePolicy);
176
215
  }
177
216
 
178
217
  public render() {
@@ -181,7 +220,8 @@ export class SlideDocsViewer {
181
220
  this.resizableContainer = new ResizableContainer(
182
221
  this.viewer.$content,
183
222
  this.context,
184
- this.enableScale
223
+ this.enableScale,
224
+ this.box,
185
225
  );
186
226
  }
187
227
 
@@ -261,6 +301,7 @@ export class SlideDocsViewer {
261
301
  onNavigate: this.onNavigate,
262
302
  onError: this.onError,
263
303
  });
304
+ this.applyInteractionState();
264
305
 
265
306
  this.resizableContainer.setSlideObject(this.slideController.slide);
266
307
  this.scaleDocsToFit();
@@ -269,9 +310,99 @@ export class SlideDocsViewer {
269
310
  return () => this.whiteboardView.callbacks.off("onSizeUpdated", this.scaleDocsToFit);
270
311
  });
271
312
 
313
+ this._observeContentSize();
314
+ this._applyCurrentBoxSize();
315
+ this.context.getAppProxy?.()?.scheduleBoxSizeSync?.();
316
+
272
317
  return this;
273
318
  }
274
319
 
320
+ private _getContentSize(): BoxSize {
321
+ const rect = (this.viewer.$content || this.box.$content).getBoundingClientRect();
322
+ return { width: rect.width, height: rect.height };
323
+ }
324
+
325
+ private _applyResize = (size: BoxSize, source: "observer" | "event"): void => {
326
+ if (size.width <= 0 || size.height <= 0) return;
327
+ if (sizesEqual(this.lastAppliedSize, size)) return;
328
+
329
+ this.lastAppliedSize = size;
330
+ this.lastAppliedSource = source;
331
+ this.resizableContainer?.updateResizableContainer();
332
+ this.slideController?.slide?.notifyFrameResize();
333
+ this.scaleDocsToFit();
334
+
335
+ const domSize = this._getContentSize();
336
+ console.log("[app-slide] resize", {
337
+ appId: this.appId,
338
+ source,
339
+ eventCount: this.boxSizeEventCount,
340
+ event: formatBoxSize(size),
341
+ dom: formatBoxSize(domSize),
342
+ });
343
+ };
344
+
345
+ private _applyCurrentBoxSize = (eventSize?: BoxSize): void => {
346
+ const size = eventSize ?? this._getContentSize();
347
+ this._applyResize(size, eventSize ? "event" : "observer");
348
+ };
349
+
350
+ private _onObserverSize = (size: BoxSize): void => {
351
+ if (size.width <= 0 || size.height <= 0) return;
352
+ this.lastObserverSize = size;
353
+ // observer 先到:立刻 resize,不等 650ms 的 boxSizeChange
354
+ this._applyResize(size, "observer");
355
+ };
356
+
357
+ private _reconnectContentObserver(): void {
358
+ const target = this.viewer.$content;
359
+ if (!target || !this.contentResizeObserver) return;
360
+ this.contentResizeObserver.disconnect();
361
+ this.contentResizeObserver.observe(target);
362
+ }
363
+
364
+ private _observeContentSize(): void {
365
+ if (!this.contentResizeObserver) {
366
+ this.contentResizeObserver = new ResizeObserverImpl(entries => {
367
+ const entry = entries[entries.length - 1];
368
+ const size = entry?.contentRect
369
+ ? { width: entry.contentRect.width, height: entry.contentRect.height }
370
+ : this._getContentSize();
371
+ this._onObserverSize(size);
372
+ });
373
+ }
374
+ this._reconnectContentObserver();
375
+ }
376
+
377
+ private _onContextBoxSizeChange = (payload: { appId: string; width: number; height: number }) => {
378
+ if (payload.appId !== this.context.appId) return;
379
+
380
+ this.boxSizeEventCount += 1;
381
+ this.latestBoxSize = { width: payload.width, height: payload.height };
382
+
383
+ // observer 已经先处理过同一尺寸时,忽略晚到的 event
384
+ if (sizesEqual(this.lastObserverSize, this.latestBoxSize) || sizesEqual(this.lastAppliedSize, this.latestBoxSize)) {
385
+ console.log("[app-slide] boxSizeChange skipped, observer already applied", {
386
+ appId: this.appId,
387
+ eventCount: this.boxSizeEventCount,
388
+ source: this.lastAppliedSource,
389
+ size: formatBoxSize(this.latestBoxSize),
390
+ });
391
+ return;
392
+ }
393
+
394
+ // observer 比 event 晚到或漏了:用 event 补一次,并重新挂 observer
395
+ this._applyResize(this.latestBoxSize, "event");
396
+ this._reconnectContentObserver();
397
+ };
398
+
399
+ private _registerBoxSizeChange(): void {
400
+ this.offBoxSizeChange = this.context.emitter.on("boxSizeChange", this._onContextBoxSizeChange);
401
+ this._observeContentSize();
402
+ this._applyCurrentBoxSize();
403
+ this.context.getAppProxy?.()?.scheduleBoxSizeSync?.();
404
+ }
405
+
275
406
  protected onError = ({ error, index }: { error: Error; index: number }) => {
276
407
  this.viewer.setPaused();
277
408
  if (this.slideController?.showRenderError) {
@@ -322,12 +453,16 @@ export class SlideDocsViewer {
322
453
  }
323
454
 
324
455
  public unmount() {
456
+ this.offBoxSizeChange?.();
457
+ this.offBoxSizeChange = undefined;
458
+ this.contentResizeObserver?.disconnect();
459
+ this.contentResizeObserver = undefined;
325
460
  if (this.slideController) {
326
461
  this.slideController.destroy();
327
462
  this.slideController = null;
328
463
  }
329
464
  this.viewer.unmount();
330
- this.resizableContainer.destroy();
465
+ this.resizableContainer.destroy(this.box);
331
466
  return this;
332
467
  }
333
468
 
package/src/index.ts CHANGED
@@ -54,6 +54,7 @@ export interface AppOptions
54
54
  | "logger"
55
55
  | "enableGlobalClick"
56
56
  | "skipActionWhenFrozen"
57
+ | "syncEventQueuePolicy"
57
58
  > {
58
59
  /** show debug controller */
59
60
  debug?: boolean;
@@ -105,6 +106,7 @@ export interface AppResult {
105
106
  prevPage: () => boolean;
106
107
  jumpToPage: (page: number) => boolean;
107
108
  setSildeReadonly: (bol: boolean) => void;
109
+ setSyncEventQueuePolicy: (policy: NonNullable<ISlideConfig["syncEventQueuePolicy"]>) => void;
108
110
  onScaleChanged: (cb: (scale: number) => void) => void;
109
111
  scaleView: (to: number) => void;
110
112
  getViewScale: () => number | undefined;
@@ -213,9 +215,9 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
213
215
  },
214
216
  });
215
217
 
216
- if (context.getAppOptions()?.justSildeReadonly) {
217
- docsViewer?.setJustSildeReadonly(true);
218
- }
218
+ const appOptions = context.getAppOptions();
219
+ docsViewer.setSyncEventQueuePolicy(appOptions?.syncEventQueuePolicy ?? "fifo");
220
+ docsViewer.setJustSildeReadonly(appOptions?.justSildeReadonly ?? false);
219
221
 
220
222
  if (import.meta.env.DEV) {
221
223
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -318,6 +320,9 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
318
320
  setSildeReadonly: (bol: boolean) => {
319
321
  docsViewer?.setJustSildeReadonly(bol);
320
322
  },
323
+ setSyncEventQueuePolicy: policy => {
324
+ docsViewer?.setSyncEventQueuePolicy(policy);
325
+ },
321
326
  viewer: () => {
322
327
  return docsViewer;
323
328
  },
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2020 netless
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.