js-draw 1.14.0 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. package/dist/Editor.css +288 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.js +5 -0
  5. package/dist/cjs/components/util/StrokeSmoother.js +11 -4
  6. package/dist/cjs/rendering/caching/CacheRecordManager.js +1 -1
  7. package/dist/cjs/rendering/renderers/CanvasRenderer.js +1 -1
  8. package/dist/cjs/testing/sendHtmlSwipe.d.ts +4 -0
  9. package/dist/cjs/testing/sendHtmlSwipe.js +14 -0
  10. package/dist/cjs/toolbar/EdgeToolbar.d.ts +1 -0
  11. package/dist/cjs/toolbar/EdgeToolbar.js +30 -110
  12. package/dist/cjs/toolbar/IconProvider.d.ts +1 -0
  13. package/dist/cjs/toolbar/IconProvider.js +27 -0
  14. package/dist/cjs/toolbar/localization.d.ts +28 -1
  15. package/dist/cjs/toolbar/localization.js +30 -1
  16. package/dist/cjs/toolbar/utils/HelpDisplay.d.ts +37 -0
  17. package/dist/cjs/toolbar/utils/HelpDisplay.js +442 -0
  18. package/dist/cjs/toolbar/utils/HelpDisplay.test.d.ts +1 -0
  19. package/dist/cjs/toolbar/utils/localization.d.ts +9 -0
  20. package/dist/cjs/toolbar/utils/localization.js +11 -0
  21. package/dist/cjs/toolbar/utils/makeDraggable.d.ts +16 -0
  22. package/dist/cjs/toolbar/utils/makeDraggable.js +130 -0
  23. package/dist/cjs/toolbar/widgets/ActionButtonWidget.d.ts +7 -0
  24. package/dist/cjs/toolbar/widgets/ActionButtonWidget.js +14 -2
  25. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +8 -1
  26. package/dist/cjs/toolbar/widgets/BaseWidget.js +25 -3
  27. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.d.ts +3 -1
  28. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +19 -4
  29. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +3 -1
  30. package/dist/cjs/toolbar/widgets/HandToolWidget.js +19 -7
  31. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +1 -0
  32. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +4 -2
  33. package/dist/cjs/toolbar/widgets/PenToolWidget.js +27 -8
  34. package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +3 -1
  35. package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +19 -5
  36. package/dist/cjs/toolbar/widgets/components/makeColorInput.d.ts +2 -0
  37. package/dist/cjs/toolbar/widgets/components/makeColorInput.js +17 -7
  38. package/dist/cjs/toolbar/widgets/components/makeGridSelector.d.ts +6 -0
  39. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +3 -0
  40. package/dist/cjs/tools/FindTool.js +18 -5
  41. package/dist/cjs/tools/PanZoom.d.ts +8 -2
  42. package/dist/cjs/tools/PanZoom.js +29 -10
  43. package/dist/cjs/tools/SelectionTool/Selection.js +16 -2
  44. package/dist/cjs/util/addLongPressOrHoverCssClasses.d.ts +3 -1
  45. package/dist/cjs/util/addLongPressOrHoverCssClasses.js +2 -1
  46. package/dist/cjs/util/cloneElementWithStyles.d.ts +6 -0
  47. package/dist/cjs/util/cloneElementWithStyles.js +32 -0
  48. package/dist/cjs/version.js +1 -1
  49. package/dist/mjs/Editor.mjs +5 -0
  50. package/dist/mjs/components/util/StrokeSmoother.mjs +11 -4
  51. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +1 -1
  52. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +1 -1
  53. package/dist/mjs/testing/sendHtmlSwipe.d.ts +4 -0
  54. package/dist/mjs/testing/sendHtmlSwipe.mjs +12 -0
  55. package/dist/mjs/toolbar/EdgeToolbar.d.ts +1 -0
  56. package/dist/mjs/toolbar/EdgeToolbar.mjs +30 -110
  57. package/dist/mjs/toolbar/IconProvider.d.ts +1 -0
  58. package/dist/mjs/toolbar/IconProvider.mjs +27 -0
  59. package/dist/mjs/toolbar/localization.d.ts +28 -1
  60. package/dist/mjs/toolbar/localization.mjs +30 -1
  61. package/dist/mjs/toolbar/utils/HelpDisplay.d.ts +37 -0
  62. package/dist/mjs/toolbar/utils/HelpDisplay.mjs +437 -0
  63. package/dist/mjs/toolbar/utils/HelpDisplay.test.d.ts +1 -0
  64. package/dist/mjs/toolbar/utils/localization.d.ts +9 -0
  65. package/dist/mjs/toolbar/utils/localization.mjs +8 -0
  66. package/dist/mjs/toolbar/utils/makeDraggable.d.ts +16 -0
  67. package/dist/mjs/toolbar/utils/makeDraggable.mjs +128 -0
  68. package/dist/mjs/toolbar/widgets/ActionButtonWidget.d.ts +7 -0
  69. package/dist/mjs/toolbar/widgets/ActionButtonWidget.mjs +14 -2
  70. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +8 -1
  71. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +25 -3
  72. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.d.ts +3 -1
  73. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +19 -4
  74. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +3 -1
  75. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +19 -7
  76. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +1 -0
  77. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +4 -2
  78. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +27 -8
  79. package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +3 -1
  80. package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +19 -5
  81. package/dist/mjs/toolbar/widgets/components/makeColorInput.d.ts +2 -0
  82. package/dist/mjs/toolbar/widgets/components/makeColorInput.mjs +17 -7
  83. package/dist/mjs/toolbar/widgets/components/makeGridSelector.d.ts +6 -0
  84. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +3 -0
  85. package/dist/mjs/tools/FindTool.mjs +18 -5
  86. package/dist/mjs/tools/PanZoom.d.ts +8 -2
  87. package/dist/mjs/tools/PanZoom.mjs +29 -10
  88. package/dist/mjs/tools/SelectionTool/Selection.mjs +16 -2
  89. package/dist/mjs/util/addLongPressOrHoverCssClasses.d.ts +3 -1
  90. package/dist/mjs/util/addLongPressOrHoverCssClasses.mjs +2 -1
  91. package/dist/mjs/util/cloneElementWithStyles.d.ts +6 -0
  92. package/dist/mjs/util/cloneElementWithStyles.mjs +30 -0
  93. package/dist/mjs/version.mjs +1 -1
  94. package/package.json +3 -3
  95. package/src/toolbar/EdgeToolbar.scss +23 -2
  96. package/src/toolbar/toolbar.scss +2 -0
  97. package/src/toolbar/utils/HelpDisplay.scss +315 -0
  98. package/src/toolbar/widgets/components/makeColorInput.scss +7 -0
  99. package/src/tools/SelectionTool/SelectionTool.scss +4 -0
