js-draw 1.0.0 → 1.0.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 (244) hide show
  1. package/README.md +20 -6
  2. package/dist/bundle.js +1 -1
  3. package/dist/cjs/Editor.js +1 -1
  4. package/dist/cjs/Editor.loadFrom.test.d.ts +1 -0
  5. package/dist/cjs/Editor.test.d.ts +1 -0
  6. package/dist/cjs/Editor.toSVG.test.d.ts +1 -0
  7. package/dist/cjs/EditorImage.test.d.ts +1 -0
  8. package/dist/cjs/EventDispatcher.test.d.ts +1 -0
  9. package/dist/cjs/SVGLoader.test.d.ts +1 -0
  10. package/dist/cjs/UndoRedoHistory.test.d.ts +1 -0
  11. package/dist/cjs/commands/uniteCommands.test.d.ts +1 -0
  12. package/dist/cjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  13. package/dist/cjs/components/BackgroundComponent.test.d.ts +1 -0
  14. package/dist/cjs/components/Stroke.test.d.ts +1 -0
  15. package/dist/cjs/components/TextComponent.test.d.ts +1 -0
  16. package/dist/cjs/components/UnknownSVGObject.test.d.ts +1 -0
  17. package/dist/cjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  18. package/dist/cjs/localizations/getLocalizationTable.test.d.ts +1 -0
  19. package/dist/cjs/rendering/RenderingStyle.test.d.ts +1 -0
  20. package/dist/cjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  21. package/dist/cjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  22. package/dist/cjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  23. package/dist/cjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  24. package/dist/cjs/shortcuts/KeyBinding.test.d.ts +1 -0
  25. package/dist/cjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  26. package/dist/cjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  27. package/dist/cjs/tools/Eraser.test.d.ts +1 -0
  28. package/dist/cjs/tools/FindTool.test.d.ts +1 -0
  29. package/dist/cjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  30. package/dist/cjs/tools/PanZoom.test.d.ts +1 -0
  31. package/dist/cjs/tools/Pen.test.d.ts +1 -0
  32. package/dist/cjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  33. package/dist/cjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  34. package/dist/cjs/util/ReactiveValue.test.d.ts +1 -0
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/cjs/version.test.d.ts +1 -0
  37. package/dist/mjs/Editor.loadFrom.test.d.ts +1 -0
  38. package/dist/mjs/Editor.mjs +1 -1
  39. package/dist/mjs/Editor.test.d.ts +1 -0
  40. package/dist/mjs/Editor.toSVG.test.d.ts +1 -0
  41. package/dist/mjs/EditorImage.test.d.ts +1 -0
  42. package/dist/mjs/EventDispatcher.test.d.ts +1 -0
  43. package/dist/mjs/SVGLoader.test.d.ts +1 -0
  44. package/dist/mjs/UndoRedoHistory.test.d.ts +1 -0
  45. package/dist/mjs/commands/uniteCommands.test.d.ts +1 -0
  46. package/dist/mjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  47. package/dist/mjs/components/BackgroundComponent.test.d.ts +1 -0
  48. package/dist/mjs/components/Stroke.test.d.ts +1 -0
  49. package/dist/mjs/components/TextComponent.test.d.ts +1 -0
  50. package/dist/mjs/components/UnknownSVGObject.test.d.ts +1 -0
  51. package/dist/mjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  52. package/dist/mjs/localizations/getLocalizationTable.test.d.ts +1 -0
  53. package/dist/mjs/rendering/RenderingStyle.test.d.ts +1 -0
  54. package/dist/mjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  55. package/dist/mjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  56. package/dist/mjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  57. package/dist/mjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  58. package/dist/mjs/shortcuts/KeyBinding.test.d.ts +1 -0
  59. package/dist/mjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  60. package/dist/mjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  61. package/dist/mjs/tools/Eraser.test.d.ts +1 -0
  62. package/dist/mjs/tools/FindTool.test.d.ts +1 -0
  63. package/dist/mjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  64. package/dist/mjs/tools/PanZoom.test.d.ts +1 -0
  65. package/dist/mjs/tools/Pen.test.d.ts +1 -0
  66. package/dist/mjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  67. package/dist/mjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  68. package/dist/mjs/util/ReactiveValue.test.d.ts +1 -0
  69. package/dist/mjs/version.mjs +1 -1
  70. package/dist/mjs/version.test.d.ts +1 -0
  71. package/dist-test/test_imports/package-lock.json +13 -0
  72. package/dist-test/test_imports/package.json +12 -0
  73. package/dist-test/test_imports/test-imports.js +11 -0
  74. package/dist-test/test_imports/test-require.cjs +14 -0
  75. package/package.json +2 -2
  76. package/src/Editor.loadFrom.test.ts +24 -0
  77. package/src/Editor.test.ts +107 -0
  78. package/src/Editor.toSVG.test.ts +294 -0
  79. package/src/Editor.ts +1443 -0
  80. package/src/EditorImage.test.ts +117 -0
  81. package/src/EditorImage.ts +609 -0
  82. package/src/EventDispatcher.test.ts +123 -0
  83. package/src/EventDispatcher.ts +72 -0
  84. package/src/Pointer.ts +183 -0
  85. package/src/SVGLoader.test.ts +114 -0
  86. package/src/SVGLoader.ts +672 -0
  87. package/src/UndoRedoHistory.test.ts +34 -0
  88. package/src/UndoRedoHistory.ts +102 -0
  89. package/src/Viewport.ts +322 -0
  90. package/src/bundle/bundled.ts +7 -0
  91. package/src/commands/Command.ts +45 -0
  92. package/src/commands/Duplicate.ts +75 -0
  93. package/src/commands/Erase.ts +95 -0
  94. package/src/commands/SerializableCommand.ts +49 -0
  95. package/src/commands/UnresolvedCommand.ts +37 -0
  96. package/src/commands/invertCommand.ts +58 -0
  97. package/src/commands/lib.ts +16 -0
  98. package/src/commands/localization.ts +47 -0
  99. package/src/commands/uniteCommands.test.ts +23 -0
  100. package/src/commands/uniteCommands.ts +140 -0
  101. package/src/components/AbstractComponent.transformBy.test.ts +23 -0
  102. package/src/components/AbstractComponent.ts +383 -0
  103. package/src/components/BackgroundComponent.test.ts +44 -0
  104. package/src/components/BackgroundComponent.ts +348 -0
  105. package/src/components/ImageComponent.ts +176 -0
  106. package/src/components/RestylableComponent.ts +161 -0
  107. package/src/components/SVGGlobalAttributesObject.ts +79 -0
  108. package/src/components/Stroke.test.ts +137 -0
  109. package/src/components/Stroke.ts +294 -0
  110. package/src/components/TextComponent.test.ts +202 -0
  111. package/src/components/TextComponent.ts +429 -0
  112. package/src/components/UnknownSVGObject.test.ts +10 -0
  113. package/src/components/UnknownSVGObject.ts +60 -0
  114. package/src/components/builders/ArrowBuilder.ts +106 -0
  115. package/src/components/builders/CircleBuilder.ts +100 -0
  116. package/src/components/builders/FreehandLineBuilder.test.ts +24 -0
  117. package/src/components/builders/FreehandLineBuilder.ts +210 -0
  118. package/src/components/builders/LineBuilder.ts +77 -0
  119. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +453 -0
  120. package/src/components/builders/RectangleBuilder.ts +73 -0
  121. package/src/components/builders/types.ts +15 -0
  122. package/src/components/lib.ts +31 -0
  123. package/src/components/localization.ts +24 -0
  124. package/src/components/util/StrokeSmoother.ts +302 -0
  125. package/src/components/util/describeComponentList.ts +18 -0
  126. package/src/dialogs/makeAboutDialog.ts +82 -0
  127. package/src/inputEvents.ts +143 -0
  128. package/src/lib.ts +91 -0
  129. package/src/localization.ts +34 -0
  130. package/src/localizations/de.ts +146 -0
  131. package/src/localizations/en.ts +8 -0
  132. package/src/localizations/es.ts +74 -0
  133. package/src/localizations/getLocalizationTable.test.ts +27 -0
  134. package/src/localizations/getLocalizationTable.ts +74 -0
  135. package/src/rendering/Display.ts +247 -0
  136. package/src/rendering/RenderablePathSpec.ts +88 -0
  137. package/src/rendering/RenderingStyle.test.ts +68 -0
  138. package/src/rendering/RenderingStyle.ts +55 -0
  139. package/src/rendering/TextRenderingStyle.ts +55 -0
  140. package/src/rendering/caching/CacheRecord.test.ts +48 -0
  141. package/src/rendering/caching/CacheRecord.ts +76 -0
  142. package/src/rendering/caching/CacheRecordManager.ts +71 -0
  143. package/src/rendering/caching/RenderingCache.test.ts +43 -0
  144. package/src/rendering/caching/RenderingCache.ts +66 -0
  145. package/src/rendering/caching/RenderingCacheNode.ts +404 -0
  146. package/src/rendering/caching/testUtils.ts +35 -0
  147. package/src/rendering/caching/types.ts +34 -0
  148. package/src/rendering/lib.ts +8 -0
  149. package/src/rendering/localization.ts +20 -0
  150. package/src/rendering/renderers/AbstractRenderer.ts +232 -0
  151. package/src/rendering/renderers/CanvasRenderer.ts +312 -0
  152. package/src/rendering/renderers/DummyRenderer.test.ts +41 -0
  153. package/src/rendering/renderers/DummyRenderer.ts +142 -0
  154. package/src/rendering/renderers/SVGRenderer.ts +434 -0
  155. package/src/rendering/renderers/TextOnlyRenderer.test.ts +34 -0
  156. package/src/rendering/renderers/TextOnlyRenderer.ts +68 -0
  157. package/src/shortcuts/KeyBinding.test.ts +61 -0
  158. package/src/shortcuts/KeyBinding.ts +257 -0
  159. package/src/shortcuts/KeyboardShortcutManager.test.ts +95 -0
  160. package/src/shortcuts/KeyboardShortcutManager.ts +163 -0
  161. package/src/shortcuts/lib.ts +3 -0
  162. package/src/testing/createEditor.ts +11 -0
  163. package/src/testing/getUniquePointerId.ts +18 -0
  164. package/src/testing/lib.ts +3 -0
  165. package/src/testing/sendPenEvent.ts +36 -0
  166. package/src/testing/sendTouchEvent.ts +71 -0
  167. package/src/toolbar/AbstractToolbar.ts +542 -0
  168. package/src/toolbar/DropdownToolbar.ts +220 -0
  169. package/src/toolbar/EdgeToolbar.test.ts +54 -0
  170. package/src/toolbar/EdgeToolbar.ts +543 -0
  171. package/src/toolbar/IconProvider.ts +861 -0
  172. package/src/toolbar/constants.ts +1 -0
  173. package/src/toolbar/lib.ts +6 -0
  174. package/src/toolbar/localization.ts +136 -0
  175. package/src/toolbar/types.ts +13 -0
  176. package/src/toolbar/widgets/ActionButtonWidget.ts +39 -0
  177. package/src/toolbar/widgets/BaseToolWidget.ts +81 -0
  178. package/src/toolbar/widgets/BaseWidget.ts +495 -0
  179. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +250 -0
  180. package/src/toolbar/widgets/EraserToolWidget.ts +84 -0
  181. package/src/toolbar/widgets/HandToolWidget.ts +239 -0
  182. package/src/toolbar/widgets/InsertImageWidget.ts +248 -0
  183. package/src/toolbar/widgets/OverflowWidget.ts +92 -0
  184. package/src/toolbar/widgets/PenToolWidget.ts +369 -0
  185. package/src/toolbar/widgets/SelectionToolWidget.ts +195 -0
  186. package/src/toolbar/widgets/TextToolWidget.ts +149 -0
  187. package/src/toolbar/widgets/components/makeColorInput.ts +184 -0
  188. package/src/toolbar/widgets/components/makeFileInput.ts +128 -0
  189. package/src/toolbar/widgets/components/makeGridSelector.ts +179 -0
  190. package/src/toolbar/widgets/components/makeSeparator.ts +17 -0
  191. package/src/toolbar/widgets/components/makeThicknessSlider.ts +62 -0
  192. package/src/toolbar/widgets/keybindings.ts +19 -0
  193. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +262 -0
  194. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +71 -0
  195. package/src/toolbar/widgets/layout/types.ts +74 -0
  196. package/src/toolbar/widgets/lib.ts +13 -0
  197. package/src/tools/BaseTool.ts +169 -0
  198. package/src/tools/Eraser.test.ts +103 -0
  199. package/src/tools/Eraser.ts +173 -0
  200. package/src/tools/FindTool.test.ts +67 -0
  201. package/src/tools/FindTool.ts +153 -0
  202. package/src/tools/InputFilter/FunctionMapper.ts +17 -0
  203. package/src/tools/InputFilter/InputMapper.ts +41 -0
  204. package/src/tools/InputFilter/InputPipeline.test.ts +41 -0
  205. package/src/tools/InputFilter/InputPipeline.ts +34 -0
  206. package/src/tools/InputFilter/InputStabilizer.ts +254 -0
  207. package/src/tools/InputFilter/StrokeKeyboardControl.ts +104 -0
  208. package/src/tools/PanZoom.test.ts +339 -0
  209. package/src/tools/PanZoom.ts +525 -0
  210. package/src/tools/PasteHandler.ts +94 -0
  211. package/src/tools/Pen.test.ts +260 -0
  212. package/src/tools/Pen.ts +284 -0
  213. package/src/tools/PipetteTool.ts +84 -0
  214. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +29 -0
  215. package/src/tools/SelectionTool/Selection.ts +647 -0
  216. package/src/tools/SelectionTool/SelectionHandle.ts +142 -0
  217. package/src/tools/SelectionTool/SelectionTool.test.ts +370 -0
  218. package/src/tools/SelectionTool/SelectionTool.ts +510 -0
  219. package/src/tools/SelectionTool/TransformMode.ts +112 -0
  220. package/src/tools/SelectionTool/types.ts +11 -0
  221. package/src/tools/SoundUITool.ts +221 -0
  222. package/src/tools/TextTool.ts +339 -0
  223. package/src/tools/ToolController.ts +224 -0
  224. package/src/tools/ToolEnabledGroup.ts +14 -0
  225. package/src/tools/ToolSwitcherShortcut.ts +39 -0
  226. package/src/tools/ToolbarShortcutHandler.ts +39 -0
  227. package/src/tools/UndoRedoShortcut.test.ts +62 -0
  228. package/src/tools/UndoRedoShortcut.ts +24 -0
  229. package/src/tools/keybindings.ts +85 -0
  230. package/src/tools/lib.ts +22 -0
  231. package/src/tools/localization.ts +76 -0
  232. package/src/types.ts +151 -0
  233. package/src/util/ReactiveValue.test.ts +168 -0
  234. package/src/util/ReactiveValue.ts +241 -0
  235. package/src/util/assertions.ts +55 -0
  236. package/src/util/fileToBase64.ts +18 -0
  237. package/src/util/guessKeyCodeFromKey.ts +36 -0
  238. package/src/util/listPrefixMatch.ts +19 -0
  239. package/src/util/stopPropagationOfScrollingWheelEvents.ts +20 -0
  240. package/src/util/untilNextAnimationFrame.ts +9 -0
  241. package/src/util/waitForAll.ts +18 -0
  242. package/src/util/waitForTimeout.ts +9 -0
  243. package/src/version.test.ts +12 -0
  244. package/src/version.ts +3 -0
