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
@@ -0,0 +1,222 @@
1
+ import ImageComponent from '../../components/ImageComponent';
2
+ import Editor from '../../Editor';
3
+ import Erase from '../../commands/Erase';
4
+ import EditorImage from '../../EditorImage';
5
+ import { SelectionTool, uniteCommands } from '../../lib';
6
+ import Mat33 from '../../math/Mat33';
7
+ import fileToBase64 from '../../util/fileToBase64';
8
+ import { ToolbarLocalization } from '../localization';
9
+ import ActionButtonWidget from './ActionButtonWidget';
10
+
11
+ export default class InsertImageWidget extends ActionButtonWidget {
12
+ private imageSelectionOverlay: HTMLElement;
13
+ private imagePreview: HTMLImageElement;
14
+ private imageFileInput: HTMLInputElement;
15
+ private imageAltTextInput: HTMLInputElement;
16
+ private statusView: HTMLElement;
17
+ private imageBase64URL: string|null;
18
+ private submitButton: HTMLButtonElement;
19
+
20
+ public constructor(editor: Editor, localization?: ToolbarLocalization) {
21
+ localization ??= editor.localization;
22
+
23
+ super(editor,
24
+ 'insert-image-widget',
25
+ () => editor.icons.makeInsertImageIcon(),
26
+ localization.image,
27
+ () => this.onClicked()
28
+ );
29
+
30
+ this.imageSelectionOverlay = document.createElement('div');
31
+ this.imageSelectionOverlay.classList.add('toolbar-image-selection-overlay');
32
+ this.fillOverlay();
33
+
34
+ this.editor.createHTMLOverlay(this.imageSelectionOverlay);
35
+ this.imageSelectionOverlay.style.display = 'none';
36
+ }
37
+
38
+ private static nextInputId = 0;
39
+
40
+ private fillOverlay() {
41
+ const container = document.createElement('div');
42
+
43
+ const chooseImageRow = document.createElement('div');
44
+ const altTextRow = document.createElement('div');
45
+ this.imagePreview = document.createElement('img');
46
+ this.statusView = document.createElement('div');
47
+ const actionButtonRow = document.createElement('div');
48
+
49
+ actionButtonRow.classList.add('action-button-row');
50
+
51
+ this.submitButton = document.createElement('button');
52
+ const cancelButton = document.createElement('button');
53
+
54
+ this.imageFileInput = document.createElement('input');
55
+ this.imageAltTextInput = document.createElement('input');
56
+ const imageFileInputLabel = document.createElement('label');
57
+ const imageAltTextLabel = document.createElement('label');
58
+
59
+ const fileInputId = `insert-image-file-input-${InsertImageWidget.nextInputId ++}`;
60
+ const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
61
+
62
+ this.imageFileInput.setAttribute('id', fileInputId);
63
+ this.imageAltTextInput.setAttribute('id', altTextInputId);
64
+ imageAltTextLabel.htmlFor = altTextInputId;
65
+ imageFileInputLabel.htmlFor = fileInputId;
66
+
67
+ this.imageFileInput.accept = 'image/*';
68
+
69
+ imageAltTextLabel.innerText = this.localizationTable.inputAltText;
70
+ imageFileInputLabel.innerText = this.localizationTable.chooseFile;
71
+
72
+ this.imageFileInput.type = 'file';
73
+ this.imageAltTextInput.type = 'text';
74
+
75
+ this.statusView.setAttribute('aria-live', 'polite');
76
+
77
+ cancelButton.innerText = this.localizationTable.cancel;
78
+ this.submitButton.innerText = this.localizationTable.submit;
79
+
80
+ this.imageFileInput.onchange = async () => {
81
+ if (this.imageFileInput.value === '' || !this.imageFileInput.files || !this.imageFileInput.files[0]) {
82
+ this.imagePreview.style.display = 'none';
83
+ this.submitButton.disabled = true;
84
+ return;
85
+ }
86
+
87
+ this.imagePreview.style.display = 'block';
88
+
89
+ const image = this.imageFileInput.files[0];
90
+
91
+ let data: string|null = null;
92
+
93
+ try {
94
+ data = await fileToBase64(image);
95
+ } catch(e) {
96
+ this.statusView.innerText = this.localizationTable.imageLoadError(e);
97
+ }
98
+
99
+ this.imageBase64URL = data;
100
+
101
+ if (data) {
102
+ this.imagePreview.src = data;
103
+ this.submitButton.disabled = false;
104
+ this.updateImageSizeDisplay();
105
+ } else {
106
+ this.submitButton.disabled = true;
107
+ this.statusView.innerText = '';
108
+ }
109
+ };
110
+
111
+ cancelButton.onclick = () => {
112
+ this.hideDialog();
113
+ };
114
+
115
+ this.imageSelectionOverlay.onclick = (evt: MouseEvent) => {
116
+ // If clicking on the backdrop
117
+ if (evt.target === this.imageSelectionOverlay) {
118
+ this.hideDialog();
119
+ }
120
+ };
121
+
122
+ chooseImageRow.replaceChildren(imageFileInputLabel, this.imageFileInput);
123
+ altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
124
+ actionButtonRow.replaceChildren(cancelButton, this.submitButton);
125
+
126
+ container.replaceChildren(
127
+ chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow
128
+ );
129
+
130
+ this.imageSelectionOverlay.replaceChildren(container);
131
+ }
132
+
133
+ private hideDialog() {
134
+ this.imageSelectionOverlay.style.display = 'none';
135
+ }
136
+
137
+ private updateImageSizeDisplay() {
138
+ const imageData = this.imageBase64URL ?? '';
139
+
140
+ const sizeInKiB = imageData.length / 1024;
141
+ const sizeInMiB = sizeInKiB / 1024;
142
+
143
+ let units = 'KiB';
144
+ let size = sizeInKiB;
145
+
146
+ if (sizeInMiB >= 1) {
147
+ size = sizeInMiB;
148
+ units = 'MiB';
149
+ }
150
+
151
+ this.statusView.innerText = this.localizationTable.imageSize(
152
+ Math.round(size), units
153
+ );
154
+ }
155
+
156
+ private clearInputs() {
157
+ this.imageFileInput.value = '';
158
+ this.imageAltTextInput.value = '';
159
+ this.imagePreview.style.display = 'none';
160
+ this.submitButton.disabled = true;
161
+ this.statusView.innerText = '';
162
+ }
163
+
164
+ private onClicked() {
165
+ this.imageSelectionOverlay.style.display = '';
166
+ this.clearInputs();
167
+ this.imageFileInput.focus();
168
+
169
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
170
+ const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
171
+
172
+ let editingImage: ImageComponent|null = null;
173
+ if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
174
+ editingImage = selectedObjects[0];
175
+
176
+ this.imageAltTextInput.value = editingImage.getAltText() ?? '';
177
+ this.imagePreview.style.display = 'block';
178
+ this.submitButton.disabled = false;
179
+
180
+ this.imageBase64URL = editingImage.getURL();
181
+ this.imagePreview.src = this.imageBase64URL;
182
+
183
+ this.updateImageSizeDisplay();
184
+ } else {
185
+ selectionTools.forEach(tool => tool.clearSelection());
186
+ }
187
+
188
+ this.submitButton.onclick = async () => {
189
+ if (!this.imageBase64URL) {
190
+ return;
191
+ }
192
+
193
+ const image = new Image();
194
+ image.src = this.imageBase64URL;
195
+ image.setAttribute('alt', this.imageAltTextInput.value);
196
+
197
+ const component = await ImageComponent.fromImage(image, Mat33.identity);
198
+
199
+ if (component.getBBox().area === 0) {
200
+ this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
201
+ return;
202
+ }
203
+
204
+ this.imageSelectionOverlay.style.display = 'none';
205
+
206
+ if (editingImage) {
207
+ const eraseCommand = new Erase([ editingImage ]);
208
+
209
+ await this.editor.dispatch(uniteCommands([
210
+ EditorImage.addElement(component),
211
+ component.transformBy(editingImage.getTransformation()),
212
+ component.setZIndex(editingImage.getZIndex()),
213
+ eraseCommand,
214
+ ]));
215
+
216
+ selectionTools[0]?.setSelection([ component ]);
217
+ } else {
218
+ await this.editor.addAndCenterComponents([ component ]);
219
+ }
220
+ };
221
+ }
222
+ }
@@ -8,3 +8,5 @@ export { default as TextToolWidget } from './TextToolWidget';
8
8
  export { default as HandToolWidget } from './HandToolWidget';
