js-draw 0.15.2 → 0.16.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 (59) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
  2. package/CHANGELOG.md +6 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +11 -0
  5. package/dist/src/Editor.js +52 -4
  6. package/dist/src/EditorImage.d.ts +11 -11
  7. package/dist/src/EditorImage.js +54 -18
  8. package/dist/src/Viewport.d.ts +5 -0
  9. package/dist/src/Viewport.js +11 -0
  10. package/dist/src/components/AbstractComponent.d.ts +1 -0
  11. package/dist/src/components/AbstractComponent.js +5 -0
  12. package/dist/src/components/ImageBackground.d.ts +2 -1
  13. package/dist/src/components/ImageBackground.js +8 -1
  14. package/dist/src/localizations/es.js +1 -1
  15. package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
  16. package/dist/src/toolbar/HTMLToolbar.js +127 -15
  17. package/dist/src/toolbar/IconProvider.d.ts +2 -0
  18. package/dist/src/toolbar/IconProvider.js +44 -0
  19. package/dist/src/toolbar/localization.d.ts +5 -0
  20. package/dist/src/toolbar/localization.js +5 -0
  21. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
  22. package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
  23. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
  24. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
  25. package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
  26. package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
  27. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
  28. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
  29. package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
  30. package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
  31. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  32. package/dist/src/toolbar/widgets/lib.js +1 -0
  33. package/dist/src/tools/PasteHandler.js +2 -2
  34. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
  35. package/dist/src/tools/SelectionTool/Selection.js +2 -1
  36. package/package.json +1 -1
  37. package/src/Editor.loadFrom.test.ts +24 -0
  38. package/src/Editor.ts +59 -4
  39. package/src/EditorImage.ts +66 -23
  40. package/src/Viewport.ts +13 -0
  41. package/src/components/AbstractComponent.ts +6 -0
  42. package/src/components/ImageBackground.test.ts +35 -0
  43. package/src/components/ImageBackground.ts +10 -1
  44. package/src/localizations/es.ts +8 -0
  45. package/src/math/Mat33.test.ts +30 -5
  46. package/src/rendering/renderers/CanvasRenderer.ts +1 -1
  47. package/src/toolbar/HTMLToolbar.ts +164 -16
  48. package/src/toolbar/IconProvider.ts +46 -0
  49. package/src/toolbar/localization.ts +10 -0
  50. package/src/toolbar/toolbar.css +2 -0
  51. package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
  52. package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
  53. package/src/toolbar/widgets/BaseWidget.ts +34 -2
  54. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
  55. package/src/toolbar/widgets/OverflowWidget.css +9 -0
  56. package/src/toolbar/widgets/OverflowWidget.ts +83 -0
  57. package/src/toolbar/widgets/lib.ts +2 -1
  58. package/src/tools/PasteHandler.ts +3 -2
  59. package/src/tools/SelectionTool/Selection.ts +2 -1
@@ -18,6 +18,9 @@ import HandToolWidget from './widgets/HandToolWidget';
18
18
  import BaseWidget from './widgets/BaseWidget';
19
19
  import ActionButtonWidget from './widgets/ActionButtonWidget';
20
20
  import InsertImageWidget from './widgets/InsertImageWidget';
21
+ import DocumentPropertiesWidget from './widgets/DocumentPropertiesWidget';
22
+ import OverflowWidget from './widgets/OverflowWidget';
23
+ import { DispatcherEventListener } from '../EventDispatcher';
21
24
 
22
25
  export const toolbarCSSPrefix = 'toolbar-';
23
26
 
