js-draw 0.10.2 → 0.11.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 (47) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +72 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +3 -1
  5. package/dist/src/Editor.js +52 -15
  6. package/dist/src/SVGLoader.js +3 -2
  7. package/dist/src/components/AbstractComponent.d.ts +1 -0
  8. package/dist/src/components/AbstractComponent.js +15 -6
  9. package/dist/src/components/ImageComponent.d.ts +3 -0
  10. package/dist/src/components/ImageComponent.js +12 -1
  11. package/dist/src/localizations/es.js +1 -1
  12. package/dist/src/rendering/renderers/SVGRenderer.js +9 -5
  13. package/dist/src/toolbar/HTMLToolbar.js +2 -1
  14. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  15. package/dist/src/toolbar/IconProvider.js +7 -0
  16. package/dist/src/toolbar/localization.d.ts +8 -0
  17. package/dist/src/toolbar/localization.js +8 -0
  18. package/dist/src/toolbar/widgets/InsertImageWidget.d.ts +19 -0
  19. package/dist/src/toolbar/widgets/InsertImageWidget.js +169 -0
  20. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  21. package/dist/src/toolbar/widgets/lib.js +1 -0
  22. package/dist/src/tools/PanZoom.d.ts +2 -1
  23. package/dist/src/tools/PanZoom.js +43 -17
  24. package/dist/src/tools/PasteHandler.js +1 -39
  25. package/dist/src/util/fileToBase64.d.ts +3 -0
  26. package/dist/src/util/fileToBase64.js +13 -0
  27. package/dist/src/util/waitForTimeout.d.ts +2 -0
  28. package/dist/src/util/waitForTimeout.js +7 -0
  29. package/package.json +11 -11
  30. package/src/Editor.ts +66 -16
  31. package/src/SVGLoader.ts +1 -0
  32. package/src/components/AbstractComponent.ts +18 -4
  33. package/src/components/ImageComponent.ts +15 -0
  34. package/src/localizations/es.ts +3 -0
  35. package/src/rendering/renderers/SVGRenderer.ts +6 -1
  36. package/src/toolbar/HTMLToolbar.ts +3 -1
  37. package/src/toolbar/IconProvider.ts +8 -0
  38. package/src/toolbar/localization.ts +19 -1
  39. package/src/toolbar/toolbar.css +2 -0
  40. package/src/toolbar/widgets/InsertImageWidget.css +44 -0
  41. package/src/toolbar/widgets/InsertImageWidget.ts +222 -0
  42. package/src/toolbar/widgets/lib.ts +2 -0
  43. package/src/tools/PanZoom.test.ts +65 -0
  44. package/src/tools/PanZoom.ts +46 -14
  45. package/src/tools/PasteHandler.ts +2 -51
  46. package/src/util/fileToBase64.ts +18 -0
  47. package/src/util/waitForTimeout.ts +9 -0
@@ -37,19 +37,19 @@ class InertialScroller {
37
37
  if (this.running) {
38
38
  return;
39
39
  }
40
- let currentVelocity = this.initialVelocity;
40
+ this.currentVelocity = this.initialVelocity;
41
41
  let lastTime = (new Date()).getTime();
42
42
  this.running = true;
43
- const maxSpeed = 8000; // units/s
43
+ const maxSpeed = 5000; // units/s
44
44
  const minSpeed = 200; // units/s
45
- if (currentVelocity.magnitude() > maxSpeed) {
46
- currentVelocity = currentVelocity.normalized().times(maxSpeed);
45
+ if (this.currentVelocity.magnitude() > maxSpeed) {
46
+ this.currentVelocity = this.currentVelocity.normalized().times(maxSpeed);
47
47
  }
48
- while (this.running && currentVelocity.magnitude() > minSpeed) {
48
+ while (this.running && this.currentVelocity.magnitude() > minSpeed) {
49
49
  const nowTime = (new Date()).getTime();
50
50
  const dt = (nowTime - lastTime) / 1000;
51
- currentVelocity = currentVelocity.times(Math.pow(1 / 8, dt));
52
- this.scrollBy(currentVelocity.times(dt));
51
+ this.currentVelocity = this.currentVelocity.times(Math.pow(1 / 8, dt));
52
+ this.scrollBy(this.currentVelocity.times(dt));
53
53
  yield untilNextAnimationFrame();
54
54
  lastTime = nowTime;
55
55
  }
@@ -58,6 +58,12 @@ class InertialScroller {
58
58
  }
59
59
  });
60
60
  }
