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.
- package/dist/Editor.css +285 -1
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.js +5 -0
- package/dist/cjs/components/util/StrokeSmoother.js +2 -2
- package/dist/cjs/rendering/caching/CacheRecordManager.js +1 -1
- package/dist/cjs/testing/sendHtmlSwipe.d.ts +4 -0
- package/dist/cjs/testing/sendHtmlSwipe.js +14 -0
- package/dist/cjs/toolbar/EdgeToolbar.d.ts +1 -0
- package/dist/cjs/toolbar/EdgeToolbar.js +30 -110
- package/dist/cjs/toolbar/IconProvider.d.ts +1 -0
- package/dist/cjs/toolbar/IconProvider.js +27 -0
- package/dist/cjs/toolbar/localization.d.ts +28 -1
- package/dist/cjs/toolbar/localization.js +30 -1
- package/dist/cjs/toolbar/utils/HelpDisplay.d.ts +37 -0
- package/dist/cjs/toolbar/utils/HelpDisplay.js +442 -0
- package/dist/cjs/toolbar/utils/HelpDisplay.test.d.ts +1 -0
- package/dist/cjs/toolbar/utils/localization.d.ts +9 -0
- package/dist/cjs/toolbar/utils/localization.js +11 -0
- package/dist/cjs/toolbar/utils/makeDraggable.d.ts +16 -0
- package/dist/cjs/toolbar/utils/makeDraggable.js +130 -0
- package/dist/cjs/toolbar/widgets/ActionButtonWidget.d.ts +7 -0
- package/dist/cjs/toolbar/widgets/ActionButtonWidget.js +14 -2
- package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +8 -1
- package/dist/cjs/toolbar/widgets/BaseWidget.js +25 -3
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.d.ts +3 -1
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +19 -4
- package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +3 -1
- package/dist/cjs/toolbar/widgets/HandToolWidget.js +19 -7
- package/dist/cjs/toolbar/widgets/InsertImageWidget.js +1 -0
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +4 -2
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +27 -8
- package/dist/cjs/toolbar/widgets/SelectionToolWidget.d.ts +3 -1
- package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +19 -5
- package/dist/cjs/toolbar/widgets/components/makeColorInput.d.ts +2 -0
- package/dist/cjs/toolbar/widgets/components/makeColorInput.js +17 -7
- package/dist/cjs/toolbar/widgets/components/makeGridSelector.d.ts +6 -0
- package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +3 -0
- package/dist/cjs/tools/FindTool.js +18 -5
- package/dist/cjs/util/addLongPressOrHoverCssClasses.d.ts +3 -1
- package/dist/cjs/util/addLongPressOrHoverCssClasses.js +2 -1
- package/dist/cjs/util/cloneElementWithStyles.d.ts +6 -0
- package/dist/cjs/util/cloneElementWithStyles.js +32 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.mjs +5 -0
- package/dist/mjs/components/util/StrokeSmoother.mjs +2 -2
- package/dist/mjs/rendering/caching/CacheRecordManager.mjs +1 -1
- package/dist/mjs/testing/sendHtmlSwipe.d.ts +4 -0
- package/dist/mjs/testing/sendHtmlSwipe.mjs +12 -0
- package/dist/mjs/toolbar/EdgeToolbar.d.ts +1 -0
- package/dist/mjs/toolbar/EdgeToolbar.mjs +30 -110
- package/dist/mjs/toolbar/IconProvider.d.ts +1 -0
- package/dist/mjs/toolbar/IconProvider.mjs +27 -0
- package/dist/mjs/toolbar/localization.d.ts +28 -1
- package/dist/mjs/toolbar/localization.mjs +30 -1
- package/dist/mjs/toolbar/utils/HelpDisplay.d.ts +37 -0
- package/dist/mjs/toolbar/utils/HelpDisplay.mjs +437 -0
- package/dist/mjs/toolbar/utils/HelpDisplay.test.d.ts +1 -0
- package/dist/mjs/toolbar/utils/localization.d.ts +9 -0
- package/dist/mjs/toolbar/utils/localization.mjs +8 -0
- package/dist/mjs/toolbar/utils/makeDraggable.d.ts +16 -0
- package/dist/mjs/toolbar/utils/makeDraggable.mjs +128 -0
- package/dist/mjs/toolbar/widgets/ActionButtonWidget.d.ts +7 -0
- package/dist/mjs/toolbar/widgets/ActionButtonWidget.mjs +14 -2
- package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +8 -1
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +25 -3
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.d.ts +3 -1
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +19 -4
- package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +3 -1
- package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +19 -7
- package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +1 -0
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +4 -2
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +27 -8
- package/dist/mjs/toolbar/widgets/SelectionToolWidget.d.ts +3 -1
- package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +19 -5
- package/dist/mjs/toolbar/widgets/components/makeColorInput.d.ts +2 -0
- package/dist/mjs/toolbar/widgets/components/makeColorInput.mjs +17 -7
- package/dist/mjs/toolbar/widgets/components/makeGridSelector.d.ts +6 -0
- package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +3 -0
- package/dist/mjs/tools/FindTool.mjs +18 -5
- package/dist/mjs/util/addLongPressOrHoverCssClasses.d.ts +3 -1
- package/dist/mjs/util/addLongPressOrHoverCssClasses.mjs +2 -1
- package/dist/mjs/util/cloneElementWithStyles.d.ts +6 -0
- package/dist/mjs/util/cloneElementWithStyles.mjs +30 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +2 -2
- package/src/toolbar/EdgeToolbar.scss +23 -2
- package/src/toolbar/toolbar.scss +2 -0
- package/src/toolbar/utils/HelpDisplay.scss +315 -0
- 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,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;
|