@@ -37,8 +40,14 @@ interface SpacerOptions {
37
40
 
38
41
  export default class HTMLToolbar {
39
42
  private container: HTMLElement;
43
+ private resizeObserver: ResizeObserver;
44
+ private listeners: DispatcherEventListener[] = [];
40
45
 
41
- private widgets: Record<string, BaseWidget> = {};
46
+ private widgetsById: Record<string, BaseWidget> = {};
47
+ private widgetList: Array<BaseWidget> = [];
48
+
49
+ // Widget to toggle overflow menu.
50
+ private overflowWidget: OverflowWidget|null = null;
42
51
 
43
52
  private static colorisStarted: boolean = false;
44
53
  private updateColoris: UpdateColorisCallback|null = null;
@@ -58,6 +67,15 @@ export default class HTMLToolbar {
58
67
  HTMLToolbar.colorisStarted = true;
59
68
  }
60
69
  this.setupColorPickers();
70
+
71
+ if ('ResizeObserver' in window) {
72
+ this.resizeObserver = new ResizeObserver((_entries) => {
73
+ this.reLayout();
74
+ });
75
+ this.resizeObserver.observe(this.container);
76
+ } else {
77
+ console.warn('ResizeObserver not supported. Toolbar will not resize.');
78
+ }
61
79
  }
62
80
 
63
81
  // @internal
@@ -116,7 +134,7 @@ export default class HTMLToolbar {
116
134
  }
117
135
  };
118
136
 
119
- this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
137
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
120
138
  if (event.kind !== EditorEventType.ColorPickerToggled) {
121
139
  return;
122
140
  }
@@ -124,14 +142,102 @@ export default class HTMLToolbar {
124
142
  // Show/hide the overlay. Making the overlay visible gives users a surface to click
125
143
  // on that shows/hides the color picker.
126
144
  closePickerOverlay.style.display = event.open ? 'block' : 'none';
127
- });
145
+ }));
128
146
 
129
147
  // Add newly-selected colors to the swatch.
130
- this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
148
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
131
149
  if (event.kind === EditorEventType.ColorPickerColorSelected) {
132
150
  addColorToSwatch(event.color.toHexString());
133
151
  }
134
- });
152
+ }));
153
+ }
154
+
155
+ private reLayoutQueued: boolean = false;
156
+ private queueReLayout() {
157
+ if (!this.reLayoutQueued) {
158
+ this.reLayoutQueued = true;
159
+ requestAnimationFrame(() => this.reLayout());
160
+ }
161
+ }
162
+
163
+ private reLayout() {
164
+ this.reLayoutQueued = false;
165
+
166
+ if (!this.overflowWidget) {
167
+ return;
168
+ }
169
+
170
+ const getTotalWidth = (widgetList: Array<BaseWidget>) => {
171
+ let totalWidth = 0;
172
+ for (const widget of widgetList) {
173
+ if (!widget.isHidden()) {
174
+ totalWidth += widget.getButtonWidth();
175
+ }
176
+ }
177
+
178
+ return totalWidth;
179
+ };
180
+
181
+ let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
182
+ let shownWidgetWidth = getTotalWidth(this.widgetList) - overflowWidgetsWidth;
183
+ let availableWidth = this.container.clientWidth * 0.87;
184
+
185
+ // If on a device that has enough vertical space, allow
186
+ // showing two rows of buttons.
187
+ // TODO: Fix magic numbers
188
+ if (window.innerHeight > availableWidth * 1.75) {
189
+ availableWidth *= 1.75;
190
+ }
191
+
192
+ let updatedChildren = false;
193
+
194
+ if (shownWidgetWidth + overflowWidgetsWidth <= availableWidth) {
195
+ // Move widgets to the main menu.
196
+ const overflowChildren = this.overflowWidget.clearChildren();
197
+
198
+ for (const child of overflowChildren) {
199
+ child.addTo(this.container);
200
+ child.setIsToplevel(true);
201
+
202
+ if (!child.isHidden()) {
203
+ shownWidgetWidth += child.getButtonWidth();
204
+ }
205
+ }
206
+
207
+ this.overflowWidget.setHidden(true);
208
+ overflowWidgetsWidth = 0;
209
+
210
+ updatedChildren = true;
211
+ }
212
+
213
+ if (shownWidgetWidth >= availableWidth) {
214
+ // Move widgets to the overflow menu.
215
+ this.overflowWidget.setHidden(false);
216
+
217
+ // Start with the rightmost widget, move to the leftmost
218
+ for (
219
+ let i = this.widgetList.length - 1;
220
+ i >= 0 && shownWidgetWidth >= availableWidth;
221
+ i--
222
+ ) {
223
+ const child = this.widgetList[i];
224
+
225
+ if (this.overflowWidget.hasAsChild(child)) {
226
+ continue;
227
+ }
228
+
229
+ if (child.canBeInOverflowMenu()) {
230
+ shownWidgetWidth -= child.getButtonWidth();
231
+ this.overflowWidget.addToOverflow(child);
232
+ }
233
+ }
234
+
235
+ updatedChildren = true;
236
+ }
237
+
238
+ if (updatedChildren) {
239
+ this.setupColorPickers();
240
+ }
135
241
  }
