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
@@ -14,14 +14,21 @@ import TextToolWidget from './widgets/TextToolWidget';
14
14
  import HandToolWidget from './widgets/HandToolWidget';
15
15
  import ActionButtonWidget from './widgets/ActionButtonWidget';
16
16
  import InsertImageWidget from './widgets/InsertImageWidget';
17
+ import DocumentPropertiesWidget from './widgets/DocumentPropertiesWidget';
18
+ import OverflowWidget from './widgets/OverflowWidget';
17
19
  export const toolbarCSSPrefix = 'toolbar-';
18
20
  export default class HTMLToolbar {
19
21
  /** @internal */
20
22
  constructor(editor, parent, localizationTable = defaultToolbarLocalization) {
21
23
  this.editor = editor;
22
24
  this.localizationTable = localizationTable;
23
- this.widgets = {};
25
+ this.listeners = [];
26
+ this.widgetsById = {};
27
+ this.widgetList = [];
28
+ // Widget to toggle overflow menu.
29
+ this.overflowWidget = null;
24
30
  this.updateColoris = null;
31
+ this.reLayoutQueued = false;
25
32
  this.container = document.createElement('div');
26
33
  this.container.classList.add(`${toolbarCSSPrefix}root`);
27
34
  this.container.setAttribute('role', 'toolbar');
@@ -31,6 +38,15 @@ export default class HTMLToolbar {
31
38
  HTMLToolbar.colorisStarted = true;
32
39
  }
33
40
  this.setupColorPickers();
41
+ if ('ResizeObserver' in window) {
42
+ this.resizeObserver = new ResizeObserver((_entries) => {
43
+ this.reLayout();
44
+ });
45
+ this.resizeObserver.observe(this.container);
46
+ }
47
+ else {
48
+ console.warn('ResizeObserver not supported. Toolbar will not resize.');
49
+ }
34
50
  }
35
51
  // @internal
36
52
  setupColorPickers() {
@@ -80,20 +96,84 @@ export default class HTMLToolbar {
80
96
  initColoris();
81
97
  }
82
98
  };
83
- this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
99
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
84
100
  if (event.kind !== EditorEventType.ColorPickerToggled) {
85
101
  return;
86
102
  }
87
103
  // Show/hide the overlay. Making the overlay visible gives users a surface to click
88
104
  // on that shows/hides the color picker.
89
105
  closePickerOverlay.style.display = event.open ? 'block' : 'none';
90
- });
106
+ }));
91
107
  // Add newly-selected colors to the swatch.
92
- this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
108
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
93
109
  if (event.kind === EditorEventType.ColorPickerColorSelected) {
94
110
  addColorToSwatch(event.color.toHexString());
95
111
  }
