js-draw 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
  2. package/CHANGELOG.md +13 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +1 -1
  5. package/dist/src/Color4.js +5 -1
  6. package/dist/src/Editor.d.ts +11 -2
  7. package/dist/src/Editor.js +66 -33
  8. package/dist/src/EditorImage.d.ts +28 -3
  9. package/dist/src/EditorImage.js +109 -18
  10. package/dist/src/EventDispatcher.d.ts +4 -3
  11. package/dist/src/SVGLoader.d.ts +1 -0
  12. package/dist/src/SVGLoader.js +15 -1
  13. package/dist/src/Viewport.d.ts +8 -3
  14. package/dist/src/Viewport.js +15 -8
  15. package/dist/src/components/AbstractComponent.d.ts +6 -1
  16. package/dist/src/components/AbstractComponent.js +15 -2
  17. package/dist/src/components/ImageBackground.d.ts +42 -0
  18. package/dist/src/components/ImageBackground.js +139 -0
  19. package/dist/src/components/ImageComponent.js +2 -0
  20. package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
  21. package/dist/src/components/builders/ArrowBuilder.js +43 -40
  22. package/dist/src/components/builders/LineBuilder.d.ts +3 -1
  23. package/dist/src/components/builders/LineBuilder.js +25 -28
  24. package/dist/src/components/builders/RectangleBuilder.js +1 -1
  25. package/dist/src/components/lib.d.ts +2 -1
  26. package/dist/src/components/lib.js +2 -1
  27. package/dist/src/components/localization.d.ts +2 -0
  28. package/dist/src/components/localization.js +2 -0
  29. package/dist/src/localizations/es.js +1 -1
  30. package/dist/src/math/Mat33.js +43 -5
  31. package/dist/src/math/Path.d.ts +5 -0
  32. package/dist/src/math/Path.js +80 -28
  33. package/dist/src/math/Vec3.js +1 -1
  34. package/dist/src/rendering/Display.js +1 -1
  35. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
  36. package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
  37. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  38. package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
  39. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  40. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  41. package/dist/src/testing/sendTouchEvent.d.ts +6 -0
  42. package/dist/src/testing/sendTouchEvent.js +26 -0
  43. package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
  44. package/dist/src/toolbar/HTMLToolbar.js +127 -15
  45. package/dist/src/toolbar/IconProvider.d.ts +2 -0
  46. package/dist/src/toolbar/IconProvider.js +45 -2
  47. package/dist/src/toolbar/localization.d.ts +5 -0
  48. package/dist/src/toolbar/localization.js +5 -0
  49. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
  50. package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
  51. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
  52. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
  53. package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
  54. package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
  55. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
  56. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
  57. package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
  58. package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
  59. package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
  60. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  61. package/dist/src/toolbar/widgets/lib.js +1 -0
  62. package/dist/src/tools/Eraser.js +5 -2
  63. package/dist/src/tools/PanZoom.js +12 -0
  64. package/dist/src/tools/PasteHandler.js +2 -2
  65. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
  66. package/dist/src/tools/SelectionTool/Selection.js +3 -2
  67. package/dist/src/tools/SelectionTool/SelectionTool.js +5 -1
  68. package/package.json +1 -1
  69. package/src/Color4.test.ts +6 -0
  70. package/src/Color4.ts +6 -1
  71. package/src/Editor.loadFrom.test.ts +24 -0
  72. package/src/Editor.ts +73 -39
  73. package/src/EditorImage.ts +136 -21
  74. package/src/EventDispatcher.ts +4 -1
  75. package/src/SVGLoader.ts +12 -1
  76. package/src/Viewport.ts +17 -7
  77. package/src/components/AbstractComponent.ts +17 -1
  78. package/src/components/ImageBackground.test.ts +35 -0
  79. package/src/components/ImageBackground.ts +176 -0
  80. package/src/components/ImageComponent.ts +2 -0
  81. package/src/components/builders/ArrowBuilder.ts +44 -41
  82. package/src/components/builders/LineBuilder.ts +26 -28
  83. package/src/components/builders/RectangleBuilder.ts +1 -1
  84. package/src/components/lib.ts +2 -0
  85. package/src/components/localization.ts +4 -0
  86. package/src/localizations/es.ts +8 -0
  87. package/src/math/Mat33.test.ts +47 -3
  88. package/src/math/Mat33.ts +47 -5
  89. package/src/math/Path.ts +87 -28
  90. package/src/math/Vec3.test.ts +4 -0
  91. package/src/math/Vec3.ts +1 -1
  92. package/src/rendering/Display.ts +1 -1
  93. package/src/rendering/renderers/AbstractRenderer.ts +20 -3
  94. package/src/rendering/renderers/CanvasRenderer.ts +17 -4
  95. package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
  96. package/src/rendering/renderers/SVGRenderer.ts +8 -1
  97. package/src/testing/sendTouchEvent.ts +43 -0
  98. package/src/toolbar/HTMLToolbar.ts +164 -16
  99. package/src/toolbar/IconProvider.ts +47 -2
  100. package/src/toolbar/localization.ts +10 -0
  101. package/src/toolbar/toolbar.css +2 -0
  102. package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
  103. package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
  104. package/src/toolbar/widgets/BaseWidget.ts +34 -2
  105. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
  106. package/src/toolbar/widgets/HandToolWidget.ts +1 -1
  107. package/src/toolbar/widgets/OverflowWidget.css +9 -0
  108. package/src/toolbar/widgets/OverflowWidget.ts +83 -0
  109. package/src/toolbar/widgets/lib.ts +2 -1
  110. package/src/tools/Eraser.test.ts +24 -1
  111. package/src/tools/Eraser.ts +6 -2
  112. package/src/tools/PanZoom.test.ts +267 -23
  113. package/src/tools/PanZoom.ts +15 -1
  114. package/src/tools/PasteHandler.ts +3 -2
  115. package/src/tools/SelectionTool/Selection.ts +3 -2
  116. package/src/tools/SelectionTool/SelectionTool.ts +6 -1
  117. package/src/types.ts +1 -0