136
242
 
137
243
  /**
@@ -147,14 +253,21 @@ export default class HTMLToolbar {
147
253
  */
148
254
  public addWidget(widget: BaseWidget) {
149
255
  // Prevent name collisions
150
- const id = widget.getUniqueIdIn(this.widgets);
256
+ const id = widget.getUniqueIdIn(this.widgetsById);
151
257
 
152
258
  // Add the widget
153
- this.widgets[id] = widget;
259
+ this.widgetsById[id] = widget;
260
+ this.widgetList.push(widget);
154
261
 
155
262
  // Add HTML elements.
156
- widget.addTo(this.container);
263
+ const container = widget.addTo(this.container);
157
264
  this.setupColorPickers();
265
+
266
+ // Ensure that the widget gets displayed in the correct
267
+ // place in the toolbar, even if it's removed and re-added.
268
+ container.style.order = `${this.widgetList.length}`;
269
+
270
+ this.queueReLayout();
158
271
  }
159
272
 
160
273
  /**
@@ -200,8 +313,8 @@ export default class HTMLToolbar {
200
313
  public serializeState(): string {
201
314
  const result: Record<string, any> = {};
202
315
 
203
- for (const widgetId in this.widgets) {
204
- result[widgetId] = this.widgets[widgetId].serializeState();
316
+ for (const widgetId in this.widgetsById) {
317
+ result[widgetId] = this.widgetsById[widgetId].serializeState();
205
318
  }
206
319
 
207
320
  return JSON.stringify(result);
@@ -215,11 +328,11 @@ export default class HTMLToolbar {
215
328
  const data = JSON.parse(state);
216
329
 
217
330
  for (const widgetId in data) {
218
- if (!(widgetId in this.widgets)) {
331
+ if (!(widgetId in this.widgetsById)) {
219
332
  console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
220
333
  }
221
334
 
222
- this.widgets[widgetId].deserializeFrom(data[widgetId]);
335
+ this.widgetsById[widgetId].deserializeFrom(data[widgetId]);
223
336
  }
224
337
  }
225
338
 
@@ -228,7 +341,11 @@ export default class HTMLToolbar {
228
341
  *
229
342
  * @return The added button.
230
343
  */
231
- public addActionButton(title: string|ActionButtonIcon, command: ()=> void): BaseWidget {
344
+ public addActionButton(
345
+ title: string|ActionButtonIcon,
346
+ command: ()=> void,
347
+ mustBeToplevel: boolean = true
348
+ ): BaseWidget {
232
349
  const titleString = typeof title === 'string' ? title : title.label;
233
350
  const widgetId = 'action-button';
234
351
 
@@ -246,7 +363,8 @@ export default class HTMLToolbar {
246
363
  makeIcon,
247
364
  titleString,
248
365
  command,
249
- this.editor.localization
366
+ this.editor.localization,
367
+ mustBeToplevel,
250
368
  );
251
369
 
252
370
  this.addWidget(widget);
@@ -300,27 +418,57 @@ export default class HTMLToolbar {
300
418
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
301
419
  }
302
420
 
303
- this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
304
-
305
421
  const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
306
422
  if (panZoomTool) {
307
423
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
308
424
  }
425
+
426
+ this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
309
427
  }
310
428
 
311
429
  public addDefaultActionButtons() {
430
+ this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
312
431
  this.addUndoRedoButtons();
313
432
  }
314
433
 
434
+ /**
435
+ * Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
436
+ * that this widget is in the correct space (if shown).
437
+ *
438
+ * @example
439
+ * ```ts
440
+ * toolbar.addDefaultToolWidgets();
441
+ * toolbar.addOverflowWidget();
442
+ * toolbar.addDefaultActionButtons();
443
+ * ```
444
+ * shows the overflow widget between the default tool widgets and the default action buttons,
445
+ * if shown.
446
+ */
447
+ public addOverflowWidget() {
448
+ this.overflowWidget = new OverflowWidget(this.editor, this.localizationTable);
449
+ this.addWidget(this.overflowWidget);
450
+ }
451
+
315
452
  /**
316
453
  * Adds both the default tool widgets and action buttons. Equivalent to
317
454
  * ```ts
318
455
  * toolbar.addDefaultToolWidgets();
456
+ * toolbar.addOverflowWidget();
319
457
  * toolbar.addDefaultActionButtons();
320
458
  * ```
321
459
  */
322
460
  public addDefaults() {
323
461
  this.addDefaultToolWidgets();
462
+ this.addOverflowWidget();
324
463
  this.addDefaultActionButtons();
325
464
  }
465
+
466
+ public remove() {
467
+ this.container.remove();
468
+ this.resizeObserver.disconnect();
469
+
470
+ for (const listener of this.listeners) {
471
+ listener.remove();
472
+ }
473
+ }
326
474
  }
