js-draw 1.25.0 → 1.27.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. package/LICENSE +1 -1
  2. package/dist/Editor.css +1 -1935
  3. package/dist/bundle.js +478 -4
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +0 -2
  6. package/dist/cjs/Editor.js +1 -1
  7. package/dist/cjs/bundle/bundled.js +2 -1
  8. package/dist/cjs/components/AbstractComponent.d.ts +15 -0
  9. package/dist/cjs/components/AbstractComponent.js +16 -0
  10. package/dist/cjs/components/Stroke.d.ts +1 -0
  11. package/dist/cjs/components/Stroke.js +7 -0
  12. package/dist/cjs/image/EditorImage.d.ts +2 -1
  13. package/dist/cjs/image/EditorImage.js +21 -6
  14. package/dist/cjs/toolbar/AbstractToolbar.js +9 -2
  15. package/dist/cjs/toolbar/IconProvider.d.ts +2 -1
  16. package/dist/cjs/toolbar/IconProvider.js +18 -8
  17. package/dist/cjs/toolbar/localization.d.ts +2 -0
  18. package/dist/cjs/toolbar/localization.js +2 -0
  19. package/dist/cjs/toolbar/widgets/BaseWidget.js +6 -1
  20. package/dist/cjs/toolbar/widgets/HandToolWidget.js +3 -3
  21. package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
  22. package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +109 -28
  23. package/dist/cjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
  24. package/dist/cjs/toolbar/widgets/components/makeButtonGrid.js +40 -0
  25. package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -3
  26. package/dist/cjs/tools/SelectionTool/Selection.js +30 -46
  27. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
  28. package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +67 -0
  29. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
  30. package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +33 -0
  31. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
  32. package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +39 -0
  33. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  34. package/dist/cjs/tools/SelectionTool/SelectionMenuShortcut.js +13 -4
  35. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
  36. package/dist/cjs/tools/SelectionTool/SelectionTool.js +68 -55
  37. package/dist/cjs/tools/SelectionTool/types.d.ts +4 -0
  38. package/dist/cjs/tools/SelectionTool/types.js +6 -1
  39. package/dist/cjs/tools/TextTool.js +5 -2
  40. package/dist/cjs/tools/lib.d.ts +1 -1
  41. package/dist/cjs/tools/lib.js +2 -1
  42. package/dist/cjs/util/ReactiveValue.js +2 -6
  43. package/dist/cjs/util/assertions.d.ts +7 -6
  44. package/dist/cjs/util/assertions.js +35 -29
  45. package/dist/cjs/version.js +1 -1
  46. package/dist/mjs/Editor.d.ts +0 -2
  47. package/dist/mjs/Editor.mjs +1 -1
  48. package/dist/mjs/bundle/bundled.mjs +2 -1
  49. package/dist/mjs/components/AbstractComponent.d.ts +15 -0
  50. package/dist/mjs/components/AbstractComponent.mjs +16 -0
  51. package/dist/mjs/components/Stroke.d.ts +1 -0
  52. package/dist/mjs/components/Stroke.mjs +7 -0
  53. package/dist/mjs/image/EditorImage.d.ts +2 -1
  54. package/dist/mjs/image/EditorImage.mjs +21 -6
  55. package/dist/mjs/toolbar/AbstractToolbar.mjs +9 -2
  56. package/dist/mjs/toolbar/IconProvider.d.ts +2 -1
  57. package/dist/mjs/toolbar/IconProvider.mjs +18 -8
  58. package/dist/mjs/toolbar/localization.d.ts +2 -0
  59. package/dist/mjs/toolbar/localization.mjs +2 -0
  60. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +6 -1
  61. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +3 -3
  62. package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +7 -0
  63. package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +109 -28
  64. package/dist/mjs/toolbar/widgets/components/makeButtonGrid.d.ts +17 -0
  65. package/dist/mjs/toolbar/widgets/components/makeButtonGrid.mjs +35 -0
  66. package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -3
  67. package/dist/mjs/tools/SelectionTool/Selection.mjs +30 -46
  68. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.d.ts +17 -0
  69. package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +61 -0
  70. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.d.ts +13 -0
  71. package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +27 -0
  72. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.d.ts +15 -0
  73. package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +36 -0
  74. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.d.ts +3 -1
  75. package/dist/mjs/tools/SelectionTool/SelectionMenuShortcut.mjs +13 -4
  76. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +10 -2
  77. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +68 -55
  78. package/dist/mjs/tools/SelectionTool/types.d.ts +4 -0
  79. package/dist/mjs/tools/SelectionTool/types.mjs +5 -0
  80. package/dist/mjs/tools/TextTool.mjs +5 -2
  81. package/dist/mjs/tools/lib.d.ts +1 -1
  82. package/dist/mjs/tools/lib.mjs +1 -1
  83. package/dist/mjs/util/ReactiveValue.mjs +2 -6
  84. package/dist/mjs/util/assertions.d.ts +7 -6
  85. package/dist/mjs/util/assertions.mjs +28 -24
  86. package/dist/mjs/version.mjs +1 -1
  87. package/package.json +4 -4
  88. package/src/toolbar/EdgeToolbar.scss +6 -1
  89. package/src/toolbar/widgets/components/components.scss +1 -0
  90. package/src/toolbar/widgets/components/makeButtonGrid.scss +25 -0
  91. package/src/tools/SelectionTool/SelectionTool.scss +12 -1
  92. package/src/tools/util/createMenuOverlay.scss +5 -3
