js-draw 1.13.2 → 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 +2 -2
  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 +2 -2
  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,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultToolbarLocalization = void 0;
4
+ const localization_1 = require("./utils/localization");
4
5
  exports.defaultToolbarLocalization = {
6
+ ...localization_1.defaultToolbarUtilsLocalization,
5
7
  pen: 'Pen',
6
8
  eraser: 'Eraser',
7
9
  select: 'Select',
@@ -54,12 +56,39 @@ exports.defaultToolbarLocalization = {
54
56
  outlinedCirclePen: 'Outlined circle',
55
57
  lockRotation: 'Lock rotation',
56
58
  paste: 'Paste',
59
+ errorImageHasZeroSize: 'Error: Image has zero size',
60
+ describeTheImage: 'Image description',
61
+ // Help text
62
+ penDropdown__baseHelpText: 'This tool draws shapes or freehand lines.',
63
+ penDropdown__colorHelpText: 'Changes the pen\'s color',
64
+ penDropdown__thicknessHelpText: 'Changes the thickness of strokes drawn by the pen.',
65
+ 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.',
66
+ 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.',
67
+ penDropdown__stabilizationHelpText: 'Draws smoother strokes.\n\nThis also adds a short delay between the mouse/stylus and the stroke.',
68
+ handDropdown__baseHelpText: 'This tool is responsible for scrolling, rotating, and zooming the editor.',
69
+ handDropdown__zoomInHelpText: 'Zooms in.',
70
+ handDropdown__zoomOutHelpText: 'Zooms out.',
71
+ handDropdown__resetViewHelpText: 'Resets the zoom level to 100% and resets scroll.',
72
+ handDropdown__zoomDisplayHelpText: 'Shows the current zoom level. 100% shows the image at its actual size.',
73
+ handDropdown__touchPanningHelpText: 'When enabled, touch gestures move the image rather than select or draw.',
74
+ handDropdown__lockRotationHelpText: 'When enabled, prevents touch gestures from rotating the screen.',
75
+ selectionDropdown__baseHelpText: 'Selects content and manipulates the selection',
76
+ selectionDropdown__resizeToHelpText: 'Crops the drawing to the size of what\'s currently selected.\n\nIf auto-resize is enabled, it will be disabled.',
77
+ selectionDropdown__deleteHelpText: 'Erases selected items.',
78
+ selectionDropdown__duplicateHelpText: 'Makes a copy of selected items.',
79
+ selectionDropdown__changeColorHelpText: 'Changes the color of selected items.',
80
+ pageDropdown__baseHelpText: 'Controls the drawing canvas\' background color, pattern, and size.',
81
+ pageDropdown__backgroundColorHelpText: 'Changes the background color of the drawing canvas.',
82
+ pageDropdown__gridCheckboxHelpText: 'Enables/disables a background grid pattern.',
83
+ 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.',
84
+ pageDropdown__aboutButtonHelpText: 'Shows version, debug, and other information.',
85
+ colorPickerPipetteHelpText: 'Picks a color from the screen.',
86
+ colorPickerToggleHelpText: 'Opens/closes the color picker.',
57
87
  closeSidebar: (toolName) => `Close sidebar for ${toolName}`,
58
88
  dropdownShown: (toolName) => `Menu for ${toolName} shown`,
59
89
  dropdownHidden: (toolName) => `Menu for ${toolName} hidden`,
60
90
  zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
61
91
  colorChangedAnnouncement: (color) => `Color changed to ${color}`,
62
92
  imageSize: (size, units) => `Image size: ${size} ${units}`,
63
- errorImageHasZeroSize: 'Error: Image has zero size',
64
93
  imageLoadError: (message) => `Error loading image: ${message}`,
65
94
  };
@@ -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,442 @@
1
+ "use strict";
2
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
4
+ 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");
5
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
+ };
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ var _HelpDisplay_helpData;
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const math_1 = require("@js-draw/math");
13
+ const makeDraggable_1 = __importDefault(require("./makeDraggable"));
14
+ const ReactiveValue_1 = require("../../util/ReactiveValue");
15
+ const cloneElementWithStyles_1 = __importDefault(require("../../util/cloneElementWithStyles"));
16
+ const addLongPressOrHoverCssClasses_1 = __importDefault(require("../../util/addLongPressOrHoverCssClasses"));
17
+ /**
18
+ * Creates the main content of the help overlay.
19
+ *
20
+ * Shows the label for a `HelpRecord` and a highlighted copy
21
+ * of that label's `targetElements`.
22
+ */
23
+ const createHelpPage = (helpItems, onItemClick, onBackgroundClick, context) => {
24
+ const container = document.createElement('div');
25
+ container.classList.add('help-page-container');
26
+ const textLabel = document.createElement('div');
27
+ textLabel.classList.add('label', '-space-above');
28
+ textLabel.setAttribute('aria-live', 'polite');
29
+ // The current active item in helpItems.
30
+ // (Only one item is active at a time, but each item can have multiple HTMLElements).
31
+ let currentItemIndex = 0;
32
+ let currentItem = helpItems[0] ?? null;
33
+ // Each help item can have multiple associated elements. We store clones of each
34
+ // of these elements in their own container.
35
+ //
36
+ // clonedElementContainers maps from help item indicies to **arrays** of containers.
37
+ //
38
+ // For example, clonedElementContainers would be
39
+ // [ [ Container1, Container2 ], [ Container3 ], [ Container4 ]]
40
+ // ↑ ↑ ↑
41
+ // HelpItem 1 HelpItem 2 HelpItem 3
42
+ // if the first help item had two elements (and thus two cloned element containers).
43
+ //
44
+ // We also store the original bounding box -- the bounding box of the clones can change
45
+ // while dragging to switch pages.
46
+ let clonedElementContainers = [];
47
+ // Clicking on the background of the help area should send an event (e.g. to allow the
48
+ // help container to be closed).
49
+ container.addEventListener('click', event => {
50
+ // If clicking directly on the container (and not on a child)
51
+ if (event.target === container) {
52
+ onBackgroundClick();
53
+ }
54
+ });
55
+ // Returns the combined bounding box of all elements associated with the currentItem
56
+ // (all active help items).
57
+ const getCombinedBBox = () => {
58
+ if (!currentItem) {
59
+ return math_1.Rect2.empty;
60
+ }
61
+ const itemBoundingBoxes = currentItem.targetElements.map(element => math_1.Rect2.of(element.getBoundingClientRect()));
62
+ return math_1.Rect2.union(...itemBoundingBoxes);
63
+ };
64
+ // Updates each cloned element's click listener and CSS classes based on whether
65
+ // that element is the current focused element.
66
+ const updateClonedElementStates = () => {
67
+ const currentItemBBox = getCombinedBBox();
68
+ for (let index = 0; index < clonedElementContainers.length; index++) {
69
+ for (const { container, bbox: containerBBox } of clonedElementContainers[index]) {
70
+ if (index === currentItemIndex) {
71
+ container.classList.add('-active');
72
+ container.classList.remove('-clickable', '-background');
73
+ container.onclick = () => { };
74
+ }
75
+ // Otherwise, if not containing the current element
76
+ else {
77
+ if (!containerBBox.containsRect(currentItemBBox)) {
78
+ container.classList.add('-clickable');
79
+ container.classList.remove('-active', '-background');
80
+ }
81
+ else {
82
+ container.classList.add('-background');
83
+ container.classList.remove('-active', '-clickable');
84
+ }
85
+ const containerIndex = index;
86
+ container.onclick = () => {
87
+ onItemClick(containerIndex);
88
+ };
89
+ }
90
+ }
91
+ }
92
+ };
93
+ // Ensures that the item label doesn't overlap the current help item's cloned element.
94
+ const updateLabelPosition = () => {
95
+ const labelBBox = math_1.Rect2.of(textLabel.getBoundingClientRect());
96
+ const combinedBBox = getCombinedBBox();
97
+ if (labelBBox.intersects(combinedBBox)) {
98
+ const containerBBox = math_1.Rect2.of(container.getBoundingClientRect());
99
+ const spaceAboveCombined = combinedBBox.topLeft.y;
100
+ const spaceBelowCombined = containerBBox.bottomLeft.y - combinedBBox.bottomLeft.y;
101
+ if (spaceAboveCombined > spaceBelowCombined && spaceAboveCombined > labelBBox.height / 3) {
102
+ // Push to the very top
103
+ textLabel.classList.remove('-small-space-above', '-large-space-above');
104
+ textLabel.classList.add('-large-space-below');
105
+ }
106
+ if (spaceAboveCombined < spaceBelowCombined && spaceBelowCombined > labelBBox.height) {
107
+ // Push to the very bottom
108
+ textLabel.classList.add('-large-space-above');
109
+ textLabel.classList.remove('-large-space-below');
110
+ }
111
+ }
112
+ };
113
+ const refreshContent = () => {
114
+ container.replaceChildren();
115
+ // Add the text label first so that screen readers will visit it first.
116
+ textLabel.classList.remove('-large-space-above');
117
+ textLabel.classList.add('-small-space-above', '-large-space-below');
118
+ container.appendChild(textLabel);
119
+ const screenBBox = new math_1.Rect2(0, 0, window.innerWidth, window.innerHeight);
120
+ clonedElementContainers = [];
121
+ for (let itemIndex = 0; itemIndex < helpItems.length; itemIndex++) {
122
+ const item = helpItems[itemIndex];
123
+ const itemCloneContainers = [];
124
+ for (const targetElement of item.targetElements) {
125
+ let targetBBox = math_1.Rect2.of(targetElement.getBoundingClientRect());
126
+ // Move the element onto the screen if not visible
127
+ if (!screenBBox.intersects(targetBBox)) {
128
+ const screenBottomCenter = screenBBox.bottomLeft.lerp(screenBBox.bottomRight, 0.5);
129
+ const targetBottomCenter = targetBBox.bottomLeft.lerp(targetBBox.bottomRight, 0.5);
130
+ const delta = screenBottomCenter.minus(targetBottomCenter);
131
+ targetBBox = targetBBox.translatedBy(delta);
132
+ }
133
+ const clonedElement = (0, cloneElementWithStyles_1.default)(targetElement);
134
+ // Interacting with the clone won't trigger event listeners, so disable
135
+ // all inputs.
136
+ for (const input of clonedElement.querySelectorAll('input')) {
137
+ input.disabled = true;
138
+ }
139
+ clonedElement.style.margin = '0';
140
+ const clonedElementContainer = document.createElement('div');
141
+ clonedElementContainer.classList.add('cloned-element-container');
142
+ clonedElementContainer.style.position = 'absolute';
143
+ clonedElementContainer.style.left = `${targetBBox.topLeft.x}px`;
144
+ clonedElementContainer.style.top = `${targetBBox.topLeft.y}px`;
145
+ clonedElementContainer.replaceChildren(clonedElement);
146
+ (0, addLongPressOrHoverCssClasses_1.default)(clonedElementContainer, { timeout: 0 });
147
+ itemCloneContainers.push({ container: clonedElementContainer, bbox: targetBBox });
148
+ container.appendChild(clonedElementContainer);
149
+ }
150
+ clonedElementContainers.push(itemCloneContainers);
151
+ }
152
+ updateClonedElementStates();
153
+ };
154
+ const refresh = () => {
155
+ refreshContent();
156
+ updateLabelPosition();
157
+ };
158
+ const onItemChange = () => {
159
+ const helpTextElement = document.createElement('div');
160
+ helpTextElement.innerText = currentItem?.helpText ?? '';
161
+ // For tests
162
+ helpTextElement.classList.add('current-item-help');
163
+ const navigationHelpElement = document.createElement('div');
164
+ navigationHelpElement.innerText = context.localization.helpScreenNavigationHelp;
165
+ navigationHelpElement.classList.add('navigation-help');
166
+ textLabel.replaceChildren(helpTextElement, ...(currentItemIndex === 0 ? [navigationHelpElement] : []));
167
+ updateClonedElementStates();
168
+ };
169
+ onItemChange();
170
+ return {
171
+ addToParent: (parent) => {
172
+ refreshContent();
173
+ parent.appendChild(container);
174
+ updateLabelPosition();
175
+ },
176
+ refresh,
177
+ setPageIndex: (pageIndex) => {
178
+ currentItemIndex = pageIndex;
179
+ currentItem = helpItems[pageIndex];
180
+ onItemChange();
181
+ },
182
+ };
183
+ };
184
+ /**
185
+ * Creates and manages an overlay that shows help text for a set of
186
+ * `HTMLElement`s.
187
+ *
188
+ * @see {@link BaseWidget.fillDropdown}.
189
+ */
190
+ class HelpDisplay {
191
+ /** Constructed internally by BaseWidget. @internal */
192
+ constructor(createOverlay, context) {
193
+ this.createOverlay = createOverlay;
194
+ this.context = context;
195
+ _HelpDisplay_helpData.set(this, []);
196
+ }
197
+ /** @internal */
198
+ showHelpOverlay() {
199
+ const overlay = document.createElement('dialog');
200
+ overlay.setAttribute('autofocus', 'true');
201
+ overlay.classList.add('toolbar-help-overlay');
202
+ // Closes the overlay with a closing animation
203
+ const closing = false;
204
+ const closeOverlay = () => {
205
+ if (closing)
206
+ return;
207
+ // If changing animationDelay, be sure to also update the CSS.
208
+ const animationDelay = 250; // ms
209
+ overlay.classList.add('-hiding');
210
+ setTimeout(() => overlay.close(), animationDelay);
211
+ };
212
+ let lastDragTimestamp = 0;
213
+ const onBackgroundClick = () => {
214
+ const wasJustDragging = performance.now() - lastDragTimestamp < 100;
215
+ if (!wasJustDragging) {
216
+ closeOverlay();
217
+ }
218
+ };
219
+ const makeCloseButton = () => {
220
+ const closeButton = document.createElement('button');
221
+ closeButton.classList.add('close-button');
222
+ closeButton.appendChild(this.context.icons.makeCloseIcon());
223
+ const label = this.context.localization.close;
224
+ closeButton.setAttribute('aria-label', label);
225
+ closeButton.setAttribute('title', label);
226
+ closeButton.onclick = () => { closeOverlay(); };
227
+ return closeButton;
228
+ };
229
+ // Wraps the label and clickable help elements
230
+ const makeNavigationContent = () => {
231
+ const currentPage = ReactiveValue_1.MutableReactiveValue.fromInitialValue(0);
232
+ const content = document.createElement('div');
233
+ content.classList.add('navigation-content');
234
+ const helpPage = createHelpPage(__classPrivateFieldGet(this, _HelpDisplay_helpData, "f"), newPageIndex => currentPage.set(newPageIndex), onBackgroundClick, this.context);
235
+ helpPage.addToParent(content);
236
+ const showPage = (pageIndex) => {
237
+ if (pageIndex >= __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").length || pageIndex < 0) {
238
+ // Hide if out of bounds
239
+ console.warn('Help screen: Navigated to out-of-bounds page', pageIndex);
240
+ content.style.display = 'none';
241
+ }
242
+ else {
243
+ content.style.display = '';
244
+ helpPage.setPageIndex(pageIndex);
245
+ }
246
+ };
247
+ currentPage.onUpdateAndNow(showPage);
248
+ const navigationControl = {
249
+ content,
250
+ currentPage,
251
+ toNext: () => {
252
+ if (navigationControl.hasNext()) {
253
+ currentPage.set(currentPage.get() + 1);
254
+ }
255
+ },
256
+ toPrevious: () => {
257
+ if (navigationControl.hasPrevious()) {
258
+ currentPage.set(currentPage.get() - 1);
259
+ }
260
+ },
261
+ hasNext: () => {
262
+ return currentPage.get() + 1 < __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").length;
263
+ },
264
+ hasPrevious: () => {
265
+ return currentPage.get() > 0;
266
+ },
267
+ refreshCurrent: () => {
268
+ helpPage.refresh();
269
+ },
270
+ };
271
+ return navigationControl;
272
+ };
273
+ // Creates next/previous buttons.
274
+ const makeNavigationButtons = (navigation) => {
275
+ const navigationButtonContainer = document.createElement('div');
276
+ navigationButtonContainer.classList.add('navigation-buttons');
277
+ const nextButton = document.createElement('button');
278
+ const previousButton = document.createElement('button');
279
+ nextButton.innerText = this.context.localization.next;
280
+ previousButton.innerText = this.context.localization.previous;
281
+ nextButton.classList.add('next');
282
+ previousButton.classList.add('previous');
283
+ const updateButtonVisibility = () => {
284
+ navigationButtonContainer.classList.remove('-has-next', '-has-previous');
285
+ if (navigation.hasNext()) {
286
+ navigationButtonContainer.classList.add('-has-next');
287
+ nextButton.disabled = false;
288
+ }
289
+ else {
290
+ navigationButtonContainer.classList.remove('-has-next');
291
+ nextButton.disabled = true;
292
+ }
293
+ if (navigation.hasPrevious()) {
294
+ navigationButtonContainer.classList.add('-has-previous');
295
+ previousButton.disabled = false;
296
+ }
297
+ else {
298
+ navigationButtonContainer.classList.remove('-has-previous');
299
+ previousButton.disabled = true;
300
+ }
301
+ };
302
+ navigation.currentPage.onUpdateAndNow(updateButtonVisibility);
303
+ nextButton.onclick = () => {
304
+ navigation.toNext();
305
+ };
306
+ previousButton.onclick = () => {
307
+ navigation.toPrevious();
308
+ };
309
+ navigationButtonContainer.replaceChildren(previousButton, nextButton);
310
+ return navigationButtonContainer;
311
+ };
312
+ const navigation = makeNavigationContent();
313
+ const navigationButtons = makeNavigationButtons(navigation);
314
+ overlay.replaceChildren(makeCloseButton(), navigationButtons, navigation.content);
315
+ this.createOverlay(overlay);
316
+ overlay.showModal();
317
+ const minDragOffsetToTransition = 30;
318
+ const setDragOffset = (offset) => {
319
+ if (offset > 0 && !navigation.hasPrevious()) {
320
+ offset = 0;
321
+ }
322
+ if (offset < 0 && !navigation.hasNext()) {
323
+ offset = 0;
324
+ }
325
+ // Clamp offset
326
+ if (offset > minDragOffsetToTransition || offset < -minDragOffsetToTransition) {
327
+ offset = minDragOffsetToTransition * Math.sign(offset);
328
+ }
329
+ overlay.style.transform = `translate(${offset}px, 0px)`;
330
+ if (offset >= minDragOffsetToTransition) {
331
+ navigationButtons.classList.add('-highlight-previous');
332
+ }
333
+ else {
334
+ navigationButtons.classList.remove('-highlight-previous');
335
+ }
336
+ if (offset <= -minDragOffsetToTransition) {
337
+ navigationButtons.classList.add('-highlight-next');
338
+ }
339
+ else {
340
+ navigationButtons.classList.remove('-highlight-next');
341
+ }
342
+ };
343
+ // Listeners
344
+ const dragListener = (0, makeDraggable_1.default)(overlay, {
345
+ draggableChildElements: [navigation.content],
346
+ onDrag: (_deltaX, _deltaY, totalDisplacement) => {
347
+ overlay.classList.add('-dragging');
348
+ setDragOffset(totalDisplacement.x);
349
+ },
350
+ onDragEnd: (dragStatistics) => {
351
+ overlay.classList.remove('-dragging');
352
+ setDragOffset(0);
353
+ if (!dragStatistics.roughlyClick) {
354
+ const xDisplacement = dragStatistics.displacement.x;
355
+ if (xDisplacement > minDragOffsetToTransition) {
356
+ navigation.toPrevious();
357
+ }
358
+ else if (xDisplacement < -minDragOffsetToTransition) {
359
+ navigation.toNext();
360
+ }
361
+ lastDragTimestamp = dragStatistics.endTimestamp;
362
+ }
363
+ },
364
+ });
365
+ let resizeObserver;
366
+ if (window.ResizeObserver) {
367
+ resizeObserver = new ResizeObserver(() => {
368
+ navigation.refreshCurrent();
369
+ });
370
+ resizeObserver.observe(overlay);
371
+ }
372
+ const onMediaChangeListener = () => {
373
+ // Refresh the cloned elements and their styles after a delay.
374
+ // This is necessary because styles are cloned, in addition to elements.
375
+ requestAnimationFrame(() => navigation.refreshCurrent());
376
+ };
377
+ // matchMedia is unsupported by jsdom
378
+ const mediaQueryList = window.matchMedia?.('(prefers-color-scheme: dark)');
379
+ mediaQueryList?.addEventListener('change', onMediaChangeListener);
380
+ // Close the overlay when clicking on the background (*directly* on any of the
381
+ // elements in closeOverlayTriggers).
382
+ const closeOverlayTriggers = [navigation.content, navigationButtons, overlay];
383
+ overlay.onclick = event => {
384
+ if (closeOverlayTriggers.includes(event.target)) {
385
+ onBackgroundClick();
386
+ }
387
+ };
388
+ overlay.onkeyup = event => {
389
+ if (event.code === 'Escape') {
390
+ closeOverlay();
391
+ event.preventDefault();
392
+ }
393
+ else if (event.code === 'ArrowRight') {
394
+ navigation.toNext();
395
+ event.preventDefault();
396
+ }
397
+ else if (event.code === 'ArrowLeft') {
398
+ navigation.toPrevious();
399
+ event.preventDefault();
400
+ }
401
+ };
402
+ overlay.addEventListener('close', () => {
403
+ this.context.announceForAccessibility(this.context.localization.helpHidden);
404
+ mediaQueryList?.removeEventListener('change', onMediaChangeListener);
405
+ dragListener.removeListeners();
406
+ resizeObserver?.disconnect();
407
+ overlay.remove();
408
+ });
409
+ }
410
+ /** Marks `helpText` as associated with a single `targetElement`. */
411
+ registerTextHelpForElement(targetElement, helpText) {
412
+ this.registerTextHelpForElements([targetElement], helpText);
413
+ }
414
+ /** Marks `helpText` as associated with all elements in `targetElements`. */
415
+ registerTextHelpForElements(targetElements, helpText) {
416
+ __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").push({ targetElements: [...targetElements], helpText });
417
+ }
418
+ /** Returns true if any help text has been registered. */
419
+ hasHelpText() {
420
+ return __classPrivateFieldGet(this, _HelpDisplay_helpData, "f").length > 0;
421
+ }
422
+ /**
423
+ * Creates and returns a button that toggles the help display.
424
+ */
425
+ createToggleButton() {
426
+ const buttonContainer = document.createElement('div');
427
+ buttonContainer.classList.add('toolbar-help-overlay-button');
428
+ const helpButton = document.createElement('button');
429
+ helpButton.classList.add('button');
430
+ const icon = this.context.icons.makeHelpIcon();
431
+ icon.classList.add('icon');
432
+ helpButton.appendChild(icon);
433
+ helpButton.setAttribute('aria-label', this.context.localization.help);
434
+ helpButton.onclick = () => {
435
+ this.showHelpOverlay();
436
+ };
437
+ buttonContainer.appendChild(helpButton);
438
+ return buttonContainer;
439
+ }
440
+ }
441
+ _HelpDisplay_helpData = new WeakMap();
442
+ exports.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,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultToolbarUtilsLocalization = void 0;
4
+ exports.defaultToolbarUtilsLocalization = {
5
+ help: 'Help',
6
+ helpHidden: 'Help hidden',
7
+ next: 'Next',
8
+ previous: 'Previous',
9
+ close: 'Close',
10
+ helpScreenNavigationHelp: 'Click on a control for more information.',
11
+ };
@@ -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;