@@ -1,6 +1,7 @@
1
1
  import { Bezier } from 'bezier-js';
2
2
  import { toRoundedString, toStringOfSamePrecision } from './rounding';
3
3
  import LineSegment2 from './LineSegment2';
4
+ import Mat33 from './Mat33';
4
5
  import Rect2 from './Rect2';
5
6
  import { Vec2 } from './Vec2';
6
7
  export var PathCommandType;
@@ -143,38 +144,39 @@ export default class Path {
143
144
  }
144
145
  return result;
145
146
  }
147
+ static mapPathCommand(part, mapping) {
148
+ switch (part.kind) {
149
+ case PathCommandType.MoveTo:
150
+ case PathCommandType.LineTo:
151
+ return {
152
+ kind: part.kind,
153
+ point: mapping(part.point),
154
+ };
155
+ break;
156
+ case PathCommandType.CubicBezierTo:
157
+ return {
158
+ kind: part.kind,
159
+ controlPoint1: mapping(part.controlPoint1),
160
+ controlPoint2: mapping(part.controlPoint2),
161
+ endPoint: mapping(part.endPoint),
162
+ };
163
+ break;
164
+ case PathCommandType.QuadraticBezierTo:
165
+ return {
166
+ kind: part.kind,
167
+ controlPoint: mapping(part.controlPoint),
168
+ endPoint: mapping(part.endPoint),
169
+ };
170
+ break;
171
+ }
172
+ const exhaustivenessCheck = part;
173
+ return exhaustivenessCheck;
174
+ }
146
175
  mapPoints(mapping) {
147
176
  const startPoint = mapping(this.startPoint);
148
177
  const newParts = [];
149
- let exhaustivenessCheck;
150
178
  for (const part of this.parts) {
151
- switch (part.kind) {
152
- case PathCommandType.MoveTo:
153
- case PathCommandType.LineTo:
154
- newParts.push({
155
- kind: part.kind,
156
- point: mapping(part.point),
157
- });
158
- break;
159
- case PathCommandType.CubicBezierTo:
160
- newParts.push({
161
- kind: part.kind,
162
- controlPoint1: mapping(part.controlPoint1),
163
- controlPoint2: mapping(part.controlPoint2),
164
- endPoint: mapping(part.endPoint),
165
- });
166
- break;
167
- case PathCommandType.QuadraticBezierTo:
168
- newParts.push({
169
- kind: part.kind,
170
- controlPoint: mapping(part.controlPoint),
171
- endPoint: mapping(part.endPoint),
172
- });
173
- break;
174
- default:
175
- exhaustivenessCheck = part;
176
- return exhaustivenessCheck;
177
- }
179
+ newParts.push(Path.mapPathCommand(part, mapping));
178
180
  }
179
181
  return new Path(startPoint, newParts);
180
182
  }