61
+ getCurrentVelocity() {
62
+ if (!this.running) {
63
+ return null;
64
+ }
65
+ return this.currentVelocity;
66
+ }
61
67
  stop() {
62
68
  if (this.running) {
63
69
  this.running = false;
@@ -71,6 +77,7 @@ export default class PanZoom extends BaseTool {
71
77
  this.editor = editor;
72
78
  this.mode = mode;
73
79
  this.transform = null;
80
+ this.lastPointerDownTimestamp = 0;
74
81
  this.inertialScroller = null;
75
82
  this.velocity = null;
76
83
  }
@@ -86,10 +93,13 @@ export default class PanZoom extends BaseTool {
86
93
  allPointersAreOfType(pointers, kind) {
87
94
  return pointers.every(pointer => pointer.device === kind);
88
95
  }
89
- onPointerDown({ allPointers: pointers }) {
90
- var _a, _b;
96
+ onPointerDown({ allPointers: pointers, current: currentPointer }) {
97
+ var _a, _b, _c, _d;
91
98
  let handlingGesture = false;
92
- (_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
99
+ const inertialScrollerVelocity = (_b = (_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.getCurrentVelocity()) !== null && _b !== void 0 ? _b : Vec2.zero;
100
+ (_c = this.inertialScroller) === null || _c === void 0 ? void 0 : _c.stop();
101
+ this.velocity = inertialScrollerVelocity;
102
+ this.lastPointerDownTimestamp = currentPointer.timeStamp;
93
103
  const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
94
104
  const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
95
105
  if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
@@ -107,22 +117,24 @@ export default class PanZoom extends BaseTool {
107
117
  }
108
118
  if (handlingGesture) {
109
119
  this.lastTimestamp = (new Date()).getTime();
110
- (_b = this.transform) !== null && _b !== void 0 ? _b : (this.transform = Viewport.transformBy(Mat33.identity));
120
+ (_d = this.transform) !== null && _d !== void 0 ? _d : (this.transform = Viewport.transformBy(Mat33.identity));
111
121
  this.editor.display.setDraftMode(true);
112
122
  }
113
123
  return handlingGesture;
114
124
  }
115
125
  updateVelocity(currentCenter) {
116
126
  const deltaPos = currentCenter.minus(this.lastScreenCenter);
117
- const deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
118
- // We divide by deltaTime. Don't divide by zero.
119
- if (deltaTime === 0) {
120
- return;
121
- }
127
+ let deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
122
128
  // Ignore duplicate events, unless there has been enough time between them.
123
129
  if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
124
130
  return;
125
131
  }
132
+ // We divide by deltaTime. Don't divide by zero.
133
+ if (deltaTime === 0) {
134
+ return;
135
+ }
136
+ // Don't divide by almost zero, either
137
+ deltaTime = Math.max(deltaTime, 0.01);
126
138
  const currentVelocity = deltaPos.times(1 / deltaTime);
127
139
  let smoothedVelocity = currentVelocity;
128
140
  if (this.velocity) {
@@ -184,12 +196,26 @@ export default class PanZoom extends BaseTool {
184
196
  this.transform = null;
185
197
  this.velocity = Vec2.zero;
186
198
  };
187
- const shouldInertialScroll = event.current.device === PointerDevice.Touch && event.allPointers.length === 1;
199
+ const minInertialScrollDt = 30;
200
+ const shouldInertialScroll = event.current.device === PointerDevice.Touch
201
+ && event.allPointers.length === 1
202
+ && this.velocity !== null
203
+ && event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
188
204
  if (shouldInertialScroll && this.velocity !== null) {
205
+ const oldVelocity = this.velocity;
189
206
  // If the user drags the screen, then stops, then lifts the pointer,
190
207
  // we want the final velocity to reflect the stop at the end (so the velocity
191
208
  // should be near zero). Handle this:
192
209
  this.updateVelocity(event.current.screenPos);
210
+ // Work around an input issue. Some devices that disable the touchscreen when a stylus
211
+ // comes near the screen fire a touch-end event at the position of the stylus when a
212
+ // touch gesture is canceled. Because the stylus is often far away from the last touch,
213
+ // this causes a great displacement between the second-to-last (from the touchscreen) and
214
+ // last (from the pen that is now near the screen) events. Only allow velocity to decrease
215
+ // to work around this:
216
+ if (oldVelocity.magnitude() < this.velocity.magnitude()) {
217
+ this.velocity = oldVelocity;
218
+ }
193
219
  // Cancel any ongoing inertial scrolling.
194
220
  (_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
195
221
  this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
@@ -12,16 +12,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  import { TextComponent } from '../components/lib';
15
- import { uniteCommands } from '../commands/lib';
16
15
  import SVGLoader from '../SVGLoader';
17
16
  import { Mat33 } from '../math/lib';
18
17
  import BaseTool from './BaseTool';
19
- import EditorImage from '../EditorImage';
20
- import SelectionTool from './SelectionTool/SelectionTool';
21
18
  import TextTool from './TextTool';
22
19
  import Color4 from '../Color4';
23
20
  import ImageComponent from '../components/ImageComponent';
24
- import Viewport from '../Viewport';
25
21
  // { @inheritDoc PasteHandler! }
26
22
  export default class PasteHandler extends BaseTool {
27
23
  constructor(editor) {
@@ -46,41 +42,7 @@ export default class PasteHandler extends BaseTool {
46
42
  }
47
43
  addComponentsFromPaste(components) {
48
44
  return __awaiter(this, void 0, void 0, function* () {
49
- let bbox = null;
50
- for (const component of components) {
51
- if (bbox) {
52
- bbox = bbox.union(component.getBBox());
53
- }
54
- else {
55
- bbox = component.getBBox();
56
- }
57
- }
58
- if (!bbox) {
59
- return;
60
- }
61
- // Find a transform that scales/moves bbox onto the screen.
62
- const visibleRect = this.editor.viewport.visibleRect;
63
- const scaleRatioX = visibleRect.width / bbox.width;
64
- const scaleRatioY = visibleRect.height / bbox.height;
65
- let scaleRatio = scaleRatioX;
66
- if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
67
- scaleRatio = scaleRatioY;
68
- }
69
- scaleRatio *= 2 / 3;
70
- scaleRatio = Viewport.roundScaleRatio(scaleRatio);
71
- const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
72
- const commands = [];
73
- for (const component of components) {
74
- // To allow deserialization, we need to add first, then transform.
75
- commands.push(EditorImage.addElement(component));
76
- commands.push(component.transformBy(transfm));
77
- }
78
- const applyChunkSize = 100;
79
- this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
80
- for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
81
- selectionTool.setEnabled(true);
82
- selectionTool.setSelection(components);
83
- }
45
+ yield this.editor.addAndCenterComponents(components);
84
46
  });
85
47
  }
86
48
  doSVGPaste(data) {
@@ -0,0 +1,3 @@
1
+ type ProgressListener = (evt: ProgressEvent<FileReader>) => void;
2
+ declare const fileToBase64: (file: File, onprogress?: ProgressListener) => Promise<string | null>;
3
+ export default fileToBase64;
@@ -0,0 +1,13 @@
1
+ const fileToBase64 = (file, onprogress) => {
2
+ const reader = new FileReader();
3
+ return new Promise((resolve, reject) => {
4
+ reader.onload = () => resolve(reader.result);
5
+ reader.onerror = reject;
6
+ reader.onabort = reject;
7
+ reader.onprogress = (evt) => {
8
+ onprogress === null || onprogress === void 0 ? void 0 : onprogress(evt);
9
+ };
10
+ reader.readAsDataURL(file);
11
+ });
12
+ };
13
+ export default fileToBase64;
@@ -0,0 +1,2 @@
1
+ declare const waitForTimeout: (timeout: number) => Promise<void>;
2
+ export default waitForTimeout;
@@ -0,0 +1,7 @@
1
+ // Returns a promise that resolves after `timeout` milliseconds.
2
+ const waitForTimeout = (timeout) => {
3
+ return new Promise(resolve => {
4
+ setTimeout(() => resolve(), timeout);
5
+ });
6
+ };
7
+ export default waitForTimeout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.10.2",
3
+ "version": "0.11.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",
@@ -83,26 +83,26 @@
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/bezier-js": "^4.1.0",
86
- "@types/jest": "^29.2.3",
86
+ "@types/jest": "^29.2.5",
87
87
  "@types/jsdom": "^20.0.1",
88
- "@types/node": "^18.11.9",
88
+ "@types/node": "^18.11.18",
89
89
  "@typescript-eslint/eslint-plugin": "^5.44.0",
90
90
  "@typescript-eslint/parser": "^5.44.0",
91
- "css-loader": "^6.7.2",
92
- "eslint": "^8.28.0",
93
- "husky": "^8.0.2",
94
- "jest": "^29.2.3",
91
+ "css-loader": "^6.7.3",
92
+ "eslint": "^8.31.0",
93
+ "husky": "^8.0.3",
94
+ "jest": "^29.3.1",
95
95
  "jest-environment-jsdom": "^29.3.1",
96
96
  "jsdom": "^20.0.3",
97
- "lint-staged": "^13.0.3",
97
+ "lint-staged": "^13.1.0",
98
98
  "pinst": "^3.0.0",
99
99
  "style-loader": "^3.3.1",
100
100
  "terser-webpack-plugin": "^5.3.6",
101
101
  "ts-jest": "^29.0.3",
102
- "ts-loader": "^9.4.1",
102
+ "ts-loader": "^9.4.2",
103
103
  "ts-node": "^10.9.1",
104
- "typedoc": "^0.23.21",
105
- "typescript": "^4.9.3",
104
+ "typedoc": "^0.23.23",
105
+ "typescript": "^4.9.4",
106
106
  "webpack": "^5.75.0"
107
107
  },
108
108
  "bugs": {
package/src/Editor.ts CHANGED
@@ -41,6 +41,10 @@ import IconProvider from './toolbar/IconProvider';
41
41
  import { toRoundedString } from './math/rounding';
42
42
  import CanvasRenderer from './rendering/renderers/CanvasRenderer';
43
43
  import untilNextAnimationFrame from './util/untilNextAnimationFrame';
44
+ import fileToBase64 from './util/fileToBase64';
45
+ import uniteCommands from './commands/uniteCommands';
46
+ import SelectionTool from './tools/SelectionTool/SelectionTool';
47
+ import AbstractComponent from './components/AbstractComponent';
44
48
 
45
49
  type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
46
50
  type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
@@ -513,20 +517,14 @@ export class Editor {
513
517
  for (const file of clipboardData.files) {
514
518
  const fileType = file.type.toLowerCase();
515
519
  if (fileType === 'image/png' || fileType === 'image/jpg') {
516
- const reader = new FileReader();
517
-
518
520
  this.showLoadingWarning(0);
521
+ const onprogress = (evt: ProgressEvent<FileReader>) => {
522
+ this.showLoadingWarning(evt.loaded / evt.total);
523
+ };
524
+
519
525
  try {
520
- const data = await new Promise((resolve: (result: string|null)=>void, reject) => {
521
- reader.onload = () => resolve(reader.result as string|null);
522
- reader.onerror = reject;
523
- reader.onabort = reject;
524
- reader.onprogress = (evt) => {
525
- this.showLoadingWarning(evt.loaded / evt.total);
526
- };
527
-
528
- reader.readAsDataURL(file);
529
- });
526
+ const data = await fileToBase64(file, onprogress);
527
+
530
528
  if (data && this.toolController.dispatchInputEvent({
531
529
  kind: InputEvtType.PasteEvent,
532
530
  mime: fileType,
@@ -629,13 +627,14 @@ export class Editor {
629
627
  /** `apply` a command. `command` will be announced for accessibility. */
630
628
  public dispatch(command: Command, addToHistory: boolean = true) {
631
629
  if (addToHistory) {
632
- // .push applies [command] to this
633
- this.history.push(command);
634
- } else {
635
- command.apply(this);
630
+ const apply = false; // Don't double-apply
631
+ this.history.push(command, apply);
636
632
  }
637
633
 
634
+ const applyResult = command.apply(this);
638
635
  this.announceForAccessibility(command.description(this, this.localization));
636
+
637
+ return applyResult;
639
638
  }
640
639
 
641
640
  /**
@@ -825,6 +824,57 @@ export class Editor {
825
824
  });
826
825
  }
827
826
 
827
+ public async addAndCenterComponents(components: AbstractComponent[], selectComponents: boolean = true) {
828
+ let bbox: Rect2|null = null;
829
+ for (const component of components) {
830
+ if (bbox) {
831
+ bbox = bbox.union(component.getBBox());
832
+ } else {
833
+ bbox = component.getBBox();
834
+ }
835
+ }
836
+
837
+ if (!bbox) {
838
+ return;
839
+ }
840
+
841
+ // Find a transform that scales/moves bbox onto the screen.
842
+ const visibleRect = this.viewport.visibleRect;
843
+ const scaleRatioX = visibleRect.width / bbox.width;
844
+ const scaleRatioY = visibleRect.height / bbox.height;
845
+
846
+ let scaleRatio = scaleRatioX;
847
+ if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
848
+ scaleRatio = scaleRatioY;
849
+ }
850
+ scaleRatio *= 2 / 3;
851
+
852
+ scaleRatio = Viewport.roundScaleRatio(scaleRatio);
853
+
854
+ const transfm = Mat33.translation(
855
+ visibleRect.center.minus(bbox.center)
856
+ ).rightMul(
857
+ Mat33.scaling2D(scaleRatio, bbox.center)
858
+ );
859
+
860
+ const commands: Command[] = [];
861
+ for (const component of components) {
862
+ // To allow deserialization, we need to add first, then transform.
863
+ commands.push(EditorImage.addElement(component));
864
+ commands.push(component.transformBy(transfm));
865
+ }
866
+
867
+ const applyChunkSize = 100;
868
+ await this.dispatch(uniteCommands(commands, applyChunkSize), true);
869
+
870
+ if (selectComponents) {
871
+ for (const selectionTool of this.toolController.getMatchingTools(SelectionTool)) {
872
+ selectionTool.setEnabled(true);
873
+ selectionTool.setSelection(components);
874
+ }
875
+ }
876
+ }
877
+
828
878
  // Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
829
879
  // If `format` is not `image/png`, a PNG image URL may still be returned (as in the
830
880
  // case of `HTMLCanvasElement::toDataURL`).
package/src/SVGLoader.ts CHANGED
@@ -287,6 +287,7 @@ export default class SVGLoader implements ImageLoader {
287
287
  private async addImage(elem: SVGImageElement) {
288
288
  const image = new Image();
289
289
  image.src = elem.getAttribute('xlink:href') ?? elem.href.baseVal;
290
+ image.setAttribute('alt', elem.getAttribute('aria-label') ?? '');
290
291
 
291
292
  try {
292
293
  const supportedAttrs: string[] = [];
@@ -90,6 +90,11 @@ export default abstract class AbstractComponent {
90
90
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
91
91
  }
92
92
 
93
+ // Returns a command that updates this component's z-index.
94
+ public setZIndex(newZIndex: number): SerializableCommand {
95
+ return new AbstractComponent.TransformElementCommand(Mat33.identity, this, newZIndex);
96
+ }
97
+
93
98
  // @returns true iff this component can be selected (e.g. by the selection tool.)
94
99
  public isSelectable(): boolean {
95
100
  return true;
@@ -110,6 +115,7 @@ export default abstract class AbstractComponent {
110
115
  public constructor(
111
116
  private affineTransfm: Mat33,
112
117
  private componentID: string,
118
+ private targetZIndex?: number,
113
119
  ) {
114
120
  super(AbstractComponent.transformElementCommandId);
115
121
  }
@@ -123,7 +129,9 @@ export default abstract class AbstractComponent {
123
129
  if (!component) {
124
130
  throw new Error(`Unable to resolve component with ID ${this.componentID}`);
125
131
  }
126
- this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component);
132
+ this.command = new AbstractComponent.TransformElementCommand(
133
+ this.affineTransfm, component, this.targetZIndex
134
+ );
127
135
  }
128
136
 
129
137
  public apply(editor: Editor) {
@@ -144,19 +152,23 @@ export default abstract class AbstractComponent {
144
152
  return {
145
153
  id: this.componentID,
146
154
  transfm: this.affineTransfm.toArray(),
155
+ targetZIndex: this.targetZIndex,
147
156
  };
148
157
  }
149
158
  };
150
159
 
151
160
  private static TransformElementCommand = class extends SerializableCommand {
152
161
  private origZIndex: number;
162
+ private targetZIndex: number;
153
163
 
154
164
  public constructor(
155
165
  private affineTransfm: Mat33,
156
166
  private component: AbstractComponent,
167
+ targetZIndex?: number,
157
168
  ) {
158
169
  super(AbstractComponent.transformElementCommandId);
159
170
  this.origZIndex = component.zIndex;
171
+ this.targetZIndex = targetZIndex ?? AbstractComponent.zIndexCounter++;
160
172
  }
161
173
 
162
174
  private updateTransform(editor: Editor, newTransfm: Mat33) {
@@ -177,7 +189,7 @@ export default abstract class AbstractComponent {
177
189
  }
178
190
 
179
191
  public apply(editor: Editor) {
180
- this.component.zIndex = AbstractComponent.zIndexCounter++;
192
+ this.component.zIndex = this.targetZIndex;
181
193
  this.updateTransform(editor, this.affineTransfm);
182
194
  editor.queueRerender();
183
195
  }
@@ -195,16 +207,17 @@ export default abstract class AbstractComponent {
195
207
  static {
196
208
  SerializableCommand.register(AbstractComponent.transformElementCommandId, (json: any, editor: Editor) => {
197
209
  const elem = editor.image.lookupElement(json.id);
198
-
199
210
  const transform = new Mat33(...(json.transfm as Mat33Array));
211
+ const targetZIndex = json.targetZIndex;
200
212
 
201
213
  if (!elem) {
202
- return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
214
+ return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id, targetZIndex);
203
215
  }
204
216
 
205
217
  return new AbstractComponent.TransformElementCommand(
206
218
  transform,
207
219
  elem,
220
+ targetZIndex,
208
221
  );
209
222
  });
210
223
  }
@@ -213,6 +226,7 @@ export default abstract class AbstractComponent {
213
226
  return {
214
227
  id: this.component.getId(),
215
228
  transfm: this.affineTransfm.toArray(),
229
+ targetZIndex: this.targetZIndex,
216
230
  };
217
231
  }
218
232
  };
@@ -77,6 +77,9 @@ export default class ImageComponent extends AbstractComponent {
77
77
  image.height = height;
78
78
  }
79
79
 
80
+ image.setAttribute('alt', elem.getAttribute('alt') ?? '');
81
+ image.setAttribute('aria-label', elem.getAttribute('aria-label') ?? '');
82
+
80
83
  return new ImageComponent({
81
84
  image,
82
85
  base64Url: url,
@@ -126,6 +129,18 @@ export default class ImageComponent extends AbstractComponent {
126
129
  return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
127
130
  }
128
131
 
132
+ public getAltText() {
133
+ return this.image.label;
134
+ }
135
+
136
+ public getURL() {
137
+ return this.image.base64Url;
138
+ }
139
+
140
+ public getTransformation(): Mat33 {
141
+ return this.image.transform;
142
+ }
143
+
129
144
  protected createClone(): AbstractComponent {
130
145
  return new ImageComponent({
131
146
  ...this.image,
@@ -58,6 +58,9 @@ const localization: EditorLocalization = {
58
58
  textTool: 'Texto',
59
59
  enterTextToInsert: 'Entra texto',
60
60
  rerenderAsText: 'Redibuja la pantalla al texto',
61
+ image: 'Imagen',
62
+ imageSize: (size: number, units: string) => `Tamaño del imagen: ${size} ${units}`,
63
+ imageLoadError: (message: string)=> `Error cargando imagen: ${message}`,
61
64
  };
62
65
 
63
66
  export default localization;
@@ -217,11 +217,16 @@ export default class SVGRenderer extends AbstractRenderer {
217
217
  }
218
218
 
219
219
  public drawImage(image: RenderableImage) {
220
+ let label = image.label ?? image.image.getAttribute('aria-label') ?? '';
221
+ if (label === '') {
222
+ label = image.image.getAttribute('alt') ?? '';
223
+ }
224
+
220
225
  const svgImgElem = document.createElementNS(svgNameSpace, 'image');
221
226
  svgImgElem.setAttribute('href', image.base64Url);
222
227
  svgImgElem.setAttribute('width', image.image.getAttribute('width') ?? '');
223
228
  svgImgElem.setAttribute('height', image.image.getAttribute('height') ?? '');
224
- svgImgElem.setAttribute('aria-label', image.image.getAttribute('aria-label') ?? image.image.getAttribute('alt') ?? '');
229
+ svgImgElem.setAttribute('aria-label', label);
225
230
  this.transformFrom(image.transform, svgImgElem);
226
231
 
227
232
  this.elem.appendChild(svgImgElem);
@@ -16,7 +16,7 @@ import SelectionToolWidget from './widgets/SelectionToolWidget';
16
16
  import TextToolWidget from './widgets/TextToolWidget';
17
17
  import HandToolWidget from './widgets/HandToolWidget';
18
18
  import BaseWidget from './widgets/BaseWidget';
19
- import { ActionButtonWidget } from './lib';
19
+ import { ActionButtonWidget, InsertImageWidget } from './lib';
20
20
 
21
21
  export const toolbarCSSPrefix = 'toolbar-';
22
22
 
@@ -236,6 +236,8 @@ export default class HTMLToolbar {
236
236
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
237
237
  }
238
238
 
239
+ this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
240
+
239
241
  const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
240
242
  if (panZoomTool) {
241
243
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
@@ -323,6 +323,14 @@ export default class IconProvider {
323
323
 
324
324
  return icon;
325
325
  }
326
+
327
+ public makeInsertImageIcon(): IconType {
328
+ return this.makeIconFromPath(`
329
+ M 5 10 L 5 90 L 95 90 L 95 10 L 5 10 z
330
+ M 10 15 L 90 15 L 90 50 L 70 75 L 40 50 L 10 75 L 10 15 z
331
+ M 22.5 25 A 7.5 7.5 0 0 0 15 32.5 A 7.5 7.5 0 0 0 22.5 40 A 7.5 7.5 0 0 0 30 32.5 A 7.5 7.5 0 0 0 22.5 25 z
332
+ `);
333
+ }
326
334
 
327
335
  public makeTextIcon(textStyle: TextStyle): IconType {
328
336
  const icon = document.createElementNS(svgNamespace, 'svg');
@@ -9,6 +9,11 @@ export interface ToolbarLocalization {
9
9
  filledRectanglePen: string;
10
10
  linePen: string;
11
11
  arrowPen: string;
12
+ image: string;
13
+ inputAltText: string;
14
+ chooseFile: string;
15
+ cancel: string;
16
+ submit: string;
12
17
  freehandPen: string;
13
18
  pressureSensitiveFreehandPen: string;
14
19
  selectObjectType: string;
@@ -30,10 +35,14 @@ export interface ToolbarLocalization {
30
35
  selectionToolKeyboardShortcuts: string;
31
36
  paste: string;
32
37
 
38
+ errorImageHasZeroSize: string;
39
+
33
40
  dropdownShown: (toolName: string)=> string;
34
41
  dropdownHidden: (toolName: string)=> string;
35
42
  zoomLevel: (zoomPercentage: number)=> string;
36
43
  colorChangedAnnouncement: (color: string)=> string;
44
+ imageSize: (size: number, units: string)=> string;
45
+ imageLoadError: (message: string)=> string;
37
46
  }
38
47
 
39
48
  export const defaultToolbarLocalization: ToolbarLocalization = {
@@ -42,6 +51,11 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
42
51
  select: 'Select',
43
52
  handTool: 'Pan',
44
53
  zoom: 'Zoom',
54
+ image: 'Image',
55
+ inputAltText: 'Alt text: ',
56
+ chooseFile: 'Choose file: ',
57
+ submit: 'Submit',
58
+ cancel: 'Cancel',
45
59
  resetView: 'Reset view',
46
60
  thicknessLabel: 'Thickness: ',
47
61
  colorLabel: 'Color: ',
@@ -72,5 +86,9 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
72
86
  dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
73
87
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
74
88
  zoomLevel: (zoomPercent: number) => `Zoom: ${zoomPercent}%`,
75
- colorChangedAnnouncement: (color: string)=> `Color changed to ${color}`,
89
+ colorChangedAnnouncement: (color: string) => `Color changed to ${color}`,
90
+ imageSize: (size: number, units: string) => `Image size: ${size} ${units}`,
91
+
92
+ errorImageHasZeroSize: 'Error: Image has zero size',
93
+ imageLoadError: (message: string)=> `Error loading image: ${message}`,
76
94
  };
@@ -1,3 +1,5 @@
1
+ @import url(./widgets/InsertImageWidget.css);
2
+
1
3
  .toolbar-root {
2
4
  background-color: var(--primary-background-color);
3
5
  --icon-color: var(--primary-foreground-color);
@@ -0,0 +1,44 @@
1
+
2
+ .toolbar-image-selection-overlay {
3
+ position: absolute;
4
+
5
+ width: 100%;
6
+ height: 100%;
7
+ z-index: 10;
8
+
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ }
13
+
14
+ .toolbar-image-selection-overlay > div {
15
+ background: var(--primary-background-color);
16
+ box-shadow: 1px 1px 3px var(--primary-shadow-color);
17
+
18
+ padding: 18px;
19
+ border-radius: 3px;
20
+ }
21
+
22
+ .toolbar-image-selection-overlay > div > div {
23
+ padding: 5px;
24
+ }
25
+
26
+ .toolbar-image-selection-overlay img {
27
+ max-width: min(50vw, 75%);
28
+ max-height: 50vh;
29
+
30
+ /* Center */
31
+ display: block;
32
+ margin-left: auto;
33
+ margin-right: auto;
34
+ }
35
+
36
+ .toolbar-image-selection-overlay .action-button-row {
37
+ margin-top: 4px;
38
+ display: flex;
39
+ flex-direction: row;
40
+ }
41
+
42
+ .toolbar-image-selection-overlay .action-button-row > button {
43
+ flex-grow: 1;
44
+ }