js-draw 1.14.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. package/dist/Editor.css +285 -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/testing/sendHtmlSwipe.d.ts +4 -0
  8. package/dist/cjs/testing/sendHtmlSwipe.js +14 -0
  9. package/dist/cjs/toolbar/EdgeToolbar.d.ts +1 -0
  10. package/dist/cjs/toolbar/EdgeToolbar.js +30 -110
  11. package/dist/cjs/toolbar/IconProvider.d.ts +1 -0
  12. package/dist/cjs/toolbar/IconProvider.js +27 -0
  13. package/dist/cjs/toolbar/localization.d.ts +28 -1
  14. package/dist/cjs/toolbar/localization.js +30 -1
  15. package/dist/cjs/toolbar/utils/HelpDisplay.d.ts +37 -0
  16. package/dist/cjs/toolbar/utils/HelpDisplay.js +442 -0
  17. package/dist/cjs/toolbar/utils/HelpDisplay.test.d.ts +1 -0
  18. package/dist/cjs/toolbar/utils/localization.d.ts +9 -0
  19. package/dist/cjs/toolbar/utils/localization.js +11 -0
  20. package/dist/cjs/toolbar/utils/makeDraggable.d.ts +16 -0
  21. package/dist/cjs/toolbar/utils/makeDraggable.js +130 -0
  22. package/dist/cjs/toolbar/widgets/ActionButtonWidget.d.ts +7 -0
  23. package/dist/cjs/toolbar/widgets/ActionButtonWidget.js +14 -2
  24. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +8 -1
  25. package/dist/cjs/toolbar/widgets/BaseWidget.js +25 -3
  26. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.d.ts +3 -1
  27. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +19 -4
  28. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +3 -1
  29. package/dist/cjs/toolbar/widgets/HandToolWidget.js +19 -7
  30. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +1 -0
  31. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +4 -2
  32. package/dist/cjs/toolbar/widgets/PenToolWidget.js +27 -8
  33. package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +3 -1
  34. package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +19 -5
  35. package/dist/cjs/toolbar/widgets/components/makeColorInput.d.ts +2 -0
  36. package/dist/cjs/toolbar/widgets/components/makeColorInput.js +17 -7
  37. package/dist/cjs/toolbar/widgets/components/makeGridSelector.d.ts +6 -0
  38. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +3 -0
  39. package/dist/cjs/tools/FindTool.js +18 -5
  40. package/dist/cjs/util/addLongPressOrHoverCssClasses.d.ts +3 -1
  41. package/dist/cjs/util/addLongPressOrHoverCssClasses.js +2 -1
  42. package/dist/cjs/util/cloneElementWithStyles.d.ts +6 -0
  43. package/dist/cjs/util/cloneElementWithStyles.js +32 -0
  44. package/dist/cjs/version.js +1 -1
  45. package/dist/mjs/Editor.mjs +5 -0
  46. package/dist/mjs/components/util/StrokeSmoother.mjs +11 -4
  47. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +1 -1
  48. package/dist/mjs/testing/sendHtmlSwipe.d.ts +4 -0
  49. package/dist/mjs/testing/sendHtmlSwipe.mjs +12 -0
  50. package/dist/mjs/toolbar/EdgeToolbar.d.ts +1 -0
  51. package/dist/mjs/toolbar/EdgeToolbar.mjs +30 -110
  52. package/dist/mjs/toolbar/IconProvider.d.ts +1 -0
  53. package/dist/mjs/toolbar/IconProvider.mjs +27 -0
  54. package/dist/mjs/toolbar/localization.d.ts +28 -1
  55. package/dist/mjs/toolbar/localization.mjs +30 -1
  56. package/dist/mjs/toolbar/utils/HelpDisplay.d.ts +37 -0
  57. package/dist/mjs/toolbar/utils/HelpDisplay.mjs +437 -0
  58. package/dist/mjs/toolbar/utils/HelpDisplay.test.d.ts +1 -0
  59. package/dist/mjs/toolbar/utils/localization.d.ts +9 -0
  60. package/dist/mjs/toolbar/utils/localization.mjs +8 -0
  61. package/dist/mjs/toolbar/utils/makeDraggable.d.ts +16 -0
  62. package/dist/mjs/toolbar/utils/makeDraggable.mjs +128 -0
  63. package/dist/mjs/toolbar/widgets/ActionButtonWidget.d.ts +7 -0
  64. package/dist/mjs/toolbar/widgets/ActionButtonWidget.mjs +14 -2
  65. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +8 -1
  66. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +25 -3
  67. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.d.ts +3 -1
  68. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +19 -4
  69. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +3 -1
  70. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +19 -7
  71. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +1 -0
  72. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +4 -2
  73. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +27 -8
  74. package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +3 -1
  75. package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +19 -5
  76. package/dist/mjs/toolbar/widgets/components/makeColorInput.d.ts +2 -0
  77. package/dist/mjs/toolbar/widgets/components/makeColorInput.mjs +17 -7
  78. package/dist/mjs/toolbar/widgets/components/makeGridSelector.d.ts +6 -0
  79. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +3 -0
  80. package/dist/mjs/tools/FindTool.mjs +18 -5
  81. package/dist/mjs/util/addLongPressOrHoverCssClasses.d.ts +3 -1
  82. package/dist/mjs/util/addLongPressOrHoverCssClasses.mjs +2 -1
  83. package/dist/mjs/util/cloneElementWithStyles.d.ts +6 -0
  84. package/dist/mjs/util/cloneElementWithStyles.mjs +30 -0
  85. package/dist/mjs/version.mjs +1 -1
  86. package/package.json +2 -2
  87. package/src/toolbar/EdgeToolbar.scss +23 -2
  88. package/src/toolbar/toolbar.scss +2 -0
  89. package/src/toolbar/utils/HelpDisplay.scss +315 -0
  90. package/src/toolbar/widgets/components/makeColorInput.scss +7 -0