@@ -0,0 +1,437 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _HelpDisplay_helpData;
7
+ import { Rect2 } from '@js-draw/math';
8
+ import makeDraggable from './makeDraggable.mjs';
9
+ import { MutableReactiveValue } from '../../util/ReactiveValue.mjs';
10
+ import cloneElementWithStyles from '../../util/cloneElementWithStyles.mjs';
11
+ import addLongPressOrHoverCssClasses from '../../util/addLongPressOrHoverCssClasses.mjs';
12
+ /**
13
+ * Creates the main content of the help overlay.
14
+ *
15
+ * Shows the label for a `HelpRecord` and a highlighted copy
16
+ * of that label's `targetElements`.
17
+ */
18
+ const createHelpPage = (helpItems, onItemClick, onBackgroundClick, context) => {
19
+ const container = document.createElement('div');
20
+ container.classList.add('help-page-container');
21
+ const textLabel = document.createElement('div');
22
+ textLabel.classList.add('label', '-space-above');
23
+ textLabel.setAttribute('aria-live', 'polite');
24
+ // The current active item in helpItems.
25
+ // (Only one item is active at a time, but each item can have multiple HTMLElements).
26
+ let currentItemIndex = 0;
27
+ let currentItem = helpItems[0] ?? null;
28
+ // Each help item can have multiple associated elements. We store clones of each
29
+ // of these elements in their own container.
30
+ //
31
+ // clonedElementContainers maps from help item indicies to **arrays** of containers.
32
+ //
33
+ // For example, clonedElementContainers would be
34
+ // [ [ Container1, Container2 ], [ Container3 ], [ Container4 ]]
35
+ // ↑ ↑ ↑
36
+ // HelpItem 1 HelpItem 2 HelpItem 3
37
+ // if the first help item had two elements (and thus two cloned element containers).
38
+ //
39
+ // We also store the original bounding box -- the bounding box of the clones can change
40
+ // while dragging to switch pages.
41
+ let clonedElementContainers = [];
42
+ // Clicking on the background of the help area should send an event (e.g. to allow the
43
+ // help container to be closed).
44
+ container.addEventListener('click', event => {
45
+ // If clicking directly on the container (and not on a child)
46
+ if (event.target === container) {
47
+ onBackgroundClick();
48
+ }
49
+ });
50
+ // Returns the combined bounding box of all elements associated with the currentItem
51
+ // (all active help items).
52
+ const getCombinedBBox = () => {
53
+ if (!currentItem) {
54
+ return Rect2.empty;
55
+ }
56
+ const itemBoundingBoxes = currentItem.targetElements.map(element => Rect2.of(element.getBoundingClientRect()));
57
+ return Rect2.union(...itemBoundingBoxes);
58
+ };
59
+ // Updates each cloned element's click listener and CSS classes based on whether
60
+ // that element is the current focused element.
61
+ const updateClonedElementStates = () => {
62
+ const currentItemBBox = getCombinedBBox();
63
+ for (let index = 0; index < clonedElementContainers.length; index++) {
64
+ for (const { container, bbox: containerBBox } of clonedElementContainers[index]) {
65
+ if (index === currentItemIndex) {
66
+ container.classList.add('-active');
67
+ container.classList.remove('-clickable', '-background');
68
+ container.onclick = () => { };
69
+ }
70
+ // Otherwise, if not containing the current element
71
+ else {
72
+ if (!containerBBox.containsRect(currentItemBBox)) {
73
+ container.classList.add('-clickable');
74
+ container.classList.remove('-active', '-background');
75
+ }
76
+ else {
77
+ container.classList.add('-background');
78
+ container.classList.remove('-active', '-clickable');
79
+ }
80
+ const containerIndex = index;
81
+ container.onclick = () => {
82
+ onItemClick(containerIndex);
83
+ };
84
+ }
85
+ }
86
+ }
87
+ };
88
+ // Ensures that the item label doesn't overlap the current help item's cloned element.
89
+ const updateLabelPosition = () => {
90
+ const labelBBox = Rect2.of(textLabel.getBoundingClientRect());
91
+ const combinedBBox = getCombinedBBox();
92
+ if (labelBBox.intersects(combinedBBox)) {
93
+ const containerBBox = Rect2.of(container.getBoundingClientRect());
94
+ const spaceAboveCombined = combinedBBox.topLeft.y;
95
+ const spaceBelowCombined = containerBBox.bottomLeft.y - combinedBBox.bottomLeft.y;
96
+ if (spaceAboveCombined > spaceBelowCombined && spaceAboveCombined > labelBBox.height / 3) {
97
+ // Push to the very top
98
+ textLabel.classList.remove('-small-space-above', '-large-space-above');
99
+ textLabel.classList.add('-large-space-below');
100
+ }
101
+ if (spaceAboveCombined < spaceBelowCombined && spaceBelowCombined > labelBBox.height) {
102
+ // Push to the very bottom
103
+ textLabel.classList.add('-large-space-above');
104
+ textLabel.classList.remove('-large-space-below');
105
+ }
106
+ }
107
+ };
108
+ const refreshContent = () => {
109
+ container.replaceChildren();
110
+ // Add the text label first so that screen readers will visit it first.
111
+ textLabel.classList.remove('-large-space-above');
112
+ textLabel.classList.add('-small-space-above', '-large-space-below');
113
+ container.appendChild(textLabel);
114
+ const screenBBox = new Rect2(0, 0, window.innerWidth, window.innerHeight);
115
+ clonedElementContainers = [];
116
+ for (let itemIndex = 0; itemIndex < helpItems.length; itemIndex++) {
117
+ const item = helpItems[itemIndex];
118
+ const itemCloneContainers = [];
119
+ for (const targetElement of item.targetElements) {
120
+ let targetBBox = Rect2.of(targetElement.getBoundingClientRect());
121
+ // Move the element onto the screen if not visible
122
+ if (!screenBBox.intersects(targetBBox)) {
123
+ const screenBottomCenter = screenBBox.bottomLeft.lerp(screenBBox.bottomRight, 0.5);
124
+ const targetBottomCenter = targetBBox.bottomLeft.lerp(targetBBox.bottomRight, 0.5);
125
+ const delta = screenBottomCenter.minus(targetBottomCenter);
126
+ targetBBox = targetBBox.translatedBy(delta);
127
+ }
128
+ const clonedElement = cloneElementWithStyles(targetElement);
129
+ // Interacting with the clone won't trigger event listeners, so disable
130
+ // all inputs.
131
+ for (const input of clonedElement.querySelectorAll('input')) {
132
+ input.disabled = true;
133
+ }
134
+ clonedElement.style.margin = '0';
135
+ const clonedElementContainer = document.createElement('div');
136
+ clonedElementContainer.classList.add('cloned-element-container');
137
+ clonedElementContainer.style.position = 'absolute';
138
+ clonedElementContainer.style.left = `${targetBBox.topLeft.x}px`;
139
+ clonedElementContainer.style.top = `${targetBBox.topLeft.y}px`;
140
+ clonedElementContainer.replaceChildren(clonedElement);
141
+ addLongPressOrHoverCssClasses(clonedElementContainer, { timeout: 0 });
142
+ itemCloneContainers.push({ container: clonedElementContainer, bbox: targetBBox });
143
+ container.appendChild(clonedElementContainer);
144
+ }
145
+ clonedElementContainers.push(itemCloneContainers);
146
+ }
147
+ updateClonedElementStates();
148
+ };
149
+ const refresh = () => {
150
+ refreshContent();
151
+ updateLabelPosition();
152
+ };
153
+ const onItemChange = () => {
154
+ const helpTextElement = document.createElement('div');
155
+ helpTextElement.innerText = currentItem?.helpText ?? '';
156
+ // For tests
157
+ helpTextElement.classList.add('current-item-help');
158
+ const navigationHelpElement = document.createElement('div');
159
+ navigationHelpElement.innerText = context.localization.helpScreenNavigationHelp;
160
+ navigationHelpElement.classList.add('navigation-help');
161
+ textLabel.replaceChildren(helpTextElement, ...(currentItemIndex === 0 ? [navigationHelpElement] : []));
162
+ updateClonedElementStates();
163
+ };
164
+ onItemChange();
165
+ return {
166
+ addToParent: (parent) => {
167
+ refreshContent();
168
+ parent.appendChild(container);
169
+ updateLabelPosition();
170
+ },
171
+ refresh,
172
+ setPageIndex: (pageIndex) => {
173
+ currentItemIndex = pageIndex;
174
+ currentItem = helpItems[pageIndex];
175
+ onItemChange();
176
+ },
177
+ };
178
+ };
179
+ /**
180
+ * Creates and manages an overlay that shows help text for a set of
181
+ * `HTMLElement`s.
182
+ *
183
+ * @see {@link BaseWidget.fillDropdown}.
184
+ */
185
+ class HelpDisplay {
186
+ /** Constructed internally by BaseWidget. @internal */
187
+ constructor(createOverlay, context) {
188
+ this.createOverlay = createOverlay;
189
+ this.context = context;
190
+ _HelpDisplay_helpData.set(this, []);
191
+ }
192
+ /** @internal */
193
+ showHelpOverlay() {
194
+ const overlay = document.createElement('dialog');
195
+ overlay.setAttribute('autofocus', 'true');
196
+ overlay.classList.add('toolbar-help-overlay');
197
+ // Closes the overlay with a closing animation
198
+ const closing = false;
199
+ const closeOverlay = () => {
200
+ if (closing)
201
+ return;
202
+ // If changing animationDelay, be sure to also update the CSS.
203
+ const animationDelay = 250; // ms
204
+ overlay.classList.add('-hiding');
205
+ setTimeout(() => overlay.close(), animationDelay);
206
+ };
207
+ let lastDragTimestamp = 0;
208
+ const onBackgroundClick = () => {
209
+ const wasJustDragging = performance.now() - lastDragTimestamp < 100;
210
+ if (!wasJustDragging) {
211
+ closeOverlay();
212
+ }
213
+ };
214
+ const makeCloseButton = () => {
215
+ const closeButton = document.createElement('button');
216
+ closeButton.classList.add('close-button');
217
+ closeButton.appendChild(this.context.icons.makeCloseIcon());
218
+ const label = this.context.localization.close;
219
+ closeButton.setAttribute('aria-label', label);
220
+ closeButton.setAttribute('title', label);
221
+ closeButton.onclick = () => { closeOverlay(); };
222
+ return closeButton;
223
+ };
224
+ // Wraps the label and clickable help elements
225
+ const makeNavigationContent = () => {
226
+ const currentPage = MutableReactiveValue.fromInitialValue(0);
227
+ const content = document.createElement('div');
228
+ content.classList.add('navigation-content');
229
+ const helpPage = createHelpPage(__classPrivateFieldGet(this, _HelpDisplay_helpData, "f"), newPageIndex => currentPage.set(newPageIndex), onBackgroundClick, this.context);
230
+ helpPage.addToParent(content);
231
+ const showPage = (pageIndex) => {
232
+ if (pageIndex >= __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").length || pageIndex < 0) {
233
+ // Hide if out of bounds
234
+ console.warn('Help screen: Navigated to out-of-bounds page', pageIndex);
235
+ content.style.display = 'none';
236
+ }
237
+ else {
238
+ content.style.display = '';
239
+ helpPage.setPageIndex(pageIndex);
240
+ }
241
+ };
242
+ currentPage.onUpdateAndNow(showPage);
243
+ const navigationControl = {
244
+ content,
245
+ currentPage,
246
+ toNext: () => {
247
+ if (navigationControl.hasNext()) {
248
+ currentPage.set(currentPage.get() + 1);
249
+ }
250
+ },
251
+ toPrevious: () => {
252
+ if (navigationControl.hasPrevious()) {
253
+ currentPage.set(currentPage.get() - 1);
254
+ }
255
+ },
256
+ hasNext: () => {
257
+ return currentPage.get() + 1 < __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").length;
258
+ },
259
+ hasPrevious: () => {
260
+ return currentPage.get() > 0;
261
+ },
262
+ refreshCurrent: () => {
263
+ helpPage.refresh();
264
+ },
265
+ };
266
+ return navigationControl;
267
+ };
268
+ // Creates next/previous buttons.
269
+ const makeNavigationButtons = (navigation) => {
270
+ const navigationButtonContainer = document.createElement('div');
271
+ navigationButtonContainer.classList.add('navigation-buttons');
272
+ const nextButton = document.createElement('button');
273
+ const previousButton = document.createElement('button');
274
+ nextButton.innerText = this.context.localization.next;
275
+ previousButton.innerText = this.context.localization.previous;
276
+ nextButton.classList.add('next');
277
+ previousButton.classList.add('previous');
278
+ const updateButtonVisibility = () => {
279
+ navigationButtonContainer.classList.remove('-has-next', '-has-previous');
280
+ if (navigation.hasNext()) {
281
+ navigationButtonContainer.classList.add('-has-next');
282
+ nextButton.disabled = false;
283
+ }
284
+ else {
285
+ navigationButtonContainer.classList.remove('-has-next');
286
+ nextButton.disabled = true;
287
+ }
288
+ if (navigation.hasPrevious()) {
289
+ navigationButtonContainer.classList.add('-has-previous');
290
+ previousButton.disabled = false;
291
+ }
292
+ else {
293
+ navigationButtonContainer.classList.remove('-has-previous');
294
+ previousButton.disabled = true;
295
+ }
296
+ };
297
+ navigation.currentPage.onUpdateAndNow(updateButtonVisibility);
298
+ nextButton.onclick = () => {
299
+ navigation.toNext();
300
+ };
301
+ previousButton.onclick = () => {
302
+ navigation.toPrevious();
303
+ };
304
+ navigationButtonContainer.replaceChildren(previousButton, nextButton);
305
+ return navigationButtonContainer;
306
+ };
307
+ const navigation = makeNavigationContent();
308
+ const navigationButtons = makeNavigationButtons(navigation);
309
+ overlay.replaceChildren(makeCloseButton(), navigationButtons, navigation.content);
310
+ this.createOverlay(overlay);
311
+ overlay.showModal();
312
+ const minDragOffsetToTransition = 30;
313
+ const setDragOffset = (offset) => {
314
+ if (offset > 0 && !navigation.hasPrevious()) {
315
+ offset = 0;
316
+ }
317
+ if (offset < 0 && !navigation.hasNext()) {
318
+ offset = 0;
319
+ }
320
+ // Clamp offset
321
+ if (offset > minDragOffsetToTransition || offset < -minDragOffsetToTransition) {
322
+ offset = minDragOffsetToTransition * Math.sign(offset);
323
+ }
324
+ overlay.style.transform = `translate(${offset}px, 0px)`;
325
+ if (offset >= minDragOffsetToTransition) {
326
+ navigationButtons.classList.add('-highlight-previous');
327
+ }
328
+ else {
329
+ navigationButtons.classList.remove('-highlight-previous');
330
+ }
331
+ if (offset <= -minDragOffsetToTransition) {
332
+ navigationButtons.classList.add('-highlight-next');
333
+ }
334
+ else {
335
+ navigationButtons.classList.remove('-highlight-next');
336
+ }
337
+ };
338
+ // Listeners
339
+ const dragListener = makeDraggable(overlay, {
340
+ draggableChildElements: [navigation.content],
341
+ onDrag: (_deltaX, _deltaY, totalDisplacement) => {
342
+ overlay.classList.add('-dragging');
343
+ setDragOffset(totalDisplacement.x);
344
+ },
345
+ onDragEnd: (dragStatistics) => {
346
+ overlay.classList.remove('-dragging');
347
+ setDragOffset(0);
348
+ if (!dragStatistics.roughlyClick) {
349
+ const xDisplacement = dragStatistics.displacement.x;
350
+ if (xDisplacement > minDragOffsetToTransition) {
351
+ navigation.toPrevious();
352
+ }
353
+ else if (xDisplacement < -minDragOffsetToTransition) {
354
+ navigation.toNext();
355
+ }
356
+ lastDragTimestamp = dragStatistics.endTimestamp;
357
+ }
358
+ },
359
+ });
360
+ let resizeObserver;
361
+ if (window.ResizeObserver) {
362
+ resizeObserver = new ResizeObserver(() => {
363
+ navigation.refreshCurrent();
364
+ });
365
+ resizeObserver.observe(overlay);
366
+ }
367
+ const onMediaChangeListener = () => {
368
+ // Refresh the cloned elements and their styles after a delay.
369
+ // This is necessary because styles are cloned, in addition to elements.
370
+ requestAnimationFrame(() => navigation.refreshCurrent());
371
+ };
372
+ // matchMedia is unsupported by jsdom
373
+ const mediaQueryList = window.matchMedia?.('(prefers-color-scheme: dark)');
374
+ mediaQueryList?.addEventListener('change', onMediaChangeListener);
375
+ // Close the overlay when clicking on the background (*directly* on any of the
376
+ // elements in closeOverlayTriggers).
377
+ const closeOverlayTriggers = [navigation.content, navigationButtons, overlay];
378
+ overlay.onclick = event => {
379
+ if (closeOverlayTriggers.includes(event.target)) {
380
+ onBackgroundClick();
381
+ }
382
+ };
383
+ overlay.onkeyup = event => {
384
+ if (event.code === 'Escape') {
385
+ closeOverlay();
386
+ event.preventDefault();
387
+ }
388
+ else if (event.code === 'ArrowRight') {
389
+ navigation.toNext();
390
+ event.preventDefault();
391
+ }
392
+ else if (event.code === 'ArrowLeft') {
393
+ navigation.toPrevious();
394
+ event.preventDefault();
395
+ }
396
+ };
397
+ overlay.addEventListener('close', () => {
398
+ this.context.announceForAccessibility(this.context.localization.helpHidden);
399
+ mediaQueryList?.removeEventListener('change', onMediaChangeListener);
400
+ dragListener.removeListeners();
401
+ resizeObserver?.disconnect();
402
+ overlay.remove();
403
+ });
404
+ }
405
+ /** Marks `helpText` as associated with a single `targetElement`. */
406
+ registerTextHelpForElement(targetElement, helpText) {
407
+ this.registerTextHelpForElements([targetElement], helpText);
408
+ }
409
+ /** Marks `helpText` as associated with all elements in `targetElements`. */
410
+ registerTextHelpForElements(targetElements, helpText) {
411
+ __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").push({ targetElements: [...targetElements], helpText });
412
+ }
413
+ /** Returns true if any help text has been registered. */
414
+ hasHelpText() {
415
+ return __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").length > 0;
416
+ }
417
+ /**
418
+ * Creates and returns a button that toggles the help display.
419
+ */
420
+ createToggleButton() {
421
+ const buttonContainer = document.createElement('div');
422
+ buttonContainer.classList.add('toolbar-help-overlay-button');
423
+ const helpButton = document.createElement('button');
424
+ helpButton.classList.add('button');
425
+ const icon = this.context.icons.makeHelpIcon();
426
+ icon.classList.add('icon');
427
+ helpButton.appendChild(icon);
428
+ helpButton.setAttribute('aria-label', this.context.localization.help);
429
+ helpButton.onclick = () => {
430
+ this.showHelpOverlay();
431
+ };
432
+ buttonContainer.appendChild(helpButton);
433
+ return buttonContainer;
434
+ }
435
+ }
436
+ _HelpDisplay_helpData = new WeakMap();
437
+ export default HelpDisplay;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export interface ToolbarUtilsLocalization {
2
+ help: string;
3
+ helpScreenNavigationHelp: string;
4
+ helpHidden: string;
5
+ next: string;
6
+ previous: string;
7
+ close: string;
8
+ }
9
+ export declare const defaultToolbarUtilsLocalization: ToolbarUtilsLocalization;
@@ -0,0 +1,8 @@
1
+ export const defaultToolbarUtilsLocalization = {
2
+ help: 'Help',
3
+ helpHidden: 'Help hidden',
4
+ next: 'Next',
5
+ previous: 'Previous',
6
+ close: 'Close',
7
+ helpScreenNavigationHelp: 'Click on a control for more information.',
8
+ };
@@ -0,0 +1,16 @@
1
+ import { Vec2 } from '@js-draw/math';
2
+ interface DragStatistics {
3
+ roughlyClick: boolean;
4
+ endTimestamp: number;
5
+ displacement: Vec2;
6
+ }
7
+ interface DraggableOptions {
8
+ draggableChildElements: HTMLElement[];
9
+ onDrag(deltaX: number, deltaY: number, totalDisplacement: Vec2): void;
10
+ onDragEnd(dragStatistics: DragStatistics): void;
11
+ }
12
+ export interface DragControl {
13
+ removeListeners(): void;
14
+ }
15
+ declare const makeDraggable: (dragElement: HTMLElement, options: DraggableOptions) => DragControl;
16
+ export default makeDraggable;
@@ -0,0 +1,128 @@
1
+ import { Vec2 } from '@js-draw/math';
2
+ const makeDraggable = (dragElement, options) => {
3
+ const dragElements = [
4
+ ...options.draggableChildElements,
5
+ dragElement,
6
+ ];
7
+ let lastX = 0;
8
+ let lastY = 0;
9
+ let startX = 0;
10
+ let startY = 0;
11
+ let pointerDown = false;
12
+ let capturedPointerId = null;
13
+ const isDraggableElement = (element) => {
14
+ if (!element) {
15
+ return false;
16
+ }
17
+ if (dragElements.includes(element)) {
18
+ return true;
19
+ }
20
+ // Some inputs handle dragging themselves. Don't also interpret such gestures
21
+ // as dragging the dropdown.
22
+ const undraggableElementTypes = ['INPUT', 'SELECT', 'IMG'];
23
+ let hasSuitableAncestors = false;
24
+ let ancestor = element.parentElement;
25
+ while (ancestor) {
26
+ if (undraggableElementTypes.includes(ancestor.tagName)) {
27
+ break;
28
+ }
29
+ if (dragElements.includes(ancestor)) {
30
+ hasSuitableAncestors = true;
31
+ break;
32
+ }
33
+ ancestor = ancestor.parentElement;
34
+ }
35
+ return !undraggableElementTypes.includes(element.tagName) && hasSuitableAncestors;
36
+ };
37
+ const removeEventListenerCallbacks = [];
38
+ const addEventListener = (listenerType, listener, options) => {
39
+ dragElement.addEventListener(listenerType, listener, options);
40
+ removeEventListenerCallbacks.push(() => {
41
+ dragElement.removeEventListener(listenerType, listener);
42
+ });
43
+ };
44
+ const clickThreshold = 5;
45
+ // Returns whether the current (or if no current, **the last**) gesture is roughly a click.
46
+ // Because this can be called **after** a gesture has just ended, it should not require
47
+ // the gesture to be in progress.
48
+ const isRoughlyClick = () => {
49
+ return Math.hypot(lastX - startX, lastY - startY) < clickThreshold;
50
+ };
51
+ let startedDragging = false;
52
+ addEventListener('pointerdown', event => {
53
+ if (event.defaultPrevented || !isDraggableElement(event.target)) {
54
+ return;
55
+ }
56
+ if (event.isPrimary) {
57
+ startedDragging = false;
58
+ lastX = event.clientX;
59
+ lastY = event.clientY;
60
+ startX = event.clientX;
61
+ startY = event.clientY;
62
+ capturedPointerId = null;
63
+ pointerDown = true;
64
+ }
65
+ }, { passive: true });
66
+ const onGestureEnd = (_event) => {
67
+ // If the pointerup/pointercancel event was for a pointer not being tracked,
68
+ if (!pointerDown) {
69
+ return;
70
+ }
71
+ if (capturedPointerId !== null) {
72
+ dragElement.releasePointerCapture(capturedPointerId);
73
+ capturedPointerId = null;
74
+ }
75
+ options.onDragEnd({
76
+ roughlyClick: isRoughlyClick(),
77
+ endTimestamp: performance.now(),
78
+ displacement: Vec2.of(lastX - startX, lastY - startY),
79
+ });
80
+ pointerDown = false;
81
+ startedDragging = false;
82
+ };
83
+ addEventListener('pointermove', event => {
84
+ if (!event.isPrimary || !pointerDown) {
85
+ return undefined;
86
+ }
87
+ // Mouse event and no buttons pressed? Cancel the event.
88
+ // This can happen if the event was canceled by a focus change (e.g. by opening a
89
+ // right-click menu).
90
+ if (event.pointerType === 'mouse' && event.buttons === 0) {
91
+ onGestureEnd(event);
92
+ return undefined;
93
+ }
94
+ // Only capture after motion -- capturing early prevents click events in Chrome.
95
+ if (capturedPointerId === null && !isRoughlyClick()) {
96
+ dragElement.setPointerCapture(event.pointerId);
97
+ capturedPointerId = event.pointerId;
98
+ }
99
+ const x = event.clientX;
100
+ const y = event.clientY;
101
+ const dx = x - lastX;
102
+ const dy = y - lastY;
103
+ const isClick = Math.abs(x - startX) <= clickThreshold && Math.abs(y - startY) <= clickThreshold;
104
+ if (!isClick || startedDragging) {
105
+ options.onDrag(dx, dy, Vec2.of(x - startX, y - startY));
106
+ lastX = x;
107
+ lastY = y;
108
+ startedDragging = true;
109
+ }
110
+ });
111
+ addEventListener('pointerleave', event => {
112
+ // Capture the pointer if it exits the container while dragging.
113
+ if (capturedPointerId === null && pointerDown && event.isPrimary) {
114
+ dragElement.setPointerCapture(event.pointerId);
115
+ capturedPointerId = event.pointerId;
116
+ }
117
+ });
118
+ addEventListener('pointerup', onGestureEnd);
119
+ addEventListener('pointercancel', onGestureEnd);
120
+ return {
121
+ removeListeners: () => {
122
+ for (const removeListenerCallback of removeEventListenerCallbacks) {
123
+ removeListenerCallback();
124
+ }
125
+ },
126
+ };
127
+ };
128
+ export default makeDraggable;
@@ -8,6 +8,13 @@ export default class ActionButtonWidget extends BaseWidget {
8
8
  protected clickAction: () => void;
9
9
  protected mustBeToplevel: boolean;
10
10
  constructor(editor: Editor, id: string, makeIcon: () => Element | null, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization, mustBeToplevel?: boolean, autoDisableInReadOnlyEditors?: boolean);
11
+ /**
12
+ * Sets the text shown in a help overlay for this button.
13
+ *
14
+ * See {@link getHelpText}.
15
+ */
16
+ setHelpText(helpText: string): void;
17
+ protected getHelpText(): string | undefined;
11
18
  protected shouldAutoDisableInReadOnlyEditor(): boolean;
12
19
  protected handleClick(): void;
13
20
  protected getTitle(): string;
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _ActionButtonWidget_autoDisableInReadOnlyEditors;
12
+ var _ActionButtonWidget_autoDisableInReadOnlyEditors, _ActionButtonWidget_helpText;
13
13
  import BaseWidget from './BaseWidget.mjs';
14
14
  class ActionButtonWidget extends BaseWidget {
15
15
  constructor(editor, id, makeIcon, title, clickAction, localizationTable, mustBeToplevel = false, autoDisableInReadOnlyEditors = true) {
@@ -19,8 +19,20 @@ class ActionButtonWidget extends BaseWidget {
19
19
  this.clickAction = clickAction;
20
20
  this.mustBeToplevel = mustBeToplevel;
21
21
  _ActionButtonWidget_autoDisableInReadOnlyEditors.set(this, void 0);
22
+ _ActionButtonWidget_helpText.set(this, undefined);
22
23
  __classPrivateFieldSet(this, _ActionButtonWidget_autoDisableInReadOnlyEditors, autoDisableInReadOnlyEditors, "f");
23
24
  }
25
+ /**
26
+ * Sets the text shown in a help overlay for this button.
27
+ *
28
+ * See {@link getHelpText}.
29
+ */
30
+ setHelpText(helpText) {
31
+ __classPrivateFieldSet(this, _ActionButtonWidget_helpText, helpText, "f");
32
+ }
33
+ getHelpText() {
34
+ return __classPrivateFieldGet(this, _ActionButtonWidget_helpText, "f");
35
+ }
24
36
  shouldAutoDisableInReadOnlyEditor() {
25
37
  return __classPrivateFieldGet(this, _ActionButtonWidget_autoDisableInReadOnlyEditors, "f");
26
38
  }
@@ -40,5 +52,5 @@ class ActionButtonWidget extends BaseWidget {
40
52
  return this.mustBeToplevel;
41
53
  }
42
54
  }
43
- _ActionButtonWidget_autoDisableInReadOnlyEditors = new WeakMap();
55
+ _ActionButtonWidget_autoDisableInReadOnlyEditors = new WeakMap(), _ActionButtonWidget_helpText = new WeakMap();
44
56
  export default ActionButtonWidget;
@@ -2,6 +2,7 @@ import Editor from '../../Editor';
2
2
  import { KeyPressEvent } from '../../inputEvents';
3
3
  import { ToolbarLocalization } from '../localization';
4
4
  import { WidgetContentLayoutManager } from './layout/types';
5
+ import HelpDisplay from '../utils/HelpDisplay';
5
6
  export type SavedToolbuttonState = Record<string, any>;
6
7
  /**
7
8
  * A set of labels that allow toolbar themes to treat buttons differently.
@@ -66,7 +67,13 @@ export default abstract class BaseWidget {
66
67
  getUniqueIdIn(container: Record<string, BaseWidget>): string;
67
68
  protected abstract getTitle(): string;
68
69
  protected abstract createIcon(): Element | null;
69
- protected fillDropdown(dropdown: HTMLElement): boolean;
70
+ protected fillDropdown(dropdown: HTMLElement, helpDisplay?: HelpDisplay): boolean;
71
+ /**
72
+ * Should return a 1-2 sentence description of the widget.
73
+ *
74
+ * At present, this is only used if this widget has an associated dropdown.
75
+ */
76
+ protected getHelpText(): undefined | string;
70
77
  /** @deprecated Renamed to `setUpButtonEventListeners`. */
71
78
  protected setupActionBtnClickListener(button: HTMLElement): void;
72
79
  protected setUpButtonEventListeners(button: HTMLElement): void;