@@ -686,5 +686,51 @@ export default class IconProvider {
686
686
  svg.setAttribute('viewBox', '0 0 100 100');
687
687
  return svg;
688
688
  }
689
+
690
+ public makeConfigureDocumentIcon(): IconType {
691
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
692
+ svg.innerHTML = `
693
+ <path
694
+ d='
695
+ M 5,5 V 95 H 95 V 5 Z m 5,5 H 90 V 90 H 10 Z
696
+ m 5,10 V 30 H 50 V 25 H 20 v -5 z
697
+ m 40,0 V 50 H 85 V 20 Z
698
+ m 2,2 H 83 V 39 L 77,28 70,42 64,35 57,45 Z
699
+ m 8.5,5 C 64.67,27 64,27.67 64,28.5 64,29.33 64.67,30 65.5,30 66.33,30 67,29.33 67,28.5 67,27.67 66.33,27 65.5,27 Z
700
+ M 15,40 v 5 h 35 v -5 z
701
+ m 0,15 v 5 h 70 v -5 z
702
+ m 0,15 v 5 h 70 v -5 z
703
+ '
704
+ style='fill: var(--icon-color);'
705
+ />
706
+ `;
707
+ svg.setAttribute('viewBox', '0 0 100 100');
708
+ return svg;
709
+ }
710
+
711
+ public makeOverflowIcon(): IconType {
712
+ return this.makeIconFromPath(`
713
+ M 15 40
714
+ A 12.5 12.5 0 0 0 2.5 52.5
715
+ A 12.5 12.5 0 0 0 15 65
716
+ A 12.5 12.5 0 0 0 27.5 52.5
717
+ A 12.5 12.5 0 0 0 15 40
718
+ z
719
+
720
+ M 50 40
721
+ A 12.5 12.5 0 0 0 37.5 52.5
722
+ A 12.5 12.5 0 0 0 50 65
723
+ A 12.5 12.5 0 0 0 62.5 52.5
724
+ A 12.5 12.5 0 0 0 50 40
725
+ z
726
+
727
+ M 85 40
728
+ A 12.5 12.5 0 0 0 72.5 52.5
729
+ A 12.5 12.5 0 0 0 85 65
730
+ A 12.5 12.5 0 0 0 97.5 52.5
731
+ A 12.5 12.5 0 0 0 85 40
732
+ z
733
+ `);
734
+ }
689
735
 
690
736
  }