@@ -329,6 +331,56 @@ export default class Path {
329
331
  path: this,
330
332
  };
331
333
  }
334
+ /**
335
+ * @returns a Path that, when rendered, looks roughly equivalent to the given path.
336
+ */
337
+ static visualEquivalent(renderablePath, visibleRect) {
338
+ var _a, _b;
339
+ const path = Path.fromRenderable(renderablePath);
340
+ const strokeWidth = (_b = (_a = renderablePath.style.stroke) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0;
341
+ const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
342
+ // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
343
+ const expandedRect = visibleRect.grownBy(strokeWidth)
344
+ .transformedBoundingBox(Mat33.scaling2D(2, visibleRect.center));
345
+ // TODO: Handle simplifying very small paths.
346
+ if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
347
+ return renderablePath;
348
+ }
349
+ const parts = [];
350
+ let startPoint = path.startPoint;
351
+ for (const part of path.parts) {
352
+ const partBBox = Path.computeBBoxForSegment(startPoint, part).grownBy(strokeWidth);
353
+ let endPoint;
354
+ if (part.kind === PathCommandType.LineTo || part.kind === PathCommandType.MoveTo) {
355
+ endPoint = part.point;
356
+ }
357
+ else {
358
+ endPoint = part.endPoint;
359
+ }
360
+ const intersectsVisible = partBBox.intersects(visibleRect);
361
+ if (intersectsVisible) {
362
+ // TODO: Can we trim parts of paths that intersect the visible rectangle?
363
+ parts.push(part);
364
+ }
365
+ else if (onlyStroked || part.kind === PathCommandType.MoveTo) {
366
+ // We're stroking (not filling) and the path doesn't intersect the bounding box.
367
+ // Don't draw it, but preserve the endpoints.
368
+ parts.push({
369
+ kind: PathCommandType.MoveTo,
370
+ point: endPoint,
371
+ });
372
+ }
373
+ else {
374
+ // Otherwise, we may be filling. Try to roughly preserve the filled region.
375
+ parts.push({
376
+ kind: PathCommandType.LineTo,
377
+ point: endPoint,
378
+ });
379
+ }
380
+ startPoint = endPoint;
381
+ }
382
+ return new Path(path.startPoint, parts).toRenderable(renderablePath.style);
383
+ }
332
384
  toString(useNonAbsCommands) {
333
385
  if (this.cachedStringVersion) {
334
386
  return this.cachedStringVersion;
@@ -65,7 +65,7 @@ export default class Vec3 {
65
65
  return Vec3.of(this.x + v.x, this.y + v.y, this.z + v.z);
66
66
  }
67
67
  minus(v) {
68
- return this.plus(v.times(-1));
68
+ return Vec3.of(this.x - v.x, this.y - v.y, this.z - v.z);
69
69
  }
70
70
  dot(other) {
71
71
  return this.x * other.x + this.y * other.y + this.z * other.z;
@@ -70,7 +70,7 @@ export default class Display {
70
70
  },
71
71
  blockResolution: cacheBlockResolution,
72
72
  cacheSize: 600 * 600 * 4 * 90,
73
- maxScale: 1.4,
73
+ maxScale: 1.3,
74
74
  // Require about 20 strokes with 4 parts each to cache an image in one of the
75
75
  // parts of the cache grid.
76
76
  minProportionalRenderTimePerCache: 20 * 4,
@@ -1,3 +1,4 @@
1
+ import Color4 from '../../Color4';
1
2
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
3
  import Mat33 from '../../math/Mat33';
3
4
  import Path, { PathCommand } from '../../math/Path';
@@ -41,8 +42,19 @@ export default abstract class AbstractRenderer {
41
42
  private flushPath;
42
43
  drawPath(path: RenderablePathSpec): void;
43
44
  drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
45
+ fillRect(rect: Rect2, fill: Color4): void;
44
46
  startObject(_boundingBox: Rect2, _clip?: boolean): void;
45
- endObject(_loaderData?: LoadSaveDataTable): void;
47
+ /**
48
+ * Notes the end of an object.
49
+ * @param _loaderData - a map from strings to JSON-ifyable objects
50
+ * and contains properties attached to the object by whatever loader loaded the image. This
51
+ * is used to preserve attributes not supported by js-draw when loading/saving an image.
52
+ * Renderers may ignore this.
53
+ *
54
+ * @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object.
55
+ * Renderers may ignore this.
56
+ */
57
+ endObject(_loaderData?: LoadSaveDataTable, _objectTags?: string[]): void;
46
58
  protected getNestingLevel(): number;
47
59
  abstract drawPoints(...points: Point2[]): void;
48
60
  canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
@@ -64,19 +64,34 @@ export default class AbstractRenderer {
64
64
  this.currentPaths.push(path);
65
65
  }
66
66
  }
67
- // Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
67
+ // Strokes a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
68
68
  // This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
69
69
  drawRect(rect, lineWidth, lineFill) {
70
70
  const path = Path.fromRect(rect, lineWidth);
71
71
  this.drawPath(path.toRenderable(lineFill));
72
72
  }
73
- // Note the start/end of an object with the given bounding box.
73
+ // Fills a rectangle.
74
+ fillRect(rect, fill) {
75
+ const path = Path.fromRect(rect);
76
+ this.drawPath(path.toRenderable({ fill }));
77
+ }
78
+ // Note the start of an object with the given bounding box.
74
79
  // Renderers are not required to support [clip]
75
80
  startObject(_boundingBox, _clip) {
76
81
  this.currentPaths = [];
77
82
  this.objectLevel++;
78
83
  }
79
- endObject(_loaderData) {
84
+ /**
85
+ * Notes the end of an object.
86
+ * @param _loaderData - a map from strings to JSON-ifyable objects
87
+ * and contains properties attached to the object by whatever loader loaded the image. This
88
+ * is used to preserve attributes not supported by js-draw when loading/saving an image.
89
+ * Renderers may ignore this.
90
+ *
91
+ * @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object.
92
+ * Renderers may ignore this.
93
+ */
94
+ endObject(_loaderData, _objectTags) {
80
95
  // Render the paths all at once
81
96
  this.flushPath();
82
97
  this.currentPaths = null;
@@ -10,6 +10,7 @@ export default class CanvasRenderer extends AbstractRenderer {
10
10
  private ctx;
11
11
  private ignoreObjectsAboveLevel;
12
12
  private ignoringObject;
13
+ private currentObjectBBox;
13
14
  private minSquareCurveApproxDist;
14
15
  private minRenderSizeAnyDimen;
15
16
  private minRenderSizeBothDimens;
@@ -30,7 +31,7 @@ export default class CanvasRenderer extends AbstractRenderer {
30
31
  drawText(text: string, transform: Mat33, style: TextStyle): void;
31
32
  drawImage(image: RenderableImage): void;
32
33
  private clipLevels;
33
- startObject(boundingBox: Rect2, clip: boolean): void;
34
+ startObject(boundingBox: Rect2, clip?: boolean): void;
34
35
  endObject(): void;
35
36
  drawPoints(...points: Point2[]): void;
36
37
  isTooSmallToRender(rect: Rect2): boolean;
@@ -1,5 +1,6 @@
1
1
  import Color4 from '../../Color4';
2
2
  import TextComponent from '../../components/TextComponent';
3
+ import Path from '../../math/Path';
3
4
  import { Vec2 } from '../../math/Vec2';
4
5
  import AbstractRenderer from './AbstractRenderer';
5
6
  export default class CanvasRenderer extends AbstractRenderer {
@@ -8,6 +9,7 @@ export default class CanvasRenderer extends AbstractRenderer {
8
9
  this.ctx = ctx;
9
10
  this.ignoreObjectsAboveLevel = null;
10
11
  this.ignoringObject = false;
12
+ this.currentObjectBBox = null;
11
13
  this.clipLevels = [];
12
14
  this.setDraftMode(false);
13
15
  }
@@ -43,8 +45,8 @@ export default class CanvasRenderer extends AbstractRenderer {
43
45
  }
44
46
  else {
45
47
  this.minSquareCurveApproxDist = 0.5;
46
- this.minRenderSizeBothDimens = 0.3;
47
- this.minRenderSizeAnyDimen = 1e-5;
48
+ this.minRenderSizeBothDimens = 0.2;
49
+ this.minRenderSizeAnyDimen = 1e-6;
48
50
  }
49
51
  }
50
52
  displaySize() {
@@ -106,9 +108,15 @@ export default class CanvasRenderer extends AbstractRenderer {
106
108
  }
107
109
  }
108
110
  drawPath(path) {
111
+ var _a;
109
112
  if (this.ignoringObject) {
110
113
  return;
111
114
  }
115
+ // If part of a huge object, it might be worth trimming the path
116
+ if ((_a = this.currentObjectBBox) === null || _a === void 0 ? void 0 : _a.containsRect(this.getViewport().visibleRect)) {
117
+ // Try to trim/remove parts of the path outside of the bounding box.
118
+ path = Path.visualEquivalent(path, this.getViewport().visibleRect);
119
+ }
112
120
  super.drawPath(path);
113
121
  }
114
122
  drawText(text, transform, style) {
@@ -140,6 +148,7 @@ export default class CanvasRenderer extends AbstractRenderer {
140
148
  this.ignoringObject = true;
141
149
  }
142
150
  super.startObject(boundingBox);
151
+ this.currentObjectBBox = boundingBox;
143
152
  if (!this.ignoringObject && clip) {
144
153
  this.clipLevels.push(this.objectLevel);
145
154
  this.ctx.save();
@@ -158,6 +167,7 @@ export default class CanvasRenderer extends AbstractRenderer {
158
167
  this.clipLevels.pop();
159
168
  }
160
169
  }
170
+ this.currentObjectBBox = null;
161
171
  super.endObject();
162
172
  // If exiting an object with a too-small-to-draw bounding box,
163
173
  if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
@@ -28,7 +28,7 @@ export default class SVGRenderer extends AbstractRenderer {
28
28
  drawText(text: string, transform: Mat33, style: TextStyle): void;
29
29
  drawImage(image: RenderableImage): void;
30
30
  startObject(boundingBox: Rect2): void;
31
- endObject(loaderData?: LoadSaveDataTable): void;
31
+ endObject(loaderData?: LoadSaveDataTable, elemClassNames?: string[]): void;
32
32
  private unimplementedMessage;
33
33
  protected beginPath(_startPoint: Point2): void;
34
34
  protected endPath(_style: RenderingStyle): void;
@@ -215,8 +215,8 @@ export default class SVGRenderer extends AbstractRenderer {
215
215
  this.textParentStyle = null;
216
216
  this.objectElems = [];
217
217
  }
218
- endObject(loaderData) {
219
- var _a;
218
+ endObject(loaderData, elemClassNames) {
219
+ var _a, _b;
220
220
  super.endObject(loaderData);
221
221
  // Don't extend paths across objects
222
222
  this.addPathToSVG();
@@ -237,6 +237,12 @@ export default class SVGRenderer extends AbstractRenderer {
237
237
  }
238
238
  }
239
239
  }
240
+ // Add class names to the object, if given.
241
+ if (elemClassNames) {
242
+ for (const elem of (_b = this.objectElems) !== null && _b !== void 0 ? _b : []) {
243
+ elem.classList.add(...elemClassNames);
244
+ }
245
+ }
240
246
  }
241
247
  // Not implemented -- use drawPath instead.
242
248
  unimplementedMessage() { throw new Error('Not implemenented!'); }
@@ -0,0 +1,6 @@
1
+ import Editor from '../Editor';
2
+ import { Vec2 } from '../math/Vec2';
3
+ import Pointer from '../Pointer';
4
+ import { InputEvtType } from '../types';
5
+ declare const sendTouchEvent: (editor: Editor, eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, screenPos: Vec2, allOtherPointers?: Pointer[]) => Pointer;
6
+ export default sendTouchEvent;
@@ -0,0 +1,26 @@
1
+ import Pointer, { PointerDevice } from '../Pointer';
2
+ import { InputEvtType } from '../types';
3
+ const sendTouchEvent = (editor, eventType, screenPos, allOtherPointers) => {
4
+ const canvasPos = editor.viewport.screenToCanvas(screenPos);
5
+ let ptrId = 0;
6
+ let maxPtrId = 0;
7
+ // Get a unique ID for the main pointer
8
+ // (try to use id=0, but don't use it if it's already in use).
9
+ for (const pointer of allOtherPointers !== null && allOtherPointers !== void 0 ? allOtherPointers : []) {
10
+ maxPtrId = Math.max(pointer.id, maxPtrId);
11
+ if (pointer.id === ptrId) {
12
+ ptrId = maxPtrId + 1;
13
+ }
14
+ }
15
+ const mainPointer = Pointer.ofCanvasPoint(canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch);
16
+ editor.toolController.dispatchInputEvent({
17
+ kind: eventType,
18
+ allPointers: [
19
+ ...(allOtherPointers !== null && allOtherPointers !== void 0 ? allOtherPointers : []),
20
+ mainPointer,
21
+ ],
22
+ current: mainPointer,
23
+ });
24
+ return mainPointer;
25
+ };
26
+ export default sendTouchEvent;
@@ -12,12 +12,19 @@ export default class HTMLToolbar {
12
12
  private editor;
13
13
  private localizationTable;
14
14
  private container;
15
- private widgets;
15
+ private resizeObserver;
16
+ private listeners;
17
+ private widgetsById;
18
+ private widgetList;
19
+ private overflowWidget;
16
20
  private static colorisStarted;
17
21
  private updateColoris;
18
22
  /** @internal */
19
23
  constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
20
24
  setupColorPickers(): void;
25
+ private reLayoutQueued;
26
+ private queueReLayout;
27
+ private reLayout;
21
28
  /**
22
29
  * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
23
30
  * (i.e. its `addTo` method should not have been called).
@@ -63,17 +70,33 @@ export default class HTMLToolbar {
63
70
  *
64
71
  * @return The added button.
65
72
  */
66
- addActionButton(title: string | ActionButtonIcon, command: () => void): BaseWidget;
73
+ addActionButton(title: string | ActionButtonIcon, command: () => void, mustBeToplevel?: boolean): BaseWidget;
67
74
  addUndoRedoButtons(): void;
68
75
  addDefaultToolWidgets(): void;
69
76
  addDefaultActionButtons(): void;
77
+ /**
78
+ * Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
79
+ * that this widget is in the correct space (if shown).
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * toolbar.addDefaultToolWidgets();
84
+ * toolbar.addOverflowWidget();
85
+ * toolbar.addDefaultActionButtons();
86
+ * ```
87
+ * shows the overflow widget between the default tool widgets and the default action buttons,
88
+ * if shown.
89
+ */
90
+ addOverflowWidget(): void;
70
91
  /**
71
92
  * Adds both the default tool widgets and action buttons. Equivalent to
72
93
  * ```ts
73
94
  * toolbar.addDefaultToolWidgets();
95
+ * toolbar.addOverflowWidget();
74
96
  * toolbar.addDefaultActionButtons();
75
97
  * ```
76
98
  */
77
99
  addDefaults(): void;
100
+ remove(): void;
78
101
  }
79
102
  export {};
@@ -14,14 +14,21 @@ import TextToolWidget from './widgets/TextToolWidget';
14
14
  import HandToolWidget from './widgets/HandToolWidget';
15
15
  import ActionButtonWidget from './widgets/ActionButtonWidget';
16
16
  import InsertImageWidget from './widgets/InsertImageWidget';
17
+ import DocumentPropertiesWidget from './widgets/DocumentPropertiesWidget';
18
+ import OverflowWidget from './widgets/OverflowWidget';
17
19
  export const toolbarCSSPrefix = 'toolbar-';
18
20
  export default class HTMLToolbar {
19
21
  /** @internal */
20
22
  constructor(editor, parent, localizationTable = defaultToolbarLocalization) {
21
23
  this.editor = editor;
22
24
  this.localizationTable = localizationTable;
23
- this.widgets = {};
25
+ this.listeners = [];
26
+ this.widgetsById = {};
27
+ this.widgetList = [];
28
+ // Widget to toggle overflow menu.
29
+ this.overflowWidget = null;
24
30
  this.updateColoris = null;
31
+ this.reLayoutQueued = false;
25
32
  this.container = document.createElement('div');
26
33
  this.container.classList.add(`${toolbarCSSPrefix}root`);
27
34
  this.container.setAttribute('role', 'toolbar');
@@ -31,6 +38,15 @@ export default class HTMLToolbar {
31
38
  HTMLToolbar.colorisStarted = true;
32
39
  }
33
40
  this.setupColorPickers();
41
+ if ('ResizeObserver' in window) {
42
+ this.resizeObserver = new ResizeObserver((_entries) => {
43
+ this.reLayout();
44
+ });
45
+ this.resizeObserver.observe(this.container);
46
+ }
47
+ else {
48
+ console.warn('ResizeObserver not supported. Toolbar will not resize.');
49
+ }
34
50
  }
35
51
  // @internal
36
52
  setupColorPickers() {
@@ -80,20 +96,84 @@ export default class HTMLToolbar {
80
96
  initColoris();
81
97
  }
82
98
  };
83
- this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
99
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
84
100
  if (event.kind !== EditorEventType.ColorPickerToggled) {
85
101
  return;
86
102
  }
87
103
  // Show/hide the overlay. Making the overlay visible gives users a surface to click
88
104
  // on that shows/hides the color picker.
89
105
  closePickerOverlay.style.display = event.open ? 'block' : 'none';
90
- });
106
+ }));
91
107
  // Add newly-selected colors to the swatch.
92
- this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
108
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
93
109
  if (event.kind === EditorEventType.ColorPickerColorSelected) {
94
110
  addColorToSwatch(event.color.toHexString());
95
111
  }
96
- });
112
+ }));
113
+ }
114
+ queueReLayout() {
115
+ if (!this.reLayoutQueued) {
116
+ this.reLayoutQueued = true;
117
+ requestAnimationFrame(() => this.reLayout());
118
+ }
119
+ }
120
+ reLayout() {
121
+ this.reLayoutQueued = false;
122
+ if (!this.overflowWidget) {
123
+ return;
124
+ }
125
+ const getTotalWidth = (widgetList) => {
126
+ let totalWidth = 0;
127
+ for (const widget of widgetList) {
128
+ if (!widget.isHidden()) {
129
+ totalWidth += widget.getButtonWidth();
130
+ }
131
+ }
132
+ return totalWidth;
133
+ };
134
+ let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
135
+ let shownWidgetWidth = getTotalWidth(this.widgetList) - overflowWidgetsWidth;
136
+ let availableWidth = this.container.clientWidth * 0.87;
137
+ // If on a device that has enough vertical space, allow
138
+ // showing two rows of buttons.
139
+ // TODO: Fix magic numbers
140
+ if (window.innerHeight > availableWidth * 1.75) {
141
+ availableWidth *= 1.75;
142
+ }
143
+ let updatedChildren = false;
144
+ if (shownWidgetWidth + overflowWidgetsWidth <= availableWidth) {
145
+ // Move widgets to the main menu.
146
+ const overflowChildren = this.overflowWidget.clearChildren();
147
+ for (const child of overflowChildren) {
148
+ child.addTo(this.container);
149
+ child.setIsToplevel(true);
150
+ if (!child.isHidden()) {
151
+ shownWidgetWidth += child.getButtonWidth();
152
+ }
153
+ }
154
+ this.overflowWidget.setHidden(true);
155
+ overflowWidgetsWidth = 0;
156
+ updatedChildren = true;
157
+ }
158
+ if (shownWidgetWidth >= availableWidth) {
159
+ // Move widgets to the overflow menu.
160
+ this.overflowWidget.setHidden(false);
161
+ // Start with the rightmost widget, move to the leftmost
162
+ for (let i = this.widgetList.length - 1; i >= 0 && shownWidgetWidth >= availableWidth; i--) {
163
+ const child = this.widgetList[i];
164
+ if (this.overflowWidget.hasAsChild(child)) {
165
+ continue;
166
+ }
167
+ if (child.canBeInOverflowMenu()) {
168
+ shownWidgetWidth -= child.getButtonWidth();
169
+ this.overflowWidget.addToOverflow(child);
170
+ }
171
+ }
172
+ updatedChildren = true;
173
+ }
174
+ if (updatedChildren) {
175
+ this.setupColorPickers();
176
+ }
97
177
  }