96
- });
112
+ }));
113
+ }
114
+ queueReLayout() {
115
+ if (!this.reLayoutQueued) {
116
+ this.reLayoutQueued = true;
117
+ requestAnimationFrame(() => this.reLayout());
118
+ }
119
+ }
120
+ reLayout() {
121
+ this.reLayoutQueued = false;
122
+ if (!this.overflowWidget) {
123
+ return;
124
+ }
125
+ const getTotalWidth = (widgetList) => {
126
+ let totalWidth = 0;
127
+ for (const widget of widgetList) {
128
+ if (!widget.isHidden()) {
129
+ totalWidth += widget.getButtonWidth();
130
+ }
131
+ }
132
+ return totalWidth;
133
+ };
134
+ let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
135
+ let shownWidgetWidth = getTotalWidth(this.widgetList) - overflowWidgetsWidth;
136
+ let availableWidth = this.container.clientWidth * 0.87;
137
+ // If on a device that has enough vertical space, allow
138
+ // showing two rows of buttons.
139
+ // TODO: Fix magic numbers
140
+ if (window.innerHeight > availableWidth * 1.75) {
141
+ availableWidth *= 1.75;
142
+ }
143
+ let updatedChildren = false;
144
+ if (shownWidgetWidth + overflowWidgetsWidth <= availableWidth) {
145
+ // Move widgets to the main menu.
146
+ const overflowChildren = this.overflowWidget.clearChildren();
147
+ for (const child of overflowChildren) {
148
+ child.addTo(this.container);
149
+ child.setIsToplevel(true);
150
+ if (!child.isHidden()) {
151
+ shownWidgetWidth += child.getButtonWidth();
152
+ }
153
+ }
154
+ this.overflowWidget.setHidden(true);
155
+ overflowWidgetsWidth = 0;
156
+ updatedChildren = true;
157
+ }
158
+ if (shownWidgetWidth >= availableWidth) {
159
+ // Move widgets to the overflow menu.
160
+ this.overflowWidget.setHidden(false);
161
+ // Start with the rightmost widget, move to the leftmost
162
+ for (let i = this.widgetList.length - 1; i >= 0 && shownWidgetWidth >= availableWidth; i--) {
163
+ const child = this.widgetList[i];
164
+ if (this.overflowWidget.hasAsChild(child)) {
165
+ continue;
166
+ }
167
+ if (child.canBeInOverflowMenu()) {
168
+ shownWidgetWidth -= child.getButtonWidth();
169
+ this.overflowWidget.addToOverflow(child);
170
+ }
171
+ }
172
+ updatedChildren = true;
173
+ }
174
+ if (updatedChildren) {
175
+ this.setupColorPickers();
176
+ }
97
177
  }
98
178
  /**
99
179
  * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
@@ -108,12 +188,17 @@ export default class HTMLToolbar {
108
188
  */
109
189
  addWidget(widget) {
110
190
  // Prevent name collisions
111
- const id = widget.getUniqueIdIn(this.widgets);
191
+ const id = widget.getUniqueIdIn(this.widgetsById);
112
192
  // Add the widget
113
- this.widgets[id] = widget;
193
+ this.widgetsById[id] = widget;
194
+ this.widgetList.push(widget);
114
195
  // Add HTML elements.
115
- widget.addTo(this.container);
196
+ const container = widget.addTo(this.container);
116
197
  this.setupColorPickers();
198
+ // Ensure that the widget gets displayed in the correct
199
+ // place in the toolbar, even if it's removed and re-added.
200
+ container.style.order = `${this.widgetList.length}`;
201
+ this.queueReLayout();
117
202
  }
118
203
  /**
119
204
  * Adds a spacer.
@@ -152,8 +237,8 @@ export default class HTMLToolbar {
152
237
  }
153
238
  serializeState() {
154
239
  const result = {};
155
- for (const widgetId in this.widgets) {
156
- result[widgetId] = this.widgets[widgetId].serializeState();
240
+ for (const widgetId in this.widgetsById) {
241
+ result[widgetId] = this.widgetsById[widgetId].serializeState();
157
242
  }
158
243
  return JSON.stringify(result);
159
244
  }
@@ -164,10 +249,10 @@ export default class HTMLToolbar {
164
249
  deserializeState(state) {
165
250
  const data = JSON.parse(state);
166
251
  for (const widgetId in data) {
167
- if (!(widgetId in this.widgets)) {
252
+ if (!(widgetId in this.widgetsById)) {
168
253
  console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
169
254
  }
170
- this.widgets[widgetId].deserializeFrom(data[widgetId]);
255
+ this.widgetsById[widgetId].deserializeFrom(data[widgetId]);
171
256
  }
172
257
  }
173
258
  /**
@@ -175,7 +260,7 @@ export default class HTMLToolbar {
175
260
  *
176
261
  * @return The added button.
177
262
  */
178
- addActionButton(title, command) {
263
+ addActionButton(title, command, mustBeToplevel = true) {
179
264
  const titleString = typeof title === 'string' ? title : title.label;
180
265
  const widgetId = 'action-button';
181
266
  const makeIcon = () => {
@@ -184,7 +269,7 @@ export default class HTMLToolbar {
184
269
  }
185
270
  return title.icon;
186
271
  };
187
- const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization);
272
+ const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization, mustBeToplevel);
188
273
  this.addWidget(widget);
189
274
  return widget;
