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