js-draw 0.4.0 → 0.5.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/pull_request_template.md +15 -0
- package/.github/workflows/firebase-hosting-merge.yml +7 -0
- package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
- package/.github/workflows/github-pages.yml +2 -0
- package/CHANGELOG.md +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +1 -1
- package/dist/src/Editor.js +8 -3
- package/dist/src/components/AbstractComponent.js +1 -0
- package/dist/src/components/Stroke.js +15 -9
- package/dist/src/components/Text.d.ts +1 -1
- package/dist/src/components/Text.js +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +33 -35
- package/dist/src/math/Vec3.d.ts +1 -1
- package/dist/src/math/Vec3.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +1 -1
- package/dist/src/testing/beforeEachFile.d.ts +1 -0
- package/dist/src/testing/beforeEachFile.js +3 -0
- package/dist/src/testing/createEditor.d.ts +1 -0
- package/dist/src/testing/createEditor.js +7 -1
- package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +2 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +15 -0
- package/dist/src/toolbar/widgets/PenToolWidget.d.ts +2 -0
- package/dist/src/toolbar/widgets/PenToolWidget.js +14 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +8 -0
- package/dist/src/tools/ToolController.js +2 -0
- package/dist/src/tools/ToolbarShortcutHandler.d.ts +12 -0
- package/dist/src/tools/ToolbarShortcutHandler.js +23 -0
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/types.d.ts +4 -2
- package/jest.config.js +5 -0
- package/package.json +15 -14
- package/src/Editor.ts +8 -2
- package/src/components/AbstractComponent.ts +2 -0
- package/src/components/Stroke.test.ts +0 -3
- package/src/components/Stroke.ts +14 -7
- package/src/components/Text.test.ts +0 -3
- package/src/components/Text.ts +2 -2
- package/src/components/builders/FreehandLineBuilder.ts +36 -42
- package/src/language/assertions.ts +2 -2
- package/src/math/LineSegment2.test.ts +8 -10
- package/src/math/Mat33.test.ts +0 -2
- package/src/math/Rect2.test.ts +0 -3
- package/src/math/Vec2.test.ts +0 -3
- package/src/math/Vec3.test.ts +0 -3
- package/src/math/Vec3.ts +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +1 -1
- package/src/testing/beforeEachFile.ts +3 -0
- package/src/testing/createEditor.ts +8 -1
- package/src/testing/global.d.ts +17 -0
- package/src/testing/loadExpectExtensions.ts +0 -15
- package/src/toolbar/toolbar.css +3 -2
- package/src/toolbar/widgets/BaseWidget.ts +19 -1
- package/src/toolbar/widgets/PenToolWidget.ts +18 -1
- package/src/tools/Pen.test.ts +150 -0
- package/src/tools/SelectionTool/SelectionTool.css +1 -1
- package/src/tools/SelectionTool/SelectionTool.ts +9 -0
- package/src/tools/ToolController.ts +2 -0
- package/src/tools/ToolbarShortcutHandler.ts +34 -0
- package/src/tools/UndoRedoShortcut.test.ts +3 -0
- package/src/tools/lib.ts +1 -0
- package/src/types.ts +13 -8
- package/tsconfig.json +3 -1
package/dist/src/Editor.d.ts
CHANGED
@@ -193,7 +193,7 @@ export declare class Editor {
|
|
193
193
|
remove: () => void;
|
194
194
|
};
|
195
195
|
addStyleSheet(content: string): HTMLStyleElement;
|
196
|
-
sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean): void;
|
196
|
+
sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean, altKey?: boolean): void;
|
197
197
|
sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
|
198
198
|
toSVG(): SVGElement;
|
199
199
|
loadFrom(loader: ImageLoader): Promise<void>;
|
package/dist/src/Editor.js
CHANGED
@@ -441,6 +441,7 @@ export class Editor {
|
|
441
441
|
kind: InputEvtType.KeyPressEvent,
|
442
442
|
key: evt.key,
|
443
443
|
ctrlKey: evt.ctrlKey,
|
444
|
+
altKey: evt.altKey,
|
444
445
|
})) {
|
445
446
|
evt.preventDefault();
|
446
447
|
}
|
@@ -453,6 +454,7 @@ export class Editor {
|
|
453
454
|
kind: InputEvtType.KeyUpEvent,
|
454
455
|
key: evt.key,
|
455
456
|
ctrlKey: evt.ctrlKey,
|
457
|
+
altKey: evt.altKey,
|
456
458
|
})) {
|
457
459
|
evt.preventDefault();
|
458
460
|
}
|
@@ -598,16 +600,19 @@ export class Editor {
|
|
598
600
|
}
|
599
601
|
// Dispatch a keyboard event to the currently selected tool.
|
600
602
|
// Intended for unit testing
|
601
|
-
sendKeyboardEvent(eventType, key, ctrlKey = false) {
|
603
|
+
sendKeyboardEvent(eventType, key, ctrlKey = false, altKey = false) {
|
602
604
|
this.toolController.dispatchInputEvent({
|
603
605
|
kind: eventType,
|
604
606
|
key,
|
605
|
-
ctrlKey
|
607
|
+
ctrlKey,
|
608
|
+
altKey,
|
606
609
|
});
|
607
610
|
}
|
608
611
|
// Dispatch a pen event to the currently selected tool.
|
609
612
|
// Intended primarially for unit tests.
|
610
|
-
sendPenEvent(eventType, point,
|
613
|
+
sendPenEvent(eventType, point,
|
614
|
+
// @deprecated
|
615
|
+
allPointers) {
|
611
616
|
const mainPointer = Pointer.ofCanvasPoint(point, eventType !== InputEvtType.PointerUpEvt, this.viewport);
|
612
617
|
this.toolController.dispatchInputEvent({
|
613
618
|
kind: eventType,
|
@@ -48,6 +48,7 @@ export default class AbstractComponent {
|
|
48
48
|
transformBy(affineTransfm) {
|
49
49
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
50
50
|
}
|
51
|
+
// Returns a copy of this component.
|
51
52
|
clone() {
|
52
53
|
const clone = this.createClone();
|
53
54
|
for (const attachmentKey in this.loadSaveData) {
|
@@ -6,7 +6,8 @@ export default class Stroke extends AbstractComponent {
|
|
6
6
|
constructor(parts) {
|
7
7
|
var _a;
|
8
8
|
super('stroke');
|
9
|
-
this.parts =
|
9
|
+
this.parts = [];
|
10
|
+
for (const section of parts) {
|
10
11
|
const path = Path.fromRenderable(section);
|
11
12
|
const pathBBox = this.bboxForPart(path.bbox, section.style);
|
12
13
|
if (!this.contentBBox) {
|
@@ -15,14 +16,14 @@ export default class Stroke extends AbstractComponent {
|
|
15
16
|
else {
|
16
17
|
this.contentBBox = this.contentBBox.union(pathBBox);
|
17
18
|
}
|
18
|
-
|
19
|
+
this.parts.push({
|
19
20
|
path,
|
20
21
|
// To implement RenderablePathSpec
|
21
22
|
startPoint: path.startPoint,
|
22
23
|
style: section.style,
|
23
24
|
commands: path.parts,
|
24
|
-
};
|
25
|
-
}
|
25
|
+
});
|
26
|
+
}
|
26
27
|
(_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
|
27
28
|
}
|
28
29
|
intersects(line) {
|
@@ -80,11 +81,16 @@ export default class Stroke extends AbstractComponent {
|
|
80
81
|
});
|
81
82
|
}
|
82
83
|
getPath() {
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
84
|
+
let result = null;
|
85
|
+
for (const part of this.parts) {
|
86
|
+
if (result) {
|
87
|
+
result = result.union(part.path);
|
88
|
+
}
|
89
|
+
else {
|
90
|
+
result !== null && result !== void 0 ? result : (result = part.path);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
return result !== null && result !== void 0 ? result : Path.empty;
|
88
94
|
}
|
89
95
|
description(localization) {
|
90
96
|
return localization.stroke;
|
@@ -29,7 +29,7 @@ export default class Text extends AbstractComponent {
|
|
29
29
|
intersects(lineSegment: LineSegment2): boolean;
|
30
30
|
protected applyTransformation(affineTransfm: Mat33): void;
|
31
31
|
protected createClone(): AbstractComponent;
|
32
|
-
|
32
|
+
getText(): string;
|
33
33
|
description(localizationTable: ImageComponentLocalization): string;
|
34
34
|
protected serializeToJSON(): Record<string, any>;
|
35
35
|
static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text;
|
@@ -113,7 +113,7 @@ export default class Text extends AbstractComponent {
|
|
113
113
|
result.push(textObject.getText());
|
114
114
|
}
|
115
115
|
}
|
116
|
-
return result.join('
|
116
|
+
return result.join('\n');
|
117
117
|
}
|
118
118
|
description(localizationTable) {
|
119
119
|
return localizationTable.text(this.getText());
|
@@ -29,6 +29,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
29
29
|
preview(renderer: AbstractRenderer): void;
|
30
30
|
build(): Stroke;
|
31
31
|
private roundPoint;
|
32
|
+
private approxCurrentCurveLength;
|
32
33
|
private finalizeCurrentCurve;
|
33
34
|
private currentSegmentToPath;
|
34
35
|
private computeExitingVec;
|
@@ -117,7 +117,7 @@ export default class FreehandLineBuilder {
|
|
117
117
|
}
|
118
118
|
}
|
119
119
|
build() {
|
120
|
-
if (this.lastPoint) {
|
120
|
+
if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
|
121
121
|
this.finalizeCurrentCurve();
|
122
122
|
}
|
123
123
|
return this.previewStroke();
|
@@ -129,6 +129,18 @@ export default class FreehandLineBuilder {
|
|
129
129
|
}
|
130
130
|
return Viewport.roundPoint(point, minFit);
|
131
131
|
}
|
132
|
+
// Returns the distance between the start, control, and end points of the curve.
|
133
|
+
approxCurrentCurveLength() {
|
134
|
+
if (!this.currentCurve) {
|
135
|
+
return 0;
|
136
|
+
}
|
137
|
+
const startPt = Vec2.ofXY(this.currentCurve.points[0]);
|
138
|
+
const controlPt = Vec2.ofXY(this.currentCurve.points[1]);
|
139
|
+
const endPt = Vec2.ofXY(this.currentCurve.points[2]);
|
140
|
+
const toControlDist = startPt.minus(controlPt).length();
|
141
|
+
const toEndDist = endPt.minus(controlPt).length();
|
142
|
+
return toControlDist + toEndDist;
|
143
|
+
}
|
132
144
|
finalizeCurrentCurve() {
|
133
145
|
// Case where no points have been added
|
134
146
|
if (!this.currentCurve) {
|
@@ -204,10 +216,8 @@ export default class FreehandLineBuilder {
|
|
204
216
|
let endVec = Vec2.ofXY(this.currentCurve.normal(1)).normalized();
|
205
217
|
startVec = startVec.times(this.curveStartWidth / 2);
|
206
218
|
endVec = endVec.times(this.curveEndWidth / 2);
|
207
|
-
if (
|
208
|
-
|
209
|
-
// fix.
|
210
|
-
console.error('startVec is NaN', startVec, endVec, this.currentCurve);
|
219
|
+
if (!isFinite(startVec.magnitude())) {
|
220
|
+
console.error('Warning: startVec is NaN or ∞', startVec, endVec, this.currentCurve);
|
211
221
|
startVec = endVec;
|
212
222
|
}
|
213
223
|
const startPt = Vec2.ofXY(this.currentCurve.get(0));
|
@@ -224,28 +234,18 @@ export default class FreehandLineBuilder {
|
|
224
234
|
}
|
225
235
|
}
|
226
236
|
const halfVecT = projectionT;
|
227
|
-
|
237
|
+
const halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
|
228
238
|
.normalized().times(this.curveStartWidth / 2 * halfVecT
|
229
239
|
+ this.curveEndWidth / 2 * (1 - halfVecT));
|
230
|
-
// Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
|
231
|
-
// of the center curve to place the boundary).
|
232
|
-
const computeBoundaryCurve = (direction, halfVec) => {
|
233
|
-
return new Bezier(startPt.plus(startVec.times(direction)), controlPoint.plus(halfVec.times(direction)), endPt.plus(endVec.times(direction)));
|
234
|
-
};
|
235
|
-
const boundariesIntersect = () => {
|
236
|
-
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
237
|
-
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
238
|
-
return upperBoundary.intersects(lowerBoundary).length > 0;
|
239
|
-
};
|
240
|
-
// If the boundaries have intersections, increasing the half vector's length could fix this.
|
241
|
-
if (boundariesIntersect()) {
|
242
|
-
halfVec = halfVec.times(1.1);
|
243
|
-
}
|
244
240
|
// Each starts at startPt ± startVec
|
241
|
+
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
|
242
|
+
const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
|
243
|
+
const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
|
244
|
+
const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
|
245
245
|
const lowerCurve = {
|
246
246
|
kind: PathCommandType.QuadraticBezierTo,
|
247
|
-
controlPoint:
|
248
|
-
endPoint:
|
247
|
+
controlPoint: lowerCurveControlPoint,
|
248
|
+
endPoint: lowerCurveEndPoint,
|
249
249
|
};
|
250
250
|
// From the end of the upperCurve to the start of the lowerCurve:
|
251
251
|
const upperToLowerConnector = {
|
@@ -255,11 +255,11 @@ export default class FreehandLineBuilder {
|
|
255
255
|
// From the end of lowerCurve to the start of upperCurve:
|
256
256
|
const lowerToUpperConnector = {
|
257
257
|
kind: PathCommandType.LineTo,
|
258
|
-
point:
|
258
|
+
point: upperCurveStartPoint,
|
259
259
|
};
|
260
260
|
const upperCurve = {
|
261
261
|
kind: PathCommandType.QuadraticBezierTo,
|
262
|
-
controlPoint:
|
262
|
+
controlPoint: upperCurveControlPoint,
|
263
263
|
endPoint: this.roundPoint(startPt.minus(startVec)),
|
264
264
|
};
|
265
265
|
return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
|
@@ -275,7 +275,6 @@ export default class FreehandLineBuilder {
|
|
275
275
|
const fuzzEq = 1e-10;
|
276
276
|
const deltaTime = newPoint.time - this.lastPoint.time;
|
277
277
|
if (newPoint.pos.eq(this.lastPoint.pos, fuzzEq) || deltaTime === 0) {
|
278
|
-
console.warn('Discarding identical point');
|
279
278
|
return;
|
280
279
|
}
|
281
280
|
else if (isNaN(newPoint.pos.magnitude())) {
|
@@ -327,29 +326,29 @@ export default class FreehandLineBuilder {
|
|
327
326
|
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
328
327
|
const maxControlPointDist = maxRelativeLength * startEndDist;
|
329
328
|
// Exit in cases where we would divide by zero
|
330
|
-
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 ||
|
329
|
+
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
|
331
330
|
return;
|
332
331
|
}
|
333
|
-
console.assert(
|
332
|
+
console.assert(isFinite(enteringVec.magnitude()), 'Pre-normalized enteringVec has NaN or ∞ magnitude!');
|
334
333
|
enteringVec = enteringVec.normalized();
|
335
334
|
exitingVec = exitingVec.normalized();
|
336
|
-
console.assert(
|
335
|
+
console.assert(isFinite(enteringVec.magnitude()), 'Normalized enteringVec has NaN or ∞ magnitude!');
|
337
336
|
const lineFromStart = new LineSegment2(segmentStart, segmentStart.plus(enteringVec.times(maxControlPointDist)));
|
338
337
|
const lineFromEnd = new LineSegment2(segmentEnd.minus(exitingVec.times(maxControlPointDist)), segmentEnd);
|
339
338
|
const intersection = lineFromEnd.intersection(lineFromStart);
|
340
339
|
// Position the control point at this intersection
|
341
|
-
let controlPoint;
|
340
|
+
let controlPoint = null;
|
342
341
|
if (intersection) {
|
343
342
|
controlPoint = intersection.point;
|
344
343
|
}
|
345
|
-
|
344
|
+
// No intersection or the intersection is one of the end points?
|
345
|
+
if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
346
346
|
// Position the control point closer to the first -- the connecting
|
347
347
|
// segment will be roughly a line.
|
348
348
|
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
|
349
349
|
}
|
350
|
-
|
351
|
-
|
352
|
-
}
|
350
|
+
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|
351
|
+
console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
|
353
352
|
const prevCurve = this.currentCurve;
|
354
353
|
this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
|
355
354
|
if (isNaN(Vec2.ofXY(this.currentCurve.normal(0)).magnitude())) {
|
@@ -369,8 +368,7 @@ export default class FreehandLineBuilder {
|
|
369
368
|
}
|
370
369
|
return true;
|
371
370
|
};
|
372
|
-
|
373
|
-
if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
|
371
|
+
if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth) {
|
374
372
|
if (!curveMatchesPoints(this.currentCurve)) {
|
375
373
|
// Use a curve that better fits the points
|
376
374
|
this.currentCurve = prevCurve;
|
package/dist/src/math/Vec3.d.ts
CHANGED
@@ -97,7 +97,7 @@ export default class Vec3 {
|
|
97
97
|
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
|
98
98
|
* ```
|
99
99
|
*/
|
100
|
-
eq(other: Vec3, fuzz
|
100
|
+
eq(other: Vec3, fuzz?: number): boolean;
|
101
101
|
toString(): string;
|
102
102
|
static unitX: Vec3;
|
103
103
|
static unitY: Vec3;
|
package/dist/src/math/Vec3.js
CHANGED
@@ -156,7 +156,7 @@ export default class Vec3 {
|
|
156
156
|
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
|
157
157
|
* ```
|
158
158
|
*/
|
159
|
-
eq(other, fuzz) {
|
159
|
+
eq(other, fuzz = 1e-10) {
|
160
160
|
for (let i = 0; i < 3; i++) {
|
161
161
|
if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
|
162
162
|
return false;
|
@@ -72,7 +72,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
72
72
|
drawPath(pathSpec) {
|
73
73
|
var _a;
|
74
74
|
const style = pathSpec.style;
|
75
|
-
const path = Path.fromRenderable(pathSpec);
|
75
|
+
const path = Path.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
|
76
76
|
// Try to extend the previous path, if possible
|
77
77
|
if (!style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) || this.lastPathString.length === 0) {
|
78
78
|
this.addPathToSVG();
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -1,3 +1,9 @@
|
|
1
1
|
import { RenderingMode } from '../rendering/Display';
|
2
2
|
import Editor from '../Editor';
|
3
|
-
|
3
|
+
/** Creates an editor. Should only be used in test files. */
|
4
|
+
export default () => {
|
5
|
+
if (jest === undefined) {
|
6
|
+
throw new Error('Files in the testing/ folder should only be used in tests!');
|
7
|
+
}
|
8
|
+
return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
|
9
|
+
};
|
@@ -1,17 +1,2 @@
|
|
1
1
|
export declare const loadExpectExtensions: () => void;
|
2
|
-
export interface CustomMatchers<R = unknown> {
|
3
|
-
objEq(expected: {
|
4
|
-
eq: (other: any, ...args: any) => boolean;
|
5
|
-
}, ...opts: any): R;
|
6
|
-
}
|
7
|
-
declare global {
|
8
|
-
export namespace jest {
|
9
|
-
interface Expect extends CustomMatchers {
|
10
|
-
}
|
11
|
-
interface Matchers<R> extends CustomMatchers<R> {
|
12
|
-
}
|
13
|
-
interface AsyncAsymmetricMatchers extends CustomMatchers {
|
14
|
-
}
|
15
|
-
}
|
16
|
-
}
|
17
2
|
export default loadExpectExtensions;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
+
import { KeyPressEvent } from '../../types';
|
2
3
|
import { ToolbarLocalization } from '../localization';
|
3
4
|
export default abstract class BaseWidget {
|
4
5
|
#private;
|
@@ -18,6 +19,7 @@ export default abstract class BaseWidget {
|
|
18
19
|
protected abstract createIcon(): Element;
|
19
20
|
protected fillDropdown(dropdown: HTMLElement): boolean;
|
20
21
|
protected setupActionBtnClickListener(button: HTMLElement): void;
|
22
|
+
protected onKeyPress(_event: KeyPressEvent): boolean;
|
21
23
|
protected abstract handleClick(): void;
|
22
24
|
protected get hasDropdown(): boolean;
|
23
25
|
protected addSubWidget(widget: BaseWidget): void;
|
@@ -10,6 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
11
|
};
|
12
12
|
var _BaseWidget_hasDropdown;
|
13
|
+
import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
|
13
14
|
import { EditorEventType, InputEvtType } from '../../types';
|
14
15
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
15
16
|
import { makeDropdownIcon } from '../icons';
|
@@ -33,6 +34,12 @@ export default class BaseWidget {
|
|
33
34
|
this.label = document.createElement('label');
|
34
35
|
this.button.setAttribute('role', 'button');
|
35
36
|
this.button.tabIndex = 0;
|
37
|
+
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
|
38
|
+
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
39
|
+
// toolbar widgets,
|
40
|
+
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
|
41
|
+
toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
|
42
|
+
}
|
36
43
|
}
|
37
44
|
// Add content to the widget's associated dropdown menu.
|
38
45
|
// Returns true if such a menu should be created, false otherwise.
|
@@ -62,6 +69,7 @@ export default class BaseWidget {
|
|
62
69
|
kind: InputEvtType.KeyPressEvent,
|
63
70
|
key: evt.key,
|
64
71
|
ctrlKey: evt.ctrlKey,
|
72
|
+
altKey: evt.altKey,
|
65
73
|
});
|
66
74
|
}
|
67
75
|
};
|
@@ -73,6 +81,7 @@ export default class BaseWidget {
|
|
73
81
|
kind: InputEvtType.KeyUpEvent,
|
74
82
|
key: evt.key,
|
75
83
|
ctrlKey: evt.ctrlKey,
|
84
|
+
altKey: evt.altKey,
|
76
85
|
});
|
77
86
|
};
|
78
87
|
button.onclick = () => {
|
@@ -81,6 +90,12 @@ export default class BaseWidget {
|
|
81
90
|
}
|
82
91
|
};
|
83
92
|
}
|
93
|
+
// Add a listener that is triggered when a key is pressed.
|
94
|
+
// Listeners will fire regardless of whether this widget is selected and require that
|
95
|
+
// {@link lib!Editor.toolController} to have an enabled {@link lib!ToolbarShortcutHandler} tool.
|
96
|
+
onKeyPress(_event) {
|
97
|
+
return false;
|
98
|
+
}
|
84
99
|
get hasDropdown() {
|
85
100
|
return __classPrivateFieldGet(this, _BaseWidget_hasDropdown, "f");
|
86
101
|
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { ComponentBuilderFactory } from '../../components/builders/types';
|
2
2
|
import Editor from '../../Editor';
|
3
3
|
import Pen from '../../tools/Pen';
|
4
|
+
import { KeyPressEvent } from '../../types';
|
4
5
|
import { ToolbarLocalization } from '../localization';
|
5
6
|
import BaseToolWidget from './BaseToolWidget';
|
6
7
|
export interface PenTypeRecord {
|
@@ -16,4 +17,5 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
16
17
|
protected createIcon(): Element;
|
17
18
|
private static idCounter;
|
18
19
|
protected fillDropdown(dropdown: HTMLElement): boolean;
|
20
|
+
protected onKeyPress(event: KeyPressEvent): boolean;
|
19
21
|
}
|
@@ -127,5 +127,19 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
127
127
|
dropdown.replaceChildren(container);
|
128
128
|
return true;
|
129
129
|
}
|
130
|
+
onKeyPress(event) {
|
131
|
+
if (!this.isSelected()) {
|
132
|
+
return false;
|
133
|
+
}
|
134
|
+
// Map alt+0-9 to different pen types.
|
135
|
+
if (/^[0-9]$/.exec(event.key) && event.ctrlKey) {
|
136
|
+
const penTypeIdx = parseInt(event.key) - 1;
|
137
|
+
if (penTypeIdx >= 0 && penTypeIdx < this.penTypes.length) {
|
138
|
+
this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
|
139
|
+
return true;
|
140
|
+
}
|
141
|
+
}
|
142
|
+
return false;
|
143
|
+
}
|
130
144
|
}
|
131
145
|
PenToolWidget.idCounter = 0;
|
@@ -8,6 +8,7 @@ import Viewport from '../../Viewport';
|
|
8
8
|
import BaseTool from '../BaseTool';
|
9
9
|
import SVGRenderer from '../../rendering/renderers/SVGRenderer';
|
10
10
|
import Selection from './Selection';
|
11
|
+
import TextComponent from '../../components/Text';
|
11
12
|
export const cssPrefix = 'selection-tool-';
|
12
13
|
// {@inheritDoc SelectionTool!}
|
13
14
|
export default class SelectionTool extends BaseTool {
|
@@ -215,10 +216,17 @@ export default class SelectionTool extends BaseTool {
|
|
215
216
|
const exportElem = document.createElementNS(svgNameSpace, 'svg');
|
216
217
|
const sanitize = true;
|
217
218
|
const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
|
219
|
+
const text = [];
|
218
220
|
for (const elem of selectedElems) {
|
219
221
|
elem.render(renderer);
|
222
|
+
if (elem instanceof TextComponent) {
|
223
|
+
text.push(elem.getText());
|
224
|
+
}
|
220
225
|
}
|
221
226
|
event.setData('image/svg+xml', exportElem.outerHTML);
|
227
|
+
if (text.length > 0) {
|
228
|
+
event.setData('text/plain', text.join('\n'));
|
229
|
+
}
|
222
230
|
return true;
|
223
231
|
}
|
224
232
|
setEnabled(enabled) {
|
@@ -10,6 +10,7 @@ import TextTool from './TextTool';
|
|
10
10
|
import PipetteTool from './PipetteTool';
|
11
11
|
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
12
12
|
import PasteHandler from './PasteHandler';
|
13
|
+
import ToolbarShortcutHandler from './ToolbarShortcutHandler';
|
13
14
|
export default class ToolController {
|
14
15
|
/** @internal */
|
15
16
|
constructor(editor, localization) {
|
@@ -35,6 +36,7 @@ export default class ToolController {
|
|
35
36
|
...primaryTools,
|
36
37
|
keyboardPanZoomTool,
|
37
38
|
new UndoRedoShortcut(editor),
|
39
|
+
new ToolbarShortcutHandler(editor),
|
38
40
|
new ToolSwitcherShortcut(editor),
|
39
41
|
new PasteHandler(editor),
|
40
42
|
];
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { KeyPressEvent } from '../types';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
declare type KeyPressListener = (event: KeyPressEvent) => boolean;
|
5
|
+
export default class ToolbarShortcutHandler extends BaseTool {
|
6
|
+
private listeners;
|
7
|
+
constructor(editor: Editor);
|
8
|
+
registerListener(listener: KeyPressListener): void;
|
9
|
+
removeListener(listener: KeyPressListener): void;
|
10
|
+
onKeyPress(event: KeyPressEvent): boolean;
|
11
|
+
}
|
12
|
+
export {};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
// Allows the toolbar to register keyboard events.
|
2
|
+
// @packageDocumentation
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
export default class ToolbarShortcutHandler extends BaseTool {
|
5
|
+
constructor(editor) {
|
6
|
+
super(editor.notifier, editor.localization.changeTool);
|
7
|
+
this.listeners = new Set([]);
|
8
|
+
}
|
9
|
+
registerListener(listener) {
|
10
|
+
this.listeners.add(listener);
|
11
|
+
}
|
12
|
+
removeListener(listener) {
|
13
|
+
this.listeners.delete(listener);
|
14
|
+
}
|
15
|
+
onKeyPress(event) {
|
16
|
+
for (const listener of this.listeners) {
|
17
|
+
if (listener(event)) {
|
18
|
+
return true;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return false;
|
22
|
+
}
|
23
|
+
}
|
package/dist/src/tools/lib.d.ts
CHANGED
@@ -12,3 +12,4 @@ export { default as TextTool } from './TextTool';
|
|
12
12
|
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
13
13
|
export { default as EraserTool } from './Eraser';
|
14
14
|
export { default as PasteHandler } from './PasteHandler';
|
15
|
+
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
package/dist/src/tools/lib.js
CHANGED
@@ -12,3 +12,4 @@ export { default as TextTool } from './TextTool';
|
|
12
12
|
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
13
13
|
export { default as EraserTool } from './Eraser';
|
14
14
|
export { default as PasteHandler } from './PasteHandler';
|
15
|
+
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
package/dist/src/types.d.ts
CHANGED
@@ -34,12 +34,14 @@ export interface WheelEvt {
|
|
34
34
|
export interface KeyPressEvent {
|
35
35
|
readonly kind: InputEvtType.KeyPressEvent;
|
36
36
|
readonly key: string;
|
37
|
-
readonly ctrlKey: boolean;
|
37
|
+
readonly ctrlKey: boolean | undefined;
|
38
|
+
readonly altKey: boolean | undefined;
|
38
39
|
}
|
39
40
|
export interface KeyUpEvent {
|
40
41
|
readonly kind: InputEvtType.KeyUpEvent;
|
41
42
|
readonly key: string;
|
42
|
-
readonly ctrlKey: boolean;
|
43
|
+
readonly ctrlKey: boolean | undefined;
|
44
|
+
readonly altKey: boolean | undefined;
|
43
45
|
}
|
44
46
|
export interface CopyEvent {
|
45
47
|
readonly kind: InputEvtType.CopyEvent;
|
package/jest.config.js
CHANGED
@@ -10,6 +10,10 @@ const config = {
|
|
10
10
|
'js',
|
11
11
|
],
|
12
12
|
|
13
|
+
testPathIgnorePatterns: [
|
14
|
+
'<rootDir>/dist/', '<rootDir>/node_modules/'
|
15
|
+
],
|
16
|
+
|
13
17
|
// Mocks.
|
14
18
|
// See https://jestjs.io/docs/webpack#handling-static-assets
|
15
19
|
moduleNameMapper: {
|
@@ -19,6 +23,7 @@ const config = {
|
|
19
23
|
},
|
20
24
|
|
21
25
|
testEnvironment: 'jsdom',
|
26
|
+
setupFilesAfterEnv: [ '<rootDir>/src/testing/beforeEachFile.ts' ],
|
22
27
|
};
|
23
28
|
|
24
29
|
module.exports = config;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.5.0",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "./dist/src/lib.d.ts",
|
6
6
|
"types": "./dist/src/lib.js",
|
@@ -71,36 +71,37 @@
|
|
71
71
|
"lint": "eslint .",
|
72
72
|
"linter-precommit": "eslint --fix --ext .js --ext .ts",
|
73
73
|
"lint-staged": "lint-staged",
|
74
|
+
"lint-ci": "eslint . --max-warnings=0 --ext .js --ext .ts",
|
74
75
|
"prepare": "husky install && yarn build",
|
75
76
|
"prepack": "yarn build && yarn test && pinst --disable",
|
76
77
|
"postpack": "pinst --enable"
|
77
78
|
},
|
78
79
|
"dependencies": {
|
79
|
-
"@melloware/coloris": "^0.16.
|
80
|
+
"@melloware/coloris": "^0.16.1",
|
80
81
|
"bezier-js": "^6.1.0"
|
81
82
|
},
|
82
83
|
"devDependencies": {
|
83
84
|
"@types/bezier-js": "^4.1.0",
|
84
|
-
"@types/jest": "^
|
85
|
+
"@types/jest": "^29.0.3",
|
85
86
|
"@types/jsdom": "^20.0.0",
|
86
|
-
"@types/node": "^18.7.
|
87
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
88
|
-
"@typescript-eslint/parser": "^5.
|
87
|
+
"@types/node": "^18.7.23",
|
88
|
+
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
89
|
+
"@typescript-eslint/parser": "^5.38.1",
|
89
90
|
"css-loader": "^6.7.1",
|
90
|
-
"eslint": "^8.
|
91
|
+
"eslint": "^8.24.0",
|
91
92
|
"husky": "^8.0.1",
|
92
|
-
"jest": "^
|
93
|
-
"jest-environment-jsdom": "^29.0.
|
93
|
+
"jest": "^29.0.3",
|
94
|
+
"jest-environment-jsdom": "^29.0.3",
|
94
95
|
"jsdom": "^20.0.0",
|
95
96
|
"lint-staged": "^13.0.3",
|
96
97
|
"pinst": "^3.0.0",
|
97
98
|
"style-loader": "^3.3.1",
|
98
|
-
"terser-webpack-plugin": "^5.3.
|
99
|
-
"ts-jest": "^
|
100
|
-
"ts-loader": "^9.
|
99
|
+
"terser-webpack-plugin": "^5.3.6",
|
100
|
+
"ts-jest": "^29.0.2",
|
101
|
+
"ts-loader": "^9.4.1",
|
101
102
|
"ts-node": "^10.9.1",
|
102
|
-
"typedoc": "^0.23.
|
103
|
-
"typescript": "^4.8.
|
103
|
+
"typedoc": "^0.23.15",
|
104
|
+
"typescript": "^4.8.3",
|
104
105
|
"webpack": "^5.74.0"
|
105
106
|
},
|
106
107
|
"bugs": {
|