js-draw 0.10.3 → 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 +4 -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.js +10 -0
- 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 +1 -1
- 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 +12 -0
- package/src/tools/PasteHandler.ts +2 -51
- package/src/util/fileToBase64.ts +18 -0
- package/src/util/waitForTimeout.ts +9 -0
package/dist/src/Editor.d.ts
CHANGED
@@ -29,6 +29,7 @@ import Pointer from './Pointer';
|
|
29
29
|
import Rect2 from './math/Rect2';
|
30
30
|
import { EditorLocalization } from './localization';
|
31
31
|
import IconProvider from './toolbar/IconProvider';
|
32
|
+
import AbstractComponent from './components/AbstractComponent';
|
32
33
|
type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
|
33
34
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
|
34
35
|
export interface EditorSettings {
|
@@ -157,7 +158,7 @@ export declare class Editor {
|
|
157
158
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
158
159
|
handleKeyEventsFrom(elem: HTMLElement): void;
|
159
160
|
/** `apply` a command. `command` will be announced for accessibility. */
|
160
|
-
dispatch(command: Command, addToHistory?: boolean): void
|
161
|
+
dispatch(command: Command, addToHistory?: boolean): void | Promise<void>;
|
161
162
|
/**
|
162
163
|
* Dispatches a command without announcing it. By default, does not add to history.
|
163
164
|
* Use this to show finalized commands that don't need to have `announceForAccessibility`
|
@@ -197,6 +198,7 @@ export declare class Editor {
|
|
197
198
|
addStyleSheet(content: string): HTMLStyleElement;
|
198
199
|
sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean, altKey?: boolean): void;
|
199
200
|
sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
|
201
|
+
addAndCenterComponents(components: AbstractComponent[], selectComponents?: boolean): Promise<void>;
|
200
202
|
toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp'): string;
|
201
203
|
toSVG(): SVGElement;
|
202
204
|
loadFrom(loader: ImageLoader): Promise<void>;
|
package/dist/src/Editor.js
CHANGED
@@ -45,6 +45,9 @@ import IconProvider from './toolbar/IconProvider';
|
|
45
45
|
import { toRoundedString } from './math/rounding';
|
46
46
|
import CanvasRenderer from './rendering/renderers/CanvasRenderer';
|
47
47
|
import untilNextAnimationFrame from './util/untilNextAnimationFrame';
|
48
|
+
import fileToBase64 from './util/fileToBase64';
|
49
|
+
import uniteCommands from './commands/uniteCommands';
|
50
|
+
import SelectionTool from './tools/SelectionTool/SelectionTool';
|
48
51
|
// { @inheritDoc Editor! }
|
49
52
|
export class Editor {
|
50
53
|
/**
|
@@ -372,18 +375,12 @@ export class Editor {
|
|
372
375
|
for (const file of clipboardData.files) {
|
373
376
|
const fileType = file.type.toLowerCase();
|
374
377
|
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
375
|
-
const reader = new FileReader();
|
376
378
|
this.showLoadingWarning(0);
|
379
|
+
const onprogress = (evt) => {
|
380
|
+
this.showLoadingWarning(evt.loaded / evt.total);
|
381
|
+
};
|
377
382
|
try {
|
378
|
-
const data = yield
|
379
|
-
reader.onload = () => resolve(reader.result);
|
380
|
-
reader.onerror = reject;
|
381
|
-
reader.onabort = reject;
|
382
|
-
reader.onprogress = (evt) => {
|
383
|
-
this.showLoadingWarning(evt.loaded / evt.total);
|
384
|
-
};
|
385
|
-
reader.readAsDataURL(file);
|
386
|
-
});
|
383
|
+
const data = yield fileToBase64(file, onprogress);
|
387
384
|
if (data && this.toolController.dispatchInputEvent({
|
388
385
|
kind: InputEvtType.PasteEvent,
|
389
386
|
mime: fileType,
|
@@ -478,13 +475,12 @@ export class Editor {
|
|
478
475
|
/** `apply` a command. `command` will be announced for accessibility. */
|
479
476
|
dispatch(command, addToHistory = true) {
|
480
477
|
if (addToHistory) {
|
481
|
-
|
482
|
-
this.history.push(command);
|
483
|
-
}
|
484
|
-
else {
|
485
|
-
command.apply(this);
|
478
|
+
const apply = false; // Don't double-apply
|
479
|
+
this.history.push(command, apply);
|
486
480
|
}
|
481
|
+
const applyResult = command.apply(this);
|
487
482
|
this.announceForAccessibility(command.description(this, this.localization));
|
483
|
+
return applyResult;
|
488
484
|
}
|
489
485
|
/**
|
490
486
|
* Dispatches a command without announcing it. By default, does not add to history.
|
@@ -628,6 +624,47 @@ export class Editor {
|
|
628
624
|
current: mainPointer,
|
629
625
|
});
|
630
626
|
}
|
627
|
+
addAndCenterComponents(components, selectComponents = true) {
|
628
|
+
return __awaiter(this, void 0, void 0, function* () {
|
629
|
+
let bbox = null;
|
630
|
+
for (const component of components) {
|
631
|
+
if (bbox) {
|
632
|
+
bbox = bbox.union(component.getBBox());
|
633
|
+
}
|
634
|
+
else {
|
635
|
+
bbox = component.getBBox();
|
636
|
+
}
|
637
|
+
}
|
638
|
+
if (!bbox) {
|
639
|
+
return;
|
640
|
+
}
|
641
|
+
// Find a transform that scales/moves bbox onto the screen.
|
642
|
+
const visibleRect = this.viewport.visibleRect;
|
643
|
+
const scaleRatioX = visibleRect.width / bbox.width;
|
644
|
+
const scaleRatioY = visibleRect.height / bbox.height;
|
645
|
+
let scaleRatio = scaleRatioX;
|
646
|
+
if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
|
647
|
+
scaleRatio = scaleRatioY;
|
648
|
+
}
|
649
|
+
scaleRatio *= 2 / 3;
|
650
|
+
scaleRatio = Viewport.roundScaleRatio(scaleRatio);
|
651
|
+
const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
|
652
|
+
const commands = [];
|
653
|
+
for (const component of components) {
|
654
|
+
// To allow deserialization, we need to add first, then transform.
|
655
|
+
commands.push(EditorImage.addElement(component));
|
656
|
+
commands.push(component.transformBy(transfm));
|
657
|
+
}
|
658
|
+
const applyChunkSize = 100;
|
659
|
+
yield this.dispatch(uniteCommands(commands, applyChunkSize), true);
|
660
|
+
if (selectComponents) {
|
661
|
+
for (const selectionTool of this.toolController.getMatchingTools(SelectionTool)) {
|
662
|
+
selectionTool.setEnabled(true);
|
663
|
+
selectionTool.setSelection(components);
|
664
|
+
}
|
665
|
+
}
|
666
|
+
});
|
667
|
+
}
|
631
668
|
// Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
|
632
669
|
// If `format` is not `image/png`, a PNG image URL may still be returned (as in the
|
633
670
|
// case of `HTMLCanvasElement::toDataURL`).
|
package/dist/src/SVGLoader.js
CHANGED
@@ -233,16 +233,17 @@ export default class SVGLoader {
|
|
233
233
|
}
|
234
234
|
}
|
235
235
|
addImage(elem) {
|
236
|
-
var _a, _b;
|
236
|
+
var _a, _b, _c;
|
237
237
|
return __awaiter(this, void 0, void 0, function* () {
|
238
238
|
const image = new Image();
|
239
239
|
image.src = (_a = elem.getAttribute('xlink:href')) !== null && _a !== void 0 ? _a : elem.href.baseVal;
|
240
|
+
image.setAttribute('alt', (_b = elem.getAttribute('aria-label')) !== null && _b !== void 0 ? _b : '');
|
240
241
|
try {
|
241
242
|
const supportedAttrs = [];
|
242
243
|
const transform = this.getTransform(elem, supportedAttrs);
|
243
244
|
const imageElem = yield ImageComponent.fromImage(image, transform);
|
244
245
|
this.attachUnrecognisedAttrs(imageElem, elem, new Set(supportedAttrs), new Set(['transform']));
|
245
|
-
(
|
246
|
+
(_c = this.onAddComponent) === null || _c === void 0 ? void 0 : _c.call(this, imageElem);
|
246
247
|
}
|
247
248
|
catch (e) {
|
248
249
|
console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
|
@@ -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
|
+
setZIndex(newZIndex: number): SerializableCommand;
|
31
32
|
isSelectable(): boolean;
|
32
33
|
getProportionalRenderingTime(): number;
|
33
34
|
private static transformElementCommandId;
|
@@ -48,6 +48,10 @@ export default class AbstractComponent {
|
|
48
48
|
transformBy(affineTransfm) {
|
49
49
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
50
50
|
}
|
51
|
+
// Returns a command that updates this component's z-index.
|
52
|
+
setZIndex(newZIndex) {
|
53
|
+
return new AbstractComponent.TransformElementCommand(Mat33.identity, this, newZIndex);
|
54
|
+
}
|
51
55
|
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
52
56
|
isSelectable() {
|
53
57
|
return true;
|
@@ -126,10 +130,11 @@ AbstractComponent.zIndexCounter = 0;
|
|
126
130
|
AbstractComponent.deserializationCallbacks = {};
|
127
131
|
AbstractComponent.transformElementCommandId = 'transform-element';
|
128
132
|
AbstractComponent.UnresolvedTransformElementCommand = class extends SerializableCommand {
|
129
|
-
constructor(affineTransfm, componentID) {
|
133
|
+
constructor(affineTransfm, componentID, targetZIndex) {
|
130
134
|
super(AbstractComponent.transformElementCommandId);
|
131
135
|
this.affineTransfm = affineTransfm;
|
132
136
|
this.componentID = componentID;
|
137
|
+
this.targetZIndex = targetZIndex;
|
133
138
|
this.command = null;
|
134
139
|
}
|
135
140
|
resolveCommand(editor) {
|
@@ -140,7 +145,7 @@ AbstractComponent.UnresolvedTransformElementCommand = class extends Serializable
|
|
140
145
|
if (!component) {
|
141
146
|
throw new Error(`Unable to resolve component with ID ${this.componentID}`);
|
142
147
|
}
|
143
|
-
this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component);
|
148
|
+
this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component, this.targetZIndex);
|
144
149
|
}
|
145
150
|
apply(editor) {
|
146
151
|
this.resolveCommand(editor);
|
@@ -157,15 +162,17 @@ AbstractComponent.UnresolvedTransformElementCommand = class extends Serializable
|
|
157
162
|
return {
|
158
163
|
id: this.componentID,
|
159
164
|
transfm: this.affineTransfm.toArray(),
|
165
|
+
targetZIndex: this.targetZIndex,
|
160
166
|
};
|
161
167
|
}
|
162
168
|
};
|
163
169
|
AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
|
164
|
-
constructor(affineTransfm, component) {
|
170
|
+
constructor(affineTransfm, component, targetZIndex) {
|
165
171
|
super(AbstractComponent.transformElementCommandId);
|
166
172
|
this.affineTransfm = affineTransfm;
|
167
173
|
this.component = component;
|
168
174
|
this.origZIndex = component.zIndex;
|
175
|
+
this.targetZIndex = targetZIndex !== null && targetZIndex !== void 0 ? targetZIndex : AbstractComponent.zIndexCounter++;
|
169
176
|
}
|
170
177
|
updateTransform(editor, newTransfm) {
|
171
178
|
// Any parent should have only one direct child.
|
@@ -182,7 +189,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
182
189
|
}
|
183
190
|
}
|
184
191
|
apply(editor) {
|
185
|
-
this.component.zIndex =
|
192
|
+
this.component.zIndex = this.targetZIndex;
|
186
193
|
this.updateTransform(editor, this.affineTransfm);
|
187
194
|
editor.queueRerender();
|
188
195
|
}
|
@@ -198,6 +205,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
198
205
|
return {
|
199
206
|
id: this.component.getId(),
|
200
207
|
transfm: this.affineTransfm.toArray(),
|
208
|
+
targetZIndex: this.targetZIndex,
|
201
209
|
};
|
202
210
|
}
|
203
211
|
},
|
@@ -205,10 +213,11 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
|
|
205
213
|
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
|
206
214
|
const elem = editor.image.lookupElement(json.id);
|
207
215
|
const transform = new Mat33(...json.transfm);
|
216
|
+
const targetZIndex = json.targetZIndex;
|
208
217
|
if (!elem) {
|
209
|
-
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
|
218
|
+
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id, targetZIndex);
|
210
219
|
}
|
211
|
-
return new AbstractComponent.TransformElementCommand(transform, elem);
|
220
|
+
return new AbstractComponent.TransformElementCommand(transform, elem, targetZIndex);
|
212
221
|
});
|
213
222
|
})(),
|
214
223
|
_a);
|
@@ -23,6 +23,9 @@ export default class ImageComponent extends AbstractComponent {
|
|
23
23
|
};
|
24
24
|
protected applyTransformation(affineTransfm: Mat33): void;
|
25
25
|
description(localizationTable: ImageComponentLocalization): string;
|
26
|
+
getAltText(): string | undefined;
|
27
|
+
getURL(): string;
|
28
|
+
getTransformation(): Mat33;
|
26
29
|
protected createClone(): AbstractComponent;
|
27
30
|
static deserializeFromJSON(data: any): ImageComponent;
|
28
31
|
}
|
@@ -33,7 +33,7 @@ export default class ImageComponent extends AbstractComponent {
|
|
33
33
|
}
|
34
34
|
// Load from an image. Waits for the image to load if incomplete.
|
35
35
|
static fromImage(elem, transform) {
|
36
|
-
var _a;
|
36
|
+
var _a, _b, _c;
|
37
37
|
return __awaiter(this, void 0, void 0, function* () {
|
38
38
|
if (!elem.complete) {
|
39
39
|
yield new Promise((resolve, reject) => {
|
@@ -70,6 +70,8 @@ export default class ImageComponent extends AbstractComponent {
|
|
70
70
|
image.width = width;
|
71
71
|
image.height = height;
|
72
72
|
}
|
73
|
+
image.setAttribute('alt', (_b = elem.getAttribute('alt')) !== null && _b !== void 0 ? _b : '');
|
74
|
+
image.setAttribute('aria-label', (_c = elem.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : '');
|
73
75
|
return new ImageComponent({
|
74
76
|
image,
|
75
77
|
base64Url: url,
|
@@ -111,6 +113,15 @@ export default class ImageComponent extends AbstractComponent {
|
|
111
113
|
description(localizationTable) {
|
112
114
|
return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
|
113
115
|
}
|
116
|
+
getAltText() {
|
117
|
+
return this.image.label;
|
118
|
+
}
|
119
|
+
getURL() {
|
120
|
+
return this.image.base64Url;
|
121
|
+
}
|
122
|
+
getTransformation() {
|
123
|
+
return this.image.transform;
|
124
|
+
}
|
114
125
|
createClone() {
|
115
126
|
return new ImageComponent(Object.assign({}, this.image));
|
116
127
|
}
|
@@ -14,5 +14,5 @@ const localization = Object.assign(Object.assign({}, defaultEditorLocalization),
|
|
14
14
|
return `Color fue cambiado a ${color}`;
|
15
15
|
}, keyboardPanZoom: 'Mover la pantalla con el teclado', penTool: function (penId) {
|
16
16
|
return `Lapiz ${penId}`;
|
17
|
-
}, selectionTool: 'Selecciona', eraserTool: 'Borrador', textTool: 'Texto', enterTextToInsert: 'Entra texto', rerenderAsText: 'Redibuja la pantalla al texto' });
|
17
|
+
}, selectionTool: 'Selecciona', eraserTool: 'Borrador', textTool: 'Texto', enterTextToInsert: 'Entra texto', rerenderAsText: 'Redibuja la pantalla al texto', image: 'Imagen', imageSize: (size, units) => `Tamaño del imagen: ${size} ${units}`, imageLoadError: (message) => `Error cargando imagen: ${message}` });
|
18
18
|
export default localization;
|
@@ -188,15 +188,19 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
188
188
|
}
|
189
189
|
}
|
190
190
|
drawImage(image) {
|
191
|
-
var _a, _b, _c, _d, _e;
|
191
|
+
var _a, _b, _c, _d, _e, _f;
|
192
|
+
let label = (_b = (_a = image.label) !== null && _a !== void 0 ? _a : image.image.getAttribute('aria-label')) !== null && _b !== void 0 ? _b : '';
|
193
|
+
if (label === '') {
|
194
|
+
label = (_c = image.image.getAttribute('alt')) !== null && _c !== void 0 ? _c : '';
|
195
|
+
}
|
192
196
|
const svgImgElem = document.createElementNS(svgNameSpace, 'image');
|
193
197
|
svgImgElem.setAttribute('href', image.base64Url);
|
194
|
-
svgImgElem.setAttribute('width', (
|
195
|
-
svgImgElem.setAttribute('height', (
|
196
|
-
svgImgElem.setAttribute('aria-label',
|
198
|
+
svgImgElem.setAttribute('width', (_d = image.image.getAttribute('width')) !== null && _d !== void 0 ? _d : '');
|
199
|
+
svgImgElem.setAttribute('height', (_e = image.image.getAttribute('height')) !== null && _e !== void 0 ? _e : '');
|
200
|
+
svgImgElem.setAttribute('aria-label', label);
|
197
201
|
this.transformFrom(image.transform, svgImgElem);
|
198
202
|
this.elem.appendChild(svgImgElem);
|
199
|
-
(
|
203
|
+
(_f = this.objectElems) === null || _f === void 0 ? void 0 : _f.push(svgImgElem);
|
200
204
|
}
|
201
205
|
startObject(boundingBox) {
|
202
206
|
super.startObject(boundingBox);
|
@@ -12,7 +12,7 @@ import EraserWidget from './widgets/EraserToolWidget';
|
|
12
12
|
import SelectionToolWidget from './widgets/SelectionToolWidget';
|
13
13
|
import TextToolWidget from './widgets/TextToolWidget';
|
14
14
|
import HandToolWidget from './widgets/HandToolWidget';
|
15
|
-
import { ActionButtonWidget } from './lib';
|
15
|
+
import { ActionButtonWidget, InsertImageWidget } from './lib';
|
16
16
|
export const toolbarCSSPrefix = 'toolbar-';
|
17
17
|
export default class HTMLToolbar {
|
18
18
|
/** @internal */
|
@@ -179,6 +179,7 @@ export default class HTMLToolbar {
|
|
179
179
|
for (const tool of toolController.getMatchingTools(TextTool)) {
|
180
180
|
this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
|
181
181
|
}
|
182
|
+
this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
|
182
183
|
const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
|
183
184
|
if (panZoomTool) {
|
184
185
|
this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
|
@@ -19,6 +19,7 @@ export default class IconProvider {
|
|
19
19
|
makeAllDevicePanningIcon(): IconType;
|
20
20
|
makeZoomIcon(): IconType;
|
21
21
|
makeRotationLockIcon(): IconType;
|
22
|
+
makeInsertImageIcon(): IconType;
|
22
23
|
makeTextIcon(textStyle: TextStyle): IconType;
|
23
24
|
makePenIcon(tipThickness: number, color: string | Color4, roundedTip?: boolean): IconType;
|
24
25
|
makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
|
@@ -284,6 +284,13 @@ export default class IconProvider {
|
|
284
284
|
icon.setAttribute('viewBox', '10 10 70 70');
|
285
285
|
return icon;
|
286
286
|
}
|
287
|
+
makeInsertImageIcon() {
|
288
|
+
return this.makeIconFromPath(`
|
289
|
+
M 5 10 L 5 90 L 95 90 L 95 10 L 5 10 z
|
290
|
+
M 10 15 L 90 15 L 90 50 L 70 75 L 40 50 L 10 75 L 10 15 z
|
291
|
+
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
|
292
|
+
`);
|
293
|
+
}
|
287
294
|
makeTextIcon(textStyle) {
|
288
295
|
var _a, _b;
|
289
296
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
@@ -7,6 +7,11 @@ export interface ToolbarLocalization {
|
|
7
7
|
filledRectanglePen: string;
|
8
8
|
linePen: string;
|
9
9
|
arrowPen: string;
|
10
|
+
image: string;
|
11
|
+
inputAltText: string;
|
12
|
+
chooseFile: string;
|
13
|
+
cancel: string;
|
14
|
+
submit: string;
|
10
15
|
freehandPen: string;
|
11
16
|
pressureSensitiveFreehandPen: string;
|
12
17
|
selectObjectType: string;
|
@@ -27,9 +32,12 @@ export interface ToolbarLocalization {
|
|
27
32
|
resetView: string;
|
28
33
|
selectionToolKeyboardShortcuts: string;
|
29
34
|
paste: string;
|
35
|
+
errorImageHasZeroSize: string;
|
30
36
|
dropdownShown: (toolName: string) => string;
|
31
37
|
dropdownHidden: (toolName: string) => string;
|
32
38
|
zoomLevel: (zoomPercentage: number) => string;
|
33
39
|
colorChangedAnnouncement: (color: string) => string;
|
40
|
+
imageSize: (size: number, units: string) => string;
|
41
|
+
imageLoadError: (message: string) => string;
|
34
42
|
}
|
35
43
|
export declare const defaultToolbarLocalization: ToolbarLocalization;
|
@@ -4,6 +4,11 @@ export const defaultToolbarLocalization = {
|
|
4
4
|
select: 'Select',
|
5
5
|
handTool: 'Pan',
|
6
6
|
zoom: 'Zoom',
|
7
|
+
image: 'Image',
|
8
|
+
inputAltText: 'Alt text: ',
|
9
|
+
chooseFile: 'Choose file: ',
|
10
|
+
submit: 'Submit',
|
11
|
+
cancel: 'Cancel',
|
7
12
|
resetView: 'Reset view',
|
8
13
|
thicknessLabel: 'Thickness: ',
|
9
14
|
colorLabel: 'Color: ',
|
@@ -31,4 +36,7 @@ export const defaultToolbarLocalization = {
|
|
31
36
|
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
32
37
|
zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
|
33
38
|
colorChangedAnnouncement: (color) => `Color changed to ${color}`,
|
39
|
+
imageSize: (size, units) => `Image size: ${size} ${units}`,
|
40
|
+
errorImageHasZeroSize: 'Error: Image has zero size',
|
41
|
+
imageLoadError: (message) => `Error loading image: ${message}`,
|
34
42
|
};
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import Editor from '../../Editor';
|
2
|
+
import { ToolbarLocalization } from '../localization';
|
3
|
+
import ActionButtonWidget from './ActionButtonWidget';
|
4
|
+
export default class InsertImageWidget extends ActionButtonWidget {
|
5
|
+
private imageSelectionOverlay;
|
6
|
+
private imagePreview;
|
7
|
+
private imageFileInput;
|
8
|
+
private imageAltTextInput;
|
9
|
+
private statusView;
|
10
|
+
private imageBase64URL;
|
11
|
+
private submitButton;
|
12
|
+
constructor(editor: Editor, localization?: ToolbarLocalization);
|
13
|
+
private static nextInputId;
|
14
|
+
private fillOverlay;
|
15
|
+
private hideDialog;
|
16
|
+
private updateImageSizeDisplay;
|
17
|
+
private clearInputs;
|
18
|
+
private onClicked;
|
19
|
+
}
|
@@ -0,0 +1,169 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
10
|
+
import ImageComponent from '../../components/ImageComponent';
|
11
|
+
import Erase from '../../commands/Erase';
|
12
|
+
import EditorImage from '../../EditorImage';
|
13
|
+
import { SelectionTool, uniteCommands } from '../../lib';
|
14
|
+
import Mat33 from '../../math/Mat33';
|
15
|
+
import fileToBase64 from '../../util/fileToBase64';
|
16
|
+
import ActionButtonWidget from './ActionButtonWidget';
|
17
|
+
export default class InsertImageWidget extends ActionButtonWidget {
|
18
|
+
constructor(editor, localization) {
|
19
|
+
localization !== null && localization !== void 0 ? localization : (localization = editor.localization);
|
20
|
+
super(editor, 'insert-image-widget', () => editor.icons.makeInsertImageIcon(), localization.image, () => this.onClicked());
|
21
|
+
this.imageSelectionOverlay = document.createElement('div');
|
22
|
+
this.imageSelectionOverlay.classList.add('toolbar-image-selection-overlay');
|
23
|
+
this.fillOverlay();
|
24
|
+
this.editor.createHTMLOverlay(this.imageSelectionOverlay);
|
25
|
+
this.imageSelectionOverlay.style.display = 'none';
|
26
|
+
}
|
27
|
+
fillOverlay() {
|
28
|
+
const container = document.createElement('div');
|
29
|
+
const chooseImageRow = document.createElement('div');
|
30
|
+
const altTextRow = document.createElement('div');
|
31
|
+
this.imagePreview = document.createElement('img');
|
32
|
+
this.statusView = document.createElement('div');
|
33
|
+
const actionButtonRow = document.createElement('div');
|
34
|
+
actionButtonRow.classList.add('action-button-row');
|
35
|
+
this.submitButton = document.createElement('button');
|
36
|
+
const cancelButton = document.createElement('button');
|
37
|
+
this.imageFileInput = document.createElement('input');
|
38
|
+
this.imageAltTextInput = document.createElement('input');
|
39
|
+
const imageFileInputLabel = document.createElement('label');
|
40
|
+
const imageAltTextLabel = document.createElement('label');
|
41
|
+
const fileInputId = `insert-image-file-input-${InsertImageWidget.nextInputId++}`;
|
42
|
+
const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
|
43
|
+
this.imageFileInput.setAttribute('id', fileInputId);
|
44
|
+
this.imageAltTextInput.setAttribute('id', altTextInputId);
|
45
|
+
imageAltTextLabel.htmlFor = altTextInputId;
|
46
|
+
imageFileInputLabel.htmlFor = fileInputId;
|
47
|
+
this.imageFileInput.accept = 'image/*';
|
48
|
+
imageAltTextLabel.innerText = this.localizationTable.inputAltText;
|
49
|
+
imageFileInputLabel.innerText = this.localizationTable.chooseFile;
|
50
|
+
this.imageFileInput.type = 'file';
|
51
|
+
this.imageAltTextInput.type = 'text';
|
52
|
+
this.statusView.setAttribute('aria-live', 'polite');
|
53
|
+
cancelButton.innerText = this.localizationTable.cancel;
|
54
|
+
this.submitButton.innerText = this.localizationTable.submit;
|
55
|
+
this.imageFileInput.onchange = () => __awaiter(this, void 0, void 0, function* () {
|
56
|
+
if (this.imageFileInput.value === '' || !this.imageFileInput.files || !this.imageFileInput.files[0]) {
|
57
|
+
this.imagePreview.style.display = 'none';
|
58
|
+
this.submitButton.disabled = true;
|
59
|
+
return;
|
60
|
+
}
|
61
|
+
this.imagePreview.style.display = 'block';
|
62
|
+
const image = this.imageFileInput.files[0];
|
63
|
+
let data = null;
|
64
|
+
try {
|
65
|
+
data = yield fileToBase64(image);
|
66
|
+
}
|
67
|
+
catch (e) {
|
68
|
+
this.statusView.innerText = this.localizationTable.imageLoadError(e);
|
69
|
+
}
|
70
|
+
this.imageBase64URL = data;
|
71
|
+
if (data) {
|
72
|
+
this.imagePreview.src = data;
|
73
|
+
this.submitButton.disabled = false;
|
74
|
+
this.updateImageSizeDisplay();
|
75
|
+
}
|
76
|
+
else {
|
77
|
+
this.submitButton.disabled = true;
|
78
|
+
this.statusView.innerText = '';
|
79
|
+
}
|
80
|
+
});
|
81
|
+
cancelButton.onclick = () => {
|
82
|
+
this.hideDialog();
|
83
|
+
};
|
84
|
+
this.imageSelectionOverlay.onclick = (evt) => {
|
85
|
+
// If clicking on the backdrop
|
86
|
+
if (evt.target === this.imageSelectionOverlay) {
|
87
|
+
this.hideDialog();
|
88
|
+
}
|
89
|
+
};
|
90
|
+
chooseImageRow.replaceChildren(imageFileInputLabel, this.imageFileInput);
|
91
|
+
altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
|
92
|
+
actionButtonRow.replaceChildren(cancelButton, this.submitButton);
|
93
|
+
container.replaceChildren(chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow);
|
94
|
+
this.imageSelectionOverlay.replaceChildren(container);
|
95
|
+
}
|
96
|
+
hideDialog() {
|
97
|
+
this.imageSelectionOverlay.style.display = 'none';
|
98
|
+
}
|
99
|
+
updateImageSizeDisplay() {
|
100
|
+
var _a;
|
101
|
+
const imageData = (_a = this.imageBase64URL) !== null && _a !== void 0 ? _a : '';
|
102
|
+
const sizeInKiB = imageData.length / 1024;
|
103
|
+
const sizeInMiB = sizeInKiB / 1024;
|
104
|
+
let units = 'KiB';
|
105
|
+
let size = sizeInKiB;
|
106
|
+
if (sizeInMiB >= 1) {
|
107
|
+
size = sizeInMiB;
|
108
|
+
units = 'MiB';
|
109
|
+
}
|
110
|
+
this.statusView.innerText = this.localizationTable.imageSize(Math.round(size), units);
|
111
|
+
}
|
112
|
+
clearInputs() {
|
113
|
+
this.imageFileInput.value = '';
|
114
|
+
this.imageAltTextInput.value = '';
|
115
|
+
this.imagePreview.style.display = 'none';
|
116
|
+
this.submitButton.disabled = true;
|
117
|
+
this.statusView.innerText = '';
|
118
|
+
}
|
119
|
+
onClicked() {
|
120
|
+
var _a;
|
121
|
+
this.imageSelectionOverlay.style.display = '';
|
122
|
+
this.clearInputs();
|
123
|
+
this.imageFileInput.focus();
|
124
|
+
const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
|
125
|
+
const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
|
126
|
+
let editingImage = null;
|
127
|
+
if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
|
128
|
+
editingImage = selectedObjects[0];
|
129
|
+
this.imageAltTextInput.value = (_a = editingImage.getAltText()) !== null && _a !== void 0 ? _a : '';
|
130
|
+
this.imagePreview.style.display = 'block';
|
131
|
+
this.submitButton.disabled = false;
|
132
|
+
this.imageBase64URL = editingImage.getURL();
|
133
|
+
this.imagePreview.src = this.imageBase64URL;
|
134
|
+
this.updateImageSizeDisplay();
|
135
|
+
}
|
136
|
+
else {
|
137
|
+
selectionTools.forEach(tool => tool.clearSelection());
|
138
|
+
}
|
139
|
+
this.submitButton.onclick = () => __awaiter(this, void 0, void 0, function* () {
|
140
|
+
var _b;
|
141
|
+
if (!this.imageBase64URL) {
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
const image = new Image();
|
145
|
+
image.src = this.imageBase64URL;
|
146
|
+
image.setAttribute('alt', this.imageAltTextInput.value);
|
147
|
+
const component = yield ImageComponent.fromImage(image, Mat33.identity);
|
148
|
+
if (component.getBBox().area === 0) {
|
149
|
+
this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
|
150
|
+
return;
|
151
|
+
}
|
152
|
+
this.imageSelectionOverlay.style.display = 'none';
|
153
|
+
if (editingImage) {
|
154
|
+
const eraseCommand = new Erase([editingImage]);
|
155
|
+
yield this.editor.dispatch(uniteCommands([
|
156
|
+
EditorImage.addElement(component),
|
157
|
+
component.transformBy(editingImage.getTransformation()),
|
158
|
+
component.setZIndex(editingImage.getZIndex()),
|
159
|
+
eraseCommand,
|
160
|
+
]));
|
161
|
+
(_b = selectionTools[0]) === null || _b === void 0 ? void 0 : _b.setSelection([component]);
|
162
|
+
}
|
163
|
+
else {
|
164
|
+
yield this.editor.addAndCenterComponents([component]);
|
165
|
+
}
|
166
|
+
});
|
167
|
+
}
|
168
|
+
}
|
169
|
+
InsertImageWidget.nextInputId = 0;
|
@@ -6,3 +6,4 @@ export { default as TextToolWidget } from './TextToolWidget';
|
|
6
6
|
export { default as HandToolWidget } from './HandToolWidget';
|
7
7
|
export { default as SelectionToolWidget } from './SelectionToolWidget';
|
8
8
|
export { default as EraserToolWidget } from './EraserToolWidget';
|
9
|
+
export { default as InsertImageWidget } from './InsertImageWidget';
|
@@ -6,3 +6,4 @@ export { default as TextToolWidget } from './TextToolWidget';
|
|
6
6
|
export { default as HandToolWidget } from './HandToolWidget';
|
7
7
|
export { default as SelectionToolWidget } from './SelectionToolWidget';
|
8
8
|
export { default as EraserToolWidget } from './EraserToolWidget';
|
9
|
+
export { default as InsertImageWidget } from './InsertImageWidget';
|
@@ -202,10 +202,20 @@ export default class PanZoom extends BaseTool {
|
|
202
202
|
&& this.velocity !== null
|
203
203
|
&& event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
|
204
204
|
if (shouldInertialScroll && this.velocity !== null) {
|
205
|
+
const oldVelocity = this.velocity;
|
205
206
|
// If the user drags the screen, then stops, then lifts the pointer,
|
206
207
|
// we want the final velocity to reflect the stop at the end (so the velocity
|
207
208
|
// should be near zero). Handle this:
|
208
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
|
+
}
|
209
219
|
// Cancel any ongoing inertial scrolling.
|
210
220
|
(_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
|
211
221
|
this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
|