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
@@ -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.
|
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.
|
191
|
+
const id = widget.getUniqueIdIn(this.widgetsById);
|
112
192
|
// Add the widget
|
113
|
-
this.
|
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.
|
156
|
-
result[widgetId] = this.
|
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.
|
252
|
+
if (!(widgetId in this.widgetsById)) {
|
168
253
|
console.warn(`Unable to deserialize widget ${widgetId} — no such widget.`);
|
169
254
|
}
|
170
|
-
this.
|
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;
|
@@ -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
|
-
|
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):
|
10
|
+
addTo(parent: HTMLElement): HTMLElement;
|
11
11
|
}
|
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
+
}
|