js-draw 0.5.0 → 0.6.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 (73) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +8 -5
  4. package/dist/src/Editor.js +4 -1
  5. package/dist/src/EditorImage.d.ts +3 -0
  6. package/dist/src/EditorImage.js +7 -0
  7. package/dist/src/SVGLoader.js +5 -6
  8. package/dist/src/components/AbstractComponent.d.ts +1 -0
  9. package/dist/src/components/AbstractComponent.js +4 -0
  10. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -0
  11. package/dist/src/components/SVGGlobalAttributesObject.js +3 -0
  12. package/dist/src/components/Text.d.ts +3 -5
  13. package/dist/src/components/Text.js +19 -10
  14. package/dist/src/components/UnknownSVGObject.d.ts +1 -0
  15. package/dist/src/components/UnknownSVGObject.js +3 -0
  16. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  17. package/dist/src/testing/beforeEachFile.js +4 -0
  18. package/dist/src/toolbar/HTMLToolbar.js +2 -3
  19. package/dist/src/toolbar/IconProvider.d.ts +24 -0
  20. package/dist/src/toolbar/IconProvider.js +415 -0
  21. package/dist/src/toolbar/lib.d.ts +1 -1
  22. package/dist/src/toolbar/lib.js +1 -2
  23. package/dist/src/toolbar/localization.d.ts +0 -1
  24. package/dist/src/toolbar/localization.js +0 -1
  25. package/dist/src/toolbar/makeColorInput.js +1 -2
  26. package/dist/src/toolbar/widgets/BaseWidget.js +1 -2
  27. package/dist/src/toolbar/widgets/EraserToolWidget.js +1 -2
  28. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +5 -3
  29. package/dist/src/toolbar/widgets/HandToolWidget.js +35 -12
  30. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -3
  31. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +3 -0
  32. package/dist/src/toolbar/widgets/SelectionToolWidget.js +20 -7
  33. package/dist/src/toolbar/widgets/TextToolWidget.js +1 -2
  34. package/dist/src/tools/PanZoom.d.ts +1 -1
  35. package/dist/src/tools/PanZoom.js +4 -1
  36. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -0
  37. package/dist/src/tools/SelectionTool/SelectionTool.js +66 -3
  38. package/dist/src/tools/ToolController.js +1 -0
  39. package/dist/src/tools/localization.d.ts +1 -0
  40. package/dist/src/tools/localization.js +1 -0
  41. package/package.json +1 -1
  42. package/src/Editor.ts +11 -5
  43. package/src/EditorImage.ts +9 -0
  44. package/src/SVGLoader.test.ts +37 -0
  45. package/src/SVGLoader.ts +5 -6
  46. package/src/components/AbstractComponent.ts +5 -0
  47. package/src/components/SVGGlobalAttributesObject.ts +4 -0
  48. package/src/components/Text.test.ts +1 -16
  49. package/src/components/Text.ts +21 -11
  50. package/src/components/UnknownSVGObject.ts +4 -0
  51. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  52. package/src/testing/beforeEachFile.ts +6 -1
  53. package/src/toolbar/HTMLToolbar.ts +2 -3
  54. package/src/toolbar/IconProvider.ts +476 -0
  55. package/src/toolbar/lib.ts +1 -1
  56. package/src/toolbar/localization.ts +0 -2
  57. package/src/toolbar/makeColorInput.ts +1 -2
  58. package/src/toolbar/widgets/BaseWidget.ts +1 -2
  59. package/src/toolbar/widgets/EraserToolWidget.ts +1 -2
  60. package/src/toolbar/widgets/HandToolWidget.ts +42 -20
  61. package/src/toolbar/widgets/PenToolWidget.ts +2 -3
  62. package/src/toolbar/widgets/SelectionToolWidget.ts +24 -8
  63. package/src/toolbar/widgets/TextToolWidget.ts +1 -2
  64. package/src/tools/PanZoom.ts +4 -1
  65. package/src/tools/SelectionTool/SelectionTool.css +1 -0
  66. package/src/tools/SelectionTool/SelectionTool.test.ts +40 -0
  67. package/src/tools/SelectionTool/SelectionTool.ts +73 -4
  68. package/src/tools/ToolController.ts +1 -0
  69. package/src/tools/localization.ts +4 -0
  70. package/typedoc.json +5 -1
  71. package/dist/src/toolbar/icons.d.ts +0 -20
  72. package/dist/src/toolbar/icons.js +0 -385
  73. package/src/toolbar/icons.ts +0 -443
