js-draw 0.15.2 → 0.16.1

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 (65) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
  2. package/CHANGELOG.md +12 -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/SVGLoader.js +6 -0
  9. package/dist/src/Viewport.d.ts +5 -0
  10. package/dist/src/Viewport.js +11 -0
  11. package/dist/src/components/AbstractComponent.d.ts +1 -0
  12. package/dist/src/components/AbstractComponent.js +5 -0
  13. package/dist/src/components/ImageBackground.d.ts +2 -1
  14. package/dist/src/components/ImageBackground.js +8 -1
  15. package/dist/src/localizations/es.js +1 -1
  16. package/dist/src/toolbar/HTMLToolbar.d.ts +26 -2
  17. package/dist/src/toolbar/HTMLToolbar.js +130 -15
  18. package/dist/src/toolbar/IconProvider.d.ts +2 -0
  19. package/dist/src/toolbar/IconProvider.js +44 -0
  20. package/dist/src/toolbar/localization.d.ts +5 -0
  21. package/dist/src/toolbar/localization.js +5 -0
  22. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
  23. package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
  24. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
  25. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
  26. package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
  27. package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
  28. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
  29. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
  30. package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
  31. package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
  32. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  33. package/dist/src/toolbar/widgets/lib.js +1 -0
  34. package/dist/src/tools/PasteHandler.js +2 -2
  35. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
  36. package/dist/src/tools/SelectionTool/Selection.js +2 -1
  37. package/dist/src/tools/SelectionTool/SelectionTool.js +1 -1
  38. package/package.json +1 -1
  39. package/src/Editor.css +4 -0
  40. package/src/Editor.loadFrom.test.ts +24 -0
  41. package/src/Editor.ts +59 -4
  42. package/src/EditorImage.ts +66 -23
  43. package/src/SVGLoader.test.ts +57 -0
  44. package/src/SVGLoader.ts +7 -0
  45. package/src/Viewport.ts +13 -0
  46. package/src/components/AbstractComponent.ts +6 -0
  47. package/src/components/ImageBackground.test.ts +35 -0
  48. package/src/components/ImageBackground.ts +10 -1
  49. package/src/localizations/es.ts +8 -0
  50. package/src/math/Mat33.test.ts +30 -5
  51. package/src/rendering/renderers/CanvasRenderer.ts +1 -1
  52. package/src/toolbar/HTMLToolbar.ts +169 -16
  53. package/src/toolbar/IconProvider.ts +46 -0
  54. package/src/toolbar/localization.ts +10 -0
  55. package/src/toolbar/toolbar.css +2 -0
  56. package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
  57. package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
  58. package/src/toolbar/widgets/BaseWidget.ts +34 -2
  59. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
  60. package/src/toolbar/widgets/OverflowWidget.css +9 -0
  61. package/src/toolbar/widgets/OverflowWidget.ts +83 -0
  62. package/src/toolbar/widgets/lib.ts +2 -1
  63. package/src/tools/PasteHandler.ts +3 -2
  64. package/src/tools/SelectionTool/Selection.ts +2 -1
  65. package/src/tools/SelectionTool/SelectionTool.ts +1 -1
@@ -14,14 +14,23 @@ 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
+ // Flex-order of the next widget to be added.
27
+ this.widgetOrderCounter = 0;
28
+ this.widgetsById = {};
29
+ this.widgetList = [];
30
+ // Widget to toggle overflow menu.
31
+ this.overflowWidget = null;
24
32
  this.updateColoris = null;
33
+ this.reLayoutQueued = false;
25
34
  this.container = document.createElement('div');
26
35
  this.container.classList.add(`${toolbarCSSPrefix}root`);
27
36
  this.container.setAttribute('role', 'toolbar');
@@ -31,6 +40,15 @@ export default class HTMLToolbar {
31
40
  HTMLToolbar.colorisStarted = true;
32
41
  }
33
42
  this.setupColorPickers();
43
+ if ('ResizeObserver' in window) {
44
+ this.resizeObserver = new ResizeObserver((_entries) => {
45
+ this.reLayout();
46
+ });
47
+ this.resizeObserver.observe(this.container);
48
+ }
49
+ else {
50
+ console.warn('ResizeObserver not supported. Toolbar will not resize.');
51
+ }
34
52
  }