190
275
  }
@@ -226,25 +311,52 @@ export default class HTMLToolbar {
226
311
  for (const tool of toolController.getMatchingTools(TextTool)) {
227
312
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
228
313
  }
229
- this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
230
314
  const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
231
315
  if (panZoomTool) {
232
316
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
233
317
  }
318
+ this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
234
319
  }
235
320
  addDefaultActionButtons() {
321
+ this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
236
322
  this.addUndoRedoButtons();
237
323
  }
324
+ /**
325
+ * Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
326
+ * that this widget is in the correct space (if shown).
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * toolbar.addDefaultToolWidgets();
331
+ * toolbar.addOverflowWidget();
332
+ * toolbar.addDefaultActionButtons();
333
+ * ```
334
+ * shows the overflow widget between the default tool widgets and the default action buttons,
335
+ * if shown.
336
+ */
337
+ addOverflowWidget() {
338
+ this.overflowWidget = new OverflowWidget(this.editor, this.localizationTable);
339
+ this.addWidget(this.overflowWidget);
340
+ }
238
341
  /**
239
342
  * Adds both the default tool widgets and action buttons. Equivalent to
240
343
  * ```ts
241
344
  * toolbar.addDefaultToolWidgets();
345
+ * toolbar.addOverflowWidget();
242
346
  * toolbar.addDefaultActionButtons();
243
347
  * ```
244
348
  */
245
349
  addDefaults() {
246
350
  this.addDefaultToolWidgets();
351
+ this.addOverflowWidget();
247
352
  this.addDefaultActionButtons();
248
353
  }
354
+ remove() {
355
+ this.container.remove();
356
+ this.resizeObserver.disconnect();
357
+ for (const listener of this.listeners) {
358
+ listener.remove();
359
+ }
360
+ }
249
361
  }
250
362
  HTMLToolbar.colorisStarted = false;
@@ -57,4 +57,6 @@ export default class IconProvider {
57
57
  makePasteIcon(): IconType;
58
58
  makeDeleteSelectionIcon(): IconType;
59
59
  makeSaveIcon(): IconType;
60
+ makeConfigureDocumentIcon(): IconType;
61
+ makeOverflowIcon(): IconType;
60
62
  }
@@ -607,4 +607,48 @@ export default class IconProvider {
607
607
  svg.setAttribute('viewBox', '0 0 100 100');
608
608
  return svg;
609
609
  }
610
+ makeConfigureDocumentIcon() {
611
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
612
+ svg.innerHTML = `
613
+ <path
614
+ d='
615
+ M 5,5 V 95 H 95 V 5 Z m 5,5 H 90 V 90 H 10 Z
616
+ m 5,10 V 30 H 50 V 25 H 20 v -5 z
617
+ m 40,0 V 50 H 85 V 20 Z
618
+ m 2,2 H 83 V 39 L 77,28 70,42 64,35 57,45 Z
619
+ 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
620
+ M 15,40 v 5 h 35 v -5 z
621
+ m 0,15 v 5 h 70 v -5 z
622
+ m 0,15 v 5 h 70 v -5 z
623
+ '
624
+ style='fill: var(--icon-color);'
625
+ />
626
+ `;
627
+ svg.setAttribute('viewBox', '0 0 100 100');
628
+ return svg;
629
+ }
630
+ makeOverflowIcon() {
631
+ return this.makeIconFromPath(`
632
+ M 15 40
633
+ A 12.5 12.5 0 0 0 2.5 52.5
634
+ A 12.5 12.5 0 0 0 15 65
635
+ A 12.5 12.5 0 0 0 27.5 52.5
636
+ A 12.5 12.5 0 0 0 15 40
637
+ z
638
+
639
+ M 50 40
640
+ A 12.5 12.5 0 0 0 37.5 52.5
641
+ A 12.5 12.5 0 0 0 50 65
642
+ A 12.5 12.5 0 0 0 62.5 52.5
643
+ A 12.5 12.5 0 0 0 50 40
644
+ z
645
+
646
+ M 85 40
647
+ A 12.5 12.5 0 0 0 72.5 52.5
648
+ A 12.5 12.5 0 0 0 85 65
649
+ A 12.5 12.5 0 0 0 97.5 52.5
650
+ A 12.5 12.5 0 0 0 85 40
651
+ z
652
+ `);
653
+ }
610
654
  }