@@ -28,6 +28,7 @@ import Display, { RenderingMode } from './rendering/Display';
28
28
  import Pointer from './Pointer';
29
29
  import Rect2 from './math/Rect2';
30
30
  import { EditorLocalization } from './localization';
31
+ import IconProvider from './toolbar/IconProvider';
31
32
  declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
32
33
  declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
33
34
  export interface EditorSettings {
@@ -44,6 +45,7 @@ export interface EditorSettings {
44
45
  /** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
45
46
  minZoom: number;
46
47
  maxZoom: number;
48
+ iconProvider: IconProvider;
47
49
  }
48
50
  export declare class Editor {
49
51
  private container;
@@ -81,18 +83,19 @@ export declare class Editor {
81
83
  * editor.dispatch(addElementCommand);
82
84
  * ```
83
85
  */
84
- image: EditorImage;
86
+ readonly image: EditorImage;
85
87
  /** Viewport for the exported/imported image. */
86
88
  private importExportViewport;
87
89
  /** @internal */
88
- localization: EditorLocalization;
89
- viewport: Viewport;
90
- toolController: ToolController;
90
+ readonly localization: EditorLocalization;
91
+ readonly icons: IconProvider;
92
+ readonly viewport: Viewport;
93
+ readonly toolController: ToolController;
91
94
  /**
92
95
  * Global event dispatcher/subscriber.
93
96
  * @see {@link types.EditorEventType}
94
97
  */
95
- notifier: EditorNotifier;
98
+ readonly notifier: EditorNotifier;
96
99
  private loadingWarning;
97
100
  private accessibilityAnnounceArea;
98
101
  private accessibilityControlArea;
@@ -41,6 +41,7 @@ import SVGLoader from './SVGLoader';
41
41
  import Pointer from './Pointer';
42
42
  import Mat33 from './math/Mat33';
43
43
  import getLocalizationTable from './localizations/getLocalizationTable';
44
+ import IconProvider from './toolbar/IconProvider';
44
45
  // { @inheritDoc Editor! }
45
46
  export class Editor {
46
47
  /**
@@ -67,7 +68,7 @@ export class Editor {
67
68
  * ```
68
69
  */
69
70
  constructor(parent, settings = {}) {
70
- var _a, _b, _c, _d;
71
+ var _a, _b, _c, _d, _e;
71
72
  this.eventListenerTargets = [];
72
73
  this.previousAccessibilityAnnouncement = '';
73
74
  this.pointers = {};
@@ -86,7 +87,9 @@ export class Editor {
86
87
  localization: this.localization,
87
88
  minZoom: (_c = settings.minZoom) !== null && _c !== void 0 ? _c : 2e-10,
88
89
  maxZoom: (_d = settings.maxZoom) !== null && _d !== void 0 ? _d : 1e12,
90
+ iconProvider: (_e = settings.iconProvider) !== null && _e !== void 0 ? _e : new IconProvider(),
89
91
  };
92
+ this.icons = this.settings.iconProvider;
90
93
  this.container = document.createElement('div');
91
94
  this.renderingRegion = document.createElement('div');
92
95
  this.container.appendChild(this.renderingRegion);
@@ -16,6 +16,9 @@ export default class EditorImage {
16
16
  render(renderer: AbstractRenderer, viewport: Viewport): void;
17
17
  /** Renders all nodes, even ones not within the viewport. @internal */
18
18
  renderAll(renderer: AbstractRenderer): void;
19
+ /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
20
+ getAllElements(): AbstractComponent[];
21
+ /** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
19
22
  getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
20
23
  /** @internal */
21
24
  onDestroyElement(elem: AbstractComponent): void;
@@ -39,6 +39,13 @@ export default class EditorImage {
39
39
  leaf.getContent().render(renderer, leaf.getBBox());
40
40
  }
41
41
  }
42
+ /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
43
+ getAllElements() {
44
+ const leaves = this.root.getLeaves();
45
+ sortLeavesByZIndex(leaves);
46
+ return leaves.map(leaf => leaf.getContent());
47
+ }
48
+ /** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
42
49
  getElementsIntersectingRegion(region) {
43
50
  const leaves = this.root.getLeavesIntersectingRegion(region);
44
51
  sortLeavesByZIndex(leaves);
@@ -99,7 +99,7 @@ export default class SVGLoader {
99
99
  }
100
100
  elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
101
101
  }
102
- if (supportedStyleAttrs) {
102
+ if (supportedStyleAttrs && node.style) {
103
103
  for (const attr of node.style) {
104
104
  if (attr === '' || !attr) {
105
105
  continue;
@@ -157,9 +157,9 @@ export default class SVGLoader {
157
157
  }
158
158
  const elemX = elem.getAttribute('x');
159
159
  const elemY = elem.getAttribute('y');
160
- if (elemX && elemY) {
161
- const x = parseFloat(elemX);
162
- const y = parseFloat(elemY);
160
+ if (elemX || elemY) {
161
+ const x = parseFloat(elemX !== null && elemX !== void 0 ? elemX : '0');
162
+ const y = parseFloat(elemY !== null && elemY !== void 0 ? elemY : '0');
163
163
  if (!isNaN(x) && !isNaN(y)) {
164
164
  supportedAttrs === null || supportedAttrs === void 0 ? void 0 : supportedAttrs.push('x', 'y');
165
165
  transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
@@ -204,7 +204,7 @@ export default class SVGLoader {
204
204
  size: fontSize,
205
205
  fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
206
206
  renderingStyle: {
207
- fill: Color4.fromString(computedStyles.fill)
207
+ fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000')
208
208
  },
209
209
  };
210
210
  const supportedAttrs = [];
@@ -339,7 +339,6 @@ export default class SVGLoader {
339
339
  (_b = this.onFinish) === null || _b === void 0 ? void 0 : _b.call(this);
340
340
  });
341
341
  }
342
- // TODO: Handling unsafe data! Tripple-check that this is secure!
343
342
  // @param sanitize - if `true`, don't store unknown attributes.
344
343
  static fromString(text, sanitize = false) {
345
344
  var _a, _b;
@@ -28,6 +28,7 @@ export default abstract class AbstractComponent {
28
28
  protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
29
29
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
30
  transformBy(affineTransfm: Mat33): SerializableCommand;
31
+ isSelectable(): boolean;
31
32
  private static transformElementCommandId;
32
33
  private static UnresolvedTransformElementCommand;
33
34
  private static TransformElementCommand;
@@ -48,6 +48,10 @@ export default class AbstractComponent {
48
48
  transformBy(affineTransfm) {
49
49
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
50
50
  }
51
+ // @returns true iff this component can be selected (e.g. by the selection tool.)
52
+ isSelectable() {
53
+ return true;
54
+ }
51
55
  // Returns a copy of this component.
52
56
  clone() {
53
57
  const clone = this.createClone();
@@ -12,6 +12,7 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
12
12
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
13
13
  intersects(_lineSegment: LineSegment2): boolean;
14
14
  protected applyTransformation(_affineTransfm: Mat33): void;
15
+ isSelectable(): boolean;
15
16
  protected createClone(): SVGGlobalAttributesObject;
16
17
  description(localization: ImageComponentLocalization): string;
17
18
  protected serializeToJSON(): string | null;
@@ -29,6 +29,9 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
29
29
  }
30
30
  applyTransformation(_affineTransfm) {
31
31
  }
32
+ isSelectable() {
33
+ return false;
34
+ }
32
35
  createClone() {
33
36
  return new SVGGlobalAttributesObject(this.attrs);
34
37
  }
@@ -12,16 +12,15 @@ export interface TextStyle {
12
12
  fontVariant?: string;
13
13
  renderingStyle: RenderingStyle;
14
14
  }
15
- declare type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
16
15
  export default class Text extends AbstractComponent {
17
16
  protected readonly textObjects: Array<string | Text>;
18
17
  private transform;
19
18
  private readonly style;
20
- private readonly getTextDimens;
21
19
  protected contentBBox: Rect2;
22
- constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle, getTextDimens?: GetTextDimensCallback);
20
+ constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
23
21
  static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
24
22
  private static textMeasuringCtx;
23
+ private static estimateTextDimens;
25
24
  private static getTextDimens;
26
25
  private computeBBoxOfPart;
27
26
  private recomputeBBox;
@@ -32,6 +31,5 @@ export default class Text extends AbstractComponent {
32
31
  getText(): string;
33
32
  description(localizationTable: ImageComponentLocalization): string;
34
33
  protected serializeToJSON(): Record<string, any>;
35
- static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text;
34
+ static deserializeFromString(json: any): Text;
36
35
  }
37
- export {};
@@ -5,15 +5,11 @@ import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  const componentTypeId = 'text';
7
7
  export default class Text extends AbstractComponent {
8
- constructor(textObjects, transform, style,
9
- // If not given, an HtmlCanvasElement is used to determine text boundaries.
10
- // @internal
11
- getTextDimens = Text.getTextDimens) {
8
+ constructor(textObjects, transform, style) {
12
9
  super(componentTypeId);
13
10
  this.textObjects = textObjects;
14
11
  this.transform = transform;
15
12
  this.style = style;
16
- this.getTextDimens = getTextDimens;
17
13
  this.recomputeBBox();
18
14
  }
19
15
  static applyTextStyles(ctx, style) {
@@ -28,9 +24,21 @@ export default class Text extends AbstractComponent {
28
24
  ].join(' ');
29
25
  ctx.textAlign = 'left';
30
26
  }
27
+ // Roughly estimate the bounding box of `text`. Use if no CanvasRenderingContext2D is available.
28
+ static estimateTextDimens(text, style) {
29
+ const widthEst = text.length * style.size;
30
+ const heightEst = style.size;
31
+ // Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should
32
+ // be above (0, 0).
33
+ return new Rect2(0, -heightEst * 2 / 3, widthEst, heightEst);
34
+ }
35
+ // Returns the bounding box of `text`. This is approximate if no Canvas is available.
31
36
  static getTextDimens(text, style) {
32
- var _a;
33
- (_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
37
+ var _a, _b;
38
+ (_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null);
39
+ if (!Text.textMeasuringCtx) {
40
+ return this.estimateTextDimens(text, style);
41
+ }
34
42
  const ctx = Text.textMeasuringCtx;
35
43
  Text.applyTextStyles(ctx, style);
36
44
  const measure = ctx.measureText(text);
@@ -41,7 +49,7 @@ export default class Text extends AbstractComponent {
41
49
  }
42
50
  computeBBoxOfPart(part) {
43
51
  if (typeof part === 'string') {
44
- const textBBox = this.getTextDimens(part, this.style);
52
+ const textBBox = Text.getTextDimens(part, this.style);
45
53
  return textBBox.transformedBoundingBox(this.transform);
46
54
  }
47
55
  else {
@@ -138,7 +146,7 @@ export default class Text extends AbstractComponent {
138
146
  style: serializableStyle,
139
147
  };
140
148
  }
141
- static deserializeFromString(json, getTextDimens = Text.getTextDimens) {
149
+ static deserializeFromString(json) {
142
150
  const style = {
143
151
  renderingStyle: styleFromJSON(json.style.renderingStyle),
144
152
  size: json.style.size,
@@ -159,7 +167,8 @@ export default class Text extends AbstractComponent {
159
167
  }
160
168
  const transformData = json.transform;
161
169
  const transform = new Mat33(...transformData);
162
- return new Text(textObjects, transform, style, getTextDimens);
170
+ return new Text(textObjects, transform, style);
163
171
  }
164
172
  }
173
+ Text.textMeasuringCtx = null;
165
174
  AbstractComponent.registerComponent(componentTypeId, (data) => Text.deserializeFromString(data));
@@ -11,6 +11,7 @@ export default class UnknownSVGObject extends AbstractComponent {
11
11
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
12
12
  intersects(lineSegment: LineSegment2): boolean;
13
13
  protected applyTransformation(_affineTransfm: Mat33): void;
14
+ isSelectable(): boolean;
14
15
  protected createClone(): AbstractComponent;
15
16
  description(localization: ImageComponentLocalization): string;
16
17
  protected serializeToJSON(): string | null;
@@ -25,6 +25,9 @@ export default class UnknownSVGObject extends AbstractComponent {
25
25
  }
26
26
  applyTransformation(_affineTransfm) {
27
27
  }
28
+ isSelectable() {
29
+ return false;
30
+ }
28
31
  createClone() {
29
32
  return new UnknownSVGObject(this.svgObject.cloneNode(true));
30
33
  }
@@ -312,8 +312,8 @@ export default class FreehandLineBuilder {
312
312
  }
313
313
  let enteringVec = this.lastExitingVec;
314
314
  if (!enteringVec) {
315
- let sampleIdx = Math.ceil(this.buffer.length / 3);
316
- if (sampleIdx === 0) {
315
+ let sampleIdx = Math.ceil(this.buffer.length / 2);
316
+ if (sampleIdx === 0 || sampleIdx >= this.buffer.length) {
317
317
  sampleIdx = this.buffer.length - 1;
318
318
  }
319
319
  enteringVec = this.buffer[sampleIdx].minus(this.buffer[0]);
@@ -1,3 +1,7 @@
1
1
  import loadExpectExtensions from './loadExpectExtensions';
2
2
  loadExpectExtensions();
3
3
  jest.useFakeTimers();
4
+ // jsdom doesn't support HTMLCanvasElement#getContext — it logs an error
5
+ // to the console. Make it return null so we can handle a non-existent Canvas
6
+ // at runtime (e.g. use something else, if available).
7
+ HTMLCanvasElement.prototype.getContext = () => null;
@@ -2,7 +2,6 @@ import { EditorEventType } from '../types';
2
2
  import { coloris, init as colorisInit } from '@melloware/coloris';
3
3
  import Color4 from '../Color4';
4
4
  import { defaultToolbarLocalization } from './localization';
5
- import { makeRedoIcon, makeUndoIcon } from './icons';
6
5
  import SelectionTool from '../tools/SelectionTool/SelectionTool';
7
6
  import PanZoomTool from '../tools/PanZoom';
8
7
  import TextTool from '../tools/TextTool';
@@ -123,13 +122,13 @@ export default class HTMLToolbar {
123
122
  undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
124
123
  const undoButton = this.addActionButton({
125
124
  label: this.localizationTable.undo,
126
- icon: makeUndoIcon()
125
+ icon: this.editor.icons.makeUndoIcon()
127
126
  }, () => {
128
127
  this.editor.history.undo();
129
128
  }, undoRedoGroup);
130
129
  const redoButton = this.addActionButton({
131
130
  label: this.localizationTable.redo,
132
- icon: makeRedoIcon(),
131
+ icon: this.editor.icons.makeRedoIcon(),
133
132
  }, () => {
134
133
  this.editor.history.redo();
135
134
  }, undoRedoGroup);
@@ -0,0 +1,24 @@
1
+ import Color4 from '../Color4';
2
+ import { ComponentBuilderFactory } from '../components/builders/types';
3
+ import { TextStyle } from '../components/Text';
4
+ import Pen from '../tools/Pen';
5
+ export default class IconProvider {
6
+ makeUndoIcon(): SVGSVGElement;
7
+ makeRedoIcon(mirror?: boolean): SVGSVGElement;
8
+ makeDropdownIcon(): SVGSVGElement;
9
+ makeEraserIcon(): SVGSVGElement;
10
+ makeSelectionIcon(): SVGSVGElement;
11
+ protected makeIconFromPath(pathData: string, fill?: string, strokeColor?: string, strokeWidth?: string): SVGSVGElement;
12
+ makeHandToolIcon(): SVGSVGElement;
13
+ makeTouchPanningIcon(): SVGSVGElement;
14
+ makeAllDevicePanningIcon(): SVGSVGElement;
15
+ makeZoomIcon: () => SVGSVGElement;
16
+ makeTextIcon(textStyle: TextStyle): SVGSVGElement;
17
+ makePenIcon(tipThickness: number, color: string | Color4): SVGSVGElement;
18
+ makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): SVGSVGElement;
19
+ makePipetteIcon(color?: Color4): SVGSVGElement;
20
+ makeResizeViewportIcon(): SVGSVGElement;
21
+ makeDuplicateSelectionIcon(): SVGSVGElement;
22
+ makeDeleteSelectionIcon(): SVGSVGElement;
23
+ makeSaveIcon(): SVGSVGElement;
24
+ }