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.
- package/.github/ISSUE_TEMPLATE/translation.yml +72 -0
- package/CHANGELOG.md +7 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +3 -1
- package/dist/src/Editor.js +52 -15
- package/dist/src/SVGLoader.js +3 -2
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +15 -6
- package/dist/src/components/ImageComponent.d.ts +3 -0
- package/dist/src/components/ImageComponent.js +12 -1
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +9 -5
- package/dist/src/toolbar/HTMLToolbar.js +2 -1
- package/dist/src/toolbar/IconProvider.d.ts +1 -0
- package/dist/src/toolbar/IconProvider.js +7 -0
- package/dist/src/toolbar/localization.d.ts +8 -0
- package/dist/src/toolbar/localization.js +8 -0
- package/dist/src/toolbar/widgets/InsertImageWidget.d.ts +19 -0
- package/dist/src/toolbar/widgets/InsertImageWidget.js +169 -0
- package/dist/src/toolbar/widgets/lib.d.ts +1 -0
- package/dist/src/toolbar/widgets/lib.js +1 -0
- package/dist/src/tools/PanZoom.d.ts +2 -1
- package/dist/src/tools/PanZoom.js +43 -17
- package/dist/src/tools/PasteHandler.js +1 -39
- package/dist/src/util/fileToBase64.d.ts +3 -0
- package/dist/src/util/fileToBase64.js +13 -0
- package/dist/src/util/waitForTimeout.d.ts +2 -0
- package/dist/src/util/waitForTimeout.js +7 -0
- package/package.json +11 -11
- package/src/Editor.ts +66 -16
- package/src/SVGLoader.ts +1 -0
- package/src/components/AbstractComponent.ts +18 -4
- package/src/components/ImageComponent.ts +15 -0
- package/src/localizations/es.ts +3 -0
- package/src/rendering/renderers/SVGRenderer.ts +6 -1
- package/src/toolbar/HTMLToolbar.ts +3 -1
- package/src/toolbar/IconProvider.ts +8 -0
- package/src/toolbar/localization.ts +19 -1
- package/src/toolbar/toolbar.css +2 -0
- package/src/toolbar/widgets/InsertImageWidget.css +44 -0
- package/src/toolbar/widgets/InsertImageWidget.ts +222 -0
- package/src/toolbar/widgets/lib.ts +2 -0
- package/src/tools/PanZoom.test.ts +65 -0
- package/src/tools/PanZoom.ts +46 -14
- package/src/tools/PasteHandler.ts +2 -51
- package/src/util/fileToBase64.ts +18 -0
- package/src/util/waitForTimeout.ts +9 -0
@@ -37,19 +37,19 @@ class InertialScroller {
|
|
37
37
|
if (this.running) {
|
38
38
|
return;
|
39
39
|
}
|
40
|
-
|
40
|
+
this.currentVelocity = this.initialVelocity;
|
41
41
|
let lastTime = (new Date()).getTime();
|
42
42
|
this.running = true;
|
43
|
-
const maxSpeed =
|
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.
|
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
|
-
(
|
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
|
-
|
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
|
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
|
-
|
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,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;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.
|
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.
|
86
|
+
"@types/jest": "^29.2.5",
|
87
87
|
"@types/jsdom": "^20.0.1",
|
88
|
-
"@types/node": "^18.11.
|
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.
|
92
|
-
"eslint": "^8.
|
93
|
-
"husky": "^8.0.
|
94
|
-
"jest": "^29.
|
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
|
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.
|
102
|
+
"ts-loader": "^9.4.2",
|
103
103
|
"ts-node": "^10.9.1",
|
104
|
-
"typedoc": "^0.23.
|
105
|
-
"typescript": "^4.9.
|
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
|
521
|
-
|
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
|
-
|
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(
|
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 =
|
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,
|
package/src/localizations/es.ts
CHANGED
@@ -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',
|
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
|
};
|
package/src/toolbar/toolbar.css
CHANGED
@@ -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
|
+
}
|