@@ -33,6 +33,11 @@ export interface ToolbarLocalization {
33
33
  resetView: string;
34
34
  selectionToolKeyboardShortcuts: string;
35
35
  paste: string;
36
+ documentProperties: string;
37
+ backgroundColor: string;
38
+ imageWidthOption: string;
39
+ imageHeightOption: string;
40
+ toggleOverflow: string;
36
41
  errorImageHasZeroSize: string;
37
42
  dropdownShown: (toolName: string) => string;
38
43
  dropdownHidden: (toolName: string) => string;
@@ -24,6 +24,11 @@ export const defaultToolbarLocalization = {
24
24
  pickColorFromScreen: 'Pick color from screen',
25
25
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
26
26
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
27
+ documentProperties: 'Document',
28
+ backgroundColor: 'Background Color: ',
29
+ imageWidthOption: 'Width: ',
30
+ imageHeightOption: 'Height: ',
31
+ toggleOverflow: 'More',
27
32
  touchPanning: 'Touchscreen panning',
28
33
  freehandPen: 'Freehand',
29
34
  pressureSensitiveFreehandPen: 'Freehand (pressure sensitive)',
@@ -5,9 +5,11 @@ export default class ActionButtonWidget extends BaseWidget {
5
5
  protected makeIcon: () => Element | null;
6
6
  protected title: string;
7
7
  protected clickAction: () => void;
8
- constructor(editor: Editor, id: string, makeIcon: () => Element | null, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization);
8
+ protected mustBeToplevel: boolean;
9
+ constructor(editor: Editor, id: string, makeIcon: () => Element | null, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization, mustBeToplevel?: boolean);
9
10
  protected handleClick(): void;
10
11
  protected getTitle(): string;
11
12
  protected createIcon(): Element | null;
12
13
  protected fillDropdown(_dropdown: HTMLElement): boolean;
14
+ canBeInOverflowMenu(): boolean;
13
15
  }
@@ -1,10 +1,11 @@
1
1
  import BaseWidget from './BaseWidget';
2
2
  export default class ActionButtonWidget extends BaseWidget {
3
- constructor(editor, id, makeIcon, title, clickAction, localizationTable) {
3
+ constructor(editor, id, makeIcon, title, clickAction, localizationTable, mustBeToplevel = false) {
4
4
  super(editor, id, localizationTable);
5
5
  this.makeIcon = makeIcon;
6
6
  this.title = title;
7
7
  this.clickAction = clickAction;
8
+ this.mustBeToplevel = mustBeToplevel;
8
9
  }
9
10
  handleClick() {
10
11
  this.clickAction();
@@ -18,4 +19,7 @@ export default class ActionButtonWidget extends BaseWidget {
18
19
  fillDropdown(_dropdown) {
19
20
  return false;
20
21
  }
22
+ canBeInOverflowMenu() {
23
+ return !this.mustBeToplevel;
24
+ }
21
25
  }
@@ -7,5 +7,5 @@ export default abstract class BaseToolWidget extends BaseWidget {
7
7
  protected targetTool: BaseTool;
8
8
  constructor(editor: Editor, targetTool: BaseTool, id: string, localizationTable?: ToolbarLocalization);
9
9
  protected handleClick(): void;
10
- addTo(parent: HTMLElement): void;
10
+ addTo(parent: HTMLElement): HTMLElement;
11
11
  }
@@ -37,7 +37,8 @@ export default class BaseToolWidget extends BaseWidget {
37
37
  }
38
38
  }
39
39
  addTo(parent) {
40
- super.addTo(parent);
40
+ const result = super.addTo(parent);
41
41
  this.setSelected(this.targetTool.isEnabled());
42
+ return result;
42
43
  }
43
44
  }
@@ -36,14 +36,19 @@ export default abstract class BaseWidget {
36
36
  protected abstract handleClick(): void;
37
37
  protected get hasDropdown(): boolean;
38
38
  protected addSubWidget(widget: BaseWidget): void;
39
- addTo(parent: HTMLElement): void;
39
+ private toolbarWidgetToggleListener;
40
+ addTo(parent: HTMLElement): HTMLElement;
40
41
  protected updateIcon(): void;
41
42
  setDisabled(disabled: boolean): void;
42
43
  setSelected(selected: boolean): void;
43
44
  protected setDropdownVisible(visible: boolean): void;
45
+ canBeInOverflowMenu(): boolean;
46
+ getButtonWidth(): number;
47
+ isHidden(): boolean;
48
+ setHidden(hidden: boolean): void;
44
49
  protected repositionDropdown(): void;
45
50
  /** Set whether the widget is contained within another. @internal */
46
- protected setIsToplevel(toplevel: boolean): void;
51
+ setIsToplevel(toplevel: boolean): void;
47
52
  protected isDropdownVisible(): boolean;
48
53
  protected isSelected(): boolean;
49
54
  private createDropdownIcon;
@@ -22,6 +22,7 @@ export default class BaseWidget {
22
22
  // Maps subWidget IDs to subWidgets.
23
23
  this.subWidgets = {};
24
24
  this.toplevel = true;
25
+ this.toolbarWidgetToggleListener = null;
25
26
  this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
26
27
  this.icon = null;
27
28
  this.container = document.createElement('div');
@@ -135,12 +136,14 @@ export default class BaseWidget {
135
136
  this.subWidgets[id] = widget;
136
137
  }
137
138
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
139
+ // Returns the element that was just added to `parent`.
138
140
  // @internal
139
141
  addTo(parent) {
140
142
  this.label.innerText = this.getTitle();
141
143
  this.setupActionBtnClickListener(this.button);
142
144
  this.icon = null;
143
145
  this.updateIcon();
146
+ this.container.replaceChildren();
144
147
  this.button.replaceChildren(this.icon, this.label);
145
148
  this.container.appendChild(this.button);
146
149
  __classPrivateFieldSet(this, _BaseWidget_hasDropdown, this.fillDropdown(this.dropdownContainer), "f");
@@ -148,7 +151,10 @@ export default class BaseWidget {
148
151
  this.dropdownIcon = this.createDropdownIcon();
149
152
  this.button.appendChild(this.dropdownIcon);
150
153
  this.container.appendChild(this.dropdownContainer);
151
- this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
154
+ if (this.toolbarWidgetToggleListener) {
155
+ this.toolbarWidgetToggleListener.remove();
156
+ }
157
+ this.toolbarWidgetToggleListener = this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
152
158
  if (evt.kind === EditorEventType.ToolbarDropdownShown
153
159
  && evt.parentWidget !== this
154
160
  // Don't hide if a submenu wash shown (it might be a submenu of
@@ -159,7 +165,11 @@ export default class BaseWidget {
159
165
  });
160
166
  }
161
167
  this.setDropdownVisible(false);
168
+ if (this.container.parentElement) {
169
+ this.container.remove();
170
+ }
162
171
  parent.appendChild(this.container);
172
+ return this.container;
163
173
  }
164
174
  updateIcon() {
165
175
  var _a, _b;
@@ -219,6 +229,18 @@ export default class BaseWidget {
219
229
  }
220
230
  this.repositionDropdown();
221
231
  }
232
+ canBeInOverflowMenu() {
233
+ return true;
234
+ }
235
+ getButtonWidth() {
236
+ return this.button.clientWidth;
237
+ }
238
+ isHidden() {
239
+ return this.container.style.display === 'none';
240
+ }
241
+ setHidden(hidden) {
242
+ this.container.style.display = hidden ? 'none' : '';
243
+ }
222
244
  repositionDropdown() {
223
245
  const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
224
246
  const screenWidth = document.body.clientWidth;
@@ -0,0 +1,19 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import BaseWidget from './BaseWidget';
4
+ export default class DocumentPropertiesWidget extends BaseWidget {
5
+ private updateDropdownContent;
6
+ constructor(editor: Editor, localizationTable?: ToolbarLocalization);
7
+ protected getTitle(): string;
8
+ protected createIcon(): Element;
9
+ protected handleClick(): void;
10
+ private dropdownUpdateQueued;
11
+ private queueDropdownUpdate;
12
+ private updateDropdown;
13
+ private getBackgroundElem;
14
+ private setBackgroundColor;
15
+ private getBackgroundColor;
16
+ private updateImportExportRectSize;
17
+ private static idCounter;
18
+ protected fillDropdown(dropdown: HTMLElement): boolean;
19
+ }
@@ -0,0 +1,135 @@
1
+ import Color4 from '../../Color4';
2
+ import ImageBackground from '../../components/ImageBackground';
3
+ import { EditorImageEventType } from '../../EditorImage';
4
+ import Rect2 from '../../math/Rect2';
5
+ import { EditorEventType } from '../../types';
6
+ import makeColorInput from '../makeColorInput';
7
+ import BaseWidget from './BaseWidget';
8
+ export default class DocumentPropertiesWidget extends BaseWidget {
9
+ constructor(editor, localizationTable) {
10
+ super(editor, 'zoom-widget', localizationTable);
11
+ this.updateDropdownContent = () => { };
12
+ this.dropdownUpdateQueued = false;
13
+ // Make it possible to open the dropdown, even if this widget isn't selected.
14
+ this.container.classList.add('dropdownShowable');
15
+ this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, () => {
16
+ this.queueDropdownUpdate();
17
+ });
18
+ this.editor.image.notifier.on(EditorImageEventType.ExportViewportChanged, () => {
19
+ this.queueDropdownUpdate();
20
+ });
21
+ }
22
+ getTitle() {
23
+ return this.localizationTable.documentProperties;
24
+ }
25
+ createIcon() {
26
+ return this.editor.icons.makeConfigureDocumentIcon();
27
+ }
28
+ handleClick() {
29
+ this.setDropdownVisible(!this.isDropdownVisible());
30
+ this.queueDropdownUpdate();
31
+ }
32
+ queueDropdownUpdate() {
33
+ if (!this.dropdownUpdateQueued) {
34
+ requestAnimationFrame(() => this.updateDropdown());
35
+ this.dropdownUpdateQueued = true;
36
+ }
37
+ }
38
+ updateDropdown() {
39
+ this.dropdownUpdateQueued = false;
40
+ if (this.isDropdownVisible()) {
41
+ this.updateDropdownContent();
42
+ }
43
+ }
44
+ getBackgroundElem() {
45
+ const backgroundComponents = [];
46
+ for (const component of this.editor.image.getBackgroundComponents()) {
47
+ if (component instanceof ImageBackground) {
48
+ backgroundComponents.push(component);
49
+ }
50
+ }
51
+ if (backgroundComponents.length === 0) {
52
+ return null;
53
+ }
54
+ // Return the last background component in the list — the component with highest z-index.
55
+ return backgroundComponents[backgroundComponents.length - 1];
56
+ }
57
+ setBackgroundColor(color) {
58
+ this.editor.dispatch(this.editor.setBackgroundColor(color));
59
+ }
60
+ getBackgroundColor() {
61
+ var _a, _b;
62
+ const background = this.getBackgroundElem();
63
+ return (_b = (_a = background === null || background === void 0 ? void 0 : background.getStyle()) === null || _a === void 0 ? void 0 : _a.color) !== null && _b !== void 0 ? _b : Color4.transparent;
64
+ }
65
+ updateImportExportRectSize(size) {
66
+ const filterDimension = (dim) => {
67
+ if (dim !== undefined && (!isFinite(dim) || dim <= 0)) {
68
+ dim = 100;
69
+ }
70
+ return dim;
71
+ };
72
+ const width = filterDimension(size.width);
73
+ const height = filterDimension(size.height);
74
+ const currentRect = this.editor.getImportExportRect();
75
+ const newRect = new Rect2(currentRect.x, currentRect.y, width !== null && width !== void 0 ? width : currentRect.w, height !== null && height !== void 0 ? height : currentRect.h);
76
+ this.editor.dispatch(this.editor.image.setImportExportRect(newRect));
77
+ this.editor.queueRerender();
78
+ }
79
+ fillDropdown(dropdown) {
80
+ const container = document.createElement('div');
81
+ const backgroundColorRow = document.createElement('div');
82
+ const backgroundColorLabel = document.createElement('label');
83
+ backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
84
+ const [colorInput, backgroundColorInputContainer, setBgColorInputValue] = makeColorInput(this.editor, color => {
85
+ if (!color.eq(this.getBackgroundColor())) {
86
+ this.setBackgroundColor(color);
87
+ }
88
+ });
89
+ colorInput.id = `document-properties-color-input-${DocumentPropertiesWidget.idCounter++}`;
90
+ backgroundColorLabel.htmlFor = colorInput.id;
91
+ backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
92
+ const addDimensionRow = (labelContent, onChange) => {
93
+ const row = document.createElement('div');
94
+ const label = document.createElement('label');
95
+ const spacer = document.createElement('span');
96
+ const input = document.createElement('input');
97
+ label.innerText = labelContent;
98
+ input.type = 'number';
99
+ input.min = '0';
100
+ input.id = `document-properties-dimension-row-${DocumentPropertiesWidget.idCounter++}`;
101
+ label.htmlFor = input.id;
102
+ spacer.style.flexGrow = '1';
103
+ input.style.flexGrow = '2';
104
+ input.style.width = '25px';
105
+ row.style.display = 'flex';
106
+ input.oninput = () => {
107
+ onChange(parseFloat(input.value));
108
+ };
109
+ row.replaceChildren(label, spacer, input);
110
+ return {
111
+ setValue: (value) => {
112
+ input.value = value.toString();
113
+ },
114
+ element: row,
115
+ };
116
+ };
117
+ const imageWidthRow = addDimensionRow(this.localizationTable.imageWidthOption, (value) => {
118
+ this.updateImportExportRectSize({ width: value });
119
+ });
120
+ const imageHeightRow = addDimensionRow(this.localizationTable.imageHeightOption, (value) => {
121
+ this.updateImportExportRectSize({ height: value });
122
+ });
123
+ this.updateDropdownContent = () => {
124
+ setBgColorInputValue(this.getBackgroundColor());
125
+ const importExportRect = this.editor.getImportExportRect();
126
+ imageWidthRow.setValue(importExportRect.width);
127
+ imageHeightRow.setValue(importExportRect.height);
128
+ };
129
+ this.updateDropdownContent();
130
+ container.replaceChildren(backgroundColorRow, imageWidthRow.element, imageHeightRow.element);
131
+ dropdown.replaceChildren(container);
132
+ return true;
133
+ }
134
+ }
135
+ DocumentPropertiesWidget.idCounter = 0;
@@ -0,0 +1,25 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import BaseWidget from './BaseWidget';
4
+ export default class OverflowWidget extends BaseWidget {
5
+ private overflowChildren;
6
+ private overflowContainer;
7
+ constructor(editor: Editor, localizationTable?: ToolbarLocalization);
8
+ protected getTitle(): string;
9
+ protected createIcon(): Element | null;
10
+ protected handleClick(): void;
11
+ protected fillDropdown(dropdown: HTMLElement): boolean;
12
+ /**
13
+ * Removes all `BaseWidget`s from this and returns them.
14
+ */
15
+ clearChildren(): BaseWidget[];
16
+ getChildWidgets(): BaseWidget[];
17
+ hasAsChild(widget: BaseWidget): boolean;
18
+ /**
19
+ * Adds `widget` to this.
20
+ * `widget`'s previous parent is still responsible
21
+ * for serializing/deserializing its state.
22
+ */
23
+ addToOverflow(widget: BaseWidget): void;
24
+ canBeInOverflowMenu(): boolean;
25
+ }