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.
- package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
- package/CHANGELOG.md +13 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -1
- package/dist/src/Color4.js +5 -1
- package/dist/src/Editor.d.ts +11 -2
- package/dist/src/Editor.js +66 -33
- package/dist/src/EditorImage.d.ts +28 -3
- package/dist/src/EditorImage.js +109 -18
- package/dist/src/EventDispatcher.d.ts +4 -3
- package/dist/src/SVGLoader.d.ts +1 -0
- package/dist/src/SVGLoader.js +15 -1
- package/dist/src/Viewport.d.ts +8 -3
- package/dist/src/Viewport.js +15 -8
- package/dist/src/components/AbstractComponent.d.ts +6 -1
- package/dist/src/components/AbstractComponent.js +15 -2
- package/dist/src/components/ImageBackground.d.ts +42 -0
- package/dist/src/components/ImageBackground.js +139 -0
- package/dist/src/components/ImageComponent.js +2 -0
- package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
- package/dist/src/components/builders/ArrowBuilder.js +43 -40
- package/dist/src/components/builders/LineBuilder.d.ts +3 -1
- package/dist/src/components/builders/LineBuilder.js +25 -28
- package/dist/src/components/builders/RectangleBuilder.js +1 -1
- package/dist/src/components/lib.d.ts +2 -1
- package/dist/src/components/lib.js +2 -1
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/math/Mat33.js +43 -5
- package/dist/src/math/Path.d.ts +5 -0
- package/dist/src/math/Path.js +80 -28
- package/dist/src/math/Vec3.js +1 -1
- package/dist/src/rendering/Display.js +1 -1
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
- package/dist/src/testing/sendTouchEvent.d.ts +6 -0
- package/dist/src/testing/sendTouchEvent.js +26 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
- package/dist/src/toolbar/HTMLToolbar.js +127 -15
- package/dist/src/toolbar/IconProvider.d.ts +2 -0
- package/dist/src/toolbar/IconProvider.js +45 -2
- package/dist/src/toolbar/localization.d.ts +5 -0
- package/dist/src/toolbar/localization.js +5 -0
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
- package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
- package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
- package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
- package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
- package/dist/src/toolbar/widgets/lib.d.ts +1 -0
- package/dist/src/toolbar/widgets/lib.js +1 -0
- package/dist/src/tools/Eraser.js +5 -2
- package/dist/src/tools/PanZoom.js +12 -0
- package/dist/src/tools/PasteHandler.js +2 -2
- package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
- package/dist/src/tools/SelectionTool/Selection.js +3 -2
- package/dist/src/tools/SelectionTool/SelectionTool.js +5 -1
- package/package.json +1 -1
- package/src/Color4.test.ts +6 -0
- package/src/Color4.ts +6 -1
- package/src/Editor.loadFrom.test.ts +24 -0
- package/src/Editor.ts +73 -39
- package/src/EditorImage.ts +136 -21
- package/src/EventDispatcher.ts +4 -1
- package/src/SVGLoader.ts +12 -1
- package/src/Viewport.ts +17 -7
- package/src/components/AbstractComponent.ts +17 -1
- package/src/components/ImageBackground.test.ts +35 -0
- package/src/components/ImageBackground.ts +176 -0
- package/src/components/ImageComponent.ts +2 -0
- package/src/components/builders/ArrowBuilder.ts +44 -41
- package/src/components/builders/LineBuilder.ts +26 -28
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/lib.ts +2 -0
- package/src/components/localization.ts +4 -0
- package/src/localizations/es.ts +8 -0
- package/src/math/Mat33.test.ts +47 -3
- package/src/math/Mat33.ts +47 -5
- package/src/math/Path.ts +87 -28
- package/src/math/Vec3.test.ts +4 -0
- package/src/math/Vec3.ts +1 -1
- package/src/rendering/Display.ts +1 -1
- package/src/rendering/renderers/AbstractRenderer.ts +20 -3
- package/src/rendering/renderers/CanvasRenderer.ts +17 -4
- package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
- package/src/rendering/renderers/SVGRenderer.ts +8 -1
- package/src/testing/sendTouchEvent.ts +43 -0
- package/src/toolbar/HTMLToolbar.ts +164 -16
- package/src/toolbar/IconProvider.ts +47 -2
- package/src/toolbar/localization.ts +10 -0
- package/src/toolbar/toolbar.css +2 -0
- package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
- package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
- package/src/toolbar/widgets/BaseWidget.ts +34 -2
- package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
- package/src/toolbar/widgets/HandToolWidget.ts +1 -1
- package/src/toolbar/widgets/OverflowWidget.css +9 -0
- package/src/toolbar/widgets/OverflowWidget.ts +83 -0
- package/src/toolbar/widgets/lib.ts +2 -1
- package/src/tools/Eraser.test.ts +24 -1
- package/src/tools/Eraser.ts +6 -2
- package/src/tools/PanZoom.test.ts +267 -23
- package/src/tools/PanZoom.ts +15 -1
- package/src/tools/PasteHandler.ts +3 -2
- package/src/tools/SelectionTool/Selection.ts +3 -2
- package/src/tools/SelectionTool/SelectionTool.ts +6 -1
- package/src/types.ts +1 -0
package/src/math/Path.ts
CHANGED
@@ -209,39 +209,42 @@ export default class Path {
|
|
209
209
|
return result;
|
210
210
|
}
|
211
211
|
|
212
|
+
private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {
|
213
|
+
switch (part.kind) {
|
214
|
+
case PathCommandType.MoveTo:
|
215
|
+
case PathCommandType.LineTo:
|
216
|
+
return {
|
217
|
+
kind: part.kind,
|
218
|
+
point: mapping(part.point),
|
219
|
+
};
|
220
|
+
break;
|
221
|
+
case PathCommandType.CubicBezierTo:
|
222
|
+
return {
|
223
|
+
kind: part.kind,
|
224
|
+
controlPoint1: mapping(part.controlPoint1),
|
225
|
+
controlPoint2: mapping(part.controlPoint2),
|
226
|
+
endPoint: mapping(part.endPoint),
|
227
|
+
};
|
228
|
+
break;
|
229
|
+
case PathCommandType.QuadraticBezierTo:
|
230
|
+
return {
|
231
|
+
kind: part.kind,
|
232
|
+
controlPoint: mapping(part.controlPoint),
|
233
|
+
endPoint: mapping(part.endPoint),
|
234
|
+
};
|
235
|
+
break;
|
236
|
+
}
|
237
|
+
|
238
|
+
const exhaustivenessCheck: never = part;
|
239
|
+
return exhaustivenessCheck;
|
240
|
+
}
|
241
|
+
|
212
242
|
public mapPoints(mapping: (point: Point2)=>Point2): Path {
|
213
243
|
const startPoint = mapping(this.startPoint);
|
214
244
|
const newParts: PathCommand[] = [];
|
215
245
|
|
216
|
-
let exhaustivenessCheck: never;
|
217
246
|
for (const part of this.parts) {
|
218
|
-
|
219
|
-
case PathCommandType.MoveTo:
|
220
|
-
case PathCommandType.LineTo:
|
221
|
-
newParts.push({
|
222
|
-
kind: part.kind,
|
223
|
-
point: mapping(part.point),
|
224
|
-
});
|
225
|
-
break;
|
226
|
-
case PathCommandType.CubicBezierTo:
|
227
|
-
newParts.push({
|
228
|
-
kind: part.kind,
|
229
|
-
controlPoint1: mapping(part.controlPoint1),
|
230
|
-
controlPoint2: mapping(part.controlPoint2),
|
231
|
-
endPoint: mapping(part.endPoint),
|
232
|
-
});
|
233
|
-
break;
|
234
|
-
case PathCommandType.QuadraticBezierTo:
|
235
|
-
newParts.push({
|
236
|
-
kind: part.kind,
|
237
|
-
controlPoint: mapping(part.controlPoint),
|
238
|
-
endPoint: mapping(part.endPoint),
|
239
|
-
});
|
240
|
-
break;
|
241
|
-
default:
|
242
|
-
exhaustivenessCheck = part;
|
243
|
-
return exhaustivenessCheck;
|
244
|
-
}
|
247
|
+
newParts.push(Path.mapPathCommand(part, mapping));
|
245
248
|
}
|
246
249
|
|
247
250
|
return new Path(startPoint, newParts);
|
@@ -431,6 +434,62 @@ export default class Path {
|
|
431
434
|
};
|
432
435
|
}
|
433
436
|
|
437
|
+
/**
|
438
|
+
* @returns a Path that, when rendered, looks roughly equivalent to the given path.
|
439
|
+
*/
|
440
|
+
public static visualEquivalent(renderablePath: RenderablePathSpec, visibleRect: Rect2): RenderablePathSpec {
|
441
|
+
const path = Path.fromRenderable(renderablePath);
|
442
|
+
const strokeWidth = renderablePath.style.stroke?.width ?? 0;
|
443
|
+
const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
|
444
|
+
|
445
|
+
// Scale the expanded rect --- the visual equivalent is only close for huge strokes.
|
446
|
+
const expandedRect = visibleRect.grownBy(strokeWidth)
|
447
|
+
.transformedBoundingBox(Mat33.scaling2D(2, visibleRect.center));
|
448
|
+
|
449
|
+
// TODO: Handle simplifying very small paths.
|
450
|
+
if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
|
451
|
+
return renderablePath;
|
452
|
+
}
|
453
|
+
const parts: PathCommand[] = [];
|
454
|
+
let startPoint = path.startPoint;
|
455
|
+
|
456
|
+
for (const part of path.parts) {
|
457
|
+
const partBBox = Path.computeBBoxForSegment(startPoint, part).grownBy(strokeWidth);
|
458
|
+
let endPoint;
|
459
|
+
|
460
|
+
if (part.kind === PathCommandType.LineTo || part.kind === PathCommandType.MoveTo) {
|
461
|
+
endPoint = part.point;
|
462
|
+
} else {
|
463
|
+
endPoint = part.endPoint;
|
464
|
+
}
|
465
|
+
|
466
|
+
const intersectsVisible = partBBox.intersects(visibleRect);
|
467
|
+
|
468
|
+
if (intersectsVisible) {
|
469
|
+
// TODO: Can we trim parts of paths that intersect the visible rectangle?
|
470
|
+
parts.push(part);
|
471
|
+
} else if (onlyStroked || part.kind === PathCommandType.MoveTo) {
|
472
|
+
// We're stroking (not filling) and the path doesn't intersect the bounding box.
|
473
|
+
// Don't draw it, but preserve the endpoints.
|
474
|
+
parts.push({
|
475
|
+
kind: PathCommandType.MoveTo,
|
476
|
+
point: endPoint,
|
477
|
+
});
|
478
|
+
}
|
479
|
+
else {
|
480
|
+
// Otherwise, we may be filling. Try to roughly preserve the filled region.
|
481
|
+
parts.push({
|
482
|
+
kind: PathCommandType.LineTo,
|
483
|
+
point: endPoint,
|
484
|
+
});
|
485
|
+
}
|
486
|
+
|
487
|
+
startPoint = endPoint;
|
488
|
+
}
|
489
|
+
|
490
|
+
return new Path(path.startPoint, parts).toRenderable(renderablePath.style);
|
491
|
+
}
|
492
|
+
|
434
493
|
private cachedStringVersion: string|null = null;
|
435
494
|
|
436
495
|
public toString(useNonAbsCommands?: boolean): string {
|
package/src/math/Vec3.test.ts
CHANGED
@@ -31,6 +31,10 @@ describe('Vec3', () => {
|
|
31
31
|
expect(Vec3.zero.orthog().dot(Vec3.zero)).toBe(0);
|
32
32
|
});
|
33
33
|
|
34
|
+
it('.minus should return the difference between two vectors', () => {
|
35
|
+
expect(Vec3.of(1, 2, 3).minus(Vec3.of(4, 5, 6))).objEq(Vec3.of(1 - 4, 2 - 5, 3 - 6));
|
36
|
+
});
|
37
|
+
|
34
38
|
it('.orthog should return a unit vector', () => {
|
35
39
|
expect(Vec3.zero.orthog().magnitude()).toBe(1);
|
36
40
|
expect(Vec3.unitZ.orthog().magnitude()).toBe(1);
|
package/src/math/Vec3.ts
CHANGED
package/src/rendering/Display.ts
CHANGED
@@ -74,7 +74,7 @@ export default class Display {
|
|
74
74
|
},
|
75
75
|
blockResolution: cacheBlockResolution,
|
76
76
|
cacheSize: 600 * 600 * 4 * 90,
|
77
|
-
maxScale: 1.
|
77
|
+
maxScale: 1.3,
|
78
78
|
|
79
79
|
// Require about 20 strokes with 4 parts each to cache an image in one of the
|
80
80
|
// parts of the cache grid.
|
@@ -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, PathCommandType } from '../../math/Path';
|
@@ -122,21 +123,37 @@ export default abstract class AbstractRenderer {
|
|
122
123
|
}
|
123
124
|
}
|
124
125
|
|
125
|
-
//
|
126
|
+
// Strokes a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
|
126
127
|
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
|
127
128
|
public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle) {
|
128
129
|
const path = Path.fromRect(rect, lineWidth);
|
129
130
|
this.drawPath(path.toRenderable(lineFill));
|
130
131
|
}
|
131
132
|
|
132
|
-
//
|
133
|
+
// Fills a rectangle.
|
134
|
+
public fillRect(rect: Rect2, fill: Color4) {
|
135
|
+
const path = Path.fromRect(rect);
|
136
|
+
this.drawPath(path.toRenderable({ fill }));
|
137
|
+
}
|
138
|
+
|
139
|
+
// Note the start of an object with the given bounding box.
|
133
140
|
// Renderers are not required to support [clip]
|
134
141
|
public startObject(_boundingBox: Rect2, _clip?: boolean) {
|
135
142
|
this.currentPaths = [];
|
136
143
|
this.objectLevel ++;
|
137
144
|
}
|
138
145
|
|
139
|
-
|
146
|
+
/**
|
147
|
+
* Notes the end of an object.
|
148
|
+
* @param _loaderData - a map from strings to JSON-ifyable objects
|
149
|
+
* and contains properties attached to the object by whatever loader loaded the image. This
|
150
|
+
* is used to preserve attributes not supported by js-draw when loading/saving an image.
|
151
|
+
* Renderers may ignore this.
|
152
|
+
*
|
153
|
+
* @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object.
|
154
|
+
* Renderers may ignore this.
|
155
|
+
*/
|
156
|
+
public endObject(_loaderData?: LoadSaveDataTable, _objectTags?: string[]) {
|
140
157
|
// Render the paths all at once
|
141
158
|
this.flushPath();
|
142
159
|
this.currentPaths = null;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
2
|
import TextComponent from '../../components/TextComponent';
|
3
3
|
import Mat33 from '../../math/Mat33';
|
4
|
+
import Path from '../../math/Path';
|
4
5
|
import Rect2 from '../../math/Rect2';
|
5
6
|
import { Point2, Vec2 } from '../../math/Vec2';
|
6
7
|
import Vec3 from '../../math/Vec3';
|
@@ -12,6 +13,7 @@ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './Abstrac
|
|
12
13
|
export default class CanvasRenderer extends AbstractRenderer {
|
13
14
|
private ignoreObjectsAboveLevel: number|null = null;
|
14
15
|
private ignoringObject: boolean = false;
|
16
|
+
private currentObjectBBox: Rect2|null = null;
|
15
17
|
|
16
18
|
// Minimum square distance of a control point from the line between the end points
|
17
19
|
// for the curve not to be drawn as a line.
|
@@ -65,15 +67,15 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
65
67
|
this.minRenderSizeAnyDimen = 0.5;
|
66
68
|
} else {
|
67
69
|
this.minSquareCurveApproxDist = 0.5;
|
68
|
-
this.minRenderSizeBothDimens = 0.
|
69
|
-
this.minRenderSizeAnyDimen = 1e-
|
70
|
+
this.minRenderSizeBothDimens = 0.2;
|
71
|
+
this.minRenderSizeAnyDimen = 1e-6;
|
70
72
|
}
|
71
73
|
}
|
72
74
|
|
73
75
|
public displaySize(): Vec2 {
|
74
76
|
return Vec2.of(
|
75
77
|
this.ctx.canvas.clientWidth,
|
76
|
-
this.ctx.canvas.clientHeight
|
78
|
+
this.ctx.canvas.clientHeight,
|
77
79
|
);
|
78
80
|
}
|
79
81
|
|
@@ -149,6 +151,15 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
149
151
|
return;
|
150
152
|
}
|
151
153
|
|
154
|
+
// If part of a huge object, it might be worth trimming the path
|
155
|
+
if (this.currentObjectBBox?.containsRect(this.getViewport().visibleRect)) {
|
156
|
+
// Try to trim/remove parts of the path outside of the bounding box.
|
157
|
+
path = Path.visualEquivalent(
|
158
|
+
path,
|
159
|
+
this.getViewport().visibleRect
|
160
|
+
);
|
161
|
+
}
|
162
|
+
|
152
163
|
super.drawPath(path);
|
153
164
|
}
|
154
165
|
|
@@ -181,13 +192,14 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
181
192
|
}
|
182
193
|
|
183
194
|
private clipLevels: number[] = [];
|
184
|
-
public startObject(boundingBox: Rect2, clip
|
195
|
+
public startObject(boundingBox: Rect2, clip?: boolean) {
|
185
196
|
if (this.isTooSmallToRender(boundingBox)) {
|
186
197
|
this.ignoreObjectsAboveLevel = this.getNestingLevel();
|
187
198
|
this.ignoringObject = true;
|
188
199
|
}
|
189
200
|
|
190
201
|
super.startObject(boundingBox);
|
202
|
+
this.currentObjectBBox = boundingBox;
|
191
203
|
|
192
204
|
if (!this.ignoringObject && clip) {
|
193
205
|
this.clipLevels.push(this.objectLevel);
|
@@ -209,6 +221,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
209
221
|
}
|
210
222
|
}
|
211
223
|
|
224
|
+
this.currentObjectBBox = null;
|
212
225
|
super.endObject();
|
213
226
|
|
214
227
|
// If exiting an object with a too-small-to-draw bounding box,
|
@@ -1,12 +1,11 @@
|
|
1
1
|
|
2
|
-
import EventDispatcher from '../../EventDispatcher';
|
3
2
|
import Mat33 from '../../math/Mat33';
|
4
3
|
import { Vec2 } from '../../math/Vec2';
|
5
4
|
import Viewport from '../../Viewport';
|
6
5
|
import DummyRenderer from './DummyRenderer';
|
7
6
|
|
8
7
|
const makeRenderer = (): [DummyRenderer, Viewport] => {
|
9
|
-
const viewport = new Viewport(
|
8
|
+
const viewport = new Viewport(() => {});
|
10
9
|
return [ new DummyRenderer(viewport), viewport ];
|
11
10
|
};
|
12
11
|
|
@@ -248,7 +248,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
248
248
|
this.objectElems = [];
|
249
249
|
}
|
250
250
|
|
251
|
-
public endObject(loaderData?: LoadSaveDataTable) {
|
251
|
+
public endObject(loaderData?: LoadSaveDataTable, elemClassNames?: string[]) {
|
252
252
|
super.endObject(loaderData);
|
253
253
|
|
254
254
|
// Don't extend paths across objects
|
@@ -273,6 +273,13 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
273
273
|
}
|
274
274
|
}
|
275
275
|
}
|
276
|
+
|
277
|
+
// Add class names to the object, if given.
|
278
|
+
if (elemClassNames) {
|
279
|
+
for (const elem of this.objectElems ?? []) {
|
280
|
+
elem.classList.add(...elemClassNames);
|
281
|
+
}
|
282
|
+
}
|
276
283
|
}
|
277
284
|
|
278
285
|
// Not implemented -- use drawPath instead.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { Vec2 } from '../math/Vec2';
|
3
|
+
import Pointer, { PointerDevice } from '../Pointer';
|
4
|
+
import { InputEvtType } from '../types';
|
5
|
+
|
6
|
+
|
7
|
+
const sendTouchEvent = (
|
8
|
+
editor: Editor,
|
9
|
+
eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
|
10
|
+
screenPos: Vec2,
|
11
|
+
allOtherPointers?: Pointer[]
|
12
|
+
) => {
|
13
|
+
const canvasPos = editor.viewport.screenToCanvas(screenPos);
|
14
|
+
|
15
|
+
let ptrId = 0;
|
16
|
+
let maxPtrId = 0;
|
17
|
+
|
18
|
+
// Get a unique ID for the main pointer
|
19
|
+
// (try to use id=0, but don't use it if it's already in use).
|
20
|
+
for (const pointer of allOtherPointers ?? []) {
|
21
|
+
maxPtrId = Math.max(pointer.id, maxPtrId);
|
22
|
+
if (pointer.id === ptrId) {
|
23
|
+
ptrId = maxPtrId + 1;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
const mainPointer = Pointer.ofCanvasPoint(
|
28
|
+
canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch
|
29
|
+
);
|
30
|
+
|
31
|
+
editor.toolController.dispatchInputEvent({
|
32
|
+
kind: eventType,
|
33
|
+
allPointers: [
|
34
|
+
...(allOtherPointers ?? []),
|
35
|
+
mainPointer,
|
36
|
+
],
|
37
|
+
current: mainPointer,
|
38
|
+
});
|
39
|
+
|
40
|
+
return mainPointer;
|
41
|
+
};
|
42
|
+
|
43
|
+
export default sendTouchEvent;
|
@@ -18,6 +18,9 @@ import HandToolWidget from './widgets/HandToolWidget';
|
|
18
18
|
import BaseWidget from './widgets/BaseWidget';
|
19
19
|
import ActionButtonWidget from './widgets/ActionButtonWidget';
|
20
20
|
import InsertImageWidget from './widgets/InsertImageWidget';
|
21
|
+
import DocumentPropertiesWidget from './widgets/DocumentPropertiesWidget';
|
22
|
+
import OverflowWidget from './widgets/OverflowWidget';
|
23
|
+
import { DispatcherEventListener } from '../EventDispatcher';
|
21
24
|
|
22
25
|
export const toolbarCSSPrefix = 'toolbar-';
|
23
26
|
|
@@ -37,8 +40,14 @@ interface SpacerOptions {
|
|
37
40
|
|
38
41
|
export default class HTMLToolbar {
|
39
42
|
private container: HTMLElement;
|
43
|
+
private resizeObserver: ResizeObserver;
|
44
|
+
private listeners: DispatcherEventListener[] = [];
|
40
45
|
|
41
|
-
private
|
46
|
+
private widgetsById: Record<string, BaseWidget> = {};
|
47
|
+
private widgetList: Array<BaseWidget> = [];
|
48
|
+
|
49
|
+
// Widget to toggle overflow menu.
|
50
|
+
private overflowWidget: OverflowWidget|null = null;
|
42
51
|
|
43
52
|
private static colorisStarted: boolean = false;
|
44
53
|
private updateColoris: UpdateColorisCallback|null = null;
|
@@ -58,6 +67,15 @@ export default class HTMLToolbar {
|
|
58
67
|
HTMLToolbar.colorisStarted = true;
|
59
68
|
}
|
60
69
|
this.setupColorPickers();
|
70
|
+
|
71
|
+
if ('ResizeObserver' in window) {
|
72
|
+
this.resizeObserver = new ResizeObserver((_entries) => {
|
73
|
+
this.reLayout();
|
74
|
+
});
|
75
|
+
this.resizeObserver.observe(this.container);
|
76
|
+
} else {
|
77
|
+
console.warn('ResizeObserver not supported. Toolbar will not resize.');
|
78
|
+
}
|
61
79
|
}
|
62
80
|
|
63
81
|
// @internal
|
@@ -116,7 +134,7 @@ export default class HTMLToolbar {
|
|
116
134
|
}
|
117
135
|
};
|
118
136
|
|
119
|
-
this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
|
137
|
+
this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
|
120
138
|
if (event.kind !== EditorEventType.ColorPickerToggled) {
|
121
139
|
return;
|
122
140
|
}
|
@@ -124,14 +142,102 @@ export default class HTMLToolbar {
|
|
124
142
|
// Show/hide the overlay. Making the overlay visible gives users a surface to click
|
125
143
|
// on that shows/hides the color picker.
|
126
144
|
closePickerOverlay.style.display = event.open ? 'block' : 'none';
|
127
|
-
});
|
145
|
+
}));
|
128
146
|
|
129
147
|
// Add newly-selected colors to the swatch.
|
130
|
-
this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
|
148
|
+
this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
|
131
149
|
if (event.kind === EditorEventType.ColorPickerColorSelected) {
|
132
150
|
addColorToSwatch(event.color.toHexString());
|
133
151
|
}
|
134
|
-
});
|
152
|
+
}));
|
153
|
+
}
|
154
|
+
|
155
|
+
private reLayoutQueued: boolean = false;
|
156
|
+
private queueReLayout() {
|
157
|
+
if (!this.reLayoutQueued) {
|
158
|
+
this.reLayoutQueued = true;
|
159
|
+
requestAnimationFrame(() => this.reLayout());
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
private reLayout() {
|
164
|
+
this.reLayoutQueued = false;
|
165
|
+
|
166
|
+
if (!this.overflowWidget) {
|
167
|
+
return;
|
168
|
+
}
|
169
|
+
|
170
|
+
const getTotalWidth = (widgetList: Array<BaseWidget>) => {
|
171
|
+
let totalWidth = 0;
|
172
|
+
for (const widget of widgetList) {
|
173
|
+
if (!widget.isHidden()) {
|
174
|
+
totalWidth += widget.getButtonWidth();
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
return totalWidth;
|
179
|
+
};
|
180
|
+
|
181
|
+
let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
|
182
|
+
let shownWidgetWidth = getTotalWidth(this.widgetList) - overflowWidgetsWidth;
|
183
|
+
let availableWidth = this.container.clientWidth * 0.87;
|
184
|
+
|
185
|
+
// If on a device that has enough vertical space, allow
|
186
|
+
// showing two rows of buttons.
|
187
|
+
// TODO: Fix magic numbers
|
188
|
+
if (window.innerHeight > availableWidth * 1.75) {
|
189
|
+
availableWidth *= 1.75;
|
190
|
+
}
|
191
|
+
|
192
|
+
let updatedChildren = false;
|
193
|
+
|
194
|
+
if (shownWidgetWidth + overflowWidgetsWidth <= availableWidth) {
|
195
|
+
// Move widgets to the main menu.
|
196
|
+
const overflowChildren = this.overflowWidget.clearChildren();
|
197
|
+
|
198
|
+
for (const child of overflowChildren) {
|
199
|
+
child.addTo(this.container);
|
200
|
+
child.setIsToplevel(true);
|
201
|
+
|
202
|
+
if (!child.isHidden()) {
|
203
|
+
shownWidgetWidth += child.getButtonWidth();
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
this.overflowWidget.setHidden(true);
|
208
|
+
overflowWidgetsWidth = 0;
|
209
|
+
|
210
|
+
updatedChildren = true;
|
211
|
+
}
|
212
|
+
|
213
|
+
if (shownWidgetWidth >= availableWidth) {
|
214
|
+
// Move widgets to the overflow menu.
|
215
|
+
this.overflowWidget.setHidden(false);
|
216
|
+
|
217
|
+
// Start with the rightmost widget, move to the leftmost
|
218
|
+
for (
|
219
|
+
let i = this.widgetList.length - 1;
|
220
|
+
i >= 0 && shownWidgetWidth >= availableWidth;
|
221
|
+
i--
|
222
|
+
) {
|
223
|
+
const child = this.widgetList[i];
|
224
|
+
|
225
|
+
if (this.overflowWidget.hasAsChild(child)) {
|
226
|
+
continue;
|
227
|
+
}
|
228
|
+
|
229
|
+
if (child.canBeInOverflowMenu()) {
|
230
|
+
shownWidgetWidth -= child.getButtonWidth();
|
231
|
+
this.overflowWidget.addToOverflow(child);
|
232
|
+
}
|
233
|
+
}
|
234
|
+
|
235
|
+
updatedChildren = true;
|
236
|
+
}
|
237
|
+
|
238
|
+
if (updatedChildren) {
|
239
|
+
this.setupColorPickers();
|
240
|
+
}
|
135
241
|
}
|
136
242
|
|
137
243
|
/**
|
@@ -147,14 +253,21 @@ export default class HTMLToolbar {
|
|
147
253
|
*/
|
148
254
|
public addWidget(widget: BaseWidget) {
|
149
255
|
// Prevent name collisions
|
150
|
-
const id = widget.getUniqueIdIn(this.
|
256
|
+
const id = widget.getUniqueIdIn(this.widgetsById);
|
151
257
|
|
152
258
|
// Add the widget
|
153
|
-
this.
|
259
|
+
this.widgetsById[id] = widget;
|
260
|
+
this.widgetList.push(widget);
|
154
261
|
|
155
262
|
// Add HTML elements.
|
156
|
-
widget.addTo(this.container);
|
263
|
+
const container = widget.addTo(this.container);
|
157
264
|
this.setupColorPickers();
|
265
|
+
|
266
|
+
// Ensure that the widget gets displayed in the correct
|
267
|
+
// place in the toolbar, even if it's removed and re-added.
|
268
|
+
container.style.order = `${this.widgetList.length}`;
|
269
|
+
|
270
|
+
this.queueReLayout();
|
158
271
|
}
|
159
272
|
|
160
273
|
/**
|
@@ -200,8 +313,8 @@ export default class HTMLToolbar {
|
|
200
313
|
public serializeState(): string {
|
201
314
|
const result: Record<string, any> = {};
|
202
315
|
|
203
|
-
for (const widgetId in this.
|
204
|
-
result[widgetId] = this.
|
316
|
+
for (const widgetId in this.widgetsById) {
|
317
|
+
result[widgetId] = this.widgetsById[widgetId].serializeState();
|
205
318
|
}
|
206
319
|
|
207
320
|
return JSON.stringify(result);
|
@@ -215,11 +328,11 @@ export default class HTMLToolbar {
|
|
215
328
|
const data = JSON.parse(state);
|
216
329
|
|
217
330
|
for (const widgetId in data) {
|
218
|
-
if (!(widgetId in this.
|
331
|
+
if (!(widgetId in this.widgetsById)) {
|
219
332
|
console.warn(`Unable to deserialize widget ${widgetId} — no such widget.`);
|
220
333
|
}
|
221
334
|
|
222
|
-
this.
|
335
|
+
this.widgetsById[widgetId].deserializeFrom(data[widgetId]);
|
223
336
|
}
|
224
337
|
}
|
225
338
|
|
@@ -228,7 +341,11 @@ export default class HTMLToolbar {
|
|
228
341
|
*
|
229
342
|
* @return The added button.
|
230
343
|
*/
|
231
|
-
public addActionButton(
|
344
|
+
public addActionButton(
|
345
|
+
title: string|ActionButtonIcon,
|
346
|
+
command: ()=> void,
|
347
|
+
mustBeToplevel: boolean = true
|
348
|
+
): BaseWidget {
|
232
349
|
const titleString = typeof title === 'string' ? title : title.label;
|
233
350
|
const widgetId = 'action-button';
|
234
351
|
|
@@ -246,7 +363,8 @@ export default class HTMLToolbar {
|
|
246
363
|
makeIcon,
|
247
364
|
titleString,
|
248
365
|
command,
|
249
|
-
this.editor.localization
|
366
|
+
this.editor.localization,
|
367
|
+
mustBeToplevel,
|
250
368
|
);
|
251
369
|
|
252
370
|
this.addWidget(widget);
|
@@ -300,27 +418,57 @@ export default class HTMLToolbar {
|
|
300
418
|
this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
|
301
419
|
}
|
302
420
|
|
303
|
-
this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
|
304
|
-
|
305
421
|
const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
|
306
422
|
if (panZoomTool) {
|
307
423
|
this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
|
308
424
|
}
|
425
|
+
|
426
|
+
this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
|
309
427
|
}
|
310
428
|
|
311
429
|
public addDefaultActionButtons() {
|
430
|
+
this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
|
312
431
|
this.addUndoRedoButtons();
|
313
432
|
}
|
314
433
|
|
434
|
+
/**
|
435
|
+
* Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
|
436
|
+
* that this widget is in the correct space (if shown).
|
437
|
+
*
|
438
|
+
* @example
|
439
|
+
* ```ts
|
440
|
+
* toolbar.addDefaultToolWidgets();
|
441
|
+
* toolbar.addOverflowWidget();
|
442
|
+
* toolbar.addDefaultActionButtons();
|
443
|
+
* ```
|
444
|
+
* shows the overflow widget between the default tool widgets and the default action buttons,
|
445
|
+
* if shown.
|
446
|
+
*/
|
447
|
+
public addOverflowWidget() {
|
448
|
+
this.overflowWidget = new OverflowWidget(this.editor, this.localizationTable);
|
449
|
+
this.addWidget(this.overflowWidget);
|
450
|
+
}
|
451
|
+
|
315
452
|
/**
|
316
453
|
* Adds both the default tool widgets and action buttons. Equivalent to
|
317
454
|
* ```ts
|
318
455
|
* toolbar.addDefaultToolWidgets();
|
456
|
+
* toolbar.addOverflowWidget();
|
319
457
|
* toolbar.addDefaultActionButtons();
|
320
458
|
* ```
|
321
459
|
*/
|
322
460
|
public addDefaults() {
|
323
461
|
this.addDefaultToolWidgets();
|
462
|
+
this.addOverflowWidget();
|
324
463
|
this.addDefaultActionButtons();
|
325
464
|
}
|
465
|
+
|
466
|
+
public remove() {
|
467
|
+
this.container.remove();
|
468
|
+
this.resizeObserver.disconnect();
|
469
|
+
|
470
|
+
for (const listener of this.listeners) {
|
471
|
+
listener.remove();
|
472
|
+
}
|
473
|
+
}
|
326
474
|
}
|