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.
- package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
- package/CHANGELOG.md +6 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +11 -0
- package/dist/src/Editor.js +52 -4
- package/dist/src/EditorImage.d.ts +11 -11
- package/dist/src/EditorImage.js +54 -18
- package/dist/src/Viewport.d.ts +5 -0
- package/dist/src/Viewport.js +11 -0
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +5 -0
- package/dist/src/components/ImageBackground.d.ts +2 -1
- package/dist/src/components/ImageBackground.js +8 -1
- package/dist/src/localizations/es.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
- package/dist/src/toolbar/HTMLToolbar.js +127 -15
- package/dist/src/toolbar/IconProvider.d.ts +2 -0
- package/dist/src/toolbar/IconProvider.js +44 -0
- package/dist/src/toolbar/localization.d.ts +5 -0
- package/dist/src/toolbar/localization.js +5 -0
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
- package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
- package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
- package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
- package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
- package/dist/src/toolbar/widgets/lib.d.ts +1 -0
- package/dist/src/toolbar/widgets/lib.js +1 -0
- package/dist/src/tools/PasteHandler.js +2 -2
- package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
- package/dist/src/tools/SelectionTool/Selection.js +2 -1
- package/package.json +1 -1
- package/src/Editor.loadFrom.test.ts +24 -0
- package/src/Editor.ts +59 -4
- package/src/EditorImage.ts +66 -23
- package/src/Viewport.ts +13 -0
- package/src/components/AbstractComponent.ts +6 -0
- package/src/components/ImageBackground.test.ts +35 -0
- package/src/components/ImageBackground.ts +10 -1
- package/src/localizations/es.ts +8 -0
- package/src/math/Mat33.test.ts +30 -5
- package/src/rendering/renderers/CanvasRenderer.ts +1 -1
- package/src/toolbar/HTMLToolbar.ts +164 -16
- package/src/toolbar/IconProvider.ts +46 -0
- package/src/toolbar/localization.ts +10 -0
- package/src/toolbar/toolbar.css +2 -0
- package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
- package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
- package/src/toolbar/widgets/BaseWidget.ts +34 -2
- package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
- package/src/toolbar/widgets/OverflowWidget.css +9 -0
- package/src/toolbar/widgets/OverflowWidget.ts +83 -0
- package/src/toolbar/widgets/lib.ts +2 -1
- package/src/tools/PasteHandler.ts +3 -2
- 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
|
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.
|
256
|
+
const id = widget.getUniqueIdIn(this.widgetsById);
|
151
257
|
|
152
258
|
// Add the widget
|
153
|
-
this.
|
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.
|
204
|
-
result[widgetId] = this.
|
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.
|
331
|
+
if (!(widgetId in this.widgetsById)) {
|
219
332
|
console.warn(`Unable to deserialize widget ${widgetId} — no such widget.`);
|
220
333
|
}
|
221
334
|
|
222
|
-
this.
|
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(
|
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
|
|
package/src/toolbar/toolbar.css
CHANGED
@@ -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.
|
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
|
-
|
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
|
+
}
|