@netless/app-slide 0.2.84 → 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/dist/index.d.ts +49 -1
- package/dist/main.cjs.js +217 -177
- package/dist/main.cjs.js.map +1 -1
- package/dist/main.es.js +810 -326
- package/dist/main.es.js.map +1 -1
- package/dist/main.iife.js +238 -198
- package/dist/main.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/DocsViewer/ResizableContainer.ts +193 -0
- package/src/DocsViewer/ScrollBar.ts +327 -0
- package/src/SlideController/index.ts +5 -2
- package/src/SlideDocsViewer/index.ts +71 -4
- package/src/index.ts +59 -0
- package/src/typings.ts +3 -0
package/package.json
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|