@@ -1,4 +1,5 @@
1
- export interface ToolbarLocalization {
1
+ import { ToolbarUtilsLocalization } from './utils/localization';
2
+ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
2
3
  fontLabel: string;
3
4
  textSize: string;
4
5
  touchPanning: string;
@@ -52,6 +53,32 @@ export interface ToolbarLocalization {
52
53
  inputStabilization: string;
53
54
  strokeAutocorrect: string;
54
55
  errorImageHasZeroSize: string;
56
+ describeTheImage: string;
57
+ penDropdown__baseHelpText: string;
58
+ penDropdown__colorHelpText: string;
59
+ penDropdown__thicknessHelpText: string;
60
+ penDropdown__penTypeHelpText: string;
61
+ penDropdown__autocorrectHelpText: string;
62
+ penDropdown__stabilizationHelpText: string;
63
+ handDropdown__baseHelpText: string;
64
+ handDropdown__zoomDisplayHelpText: string;
65
+ handDropdown__zoomInHelpText: string;
66
+ handDropdown__zoomOutHelpText: string;
67
+ handDropdown__resetViewHelpText: string;
68
+ handDropdown__touchPanningHelpText: string;
69
+ handDropdown__lockRotationHelpText: string;
70
+ selectionDropdown__baseHelpText: string;
71
+ selectionDropdown__resizeToHelpText: string;
72
+ selectionDropdown__deleteHelpText: string;
73
+ selectionDropdown__duplicateHelpText: string;
74
+ selectionDropdown__changeColorHelpText: string;
75
+ pageDropdown__baseHelpText: string;
76
+ pageDropdown__backgroundColorHelpText: string;
77
+ pageDropdown__gridCheckboxHelpText: string;
78
+ pageDropdown__aboutButtonHelpText: string;
79
+ pageDropdown__autoresizeCheckboxHelpText: string;
80
+ colorPickerPipetteHelpText: string;
81
+ colorPickerToggleHelpText: string;
55
82
  closeSidebar: (toolName: string) => string;
56
83
  dropdownShown: (toolName: string) => string;
57
84
  dropdownHidden: (toolName: string) => string;
@@ -1,4 +1,6 @@
1
+ import { defaultToolbarUtilsLocalization } from './utils/localization.mjs';
1
2
  export const defaultToolbarLocalization = {
3
+ ...defaultToolbarUtilsLocalization,
2
4
  pen: 'Pen',
3
5
  eraser: 'Eraser',
4
6
  select: 'Select',
@@ -51,12 +53,39 @@ export const defaultToolbarLocalization = {
51
53
  outlinedCirclePen: 'Outlined circle',
52
54
  lockRotation: 'Lock rotation',
53
55
  paste: 'Paste',
56
+ errorImageHasZeroSize: 'Error: Image has zero size',
57
+ describeTheImage: 'Image description',
58
+ // Help text
59
+ penDropdown__baseHelpText: 'This tool draws shapes or freehand lines.',
60
+ penDropdown__colorHelpText: 'Changes the pen\'s color',
61
+ penDropdown__thicknessHelpText: 'Changes the thickness of strokes drawn by the pen.',
62
+ penDropdown__penTypeHelpText: 'Changes the pen style.\n\nEither a “pen tip” style or “shape” can be chosen. Choosing a “pen tip” style draws freehand lines. Choosing a “shape” draws shapes.',
63
+ penDropdown__autocorrectHelpText: 'Converts approximate freehand lines and rectangles to perfect ones.\n\nThe pen must be held stationary at the end of a stroke to trigger a correction.',
64
+ penDropdown__stabilizationHelpText: 'Draws smoother strokes.\n\nThis also adds a short delay between the mouse/stylus and the stroke.',
65
+ handDropdown__baseHelpText: 'This tool is responsible for scrolling, rotating, and zooming the editor.',
66
+ handDropdown__zoomInHelpText: 'Zooms in.',
67
+ handDropdown__zoomOutHelpText: 'Zooms out.',
68
+ handDropdown__resetViewHelpText: 'Resets the zoom level to 100% and resets scroll.',
69
+ handDropdown__zoomDisplayHelpText: 'Shows the current zoom level. 100% shows the image at its actual size.',
70
+ handDropdown__touchPanningHelpText: 'When enabled, touch gestures move the image rather than select or draw.',
71
+ handDropdown__lockRotationHelpText: 'When enabled, prevents touch gestures from rotating the screen.',
72
+ selectionDropdown__baseHelpText: 'Selects content and manipulates the selection',
73
+ selectionDropdown__resizeToHelpText: 'Crops the drawing to the size of what\'s currently selected.\n\nIf auto-resize is enabled, it will be disabled.',
74
+ selectionDropdown__deleteHelpText: 'Erases selected items.',
75
+ selectionDropdown__duplicateHelpText: 'Makes a copy of selected items.',
76
+ selectionDropdown__changeColorHelpText: 'Changes the color of selected items.',
77
+ pageDropdown__baseHelpText: 'Controls the drawing canvas\' background color, pattern, and size.',
78
+ pageDropdown__backgroundColorHelpText: 'Changes the background color of the drawing canvas.',
79
+ pageDropdown__gridCheckboxHelpText: 'Enables/disables a background grid pattern.',
80
+ pageDropdown__autoresizeCheckboxHelpText: 'When checked, the page grows to fit the drawing.\n\nWhen unchecked, the page is visible and its size can be set manually.',
81
+ pageDropdown__aboutButtonHelpText: 'Shows version, debug, and other information.',
82
+ colorPickerPipetteHelpText: 'Picks a color from the screen.',
83
+ colorPickerToggleHelpText: 'Opens/closes the color picker.',
54
84
  closeSidebar: (toolName) => `Close sidebar for ${toolName}`,
55
85
  dropdownShown: (toolName) => `Menu for ${toolName} shown`,
56
86
  dropdownHidden: (toolName) => `Menu for ${toolName} hidden`,
57
87
  zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
58
88
  colorChangedAnnouncement: (color) => `Color changed to ${color}`,
59
89
  imageSize: (size, units) => `Image size: ${size} ${units}`,
60
- errorImageHasZeroSize: 'Error: Image has zero size',
61
90
  imageLoadError: (message) => `Error loading image: ${message}`,
62
91
  };
@@ -0,0 +1,37 @@
1
+ import { ToolbarContext } from '../types';
2
+ export interface HelpRecord {
3
+ readonly helpText: string;
4
+ /**
5
+ * Elements that have `helpText`. Conceptually, these
6
+ * `HTMLElement`s form a single control (e.g. different radio
7
+ * buttons in a button group).
8
+ *
9
+ * The elements are all shown at once by a `HelpDisplay`.
10
+ */
11
+ readonly targetElements: HTMLElement[];
12
+ }
13
+ /**
14
+ * Creates and manages an overlay that shows help text for a set of
15
+ * `HTMLElement`s.
16
+ *
17
+ * @see {@link BaseWidget.fillDropdown}.
18
+ */
19
+ export default class HelpDisplay {
20
+ #private;
21
+ private createOverlay;
22
+ private context;
23
+ /** Constructed internally by BaseWidget. @internal */
24
+ constructor(createOverlay: (htmlElement: HTMLElement) => void, context: ToolbarContext);
25
+ /** @internal */
26
+ showHelpOverlay(): void;
27
+ /** Marks `helpText` as associated with a single `targetElement`. */
28
+ registerTextHelpForElement(targetElement: HTMLElement, helpText: string): void;
29
+ /** Marks `helpText` as associated with all elements in `targetElements`. */
30
+ registerTextHelpForElements(targetElements: HTMLElement[], helpText: string): void;
31
+ /** Returns true if any help text has been registered. */
32
+ hasHelpText(): boolean;
33
+ /**
34
+ * Creates and returns a button that toggles the help display.
35
+ */
36
+ createToggleButton(): HTMLElement;
37
+ }
@@ -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;