Browse Source

feat: update whiteboard

wxyyxc1992 5 years ago
parent
commit
40c92270a2

example/index.less → example/drawboard-fullscreen/index.ts View File


+ 0
- 0
example/drawboard/index.ts View File


+ 20
- 12
example/index.ts View File

1
-import { eventHub } from './../src/event/EventHub';
2
-import { WhitePage } from './../src/board/WhitePage/index';
1
+import { EventHub } from './../src/event/EventHub';
2
+import { Whiteboard } from './../src/board/Whiteboard/index';
3
 
3
 
4
-const page1 = new WhitePage(document.getElementById('image1') as HTMLImageElement, { eventHub });
4
+const eventHub = new EventHub();
5
 
5
 
6
-page1.drawboard.show((dataUrl: string) => {
7
-  const res = document.getElementById('resultImage') as HTMLImageElement;
8
-  res.src = dataUrl;
9
-});
6
+eventHub.on('sync', (changeEv: SyncEvent) => {});
7
+
8
+const images = [
9
+  'https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
10
+  'http://upload-images.jianshu.io/upload_images/1647496-d281090a702045e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
11
+  'http://upload-images.jianshu.io/upload_images/1647496-611a416be07d7ca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'
12
+];
10
 
13
 
11
-const page2 = new WhitePage(document.getElementById('image2') as HTMLImageElement, {
12
-  mode: 'mirror',
14
+const whiteboard = new Whiteboard(document.getElementById('root') as HTMLDivElement, {
15
+  sources: images,
13
   eventHub
16
   eventHub
14
 });
17
 });
15
 
18
 
16
-page2.drawboard.show((dataUrl: string) => {
17
-  const res = document.getElementById('resultImage') as HTMLImageElement;
18
-  res.src = dataUrl;
19
+whiteboard.open();
20
+
21
+const mirrorWhiteboard = new Whiteboard(document.getElementById('root-mirror') as HTMLDivElement, {
22
+  sources: images,
23
+  eventHub,
24
+  mode: 'mirror'
19
 });
25
 });
26
+
27
+mirrorWhiteboard.open();

+ 0
- 0
example/mirror-whiteboard/index.ts View File


+ 0
- 0
example/mirror/index.less View File


+ 32
- 0
example/mirror/index.ts View File

1
+import { EventHub } from './../../src/event/EventHub';
2
+import { WhitePage } from '../../src/board/WhitePage/index';
3
+import { SyncEvent } from '../../src/event/Event';
4
+
5
+const eventHub = new EventHub();
6
+
7
+eventHub.on('sync', (changeEv: SyncEvent) => {
8
+  console.log(changeEv);
9
+});
10
+
11
+const page1 = new WhitePage(
12
+  { imgEle: document.getElementById('image1') as HTMLImageElement },
13
+  { eventHub }
14
+);
15
+
16
+page1.drawboard.open((dataUrl: string) => {
17
+  const res = document.getElementById('resultImage') as HTMLImageElement;
18
+  res.src = dataUrl;
19
+});
20
+
21
+const page2 = new WhitePage(
22
+  { imgEle: document.getElementById('image2') as HTMLImageElement },
23
+  {
24
+    mode: 'mirror',
25
+    eventHub
26
+  }
27
+);
28
+
29
+page2.drawboard.open((dataUrl: string) => {
30
+  const res = document.getElementById('resultImage') as HTMLImageElement;
31
+  res.src = dataUrl;
32
+});

+ 13
- 0
example/whiteboard/index.ts View File

1
+import { Whiteboard } from '../../src/board/Whiteboard/index';
2
+
3
+const images = [
4
+  'https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
5
+  'http://upload-images.jianshu.io/upload_images/1647496-d281090a702045e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
6
+  'http://upload-images.jianshu.io/upload_images/1647496-611a416be07d7ca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'
7
+];
8
+
9
+const whiteboard = new Whiteboard(document.getElementById('root') as HTMLDivElement, {
10
+  sources: images
11
+});
12
+
13
+whiteboard.open();

+ 1
- 0
src/assets/bx-left-arrow.svg View File

1
+<svg t="1553418887330" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2264" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M787.797333 90.197333a42.581333 42.581333 0 0 0-44.074666 2.688l-554.666667 384a42.794667 42.794667 0 0 0 0 70.186667l554.666667 384A42.666667 42.666667 0 0 0 810.666667 896V128a42.666667 42.666667 0 0 0-22.869334-37.802667zM725.333333 814.549333L288.298667 512 725.333333 209.450667v605.098666z" p-id="2265"></path></svg>

+ 1
- 0
src/assets/bx-right-arrow.svg View File

1
+<svg t="1553418900235" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2709" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M236.202667 933.802667a42.837333 42.837333 0 0 0 44.074666-2.730667l554.666667-384a42.666667 42.666667 0 0 0 0-70.144l-554.666667-384A42.581333 42.581333 0 0 0 213.333333 128v768a42.666667 42.666667 0 0 0 22.869334 37.802667zM298.666667 209.450667L735.701333 512 298.666667 814.549333V209.450667z" p-id="2710"></path></svg>

+ 0
- 0
src/board/Baseboard/index.less View File


+ 68
- 0
src/board/Baseboard/index.ts View File

1
+import { uuid } from './../../utils/uuid';
2
+import { WhitePageSource } from './../types';
3
+import { SvgHelper } from './../../renderer/SvgHelper/index';
4
+
5
+/** 基础的绘制版 */
6
+export class Baseboard {
7
+  id: string = uuid();
8
+
9
+  /** 元素 */
10
+  source: WhitePageSource;
11
+
12
+  // 目前使用 Image 元素作为输出源
13
+  target: HTMLImageElement;
14
+  targetRect: ClientRect;
15
+
16
+  boardCanvas: SVGSVGElement;
17
+  boardHolder: HTMLDivElement;
18
+  defs: SVGDefsElement;
19
+
20
+  width: number;
21
+  height: number;
22
+
23
+  constructor(source: WhitePageSource) {
24
+    this.source = source;
25
+
26
+    if (source.imgEle) {
27
+      this.target = source.imgEle!;
28
+
29
+      // 如果仅传入图片地址或者 Blob,则必须为全屏模式
30
+      this.width = this.target.clientWidth;
31
+      this.height = this.target.clientHeight;
32
+    }
33
+  }
34
+
35
+  protected initBoard = () => {
36
+    // init holder
37
+    this.boardHolder = document.createElement('div');
38
+    this.boardHolder.id = `fcw-board-holder-${this.id}`;
39
+    // fix for Edge's touch behavior
40
+    this.boardHolder.style.setProperty('touch-action', 'none');
41
+    this.boardHolder.style.setProperty('-ms-touch-action', 'none');
42
+    document.body.appendChild(this.boardHolder);
43
+
44
+    // init canvas
45
+    this.boardCanvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
46
+    this.boardCanvas.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
47
+    this.boardCanvas.setAttribute('width', this.width.toString());
48
+    this.boardCanvas.setAttribute('height', this.height.toString());
49
+    this.boardCanvas.setAttribute(
50
+      'viewBox',
51
+      '0 0 ' + this.width.toString() + ' ' + this.height.toString()
52
+    );
53
+
54
+    this.boardHolder.style.position = 'absolute';
55
+    this.boardHolder.style.width = `${this.width}px`;
56
+    this.boardHolder.style.height = `${this.height}px`;
57
+    this.boardHolder.style.transformOrigin = 'top left';
58
+    this.positionBoard();
59
+    this.defs = SvgHelper.createDefs();
60
+    this.boardCanvas.appendChild(this.defs);
61
+    this.boardHolder.appendChild(this.boardCanvas);
62
+  };
63
+
64
+  protected positionBoard = () => {
65
+    this.boardHolder.style.top = this.targetRect.top + 'px';
66
+    this.boardHolder.style.left = this.targetRect.left + 'px';
67
+  };
68
+}

+ 95
- 157
src/board/Drawboard/index.ts View File

1
+import { WhitePageSource } from './../types';
2
+import { Baseboard } from './../Baseboard/index';
1
 import { BaseMarker } from './../../markers/BaseMarker/index';
3
 import { BaseMarker } from './../../markers/BaseMarker/index';
2
-import { lineMarkerToolbarItem } from './../../toolbar/toolbar-items';
4
+import { getToolbars } from './../../toolbar/toolbar-items';
3
 import { WhitePage } from './../WhitePage/index';
5
 import { WhitePage } from './../WhitePage/index';
4
-import { onChangeFunc } from './../../event/Event';
5
-import { uuid } from './../../utils/uuid';
6
-import { SvgHelper } from '../../renderer/SvgHelper';
6
+import { onSyncFunc } from './../../event/Event';
7
+
7
 import { Synthetizer } from '../../renderer/Synthetizer';
8
 import { Synthetizer } from '../../renderer/Synthetizer';
8
 import { Toolbar } from '../../toolbar/Toolbar';
9
 import { Toolbar } from '../../toolbar/Toolbar';
9
 import { ToolbarItem } from '../../toolbar/ToolbarItem';
10
 import { ToolbarItem } from '../../toolbar/ToolbarItem';