98
178
  /**
99
179
  * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
@@ -108,12 +188,17 @@ export default class HTMLToolbar {
108
188
  */
109
189
  addWidget(widget) {
110
190
  // Prevent name collisions
111
- const id = widget.getUniqueIdIn(this.widgets);
191
+ const id = widget.getUniqueIdIn(this.widgetsById);
112
192
  // Add the widget
113
- this.widgets[id] = widget;
193
+ this.widgetsById[id] = widget;
194
+ this.widgetList.push(widget);
114
195
  // Add HTML elements.
115
- widget.addTo(this.container);
196
+ const container = widget.addTo(this.container);
116
197
  this.setupColorPickers();
198
+ // Ensure that the widget gets displayed in the correct
199
+ // place in the toolbar, even if it's removed and re-added.
200
+ container.style.order = `${this.widgetList.length}`;
201
+ this.queueReLayout();
117
202
  }
118
203
  /**
119
204
  * Adds a spacer.
@@ -152,8 +237,8 @@ export default class HTMLToolbar {
152
237
  }
153
238
  serializeState() {
154
239
  const result = {};
155
- for (const widgetId in this.widgets) {
156
- result[widgetId] = this.widgets[widgetId].serializeState();
240
+ for (const widgetId in this.widgetsById) {
241
+ result[widgetId] = this.widgetsById[widgetId].serializeState();
157
242
  }
158
243
  return JSON.stringify(result);
159
244
  }
@@ -164,10 +249,10 @@ export default class HTMLToolbar {
164
249
  deserializeState(state) {
165
250
  const data = JSON.parse(state);
166
251
  for (const widgetId in data) {
167
- if (!(widgetId in this.widgets)) {
252
+ if (!(widgetId in this.widgetsById)) {
168
253
  console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
169
254
  }
170
- this.widgets[widgetId].deserializeFrom(data[widgetId]);
255
+ this.widgetsById[widgetId].deserializeFrom(data[widgetId]);
171
256
  }
172
257
  }
173
258
  /**
@@ -175,7 +260,7 @@ export default class HTMLToolbar {
175
260
  *
176
261
  * @return The added button.
177
262
  */