9
9
  export { default as SelectionToolWidget } from './SelectionToolWidget';
10
10
  export { default as EraserToolWidget } from './EraserToolWidget';
11
+
12
+ export { default as InsertImageWidget } from './InsertImageWidget';
@@ -0,0 +1,65 @@
1
+
2
+ import Editor from '../Editor';
3
+ import { Mat33, Pointer, PointerDevice, Vec2 } from '../lib';
4
+ import createEditor from '../testing/createEditor';
5
+ import { InputEvtType } from '../types';
6
+ import waitForTimeout from '../util/waitForTimeout';
7
+ import PanZoom from './PanZoom';
8
+
9
+ const selectPanZom = (editor: Editor): PanZoom => {
10
+ const primaryTools = editor.toolController.getPrimaryTools();
11
+ const panZoom = primaryTools.filter(tool => tool instanceof PanZoom)[0] as PanZoom;
12
+ panZoom.setEnabled(true);
13
+ return panZoom;
14
+ };
15
+
16
+ const sendTouchEvent = (
17
+ editor: Editor,
18
+ eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
19
+ screenPos: Vec2,
20
+ ) => {
21
+ const canvasPos = editor.viewport.screenToCanvas(screenPos);
22
+
23
+ const ptrId = 0;
24
+ const mainPointer = Pointer.ofCanvasPoint(
25
+ canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch
26
+ );
27
+
28
+ editor.toolController.dispatchInputEvent({
29
+ kind: eventType,
30
+ allPointers: [
31
+ mainPointer,
32
+ ],
33
+ current: mainPointer,
34
+ });
35
+ };
36
+
37
+ describe('PanZoom', () => {
38
+ it('touch and drag should pan, then inertial scroll', async () => {
39
+ const editor = createEditor();
40
+ selectPanZom(editor);
41
+ editor.viewport.resetTransform(Mat33.identity);
42
+
43
+ const origTranslation = editor.viewport.canvasToScreen(Vec2.zero);
44
+
45
+ sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
46
+ for (let i = 1; i <= 10; i++) {
47
+ jest.advanceTimersByTime(10);
48
+ sendTouchEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(i * 10, 0));
49
+ }
50
+
51
+ // Use real timers -- we need to be able to start the inertial scroller.
52
+ jest.useRealTimers();
53
+ sendTouchEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(100, 0));
54
+
55
+ const updatedTranslation = editor.viewport.canvasToScreen(Vec2.zero);
56
+ expect(updatedTranslation.minus(origTranslation).magnitude()).toBe(100);
57
+
58
+ await waitForTimeout(600); // ms
59
+ jest.useFakeTimers();
60
+
61
+ // Should inertial scroll
62
+ const afterDelayTranslation = editor.viewport.canvasToScreen(Vec2.zero);
63
+ expect(afterDelayTranslation.minus(updatedTranslation).magnitude()).toBeGreaterThan(0);
64
+ });
65
+ });
@@ -30,6 +30,7 @@ type ScrollByCallback = (delta: Vec2) => void;
30
30
 