@@ -0,0 +1,543 @@
1
+ import Editor from '../Editor';
2
+ import { ToolbarLocalization } from './localization';
3
+ import BaseWidget, { ToolbarWidgetTag } from './widgets/BaseWidget';
4
+ import { toolbarCSSPrefix } from './constants';
5
+ import EdgeToolbarLayoutManager from './widgets/layout/EdgeToolbarLayoutManager';
6
+ import { MutableReactiveValue, ReactiveValue } from '../util/ReactiveValue';
7
+ import AbstractToolbar, { SpacerOptions } from './AbstractToolbar';
8
+ import stopPropagationOfScrollingWheelEvents from '../util/stopPropagationOfScrollingWheelEvents';
9
+
10
+
11
+ export const makeEdgeToolbar = (editor: Editor): AbstractToolbar => {
12
+ return new EdgeToolbar(editor, editor.getRootElement(), editor.localization);
13
+ };
14
+
15
+
16
+ export default class EdgeToolbar extends AbstractToolbar {
17
+ private toolbarContainer: HTMLElement;
18
+
19
+ // Row that contains action buttons
20
+ private toolbarActionRow: HTMLElement;
21
+
22
+ // Row that contains tools
23
+ private toolbarToolRow: HTMLElement;
24
+
25
+ private toolRowResizeObserver: ResizeObserver;
26
+
27
+ private menuContainer: HTMLElement;
28
+ private sidebarContainer: HTMLElement;
29
+ private sidebarContent: HTMLElement;
30
+ private closeButton: HTMLElement;
31
+
32
+ private layoutManager: EdgeToolbarLayoutManager;
33
+
34
+ private sidebarVisible: MutableReactiveValue<boolean>;
35
+ private sidebarY: MutableReactiveValue<number>;
36
+ private sidebarTitle: MutableReactiveValue<string>;
37
+
38
+ /** @internal */
39
+ public constructor(
40
+ editor: Editor, parent: HTMLElement,
41
+ localizationTable: ToolbarLocalization,
42
+ ) {
43
+ super(editor, localizationTable);
44
+
45
+ this.toolbarContainer = document.createElement('div');
46
+ this.toolbarContainer.classList.add(`${toolbarCSSPrefix}root`);
47
+ this.toolbarContainer.classList.add(`${toolbarCSSPrefix}element`);
48
+ this.toolbarContainer.classList.add(`${toolbarCSSPrefix}edge-toolbar`);
49
+ this.toolbarContainer.setAttribute('role', 'toolbar');
50
+
51
+ this.toolbarActionRow = document.createElement('div');
52
+ this.toolbarActionRow.classList.add('toolbar-element', 'toolbar-action-row');
53
+ this.toolbarToolRow = document.createElement('div');
54
+ this.toolbarToolRow.classList.add('toolbar-element', 'toolbar-tool-row');
55
+
56
+ stopPropagationOfScrollingWheelEvents(this.toolbarToolRow);
57
+
58
+ if ('ResizeObserver' in window) {
59
+ this.toolRowResizeObserver = new ResizeObserver((_entries) => {
60
+ this.onToolbarRowResize();
61
+ });
62
+ this.toolRowResizeObserver.observe(this.toolbarToolRow);
63
+ } else {
64
+ console.warn('ResizeObserver not supported. Toolbar will not resize.');
65
+ }
66
+
67
+ this.toolbarContainer.replaceChildren(this.toolbarActionRow, this.toolbarToolRow);
68
+ parent.appendChild(this.toolbarContainer);
69
+
70
+ this.sidebarVisible = ReactiveValue.fromInitialValue(false);
71
+ this.sidebarY = ReactiveValue.fromInitialValue(0);
72
+
73
+ // Create the container elements
74
+ this.menuContainer = document.createElement('div');
75
+ this.menuContainer.classList.add(`${toolbarCSSPrefix}edgemenu-container`);
76
+
77
+ this.sidebarContainer = document.createElement('div');
78
+ this.sidebarContainer.classList.add(
79
+ `${toolbarCSSPrefix}edgemenu`,
80
+ `${toolbarCSSPrefix}element`,
81
+ );
82
+ this.sidebarContainer.classList.add(`${toolbarCSSPrefix}tool-properties`);
83
+
84
+ this.sidebarContent = document.createElement('div');
85
+
86
+ // Setup resizing/dragging
87
+ this.sidebarY.onUpdateAndNow(y => {
88
+ const belowEdgeClassName = 'dropdown-below-edge';
89
+ if (y > 0) {
90
+ this.sidebarContainer.style.translate = `0 ${y}px`;
91
+ this.sidebarContainer.style.paddingBottom = '';
92
+ this.menuContainer.classList.add(belowEdgeClassName);
93
+ } else {
94
+ this.sidebarContainer.style.translate = '';
95
+ this.sidebarContainer.style.paddingBottom = `${-y}px`;
96
+ this.menuContainer.classList.remove(belowEdgeClassName);
97
+ }
98
+ });
99
+
100
+ this.closeButton = document.createElement('button');
101
+ this.closeButton.classList.add('drag-elem');
102
+
103
+ // The close button has default focus -- forward its events to the main editor so that keyboard
104
+ // shortcuts still work.
105
+ this.editor.handleKeyEventsFrom(this.closeButton, event => {
106
+ // Don't send
107
+ return event.code !== 'Space' && event.code !== 'Enter' && event.code !== 'Tab';
108
+ });
109
+
110
+ // Close the sidebar when pressing escape
111
+ this.sidebarContainer.addEventListener('keyup', event => {
112
+ if (!event.defaultPrevented && event.code === 'Escape') {
113
+ this.sidebarVisible.set(false);
114
+ event.preventDefault();
115
+ }
116
+ });
117
+
118
+ this.initDragListeners();
119
+
120
+ // Initialize the layout manager
121
+ const setSidebarContent = (...content: HTMLElement[]) => {
122
+ this.sidebarContent.replaceChildren(...content);
123
+ this.setupColorPickers();
124
+ };
125
+
126
+ this.sidebarTitle = MutableReactiveValue.fromInitialValue('');
127
+
128
+ this.layoutManager = new EdgeToolbarLayoutManager(
129
+ setSidebarContent,
130
+ this.sidebarTitle,
131
+ this.sidebarVisible,
132
+ editor.announceForAccessibility.bind(editor),
133
+ localizationTable,
134
+ );
135
+
136
+ this.sidebarTitle.onUpdateAndNow(title => {
137
+ this.closeButton.setAttribute('aria-label', localizationTable.closeSidebar(title));
138
+ });
139
+
140
+ // Make things visible/keep hidden.
141
+ this.listenForVisibilityChanges();
142
+
143
+ this.sidebarContainer.replaceChildren(this.closeButton, this.sidebarContent);
144
+ this.menuContainer.replaceChildren(this.sidebarContainer);
145
+ parent.appendChild(this.menuContainer);
146
+ }
147
+
148
+ private listenForVisibilityChanges() {
149
+ let animationTimeout: ReturnType<typeof setTimeout>|null = null;
150
+ const animationDuration = 170;
151
+
152
+ if (!this.sidebarVisible.get()) {
153
+ this.menuContainer.style.display = 'none';
154
+
155
+ // Set the initial opacity to 0 to allow the `transition` property
156
+ // to animate it to 1.
157
+ this.menuContainer.style.opacity = '0';
158
+ }
159
+
160
+ this.sidebarVisible.onUpdate(visible => {
161
+ const animationProperties = `${animationDuration}ms ease`;
162
+
163
+ if (visible) {
164
+ this.sidebarY.set(this.snappedSidebarY());
165
+
166
+ if (animationTimeout) {
167
+ clearTimeout(animationTimeout);
168
+ animationTimeout = null;
169
+ }
170
+
171
+ this.menuContainer.style.display = '';
172
+ this.sidebarContainer.style.animation = `${animationProperties} ${toolbarCSSPrefix}-edgemenu-transition-in`;
173
+ this.menuContainer.style.animation = `${animationProperties} ${toolbarCSSPrefix}-edgemenu-container-transition-in`;
174
+ this.menuContainer.style.opacity = '1';
175
+
176
+
177
+ // Focus the close button when first shown.
178
+ this.closeButton.focus();
179
+ } else {
180
+ this.closeColorPickers();
181
+
182
+ if (animationTimeout === null) {
183
+ this.sidebarContainer.style.animation = ` ${animationProperties} ${toolbarCSSPrefix}-edgemenu-transition-out`;
184
+ this.menuContainer.style.animation = `${animationProperties} ${toolbarCSSPrefix}-edgemenu-container-transition-out`;
185
+
186
+ // Manually set the container's opacity to prevent flickering when closing
187
+ // the toolbar.
188
+ this.menuContainer.style.opacity = '0';
189
+
190
+ // Hide overflow -- don't show the part of the edge toolbar that's outside of
191
+ // the editor.
192
+ //this.menuContainer.style.overflowY = 'hidden';
193
+
194
+ this.editor.announceForAccessibility(
195
+ this.localizationTable.dropdownHidden(this.sidebarTitle.get())
196
+ );
197
+
198
+ animationTimeout = setTimeout(() => {
199
+ this.menuContainer.style.display = 'none';
200
+ this.menuContainer.style.overflowY = '';
201
+ animationTimeout = null;
202
+ }, animationDuration);
203
+ }
204
+ }
205
+ });
206
+ }
207
+
208
+ private onToolbarRowResize() {
209
+ const setExtraPadding = () => {
210
+ const visibleWidth = this.toolbarToolRow.clientWidth;
211
+
212
+ // Determine whether extra spacing needs to be added so that one button is cut
213
+ // in half. Ideally, when there is scroll, one button will be cut in half to show
214
+ // that scrolling is possible.
215
+ let currentWidth = 0;
216
+ let extraPadding = 0;
217
+ let numVisibleButtons = 0;
218
+ for (const child of this.toolbarToolRow.children) {
219
+ // Use the first child -- padding is applied around that child. Assumes
220
+ // that the button's width is its height plus some padding.
221
+ const buttonBaseSize = child.clientHeight;
222
+ currentWidth += buttonBaseSize;
223
+ numVisibleButtons ++;
224
+
225
+ if (currentWidth > visibleWidth) {
226
+ // We want extraPadding + (currentWidth - buttonWidth / 2) = visibleWidth.
227
+ // Thus, extraPadding = visibleWidth - currentWidth + buttonWidth / 2;
228
+ extraPadding = visibleWidth - currentWidth + buttonBaseSize / 2;
229
+
230
+ // Ensure that the padding is positive
231
+ if (extraPadding < 0) {
232
+ extraPadding += buttonBaseSize;
233
+ }
234
+ break;
235
+ }
236
+ }
237
+
238
+ const perButtonPadding = Math.round(extraPadding/numVisibleButtons * 10) / 10;
239
+ this.toolbarToolRow.style.setProperty('--extra-left-right-padding', `${perButtonPadding}px`);
240
+ };
241
+
242
+ const actionRowBBox = this.toolbarActionRow.getBoundingClientRect();
243
+ const toolbarRowBBox = this.toolbarToolRow.getBoundingClientRect();
244
+ const inSameRow = actionRowBBox.y === toolbarRowBBox.y;
245
+ const onDifferentRows = actionRowBBox.y + actionRowBBox.height <= toolbarRowBBox.y;
246
+
247
+ if (onDifferentRows) {
248
+ this.toolbarContainer.classList.remove('one-row');
249
+ } else {
250
+ this.toolbarContainer.classList.add('one-row');
251
+ }
252
+
253
+ if (this.toolbarToolRow.clientWidth < this.toolbarToolRow.scrollWidth) {
254
+ this.toolbarToolRow.classList.add('has-scroll');
255
+
256
+ // If both button areas are in the same row, don't change the padding --
257
+ // it could lead to an endless loop of reseize events.
258
+ if (!inSameRow) {
259
+ setExtraPadding();
260
+ }
261
+ } else {
262
+ this.toolbarToolRow.classList.remove('has-scroll', 'extra-padding');
263
+ }
264
+ }
265
+
266
+ public override addSpacer(_options?: Partial<SpacerOptions> | undefined): void {
267
+ //throw new Error('Method not implemented.');
268
+ // Unused for this toolbar.
269
+ }
270
+
271
+ public override addUndoRedoButtons(): void {
272
+ super.addUndoRedoButtons(false);
273
+ }
274
+
275
+ public override addDefaults(): void {
276
+ this.addDefaultActionButtons();
277
+ this.addDefaultToolWidgets();
278
+ }
279
+
280
+ private updateWidgetCSSClasses(widget: BaseWidget) {
281
+ const tags = widget.getTags();
282
+ widget.removeCSSClassFromContainer('label-inline');
283
+ widget.removeCSSClassFromContainer('label-left');
284
+ widget.removeCSSClassFromContainer('label-right');
285
+
286
+ if (tags.includes(ToolbarWidgetTag.Save)) {
287
+ widget.addCSSClassToContainer('label-inline');
288
+ widget.addCSSClassToContainer('label-right');
289
+ }
290
+
291
+ if (tags.includes(ToolbarWidgetTag.Exit)) {
292
+ widget.addCSSClassToContainer('label-inline');
293
+ widget.addCSSClassToContainer('label-left');
294
+ }
295
+ }
296
+
297
+ protected override addWidgetInternal(widget: BaseWidget) {
298
+ this.updateWidgetCSSClasses(widget);
299
+
300
+ widget.setLayoutManager(this.layoutManager);
301
+ if (widget.mustBeInToplevelMenu()) {
302
+ widget.addTo(this.toolbarActionRow);
303
+ } else {
304
+ widget.addTo(this.toolbarToolRow);
305
+ }
306
+ }
307
+
308
+ protected override removeWidgetInternal(widget: BaseWidget): void {
309
+ widget.remove();
310
+ }
311
+
312
+ protected override onRemove() {
313
+ this.toolbarContainer.remove();
314
+ this.menuContainer.remove();
315
+ this.toolRowResizeObserver.disconnect();
316
+ }
317
+
318
+ private initDragListeners() {
319
+ let lastX = 0;
320
+ let lastY = 0;
321
+ let startX = 0;
322
+ let startY = 0;
323
+ let pointerDown = false;
324
+ let capturedPointerId: number|null = null;
325
+
326
+ const dragElements = [ this.closeButton, this.sidebarContainer, this.sidebarContent ];
327
+
328
+ this.manageListener(this.editor.handlePointerEventsExceptClicksFrom(this.menuContainer, (eventName, event) => {
329
+ if (event.target === this.menuContainer) {
330
+ if (eventName === 'pointerdown') {
331
+ this.sidebarVisible.set(false);
332
+ }
333
+
334
+ if (eventName === 'pointerup') {
335
+ this.editor.focus();
336
+ }
337
+
338
+ return true;
339
+ }
340
+
341
+ if (!this.sidebarVisible.get()) {
342
+ return true;
343
+ }
344
+
345
+ // Don't send pointer events that don't directly target mainContainer
346
+ // to the editor
347
+ return false;
348
+ }, (_eventName, event) => {
349
+ return event.target === this.menuContainer;
350
+ }));
351
+
352
+ const isDraggableElement = (element: HTMLElement|null) => {
353
+ if (!element) {
354
+ return false;
355
+ }
356
+
357
+ if (dragElements.includes(element)) {
358
+ return true;
359
+ }
360
+
361
+ // Some inputs handle dragging themselves. Don't also interpret such gestures
362
+ // as dragging the dropdown.
363
+ const undraggableElementTypes = [ 'INPUT', 'SELECT', 'IMG' ];
364
+
365
+ let hasSuitableAncestors = false;
366
+ let ancestor = element.parentElement;
367
+ while (ancestor) {
368
+ if (undraggableElementTypes.includes(ancestor.tagName)) {
369
+ break;
370
+ }
371
+ if (ancestor === this.sidebarContainer) {
372
+ hasSuitableAncestors = true;
373
+ break;
374
+ }
375
+ ancestor = ancestor.parentElement;
376
+ }
377
+
378
+ return !undraggableElementTypes.includes(element.tagName) && hasSuitableAncestors;
379
+ };
380
+
381
+ const clickThreshold = 5;
382
+
383
+ // Returns whether the current (or if no current, **the last**) gesture is roughly a click.
384
+ // Because this can be called **after** a gesture has just ended, it should not require
385
+ // the gesture to be in progress.
386
+ const isRoughlyClick = () => {
387
+ return Math.hypot(lastX - startX, lastY - startY) < clickThreshold;
388
+ };
389
+
390
+ let startedDragging = false;
391
+
392
+ this.sidebarContainer.addEventListener('pointerdown', event => {
393
+ if (event.defaultPrevented || !isDraggableElement(event.target as HTMLElement)) {
394
+ return;
395
+ }
396
+
397
+ if (event.isPrimary) {
398
+ startedDragging = false;
399
+
400
+ lastX = event.clientX;
401
+ lastY = event.clientY;
402
+
403
+ startX = event.clientX;
404
+ startY = event.clientY;
405
+
406
+ capturedPointerId = null;
407
+ pointerDown = true;
408
+ }
409
+ }, { passive: true });
410
+
411
+ let gestureEndTimestamp = 0;
412
+ const onGestureEnd = (_event: Event) => {
413
+ // If the pointerup/pointercancel event was for a pointer not being tracked,
414
+ if (!pointerDown) {
415
+ return;
416
+ }
417
+
418
+ gestureEndTimestamp = performance.now();
419
+
420
+ if (capturedPointerId !== null) {
421
+ this.sidebarContainer.releasePointerCapture(capturedPointerId);
422
+ capturedPointerId = null;
423
+ }
424
+
425
+ this.finalizeDrag();
426
+ pointerDown = false;
427
+ startedDragging = false;
428
+ };
429
+
430
+ this.sidebarContainer.onpointermove = event => {
431
+ if (!event.isPrimary || !pointerDown) {
432
+ return undefined;
433
+ }
434
+
435
+ // Mouse event and no buttons pressed? Cancel the event.
436
+ // This can happen if the event was canceled by a focus change (e.g. by opening a
437
+ // right-click menu).
438
+ if (event.pointerType === 'mouse' && event.buttons === 0) {
439
+ onGestureEnd(event);
440
+ return undefined;
441
+ }
442
+
443
+ // Only capture after motion -- capturing early prevents click events in Chrome.
444
+ if (capturedPointerId === null && !isRoughlyClick()) {
445
+ this.sidebarContainer.setPointerCapture(event.pointerId);
446
+ capturedPointerId = event.pointerId;
447
+ }
448
+
449
+ const x = event.clientX;
450
+ const y = event.clientY;
451
+ const dx = x - lastX;
452
+ const dy = y - lastY;
453
+
454
+ if (Math.abs(y - startY) > clickThreshold || startedDragging) {
455
+ this.handleDrag(dx, dy);
456
+
457
+ lastX = x;
458
+ lastY = y;
459
+
460
+ startedDragging = true;
461
+ }
462
+ };
463
+
464
+ this.sidebarContainer.onpointerleave = event => {{
465
+ // Capture the pointer if it exits the container while dragging.
466
+ if (capturedPointerId === null && pointerDown && event.isPrimary) {
467
+ this.sidebarContainer.setPointerCapture(event.pointerId);
468
+ capturedPointerId = event.pointerId;
469
+ }
470
+ }};
471
+
472
+ this.closeButton.onclick = () => {
473
+ const wasJustDragging = performance.now() - gestureEndTimestamp < 100;
474
+ const roughlyClick = isRoughlyClick();
475
+
476
+ // Ignore the click event if it was caused by dragging the button.
477
+ if (wasJustDragging && roughlyClick || !wasJustDragging) {
478
+ this.sidebarVisible.set(false);
479
+ }
480
+ };
481
+
482
+ this.sidebarContainer.onpointerup = onGestureEnd;
483
+ this.sidebarContainer.onpointercancel = onGestureEnd;
484
+ }
485
+
486
+ /**
487
+ * Updates the position of this menu **during** a drag. After a drag ends,
488
+ * {@link finalizeDrag} should be called.
489
+ */
490
+ private handleDrag(_deltaX: number, deltaY: number) {
491
+ this.sidebarContainer.style.transition = 'none';
492
+ this.sidebarY.set(this.sidebarY.get() + deltaY);
493
+ }
494
+
495
+ /** Returns `this.sidebarY` rounded to a valid value. */
496
+ private snappedSidebarY(sidebarY?: number) {
497
+ const y = sidebarY ?? this.sidebarY.get();
498
+
499
+ const snapYs = [ -100, 0 ];
500
+
501
+ // Allow some amount of scrolling if the sidebar is too tall to fit entirely
502
+ // in the window.
503
+ if (this.sidebarContainer.clientHeight > window.innerHeight) {
504
+ snapYs.push(100);
505
+ }
506
+
507
+ let closestSnap = snapYs[0];
508
+ for (const snapY of snapYs) {
509
+ if (Math.abs(snapY - y) < Math.abs(closestSnap - y)) {
510
+ closestSnap = snapY;
511
+ }
512
+ }
513
+
514
+ return closestSnap;
515
+ }
516
+
517
+ /**
518
+ * Moves the menu to a valid location or closes it, depending on
519
+ * the position set by the drag.
520
+ */
521
+ private finalizeDrag() {
522
+ this.sidebarContainer.style.transition = '';
523
+ if (this.sidebarY.get() > this.sidebarContainer.clientHeight / 2) {
524
+ this.sidebarVisible.set(false);
525
+ } else {
526
+ // Snap to the closest valid Y.
527
+ this.sidebarY.set(this.snappedSidebarY());
528
+ }
529
+ }
530
+
531
+ protected override serializeInternal() {
532
+ return {
533
+ menuSizeY: this.snappedSidebarY(),
534
+ };
535
+ }
536
+
537
+ protected override deserializeInternal(json: any) {
538
+ if (typeof json === 'object' && typeof json['menuSizeY'] === 'number') {
539
+ // Load the y-position of the sidebar -- call snappedSidebarY to ensure validity.
540
+ this.sidebarY.set(this.snappedSidebarY(json['menuSizeY']));
541
+ }
542
+ }
543
+ }