Ver código fonte

feat: update

wxyyxc1992 5 anos atrás
pai
commit
9b18ca2c44

+ 1
- 0
src/assets/drag.svg Ver arquivo

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1555470391343" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2050" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" id="drag-handler"><defs><style type="text/css"></style></defs><path d="M298.666667 810.666667v-85.333334h85.333333v85.333334H298.666667m170.666666 0v-85.333334h85.333334v85.333334h-85.333334m170.666667 0v-85.333334h85.333333v85.333334h-85.333333m-341.333333-170.666667v-85.333333h85.333333v85.333333H298.666667m170.666666 0v-85.333333h85.333334v85.333333h-85.333334m170.666667 0v-85.333333h85.333333v85.333333h-85.333333m-341.333333-170.666667V384h85.333333v85.333333H298.666667m170.666666 0V384h85.333334v85.333333h-85.333334m170.666667 0V384h85.333333v85.333333h-85.333333M298.666667 298.666667V213.333333h85.333333v85.333334H298.666667m170.666666 0V213.333333h85.333334v85.333334h-85.333334m170.666667 0V213.333333h85.333333v85.333334h-85.333333z" fill="" p-id="2051"></path></svg>

+ 0
- 8
src/drawboard/Baseboard/index.ts Ver arquivo

@@ -71,12 +71,4 @@ export class Baseboard {
71 71
     this.boardHolder.style.top = this.targetRect.top + 'px';
72 72
     this.boardHolder.style.left = this.targetRect.left + 'px';
73 73
   };
74
-
75
-  protected initEmbeddingToolbar = () => {};
76
-
77
-  protected positionEmbeddingToolbar = () => {};
78
-
79
-  protected initFloatingToolbar = () => {};
80
-
81
-  protected positionFloatingToolbar = () => {};
82 74
 }

+ 0
- 47
src/drawboard/Drawboard/index.less Ver arquivo

@@ -19,53 +19,6 @@
19 19
   fill: #ff8080;
20 20
 }
21 21
 
