js-draw 1.18.0 → 1.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. package/README.md +51 -0
  2. package/dist/Editor.css +78 -5
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +20 -1
  6. package/dist/cjs/Editor.js +6 -0
  7. package/dist/cjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
  8. package/dist/cjs/{SVGLoader.js → SVGLoader/index.js} +15 -30
  9. package/dist/cjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
  10. package/dist/cjs/SVGLoader/utils/determineFontSize.js +27 -0
  11. package/dist/cjs/Viewport.d.ts +33 -1
  12. package/dist/cjs/components/TextComponent.js +3 -1
  13. package/dist/cjs/image/EditorImage.d.ts +2 -1
  14. package/dist/cjs/image/EditorImage.js +101 -5
  15. package/dist/cjs/rendering/caching/RenderingCacheNode.js +20 -15
  16. package/dist/cjs/rendering/renderers/CanvasRenderer.js +4 -4
  17. package/dist/cjs/testing/findNodeWithText.d.ts +3 -0
  18. package/dist/cjs/testing/findNodeWithText.js +16 -0
  19. package/dist/cjs/testing/firstElementAncestorOfNode.d.ts +3 -0
  20. package/dist/cjs/testing/firstElementAncestorOfNode.js +13 -0
  21. package/dist/cjs/testing/sendKeyPressRelease.d.ts +3 -0
  22. package/dist/cjs/testing/sendKeyPressRelease.js +8 -0
  23. package/dist/cjs/testing/sendPenEvent.d.ts +2 -2
  24. package/dist/cjs/testing/sendPenEvent.js +26 -3
  25. package/dist/cjs/toolbar/localization.d.ts +3 -0
  26. package/dist/cjs/toolbar/localization.js +3 -0
  27. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +1 -0
  28. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -0
  29. package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +23 -0
  30. package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.js +65 -0
  31. package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
  32. package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.js +21 -0
  33. package/dist/cjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
  34. package/dist/cjs/toolbar/widgets/InsertImageWidget/index.js +289 -0
  35. package/dist/cjs/toolbar/widgets/TextToolWidget.js +5 -3
  36. package/dist/cjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
  37. package/dist/cjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
  38. package/dist/cjs/toolbar/widgets/components/makeFileInput.js +113 -45
  39. package/dist/cjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
  40. package/dist/cjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
  41. package/dist/cjs/toolbar/widgets/components/makeSnappedList.js +168 -0
  42. package/dist/cjs/tools/Eraser.d.ts +7 -2
  43. package/dist/cjs/tools/Eraser.js +76 -6
  44. package/dist/cjs/tools/PanZoom.d.ts +54 -0
  45. package/dist/cjs/tools/PanZoom.js +54 -2
  46. package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -2
  47. package/dist/cjs/tools/SelectionTool/Selection.js +20 -20
  48. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
  49. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +6 -0
  50. package/dist/cjs/tools/SelectionTool/SelectionTool.js +1 -1
  51. package/dist/cjs/tools/SelectionTool/types.d.ts +19 -0
  52. package/dist/cjs/tools/TextTool.js +2 -1
  53. package/dist/cjs/tools/TextTool.test.d.ts +1 -0
  54. package/dist/cjs/tools/ToolController.d.ts +2 -0
  55. package/dist/cjs/tools/ToolController.js +10 -1
  56. package/dist/cjs/util/ReactiveValue.d.ts +6 -0
  57. package/dist/cjs/util/ReactiveValue.js +16 -0
  58. package/dist/cjs/util/bytesToSizeString.d.ts +8 -0
  59. package/dist/cjs/util/bytesToSizeString.js +26 -0
  60. package/dist/cjs/util/bytesToSizeString.test.d.ts +1 -0
  61. package/dist/cjs/util/stopPropagationOfScrollingWheelEvents.js +10 -6
  62. package/dist/cjs/util/waitForAll.d.ts +2 -0
  63. package/dist/cjs/util/waitForAll.js +2 -0
  64. package/dist/cjs/util/waitForImageLoaded.js +3 -0
  65. package/dist/cjs/util/waitForTimeout.d.ts +1 -0
  66. package/dist/cjs/util/waitForTimeout.js +1 -1
  67. package/dist/cjs/version.js +1 -1
  68. package/dist/mjs/Editor.d.ts +20 -1
  69. package/dist/mjs/Editor.mjs +6 -0
  70. package/dist/mjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
  71. package/dist/mjs/{SVGLoader.mjs → SVGLoader/index.mjs} +15 -30
  72. package/dist/mjs/SVGLoader/index.test.d.ts +1 -0
  73. package/dist/mjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
  74. package/dist/mjs/SVGLoader/utils/determineFontSize.mjs +25 -0
  75. package/dist/mjs/Viewport.d.ts +33 -1
  76. package/dist/mjs/components/TextComponent.mjs +3 -1
  77. package/dist/mjs/image/EditorImage.d.ts +2 -1
  78. package/dist/mjs/image/EditorImage.mjs +101 -5
  79. package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -15
  80. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +4 -4
  81. package/dist/mjs/testing/findNodeWithText.d.ts +3 -0
  82. package/dist/mjs/testing/findNodeWithText.mjs +14 -0
  83. package/dist/mjs/testing/firstElementAncestorOfNode.d.ts +3 -0
  84. package/dist/mjs/testing/firstElementAncestorOfNode.mjs +11 -0
  85. package/dist/mjs/testing/sendKeyPressRelease.d.ts +3 -0
  86. package/dist/mjs/testing/sendKeyPressRelease.mjs +6 -0
  87. package/dist/mjs/testing/sendPenEvent.d.ts +2 -2
  88. package/dist/mjs/testing/sendPenEvent.mjs +3 -3
  89. package/dist/mjs/toolbar/localization.d.ts +3 -0
  90. package/dist/mjs/toolbar/localization.mjs +3 -0
  91. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +1 -0
  92. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -0
  93. package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +23 -0
  94. package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.mjs +61 -0
  95. package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
  96. package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.mjs +16 -0
  97. package/dist/mjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
  98. package/dist/mjs/toolbar/widgets/InsertImageWidget/index.mjs +284 -0
  99. package/dist/mjs/toolbar/widgets/InsertImageWidget/index.test.d.ts +1 -0
  100. package/dist/mjs/toolbar/widgets/TextToolWidget.mjs +5 -3
  101. package/dist/mjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
  102. package/dist/mjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
  103. package/dist/mjs/toolbar/widgets/components/makeFileInput.mjs +113 -45
  104. package/dist/mjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
  105. package/dist/mjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
  106. package/dist/mjs/toolbar/widgets/components/makeSnappedList.mjs +163 -0
  107. package/dist/mjs/tools/Eraser.d.ts +7 -2
  108. package/dist/mjs/tools/Eraser.mjs +76 -6
  109. package/dist/mjs/tools/PanZoom.d.ts +54 -0
  110. package/dist/mjs/tools/PanZoom.mjs +54 -2
  111. package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -2
  112. package/dist/mjs/tools/SelectionTool/Selection.mjs +20 -20
  113. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
  114. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +6 -0
  115. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +1 -1
  116. package/dist/mjs/tools/SelectionTool/types.d.ts +19 -0
  117. package/dist/mjs/tools/TextTool.mjs +2 -1
  118. package/dist/mjs/tools/TextTool.test.d.ts +1 -0
  119. package/dist/mjs/tools/ToolController.d.ts +2 -0
  120. package/dist/mjs/tools/ToolController.mjs +10 -1
  121. package/dist/mjs/util/ReactiveValue.d.ts +6 -0
  122. package/dist/mjs/util/ReactiveValue.mjs +16 -0
  123. package/dist/mjs/util/bytesToSizeString.d.ts +8 -0
  124. package/dist/mjs/util/bytesToSizeString.mjs +24 -0
  125. package/dist/mjs/util/bytesToSizeString.test.d.ts +1 -0
  126. package/dist/mjs/util/stopPropagationOfScrollingWheelEvents.mjs +10 -6
  127. package/dist/mjs/util/waitForAll.d.ts +2 -0
  128. package/dist/mjs/util/waitForAll.mjs +2 -0
  129. package/dist/mjs/util/waitForImageLoaded.mjs +3 -0
  130. package/dist/mjs/util/waitForTimeout.d.ts +1 -0
  131. package/dist/mjs/util/waitForTimeout.mjs +1 -1
  132. package/dist/mjs/version.mjs +1 -1
  133. package/package.json +4 -4
  134. package/src/toolbar/EdgeToolbar.scss +8 -3
  135. package/src/toolbar/toolbar.scss +1 -7
  136. package/src/toolbar/widgets/{InsertImageWidget.scss → InsertImageWidget/index.scss} +3 -2
  137. package/src/toolbar/widgets/components/components.scss +2 -1
  138. package/src/toolbar/widgets/components/makeFileInput.scss +14 -1
  139. package/src/toolbar/widgets/components/makeSnappedList.scss +74 -0
  140. package/src/toolbar/widgets/widgets.scss +7 -0
  141. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
  142. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +0 -269
  143. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
  144. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +0 -264
  145. /package/dist/cjs/{SVGLoader.test.d.ts → SVGLoader/index.test.d.ts} +0 -0
  146. /package/dist/{mjs/SVGLoader.test.d.ts → cjs/toolbar/widgets/InsertImageWidget/index.test.d.ts} +0 -0