10
 
11
 
11
 import './index.less';
12
 import './index.less';
12
-import {
13
-  arrowMarkerToolbarItem,
14
-  highlightMarkerToolbarItem,
15
-  textMarkerToolbarItem,
16
-  coverMarkerToolbarItem,
17
-  rectMarkerToolbarItem
18
-} from '../../toolbar/toolbar-items';
19
-
20
-const OkIcon = require('../../assets/check.svg');
21
-const DeleteIcon = require('../../assets/eraser.svg');
22
-const PointerIcon = require('../../assets/mouse-pointer.svg');
23
-const CloseIcon = require('../../assets/times.svg');
24
-
25
-export class Drawboard {
26
-  id: string = uuid();
13
+
14
+export class Drawboard extends Baseboard {
15
+  /** Options */
16
+  private scale = 1.0;
27
 
17
 
28
   /** 句柄 */
18
   /** 句柄 */
29
   page: WhitePage;
19
   page: WhitePage;
30
 
20
 
31
-  private target: HTMLImageElement;
32
-  private markerImage: SVGSVGElement;
33
-  private markerImageHolder: HTMLDivElement;
34
-  private defs: SVGDefsElement;
35
-
36
-  private targetRect: ClientRect;
37
-  private width: number;
38
-  private height: number;
39
-
40
   private markers: BaseMarker[];
21
   private markers: BaseMarker[];
41
   get markerMap(): { [key: string]: BaseMarker } {
22
   get markerMap(): { [key: string]: BaseMarker } {
42
     const map = {};
23
     const map = {};
48
   private activeMarker: BaseMarker | null;
29
   private activeMarker: BaseMarker | null;
49
 
30
 
50
   private toolbar: Toolbar;
31
   private toolbar: Toolbar;
32
+  private toolbars: ToolbarItem[];
51
   private toolbarUI: HTMLElement;
33
   private toolbarUI: HTMLElement;
52
 
34
 
53
-  private completeCallback: (dataUrl: string) => void;
54
-  private cancelCallback: () => void;
55
-  private onChange: onChangeFunc = () => {};
56
-
57
-  private toolbars: ToolbarItem[] = [
58
-    {
59
-      icon: PointerIcon,
60
-      name: 'pointer',
61
-      tooltipText: 'Pointer'
62
-    },
63
-    {
64
-      icon: DeleteIcon,
65
-      name: 'delete',
66
-      tooltipText: 'Delete'
67
-    },
68
-    {
69
-      name: 'separator',
70
-      tooltipText: ''
71
-    },
72
-    rectMarkerToolbarItem,
73
-    coverMarkerToolbarItem,
74
-    highlightMarkerToolbarItem,
75
-    lineMarkerToolbarItem,
76
-    arrowMarkerToolbarItem,
77
-    textMarkerToolbarItem,
78
-    {
79
-      name: 'separator',
80
-      tooltipText: ''
81
-    },
82
-    {
83
-      icon: OkIcon,
84
-      name: 'ok',
85
-      tooltipText: 'OK'
86
-    },
87
-    {
88
-      icon: CloseIcon,
89
-      name: 'close',
90
-      tooltipText: 'Close'
91
-    }
92
-  ];
93
-
94
-  private scale = 1.0;
35
+  /** 回调 */
36
+  private onComplete: (dataUrl: string) => void = () => {};
37
+  private onChange: onSyncFunc = () => {};
38
+  private onCancel: () => void;
95
 
39
 
96
   constructor(
40
   constructor(
97
-    target: HTMLImageElement,
98
-    { page, onChange }: { page?: WhitePage; onChange?: onChangeFunc } = {}
41
+    source: WhitePageSource,
42
+    { page, onChange }: { page?: WhitePage; onChange?: onSyncFunc } = {}
99
   ) {
43
   ) {
44
+    super(source);
45
+
100
     if (page) {
46
     if (page) {
101
       this.page = page;
47
       this.page = page;
102
     }
48
     }
103
 
49
 
104
-    this.target = target;
105
-
106
-    // 如果仅传入图片地址或者 Blob,则必须为全屏模式
107
-    this.width = target.clientWidth;
108
-    this.height = target.clientHeight;
109
-
110
     this.markers = [];
50
     this.markers = [];
111
     this.activeMarker = null;
51
     this.activeMarker = null;
52
+    this.toolbars = getToolbars(page);
112
 
53
 
113
     if (onChange) {
54
     if (onChange) {
114
       this.onChange = onChange;
55
       this.onChange = onChange;
115
     }
56
     }
116
   }
57
   }
117
 
58
 
118
-  public show = (completeCallback: (dataUrl: string) => void, cancelCallback?: () => void) => {
119
-    this.completeCallback = completeCallback;
120
-
121
-    if (cancelCallback) {
122
-      this.cancelCallback = cancelCallback;
59
+  /** @region LifeCycle open - hide - show - ... - close */
60
+  /** 打开画板 */
61
+  public open = (onComplete?: (dataUrl: string) => void, onCancel?: () => void) => {
62
+    if (onComplete) {
63
+      this.onComplete = onComplete;
123
     }
64
     }
124
 
65
 
125
-    this.open();
126
-
127
-    if (this.page.mode !== 'mirror') {
128
-      this.showUI();
66
+    if (onCancel) {
67
+      this.onCancel = onCancel;
129
     }
68
     }
130
-  };
131
 
69
 
132
-  public open = () => {
133
     this.setTargetRect();
70
     this.setTargetRect();
134
 
71
 
135
-    this.initMarkerCanvas();
72
+    this.initBoard();
136
     this.attachEvents();
73
     this.attachEvents();
137
     this.setStyles();
74
     this.setStyles();
138
 
75
 
139
     window.addEventListener('resize', this.adjustUI);
76
     window.addEventListener('resize', this.adjustUI);
77
+
78
+    if (this.page.mode === 'master') {
79
+      this.showUI();
80
+    }
140
   };
81
   };
141
 
82
 
142
-  public render = (completeCallback: (dataUrl: string) => void, cancelCallback?: () => void) => {
143
-    this.completeCallback = completeCallback;
83
+  public hide = () => {
84
+    if (this.source.imgSrc) {
85
+      this.target.style.display = 'none';
86
+    }
87
+    // 这里不使用 display:none,是为了保证在隐藏时候仍然可以执行更新
88
+    this.boardHolder.style.opacity = '0';
89
+    this.boardHolder.style.zIndex = '-1';
90
+
91
+    if (this.toolbar) {
92
+      this.toolbar.hide();
93
+    }
94
+  };
144
 
95
 
145
-    if (cancelCallback) {
146
-      this.cancelCallback = cancelCallback;
96
+  public show = () => {
97
+    if (this.source.imgSrc) {
98
+      this.target.style.display = 'block';
147
     }
99
     }
148
 
100
 
149
-    this.selectMarker(null);
150
-    this.startRender(this.renderFinished);
101
+    this.boardHolder.style.opacity = '1';
102
+    this.boardHolder.style.zIndex = '9999';
103
+
104
+    if (this.toolbar) {
105
+      this.toolbar.show();
106
+    }
151
   };
107
   };
152
 
108
 
153
   public close = () => {
109
   public close = () => {
154
     if (this.toolbarUI) {
110
     if (this.toolbarUI) {
155
       document.body.removeChild(this.toolbarUI);
111
       document.body.removeChild(this.toolbarUI);
156
     }
112
     }
157
-    if (this.markerImage) {
158
-      document.body.removeChild(this.markerImageHolder);
113
+    if (this.boardCanvas) {
114
+      document.body.removeChild(this.boardHolder);
159
     }
115
     }
160
   };
116
   };
161
 
117
 
162
-  public addMarker = (markerType: typeof BaseMarker, { id }: { id?: string } = {}) => {
163
-    const marker = markerType.createMarker();
118
+  public render = (onComplete: (dataUrl: string) => void, onCancel?: () => void) => {
119
+    this.onComplete = onComplete;
164
 
120
 
165
-    // 假如 Drawboard 存在 Page 引用,则传导给 Marker
166
-    if (this.page) {
167
-      marker.page = this.page;
121
+    if (onCancel) {
122
+      this.onCancel = onCancel;
168
     }
123
     }
169
 
124
 
125
+    this.selectMarker(null);
126
+    this.startRender(this.renderFinished);
127
+  };
128
+
129
+  public addMarker = (markerType: typeof BaseMarker, { id }: { id?: string } = {}) => {
130
+    // 假如 Drawboard 存在 Page 引用,则传导给 Marker
131
+    const marker = markerType.createMarker(this.page);
132
+
170
     if (id) {
133
     if (id) {
171
       marker.id = id;
134
       marker.id = id;
172
     }
135
     }
176
 
139
 
177
     if (marker.defs && marker.defs.length > 0) {
140
     if (marker.defs && marker.defs.length > 0) {
178
       for (const d of marker.defs) {
141
       for (const d of marker.defs) {
179
-        if (d.id && !this.markerImage.getElementById(d.id)) {
142
+        if (d.id && !this.boardCanvas.getElementById(d.id)) {
180
           this.defs.appendChild(d);
143
           this.defs.appendChild(d);
181
         }
144
         }
182
       }
145
       }
184
 
147
 
185
     // 触发事件流
148
     // 触发事件流
186
     this.onChange({
149
     this.onChange({
187
-      target: 'drawboard',
188
-      id: this.id,
150
+      target: 'marker',
151
+      parentId: this.page ? this.page.id : this.id,
189
       event: 'add',
152
       event: 'add',
190
       data: { type: marker.type, id: marker.id }
153
       data: { type: marker.type, id: marker.id }
191
     });
154
     });
194
 
157
 
195
     this.selectMarker(marker);
158
     this.selectMarker(marker);
196
 
159
 
197
-    this.markerImage.appendChild(marker.visual);
160
+    this.boardCanvas.appendChild(marker.visual);
198
 
161
 
199
     const bbox = marker.visual.getBBox();
162
     const bbox = marker.visual.getBBox();
200
     const x = this.width / 2 / this.scale - bbox.width / 2;
163
     const x = this.width / 2 / this.scale - bbox.width / 2;
207
 
170
 
208
   public deleteActiveMarker = () => {
171
   public deleteActiveMarker = () => {
209
     if (this.activeMarker) {
172
     if (this.activeMarker) {
173
+      // 触发事件
174
+      if (this.onChange) {
175
+        this.onChange({
176
+          event: 'remove',
177
+          id: this.activeMarker.id,
178
+          target: 'marker',
179
+          data: { id: this.activeMarker.id }
180
+        });
181
+      }
210
       this.deleteMarker(this.activeMarker);
182
       this.deleteMarker(this.activeMarker);
211
     }
183
     }
212
   };
184
   };
222
 
194
 
223
   private startRender = (done: (dataUrl: string) => void) => {
195
   private startRender = (done: (dataUrl: string) => void) => {
224
     const renderer = new Synthetizer();
196
     const renderer = new Synthetizer();
225
-    renderer.rasterize(this.target, this.markerImage, done);
197
+    renderer.rasterize(this.target, this.boardCanvas, done);
226
   };
198
   };
227
 
199
 
228
   private attachEvents = () => {
200
   private attachEvents = () => {
229
-    this.markerImage.addEventListener('mousedown', this.mouseDown);
230
-    this.markerImage.addEventListener('mousemove', this.mouseMove);
231
-    this.markerImage.addEventListener('mouseup', this.mouseUp);
201
+    this.boardCanvas.addEventListener('mousedown', this.mouseDown);
202
+    this.boardCanvas.addEventListener('mousemove', this.mouseMove);
203
+    this.boardCanvas.addEventListener('mouseup', this.mouseUp);
232
   };
204
   };
233
 
205
 
234
   private mouseDown = (ev: MouseEvent) => {
206
   private mouseDown = (ev: MouseEvent) => {
252
     }
224
     }
253
   };
225
   };
254
 
226
 
255
-  private initMarkerCanvas = () => {
256
-    this.markerImageHolder = document.createElement('div');
257
-    // fix for Edge's touch behavior
258
-    this.markerImageHolder.style.setProperty('touch-action', 'none');
259
-    this.markerImageHolder.style.setProperty('-ms-touch-action', 'none');
260
-
261
-    this.markerImage = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
262
-    this.markerImage.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
263
-    this.markerImage.setAttribute('width', this.width.toString());
264
-    this.markerImage.setAttribute('height', this.height.toString());
265
-    this.markerImage.setAttribute(
266
-      'viewBox',
267
-      '0 0 ' + this.width.toString() + ' ' + this.height.toString()
268
-    );
269
-
270
-    this.markerImageHolder.style.position = 'absolute';
271
-    this.markerImageHolder.style.width = `${this.width}px`;
272
-    this.markerImageHolder.style.height = `${this.height}px`;
273
-    this.markerImageHolder.style.transformOrigin = 'top left';
274
-    this.positionMarkerImage();
275
-
276
-    this.defs = SvgHelper.createDefs();
277
-    this.markerImage.appendChild(this.defs);
278
-
279
-    this.markerImageHolder.appendChild(this.markerImage);
280
-
281
-    document.body.appendChild(this.markerImageHolder);
282
-  };
283
-
284
   private adjustUI = (ev: UIEvent) => {
227
   private adjustUI = (ev: UIEvent) => {
285
     this.adjustSize();
228
     this.adjustSize();
286
     this.positionUI();
229
     this.positionUI();
290
     this.width = this.target.clientWidth;
233
     this.width = this.target.clientWidth;
291
     this.height = this.target.clientHeight;
234
     this.height = this.target.clientHeight;
292
 
235
 
293
-    const scale = this.target.clientWidth / this.markerImageHolder.clientWidth;
236
+    const scale = this.target.clientWidth / this.boardHolder.clientWidth;
294
     if (scale !== 1.0) {
237
     if (scale !== 1.0) {
295
       this.scale *= scale;
238
       this.scale *= scale;
296
-      this.markerImageHolder.style.width = `${this.width}px`;
297
-      this.markerImageHolder.style.height = `${this.height}px`;
239
+      this.boardHolder.style.width = `${this.width}px`;
240
+      this.boardHolder.style.height = `${this.height}px`;
298
 
241
 
299
-      this.markerImageHolder.style.transform = `scale(${this.scale})`;
242
+      this.boardHolder.style.transform = `scale(${this.scale})`;
300
     }
243
     }
301
   };
244
   };
302
 
245
 
303
   private positionUI = () => {
246
   private positionUI = () => {
304
     this.setTargetRect();
247
     this.setTargetRect();
305
-    this.positionMarkerImage();
248
+    this.positionBoard();
306
     this.positionToolbar();
249
     this.positionToolbar();
307
   };
250
   };
308
 
251
 
309
-  private positionMarkerImage = () => {
310
-    this.markerImageHolder.style.top = this.targetRect.top + 'px';
311
-    this.markerImageHolder.style.left = this.targetRect.left + 'px';
312
-  };
313
-
314
   private positionToolbar = () => {
252
   private positionToolbar = () => {
315
     this.toolbarUI.style.left = `${this.targetRect.left +
253
     this.toolbarUI.style.left = `${this.targetRect.left +
316
       this.target.offsetWidth -
254
       this.target.offsetWidth -
378
             }
316
             }
379
         `;
317
         `;
380
 
318
 
381
-    this.markerImage.appendChild(editorStyleSheet);
319
+    this.boardCanvas.appendChild(editorStyleSheet);
382
   };
320
   };
383
 
321
 
384
   private toolbarClick = (ev: MouseEvent, toolbarItem: ToolbarItem) => {
322
   private toolbarClick = (ev: MouseEvent, toolbarItem: ToolbarItem) => {
418
     this.activeMarker = marker;
356
     this.activeMarker = marker;
419
   };
357
   };
420
 
358
 
421
-  private deleteMarker = (marker: BaseMarker) => {
422
-    this.markerImage.removeChild(marker.visual);
359
+  public deleteMarker = (marker: BaseMarker) => {
360
+    this.boardCanvas.removeChild(marker.visual);
423
     if (this.activeMarker === marker) {
361
     if (this.activeMarker === marker) {
424
       this.activeMarker = null;
362
       this.activeMarker = null;
425
     }
363
     }
433
 
371
 
434
   private cancel = () => {
372
   private cancel = () => {
435
     this.close();
373
     this.close();
436
-    if (this.cancelCallback) {
437
-      this.cancelCallback();
374
+    if (this.onCancel) {
375
+      this.onCancel();
438
     }
376
     }
439
   };
377
   };
440
 
378
 
441
   private renderFinished = (dataUrl: string) => {
379
   private renderFinished = (dataUrl: string) => {
442
-    this.completeCallback(dataUrl);
380
+    this.onComplete(dataUrl);
443
   };
381
   };
444
 
382
 
445
   private renderFinishedClose = (dataUrl: string) => {
383
   private renderFinishedClose = (dataUrl: string) => {
446
     this.close();
384
     this.close();
447
-    this.completeCallback(dataUrl);
385
+    this.onComplete(dataUrl);
448
   };
386
   };
449
 }
387
 }

+ 6
- 0
src/board/WhitePage/index.less View File

1
+.fcw-page {
2
+  img {
3
+    width: 100%;
4
+    height: 100%;
5
+  }
6
+}

+ 162
- 28
src/board/WhitePage/index.ts View File

1
+import { TextMarker } from './../../markers/TextMarker/index';
1
 import { MarkerType } from './../../markers/types';
2
 import { MarkerType } from './../../markers/types';
2
-import { ChangeEvent } from './../../event/Event';
3
+import { SyncEvent } from './../../event/Event';
3
 import { EventHub } from '../../event/EventHub';
4
 import { EventHub } from '../../event/EventHub';
4
-import { WhiteboardMode } from './../types';
5
+import { WhiteboardMode, WhitePageSource } from './../types';
5
 import { Drawboard } from './../Drawboard/index';
6
 import { Drawboard } from './../Drawboard/index';
6
 import { uuid } from './../../utils/uuid';
7
 import { uuid } from './../../utils/uuid';
7
 import { getMarkerByType } from '../../markers/types';
8
 import { getMarkerByType } from '../../markers/types';
8
 
9
 
10
+import './index.less';
11
+import { createDivWithClassName } from 'fc-whiteboard/src/utils/dom';
12
+
13
+const prefix = 'fcw-page';
14
+
9
 /** 白板中的每一页 */
15
 /** 白板中的每一页 */
10
 export class WhitePage {
16
 export class WhitePage {
11
   id: string = uuid();
17
   id: string = uuid();
12
 
18
 
13
-  drawboard: Drawboard;
19
+  source: WhitePageSource;
20
+  target: HTMLImageElement;
21
+
22
+  /** UI Options */
23
+  container: HTMLDivElement;
24
+  // 父容器指针
25
+  parentContainer?: HTMLDivElement;
14
   mode: WhiteboardMode = 'master';
26
   mode: WhiteboardMode = 'master';
15
 
27
 
28
+  /** Handlers */
29
+  drawboard: Drawboard;
30
+  eventHub?: EventHub;
31
+
16
   constructor(
32
   constructor(
17
-    target: HTMLImageElement,
18
-    { mode, eventHub }: { mode?: WhiteboardMode; eventHub?: EventHub } = {}
33
+    source: WhitePageSource,
34
+    {
35
+      mode,
36
+      eventHub,
37
+      parentContainer
38
+    }: { mode?: WhiteboardMode; eventHub?: EventHub; parentContainer?: HTMLDivElement } = {}
19
   ) {
39
   ) {
20
     if (mode) {
40
     if (mode) {
21
       this.mode = mode;
41
       this.mode = mode;
22
     }
42
     }
43
+    this.eventHub = eventHub;
44
+    this.parentContainer = parentContainer;
45
+
46
+    this.initSource(source);
23
 
47
 
24
     if (this.mode === 'master') {
48
     if (this.mode === 'master') {
25
-      if (eventHub) {
26
-        this.drawboard = new Drawboard(target, {
27
-          page: this,
28
-          onChange: ev => eventHub.emit('change', ev)
29
-        });
30
-      } else {
31
-        this.drawboard = new Drawboard(target, { page: this });
32
-      }
49
+      this.initMaster();
33
     }
50
     }
34
 
51
 
35
     if (this.mode === 'mirror') {
52
     if (this.mode === 'mirror') {
36
-      if (!eventHub) {
37
-        throw new Error('Invalid eventHub');
38
-      }
53
+      this.initMirror();
54
+    }
55
+  }
56
+
57
+  /** LifeCycle open - close */
58
+  public open() {
59
+    this.drawboard.open();
60
+  }
61
+
62
+  public hide() {
63
+    this.drawboard.hide();
64
+  }
65
+
66
+  public show() {
67
+    this.drawboard.show();
68
+  }
69
+
70
+  public close() {
71
+    this.drawboard.close();
72
+  }
73
+
74
+  /** 初始化源 */
75
+  private initSource(source: WhitePageSource) {
76
+    // 判断 Source 的类型是否符合要求
77
+    if (typeof source.imgSrc === 'string' && !this.parentContainer) {
78
+      throw new Error('Invalid source, If you set image url, you must also set parentContainer');
79
+    }
80
+
81
+    this.source = source;
82
+
83
+    // 如果传入的 imgEle,则直接使用
84
+    if (source.imgEle) {
85
+      this.target = source.imgEle!;
86
+    }
87
+
88
+    // 如果是图片,则需要创建 Image 元素
89
+    if (typeof source.imgSrc === 'string') {
90
+      this.container = createDivWithClassName(prefix, this.parentContainer!);
91
+      this.container.id = this.id;
92
+
93
+      this.target = document.createElement('img');
94
+      this.target.src = source.imgSrc;
95
+      this.target.alt = 'Siema image';
96
+
97
+      this.container.appendChild(this.target);
98
+    }
99
+  }
100
+
101
+  /** 以 Master 模式启动 */
102
+  protected initMaster() {
103
+    if (this.eventHub) {
104
+      // 对于 WhitePage 中加载的 Drawboard,必须是传入自身可控的 Image 元素
105
+      this.drawboard = new Drawboard(
106
+        { imgEle: this.target },
107
+        {
108
+          page: this,
109
+          onChange: ev => this.eventHub!.emit('sync', ev)
110
+        }
111
+      );
112
+    } else {
113
+      this.drawboard = new Drawboard({ imgEle: this.target }, { page: this });
114
+    }
115
+  }
39
 
116
 
40
-      this.drawboard = new Drawboard(target, { page: this });
117
+  /** 以 Mirror 模式启动 */
118
+  protected initMirror() {
119
+    if (!this.eventHub) {
120
+      throw new Error('Invalid eventHub');
121
+    }
122
+
123
+    this.drawboard = new Drawboard({ imgEle: this.target }, { page: this });
41
 
124
 
42
-      eventHub.on('change', (ev: ChangeEvent) => {
43
-        if (ev.event === 'add') {
44
-          const data: { id: string; type: MarkerType } = ev.data as {
45
-            id: string;
46
-            type: MarkerType;
47
-          };
48
-          this.drawboard.addMarker(getMarkerByType(data.type), { id: data.id });
125
+    this.eventHub.on('sync', (ev: SyncEvent) => {
126
+      try {
127
+        // 判断是否为 WhitePage 的同步
128
+        if (ev.target === 'page' && ev.id === this.id) {
129
+          this.onPageSync();
49
         }
130
         }
50
 
131
 
51
-        if (ev.event === 'move' || ev.event === 'resize') {
52
-          const marker = this.drawboard.markerMap[ev.id];
53
-          marker.reactToManipulation(ev.event, ev.data as any);
132
+        // 处理 Marker 的同步事件
133
+        if (ev.target === 'marker') {
134
+          this.onMarkerSync(ev);
54
         }
135
         }
55
-      });
136
+      } catch (e) {
137
+        console.warn(e);
138
+      }
139
+    });
140
+  }
141
+
142
+  /** 处理 Page 的同步事件 */
143
+  private onPageSync() {}
144
+
145
+  /** 处理 Marker 的同步事件 */
146
+  private onMarkerSync(ev: SyncEvent) {
147
+    if (ev.event === 'add' && ev.parentId === this.id) {
148
+      const data: { id: string; type: MarkerType } = ev.data as {
149
+        id: string;
150
+        type: MarkerType;
151
+      };
152
+
153
+      // 这里判断该 Marker 是否已经添加过;如果已经存在则忽略
154
+      const marker = this.drawboard.markerMap[data.id];
155
+      if (!marker) {
156
+        this.drawboard.addMarker(getMarkerByType(data.type), { id: data.id });
157
+      }
158
+    }
159
+
160
+    // 其余的情况,不存在 id 则直接返回空
161
+    if (!ev.id) {
162
+      return;
163
+    }
164
+
165
+    if (ev.event === 'remove') {
166
+      const data: { id: string } = ev.data as {
167
+        id: string;
168
+      };
169
+
170
+      const marker = this.drawboard.markerMap[data.id];
171
+      if (marker) {
172
+        this.drawboard.deleteMarker(marker);
173
+      }
174
+    }
175
+
176
+    if (ev.event === 'move' || ev.event === 'resize') {
177
+      const marker = this.drawboard.markerMap[ev.id];
178
+
179
+      if (marker) {
180
+        marker.reactToManipulation(ev.event, ev.data as any);
181
+      }
182
+    }
183
+
184
+    // 响应文本变化事件
185
+    if (ev.event === 'changeText') {
186
+      const marker = this.drawboard.markerMap[ev.id] as TextMarker;
187
+      if (marker) {
188
+        marker.setText(ev.data as string);
189
+      }
56
     }
190
     }
57
   }
191
   }
58
 }
192
 }

+ 39
- 0
src/board/Whiteboard/index.less View File

1
+.fcw-board {
2
+  // 设置需要元素的绝对布局
3
+  &-imgs,
4
+  &-pages,
5
+  .fcw-page {
6
+    position: absolute;
7
+    top: 0;
8
+    left: 0;
9
+    width: 100%;
10
+    height: 100%;
11
+  }
12
+
13
+  &-img {
14
+    width: 100%;
15
+    height: 100%;
16
+  }
17
+
18
+  &-flip-arrow {
19
+    height: 20px;
20
+    width: 20px;
21
+
22
+    cursor: pointer;
23
+  }
24
+
25
+  &-controller {
26
+    position: absolute;
27
+    display: flex;
28
+    width: 50px;
29
+    height: 30px;
30
+    justify-content: space-between;
31
+    align-items: center;
32
+    top: -30px;
33
+    background-color: #cccccc;
34
+    padding: 0px 5px;
35
+    margin: 0px;
36
+    border-top-left-radius: 10px;
37
+    border-top-right-radius: 10px;
38
+  }
39
+}

+ 311
- 6
src/board/Whiteboard/index.ts View File

1
-import { WhiteboardMode, WhitePageSource } from '../types';
1
+import { SyncEvent } from './../../event/Event';
2
+import * as Siema from 'siema';
3
+
4
+import { WhiteboardMode } from '../types';
5
+import { WhitePage } from '../WhitePage/index';
6
+import { EventHub } from '../../event/EventHub';
7
+import { uuid } from '../../utils/uuid';
8
+import { addClassName, createDivWithClassName } from '../../utils/dom';
9
+
10
+import './index.less';
11
+
12
+const LeftArrowIcon = require('../../assets/bx-left-arrow.svg');
13
+const RightArrowIcon = require('../../assets/bx-right-arrow.svg');
14
+
15
+const prefix = 'fcw-board';
16
+
17
+export class SerializableWhiteboard {
18
+  id: string;
19
+  sources: string[];
20
+  pageIds: string[];
21
+  visiblePageIndex: number;
22
+}
2
 
23
 
3
 export class Whiteboard {
24
 export class Whiteboard {
4
-  mode: WhiteboardMode = 'master';
5
-  sources: WhitePageSource[] = [];
25
+  id: string = uuid();
26
+  sources: string[] = [];
6
 
27
 
7
-  /** @region UI Options */
28
+  /** 元素 */
29
+  // 如果传入的是图片地址,则需要挂载到该 Target 元素下
30
+  target: HTMLDivElement;
31
+  imgsContainer: HTMLDivElement;
32
+  pagesContainer: HTMLDivElement;
33
+
34
+  /** UI Options */
35
+  // 事件中心
36
+  eventHub?: EventHub;
37
+  // 编辑模式
38
+  mode: WhiteboardMode = 'master';
8
   // 是否为全屏模式
39
   // 是否为全屏模式
9
   isFullscreen: boolean = false;
40
   isFullscreen: boolean = false;
10
 
41
 
11
-  constructor() {}
42
+  /** 句柄 */
43
+  pages: WhitePage[] = [];
44
+  siema: any;
45
+
46
+  /** State | 内部状态 */
47
+  // 是否被初始化过,如果尚未被初始化,则等待来自于 Master 的同步消息
48
+  isInitialized: boolean = false;
49
+  visiblePageIndex: number = 0;
50
+  emitInterval: any;
51
+
52
+  constructor(
53
+    target: HTMLDivElement,
54
+    {
55
+      sources,
56
+      eventHub,
57
+      mode,
58
+      visiblePageIndex
59
+    }: {
60
+      sources?: string[];
61
+      eventHub?: EventHub;
62
+      mode?: WhiteboardMode;
63
+      visiblePageIndex?: number;
64
+    } = {}
65
+  ) {
66
+    if (target) {
67
+      this.target = target;
68
+    } else {
69
+      this.target = document.createElement('div');
70
+      document.body.appendChild(this.target);
71
+    }
72
+
73
+    if (!this.target.id) {
74
+      this.target.id = this.id;
75
+    }
76
+
77
+    addClassName(this.target, prefix);
78
+
79
+    if (sources) {
80
+      this.sources = sources;
81
+    }
82
+
83
+    this.eventHub = eventHub;
84
+
85
+    if (mode) {
86
+      this.mode = mode;
87
+    }
88
+
89
+    // set inner state
90
+    if (typeof visiblePageIndex !== 'undefined') {
91
+      this.visiblePageIndex = visiblePageIndex;
92
+    }
93
+
94
+    this.init();
95
+  }
96
+
97
+  /** LifeCycle */
98
+  public open() {
99
+    // 依次渲染所有的页,隐藏非当前页之外的其他页
100
+    this.pages.forEach((page, i) => {
101
+      page.open();
102
+
103
+      if (i !== this.visiblePageIndex) {
104
+        page.hide();
105
+      }
106
+    });
107
+  }
108
+
109
+  /** 关闭当前的 Whiteboard */
110
+  public close() {
111
+    if (this.emitInterval) {
112
+      clearInterval(this.emitInterval);
113
+    }
114
+  }
115
+
116
+  /** 获取当前快照 */
117
+  public snap(): SerializableWhiteboard {
118
+    return {
119
+      id: this.id,
120
+      sources: this.sources,
121
+      pageIds: this.pages.map(page => page.id),
122
+      visiblePageIndex: this.visiblePageIndex
123
+    };
124
+  }
12
 
125
 
13
   /** 初始化操作 */
126
   /** 初始化操作 */
14
-  init() {}
127
+  private init() {
128
+    // 为 target 添加子 imgs 容器
129
+    this.imgsContainer = createDivWithClassName(`${prefix}-imgs`, this.target);
130
+    // 为 target 添加子 pages 容器
131
+    this.pagesContainer = createDivWithClassName(`${prefix}-pages`, this.target);
132
+
133
+    if (this.mode === 'master') {
134
+      this.initMaster();
135
+
136
+      this.emitSnapshot();
137
+    }
138
+
139
+    if (this.mode === 'mirror') {
140
+      this.initMirror();
141
+    }
142
+  }
143
+
144
+  /** 以主模式启动 */
145
+  private initMaster() {
146
+    // 初始化所有的 WhitePages
147
+    this.sources.forEach(source => {
148
+      const page = new WhitePage(
149
+        { imgSrc: source },
150
+        {
151
+          mode: this.mode,
152
+          eventHub: this.eventHub,
153
+          parentContainer: this.pagesContainer
154
+        }
155
+      );
156
+
157
+      // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
158
+      page.container.style.opacity = '0';
159
+
160
+      this.pages.push(page);
161
+    });
162
+
163
+    this.initSiema();
164
+
165
+    // 初始化控制节点
166
+    const controller = createDivWithClassName(`${prefix}-controller`, this.target);
167
+
168
+    const prevEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
169
+    prevEle.innerHTML = LeftArrowIcon;
170
+
171
+    const nextEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
172
+    nextEle.innerHTML = RightArrowIcon;
173
+
174
+    nextEle!.addEventListener('click', () => {
175
+      const nextPageIndex =
176
+        this.visiblePageIndex + 1 > this.pages.length - 1 ? 0 : this.visiblePageIndex + 1;
177
+      this.onPageChange(nextPageIndex);
178
+    });
179
+    prevEle!.addEventListener('click', () => {
180
+      const nextPageIndex =
181
+        this.visiblePageIndex - 1 < 0 ? this.pages.length - 1 : this.visiblePageIndex - 1;
182
+
183
+      this.onPageChange(nextPageIndex);
184
+    });
185
+  }
186
+
187
+  /** 以镜像模式启动 */
188
+  private initMirror() {
189
+    if (!this.eventHub) {
190
+      throw new Error('Invalid eventHub');
191
+    }
192
+
193
+    this.eventHub.on('sync', (ev: SyncEvent) => {
194
+      if (ev.target !== 'whiteboard') {
195
+        return;
196
+      }
197
+
198
+      if (ev.event === 'snap') {
199
+        // 如果已经初始化完毕,则直接跳过
200
+        if (this.isInitialized) {
201
+          return;
202
+        }
203
+
204
+        this.onSnapshot(ev.data as SerializableWhiteboard);
205
+      }
206
+
207
+      if (ev.event === 'changeIndex' && ev.id === this.id) {
208
+        if (this.isInitialized) {
209
+          this.onPageChange(ev.data as number);
210
+        }
211
+      }
212
+    });
213
+  }
214
+
215
+  /** 初始化 Siema */
216
+  private initSiema() {
217
+    // 初始化所有的占位图片,用于给 Siema 播放使用
218
+    this.sources.forEach(source => {
219
+      const imgEle = document.createElement('img');
220
+      addClassName(imgEle, `${prefix}-img`);
221
+      imgEle.src = source;
222
+      imgEle.alt = 'Siema image';
223
+
224
+      this.imgsContainer.appendChild(imgEle);
225
+    });
226
+
227
+    // 初始化 Siema,并且添加控制节点
228
+    this.siema = new Siema({
229
+      selector: this.imgsContainer,
230
+      duration: 200,
231
+      easing: 'ease-out',
232
+      perPage: 1,
233
+      startIndex: 0,
234
+      draggable: false,
235
+      multipleDrag: true,
236
+      threshold: 20,
237
+      loop: false,
238
+      rtl: false
239
+    });
240
+  }
241
+
242
+  /** 响应页面切换的事件 */
243
+  private onPageChange(nextPageIndex: number) {
244
+    this.siema.goTo(nextPageIndex);
245
+    this.visiblePageIndex = nextPageIndex;
246
+
247
+    // 将所有的 Page 隐藏
248
+    this.pages.forEach((page, i) => {
249
+      if (nextPageIndex === i) {
250
+        page.show();
251
+      } else {
252
+        page.hide();
253
+      }
254
+    });
255
+
256
+    if (this.mode === 'master' && this.eventHub) {
257
+      this.eventHub.emit('sync', {
258
+        event: 'changeIndex',
259
+        id: this.id,
260
+        target: 'whiteboard',
261
+        data: nextPageIndex
262
+      });
263
+    }
264
+  }
265
+
266
+  private emitSnapshot() {
267
+    const innerFunc = () => {
268
+      if (this.eventHub) {
269
+        this.eventHub.emit('sync', {
270
+          event: 'snap',
271
+          id: this.id,
272
+          target: 'whiteboard',
273
+          data: this.snap()
274
+        });
275
+      }
276
+    };
277
+
278
+    // 定期触发事件
279
+    this.emitInterval = setInterval(() => {
280
+      innerFunc();
281
+    }, 5 * 1000);
282
+
283
+    // 首次事件,延时 500ms 发出
284
+    setTimeout(innerFunc, 500);
285
+  }
286
+
287
+  /** 响应获取到的快照事件 */
288
+  private onSnapshot(snap: SerializableWhiteboard) {
289
+    const { id, sources, pageIds, visiblePageIndex } = snap;
290
+
291
+    if (!this.isInitialized) {
292
+      this.id = id;
293
+      this.sources = sources;
294
+      this.initSiema();
295
+
296
+      // 初始化所有的 WhitePages
297
+      this.sources.forEach((source, i) => {
298
+        const page = new WhitePage(
299
+          { imgSrc: source },
300
+          {
301
+            mode: this.mode,
302
+            eventHub: this.eventHub,
303
+            parentContainer: this.pagesContainer
304
+          }
305
+        );
306
+        page.id = pageIds[i];
307
+
308
+        // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
309
+        page.container.style.opacity = '0';
310
+
311
+        this.pages.push(page);
312
+
313
+        page.open();
314
+      });
315
+    }
316
+
317
+    this.isInitialized = true;
318
+    this.onPageChange(visiblePageIndex);
319
+  }
15
 }
320
 }

+ 6
- 2
src/board/types.ts View File

1
 // 是主动绘制模式,还是镜像模式
1
 // 是主动绘制模式,还是镜像模式
2
 export type WhiteboardMode = 'master' | 'mirror';
2
 export type WhiteboardMode = 'master' | 'mirror';
3
 
3
 
4
-export type RenderCallback = (container: HTMLElement) => void;
4
+export type WhitePageSource = {
5
+  // 需要展示的图片元素
6
+  imgEle?: HTMLImageElement;
5
 
7
 
6
-export type WhitePageSource = string | RenderCallback;
8
+  // 需要展示的图片地址
9
+  imgSrc?: string;
10
+};

+ 21
- 6
src/event/Event.ts View File

1
-export type TargetType = 'page' | 'drawboard' | 'marker';
2
-export type EventType = 'add' | 'resize' | 'move' | 'remove';
1
+export type TargetType = 'whiteboard' | 'page' | 'marker';
2
+export type EventType =
3
+  // 完全的状态同步,FCW 支持两种状态的同步交换:Snapshot(Snap) 与 KeyActions(KA) 方式
4
+  | 'snap'
5
+  // 添加
6
+  | 'add'
7
+  // 尺寸重置
8
+  | 'resize'
9
+  // 移动
10
+  | 'move'
11
+  // 移除
12
+  | 'remove'
13
+  // 下标改变
14
+  | 'changeIndex'
15
+  // 文本改变
16
+  | 'changeText';
3
 export type PositionType =
17
 export type PositionType =
4
   | 'left'
18
   | 'left'
5
   | 'right'
19
   | 'right'
12
   | 'topCenter'
26
   | 'topCenter'
13
   | 'bottomCenter';
27
   | 'bottomCenter';
14
 
28
 
15
-export interface ChangeEvent {
29
+export interface SyncEvent {
16
   target: TargetType;
30
   target: TargetType;
17
-  id: string;
31
+  id?: string;
32
+  parentId?: string;
18
   event: EventType;
33
   event: EventType;
19
-  data?: object | string;
34
+  data?: object | string | number;
20
 }
35
 }
21
 
36
 
22
-export type onChangeFunc = (ev: ChangeEvent) => void;
37
+export type onSyncFunc = (ev: SyncEvent) => void;

+ 1
- 8
src/event/EventHub.ts View File

1
-import { ChangeEvent } from './Event';
2
 import * as EventEmitter from 'eventemitter3';
1
 import * as EventEmitter from 'eventemitter3';
3
 
2
 
4
-export class EventHub extends EventEmitter<'change'> {}
5
-
6
-export const eventHub = new EventHub();
7
-
8
-eventHub.on('change', (changeEv: ChangeEvent) => {
9
-  console.log(changeEv);
10
-});
3
+export class EventHub extends EventEmitter<'sync'> {}

+ 3
- 1
src/markers/ArrowMarker/index.ts View File

1
 import { MarkerType } from './../types';
1
 import { MarkerType } from './../types';
2
 import { LinearMarker } from '../LinearMarker';
2
 import { LinearMarker } from '../LinearMarker';
3
 import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
3
 import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4
 
5
 
5
 export class ArrowMarker extends LinearMarker {
6
 export class ArrowMarker extends LinearMarker {
6
   type: MarkerType = 'arrow';
7
   type: MarkerType = 'arrow';
7
 
8
 
8
-  public static createMarker = (): LinearMarker => {
9
+  public static createMarker = (page?: WhitePage): LinearMarker => {
9
     const marker = new ArrowMarker();
10
     const marker = new ArrowMarker();
11
+    marker.page = page;
10
     marker.setup();
12
     marker.setup();
11
     return marker;
13
     return marker;
12
   };
14
   };

+ 12
- 6
src/markers/BaseMarker/index.ts View File

1
 import { WhitePage } from './../../board/WhitePage/index';
1
 import { WhitePage } from './../../board/WhitePage/index';
2
 import { PositionType } from 'fc-whiteboard/src/event/Event';
2
 import { PositionType } from 'fc-whiteboard/src/event/Event';
3
-import { onChangeFunc, EventType } from '../../event/Event';
3
+import { onSyncFunc, EventType } from '../../event/Event';
4
 import { MarkerType } from '../types';
4
 import { MarkerType } from '../types';
5
 import * as uuid from 'uuid/v1';
5
 import * as uuid from 'uuid/v1';
6
 import { SvgHelper } from '../../renderer/SvgHelper';
6
 import { SvgHelper } from '../../renderer/SvgHelper';
9
   id: string = uuid();
9
   id: string = uuid();
10
   type: MarkerType = 'base';
10
   type: MarkerType = 'base';
11
   // 归属的
11
   // 归属的
12
-  page: WhitePage;
12
+  page?: WhitePage;
13
   // Marker 的属性发生变化后的回调
13
   // Marker 的属性发生变化后的回调
14
-  onChange: onChangeFunc = () => {};
14
+  onChange: onSyncFunc = () => {};
15
 
15
 
16
-  public static createMarker = (): BaseMarker => {
16
+  public static createMarker = (page?: WhitePage): BaseMarker => {
17
     const marker = new BaseMarker();
17
     const marker = new BaseMarker();
18
+    marker.page = page;
18
     marker.setup();
19
     marker.setup();
19
     return marker;
20
     return marker;
20
   };
21
   };
30
   protected height: number = 50;
31
   protected height: number = 50;
31
 
32
 
32
   protected isActive: boolean = true;
33
   protected isActive: boolean = true;
34
+  protected isDragging: boolean = false;
33
   protected isResizing: boolean = false;
35
   protected isResizing: boolean = false;
34
 
36
 
35
   protected previousMouseX: number = 0;
37
   protected previousMouseX: number = 0;
36
   protected previousMouseY: number = 0;
38
   protected previousMouseY: number = 0;
37
 
39
 
38
-  private isDragging: boolean = false;
39
-
40
   public reactToManipulation(
40
   public reactToManipulation(
41
     type: EventType,
41
     type: EventType,
42
     { dx, dy, pos }: { dx: number; dy: number; pos: PositionType }
42
     { dx, dy, pos }: { dx: number; dy: number; pos: PositionType }
50
     }
50
     }
51
   }
51
   }
52
 
52
 
53
+  /** 响应元素视图状态变化 */
53
   public manipulate = (ev: MouseEvent) => {
54
   public manipulate = (ev: MouseEvent) => {
54
     const scale = this.visual.getScreenCTM()!.a;
55
     const scale = this.visual.getScreenCTM()!.a;
55
     const dx = (ev.screenX - this.previousMouseX) / scale;
56
     const dx = (ev.screenX - this.previousMouseX) / scale;
165
 
166
 
166
   private mouseDown = (ev: MouseEvent) => {
167
   private mouseDown = (ev: MouseEvent) => {
167
     ev.stopPropagation();
168
     ev.stopPropagation();
169
+
170
+    if (this.page && this.page.mode === 'mirror') {
171
+      return;
172
+    }
173
+
168
     this.select();
174
     this.select();
169
     this.isDragging = true;
175
     this.isDragging = true;
170
     this.previousMouseX = ev.screenX;
176
     this.previousMouseX = ev.screenX;

+ 3
- 1
src/markers/CoverMarker/index.ts View File

1
 import { MarkerType } from './../types';
1
 import { MarkerType } from './../types';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4
 
5
 
5
 export class CoverMarker extends RectBaseMarker {
6
 export class CoverMarker extends RectBaseMarker {
6
   type: MarkerType = 'cover';
7
   type: MarkerType = 'cover';
7
 
8
 
8
-  public static createMarker = (): RectBaseMarker => {
9
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
9
     const marker = new CoverMarker();
10
     const marker = new CoverMarker();
11
+    marker.page = page;
10
     marker.setup();
12
     marker.setup();
11
     return marker;
13
     return marker;
12
   };
14
   };

+ 3
- 1
src/markers/HighlightMarker/index.ts View File

1
 import { MarkerType } from './../types';
1
 import { MarkerType } from './../types';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4
 
5
 
5
 export class HighlightMarker extends RectBaseMarker {
6
 export class HighlightMarker extends RectBaseMarker {
6
   type: MarkerType = 'highlight';
7
   type: MarkerType = 'highlight';
7
 
8
 
8
-  public static createMarker = (): RectBaseMarker => {
9
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
9
     const marker = new HighlightMarker();
10
     const marker = new HighlightMarker();
11
+    marker.page = page;
10
     marker.setup();
12
     marker.setup();
11
     return marker;
13
     return marker;
12
   };
14
   };

+ 3
- 1
src/markers/LineMarker/index.ts View File

1
 import { MarkerType } from './../types';
1
 import { MarkerType } from './../types';
2
 import { LinearMarker } from '../LinearMarker';
2
 import { LinearMarker } from '../LinearMarker';
3
 import { SvgHelper } from './../../renderer/SvgHelper/index';
3
 import { SvgHelper } from './../../renderer/SvgHelper/index';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4
 
5
 
5
 export class LineMarker extends LinearMarker {
6
 export class LineMarker extends LinearMarker {
6
   type: MarkerType = 'line';
7
   type: MarkerType = 'line';
7
 
8
 
8
-  public static createMarker = (): LinearMarker => {
9
+  public static createMarker = (page?: WhitePage): LinearMarker => {
9
     const marker = new LineMarker();
10
     const marker = new LineMarker();
11
+    marker.page = page;
10
     marker.setup();
12
     marker.setup();
11
     return marker;
13
     return marker;
12
   };
14
   };

+ 7
- 1
src/markers/LinearMarker/index.ts View File

1
+import { WhitePage } from './../../board/WhitePage/index';
1
 import { BaseMarker } from '../BaseMarker';
2
 import { BaseMarker } from '../BaseMarker';
2
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
3
 import { SvgHelper } from '../../renderer/SvgHelper';
4
 import { SvgHelper } from '../../renderer/SvgHelper';
4
 import { PositionType } from 'fc-whiteboard/src/event/Event';
5
 import { PositionType } from 'fc-whiteboard/src/event/Event';
5
 
6
 
6
 export class LinearMarker extends BaseMarker {
7
 export class LinearMarker extends BaseMarker {
7
-  public static createMarker = (): LinearMarker => {
8
+  public static createMarker = (page?: WhitePage): LinearMarker => {
8
     const marker = new LinearMarker();
9
     const marker = new LinearMarker();
10
+    marker.page = page;
9
     marker.setup();
11
     marker.setup();
10
     return marker;
12
     return marker;
11
   };
13
   };
54
     this.addToRenderVisual(this.markerLine);
56
     this.addToRenderVisual(this.markerLine);
55
 
57
 
56
     this.addControlBox();
58
     this.addControlBox();
59
+
60
+    if (this.page && this.page.mode === 'mirror') {
61
+      this.controlBox.style.display = 'none';
62
+    }
57
   }
63
   }
58
 
64
 
59
   protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
65
   protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {

+ 3
- 1
src/markers/RectMarker/RectBaseMarker.ts View File

1
 import { PositionType } from '../../event/Event';
1
 import { PositionType } from '../../event/Event';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
3
 import { RectangularMarker } from '../RectangularMarker';
3
 import { RectangularMarker } from '../RectangularMarker';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4
 
5
 
5
 export class RectBaseMarker extends RectangularMarker {
6
 export class RectBaseMarker extends RectangularMarker {
6
-  public static createMarker = (): RectBaseMarker => {
7
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
7
     const marker = new RectBaseMarker();
8
     const marker = new RectBaseMarker();
9
+    marker.page = page;
8
     marker.setup();
10
     marker.setup();
9
     return marker;
11
     return marker;
10
   };
12
   };

+ 3
- 1
src/markers/RectMarker/index.ts View File

1
+import { WhitePage } from './../../board/WhitePage/index';
1
 import { MarkerType } from './../types';
2
 import { MarkerType } from './../types';
2
 import { RectBaseMarker } from './RectBaseMarker';
3
 import { RectBaseMarker } from './RectBaseMarker';
3
 import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
4
 import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
5
 export class RectMarker extends RectBaseMarker {
6
 export class RectMarker extends RectBaseMarker {
6
   type: MarkerType = 'rect';
7
   type: MarkerType = 'rect';
7
 
8
 
8
-  public static createMarker = (): RectBaseMarker => {
9
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
9
     const marker = new RectMarker();
10
     const marker = new RectMarker();
11
+    marker.page = page;
10
     marker.setup();
12
     marker.setup();
11
     return marker;
13
     return marker;
12
   };
14
   };

+ 7
- 1
src/markers/RectangularMarker/index.ts View File

3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
4
 import { PositionType } from '../../event/Event';
4
 import { PositionType } from '../../event/Event';
5
 import { SvgHelper } from '../../renderer/SvgHelper';
5
 import { SvgHelper } from '../../renderer/SvgHelper';
6
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
6
 
7
 
7
 export class RectangularMarker extends BaseMarker {
8
 export class RectangularMarker extends BaseMarker {
8
-  public static createMarker = (): RectangularMarker => {
9
+  public static createMarker = (page?: WhitePage): RectangularMarker => {
9
     const marker = new RectangularMarker();
10
     const marker = new RectangularMarker();
11
+    marker.page = page;
10
     marker.setup();
12
     marker.setup();
11
     return marker;
13
     return marker;
12
   };
14
   };
40
     super.setup();
42
     super.setup();
41
 
43
 
42
     this.addControlBox();
44
     this.addControlBox();
45
+
46
+    if (this.page && this.page.mode === 'mirror') {
47
+      this.controlBox.style.display = 'none';
48
+    }
43
   }
49
   }
44
 
50
 
45
   protected resizeByEvent(x: number, y: number, pos: PositionType) {
51
   protected resizeByEvent(x: number, y: number, pos: PositionType) {

+ 18
- 3
src/markers/TextMarker/index.ts View File

1
 import { MarkerType } from '../types';
1
 import { MarkerType } from '../types';
2
 import { RectangularMarker } from '../RectangularMarker';
2
 import { RectangularMarker } from '../RectangularMarker';
3
 import { SvgHelper } from '../../renderer/SvgHelper';
3
 import { SvgHelper } from '../../renderer/SvgHelper';
4
+import { PositionType } from 'fc-whiteboard/src/event/Event';
5
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4
 
6
 
5
 const OkIcon = require('../../assets/check.svg');
7
 const OkIcon = require('../../assets/check.svg');
6
 const CancelIcon = require('../../assets/times.svg');
8
 const CancelIcon = require('../../assets/times.svg');
8
 export class TextMarker extends RectangularMarker {
10
 export class TextMarker extends RectangularMarker {
9
   type: MarkerType = 'text';
11
   type: MarkerType = 'text';
10
 
12
 
11
-  public static createMarker = (): TextMarker => {
13
+  public static createMarker = (page?: WhitePage): TextMarker => {
12
     const marker = new TextMarker();
14
     const marker = new TextMarker();
15
+    marker.page = page;
13
     marker.setup();
16
     marker.setup();
14
     return marker;
17
     return marker;
15
   };
18
   };
16
 
19
 
20
+  /** 设置文本 */
21
+  public setText(text: string) {
22
+    this.text = text;
23
+    this.renderText();
24
+  }
25
+
17
   protected readonly MIN_SIZE = 50;
26
   protected readonly MIN_SIZE = 50;
18
 
27
 
19
   private readonly DEFAULT_TEXT = 'Double-click to edit text';
28
   private readonly DEFAULT_TEXT = 'Double-click to edit text';
41
     this.visual.addEventListener('touchstart', this.onTap);
50
     this.visual.addEventListener('touchstart', this.onTap);
42
   }
51
   }
43
 
52
 
44
-  protected resize(x: number, y: number) {
45
-    super.resize(x, y);
53
+  protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
54
+    super.resize(x, y, onPosition);
55
+
46
     this.sizeText();
56
     this.sizeText();
47
   }
57
   }
48
 
58
 
128
     buttons.appendChild(cancelButton);
138
     buttons.appendChild(cancelButton);
129
   };
139
   };
130
 
140
 
141
+  /** 响应文本输入的事件 */
131
   private onEditorOkClick = (ev: MouseEvent | null) => {
142
   private onEditorOkClick = (ev: MouseEvent | null) => {
132
     if (this.editorTextArea.value.trim()) {
143
     if (this.editorTextArea.value.trim()) {
133
       this.text = this.editorTextArea.value;
144
       this.text = this.editorTextArea.value;
134
     } else {
145
     } else {
135
       this.text = this.DEFAULT_TEXT;
146
       this.text = this.DEFAULT_TEXT;
136
     }
147
     }
148
+
149
+    // 触发文本修改时间
150
+    this.onChange({ target: 'marker', id: this.id, event: 'changeText', data: this.text });
151
+
137
     this.renderText();
152
     this.renderText();
138
     this.closeEditor();
153
     this.closeEditor();
139
   };
154
   };

+ 5
- 2
src/markers/types.ts View File

1
+import { HighlightMarker } from './HighlightMarker/index';
1
 import { TextMarker } from './TextMarker/index';
2
 import { TextMarker } from './TextMarker/index';
2
 import { ArrowMarker } from './ArrowMarker/index';
3
 import { ArrowMarker } from './ArrowMarker/index';
3
 import { BaseMarker } from './BaseMarker/index';
4
 import { BaseMarker } from './BaseMarker/index';
9
 
10
 
10
 export function getMarkerByType(type: MarkerType): typeof BaseMarker {
11
 export function getMarkerByType(type: MarkerType): typeof BaseMarker {
11
   switch (type) {
12
   switch (type) {
12
-    case 'base':
13
-      return BaseMarker;
14
     case 'arrow':
13
     case 'arrow':
15
       return ArrowMarker;
14
       return ArrowMarker;
15
+    case 'base':
16
+      return BaseMarker;
16
     case 'cover':
17
     case 'cover':
17
       return CoverMarker;
18
       return CoverMarker;
19
+    case 'highlight':
20
+      return HighlightMarker;
18
     case 'line':
21
     case 'line':
19
       return LineMarker;
22
       return LineMarker;
20
     case 'rect':
23
     case 'rect':

+ 9
- 2
src/renderer/Synthetizer/index.ts View File

1
+import { isHTMLImageElement } from 'fc-whiteboard/src/utils/validator';
2
+
3
+/** 图片导出 */
1
 export class Synthetizer {
4
 export class Synthetizer {
2
   public rasterize(
5
   public rasterize(
3
-    target: HTMLImageElement,
6
+    target: HTMLImageElement | HTMLDivElement,
4
     markerImage: SVGSVGElement,
7
     markerImage: SVGSVGElement,
5
     done: (dataUrl: string) => void
8
     done: (dataUrl: string) => void
6
   ) {
9
   ) {
10
+    if (!isHTMLImageElement(target)) {
11
+      throw new Error('Error: only support export HTMLImageElement');
12
+    }
13
+
7
     const canvas = document.createElement('canvas');
14
     const canvas = document.createElement('canvas');
8
     canvas.width = markerImage.width.baseVal.value;
15
     canvas.width = markerImage.width.baseVal.value;
9
     canvas.height = markerImage.height.baseVal.value;
16
     canvas.height = markerImage.height.baseVal.value;
16
       throw new Error('Invalid ctx');
23
       throw new Error('Invalid ctx');
17
     }
24
     }
18
 
25
 
19
-    ctx.drawImage(target, 0, 0, canvas.width, canvas.height);
26
+    ctx.drawImage(target as HTMLImageElement, 0, 0, canvas.width, canvas.height);
20
 
27
 
21
     const DOMURL = window.URL; // || window.webkitURL || window;
28
     const DOMURL = window.URL; // || window.webkitURL || window;
22
 
29
 

+ 15
- 0
src/toolbar/Toolbar.ts View File

1
 import { ToolbarButton } from './ToolbarButton';
1
 import { ToolbarButton } from './ToolbarButton';
2
 import { ToolbarItem } from './ToolbarItem';
2
 import { ToolbarItem } from './ToolbarItem';
3
+import { uuid } from '../utils/uuid';
3
 
4
 
4
 export class Toolbar {
5
 export class Toolbar {
6
+  id: string = uuid();
7
+
5
   private toolbarItems: ToolbarItem[];
8
   private toolbarItems: ToolbarItem[];
6
   private toolbarUI: HTMLElement;
9
   private toolbarUI: HTMLElement;
7
 
10
 
15
     this.clickHandler = clickHandler;
18
     this.clickHandler = clickHandler;
16
   }
19
   }
17
 
20
 
21
+  /** 获取 UI 元素 */
18
   public getUI = (): HTMLElement => {
22
   public getUI = (): HTMLElement => {
19
     this.toolbarUI = document.createElement('div');
23
     this.toolbarUI = document.createElement('div');
24
+    this.toolbarUI.id = `fcw-toolbar-${this.id}`;
20
     this.toolbarUI.className = 'fc-whiteboard-toolbar';
25
     this.toolbarUI.className = 'fc-whiteboard-toolbar';
21
 
26
 
22
     for (const toolbarItem of this.toolbarItems) {
27
     for (const toolbarItem of this.toolbarItems) {
26
 
31
 
27
     return this.toolbarUI;
32
     return this.toolbarUI;
28
   };
33
   };
34
+
35
+  public hide() {
36
+    this.toolbarUI.style.opacity = '0';
37
+    this.toolbarUI.style.zIndex = '-1';
38
+  }
39
+
40
+  public show() {
41
+    this.toolbarUI.style.opacity = '1';
42
+    this.toolbarUI.style.zIndex = '999';
43
+  }
29
 }
44
 }

+ 55
- 0
src/toolbar/toolbar-items.ts View File

1
+import { WhitePage } from './../board/WhitePage/index';
1
 import { RectMarker } from './../markers/RectMarker/index';
2
 import { RectMarker } from './../markers/RectMarker/index';
2
 import { CoverMarker } from './../markers/CoverMarker/index';
3
 import { CoverMarker } from './../markers/CoverMarker/index';
3
 import { TextMarker } from './../markers/TextMarker/index';
4
 import { TextMarker } from './../markers/TextMarker/index';
6
 import { ToolbarItem } from './ToolbarItem';
7
 import { ToolbarItem } from './ToolbarItem';
7
 import { LineMarker } from '../markers/LineMarker';
8
 import { LineMarker } from '../markers/LineMarker';
8
 
9
 
10
+const OkIcon = require('../assets/check.svg');
11
+const DeleteIcon = require('../assets/eraser.svg');
12
+const PointerIcon = require('../assets/mouse-pointer.svg');
13
+const CloseIcon = require('../assets/times.svg');
14
+
9
 export const highlightMarkerToolbarItem = new ToolbarItem({
15
 export const highlightMarkerToolbarItem = new ToolbarItem({
10
   name: 'cover-marker',
16
   name: 'cover-marker',
11
   tooltipText: 'Cover',
17
   tooltipText: 'Cover',
47
   icon: require('../assets/line.svg'),
53
   icon: require('../assets/line.svg'),
48
   markerType: LineMarker
54
   markerType: LineMarker
49
 });
55
 });
56
+
57
+export function getToolbars(page?: WhitePage) {
58
+  const toolbars = [
59
+    {
60
+      icon: PointerIcon,
61
+      name: 'pointer',
62
+      tooltipText: 'Pointer'
63
+    },
64
+    {
65
+      icon: DeleteIcon,
66
+      name: 'delete',
67
+      tooltipText: 'Delete'
68
+    },
69
+    {
70
+      name: 'separator',
71
+      tooltipText: ''
72
+    },
73
+    rectMarkerToolbarItem,
74
+    coverMarkerToolbarItem,
75
+    highlightMarkerToolbarItem,
76
+    lineMarkerToolbarItem,
77
+    arrowMarkerToolbarItem,
78
+    textMarkerToolbarItem,
79
+    {
80
+      name: 'separator',
81
+      tooltipText: ''
82
+    }
83
+  ];
84
+
85
+  if (!page) {
86
+    toolbars.push(
87
+      ...[
88
+        {
89
+          icon: OkIcon,
90
+          name: 'ok',
91
+          tooltipText: 'OK'
92
+        }
93
+      ]
94
+    );
95
+  }
96
+
97
+  toolbars.push({
98
+    icon: CloseIcon,
99
+    name: 'close',
100
+    tooltipText: 'Close'
101
+  });
102
+
103
+  return toolbars;
104
+}

+ 2
- 0
src/types.d.ts View File

11
   const content: string;
11
   const content: string;
12
   export default content;
12
   export default content;
13
 }
13
 }
14
+
15
+declare module 'siema';

+ 23
- 0
src/utils/dom.ts View File

1
+/** 创建包含样式类名的 Div 元素 */
2
+export function createDivWithClassName(className?: string, parent?: HTMLElement) {
3
+  const ele = document.createElement('div');
4
+
5
+  if (parent) {
6
+    parent.appendChild(ele);
7
+  }
8
+
9
+  if (className) {
10
+    addClassName(ele, className);
11
+  }
12
+
13
+  return ele;
14
+}
15
+
16
+/** 添加样式类名 */
17
+export function addClassName(ele: HTMLElement, className: string) {
18
+  if (!ele) {
19
+    return;
20
+  }
21
+
22
+  ele.className = `${ele.className || ''} ${className}`.trim();
23
+}

+ 8
- 0
src/utils/validator.ts View File

1
+/** 判断是否为有效的 HTMLImageElement */
2
+export function isHTMLImageElement(ele: any) {
3
+  if (typeof ele === 'object' && ele instanceof HTMLImageElement) {
4
+    return true;
5
+  }
6
+
7
+  return false;
8
+}