22
-.fc-whiteboard-toolbar {
23
-  background-color: #cccccc;
24
-  padding: 0px 5px;
25
-  margin: 0px;
26
-  border-top-left-radius: 10px;
27
-  border-top-right-radius: 10px;
28
-
29
-  display: grid;
30
-  grid-template-columns: repeat(20, auto);
31
-}
32
-
33
-.fc-whiteboard-toolbar-button,
34
-.fc-whiteboard-toolbar-logo a {
35
-  display: inline-block;
36
-  margin: 2px;
37
-  padding: 3px;
38
-  cursor: pointer;
39
-  width: 20px;
40
-  height: 20px;
41
-  border-radius: 2px;
42
-  border-bottom: transparent solid 1px;
43
-  border-right: transparent solid 1px;
44
-
45
-  fill: #333333;
46
-
47
-  display: grid;
48
-  align-items: center;
49
-  justify-items: center;
50
-}
51
-
52
-.fc-whiteboard-toolbar-separator {
53
-  margin: 5px 5px;
54
-  border: 1px solid #dddddd;
55
-}
56
-
57
-.fc-whiteboard-toolbar-button:hover,
58
-.fc-whiteboard-toolbar-logo a:hover {
59
-  background-color: #eeeeee;
60
-  background: radial-gradient(#eeeeee, #cccccc);
61
-
62
-  fill: #ff8080;
63
-}
64
-
65
-.fc-whiteboard-toolbar-button svg {
66
-  height: 16px;
67
-}
68
-
69 22
 .fc-whiteboard-text-editor {
70 23
   position: fixed;
71 24
   z-index: 20000;

+ 42
- 7
src/drawboard/Drawboard/index.ts Ver arquivo

@@ -29,8 +29,9 @@ export class Drawboard extends Baseboard {
29 29
   }
30 30
   activeMarker: BaseMarker | null;
31 31
 
32
+  toolbarItems: ToolbarItem[];
33
+
32 34
   toolbar: Toolbar;
33
-  toolbars: ToolbarItem[];
34 35
   toolbarUI: HTMLElement;
35 36
 
36 37
   /** 回调 */
@@ -40,7 +41,12 @@ export class Drawboard extends Baseboard {
40 41
 
41 42
   constructor(
42 43
     source: Source,
43
-    { page, zIndex, onChange }: { page?: WhitePage; zIndex?: number; onChange?: onSyncFunc } = {}
44
+    {
45
+      page,
46
+      zIndex,
47
+      extraToolbarItems,
48
+      onChange
49
+    }: Partial<Drawboard> & { extraToolbarItems?: ToolbarItem[] } = {}
44 50
   ) {
45 51
     super(source);
46 52
 
@@ -54,7 +60,14 @@ export class Drawboard extends Baseboard {
54 60
 
55 61
     this.markers = [];
56 62
     this.activeMarker = null;
57
-    this.toolbars = getToolbars(page);
63
+
64
+    const toolbarItems = getToolbars(page);
65
+
66
+    if (extraToolbarItems) {
67
+      toolbarItems.push(...extraToolbarItems);
68
+    }
69
+
70
+    this.toolbarItems = toolbarItems;
58 71
 
59 72
     if (onChange) {
60 73
       this.onChange = onChange;
@@ -80,7 +93,7 @@ export class Drawboard extends Baseboard {
80 93
 
81 94
     window.addEventListener('resize', this.adjustUI);
82 95
 
83
-    if (this.page.mode === 'master') {
96
+    if ((this.page && this.page.mode === 'master') || !this.page) {
84 97
       this.showUI();
85 98
     }
86 99
   };
@@ -263,15 +276,34 @@ export class Drawboard extends Baseboard {
263 276
   };
264 277
 
265 278
   private showUI = () => {
266
-    this.toolbar = new Toolbar(this.toolbars, this.toolbarClick);
279
+    this.toolbar = new Toolbar(this.toolbarItems, this.toolbarClick);
267 280
     this.toolbar.zIndex = this.zIndex;
268 281
 
269
-    this.toolbarUI = this.toolbar.getUI();
282
+    this.toolbarUI = this.toolbar.getUI(this);
270 283
 
271 284
     document.body.appendChild(this.toolbarUI);
272 285
     this.toolbarUI.style.position = 'absolute';
273 286
 
274 287
     this.positionToolbar();
288
+
289
+    // 处理元素的拖拽事件
290
+    this.toolbar.toolbarButtons.forEach(button => {
291
+      if (button.toolbarItem.draggable) {
292
+        button.container.draggable = true;
293
+        button.container.ondragstart = ev => {
294
+          if (ev) {
295
+            ev.dataTransfer!.setData('id', button.id);
296
+          }
297
+        };
298
+      }
299
+    });
300
+
301
+    this.boardCanvas.ondragover = ev => {
302
+      ev.preventDefault();
303
+    };
304
+    this.boardCanvas.ondrop = ev => {
305
+      console.log(ev);
306
+    };
275 307
   };
276 308
 
277 309
   private setStyles = () => {
@@ -329,8 +361,11 @@ export class Drawboard extends Baseboard {
329 361
     this.boardCanvas.appendChild(editorStyleSheet);
330 362
   };
331 363
 
364
+  /** 处理 Toolbar 的点击事件 */
332 365
   private toolbarClick = (ev: MouseEvent, toolbarItem: ToolbarItem) => {
333
-    if (toolbarItem.markerType) {
366
+    if (toolbarItem.onClick) {
367
+      toolbarItem.onClick();
368
+    } else if (toolbarItem.markerType) {
334 369
       this.addMarker(toolbarItem.markerType);
335 370
     } else {
336 371
       // command button

+ 7
- 2
src/index.ts Ver arquivo

@@ -1,4 +1,9 @@
1
-export { EventHub } from './event/EventHub';
2 1
 export { Drawboard } from './drawboard/Drawboard';
3
-export { Whiteboard } from './whiteboard/Whiteboard';
2
+export { separatorToolbarItem, closeToolbarItem } from './toolbar/toolbar-items';
3
+
4 4
 export { Mode } from './utils/types';
5
+export { SyncEvent } from './event/SyncEvent';
6
+export { EventHub } from './event/EventHub';
7
+export { Whiteboard } from './whiteboard/Whiteboard';
8
+export { MirrorWhiteboard } from './whiteboard/MirrorWhiteboard';
9
+export { ReplayWhiteboard } from './whiteboard/ReplayWhiteboard';

+ 3
- 3
src/markers/ArrowMarker/index.ts Ver arquivo

@@ -9,14 +9,14 @@ export class ArrowMarker extends LinearMarker {
9 9
   public static createMarker = (page?: WhitePage): LinearMarker => {
10 10
     const marker = new ArrowMarker();
11 11
     marker.page = page;
12
-    marker.setup();
12
+    marker.init();
13 13
     return marker;
14 14
   };
15 15
 
16 16
   private readonly ARROW_SIZE = 6;
17 17
 
18
-  protected setup() {
19
-    super.setup();
18
+  protected init() {
19
+    super.init();
20 20
     SvgHelper.setAttributes(this.visual, [['class', 'arrow-marker']]);
21 21
 
22 22
     const tip = SvgHelper.createPolygon(

+ 17
- 69
src/markers/BaseMarker/index.ts Ver arquivo

@@ -6,8 +6,10 @@ import * as uuid from 'uuid/v1';
6 6
 import { SvgHelper } from '../../renderer/SvgHelper';
7 7
 import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
8 8
 import { Drawboard } from '../../drawboard/Drawboard/index';
9
+import { DomEventAware } from '../../renderer/DomEventAware/index';
10
+import { isNil } from '../../utils/types';
9 11
 
10
-export class BaseMarker {
12
+export class BaseMarker extends DomEventAware {
11 13
   id: string = uuid();
12 14
   type: MarkerType = 'base';
13 15
   // 归属的 WhitePage
@@ -20,7 +22,7 @@ export class BaseMarker {
20 22
   public static createMarker = (page?: WhitePage): BaseMarker => {
21 23
     const marker = new BaseMarker();
22 24
     marker.page = page;
23
-    marker.setup();
25
+    marker.init();
24 26
     return marker;
25 27
   };
26 28
 
@@ -31,36 +33,31 @@ export class BaseMarker {
31 33
 
32 34
   public defs: SVGElement[] = [];
33 35
 
34
-  x: number = 0;
35
-  y: number = 0;
36 36
   width: number = 200;
37 37
   height: number = 50;
38 38
 
39
-  protected isActive: boolean = true;
40
-  protected isDragging: boolean = false;
41
-  protected isResizing: boolean = false;
42
-
43
-  protected previousMouseX: number = 0;
44
-  protected previousMouseY: number = 0;
39
+  isActive: boolean = true;
40
+  isDragging: boolean = false;
41
+  isResizing: boolean = false;
45 42
 
46 43
   public reactToManipulation(
47 44
     type: EventType,
48 45
     { dx, dy, pos }: { dx?: number; dy?: number; pos?: PositionType } = {}
49 46
   ) {
50 47
     if (type === 'moveMarker') {
51
-      if (!dx || !dy) {
48
+      if (isNil(dx) || isNil(dy)) {
52 49
         return;
53 50
       }
54 51
 
55
-      this.move(dx, dy);
52
+      this.move(dx!, dy!);
56 53
     }
57 54
 
58 55
     if (type === 'resizeMarker') {
59
-      if (!dx || !dy) {
56
+      if (isNil(dx) || isNil(dy)) {
60 57
         return;
61 58
       }
62 59
 
63
-      this.resizeByEvent(dx, dy, pos);
60
+      this.resizeByEvent(dx!, dy!, pos);
64 61
     }
65 62
   }
66 63
 
@@ -163,20 +160,13 @@ export class BaseMarker {
163 160
     this.y = y;
164 161
   };
165 162
 
166
-  /** Init */
167
-  protected setup() {
163
+  /** Init base marker */
164
+  protected init() {
168 165
     this.visual = SvgHelper.createGroup();
169 166
     // translate
170 167
     this.visual.transform.baseVal.appendItem(SvgHelper.createTransform());
171 168
 
172
-    this.visual.addEventListener('mousedown', this.mouseDown);
173
-    this.visual.addEventListener('mouseup', this.mouseUp);
174
-    this.visual.addEventListener('mousemove', this.mouseMove);
175
-
176
-    this.visual.addEventListener('touchstart', this.onTouch, { passive: false });
177
-    this.visual.addEventListener('touchend', this.onTouch, { passive: false });
178
-    this.visual.addEventListener('touchmove', this.onTouch, { passive: false });
179
-
169
+    super.init(this.visual);
180 170
     this.renderVisual = SvgHelper.createGroup([['class', 'render-visual']]);
181 171
     this.visual.appendChild(this.renderVisual);
182 172
   }
@@ -189,49 +179,7 @@ export class BaseMarker {
189 179
     this.renderVisual.appendChild(el);
190 180
   };
191 181
 
192
-  /** 截获 Touch 事件,并且转发为 Mouse 事件 */
193
-  protected onTouch(ev: TouchEvent) {
194
-    ev.preventDefault();
195
-    const newEvt = document.createEvent('MouseEvents');
196
-    const touch = ev.changedTouches[0];
197
-    let type = null;
198
-
199
-    switch (ev.type) {
200
-      case 'touchstart':
201
-        type = 'mousedown';
202
-        break;
203
-      case 'touchmove':
204
-        type = 'mousemove';
205
-        break;
206
-      case 'touchend':
207
-        type = 'mouseup';
208
-        break;
209
-      default:
210
-        break;
211
-    }
212
-
213
-    newEvt.initMouseEvent(
214
-      type!,
215
-      true,
216
-      true,
217
-      window,
218
-      0,
219
-      touch.screenX,
220
-      touch.screenY,
221
-      touch.clientX,
222
-      touch.clientY,
223
-      ev.ctrlKey,
224
-      ev.altKey,
225
-      ev.shiftKey,
226
-      ev.metaKey,
227
-      0,
228
-      null
229
-    );
230
-
231
-    ev.target!.dispatchEvent(newEvt);
232
-  }
233
-
234
-  private mouseDown = (ev: MouseEvent) => {
182
+  protected onMouseDown = (ev: MouseEvent) => {
235 183
     ev.stopPropagation();
236 184
 
237 185
     if (this.page && this.page.mode === 'mirror') {
@@ -244,12 +192,12 @@ export class BaseMarker {
244 192
     this.previousMouseY = ev.screenY;
245 193
   };
246 194
 
247
-  private mouseUp = (ev: MouseEvent) => {
195
+  protected onMouseUp = (ev: MouseEvent) => {
248 196
     ev.stopPropagation();
249 197
     this.endManipulation();
250 198
   };
251 199
 
252
-  private mouseMove = (ev: MouseEvent) => {
200
+  protected onMouseMove = (ev: MouseEvent) => {
253 201
     ev.stopPropagation();
254 202
     this.manipulate(ev);
255 203
   };

+ 3
- 3
src/markers/CoverMarker/index.ts Ver arquivo

@@ -9,12 +9,12 @@ export class CoverMarker extends RectBaseMarker {
9 9
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
10 10
     const marker = new CoverMarker();
11 11
     marker.page = page;
12
-    marker.setup();
12
+    marker.init();
13 13
     return marker;
14 14
   };
15 15
 
16
-  protected setup() {
17
-    super.setup();
16
+  protected init() {
17
+    super.init();
18 18
     SvgHelper.setAttributes(this.visual, [['class', 'cover-marker']]);
19 19
   }
20 20
 }

+ 3
- 3
src/markers/HighlightMarker/index.ts Ver arquivo

@@ -9,12 +9,12 @@ export class HighlightMarker extends RectBaseMarker {
9 9
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
10 10
     const marker = new HighlightMarker();
11 11
     marker.page = page;
12
-    marker.setup();
12
+    marker.init();
13 13
     return marker;
14 14
   };
15 15
 
16
-  protected setup() {
17
-    super.setup();
16
+  protected init() {
17
+    super.init();
18 18
     SvgHelper.setAttributes(this.visual, [['class', 'highlight-marker']]);
19 19
   }
20 20
 }

+ 3
- 3
src/markers/LineMarker/index.ts Ver arquivo

@@ -9,12 +9,12 @@ export class LineMarker extends LinearMarker {
9 9
   public static createMarker = (page?: WhitePage): LinearMarker => {
10 10
     const marker = new LineMarker();
11 11
     marker.page = page;
12
-    marker.setup();
12
+    marker.init();
13 13
     return marker;
14 14
   };
15 15
 
16
-  protected setup() {
17
-    super.setup();
16
+  protected init() {
17
+    super.init();
18 18
     SvgHelper.setAttributes(this.visual, [['class', 'line-marker']]);
19 19
   }
20 20
 }

+ 7
- 6
src/markers/LinearMarker/index.ts Ver arquivo

@@ -13,7 +13,7 @@ export class LinearMarker extends BaseMarker implements LinearBound {
13 13
   public static createMarker = (page?: WhitePage): LinearMarker => {
14 14
     const marker = new LinearMarker();
15 15
     marker.page = page;
16
-    marker.setup();
16
+    marker.init();
17 17
     return marker;
18 18
   };
19 19
 
@@ -78,8 +78,8 @@ export class LinearMarker extends BaseMarker implements LinearBound {
78 78
     this.controlBox.style.display = 'none';
79 79
   }
80 80
 
81
-  protected setup() {
82
-    super.setup();
81
+  protected init() {
82
+    super.init();
83 83
 
84 84
     this.markerBgLine = SvgHelper.createLine(0, 0, this.x2, 0, [
85 85
       ['stroke', 'transparent'],
@@ -138,16 +138,13 @@ export class LinearMarker extends BaseMarker implements LinearBound {
138 138
     } else {
139 139
       this.activeGrip = this.controlGrips.right;
140 140
     }
141
-
142 141
     this.resize(x, y);
143 142
   }
144 143
 
145 144
   /** Init */
146
-
147 145
   private addControlBox = () => {
148 146
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-line-control-box']]);
149 147
     this.addToVisual(this.controlBox);
150
-
151 148
     this.addControlGrips();
152 149
   };
153 150
 
@@ -177,6 +174,10 @@ export class LinearMarker extends BaseMarker implements LinearBound {
177 174
     grip.visual.addEventListener('touchend', this.onTouch, { passive: false });
178 175
     grip.visual.addEventListener('touchmove', this.onTouch, { passive: false });
179 176
 
177
+    if (this.page && this.page.mode === 'mirror') {
178
+      grip.visual.style.visibility = 'hidden';
179
+    }
180
+
180 181
     return grip;
181 182
   };
182 183
 

+ 3
- 3
src/markers/RectMarker/RectBaseMarker.ts Ver arquivo

@@ -8,7 +8,7 @@ export class RectBaseMarker extends RectangularMarker {
8 8
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
9 9
     const marker = new RectBaseMarker();
10 10
     marker.page = page;
11
-    marker.setup();
11
+    marker.init();
12 12
     return marker;
13 13
   };
14 14
 
@@ -25,8 +25,8 @@ export class RectBaseMarker extends RectangularMarker {
25 25
     }
26 26
   }
27 27
 
28
-  protected setup() {
29
-    super.setup();
28
+  protected init() {
29
+    super.init();
30 30
 
31 31
     this.markerRect = SvgHelper.createRect(this.width, this.height);
32 32
     this.addToRenderVisual(this.markerRect);

+ 3
- 3
src/markers/RectMarker/index.ts Ver arquivo

@@ -9,12 +9,12 @@ export class RectMarker extends RectBaseMarker {
9 9
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
10 10
     const marker = new RectMarker();
11 11
     marker.page = page;
12
-    marker.setup();
12
+    marker.init();
13 13
     return marker;
14 14
   };
15 15
 
16
-  protected setup() {
17
-    super.setup();
16
+  protected init() {
17
+    super.init();
18 18
     SvgHelper.setAttributes(this.visual, [['class', 'rect-marker']]);
19 19
   }
20 20
 }

+ 3
- 3
src/markers/RectangularMarker/index.ts Ver arquivo

@@ -10,7 +10,7 @@ export class RectangularMarker extends BaseMarker {
10 10
   public static createMarker = (page?: WhitePage): RectangularMarker => {
11 11
     const marker = new RectangularMarker();
12 12
     marker.page = page;
13
-    marker.setup();
13
+    marker.init();
14 14
     return marker;
15 15
   };
16 16
 
@@ -65,8 +65,8 @@ export class RectangularMarker extends BaseMarker {
65 65
     this.controlBox.style.display = 'none';
66 66
   }
67 67
 
68
-  protected setup() {
69
-    super.setup();
68
+  protected init() {
69
+    super.init();
70 70
 
71 71
     this.addControlBox();
72 72
 

+ 3
- 3
src/markers/TextMarker/index.ts Ver arquivo

@@ -14,7 +14,7 @@ export class TextMarker extends RectangularMarker {
14 14
   public static createMarker = (page?: WhitePage): TextMarker => {
15 15
     const marker = new TextMarker();
16 16
     marker.page = page;
17
-    marker.setup();
17
+    marker.init();
18 18
     return marker;
19 19
   };
20 20
 
@@ -51,8 +51,8 @@ export class TextMarker extends RectangularMarker {
51 51
     }
52 52
   }
53 53
 
54
-  protected setup() {
55
-    super.setup();
54
+  protected init() {
55
+    super.init();
56 56
     this.textElement = SvgHelper.createText();
57 57
     this.addToRenderVisual(this.textElement);
58 58
     SvgHelper.setAttributes(this.visual, [['class', 'text-marker']]);

+ 62
- 0
src/renderer/DomEventAware/index.ts Ver arquivo

@@ -0,0 +1,62 @@
1
+export abstract class DomEventAware {
2
+  x: number = 0;
3
+  y: number = 0;
4
+  previousMouseX: number = 0;
5
+  previousMouseY: number = 0;
6
+
7
+  protected init(ele: Element) {
8
+    ele.addEventListener('mousedown', this.onMouseDown);
9
+    ele.addEventListener('mouseup', this.onMouseUp);
10
+    ele.addEventListener('mousemove', this.onMouseMove);
11
+
12
+    ele.addEventListener('touchstart', this.onTouch, { passive: false });
13
+    ele.addEventListener('touchend', this.onTouch, { passive: false });
14
+    ele.addEventListener('touchmove', this.onTouch, { passive: false });
15
+  }
16
+
17
+  /** 截获 Touch 事件,并且转发为 Mouse 事件 */
18
+  protected onTouch(ev: TouchEvent) {
19
+    ev.preventDefault();
20
+    const newEvt = document.createEvent('MouseEvents');
21
+    const touch = ev.changedTouches[0];
22
+    let type = null;
23
+
24
+    switch (ev.type) {
25
+      case 'touchstart':
26
+        type = 'mousedown';
27
+        break;
28
+      case 'touchmove':
29
+        type = 'mousemove';
30
+        break;
31
+      case 'touchend':
32
+        type = 'mouseup';
33
+        break;
34
+      default:
35
+        break;
36
+    }
37
+
38
+    newEvt.initMouseEvent(
39
+      type!,
40
+      true,
41
+      true,
42
+      window,
43
+      0,
44
+      touch.screenX,
45
+      touch.screenY,
46
+      touch.clientX,
47
+      touch.clientY,
48
+      ev.ctrlKey,
49
+      ev.altKey,
50
+      ev.shiftKey,
51
+      ev.metaKey,
52
+      0,
53
+      null
54
+    );
55
+
56
+    ev.target!.dispatchEvent(newEvt);
57
+  }
58
+
59
+  protected abstract onMouseDown(ev: MouseEvent): void;
60
+  protected abstract onMouseUp(ev: MouseEvent): void;
61
+  protected abstract onMouseMove(ev: MouseEvent): void;
62
+}

+ 0
- 0
src/toolbar/EmbeddingToolbar/index.ts Ver arquivo


+ 0
- 0
src/toolbar/FloatingToolbar/index.ts Ver arquivo


+ 45
- 3
src/toolbar/Toolbar.ts Ver arquivo

@@ -1,13 +1,23 @@
1
+import interact from 'interactjs';
2
+
1 3
 import { ToolbarButton } from './ToolbarButton';
2 4
 import { ToolbarItem } from './ToolbarItem';
3 5
 import { uuid } from '../utils/uuid';
4 6
 
5
-export class Toolbar {
7
+import './index.less';
8
+import { DomEventAware } from '../renderer/DomEventAware/index';
9
+import { dragToolbarItem } from './toolbar-items';
10
+import { Drawboard } from '../drawboard/Drawboard/index';
11
+
12
+export type MouseHandler = (ev: MouseEvent) => void;
13
+
14
+export class Toolbar extends DomEventAware {
6 15
   id: string = uuid();
7 16
   zIndex: number = 999;
8 17
 
9 18
   toolbarItems: ToolbarItem[];
10 19
   toolbarUI: HTMLElement;
20
+  toolbarButtons: ToolbarButton[] = [];
11 21
 
12 22
   clickHandler: (ev: MouseEvent, toolbarItem: ToolbarItem) => void;
13 23
 
@@ -15,21 +25,32 @@ export class Toolbar {
15 25
     toolbarItems: ToolbarItem[],
16 26
     clickHandler: (ev: MouseEvent, toolbarItem: ToolbarItem) => void
17 27
   ) {
18
-    this.toolbarItems = toolbarItems;
28
+    super();
29
+
30
+    this.toolbarItems = [dragToolbarItem, ...toolbarItems];
19 31
     this.clickHandler = clickHandler;
20 32
   }
21 33
 
22 34
   /** 获取 UI 元素 */
23
-  public getUI = (): HTMLElement => {
35
+  public getUI = (drawboard: Drawboard): HTMLElement => {
24 36
     this.toolbarUI = document.createElement('div');
25 37
     this.toolbarUI.id = `fcw-toolbar-${this.id}`;
26 38
     this.toolbarUI.className = 'fc-whiteboard-toolbar';
27 39
 
28 40
     for (const toolbarItem of this.toolbarItems) {
29 41
       const toolbarButton = new ToolbarButton(toolbarItem, this.clickHandler);
42
+      toolbarButton.drawboard = drawboard;
30 43
       this.toolbarUI.appendChild(toolbarButton.getElement());
44
+
45
+      this.toolbarButtons.push(toolbarButton);
31 46
     }
32 47
 
48
+    super.init(this.toolbarUI);
49
+
50
+    interact('#drag-handler').draggable({
51
+      onmove: this.onDragMove
52
+    });
53
+
33 54
     return this.toolbarUI;
34 55
   };
35 56
 
@@ -42,4 +63,25 @@ export class Toolbar {
42 63
     this.toolbarUI.style.visibility = 'visible';
43 64
     this.toolbarUI.style.zIndex = `${this.zIndex}`;
44 65
   }
66
+
67
+  protected onMouseDown = (downEv: MouseEvent) => {};
68
+
69
+  protected onMouseUp = (ev: MouseEvent) => {};
70
+
71
+  protected onMouseMove = (ev: MouseEvent) => {};
72
+
73
+  protected onDragMove = (event: any) => {
74
+    var target = this.toolbarUI;
75
+
76
+    // keep the dragged position in the data-x/data-y attributes
77
+    var x = ((parseFloat(target.getAttribute('data-x') as string) || 0) + event.dx) as any;
78
+    var y = ((parseFloat(target.getAttribute('data-y') as string) || 0) + event.dy) as any;
79
+
80
+    // translate the element
81
+    target.style.webkitTransform = target.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
82
+
83
+    // update the posiion attributes
84
+    target.setAttribute('data-x', x);
85
+    target.setAttribute('data-y', y);
86
+  };
45 87
 }

+ 15
- 4
src/toolbar/ToolbarButton.ts Ver arquivo

@@ -1,8 +1,15 @@
1
+import { uuid } from './../utils/uuid';
1 2
 import { ToolbarItem } from './ToolbarItem';
3
+import { Drawboard } from '../drawboard/Drawboard';
2 4
 
5
+/** 工作栏按钮 */
3 6
 export class ToolbarButton {
4
-  private toolbarItem: ToolbarItem;
5
-  private clickHandler: (ev: MouseEvent, toolbarItem: ToolbarItem) => void;
7
+  id = uuid();
8
+  drawboard: Drawboard;
9
+  toolbarItem: ToolbarItem;
10
+  container: HTMLDivElement;
11
+
12
+  clickHandler: (ev: MouseEvent, toolbarItem: ToolbarItem) => void;
6 13
 
7 14
   constructor(
8 15
     toolbarItem: ToolbarItem,
@@ -17,8 +24,10 @@ export class ToolbarButton {
17 24
 
18 25
   public getElement = (): HTMLElement => {
19 26
     const div = document.createElement('div');
27
+
20 28
     if (this.toolbarItem.name !== 'separator') {
21 29
       div.className = 'fc-whiteboard-toolbar-button';
30
+
22 31
       if (this.clickHandler) {
23 32
         div.addEventListener('click', (ev: MouseEvent) => {
24 33
           if (this.clickHandler) {
@@ -28,15 +37,17 @@ export class ToolbarButton {
28 37
       }
29 38
 
30 39
       if (this.toolbarItem.icon) {
31
-        div.title = this.toolbarItem.tooltipText;
40
+        div.title = this.toolbarItem.tooltipText || '';
32 41
         div.innerHTML = this.toolbarItem.icon;
33 42
       } else {
34
-        div.innerText = this.toolbarItem.tooltipText;
43
+        div.innerText = this.toolbarItem.tooltipText || '';
35 44
       }
36 45
     } else {
37 46
       div.className = 'fc-whiteboard-toolbar-separator';
38 47
     }
39 48
 
49
+    this.container = div;
50
+
40 51
     return div;
41 52
   };
42 53
 }

+ 12
- 12
src/toolbar/ToolbarItem.ts Ver arquivo

@@ -1,24 +1,24 @@
1 1
 import { BaseMarker } from './../markers/BaseMarker/index';
2
+
3
+/** 对于工具栏的定义 */
2 4
 export class ToolbarItem {
3 5
   name: string;
4
-  tooltipText: string;
6
+  tooltipText?: string;
5 7
   icon?: string;
6 8
   markerType?: typeof BaseMarker;
9
+  onClick?: () => void;
10
+  draggable?: boolean;
11
+
12
+  constructor({ name, tooltipText, icon, markerType, onClick, draggable }: Partial<ToolbarItem>) {
13
+    if (!name) {
14
+      throw new Error('Invalid params');
15
+    }
7 16
 
8
-  constructor({
9
-    name,
10
-    tooltipText,
11
-    icon,
12
-    markerType
13
-  }: {
14
-    name: string;
15
-    tooltipText: string;
16
-    icon?: string;
17
-    markerType?: typeof BaseMarker;
18
-  }) {
19 17
     this.name = name;
20 18
     this.tooltipText = tooltipText;
21 19
     this.icon = icon;
22 20
     this.markerType = markerType;
21
+    this.onClick = onClick;
22
+    this.draggable = draggable;
23 23
   }
24 24
 }

+ 49
- 0
src/toolbar/index.less Ver arquivo

@@ -0,0 +1,49 @@
1
+.fc-whiteboard-toolbar {
2
+  height: 36px;
3
+  background: rgba(255, 255, 255, 1);
4
+  box-shadow: 0px 2px 9px 0px rgba(194, 185, 187, 0.29);
5
+  border-radius: 13px;
6
+  opacity: 0.9;
7
+  border: 1px solid rgba(230, 236, 247, 1);
8
+
9
+  display: flex;
10
+  align-items: center;
11
+}
12
+
13
+.fc-whiteboard-toolbar-button,
14
+.fc-whiteboard-toolbar-logo a {
15
+  display: inline-block;
16
+  margin: 2px;
17
+  padding: 3px;
18
+  cursor: pointer;
19
+  width: 20px;
20
+  height: 20px;
21
+  border-radius: 2px;
22
+  border-bottom: transparent solid 1px;
23
+  border-right: transparent solid 1px;
24
+
25
+  fill: #333333;
26
+
27
+  display: grid;
28
+  align-items: center;
29
+  justify-items: center;
30
+}
31
+
32
+.fc-whiteboard-toolbar-separator {
33
+  width: 0px;
34
+  height: 20px;
35
+  margin: 5px 5px;
36
+  border: 1px solid #cad3df;
37
+}
38
+
39
+.fc-whiteboard-toolbar-button:hover,
40
+.fc-whiteboard-toolbar-logo a:hover {
41
+  background-color: #eeeeee;
42
+  background: radial-gradient(#eeeeee, #cccccc);
43
+
44
+  fill: #ff8080;
45
+}
46
+
47
+.fc-whiteboard-toolbar-button svg {
48
+  height: 16px;
49
+}

+ 17
- 16
src/toolbar/toolbar-items.ts Ver arquivo

@@ -12,6 +12,12 @@ const DeleteIcon = require('../assets/eraser.svg');
12 12
 const PointerIcon = require('../assets/mouse-pointer.svg');
13 13
 const CloseIcon = require('../assets/times.svg');
14 14
 
15
+export const dragToolbarItem = new ToolbarItem({
16
+  name: 'drag',
17
+  tooltipText: 'Drag',
18
+  icon: require('../assets/drag.svg')
19
+});
20
+
15 21
 export const highlightMarkerToolbarItem = new ToolbarItem({
16 22
   name: 'cover-marker',
17 23
   tooltipText: 'Cover',
@@ -54,32 +60,33 @@ export const lineMarkerToolbarItem = new ToolbarItem({
54 60
   markerType: LineMarker
55 61
 });
56 62
 
63
+export const closeToolbarItem = new ToolbarItem({
64
+  icon: CloseIcon,
65
+  name: 'close',
66
+  tooltipText: 'Close'
67
+});
68
+
69
+export const separatorToolbarItem = new ToolbarItem({ name: 'separator', tooltipText: '' });
70
+
57 71
 export function getToolbars(page?: WhitePage) {
58 72
   const toolbars = [
59 73
     {
60 74
       icon: PointerIcon,
61 75
       name: 'pointer',
62
-      tooltipText: 'Pointer'
76
+      tooltipText: 'Pointer',
77
+      draggable: true
63 78
     },
64 79
     {
65 80
       icon: DeleteIcon,
66 81
       name: 'delete',
67 82
       tooltipText: 'Delete'
68 83
     },
69
-    {
70
-      name: 'separator',
71
-      tooltipText: ''
72
-    },
73 84
     rectMarkerToolbarItem,
74 85
     coverMarkerToolbarItem,
75 86
     highlightMarkerToolbarItem,
76 87
     lineMarkerToolbarItem,
77 88
     arrowMarkerToolbarItem,
78
-    textMarkerToolbarItem,
79
-    {
80
-      name: 'separator',
81
-      tooltipText: ''
82
-    }
89
+    textMarkerToolbarItem
83 90
   ];
84 91
 
85 92
   if (!page) {
@@ -94,11 +101,5 @@ export function getToolbars(page?: WhitePage) {
94 101
     );
95 102
   }
96 103
 
97
-  toolbars.push({
98
-    icon: CloseIcon,
99
-    name: 'close',
100
-    tooltipText: 'Close'
101
-  });
102
-
103 104
   return toolbars;
104 105
 }

+ 4
- 0
src/utils/types.ts Ver arquivo

@@ -8,3 +8,7 @@ export type Source = {
8 8
   // 需要展示的图片地址
9 9
   imgSrc?: string;
10 10
 };
11
+
12
+export function isNil(mayBeNil: any) {
13
+  return mayBeNil === null || mayBeNil === undefined;
14
+}

+ 2
- 0
src/whiteboard/AbstractWhiteboard/index.less Ver arquivo

@@ -1,4 +1,6 @@
1 1
 .fcw-board {
2
+  position: relative;
3
+
2 4
   // 设置需要元素的绝对布局
3 5
   &-imgs,
4 6
   &-pages,

+ 2
- 0
src/whiteboard/AbstractWhiteboard/index.ts Ver arquivo

@@ -19,6 +19,7 @@ export abstract class AbstractWhiteboard {
19 19
   /** 元素 */
20 20
   // 如果传入的是图片地址,则需要挂载到该 Target 元素下
21 21
   target: HTMLDivElement;
22
+  imgEles: HTMLImageElement[] = [];
22 23
   imgsContainer: HTMLDivElement;
23 24
   pagesContainer: HTMLDivElement;
24 25
 
@@ -169,6 +170,7 @@ export abstract class AbstractWhiteboard {
169 170
       imgEle.src = source;
170 171
       imgEle.alt = 'Siema image';
171 172
 
173
+      this.imgEles.push(imgEle);
172 174
       this.imgsContainer.appendChild(imgEle);
173 175
     });
174 176
 

+ 1
- 2
src/whiteboard/MirrorWhiteboard/index.ts Ver arquivo

@@ -66,7 +66,7 @@ export class MirrorWhiteboard extends AbstractWhiteboard {
66 66
 
67 67
   /** 响应获取到的快照事件 */
68 68
   private applySnap(snap: WhiteboardSnap) {
69
-    const { id, sources, pageIds, visiblePageIndex } = snap;
69
+    const { id, sources, pageIds } = snap;
70 70
 
71 71
     if (!this.isInitialized && !this.isSyncing) {
72 72
       this.id = id;
@@ -96,7 +96,6 @@ export class MirrorWhiteboard extends AbstractWhiteboard {
96 96
       this.initSiema();
97 97
       this.isInitialized = true;
98 98
       this.isSyncing = false;
99
-      this.onPageChange(visiblePageIndex);
100 99
     }
101 100
 
102 101
     // 如果已经初始化完毕,则进行状态同步

+ 119
- 0
src/whiteboard/ReplayWhiteboard/index.ts Ver arquivo

@@ -0,0 +1,119 @@
1
+import { MirrorWhiteboard } from '../MirrorWhiteboard';
2
+import { SyncEvent } from '../../event/SyncEvent';
3
+import { EventHub } from 'fc-whiteboard/src/event/EventHub';
4
+
5
+// 窗口大小为一分钟
6
+const windowSize = 60 * 1000;
7
+const duration = 0.05;
8
+
9
+/** 根据某个时间点,获取一系列的 Event,获取的是绝对时间;这里的 startTime 和 endTime 都是当初记录时候关联的绝对时间 */
10
+export type onLoadFunc = (startTime: number, endTime: number) => Promise<SyncEvent[]>;
11
+
12
+export class ReplayWhiteboard extends MirrorWhiteboard {
13
+  leftEvents: SyncEvent[] = [];
14
+
15
+  // 记录开始时间
16
+  recordStartTime: number;
17
+  // 当前的相对时间
18
+  currentRelativeTime: number = 0;
19
+  loadedRelativeTime: number = -1;
20
+
21
+  // Inner Handler
22
+  interval: any;
23
+  loadingLock: boolean = false;
24
+  seekingLock: boolean = false;
25
+  onLoad: onLoadFunc;
26
+
27
+  /** Getter & Setter */
28
+  /** 设置录播相关的上下文信息 */
29
+  setContext(recordStartTime: number, onLoad: onLoadFunc) {
30
+    this.recordStartTime = recordStartTime;
31
+    this.onLoad = onLoad;
32
+
33
+    if (this.interval) {
34
+      clearInterval(this.interval);
35
+    }
36
+
37
+    this.interval = setInterval(() => {
38
+      this.seek();
39
+    }, duration * 1000);
40
+  }
41
+
42
+  setCurrentRelativeTime(time: number) {
43
+    this.currentRelativeTime = time;
44
+    this.loadedRelativeTime = this.currentRelativeTime - 1;
45
+  }
46
+
47
+  /** 扩展父类的关闭函数 */
48
+  close() {
49
+    super.close();
50
+
51
+    if (this.interval) {
52
+      clearInterval(this.interval);
53
+    }
54
+  }
55
+
56
+  protected init() {
57
+    this.eventHub = new EventHub();
58
+
59
+    super.init();
60
+  }
61
+
62
+  /** 加载全部的事件 */
63
+  private loadEvents() {
64
+    if (this.onLoad && !this.loadingLock) {
65
+      this.loadingLock = true;
66
+
67
+      const startTime = this.recordStartTime + this.currentRelativeTime;
68
+      const endTime = startTime + windowSize;
69
+
70
+      this.onLoad(startTime, endTime)
71
+        .then(events => {
72
+          this.loadedRelativeTime = this.currentRelativeTime;
73
+          this.leftEvents.push(...(events || []));
74
+        })
75
+        .finally(() => {
76
+          this.loadingLock = false;
77
+        });
78
+    }
79
+  }
80
+
81
+  /** 回调,定期更新时间,执行操作 */
82
+  private seek = () => {
83
+    if (this.seekingLock) {
84
+      return;
85
+    }
86
+    this.seekingLock = true;
87
+    // 如果已经进入到了加载完毕的状态,则
88
+    if (this.loadedRelativeTime < this.currentRelativeTime) {
89
+      this.loadEvents();
90
+    }
91
+
92
+    // 获取需要回放的 Events,即所有小于当前时间点的 Events
93
+
94
+    const waitingEvents: SyncEvent[] = [];
95
+    const leftEvents: SyncEvent[] = [];
96
+
97
+    this.leftEvents.forEach((e, i) => {
98
+      if (e.timestamp && e.timestamp < this.currentRelativeTime + this.recordStartTime) {
99
+        waitingEvents.push(e);
100
+      } else {
101
+        leftEvents.push(e);
102
+      }
103
+    });
104
+
105
+    this.leftEvents = leftEvents;
106
+
107
+    waitingEvents.forEach(e => {
108
+      this.perform(e);
109
+    });
110
+
111
+    this.currentRelativeTime += duration;
112
+    this.seekingLock = false;
113
+  };
114
+
115
+  /** 执行事件回放操作 */
116
+  private perform = (e: SyncEvent) => {
117
+    this.eventHub!.emit('sync', e);
118
+  };
119
+}

+ 9
- 4
src/whiteboard/WhitePage/index.ts Ver arquivo

@@ -9,6 +9,7 @@ import { WhitepageSnap } from '../AbstractWhiteboard/snap';
9 9
 import { AbstractWhiteboard } from '../AbstractWhiteboard/index';
10 10
 
11 11
 import './index.less';
12
+import { ToolbarItem } from 'fc-whiteboard/src/toolbar/ToolbarItem';
12 13
 
13 14
 const prefix = 'fcw-page';
14 15
 
@@ -34,8 +35,11 @@ export class WhitePage {
34 35
     {
35 36
       mode,
36 37
       whiteboard,
37
-      parentContainer
38
-    }: { mode?: Mode; whiteboard?: AbstractWhiteboard; parentContainer?: HTMLDivElement } = {}
38
+      parentContainer,
39
+      extraToolbarItems
40
+    }: Partial<WhitePage> & {
41
+      extraToolbarItems?: ToolbarItem[];
42
+    } = {}
39 43
   ) {
40 44
     if (mode) {
41 45
       this.mode = mode;
@@ -46,7 +50,7 @@ export class WhitePage {
46 50
     this.initSource(source);
47 51
 
48 52
     if (this.mode === 'master') {
49
-      this.initMaster();
53
+      this.initMaster(extraToolbarItems);
50 54
     }
51 55
 
52 56
     if (this.mode === 'mirror') {
@@ -126,13 +130,14 @@ export class WhitePage {
126 130
   }
127 131
 
128 132
   /** 以 Master 模式启动 */
129
-  protected initMaster() {
133
+  protected initMaster(extraToolbarItems?: ToolbarItem[]) {
130 134
     if (this.whiteboard) {
131 135
       // 对于 WhitePage 中加载的 Drawboard,必须是传入自身可控的 Image 元素
132 136
       this.drawboard = new Drawboard(
133 137
         { imgEle: this.target },
134 138
         {
135 139
           page: this,
140
+          extraToolbarItems,
136 141
           onChange: ev => this.whiteboard!.emit(ev)
137 142
         }
138 143
       );

+ 27
- 22
src/whiteboard/Whiteboard/index.ts Ver arquivo

@@ -2,6 +2,7 @@ import { WhitePage } from '../WhitePage/index';
2 2
 import { createDivWithClassName } from '../../utils/dom';
3 3
 import { AbstractWhiteboard } from '../AbstractWhiteboard/index';
4 4
 import { Mode } from '../../utils/types';
5
+import { separatorToolbarItem } from '../../toolbar/toolbar-items';
5 6
 
6 7
 const LeftArrowIcon = require('../../assets/bx-left-arrow.svg');
7 8
 const RightArrowIcon = require('../../assets/bx-right-arrow.svg');
@@ -25,6 +26,30 @@ export class Whiteboard extends AbstractWhiteboard {
25 26
 
26 27
   /** 以主模式启动 */
27 28
   private initMaster() {
29
+    // 初始化控制节点
30
+    const prevToolbarItem = {
31
+      icon: LeftArrowIcon,
32
+      name: 'prev-flip-arrow',
33
+      tooltipText: 'Prev',
34
+      onClick: () => {
35
+        const nextPageIndex =
36
+          this.visiblePageIndex + 1 > this.pages.length - 1 ? 0 : this.visiblePageIndex + 1;
37
+        this.onPageChange(nextPageIndex);
38
+      }
39
+    };
40
+
41
+    const nextToolbarItem = {
42
+      icon: RightArrowIcon,
43
+      name: 'next-flip-arrow',
44
+      tooltipText: 'Next',
45
+      onClick: () => {
46
+        const nextPageIndex =
47
+          this.visiblePageIndex - 1 < 0 ? this.pages.length - 1 : this.visiblePageIndex - 1;
48
+
49
+        this.onPageChange(nextPageIndex);
50
+      }
51
+    };
52
+
28 53
     // 初始化所有的 WhitePages
29 54
     this.sources.forEach(source => {
30 55
       const page = new WhitePage(
@@ -32,7 +57,8 @@ export class Whiteboard extends AbstractWhiteboard {
32 57
         {
33 58
           mode: this.mode,
34 59
           whiteboard: this,
35
-          parentContainer: this.pagesContainer
60
+          parentContainer: this.pagesContainer,
61
+          extraToolbarItems: [separatorToolbarItem, prevToolbarItem, nextToolbarItem]
36 62
         }
37 63
       );
38 64
 
@@ -43,27 +69,6 @@ export class Whiteboard extends AbstractWhiteboard {
43 69
     });
44 70
 
45 71
     this.initSiema();
46
-
47
-    // 初始化控制节点
48
-    const controller = createDivWithClassName(`${prefix}-controller`, this.target);
49
-
50
-    const prevEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
51
-    prevEle.innerHTML = LeftArrowIcon;
52
-
53
-    const nextEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
54
-    nextEle.innerHTML = RightArrowIcon;
55
-
56
-    nextEle!.addEventListener('click', () => {
57
-      const nextPageIndex =
58
-        this.visiblePageIndex + 1 > this.pages.length - 1 ? 0 : this.visiblePageIndex + 1;
59
-      this.onPageChange(nextPageIndex);
60
-    });
61
-    prevEle!.addEventListener('click', () => {
62
-      const nextPageIndex =
63
-        this.visiblePageIndex - 1 < 0 ? this.pages.length - 1 : this.visiblePageIndex - 1;
64
-
65
-      this.onPageChange(nextPageIndex);
66
-    });
67 72
   }
68 73
 
69 74
   /** 响应页面切换的事件 */