@@ -1,269 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const ImageComponent_1 = __importDefault(require("../../components/ImageComponent"));
7
- const Erase_1 = __importDefault(require("../../commands/Erase"));
8
- const EditorImage_1 = __importDefault(require("../../image/EditorImage"));
9
- const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
10
- const SelectionTool_1 = __importDefault(require("../../tools/SelectionTool/SelectionTool"));
11
- const math_1 = require("@js-draw/math");
12
- const fileToBase64Url_1 = __importDefault(require("../../util/fileToBase64Url"));
13
- const BaseWidget_1 = __importDefault(require("./BaseWidget"));
14
- const types_1 = require("../../types");
15
- const constants_1 = require("../constants");
16
- const makeFileInput_1 = __importDefault(require("./components/makeFileInput"));
17
- class ImageWrapper {
18
- constructor(imageBase64Url, preview, onUrlUpdate) {
19
- this.imageBase64Url = imageBase64Url;
20
- this.preview = preview;
21
- this.onUrlUpdate = onUrlUpdate;
22
- this.originalSrc = imageBase64Url;
23
- preview.src = imageBase64Url;
24
- }
25
- updateImageData(base64DataUrl) {
26
- this.preview.src = base64DataUrl;
27
- this.imageBase64Url = base64DataUrl;
28
- this.onUrlUpdate();
29
- }
30
- decreaseSize(resizeFactor = 3 / 4) {
31
- const canvas = document.createElement('canvas');
32
- canvas.width = this.preview.naturalWidth * resizeFactor;
33
- canvas.height = this.preview.naturalHeight * resizeFactor;
34
- const ctx = canvas.getContext('2d');
35
- ctx?.drawImage(this.preview, 0, 0, canvas.width, canvas.height);
36
- // JPEG can be much smaller than PNG for the same image size. Prefer it if
37
- // the image is already a JPEG.
38
- const format = this.originalSrc?.startsWith('data:image/jpeg;') ? 'image/jpeg' : 'image/png';
39
- this.updateImageData(canvas.toDataURL(format));
40
- }
41
- reset() {
42
- this.updateImageData(this.originalSrc);
43
- }
44
- isChanged() {
45
- return this.imageBase64Url !== this.originalSrc;
46
- }
47
- getBase64Url() {
48
- return this.imageBase64Url;
49
- }
50
- static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
51
- return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
52
- }
53
- }
54
- class InsertImageWidget extends BaseWidget_1.default {
55
- constructor(editor, localization) {
56
- localization ??= editor.localization;
57
- super(editor, 'insert-image-widget', localization);
58
- this.image = null;
59
- // Make the dropdown showable
60
- this.container.classList.add('dropdownShowable');
61
- editor.notifier.on(types_1.EditorEventType.SelectionUpdated, event => {
62
- if (event.kind === types_1.EditorEventType.SelectionUpdated && this.isDropdownVisible()) {
63
- this.updateInputs();
64
- }
65
- });
66
- }
67
- getTitle() {
68
- return this.localizationTable.image;
69
- }
70
- createIcon() {
71
- return this.editor.icons.makeInsertImageIcon();
72
- }
73
- setDropdownVisible(visible) {
74
- super.setDropdownVisible(visible);
75
- // Update the dropdown just before showing.
76
- if (this.isDropdownVisible()) {
77
- this.updateInputs();
78
- }
79
- }
80
- handleClick() {
81
- this.setDropdownVisible(!this.isDropdownVisible());
82
- }
83
- fillDropdown(dropdown) {
84
- const container = document.createElement('div');
85
- container.classList.add('insert-image-widget-dropdown-content', `${constants_1.toolbarCSSPrefix}spacedList`, `${constants_1.toolbarCSSPrefix}nonbutton-controls-main-list`);
86
- const { container: chooseImageRow, selectedFiles, } = (0, makeFileInput_1.default)(this.localizationTable.chooseFile, this.editor, 'image/*');
87
- const altTextRow = document.createElement('div');
88
- this.imagePreview = document.createElement('img');
89
- this.statusView = document.createElement('div');
90
- const actionButtonRow = document.createElement('div');
91
- actionButtonRow.classList.add('action-button-row');
92
- this.statusView.classList.add('insert-image-image-status-view');
93
- this.submitButton = document.createElement('button');
94
- this.selectedFiles = selectedFiles;
95
- this.imageAltTextInput = document.createElement('input');
96
- // Label the alt text input
97
- const imageAltTextLabel = document.createElement('label');
98
- const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
99
- this.imageAltTextInput.setAttribute('id', altTextInputId);
100
- imageAltTextLabel.htmlFor = altTextInputId;
101
- imageAltTextLabel.innerText = this.localizationTable.inputAltText;
102
- this.imageAltTextInput.type = 'text';
103
- this.imageAltTextInput.placeholder = this.localizationTable.describeTheImage;
104
- this.statusView.setAttribute('aria-live', 'polite');
105
- this.submitButton.innerText = this.localizationTable.submit;
106
- this.selectedFiles.onUpdateAndNow(async (files) => {
107
- if (files.length === 0) {
108
- this.image = null;
109
- this.onImageDataUpdate();
110
- return;
111
- }
112
- this.imagePreview.style.display = 'block';
113
- const image = files[0];
114
- let data = null;
115
- let errorMessage = null;
116
- try {
117
- data = await (0, fileToBase64Url_1.default)(image);
118
- }
119
- catch (error) {
120
- console.error('Image load error', error);
121
- errorMessage = this.localizationTable.imageLoadError(error);
122
- }
123
- if (data) {
124
- this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
125
- }
126
- else {
127
- this.image = null;
128
- }
129
- this.onImageDataUpdate();
130
- // Show the error after image update callbacks to ensure it is
131
- // actually shown.
132
- if (errorMessage) {
133
- this.statusView.innerText = errorMessage;
134
- }
135
- });
136
- altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
137
- actionButtonRow.replaceChildren(this.submitButton);
138
- container.replaceChildren(chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow);
139
- dropdown.replaceChildren(container);
140
- return true;
141
- }
142
- onImageDataUpdate() {
143
- const base64Data = this.image?.getBase64Url();
144
- if (base64Data) {
145
- this.submitButton.disabled = false;
146
- this.submitButton.style.display = '';
147
- this.imagePreview.style.display = '';
148
- this.updateImageSizeDisplay();
149
- }
150
- else {
151
- this.submitButton.disabled = true;
152
- this.submitButton.style.display = 'none';
153
- this.statusView.innerText = '';
154
- this.imagePreview.style.display = 'none';
155
- this.submitButton.disabled = true;
156
- }
157
- }
158
- hideDialog() {
159
- this.setDropdownVisible(false);
160
- }
161
- updateImageSizeDisplay() {
162
- const imageData = this.image?.getBase64Url() ?? '';
163
- const sizeInKiB = imageData.length / 1024;
164
- const sizeInMiB = sizeInKiB / 1024;
165
- let units = 'KiB';
166
- let size = sizeInKiB;
167
- if (sizeInMiB >= 1) {
168
- size = sizeInMiB;
169
- units = 'MiB';
170
- }
171
- const sizeText = document.createElement('span');
172
- sizeText.innerText = this.localizationTable.imageSize(Math.round(size), units);
173
- // Add a button to allow decreasing the size of large images.
174
- const decreaseSizeButton = document.createElement('button');
175
- decreaseSizeButton.innerText = this.localizationTable.decreaseImageSize;
176
- decreaseSizeButton.onclick = () => {
177
- this.image?.decreaseSize();
178
- };
179
- const resetSizeButton = document.createElement('button');
180
- resetSizeButton.innerText = this.localizationTable.resetImage;
181
- resetSizeButton.onclick = () => {
182
- this.image?.reset();
183
- };
184
- this.statusView.replaceChildren(sizeText);
185
- const largeImageThreshold = 0.12; // MiB
186
- if (sizeInMiB > largeImageThreshold) {
187
- this.statusView.appendChild(decreaseSizeButton);
188
- }
189
- else if (this.image?.isChanged()) {
190
- this.statusView.appendChild(resetSizeButton);
191
- }
192
- }
193
- updateInputs() {
194
- const resetInputs = () => {
195
- this.selectedFiles?.set([]);
196
- this.imageAltTextInput.value = '';
197
- this.imagePreview.style.display = 'none';
198
- this.submitButton.disabled = true;
199
- this.statusView.innerText = '';
200
- this.submitButton.style.display = '';
201
- this.imageAltTextInput.oninput = null;
202
- };
203
- resetInputs();
204
- const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool_1.default);
205
- const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
206
- // Check: Is there a selected image that can be edited?
207
- let editingImage = null;
208
- if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent_1.default) {
209
- editingImage = selectedObjects[0];
210
- this.imageAltTextInput.value = editingImage.getAltText() ?? '';
211
- this.image = ImageWrapper.fromSrcAndPreview(editingImage.getURL(), this.imagePreview, () => this.onImageDataUpdate());
212
- this.onImageDataUpdate();
213
- }
214
- else if (selectedObjects.length > 0) {
215
- // If not, clear the selection.
216
- selectionTools.forEach(tool => tool.clearSelection());
217
- }
218
- // Show the submit button only when there is data to submit.
219
- this.submitButton.style.display = 'none';
220
- this.imageAltTextInput.oninput = () => {
221
- if (this.imagePreview.src?.length > 0) {
222
- this.submitButton.style.display = '';
223
- }
224
- };
225
- this.submitButton.onclick = async () => {
226
- if (!this.image) {
227
- return;
228
- }
229
- const image = new Image();
230
- image.src = this.image.getBase64Url();
231
- image.setAttribute('alt', this.imageAltTextInput.value);
232
- let component;
233
- try {
234
- component = await ImageComponent_1.default.fromImage(image, math_1.Mat33.identity);
235
- }
236
- catch (error) {
237
- console.error('Error loading image', error);
238
- this.statusView.innerText = this.localizationTable.imageLoadError(error);
239
- return;
240
- }
241
- if (component.getBBox().area === 0) {
242
- this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
243
- return;
244
- }
245
- this.hideDialog();
246
- if (editingImage) {
247
- const eraseCommand = new Erase_1.default([editingImage]);
248
- // Try to preserve the original width
249
- const originalTransform = editingImage.getTransformation();
250
- // || 1: Prevent division by zero
251
- const originalWidth = editingImage.getBBox().width || 1;
252
- const newWidth = component.getBBox().transformedBoundingBox(originalTransform).width || 1;
253
- const widthAdjustTransform = math_1.Mat33.scaling2D(originalWidth / newWidth);
254
- await this.editor.dispatch((0, uniteCommands_1.default)([
255
- EditorImage_1.default.addElement(component),
256
- component.transformBy(originalTransform.rightMul(widthAdjustTransform)),
257
- component.setZIndex(editingImage.getZIndex()),
258
- eraseCommand,
259
- ]));
260
- selectionTools[0]?.setSelection([component]);
261
- }
262
- else {
263
- await this.editor.addAndCenterComponents([component]);
264
- }
265
- };
266
- }
267
- }
268
- InsertImageWidget.nextInputId = 0;
269
- exports.default = InsertImageWidget;
@@ -1,22 +0,0 @@
1
- import Editor from '../../Editor';
2
- import { ToolbarLocalization } from '../localization';
3
- import BaseWidget from './BaseWidget';
4
- export default class InsertImageWidget extends BaseWidget {
5
- private imagePreview;
6
- private image;
7
- private selectedFiles;
8
- private imageAltTextInput;
9
- private statusView;
10
- private submitButton;
11
- constructor(editor: Editor, localization?: ToolbarLocalization);
12
- protected getTitle(): string;
13
- protected createIcon(): Element | null;
14
- protected setDropdownVisible(visible: boolean): void;
15
- protected handleClick(): void;
16
- private static nextInputId;
17
- protected fillDropdown(dropdown: HTMLElement): boolean;
18
- private onImageDataUpdate;
19
- private hideDialog;
20
- private updateImageSizeDisplay;
21
- private updateInputs;
22
- }
@@ -1,264 +0,0 @@
1
- import ImageComponent from '../../components/ImageComponent.mjs';
2
- import Erase from '../../commands/Erase.mjs';
3
- import EditorImage from '../../image/EditorImage.mjs';
4
- import uniteCommands from '../../commands/uniteCommands.mjs';
5
- import SelectionTool from '../../tools/SelectionTool/SelectionTool.mjs';
6
- import { Mat33 } from '@js-draw/math';
7
- import fileToBase64Url from '../../util/fileToBase64Url.mjs';
8
- import BaseWidget from './BaseWidget.mjs';
9
- import { EditorEventType } from '../../types.mjs';
10
- import { toolbarCSSPrefix } from '../constants.mjs';
11
- import makeFileInput from './components/makeFileInput.mjs';
12
- class ImageWrapper {
13
- constructor(imageBase64Url, preview, onUrlUpdate) {
14
- this.imageBase64Url = imageBase64Url;
15
- this.preview = preview;
16
- this.onUrlUpdate = onUrlUpdate;
17
- this.originalSrc = imageBase64Url;
18
- preview.src = imageBase64Url;
19
- }
20
- updateImageData(base64DataUrl) {
21
- this.preview.src = base64DataUrl;
22
- this.imageBase64Url = base64DataUrl;
23
- this.onUrlUpdate();
24
- }
25
- decreaseSize(resizeFactor = 3 / 4) {
26
- const canvas = document.createElement('canvas');
27
- canvas.width = this.preview.naturalWidth * resizeFactor;
28
- canvas.height = this.preview.naturalHeight * resizeFactor;
29
- const ctx = canvas.getContext('2d');
30
- ctx?.drawImage(this.preview, 0, 0, canvas.width, canvas.height);
31
- // JPEG can be much smaller than PNG for the same image size. Prefer it if
32
- // the image is already a JPEG.
33
- const format = this.originalSrc?.startsWith('data:image/jpeg;') ? 'image/jpeg' : 'image/png';
34
- this.updateImageData(canvas.toDataURL(format));
35
- }
36
- reset() {
37
- this.updateImageData(this.originalSrc);
38
- }
39
- isChanged() {
40
- return this.imageBase64Url !== this.originalSrc;
41
- }
42
- getBase64Url() {
43
- return this.imageBase64Url;
44
- }
45
- static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
46
- return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
47
- }
48
- }
49
- class InsertImageWidget extends BaseWidget {
50
- constructor(editor, localization) {
51
- localization ??= editor.localization;
52
- super(editor, 'insert-image-widget', localization);
53
- this.image = null;
54
- // Make the dropdown showable
55
- this.container.classList.add('dropdownShowable');
56
- editor.notifier.on(EditorEventType.SelectionUpdated, event => {
57
- if (event.kind === EditorEventType.SelectionUpdated && this.isDropdownVisible()) {
58
- this.updateInputs();
59
- }
60
- });
61
- }
62
- getTitle() {
63
- return this.localizationTable.image;
64
- }
65
- createIcon() {
66
- return this.editor.icons.makeInsertImageIcon();
67
- }
68
- setDropdownVisible(visible) {
69
- super.setDropdownVisible(visible);
70
- // Update the dropdown just before showing.
71
- if (this.isDropdownVisible()) {
72
- this.updateInputs();
73
- }
74
- }
75
- handleClick() {
76
- this.setDropdownVisible(!this.isDropdownVisible());
77
- }
78
- fillDropdown(dropdown) {
79
- const container = document.createElement('div');
80
- container.classList.add('insert-image-widget-dropdown-content', `${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}nonbutton-controls-main-list`);
81
- const { container: chooseImageRow, selectedFiles, } = makeFileInput(this.localizationTable.chooseFile, this.editor, 'image/*');
82
- const altTextRow = document.createElement('div');
83
- this.imagePreview = document.createElement('img');
84
- this.statusView = document.createElement('div');
85
- const actionButtonRow = document.createElement('div');
86
- actionButtonRow.classList.add('action-button-row');
87
- this.statusView.classList.add('insert-image-image-status-view');
88
- this.submitButton = document.createElement('button');
89
- this.selectedFiles = selectedFiles;
90
- this.imageAltTextInput = document.createElement('input');
91
- // Label the alt text input
92
- const imageAltTextLabel = document.createElement('label');
93
- const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
94
- this.imageAltTextInput.setAttribute('id', altTextInputId);
95
- imageAltTextLabel.htmlFor = altTextInputId;
96
- imageAltTextLabel.innerText = this.localizationTable.inputAltText;
97
- this.imageAltTextInput.type = 'text';
98
- this.imageAltTextInput.placeholder = this.localizationTable.describeTheImage;
99
- this.statusView.setAttribute('aria-live', 'polite');
100
- this.submitButton.innerText = this.localizationTable.submit;
101
- this.selectedFiles.onUpdateAndNow(async (files) => {
102
- if (files.length === 0) {
103
- this.image = null;
104
- this.onImageDataUpdate();
105
- return;
106
- }
107
- this.imagePreview.style.display = 'block';
108
- const image = files[0];
109
- let data = null;
110
- let errorMessage = null;
111
- try {
112
- data = await fileToBase64Url(image);
113
- }
114
- catch (error) {
115
- console.error('Image load error', error);
116
- errorMessage = this.localizationTable.imageLoadError(error);
117
- }
118
- if (data) {
119
- this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
120
- }
121
- else {
122
- this.image = null;
123
- }
124
- this.onImageDataUpdate();
125
- // Show the error after image update callbacks to ensure it is
126
- // actually shown.
127
- if (errorMessage) {
128
- this.statusView.innerText = errorMessage;
129
- }
130
- });
131
- altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
132
- actionButtonRow.replaceChildren(this.submitButton);
133
- container.replaceChildren(chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow);
134
- dropdown.replaceChildren(container);
135
- return true;
136
- }
137
- onImageDataUpdate() {
138
- const base64Data = this.image?.getBase64Url();
139
- if (base64Data) {
140
- this.submitButton.disabled = false;
141
- this.submitButton.style.display = '';
142
- this.imagePreview.style.display = '';
143
- this.updateImageSizeDisplay();
144
- }
145
- else {
146
- this.submitButton.disabled = true;
147
- this.submitButton.style.display = 'none';
148
- this.statusView.innerText = '';
149
- this.imagePreview.style.display = 'none';
150
- this.submitButton.disabled = true;
151
- }
152
- }
153
- hideDialog() {
154
- this.setDropdownVisible(false);
155
- }
156
- updateImageSizeDisplay() {
157
- const imageData = this.image?.getBase64Url() ?? '';
158
- const sizeInKiB = imageData.length / 1024;
159
- const sizeInMiB = sizeInKiB / 1024;
160
- let units = 'KiB';
161
- let size = sizeInKiB;
162
- if (sizeInMiB >= 1) {
163
- size = sizeInMiB;
164
- units = 'MiB';
165
- }
166
- const sizeText = document.createElement('span');
167
- sizeText.innerText = this.localizationTable.imageSize(Math.round(size), units);
168
- // Add a button to allow decreasing the size of large images.
169
- const decreaseSizeButton = document.createElement('button');
170
- decreaseSizeButton.innerText = this.localizationTable.decreaseImageSize;
171
- decreaseSizeButton.onclick = () => {
172
- this.image?.decreaseSize();
173
- };
174
- const resetSizeButton = document.createElement('button');
175
- resetSizeButton.innerText = this.localizationTable.resetImage;
176
- resetSizeButton.onclick = () => {
177
- this.image?.reset();
178
- };
179
- this.statusView.replaceChildren(sizeText);
180
- const largeImageThreshold = 0.12; // MiB
181
- if (sizeInMiB > largeImageThreshold) {
182
- this.statusView.appendChild(decreaseSizeButton);
183
- }
184
- else if (this.image?.isChanged()) {
185
- this.statusView.appendChild(resetSizeButton);
186
- }
187
- }
188
- updateInputs() {
189
- const resetInputs = () => {
190
- this.selectedFiles?.set([]);
191
- this.imageAltTextInput.value = '';
192
- this.imagePreview.style.display = 'none';
193
- this.submitButton.disabled = true;
194
- this.statusView.innerText = '';
195
- this.submitButton.style.display = '';
196
- this.imageAltTextInput.oninput = null;
197
- };
198
- resetInputs();
199
- const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
200
- const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
201
- // Check: Is there a selected image that can be edited?
202
- let editingImage = null;
203
- if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
204
- editingImage = selectedObjects[0];
205
- this.imageAltTextInput.value = editingImage.getAltText() ?? '';
206
- this.image = ImageWrapper.fromSrcAndPreview(editingImage.getURL(), this.imagePreview, () => this.onImageDataUpdate());
207
- this.onImageDataUpdate();
208
- }
209
- else if (selectedObjects.length > 0) {
210
- // If not, clear the selection.
211
- selectionTools.forEach(tool => tool.clearSelection());
212
- }
213
- // Show the submit button only when there is data to submit.
214
- this.submitButton.style.display = 'none';
215
- this.imageAltTextInput.oninput = () => {
216
- if (this.imagePreview.src?.length > 0) {
217
- this.submitButton.style.display = '';
218
- }
219
- };
220
- this.submitButton.onclick = async () => {
221
- if (!this.image) {
222
- return;
223
- }
224
- const image = new Image();
225
- image.src = this.image.getBase64Url();
226
- image.setAttribute('alt', this.imageAltTextInput.value);
227
- let component;
228
- try {
229
- component = await ImageComponent.fromImage(image, Mat33.identity);
230
- }
231
- catch (error) {
232
- console.error('Error loading image', error);
233
- this.statusView.innerText = this.localizationTable.imageLoadError(error);
234
- return;
235
- }
236
- if (component.getBBox().area === 0) {
237
- this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
238
- return;
239
- }
240
- this.hideDialog();
241
- if (editingImage) {
242
- const eraseCommand = new Erase([editingImage]);
243
- // Try to preserve the original width
244
- const originalTransform = editingImage.getTransformation();
245
- // || 1: Prevent division by zero
246
- const originalWidth = editingImage.getBBox().width || 1;
247
- const newWidth = component.getBBox().transformedBoundingBox(originalTransform).width || 1;
248
- const widthAdjustTransform = Mat33.scaling2D(originalWidth / newWidth);
249
- await this.editor.dispatch(uniteCommands([
250
- EditorImage.addElement(component),
251
- component.transformBy(originalTransform.rightMul(widthAdjustTransform)),
252
- component.setZIndex(editingImage.getZIndex()),
253
- eraseCommand,
254
- ]));
255
- selectionTools[0]?.setSelection([component]);
256
- }
257
- else {
258
- await this.editor.addAndCenterComponents([component]);
259
- }
260
- };
261
- }
262
- }
263
- InsertImageWidget.nextInputId = 0;
264
- export default InsertImageWidget;