178
- addActionButton(title, command) {
263
+ addActionButton(title, command, mustBeToplevel = true) {
179
264
  const titleString = typeof title === 'string' ? title : title.label;
180
265
  const widgetId = 'action-button';
181
266
  const makeIcon = () => {
@@ -184,7 +269,7 @@ export default class HTMLToolbar {
184
269
  }
185
270
  return title.icon;
186
271
  };
187
- const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization);
272
+ const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization, mustBeToplevel);
188
273
  this.addWidget(widget);
189
274
  return widget;
190
275
  }
@@ -226,25 +311,52 @@ export default class HTMLToolbar {
226
311
  for (const tool of toolController.getMatchingTools(TextTool)) {
227
312
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
228
313
  }
229
- this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
230
314
  const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
231
315
  if (panZoomTool) {
232
316
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
233
317
  }
318
+ this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
234
319
  }
235
320
  addDefaultActionButtons() {
321
+ this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
236
322
  this.addUndoRedoButtons();
237
323
  }
324
+ /**
325
+ * Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
326
+ * that this widget is in the correct space (if shown).
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * toolbar.addDefaultToolWidgets();
331
+ * toolbar.addOverflowWidget();
332
+ * toolbar.addDefaultActionButtons();
333
+ * ```
334
+ * shows the overflow widget between the default tool widgets and the default action buttons,
335
+ * if shown.
336
+ */
337
+ addOverflowWidget() {
338
+ this.overflowWidget = new OverflowWidget(this.editor, this.localizationTable);
339
+ this.addWidget(this.overflowWidget);
340
+ }
238
341
  /**
239
342
  * Adds both the default tool widgets and action buttons. Equivalent to
240
343
  * ```ts
241
344
  * toolbar.addDefaultToolWidgets();
345
+ * toolbar.addOverflowWidget();
242
346
  * toolbar.addDefaultActionButtons();
243
347
  * ```
244
348
  */
245
349
  addDefaults() {
246
350
  this.addDefaultToolWidgets();
351
+ this.addOverflowWidget();
247
352
  this.addDefaultActionButtons();
248
353
  }
354
+ remove() {
355
+ this.container.remove();
356
+ this.resizeObserver.disconnect();
357
+ for (const listener of this.listeners) {
358
+ listener.remove();
359
+ }
360
+ }
249
361
  }
250
362
  HTMLToolbar.colorisStarted = false;
@@ -57,4 +57,6 @@ export default class IconProvider {
57
57
  makePasteIcon(): IconType;
58
58
  makeDeleteSelectionIcon(): IconType;
59
59
  makeSaveIcon(): IconType;
60
+ makeConfigureDocumentIcon(): IconType;
61
+ makeOverflowIcon(): IconType;
60
62
  }