@@ -28,6 +28,12 @@ export var ComponentSizingMode;
28
28
  })(ComponentSizingMode || (ComponentSizingMode = {}));
29
29
  /**
30
30
  * A base class for everything that can be added to an {@link EditorImage}.
31
+ *
32
+ * In addition to the `abstract` methods, there are a few methods that should be
33
+ * overridden when creating a selectable/erasable subclass:
34
+ * - {@link keyPoints}: Overriding this may improve how the component interacts with the selection tool.
35
+ * - {@link withRegionErased}: Override/implement this to allow the component to be partially erased
36
+ * by the partial stroke eraser.
31
37
  */
32
38
  class AbstractComponent {
33
39
  constructor(
@@ -136,6 +142,16 @@ class AbstractComponent {
136
142
  const testLines = rect.getEdges();
137
143
  return testLines.some((edge) => this.intersects(edge));
138
144
  }
145
+ /**
146
+ * Returns a selection of points within this object. Each contiguous section
147
+ * of this object should have a point in the returned array.
148
+ *
149
+ * Subclasses should override this method if the center of the bounding box is
150
+ * not contained within the object.
151
+ */
152
+ keyPoints() {
153
+ return [this.getBBox().center];
154
+ }
139
155
  // @returns true iff this component can be selected (e.g. by the selection tool.)
140
156
  isSelectable() {
141
157
  return true;
@@ -54,6 +54,7 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
54
54
  /** @beta -- May fail for concave `path`s */
55
55
  withRegionErased(eraserPath: Path, viewport: Viewport): Stroke[];
56
56
  intersects(line: LineSegment2): boolean;
57
+ keyPoints(): import("@js-draw/math").Vec3[];
57
58
  intersectsRect(rect: Rect2): boolean;
58
59
  private simplifiedPath;
59
60
  private computeSimplifiedPathFor;
@@ -294,6 +294,13 @@ export default class Stroke extends AbstractComponent {
294
294
  }
295
295
  return false;
296
296
  }
297
+ keyPoints() {
298
+ return this.parts
299
+ .map((part) => {
300
+ return part.startPoint;
301
+ })
302
+ .flat();
303
+ }
297
304
  intersectsRect(rect) {
298
305
  // AbstractComponent::intersectsRect can be inexact for strokes with non-zero
299
306
  // stroke radius (has many false negatives). As such, additional checks are
@@ -225,7 +225,7 @@ export declare class ImageNode {
225
225
  getContent(): AbstractComponent | null;
226
226
  getParent(): ImageNode | null;
227
227
  protected getChildrenIntersectingRegion(region: Rect2, isTooSmallFilter?: TooSmallToRenderCheck): ImageNode[];
228
- getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
228
+ getChildrenOrSelfIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
229
229
  /**
230
230
  * Returns a list of `ImageNode`s with content (and thus no children).
231
231
  * Override getChildrenIntersectingRegion to customize how this method
@@ -258,6 +258,7 @@ export declare class RootImageNode extends ImageNode {
258
258
  private fullscreenChildren;
259
259
  private dataComponents;
260
260
  protected getChildrenIntersectingRegion(region: Rect2, _isTooSmall?: TooSmallToRenderCheck): ImageNode[];
261
+ getChildrenOrSelfIntersectingRegion(region: Rect2, _isTooSmall?: TooSmallToRenderCheck): ImageNode[];
261
262
  getLeaves(): ImageNode[];
262
263
  removeChild(child: ImageNode): void;
263
264
  getChildWithContent(target: AbstractComponent): ImageNode | null;
@@ -538,11 +538,11 @@ export class ImageNode {
538
538
  return !isTooSmallFilter?.(bbox) && bbox.intersects(region);
539
539
  });
540
540
  }
541
- getChildrenOrSelfIntersectingRegion(region) {
542
- if (this.content) {
541
+ getChildrenOrSelfIntersectingRegion(region, isTooSmall) {
542
+ if (this.content && this.bbox.intersects(region) && !isTooSmall?.(this.bbox)) {
543
543
  return [this];
544
544
  }
545
- return this.getChildrenIntersectingRegion(region);
545
+ return this.getChildrenIntersectingRegion(region, isTooSmall);
546
546
  }
547
547
  /**
548
548
  * Returns a list of `ImageNode`s with content (and thus no children).
@@ -560,10 +560,17 @@ export class ImageNode {
560
560
  workList.push(this);
561
561
  while (workList.length > 0) {
562
562
  const current = workList.pop();
563
- if (current.content !== null) {
564
- result.push(current);
563
+ // Split the children into leaves and non-leaves
564
+ const processed = current.getChildrenOrSelfIntersectingRegion(region, isTooSmall);
565
+ for (const item of processed) {
566
+ if (item.content) {
567
+ result.push(item);
568
+ }
569
+ else {
570
+ // Non-leaves need to be processed
571
+ workList.push(item);
572
+ }
565
573
  }
566
- workList.push(...current.getChildrenIntersectingRegion(region, isTooSmall));
567
574
  }
568
575
  return result;
569
576
  }
@@ -917,6 +924,14 @@ export class RootImageNode extends ImageNode {
917
924
  }
918
925
  return result;
919
926
  }
927
+ getChildrenOrSelfIntersectingRegion(region, _isTooSmall) {
928
+ const content = this.getContent();
929
+ // Fullscreen components always intersect/contain
930
+ if (content && content.getSizingMode() === ComponentSizingMode.FillScreen) {
931
+ return [this];
932
+ }
933
+ return super.getChildrenOrSelfIntersectingRegion(region, _isTooSmall);
934
+ }
920
935
  getLeaves() {
921
936
  const leaves = super.getLeaves();
922
937
  // Add fullscreen/data components — this method should
@@ -31,6 +31,7 @@ import { Color4 } from '@js-draw/math';
31
31
  import { toolbarCSSPrefix } from './constants.mjs';
32
32
  import SaveActionWidget from './widgets/SaveActionWidget.mjs';
33
33
  import ExitActionWidget from './widgets/ExitActionWidget.mjs';
34
+ import { assertIsObject, assertTruthy } from '../util/assertions.mjs';
34
35
  /**
35
36
  * Abstract base class for js-draw editor toolbars.
36
37
  *
@@ -205,8 +206,12 @@ class AbstractToolbar {
205
206
  */
206
207
  deserializeState(state) {
207
208
  const data = JSON.parse(state);
209
+ assertIsObject(data);
210
+ assertTruthy(data);
208
211
  const rootId = AbstractToolbar.rootToolbarId;
209
- this.deserializeInternal(data[rootId]);
212
+ if (rootId in data && typeof data[rootId] !== 'undefined') {
213
+ this.deserializeInternal(data[rootId]);
214
+ }
210
215
  for (const widgetId in data) {
211
216
  if (widgetId === rootId) {
212
217
  continue;
@@ -215,7 +220,9 @@ class AbstractToolbar {
215
220
  console.warn(`Unable to deserialize widget ${widgetId} ­— no such widget.`);
216
221
  continue;
217
222
  }
218
- __classPrivateFieldGet(this, _AbstractToolbar_widgetsById, "f")[widgetId].deserializeFrom(data[widgetId]);
223
+ if (typeof data[widgetId] === 'object' && data[widgetId]) {
224
+ __classPrivateFieldGet(this, _AbstractToolbar_widgetsById, "f")[widgetId].deserializeFrom(data[widgetId]);
225
+ }
219
226
  }
220
227
  }
221
228
  /**
@@ -2,6 +2,7 @@ import { Color4 } from '@js-draw/math';
2
2
  import TextRenderingStyle from '../rendering/TextRenderingStyle';
3
3
  import { PenStyle } from '../tools/Pen';
4
4
  import { EraserMode } from '../tools/Eraser';
5
+ import { SelectionMode } from '../tools/SelectionTool/types';
5
6
  export type IconElemType = HTMLImageElement | SVGElement;
6
7
  /**
7
8
  * Provides icons that can be used in the toolbar and other locations.
@@ -41,7 +42,7 @@ export default class IconProvider {
41
42
  makeRedoIcon(): IconElemType;
42
43
  makeDropdownIcon(): IconElemType;
43
44
  makeEraserIcon(eraserSize?: number, mode?: EraserMode): IconElemType;
44
- makeSelectionIcon(): IconElemType;
45
+ makeSelectionIcon(mode?: SelectionMode): IconElemType;
45
46
  makeRotateIcon(): IconElemType;
46
47
  makeHandToolIcon(): IconElemType;
47
48
  makeTouchPanningIcon(): IconElemType;
@@ -11,6 +11,7 @@ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBu
11
11
  import { makePolylineBuilder } from '../components/builders/PolylineBuilder.mjs';
12
12
  import { EraserMode } from '../tools/Eraser.mjs';
13
13
  import { createSvgElement, createSvgElements, createSvgPaths } from '../util/createElement.mjs';
14
+ import { SelectionMode } from '../tools/SelectionTool/types.mjs';
14
15
  const svgNamespace = 'http://www.w3.org/2000/svg';
15
16
  let checkerboardIdCounter = 0;
16
17
  const makeCheckerboardPattern = () => {
@@ -164,15 +165,24 @@ class IconProvider {
164
165
  });
165
166
  return icon;
166
167
  }
167
- makeSelectionIcon() {
168
+ makeSelectionIcon(mode = SelectionMode.Rectangle) {
168
169
  const icon = document.createElementNS(svgNamespace, 'svg');
169
- // Draw a cursor-like shape
170
- icon.innerHTML = `
171
- <g>
172
- <rect x="10" y="10" width="70" height="70" fill="pink" stroke="black"/>
173
- <rect x="75" y="75" width="10" height="10" fill="white" stroke="black"/>
174
- </g>
175
- `;
170
+ if (mode === SelectionMode.Rectangle) {
171
+ icon.innerHTML = `
172
+ <g>
173
+ <rect x="10" y="10" width="70" height="70" fill="pink" stroke="black" stroke-dasharray="32 9"/>
174
+ <rect x="75" y="75" width="10" height="10" fill="white" stroke="black"/>
175
+ </g>
176
+ `;
177
+ }
178
+ else {
179
+ icon.innerHTML = `
180
+ <g>
181
+ <rect x="10" y="10" width="76" height="76" rx="50" stroke-dasharray="32 9" fill="pink" stroke="black"/>
182
+ <rect x="71" y="71" width="10" height="10" fill="white" stroke="black"/>
183
+ </g>
184
+ `;
185
+ }
176
186
  icon.setAttribute('viewBox', '0 0 100 100');
177
187
  return icon;
178
188
  }
@@ -44,6 +44,8 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
44
44
  resetView: string;
45
45
  reformatSelection: string;
46
46
  selectionToolKeyboardShortcuts: string;
47
+ selectionTool__lassoSelect: string;
48
+ selectionTool__lassoSelect__help: string;
47
49
  paste: string;
48
50
  documentProperties: string;
49
51
  backgroundColor: string;
@@ -34,6 +34,8 @@ export const defaultToolbarLocalization = {
34
34
  pickColorFromScreen: 'Pick color from screen',
35
35
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
36
36
  colorSelectionCanceledAnnouncement: 'Color selection canceled',
37
+ selectionTool__lassoSelect: 'Freeform selection',
38
+ selectionTool__lassoSelect__help: 'When enabled, dragging creates a freeform (lasso) selection.',
37
39
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
38
40
  documentProperties: 'Page',
39
41
  backgroundColor: 'Background color',
@@ -16,6 +16,7 @@ import { toolbarCSSPrefix } from '../constants.mjs';
16
16
  import DropdownLayoutManager from './layout/DropdownLayoutManager.mjs';
17
17
  import addLongPressOrHoverCssClasses from '../../util/addLongPressOrHoverCssClasses.mjs';
18
18
  import HelpDisplay from '../utils/HelpDisplay.mjs';
19
+ import { assertIsObject } from '../../util/assertions.mjs';
19
20
  /**
20
21
  * A set of labels that allow toolbar themes to treat buttons differently.
21
22
  */
@@ -446,10 +447,14 @@ class BaseWidget {
446
447
  */
447
448
  deserializeFrom(state) {
448
449
  if (state.subwidgetState) {
450
+ assertIsObject(state.subwidgetState);
449
451
  // Deserialize all subwidgets.
450
452
  for (const subwidgetId in state.subwidgetState) {
451
453
  if (subwidgetId in this.subWidgets) {
452
- this.subWidgets[subwidgetId].deserializeFrom(state.subwidgetState[subwidgetId]);
454
+ const serializedSubwidgetState = state.subwidgetState[subwidgetId];
455
+ if (serializedSubwidgetState) {
456
+ this.subWidgets[subwidgetId].deserializeFrom(serializedSubwidgetState);
457
+ }
453
458
  }
454
459
  }
455
460
  }
@@ -28,7 +28,7 @@ const makeZoomControl = (localizationTable, editor, helpDisplay) => {
28
28
  zoomLevel = Math.round(zoomLevel * 1000) / 1000;
29
29
  }
30
30
  if (zoomLevel !== lastZoom) {
31
- zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
31
+ zoomLevelDisplay.textContent = localizationTable.zoomLevel(zoomLevel);
32
32
  lastZoom = zoomLevel;
33
33
  }
34
34
  };
@@ -189,10 +189,10 @@ export default class HandToolWidget extends BaseToolWidget {
189
189
  }
190
190
  deserializeFrom(state) {
191
191
  if (state.touchPanning !== undefined) {
192
- this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, state.touchPanning);
192
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, !!state.touchPanning);
193
193
  }
194
194
  if (state.rotationLocked !== undefined) {
195
- this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, state.rotationLocked);
195
+ this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, !!state.rotationLocked);
196
196
  }
197
197
  super.deserializeFrom(state);
198
198
  }
@@ -4,14 +4,21 @@ import { KeyPressEvent } from '../../inputEvents';
4
4
  import { ToolbarLocalization } from '../localization';
5
5
  import BaseToolWidget from './BaseToolWidget';
6
6
  import HelpDisplay from '../utils/HelpDisplay';
7
+ import { SavedToolbuttonState } from './BaseWidget';
7
8
  export default class SelectionToolWidget extends BaseToolWidget {
8
9
  private tool;
9
10
  private updateFormatMenu;
11
+ private hasSelectionValue;
10
12
  constructor(editor: Editor, tool: SelectionTool, localization?: ToolbarLocalization);
11
13
  private resizeImageToSelection;
12
14
  protected onKeyPress(event: KeyPressEvent): boolean;
13
15
  protected getTitle(): string;
14
16
  protected createIcon(): Element;
15
17
  protected getHelpText(): string;
18
+ protected createSelectionActions(helpDisplay?: HelpDisplay): {
19
+ container: HTMLDivElement;
20
+ };
16
21
  protected fillDropdown(dropdown: HTMLElement, helpDisplay?: HelpDisplay): boolean;
22
+ serializeState(): SavedToolbuttonState;
23
+ deserializeFrom(state: SavedToolbuttonState): void;
17
24
  }
@@ -1,13 +1,16 @@
1
1
  import { Color4 } from '@js-draw/math';
2
2
  import { isRestylableComponent } from '../../components/RestylableComponent.mjs';
3
3
  import uniteCommands from '../../commands/uniteCommands.mjs';
4
+ import { SelectionMode } from '../../tools/SelectionTool/SelectionTool.mjs';
4
5
  import { EditorEventType } from '../../types.mjs';
5
6
  import makeColorInput from './components/makeColorInput.mjs';
6
- import ActionButtonWidget from './ActionButtonWidget.mjs';
7
7
  import BaseToolWidget from './BaseToolWidget.mjs';
8
8
  import { resizeImageToSelectionKeyboardShortcut } from './keybindings.mjs';
9
9
  import makeSeparator from './components/makeSeparator.mjs';
10
10
  import { toolbarCSSPrefix } from '../constants.mjs';
11
+ import BaseWidget from './BaseWidget.mjs';
12
+ import makeButtonGrid from './components/makeButtonGrid.mjs';
13
+ import { MutableReactiveValue } from '../../util/ReactiveValue.mjs';
11
14
  const makeFormatMenu = (editor, selectionTool, localizationTable) => {
12
15
  const container = document.createElement('div');
13
16
  container.classList.add('selection-format-menu', `${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}indentedList`);
@@ -63,48 +66,63 @@ const makeFormatMenu = (editor, selectionTool, localizationTable) => {
63
66
  },
64
67
  };
65
68
  };
69
+ class LassoSelectToggle extends BaseWidget {
70
+ constructor(editor, tool, localizationTable) {
71
+ super(editor, 'selection-mode-toggle', localizationTable);
72
+ this.tool = tool;
73
+ editor.notifier.on(EditorEventType.ToolUpdated, (toolEvt) => {
74
+ if (toolEvt.kind === EditorEventType.ToolUpdated && toolEvt.tool === tool) {
75
+ this.setSelected(tool.modeValue.get() === SelectionMode.Lasso);
76
+ }
77
+ });
78
+ this.setSelected(false);
79
+ }
80
+ shouldAutoDisableInReadOnlyEditor() {
81
+ return false;
82
+ }
83
+ setModeFlag(enabled) {
84
+ this.tool.modeValue.set(enabled ? SelectionMode.Lasso : SelectionMode.Rectangle);
85
+ }
86
+ handleClick() {
87
+ this.setModeFlag(!this.isSelected());
88
+ }
89
+ getTitle() {
90
+ return this.localizationTable.selectionTool__lassoSelect;
91
+ }
92
+ createIcon() {
93
+ return this.editor.icons.makeSelectionIcon(SelectionMode.Lasso);
94
+ }
95
+ fillDropdown(_dropdown) {
96
+ return false;
97
+ }
98
+ getHelpText() {
99
+ return this.localizationTable.selectionTool__lassoSelect__help;
100
+ }
101
+ }
66
102
  export default class SelectionToolWidget extends BaseToolWidget {
67
103
  constructor(editor, tool, localization) {
68
104
  super(editor, tool, 'selection-tool-widget', localization);
69
105
  this.tool = tool;
70
106
  this.updateFormatMenu = () => { };
71
- const resizeButton = new ActionButtonWidget(editor, 'resize-btn', () => editor.icons.makeResizeImageToSelectionIcon(), this.localizationTable.resizeImageToSelection, () => {
72
- this.resizeImageToSelection();
73
- }, localization);
74
- resizeButton.setHelpText(this.localizationTable.selectionDropdown__resizeToHelpText);
75
- const deleteButton = new ActionButtonWidget(editor, 'delete-btn', () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
76
- const selection = this.tool.getSelection();
77
- this.editor.dispatch(selection.deleteSelectedObjects());
78
- this.tool.clearSelection();
79
- }, localization);
80
- deleteButton.setHelpText(this.localizationTable.selectionDropdown__deleteHelpText);
81
- const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, async () => {
107
+ this.addSubWidget(new LassoSelectToggle(editor, tool, this.localizationTable));
108
+ const hasSelection = () => {
82
109
  const selection = this.tool.getSelection();
83
- this.editor.dispatch(await selection.duplicateSelectedObjects());
84
- this.setDropdownVisible(false);
85
- }, localization);
86
- duplicateButton.setHelpText(this.localizationTable.selectionDropdown__duplicateHelpText);
87
- this.addSubWidget(resizeButton);
88
- this.addSubWidget(deleteButton);
89
- this.addSubWidget(duplicateButton);
90
- const updateDisabled = (disabled) => {
91
- resizeButton.setDisabled(disabled);
92
- deleteButton.setDisabled(disabled);
93
- duplicateButton.setDisabled(disabled);
110
+ return !!selection && selection.getSelectedItemCount() > 0;
94
111
  };
95
- updateDisabled(true);
112
+ this.hasSelectionValue = MutableReactiveValue.fromInitialValue(hasSelection());
96
113
  // Enable/disable actions based on whether items are selected
97
114
  this.editor.notifier.on(EditorEventType.ToolUpdated, (toolEvt) => {
98
115
  if (toolEvt.kind !== EditorEventType.ToolUpdated) {
99
116
  throw new Error('Invalid event type!');
100
117
  }
101
118
  if (toolEvt.tool === this.tool) {
102
- const selection = this.tool.getSelection();
103
- const hasSelection = selection && selection.getSelectedItemCount() > 0;
104
- updateDisabled(!hasSelection);
119
+ this.hasSelectionValue.set(hasSelection());
105
120
  this.updateFormatMenu();
106
121
  }
107
122
  });
123
+ tool.modeValue.onUpdate(() => {
124
+ this.updateIcon();
125
+ });
108
126
  }
109
127
  resizeImageToSelection() {
110
128
  const selection = this.tool.getSelection();
@@ -130,16 +148,66 @@ export default class SelectionToolWidget extends BaseToolWidget {
130
148
  return this.localizationTable.select;
131
149
  }
132
150
  createIcon() {
133
- return this.editor.icons.makeSelectionIcon();
151
+ return this.editor.icons.makeSelectionIcon(this.tool.modeValue.get());
134
152
  }
135
153
  getHelpText() {
136
154
  return this.localizationTable.selectionDropdown__baseHelpText;
137
155
  }
156
+ createSelectionActions(helpDisplay) {
157
+ const icons = this.editor.icons;
158
+ const grid = makeButtonGrid([
159
+ {
160
+ icon: () => icons.makeDeleteSelectionIcon(),
161
+ label: this.localizationTable.deleteSelection,
162
+ onCreated: (button) => {
163
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__deleteHelpText);
164
+ },
165
+ onClick: () => {
166
+ const selection = this.tool.getSelection();
167
+ this.editor.dispatch(selection.deleteSelectedObjects());
168
+ this.tool.clearSelection();
169
+ },
170
+ enabled: this.hasSelectionValue,
171
+ },
172
+ {
173
+ icon: () => icons.makeDuplicateSelectionIcon(),
174
+ label: this.localizationTable.duplicateSelection,
175
+ onCreated: (button) => {
176
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__duplicateHelpText);
177
+ },
178
+ onClick: async () => {
179
+ const selection = this.tool.getSelection();
180
+ const command = await selection?.duplicateSelectedObjects();
181
+ if (command) {
182
+ this.editor.dispatch(command);
183
+ }
184
+ },
185
+ enabled: this.hasSelectionValue,
186
+ },
187
+ {
188
+ icon: () => icons.makeResizeImageToSelectionIcon(),
189
+ label: this.localizationTable.resizeImageToSelection,
190
+ onCreated: (button) => {
191
+ helpDisplay?.registerTextHelpForElement(button, this.localizationTable.selectionDropdown__resizeToHelpText);
192
+ },
193
+ onClick: () => {
194
+ this.resizeImageToSelection();
195
+ },
196
+ enabled: this.hasSelectionValue,
197
+ },
198
+ ], 3);
199
+ return { container: grid.container };
200
+ }
138
201
  fillDropdown(dropdown, helpDisplay) {
139
202
  super.fillDropdown(dropdown, helpDisplay);
140
203
  const controlsContainer = document.createElement('div');
141
204
  controlsContainer.classList.add(`${toolbarCSSPrefix}nonbutton-controls-main-list`);
142
205
  dropdown.appendChild(controlsContainer);
206
+ // Actions (duplicate, delete, etc.)
207
+ makeSeparator().addTo(controlsContainer);
208
+ const actions = this.createSelectionActions(helpDisplay);
209
+ controlsContainer.appendChild(actions.container);
210
+ // Formatting
143
211
  makeSeparator(this.localizationTable.reformatSelection).addTo(controlsContainer);
144
212
  const formatMenu = makeFormatMenu(this.editor, this.tool, this.localizationTable);
145
213
  formatMenu.addTo(controlsContainer);
@@ -150,4 +218,17 @@ export default class SelectionToolWidget extends BaseToolWidget {
150
218
  formatMenu.update();
151
219
  return true;
152
220
  }
221
+ serializeState() {
222
+ return {
223
+ ...super.serializeState(),
224
+ selectionMode: this.tool.modeValue.get(),
225
+ };
226
+ }
227
+ deserializeFrom(state) {
228
+ super.deserializeFrom(state);
229
+ const isValidSelectionMode = Object.values(SelectionMode).includes(state.selectionMode);
230
+ if (isValidSelectionMode) {
231
+ this.tool.modeValue.set(state.selectionMode);
232
+ }
233
+ }
153
234
  }