35
53
  // @internal
36
54
  setupColorPickers() {
@@ -80,20 +98,84 @@ export default class HTMLToolbar {
80
98
  initColoris();
81
99
  }
82
100
  };
83
- this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
101
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
84
102
  if (event.kind !== EditorEventType.ColorPickerToggled) {
85
103
  return;
86
104
  }
87
105
  // Show/hide the overlay. Making the overlay visible gives users a surface to click
88
106
  // on that shows/hides the color picker.
89
107
  closePickerOverlay.style.display = event.open ? 'block' : 'none';
90
- });
108
+ }));
91
109
  // Add newly-selected colors to the swatch.
92
- this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
110
+ this.listeners.push(this.editor.notifier.on(EditorEventType.ColorPickerColorSelected, event => {
93
111
  if (event.kind === EditorEventType.ColorPickerColorSelected) {
94
112
  addColorToSwatch(event.color.toHexString());
95
113
  }
96
- });
114
+ }));
115
+ }
116
+ queueReLayout() {
117
+ if (!this.reLayoutQueued) {
118
+ this.reLayoutQueued = true;
119
+ requestAnimationFrame(() => this.reLayout());
120
+ }
121
+ }
122
+ reLayout() {
123
+ this.reLayoutQueued = false;
124
+ if (!this.overflowWidget) {
125
+ return;
126
+ }
127
+ const getTotalWidth = (widgetList) => {
128
+ let totalWidth = 0;
129
+ for (const widget of widgetList) {
130
+ if (!widget.isHidden()) {
131
+ totalWidth += widget.getButtonWidth();
132
+ }
133
+ }
134
+ return totalWidth;
135
+ };
136
+ let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
137
+ let shownWidgetWidth = getTotalWidth(this.widgetList) - overflowWidgetsWidth;
138
+ let availableWidth = this.container.clientWidth * 0.87;
139
+ // If on a device that has enough vertical space, allow
140
+ // showing two rows of buttons.
141
+ // TODO: Fix magic numbers
142
+ if (window.innerHeight > availableWidth * 1.75) {
143
+ availableWidth *= 1.75;
144
+ }
145
+ let updatedChildren = false;
146
+ if (shownWidgetWidth + overflowWidgetsWidth <= availableWidth) {
147
+ // Move widgets to the main menu.
148
+ const overflowChildren = this.overflowWidget.clearChildren();
149
+ for (const child of overflowChildren) {
150
+ child.addTo(this.container);
151
+ child.setIsToplevel(true);
152
+ if (!child.isHidden()) {
153
+ shownWidgetWidth += child.getButtonWidth();
154
+ }
155
+ }
156
+ this.overflowWidget.setHidden(true);
157
+ overflowWidgetsWidth = 0;
158
+ updatedChildren = true;
159
+ }
160
+ if (shownWidgetWidth >= availableWidth) {
161
+ // Move widgets to the overflow menu.
162
+ this.overflowWidget.setHidden(false);
163
+ // Start with the rightmost widget, move to the leftmost
164
+ for (let i = this.widgetList.length - 1; i >= 0 && shownWidgetWidth >= availableWidth; i--) {
165
+ const child = this.widgetList[i];
166
+ if (this.overflowWidget.hasAsChild(child)) {
167
+ continue;
168
+ }
169
+ if (child.canBeInOverflowMenu()) {
170
+ shownWidgetWidth -= child.getButtonWidth();
171
+ this.overflowWidget.addToOverflow(child);
172
+ }
173
+ }
174
+ updatedChildren = true;
175
+ }
176
+ if (updatedChildren) {
177
+ this.setupColorPickers();
178
+ }
97
179
  }
98
180
  /**
99
181
  * Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
@@ -108,12 +190,17 @@ export default class HTMLToolbar {
108
190
  */
109
191
  addWidget(widget) {
110
192
  // Prevent name collisions
111
- const id = widget.getUniqueIdIn(this.widgets);
193
+ const id = widget.getUniqueIdIn(this.widgetsById);
112
194
  // Add the widget
113
- this.widgets[id] = widget;
195
+ this.widgetsById[id] = widget;
196
+ this.widgetList.push(widget);
114
197
  // Add HTML elements.
115
- widget.addTo(this.container);
198
+ const container = widget.addTo(this.container);
116
199
  this.setupColorPickers();
200
+ // Ensure that the widget gets displayed in the correct
201
+ // place in the toolbar, even if it's removed and re-added.
202
+ container.style.order = `${this.widgetOrderCounter++}`;
203
+ this.queueReLayout();
117
204
  }
