@netless/forge-imagery-doc 0.1.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/build.mjs +34 -0
- package/dist/Cont.d.ts +1 -0
- package/dist/Cont.d.ts.map +1 -0
- package/dist/Container.d.ts +16 -0
- package/dist/Container.d.ts.map +1 -0
- package/dist/ContinuousContainer.d.ts +31 -0
- package/dist/ContinuousContainer.d.ts.map +1 -0
- package/dist/FooterView.d.ts +18 -0
- package/dist/FooterView.d.ts.map +1 -0
- package/dist/ImageryDoc.d.ts +57 -0
- package/dist/ImageryDoc.d.ts.map +1 -0
- package/dist/ImageryDocApplication.d.ts +38 -0
- package/dist/ImageryDocApplication.d.ts.map +1 -0
- package/dist/ImageryDocPermissions.d.ts +80 -0
- package/dist/ImageryDocPermissions.d.ts.map +1 -0
- package/dist/InfinityScroll.d.ts +15 -0
- package/dist/InfinityScroll.d.ts.map +1 -0
- package/dist/LazyImage.d.ts +11 -0
- package/dist/LazyImage.d.ts.map +1 -0
- package/dist/SideBarView.d.ts +4 -0
- package/dist/SideBarView.d.ts.map +1 -0
- package/dist/SingleContainer.d.ts +36 -0
- package/dist/SingleContainer.d.ts.map +1 -0
- package/dist/icons.d.ts +6 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/imagery-doc.esm.js +1031 -0
- package/dist/imagery-doc.esm.js.map +7 -0
- package/dist/imagery-doc.js +1064 -0
- package/dist/imagery-doc.js.map +7 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/infinityScrollor.d.ts +5 -0
- package/dist/infinityScrollor.d.ts.map +1 -0
- package/package.json +23 -0
- package/src/Container.ts +17 -0
- package/src/ContinuousContainer.ts +157 -0
- package/src/FooterView.ts +139 -0
- package/src/ImageryDoc.ts +64 -0
- package/src/ImageryDocApplication.ts +225 -0
- package/src/ImageryDocPermissions.ts +159 -0
- package/src/InfinityScroll.ts +55 -0
- package/src/LazyImage.ts +66 -0
- package/src/SingleContainer.ts +248 -0
- package/src/icons.ts +9 -0
- package/src/index.ts +5 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {AbstractApplication} from "@netless/forge-room";
|
|
2
|
+
import {Whiteboard, WhiteboardApplication, WhiteboardPermissionFlag} from "@netless/forge-whiteboard";
|
|
3
|
+
import * as Y from "yjs";
|
|
4
|
+
|
|
5
|
+
import {ImageryDoc} from "./ImageryDoc";
|
|
6
|
+
import {LazyImage} from "./LazyImage";
|
|
7
|
+
import {Container} from "./Container";
|
|
8
|
+
import {ImageryDocPermissionFlag, ImageryDocPermissions} from "./ImageryDocPermissions";
|
|
9
|
+
import {FooterView} from "./FooterView";
|
|
10
|
+
import {ContinuousContainer} from "./ContinuousContainer";
|
|
11
|
+
import {SingleContainer} from "./SingleContainer";
|
|
12
|
+
|
|
13
|
+
export interface ImageryDocOption {
|
|
14
|
+
images: {
|
|
15
|
+
src: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}[];
|
|
19
|
+
/**
|
|
20
|
+
* 从指定的白板 id 继承白板权限以及工具栏配置
|
|
21
|
+
*/
|
|
22
|
+
inheritWhiteboardId?: string;
|
|
23
|
+
displayMode: "continuous" | "single";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const IMAGERY_DOC_APP_NAME = "imagery_doc";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 需用户自行保证 ImageryDocApplication.view 的高宽比一致
|
|
30
|
+
*/
|
|
31
|
+
export class ImageryDocApplication extends AbstractApplication<ImageryDocOption, ImageryDoc> {
|
|
32
|
+
|
|
33
|
+
static applicationName = IMAGERY_DOC_APP_NAME;
|
|
34
|
+
|
|
35
|
+
public readonly name: string = IMAGERY_DOC_APP_NAME;
|
|
36
|
+
|
|
37
|
+
public emitter: ImageryDoc = new ImageryDoc();
|
|
38
|
+
|
|
39
|
+
private whiteboardApp!: WhiteboardApplication;
|
|
40
|
+
private whiteboard!: Whiteboard;
|
|
41
|
+
private container!: Container;
|
|
42
|
+
private rootView: HTMLDivElement = document.createElement("div");
|
|
43
|
+
private contentContainer: HTMLDivElement = document.createElement("div");
|
|
44
|
+
private whiteboardContainer: HTMLDivElement = document.createElement("div");
|
|
45
|
+
private footerContainer: HTMLDivElement = document.createElement("div");
|
|
46
|
+
private permissions!: ImageryDocPermissions;
|
|
47
|
+
private footer: FooterView;
|
|
48
|
+
private images: LazyImage[] = [];
|
|
49
|
+
|
|
50
|
+
public constructor() {
|
|
51
|
+
super();
|
|
52
|
+
this.rootView.setAttribute("data-forge-app", "imagery-doc");
|
|
53
|
+
this.rootView.style.background = "red";
|
|
54
|
+
this.rootView.style.overflow = "hidden";
|
|
55
|
+
|
|
56
|
+
this.contentContainer.style.width = "100%";
|
|
57
|
+
this.contentContainer.style.height = "100%";
|
|
58
|
+
this.contentContainer.style.display = "flex";
|
|
59
|
+
this.contentContainer.style.flexDirection = "column";
|
|
60
|
+
|
|
61
|
+
this.footer = new FooterView(this.emitter);
|
|
62
|
+
this.rootView.appendChild(this.contentContainer);
|
|
63
|
+
|
|
64
|
+
this.emitter.on("renderStart", pageIndex => {
|
|
65
|
+
this.footer.prevPageState(pageIndex !== 0);
|
|
66
|
+
this.footer.nextPageState(pageIndex !== this.images.length - 1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const that = this;
|
|
70
|
+
Object.defineProperty(this.emitter, "footView", {
|
|
71
|
+
get(): HTMLDivElement {
|
|
72
|
+
return that.footerContainer;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
Object.defineProperty(this.emitter, "view", {
|
|
76
|
+
get(): any {
|
|
77
|
+
return that.rootView;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(this.emitter, "permissions", {
|
|
81
|
+
get(): any {
|
|
82
|
+
return that.permissions;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(this.emitter, "pageIndex", {
|
|
86
|
+
get(): any {
|
|
87
|
+
return that.container.pageIndex;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(this.emitter, "pageCount", {
|
|
91
|
+
get(): any {
|
|
92
|
+
return that.images.length;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
Object.defineProperty(this.emitter, "goto", {
|
|
96
|
+
get(): any {
|
|
97
|
+
return (index: number) => {
|
|
98
|
+
if (that.permissions.hasPermission(ImageryDocPermissionFlag.switchPage)) {
|
|
99
|
+
that.container.goto(index);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(this.emitter, "imgContent", {
|
|
105
|
+
get(): any {
|
|
106
|
+
return (index: number) => {
|
|
107
|
+
return that.images[index].getImgContent();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
Object.defineProperty(this.emitter, "imgSize", {
|
|
112
|
+
get(): any {
|
|
113
|
+
return (index: number) => {
|
|
114
|
+
return {
|
|
115
|
+
width: that.images[index].width,
|
|
116
|
+
height: that.images[index].height
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public async initialize(option: ImageryDocOption): Promise<void> {
|
|
124
|
+
const whiteboardApp = new WhiteboardApplication();
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
whiteboardApp.roomDoc = this.roomDoc;
|
|
127
|
+
// @ts-ignore
|
|
128
|
+
whiteboardApp.appId = `${this.appId}_wb`;
|
|
129
|
+
// @ts-ignore
|
|
130
|
+
whiteboardApp.userId = this.userId;
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
whiteboardApp.userManager = this.userManager;
|
|
133
|
+
// @ts-ignore
|
|
134
|
+
whiteboardApp.applicationManager = this.applicationManager;
|
|
135
|
+
await whiteboardApp.initialize({
|
|
136
|
+
width: -1,
|
|
137
|
+
height: -1
|
|
138
|
+
});
|
|
139
|
+
this.whiteboardApp = whiteboardApp;
|
|
140
|
+
this.whiteboard = whiteboardApp.emitter;
|
|
141
|
+
this.whiteboard.enableCameraByMouse = false;
|
|
142
|
+
this.whiteboard.enableCameraByTouch = false;
|
|
143
|
+
this.whiteboard.view.style.width = "100%";
|
|
144
|
+
this.whiteboard.view.style.height = "100%";
|
|
145
|
+
this.whiteboard.view.style.position = "absolute";
|
|
146
|
+
this.whiteboard.view.style.top = "0";
|
|
147
|
+
this.whiteboard.view.style.left = "0";
|
|
148
|
+
this.whiteboard.setCanvasBackgroundColor("#f0f0f000");
|
|
149
|
+
|
|
150
|
+
if (option.displayMode === "single") {
|
|
151
|
+
this.container = new SingleContainer(
|
|
152
|
+
this.getMap("single-container"),
|
|
153
|
+
whiteboardApp,
|
|
154
|
+
this.whiteboardContainer,
|
|
155
|
+
this.emitter,
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
this.container = new ContinuousContainer(
|
|
159
|
+
this.getMap("continuous-container"),
|
|
160
|
+
whiteboardApp,
|
|
161
|
+
this.whiteboardContainer,
|
|
162
|
+
this.emitter,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.whiteboardContainer.style.position = "relative";
|
|
167
|
+
this.whiteboardContainer.style.flex = "1 1 auto";
|
|
168
|
+
this.whiteboardContainer.style.overflow = "hidden";
|
|
169
|
+
this.whiteboardContainer.appendChild(this.container.view);
|
|
170
|
+
this.whiteboardContainer.appendChild(this.whiteboard.view);
|
|
171
|
+
|
|
172
|
+
this.contentContainer.appendChild(this.whiteboardContainer);
|
|
173
|
+
this.contentContainer.appendChild(this.footerContainer);
|
|
174
|
+
this.footerContainer.appendChild(this.footer.root);
|
|
175
|
+
|
|
176
|
+
this.whiteboard.setViewModeToMain();
|
|
177
|
+
|
|
178
|
+
let i = 0;
|
|
179
|
+
this.whiteboard.permissions.addPermission(WhiteboardPermissionFlag.mainView);
|
|
180
|
+
while (i < option.images.length) {
|
|
181
|
+
const image = option.images[i];
|
|
182
|
+
this.images[i] = new LazyImage(image.src, image.width, image.height);
|
|
183
|
+
this.container.append(this.images[i]);
|
|
184
|
+
if (option.displayMode === "single") {
|
|
185
|
+
this.whiteboard.addPage(`doc_${i}`);
|
|
186
|
+
}
|
|
187
|
+
i += 1;
|
|
188
|
+
}
|
|
189
|
+
if (option.displayMode === "continuous") {
|
|
190
|
+
this.whiteboard.addPage("doc_continuous");
|
|
191
|
+
this.whiteboard.gotoPage("doc_continuous");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (option.displayMode === "single") {
|
|
195
|
+
this.whiteboard.setViewModeToMain();
|
|
196
|
+
} else {
|
|
197
|
+
this.whiteboard.setViewModeToFree();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.permissions = new ImageryDocPermissions(this.userManager, (userId: string) => {
|
|
201
|
+
return this.userMap(userId);
|
|
202
|
+
});
|
|
203
|
+
this.permissions.addPermission(ImageryDocPermissionFlag.all);
|
|
204
|
+
this.whiteboardApp.disableViewModel();
|
|
205
|
+
this.container.init();
|
|
206
|
+
|
|
207
|
+
this.whiteboard.permissions.removePermission(WhiteboardPermissionFlag.mainView);
|
|
208
|
+
if (option.inheritWhiteboardId) {
|
|
209
|
+
whiteboardApp.linkToWhiteboard(option.inheritWhiteboardId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Promise.resolve(undefined);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private userMap(userId: string): Y.Map<any> {
|
|
216
|
+
return this.getMap(`user/${userId}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public async dispose(): Promise<void> {
|
|
220
|
+
await this.whiteboardApp.dispose();
|
|
221
|
+
this.rootView.parentElement?.removeChild(this.rootView);
|
|
222
|
+
this.container.dispose();
|
|
223
|
+
return Promise.resolve(undefined);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { type RoomUser, type UserManager } from "@netless/forge-room";
|
|
2
|
+
import EventEmitter from "eventemitter3";
|
|
3
|
+
import * as Y from "yjs";
|
|
4
|
+
|
|
5
|
+
export enum ImageryDocPermissionFlag {
|
|
6
|
+
/**
|
|
7
|
+
* 没有任何权限, 只能同步他人的操作
|
|
8
|
+
*/
|
|
9
|
+
none = 0b0000000000000000,
|
|
10
|
+
/**
|
|
11
|
+
* 可以切换页面
|
|
12
|
+
*/
|
|
13
|
+
switchPage = 0b0000000000000001,
|
|
14
|
+
/**
|
|
15
|
+
* 可以操作相机, 包括缩放, 平移
|
|
16
|
+
*/
|
|
17
|
+
camera = 0b0000000000000010,
|
|
18
|
+
/**
|
|
19
|
+
* 可以操作侧边栏
|
|
20
|
+
*/
|
|
21
|
+
sideBar = 0b0000000000000100,
|
|
22
|
+
/**
|
|
23
|
+
* 拥有所有权限
|
|
24
|
+
*/
|
|
25
|
+
all = ImageryDocPermissionFlag.switchPage |
|
|
26
|
+
ImageryDocPermissionFlag.camera |
|
|
27
|
+
ImageryDocPermissionFlag.sideBar
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ImageryDocPermissionEvents {
|
|
31
|
+
/**
|
|
32
|
+
* 当用户权限变更时触发
|
|
33
|
+
* @param { string } userId 对应 userId
|
|
34
|
+
* @param { ImageryDocPermissionFlag[] } flags 当前权限列表
|
|
35
|
+
* @param { number } value 权限值
|
|
36
|
+
*/
|
|
37
|
+
change: (userId: string, flags: ImageryDocPermissionFlag[], value: number) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ImageryDocPermissions extends EventEmitter<ImageryDocPermissionEvents> {
|
|
41
|
+
|
|
42
|
+
private requestUserMap: (userId: string) => Y.Map<any>;
|
|
43
|
+
private userManager: UserManager;
|
|
44
|
+
private observers: Map<string, any> = new Map();
|
|
45
|
+
|
|
46
|
+
public constructor(
|
|
47
|
+
userManager: UserManager,
|
|
48
|
+
requestUserMap: (userId: string) => Y.Map<any>,
|
|
49
|
+
) {
|
|
50
|
+
super();
|
|
51
|
+
this.userManager = userManager;
|
|
52
|
+
this.requestUserMap = requestUserMap;
|
|
53
|
+
this.createModel(this.userManager.selfId);
|
|
54
|
+
this.userManager.userIdList().forEach((userId) => {
|
|
55
|
+
this.addObserve(userId);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.userManager.on("join", this.handleUserJoin);
|
|
59
|
+
this.userManager.on("leave", this.handleUserLeave);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private handleUserLeave = (user: RoomUser) => {
|
|
63
|
+
const cb = this.observers.get(user.id);
|
|
64
|
+
if (cb) {
|
|
65
|
+
this.requestUserMap(user.id).unobserve(cb);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
private handleUserJoin = (user: RoomUser) => {
|
|
70
|
+
this.addObserve(user.id);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
private addObserve(userId: string) {
|
|
74
|
+
const observer = (evt: Y.YMapEvent<any>) => {
|
|
75
|
+
this.handleUserPermissionChange(userId, evt);
|
|
76
|
+
};
|
|
77
|
+
this.observers.set(userId, observer);
|
|
78
|
+
this.requestUserMap(userId).observe(observer)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private createModel(userId: string) {
|
|
82
|
+
const userMap = this.requestUserMap(userId);
|
|
83
|
+
if (!userMap.has("permission")) {
|
|
84
|
+
userMap.set("permission", 0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private handleUserPermissionChange(userId: string, evt: Y.YMapEvent<any>) {
|
|
89
|
+
for (const [key, value] of evt.changes.keys.entries()) {
|
|
90
|
+
if (key === "permission") {
|
|
91
|
+
if (value.action === "add" || value.action === "update") {
|
|
92
|
+
const newValue = this.requestUserMap(userId).get("permission");
|
|
93
|
+
this.emit("change", userId, this.resolveFlags(newValue), newValue);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 解析权限列表组合
|
|
101
|
+
* @param {number} value - 权限数字值
|
|
102
|
+
* @return {WhiteboardPermissionFlag[]} - 权限列表
|
|
103
|
+
*/
|
|
104
|
+
public resolveFlags(value: number): ImageryDocPermissionFlag[] {
|
|
105
|
+
return [
|
|
106
|
+
ImageryDocPermissionFlag.switchPage,
|
|
107
|
+
ImageryDocPermissionFlag.sideBar,
|
|
108
|
+
ImageryDocPermissionFlag.camera
|
|
109
|
+
].filter(v => (v & value) !== 0)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取权限列表组合对应的数值
|
|
114
|
+
* @param { string } userId 不传表示获取自己
|
|
115
|
+
*/
|
|
116
|
+
public getPermissionValue(userId?: string): number {
|
|
117
|
+
return this.requestUserMap(userId ?? this.userManager.selfId).get("permission") ?? 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 获取权限列表
|
|
122
|
+
* @param {string=} userId 可选, 不传表示获取自己
|
|
123
|
+
*/
|
|
124
|
+
public getPermissionFlags(userId?: string): ImageryDocPermissionFlag[] {
|
|
125
|
+
const value = this.requestUserMap(userId ?? this.userManager.selfId).get("permission") ?? 0;
|
|
126
|
+
return this.resolveFlags(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 返回对应 userId 是否有相应权限
|
|
131
|
+
* @param {string=} userId 可选, 不传表示返回自己是否有相应权限
|
|
132
|
+
* @param {WhiteboardPermissionFlag} flag
|
|
133
|
+
*/
|
|
134
|
+
public hasPermission(flag: ImageryDocPermissionFlag, userId?: string): boolean {
|
|
135
|
+
return ((this.requestUserMap(userId ?? this.userManager.selfId).get("permission") ?? 0) & flag) !== 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 添加权限
|
|
140
|
+
* @param {WhiteboardPermissionFlag} flag 权限标记
|
|
141
|
+
* @param {string=} userId 可选, 为 userId 添加权限, 不传表示为自己添加权限
|
|
142
|
+
*/
|
|
143
|
+
public addPermission(flag: ImageryDocPermissionFlag, userId?: string) {
|
|
144
|
+
const userMap = this.requestUserMap(userId ?? this.userManager.selfId);
|
|
145
|
+
const oldValue = userMap.get("permission") ?? 0;
|
|
146
|
+
this.requestUserMap(userId ?? this.userManager.selfId).set("permission", oldValue | flag);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 移除权限
|
|
151
|
+
* @param {WhiteboardPermissionFlag} flag 权限标记
|
|
152
|
+
* @param {string=} userId 可选, 为 userId 移除权限, 不传表示为自己移除权限
|
|
153
|
+
*/
|
|
154
|
+
public removePermission(flag: ImageryDocPermissionFlag, userId?: string) {
|
|
155
|
+
const userMap = this.requestUserMap(userId ?? this.userManager.selfId);
|
|
156
|
+
const oldValue = userMap.get("permission") ?? 0;
|
|
157
|
+
this.requestUserMap(userId ?? this.userManager.selfId).set("permission", oldValue & (~flag));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
|
|
3
|
+
interface InfinityScrollEvents {
|
|
4
|
+
scale: (value: number) => void;
|
|
5
|
+
translate: (x: number, y: number) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class InfinityScroll extends EventEmitter<InfinityScrollEvents> {
|
|
9
|
+
|
|
10
|
+
public readonly view: HTMLDivElement;
|
|
11
|
+
private lastDelta: {x: number, y: number} = { x: 0, y: 0 };
|
|
12
|
+
private lastTriggerTime: number = 0;
|
|
13
|
+
|
|
14
|
+
public constructor(view: HTMLDivElement) {
|
|
15
|
+
super();
|
|
16
|
+
this.view = view;
|
|
17
|
+
this.view.addEventListener("wheel", this.handleWheel, { passive: false, capture: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private handleWheel = (evt: WheelEvent) => {
|
|
21
|
+
evt.preventDefault();
|
|
22
|
+
evt.stopImmediatePropagation();
|
|
23
|
+
evt.stopPropagation();
|
|
24
|
+
|
|
25
|
+
if (Date.now() - this.lastTriggerTime < 32) {
|
|
26
|
+
this.lastDelta.y += evt.deltaY;
|
|
27
|
+
this.lastDelta.x += evt.deltaX;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (evt.ctrlKey) { // 双指捏合
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
const wheelDelta = Math.sign((evt.wheelDelta || 100)) * 120;
|
|
33
|
+
const deltaY = evt.deltaY + this.lastDelta.y;
|
|
34
|
+
let scale: number;
|
|
35
|
+
if (deltaY > 0) {
|
|
36
|
+
scale = Math.max(1 - Math.abs(deltaY / wheelDelta), 0.00000001)
|
|
37
|
+
} else { // 放大
|
|
38
|
+
scale = 1 + Math.abs(deltaY / wheelDelta)
|
|
39
|
+
}
|
|
40
|
+
scale = Math.max(0.9, scale);
|
|
41
|
+
scale = Math.min(1.1, scale);
|
|
42
|
+
this.emit("scale", scale);
|
|
43
|
+
} else { // 拖动
|
|
44
|
+
const deltaX = this.lastDelta.x + evt.deltaX;
|
|
45
|
+
const deltaY = this.lastDelta.y + evt.deltaY;
|
|
46
|
+
this.emit("translate", deltaX, deltaY);
|
|
47
|
+
}
|
|
48
|
+
this.lastTriggerTime = Date.now();
|
|
49
|
+
this.lastDelta = { x: 0, y: 0 };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
public dispose() {
|
|
53
|
+
this.view.removeEventListener("wheel", this.handleWheel);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/LazyImage.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { kvStore } from "@netless/forge-room";
|
|
2
|
+
|
|
3
|
+
export class LazyImage {
|
|
4
|
+
|
|
5
|
+
public readonly view: HTMLDivElement;
|
|
6
|
+
public readonly width: number;
|
|
7
|
+
public readonly height: number;
|
|
8
|
+
private src: string;
|
|
9
|
+
|
|
10
|
+
public constructor(src: string, width: number, height: number) {
|
|
11
|
+
this.src = src;
|
|
12
|
+
this.width = width;
|
|
13
|
+
this.height = height;
|
|
14
|
+
this.view = document.createElement("div");
|
|
15
|
+
this.view.setAttribute("data-forge-src", src);
|
|
16
|
+
this.view.style.width = `${width}px`;
|
|
17
|
+
this.view.style.height = `${height}px`;
|
|
18
|
+
this.view.style.transformOrigin = "0 0";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public scale(scale: number) {
|
|
22
|
+
// this.localScale = scale;
|
|
23
|
+
this.view.style.width = `${this.width * scale}px`;
|
|
24
|
+
this.view.style.height = `${this.height * scale}px`;
|
|
25
|
+
// this.img.style.width = "100%";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async getImgContent(): Promise<string> {
|
|
29
|
+
let base64Data: string | null = null;
|
|
30
|
+
try {
|
|
31
|
+
base64Data = await kvStore.getItem(this.src);
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
if (base64Data) {
|
|
36
|
+
return base64Data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const response = await fetch(this.src);
|
|
40
|
+
const blob = await response.blob();
|
|
41
|
+
return new Promise<string>((resolve, reject) => {
|
|
42
|
+
const reader = new FileReader();
|
|
43
|
+
reader.onload = (e) => {
|
|
44
|
+
kvStore.setItem(this.src, reader.result as string);
|
|
45
|
+
resolve(reader.result as string);
|
|
46
|
+
};
|
|
47
|
+
reader.onerror = () => {
|
|
48
|
+
reject(reader.error);
|
|
49
|
+
};
|
|
50
|
+
reader.readAsDataURL(blob);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async prepare(): Promise<void> {
|
|
55
|
+
if (this.view.children.length > 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const base64Data: string = await this.getImgContent();
|
|
59
|
+
const img = document.createElement("img");
|
|
60
|
+
img.src = base64Data;
|
|
61
|
+
img.style.width = "100%";
|
|
62
|
+
if (this.view.children.length === 0) {
|
|
63
|
+
this.view.appendChild(img);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|