@@ -0,0 +1,17 @@
1
+ import { ReactiveValue } from '../../../util/ReactiveValue';
2
+ import { IconElemType } from '../../IconProvider';
3
+ interface Button {
4
+ icon: () => IconElemType;
5
+ label: string;
6
+ onClick: () => void;
7
+ onCreated?: (button: HTMLElement) => void;
8
+ enabled?: ReactiveValue<boolean>;
9
+ }
10
+ /**
11
+ * Creates HTML `button` elements from `buttonSpecs` and displays them in a
12
+ * grid with `columnCount` columns.
13
+ */
14
+ declare const makeButtonGrid: (buttonSpecs: Button[], columnCount: number) => {
15
+ container: HTMLDivElement;
16
+ };
17
+ export default makeButtonGrid;
@@ -0,0 +1,35 @@
1
+ import addLongPressOrHoverCssClasses from '../../../util/addLongPressOrHoverCssClasses.mjs';
2
+ /**
3
+ * Creates HTML `button` elements from `buttonSpecs` and displays them in a
4
+ * grid with `columnCount` columns.
5
+ */
6
+ const makeButtonGrid = (buttonSpecs, columnCount) => {
7
+ const container = document.createElement('div');
8
+ container.classList.add('toolbar-button-grid');
9
+ container.style.setProperty('--column-count', `${columnCount}`);
10
+ const makeButton = (buttonSpec) => {
11
+ const buttonElement = document.createElement('button');
12
+ buttonElement.classList.add('button');
13
+ const iconElement = buttonSpec.icon();
14
+ iconElement.classList.add('icon');
15
+ const labelElement = document.createElement('label');
16
+ labelElement.textContent = buttonSpec.label;
17
+ labelElement.classList.add('button-label-text');
18
+ buttonElement.onclick = buttonSpec.onClick;
19
+ if (buttonSpec.enabled) {
20
+ buttonSpec.enabled.onUpdateAndNow((enabled) => {
21
+ buttonElement.disabled = !enabled;
22
+ });
23
+ }
24
+ buttonElement.replaceChildren(iconElement, labelElement);
25
+ container.appendChild(buttonElement);
26
+ addLongPressOrHoverCssClasses(buttonElement);
27
+ buttonSpec.onCreated?.(buttonElement);
28
+ return buttonElement;
29
+ };
30
+ buttonSpecs.map(makeButton);
31
+ return {
32
+ container,
33
+ };
34
+ };
35
+ export default makeButtonGrid;
@@ -20,7 +20,7 @@ export default class Selection {
20
20
  private innerContainer;
21
21
  private backgroundElem;
22
22
  private hasParent;
23
- constructor(startPoint: Point2, editor: Editor, showContextMenu: (anchor: Point2) => void);
23
+ constructor(selectedElems: AbstractComponent[], editor: Editor, showContextMenu: (anchor: Point2) => void);
24
24
  getBackgroundElem(): HTMLElement;
25
25
  getTransform(): Mat33;
26
26
  get preTransformRegion(): Rect2;
@@ -43,7 +43,6 @@ export default class Selection {
43
43
  sendToBack(): SerializableCommand | null;
44
44
  private static ApplyTransformationCommand;
45
45
  private previewTransformCmds;
46
- resolveToObjects(): boolean;
47
46
  recomputeRegion(): boolean;
48
47
  padRegion(): void;
49
48
  getMinCanvasSize(): number;
@@ -63,10 +62,10 @@ export default class Selection {
63
62
  private selectionDuplicatedAnimationTimeout;
64
63
  private runSelectionDuplicatedAnimation;
65
64
  duplicateSelectedObjects(): Promise<Command>;
65
+ snapSelectedObjectsToGrid(): void;
66
66
  setHandlesVisible(showHandles: boolean): void;
67
67
  addTo(elem: HTMLElement): void;
68
68
  setToPoint(point: Point2): void;
69
69
  cancelSelection(): void;
70
- setSelectedObjects(objects: AbstractComponent[], bbox: Rect2): void;
71
70
  getSelectedObjects(): AbstractComponent[];
72
71
  }