118
205
  /**
119
206
  * Adds a spacer.
@@ -148,12 +235,13 @@ export default class HTMLToolbar {
148
235
  if (options.maxSize) {
149
236
  spacer.style.maxWidth = options.maxSize;
150
237
  }
238
+ spacer.style.order = `${this.widgetOrderCounter++}`;
151
239
  this.container.appendChild(spacer);
152
240
  }
153
241
  serializeState() {
154
242
  const result = {};
155
- for (const widgetId in this.widgets) {
156
- result[widgetId] = this.widgets[widgetId].serializeState();
243
+ for (const widgetId in this.widgetsById) {
244
+ result[widgetId] = this.widgetsById[widgetId].serializeState();
157
245
  }
158
246
  return JSON.stringify(result);
159
247
  }
@@ -164,10 +252,10 @@ export default class HTMLToolbar {
164
252
  deserializeState(state) {
165
253
  const data = JSON.parse(state);
166
254
  for (const widgetId in data) {
167
- if (!(widgetId in this.widgets)) {
255
+ if (!(widgetId in this.widgetsById)) {
168
256
  console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
169
257
  }
170
- this.widgets[widgetId].deserializeFrom(data[widgetId]);
258
+ this.widgetsById[widgetId].deserializeFrom(data[widgetId]);
171
259
  }
172
260
  }
173
261
  /**
@@ -175,7 +263,7 @@ export default class HTMLToolbar {
175
263
  *
176
264
  * @return The added button.
177
265
  */
178
- addActionButton(title, command) {
266
+ addActionButton(title, command, mustBeToplevel = true) {
179
267
  const titleString = typeof title === 'string' ? title : title.label;
180
268
  const widgetId = 'action-button';
181
269
  const makeIcon = () => {
@@ -184,7 +272,7 @@ export default class HTMLToolbar {
184
272
  }
185
273
  return title.icon;
186
274
  };
187
- const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization);
275
+ const widget = new ActionButtonWidget(this.editor, widgetId, makeIcon, titleString, command, this.editor.localization, mustBeToplevel);
188
276
  this.addWidget(widget);
189
277
  return widget;
190
278
  }
@@ -226,25 +314,52 @@ export default class HTMLToolbar {
226
314
  for (const tool of toolController.getMatchingTools(TextTool)) {
227
315
  this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
228
316
  }
229
- this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
230
317
  const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
231
318
  if (panZoomTool) {
232
319
  this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
233
320
  }
321
+ this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
234
322
  }
235
323
  addDefaultActionButtons() {
324
+ this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
236
325
  this.addUndoRedoButtons();
237
326
  }
327
+ /**
328
+ * Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
329
+ * that this widget is in the correct space (if shown).
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * toolbar.addDefaultToolWidgets();
334
+ * toolbar.addOverflowWidget();
335
+ * toolbar.addDefaultActionButtons();
336
+ * ```
337
+ * shows the overflow widget between the default tool widgets and the default action buttons,
338
+ * if shown.
339
+ */
340
+ addOverflowWidget() {
341
+ this.overflowWidget = new OverflowWidget(this.editor, this.localizationTable);
342
+ this.addWidget(this.overflowWidget);
343
+ }
238
344
  /**
239
345
  * Adds both the default tool widgets and action buttons. Equivalent to
240
346
  * ```ts
241
347
  * toolbar.addDefaultToolWidgets();
348
+ * toolbar.addOverflowWidget();
242
349
  * toolbar.addDefaultActionButtons();
243
350
  * ```
244
351
  */
245
352
  addDefaults() {
246
353
  this.addDefaultToolWidgets();
354
+ this.addOverflowWidget();
247
355
  this.addDefaultActionButtons();
248
356
  }
357
+ remove() {
358
+ this.container.remove();
359
+ this.resizeObserver.disconnect();
360
+ for (const listener of this.listeners) {
361
+ listener.remove();
362
+ }
363
+ }
249
364
  }
250
365
  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
+ }