@@ -35,6 +35,11 @@ export interface ToolbarLocalization {
35
35
  resetView: string;
36
36
  selectionToolKeyboardShortcuts: string;
37
37
  paste: string;
38
+ documentProperties: string;
39
+ backgroundColor: string;
40
+ imageWidthOption: string;
41
+ imageHeightOption: string;
42
+ toggleOverflow: string,
38
43
 
39
44
  errorImageHasZeroSize: string;
40
45
 
@@ -72,6 +77,11 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
72
77
  pickColorFromScreen: 'Pick color from screen',
73
78
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
74
79
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
80
+ documentProperties: 'Document',
81
+ backgroundColor: 'Background Color: ',
82
+ imageWidthOption: 'Width: ',
83
+ imageHeightOption: 'Height: ',
84
+ toggleOverflow: 'More',
75
85
 
76
86
  touchPanning: 'Touchscreen panning',
77
87
 
@@ -1,4 +1,6 @@
1
1
  @import url(./widgets/InsertImageWidget.css);
2
+ @import url(./widgets/OverflowWidget.css);
3
+
2
4
 
3
5
  .toolbar-root {
4
6
  background-color: var(--primary-background-color);
@@ -12,6 +12,7 @@ export default class ActionButtonWidget extends BaseWidget {
12
12
  protected clickAction: ()=>void,
13
13
 
14
14
  localizationTable?: ToolbarLocalization,
15
+ protected mustBeToplevel: boolean = false,
15
16
  ) {
16
17
  super(editor, id, localizationTable);
17
18
  }
@@ -31,4 +32,8 @@ export default class ActionButtonWidget extends BaseWidget {
31
32
  protected fillDropdown(_dropdown: HTMLElement): boolean {
32
33
  return false;
33
34
  }
35
+
36
+ public canBeInOverflowMenu(): boolean {
37
+ return !this.mustBeToplevel;
38
+ }
34
39
  }
@@ -48,7 +48,9 @@ export default abstract class BaseToolWidget extends BaseWidget {
48
48
  }
49
49
 
50
50
  public addTo(parent: HTMLElement) {
51
- super.addTo(parent);
51
+ const result = super.addTo(parent);
52
52
  this.setSelected(this.targetTool.isEnabled());
53
+
54
+ return result;
53
55
  }
54
56
  }
@@ -1,4 +1,5 @@
1
1
  import Editor from '../../Editor';
2
+ import { DispatcherEventListener } from '../../EventDispatcher';
2
3
  import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
3
4
  import { EditorEventType, InputEvtType, KeyPressEvent } from '../../types';
4
5
  import { toolbarCSSPrefix } from '../HTMLToolbar';
@@ -168,7 +169,10 @@ export default abstract class BaseWidget {
168
169
  this.subWidgets[id] = widget;
169
170
  }
170
171
 
172
+ private toolbarWidgetToggleListener: DispatcherEventListener|null = null;
173
+
171
174
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
175
+ // Returns the element that was just added to `parent`.
172
176
  // @internal
173
177
  public addTo(parent: HTMLElement) {
174
178
  this.label.innerText = this.getTitle();
@@ -178,6 +182,8 @@ export default abstract class BaseWidget {
178
182
  this.icon = null;
179
183
  this.updateIcon();
180
184
 
185
+ this.container.replaceChildren();
186
+
181
187
  this.button.replaceChildren(this.icon!, this.label);
182
188
  this.container.appendChild(this.button);
183
189
 
@@ -187,7 +193,11 @@ export default abstract class BaseWidget {
187
193
  this.button.appendChild(this.dropdownIcon);
188
194
  this.container.appendChild(this.dropdownContainer);
189
195
 
190
- this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
196
+ if (this.toolbarWidgetToggleListener) {
197
+ this.toolbarWidgetToggleListener.remove();
198
+ }
199
+
200
+ this.toolbarWidgetToggleListener = this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
191
201
  if (
192
202
  evt.kind === EditorEventType.ToolbarDropdownShown
193
203
  && evt.parentWidget !== this
@@ -202,7 +212,13 @@ export default abstract class BaseWidget {
202
212
  }
203
213
 
204
214
  this.setDropdownVisible(false);
215
+
216
+ if (this.container.parentElement) {
217
+ this.container.remove();
218
+ }
219
+
205
220
  parent.appendChild(this.container);
221
+ return this.container;
206
222
  }
207
223
 
208
224
 
@@ -272,6 +288,22 @@ export default abstract class BaseWidget {
272
288
  this.repositionDropdown();
273
289
  }
274
290
 
291
+ public canBeInOverflowMenu(): boolean {
292
+ return true;
293
+ }
294
+
295
+ public getButtonWidth(): number {
296
+ return this.button.clientWidth;
297
+ }
298
+
299
+ public isHidden(): boolean {
300
+ return this.container.style.display === 'none';
301
+ }
302
+
303
+ public setHidden(hidden: boolean) {
304
+ this.container.style.display = hidden ? 'none' : '';
305
+ }
306
+
275
307
  protected repositionDropdown() {
276
308
  const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
277
309
  const screenWidth = document.body.clientWidth;
@@ -286,7 +318,7 @@ export default abstract class BaseWidget {
286
318
  }
287
319
 
288
320
  /** Set whether the widget is contained within another. @internal */
289
- protected setIsToplevel(toplevel: boolean) {
321
+ public setIsToplevel(toplevel: boolean) {
290
322
  this.toplevel = toplevel;
291
323
  }
292
324
 
@@ -0,0 +1,185 @@
1
+ import Color4 from '../../Color4';
2
+ import ImageBackground from '../../components/ImageBackground';
3
+ import Editor from '../../Editor';
4
+ import { EditorImageEventType } from '../../EditorImage';
5
+ import Rect2 from '../../math/Rect2';
6
+ import { EditorEventType } from '../../types';
7
+ import { ToolbarLocalization } from '../localization';
8
+ import makeColorInput from '../makeColorInput';
9
+ import BaseWidget from './BaseWidget';
10
+
11
+ export default class DocumentPropertiesWidget extends BaseWidget {
12
+ private updateDropdownContent: ()=>void = () => {};
13
+
14
+ public constructor(editor: Editor, localizationTable?: ToolbarLocalization) {
15
+ super(editor, 'zoom-widget', localizationTable);
16
+
17
+ // Make it possible to open the dropdown, even if this widget isn't selected.
18
+ this.container.classList.add('dropdownShowable');
19
+
20
+ this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, () => {
21
+ this.queueDropdownUpdate();
22
+ });
23
+
24
+
25
+ this.editor.image.notifier.on(EditorImageEventType.ExportViewportChanged, () => {
26
+ this.queueDropdownUpdate();
27
+ });
28
+ }
29
+
30
+ protected getTitle(): string {
31
+ return this.localizationTable.documentProperties;
32
+ }
33
+
34
+ protected createIcon(): Element {
35
+ return this.editor.icons.makeConfigureDocumentIcon();
36
+ }
37
+
38
+ protected handleClick() {
39
+ this.setDropdownVisible(!this.isDropdownVisible());
40
+ this.queueDropdownUpdate();
41
+ }
42
+
43
+ private dropdownUpdateQueued: boolean = false;
44
+ private queueDropdownUpdate() {
45
+ if (!this.dropdownUpdateQueued) {
46
+ requestAnimationFrame(() => this.updateDropdown());
47
+ this.dropdownUpdateQueued = true;
48
+ }
49
+ }
50
+
51
+ private updateDropdown() {
52
+ this.dropdownUpdateQueued = false;
53
+
54
+ if (this.isDropdownVisible()) {
55
+ this.updateDropdownContent();
56
+ }
57
+ }
58
+
59
+ private getBackgroundElem() {
60
+ const backgroundComponents = [];
61
+
62
+ for (const component of this.editor.image.getBackgroundComponents()) {
63
+ if (component instanceof ImageBackground) {
64
+ backgroundComponents.push(component);
65
+ }
66
+ }
67
+
68
+ if (backgroundComponents.length === 0) {
69
+ return null;
70
+ }
71
+
72
+ // Return the last background component in the list — the component with highest z-index.
73
+ return backgroundComponents[backgroundComponents.length - 1];
74
+ }
75
+
76
+ private setBackgroundColor(color: Color4) {
77
+ this.editor.dispatch(this.editor.setBackgroundColor(color));
78
+ }
79
+
80
+ private getBackgroundColor() {
81
+ const background = this.getBackgroundElem();
82
+
83
+ return background?.getStyle()?.color ?? Color4.transparent;
84
+ }
85
+
86
+ private updateImportExportRectSize(size: { width?: number, height?: number }) {
87
+ const filterDimension = (dim: number|undefined) => {
88
+ if (dim !== undefined && (!isFinite(dim) || dim <= 0)) {
89
+ dim = 100;
90
+ }
91
+
92
+ return dim;
93
+ };
94
+
95
+ const width = filterDimension(size.width);
96
+ const height = filterDimension(size.height);
97
+
98
+ const currentRect = this.editor.getImportExportRect();
99
+ const newRect = new Rect2(
100
+ currentRect.x, currentRect.y,
101
+ width ?? currentRect.w, height ?? currentRect.h
102
+ );
103
+
104
+ this.editor.dispatch(this.editor.image.setImportExportRect(newRect));
105
+ this.editor.queueRerender();
106
+ }
107
+
108
+ private static idCounter = 0;
109
+
110
+ protected fillDropdown(dropdown: HTMLElement): boolean {
111
+ const container = document.createElement('div');
112
+
113
+ const backgroundColorRow = document.createElement('div');
114
+ const backgroundColorLabel = document.createElement('label');
115
+
116
+ backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
117
+
118
+ const [ colorInput, backgroundColorInputContainer, setBgColorInputValue ] = makeColorInput(this.editor, color => {
119
+ if (!color.eq(this.getBackgroundColor())) {
120
+ this.setBackgroundColor(color);
121
+ }
122
+ });
123
+
124
+ colorInput.id = `document-properties-color-input-${DocumentPropertiesWidget.idCounter++}`;
125
+ backgroundColorLabel.htmlFor = colorInput.id;
126
+
127
+ backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
128
+
129
+ const addDimensionRow = (labelContent: string, onChange: (value: number)=>void) => {
130
+ const row = document.createElement('div');
131
+ const label = document.createElement('label');
132
+ const spacer = document.createElement('span');
133
+ const input = document.createElement('input');
134
+
135
+ label.innerText = labelContent;
136
+ input.type = 'number';
137
+ input.min = '0';
138
+ input.id = `document-properties-dimension-row-${DocumentPropertiesWidget.idCounter++}`;
139
+ label.htmlFor = input.id;
140
+
141
+ spacer.style.flexGrow = '1';
142
+ input.style.flexGrow = '2';
143
+ input.style.width = '25px';
144
+
145
+ row.style.display = 'flex';
146
+
147
+ input.oninput = () => {
148
+ onChange(parseFloat(input.value));
149
+ };
150
+
151
+ row.replaceChildren(label, spacer, input);
152
+
153
+ return {
154
+ setValue: (value: number) => {
155
+ input.value = value.toString();
156
+ },
157
+ element: row,
158
+ };
159
+ };
160
+
161
+ const imageWidthRow = addDimensionRow(this.localizationTable.imageWidthOption, (value: number) => {
162
+ this.updateImportExportRectSize({ width: value });
163
+ });
164
+ const imageHeightRow = addDimensionRow(this.localizationTable.imageHeightOption, (value: number) => {
165
+ this.updateImportExportRectSize({ height: value });
166
+ });
167
+
168
+ this.updateDropdownContent = () => {
169
+ setBgColorInputValue(this.getBackgroundColor());
170
+
171
+ const importExportRect = this.editor.getImportExportRect();
172
+ imageWidthRow.setValue(importExportRect.width);
173
+ imageHeightRow.setValue(importExportRect.height);
174
+ };
175
+ this.updateDropdownContent();
176
+
177
+
178
+ container.replaceChildren(
179
+ backgroundColorRow, imageWidthRow.element, imageHeightRow.element
180
+ );
181
+ dropdown.replaceChildren(container);
182
+
183
+ return true;
184
+ }
185
+ }
@@ -0,0 +1,9 @@
1
+
2
+ .toolbar-overflow-widget-overflow-list {
3
+ display: flex;
4
+ flex-direction: column;
5
+ flex-wrap: wrap;
6
+
7
+ overflow-x: auto;
8
+ max-height: 100%;
9
+ }