31
31
  class InertialScroller {
32
32
  private running: boolean = false;
33
+ private currentVelocity: Vec2;
33
34
 
34
35
  public constructor(
35
36
  private initialVelocity: Vec2,
@@ -44,22 +45,22 @@ class InertialScroller {
44
45
  return;
45
46
  }
46
47
 
47
- let currentVelocity = this.initialVelocity;
48
+ this.currentVelocity = this.initialVelocity;
48
49
  let lastTime = (new Date()).getTime();
49
50
  this.running = true;
50
51
 
51
- const maxSpeed = 8000; // units/s
52
+ const maxSpeed = 5000; // units/s
52
53
  const minSpeed = 200; // units/s
53
- if (currentVelocity.magnitude() > maxSpeed) {
54
- currentVelocity = currentVelocity.normalized().times(maxSpeed);
54
+ if (this.currentVelocity.magnitude() > maxSpeed) {
55
+ this.currentVelocity = this.currentVelocity.normalized().times(maxSpeed);
55
56
  }
56
57
 
57
- while (this.running && currentVelocity.magnitude() > minSpeed) {
58
+ while (this.running && this.currentVelocity.magnitude() > minSpeed) {
58
59
  const nowTime = (new Date()).getTime();
59
60
  const dt = (nowTime - lastTime) / 1000;
60
61
 
61
- currentVelocity = currentVelocity.times(Math.pow(1/8, dt));
62
- this.scrollBy(currentVelocity.times(dt));
62
+ this.currentVelocity = this.currentVelocity.times(Math.pow(1/8, dt));
63
+ this.scrollBy(this.currentVelocity.times(dt));
63
64
 
64
65
  await untilNextAnimationFrame();
65
66
  lastTime = nowTime;
@@ -70,6 +71,14 @@ class InertialScroller {
70
71
  }
71
72
  }
72
73
 
74
+ public getCurrentVelocity(): Vec2|null {
75
+ if (!this.running) {
76
+ return null;
77
+ }
78
+
79
+ return this.currentVelocity;
80
+ }
81
+
73
82
  public stop(): void {
74
83
  if (this.running) {
75
84
  this.running = false;
@@ -85,6 +94,7 @@ export default class PanZoom extends BaseTool {
85
94
  private lastDist: number;
86
95
  private lastScreenCenter: Point2;
87
96
  private lastTimestamp: number;
97
+ private lastPointerDownTimestamp: number = 0;
88
98
 
89
99
  private inertialScroller: InertialScroller|null = null;
90
100
  private velocity: Vec2|null = null;
@@ -108,10 +118,14 @@ export default class PanZoom extends BaseTool {
108
118
  return pointers.every(pointer => pointer.device === kind);
109
119
  }
110
120
 
111
- public onPointerDown({ allPointers: pointers }: PointerEvt): boolean {
121
+ public onPointerDown({ allPointers: pointers, current: currentPointer }: PointerEvt): boolean {
112
122
  let handlingGesture = false;
113
123
 
124
+ const inertialScrollerVelocity = this.inertialScroller?.getCurrentVelocity() ?? Vec2.zero;
114
125
  this.inertialScroller?.stop();
126
+ this.velocity = inertialScrollerVelocity;
127
+
128
+ this.lastPointerDownTimestamp = currentPointer.timeStamp;
115
129
 
116
130
  const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
117
131
  const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
@@ -142,17 +156,19 @@ export default class PanZoom extends BaseTool {
142
156
 
143
157
  private updateVelocity(currentCenter: Point2) {
144
158
  const deltaPos = currentCenter.minus(this.lastScreenCenter);
145
- const deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
159
+ let deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
146
160
 
161
+ // Ignore duplicate events, unless there has been enough time between them.
162
+ if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
163
+ return;
164
+ }
147
165
  // We divide by deltaTime. Don't divide by zero.
148
166
  if (deltaTime === 0) {
149
167
  return;
150
168
  }
151
169
 
152
- // Ignore duplicate events, unless there has been enough time between them.
153
- if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
154
- return;
155
- }
170
+ // Don't divide by almost zero, either
171
+ deltaTime = Math.max(deltaTime, 0.01);
156
172
 
157
173
  const currentVelocity = deltaPos.times(1 / deltaTime);
158
174
  let smoothedVelocity = currentVelocity;
@@ -233,15 +249,31 @@ export default class PanZoom extends BaseTool {
233
249
  this.velocity = Vec2.zero;
234
250
  };
235
251
 
252
+ const minInertialScrollDt = 30;
236
253
  const shouldInertialScroll =
237
- event.current.device === PointerDevice.Touch && event.allPointers.length === 1;
254
+ event.current.device === PointerDevice.Touch
255
+ && event.allPointers.length === 1
256
+ && this.velocity !== null
257
+ && event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
238
258
 
239
259
  if (shouldInertialScroll && this.velocity !== null) {
260
+ const oldVelocity = this.velocity;
261
+
240
262
  // If the user drags the screen, then stops, then lifts the pointer,
241
263
  // we want the final velocity to reflect the stop at the end (so the velocity
242
264
  // should be near zero). Handle this:
243
265
  this.updateVelocity(event.current.screenPos);
244
266
 
267
+ // Work around an input issue. Some devices that disable the touchscreen when a stylus
268
+ // comes near the screen fire a touch-end event at the position of the stylus when a
269
+ // touch gesture is canceled. Because the stylus is often far away from the last touch,
270
+ // this causes a great displacement between the second-to-last (from the touchscreen) and
271
+ // last (from the pen that is now near the screen) events. Only allow velocity to decrease
272
+ // to work around this:
273
+ if (oldVelocity.magnitude() < this.velocity.magnitude()) {
274
+ this.velocity = oldVelocity;
275
+ }
276
+
245
277
  // Cancel any ongoing inertial scrolling.
246
278
  this.inertialScroller?.stop();
247
279
 
@@ -5,18 +5,14 @@
5
5
 
6
6
  import Editor from '../Editor';
7
7
  import { AbstractComponent, TextComponent } from '../components/lib';
8
- import { Command, uniteCommands } from '../commands/lib';
9
8
  import SVGLoader from '../SVGLoader';
10
9
  import { PasteEvent } from '../types';
11
- import { Mat33, Rect2 } from '../math/lib';
10
+ import { Mat33 } from '../math/lib';
12
11
  import BaseTool from './BaseTool';
13
- import EditorImage from '../EditorImage';
14
- import SelectionTool from './SelectionTool/SelectionTool';
15
12
  import TextTool from './TextTool';
16
13
  import Color4 from '../Color4';
17
14
  import { TextStyle } from '../components/TextComponent';
18
15
  import ImageComponent from '../components/ImageComponent';
19
- import Viewport from '../Viewport';
20
16
 
21
17
  // { @inheritDoc PasteHandler! }
22
18
  export default class PasteHandler extends BaseTool {
@@ -44,52 +40,7 @@ export default class PasteHandler extends BaseTool {
44
40
  }
45
41
 
46
42
  private async addComponentsFromPaste(components: AbstractComponent[]) {
47
- let bbox: Rect2|null = null;
48
- for (const component of components) {
49
- if (bbox) {
50
- bbox = bbox.union(component.getBBox());
51
- } else {
52
- bbox = component.getBBox();
53
- }
54
- }
55
-
56
- if (!bbox) {
57
- return;
58
- }
59
-
60
- // Find a transform that scales/moves bbox onto the screen.
61
- const visibleRect = this.editor.viewport.visibleRect;
62
- const scaleRatioX = visibleRect.width / bbox.width;
63
- const scaleRatioY = visibleRect.height / bbox.height;
64
-
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
-
71
- scaleRatio = Viewport.roundScaleRatio(scaleRatio);
72
-
73
- const transfm = Mat33.translation(
74
- visibleRect.center.minus(bbox.center)
75
- ).rightMul(
76
- Mat33.scaling2D(scaleRatio, bbox.center)
77
- );
78
-
79
- const commands: Command[] = [];
80
- for (const component of components) {
81
- // To allow deserialization, we need to add first, then transform.
82
- commands.push(EditorImage.addElement(component));
83
- commands.push(component.transformBy(transfm));
84
- }
85
-
86
- const applyChunkSize = 100;
87
- this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
88
-
89
- for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
90
- selectionTool.setEnabled(true);
91
- selectionTool.setSelection(components);
92
- }
43
+ await this.editor.addAndCenterComponents(components);
93
44
  }
94
45
 
95
46
  private async doSVGPaste(data: string) {
@@ -0,0 +1,18 @@
1
+
2
+ type ProgressListener = (evt: ProgressEvent<FileReader>)=> void;
3
+ const fileToBase64 = (file: File, onprogress?: ProgressListener): Promise<string|null> => {
4
+ const reader = new FileReader();
5
+
6
+ return new Promise((resolve: (result: string|null)=>void, reject) => {
7
+ reader.onload = () => resolve(reader.result as string|null);
8
+ reader.onerror = reject;
9
+ reader.onabort = reject;
10
+ reader.onprogress = (evt) => {
11
+ onprogress?.(evt);
12
+ };
13
+
14
+ reader.readAsDataURL(file);
15
+ });
16
+ };
17
+
18
+ export default fileToBase64;
@@ -0,0 +1,9 @@
1
+
2
+ // Returns a promise that resolves after `timeout` milliseconds.
3
+ const waitForTimeout = (timeout: number): Promise<void> => {
4
+ return new Promise(resolve => {
5
+ setTimeout(() => resolve(), timeout);
6
+ });
7
+ };
8
+
9
+ export default waitForTimeout;