@mpgd/tutorial 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/definition.d.ts +38 -0
- package/dist/definition.js +33 -0
- package/dist/director.d.ts +39 -0
- package/dist/director.js +230 -0
- package/dist/driver.d.ts +50 -0
- package/dist/driver.js +755 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/platform-storage.d.ts +14 -0
- package/dist/platform-storage.js +74 -0
- package/dist/progress.d.ts +23 -0
- package/dist/progress.js +80 -0
- package/dist/testing.d.ts +59 -0
- package/dist/testing.js +156 -0
- package/driver.css +91 -0
- package/package.json +71 -0
package/dist/driver.js
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import { driver } from 'driver.js';
|
|
2
|
+
import { tutorialDomAttributes } from './index.js';
|
|
3
|
+
export function createDriverTutorialPresenter(input) {
|
|
4
|
+
const root = input.root ?? document;
|
|
5
|
+
const ownerDocumentCandidate = root.nodeType === 9 ? root : root.ownerDocument;
|
|
6
|
+
if (ownerDocumentCandidate === null) {
|
|
7
|
+
throw new Error('Driver tutorial presenter requires an attached owner document.');
|
|
8
|
+
}
|
|
9
|
+
const ownerDocument = ownerDocumentCandidate;
|
|
10
|
+
if (typeof document === 'undefined' || ownerDocument !== document) {
|
|
11
|
+
throw new Error('Driver tutorial presenter supports only the current browser document.');
|
|
12
|
+
}
|
|
13
|
+
const browserWindowCandidate = ownerDocument.defaultView;
|
|
14
|
+
if (browserWindowCandidate === null) {
|
|
15
|
+
throw new Error('Driver tutorial presenter requires a browser document.');
|
|
16
|
+
}
|
|
17
|
+
const browserWindow = browserWindowCandidate;
|
|
18
|
+
const stateHost = input.stateHost ?? ownerDocument.body;
|
|
19
|
+
const targetAttribute = input.targetAttribute ?? tutorialDomAttributes.target;
|
|
20
|
+
const escapedCopy = (value) => escapeTutorialText(ownerDocument, value);
|
|
21
|
+
let activePresentation = null;
|
|
22
|
+
let activePresentationKey = null;
|
|
23
|
+
let instance = null;
|
|
24
|
+
let frame;
|
|
25
|
+
let destroyed = false;
|
|
26
|
+
let reportedActive = false;
|
|
27
|
+
let targetSemanticsGuard;
|
|
28
|
+
let targetRestorePending = false;
|
|
29
|
+
let dismissedPresentationKey = null;
|
|
30
|
+
let retryableDismissedKey = null;
|
|
31
|
+
let missingTargetErrorKey = null;
|
|
32
|
+
let syncModalSemantics;
|
|
33
|
+
const pendingSkipOperations = new Set();
|
|
34
|
+
const reportActive = (active) => {
|
|
35
|
+
if (reportedActive === active) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
reportedActive = active;
|
|
39
|
+
input.onActiveChange?.(active);
|
|
40
|
+
};
|
|
41
|
+
const scheduleRefresh = () => {
|
|
42
|
+
if (destroyed || frame !== undefined || dismissedPresentationKey !== null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
frame = browserWindow.requestAnimationFrame(() => {
|
|
46
|
+
frame = undefined;
|
|
47
|
+
if (destroyed || dismissedPresentationKey !== null) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const currentInstance = instance;
|
|
51
|
+
if (currentInstance === null || !currentInstance.isActive()) {
|
|
52
|
+
driveActiveStep();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
rebindActiveTarget(currentInstance, activePresentation);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const createInstance = (presentation) => {
|
|
59
|
+
let created;
|
|
60
|
+
let modalSemanticsGuard;
|
|
61
|
+
let popover = null;
|
|
62
|
+
const syncCreatedModalSemantics = () => {
|
|
63
|
+
if (popover === null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const modalSelector = input.modalSelector ?? '[role="dialog"][aria-modal="true"]';
|
|
67
|
+
const modal = resolveUnderlyingTutorialModal(popover, presentation.step.interaction, created.getActiveElement(), modalSelector, modalSemanticsGuard?.modal ?? null);
|
|
68
|
+
if (modalSemanticsGuard?.modal === modal && modalSemanticsGuard.popover === popover) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
modalSemanticsGuard?.restore();
|
|
72
|
+
modalSemanticsGuard = configureTutorialModalSemantics(popover, presentation.step.interaction, modal);
|
|
73
|
+
};
|
|
74
|
+
created = driver({
|
|
75
|
+
allowClose: true,
|
|
76
|
+
allowKeyboardControl: false,
|
|
77
|
+
allowScroll: true,
|
|
78
|
+
animate: false,
|
|
79
|
+
disableActiveInteraction: true,
|
|
80
|
+
doneBtnText: presentation.finalStep === true
|
|
81
|
+
? escapedCopy(presentation.copy.done)
|
|
82
|
+
: escapedCopy(presentation.copy.next),
|
|
83
|
+
nextBtnText: escapedCopy(presentation.copy.next),
|
|
84
|
+
overlayClickBehavior: () => undefined,
|
|
85
|
+
overlayColor: '#0e1214',
|
|
86
|
+
overlayOpacity: 0.78,
|
|
87
|
+
popoverClass: 'mpgd-tutorial-popover',
|
|
88
|
+
popoverOffset: 12,
|
|
89
|
+
prevBtnText: '',
|
|
90
|
+
showButtons: ['next', 'close'],
|
|
91
|
+
showProgress: false,
|
|
92
|
+
skipMissingElement: false,
|
|
93
|
+
smoothScroll: false,
|
|
94
|
+
stagePadding: 7,
|
|
95
|
+
stageRadius: 9,
|
|
96
|
+
waitForElement: 900,
|
|
97
|
+
onCloseClick: () => {
|
|
98
|
+
const dismissedKey = activePresentationKey;
|
|
99
|
+
dismissedPresentationKey = dismissedKey;
|
|
100
|
+
retryableDismissedKey = null;
|
|
101
|
+
destroyInstancePreservingTarget(created);
|
|
102
|
+
const skipOperation = Promise.resolve()
|
|
103
|
+
.then(() => input.onSkip())
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
if (dismissedPresentationKey === dismissedKey) {
|
|
106
|
+
retryableDismissedKey = dismissedKey;
|
|
107
|
+
}
|
|
108
|
+
input.onError?.(error);
|
|
109
|
+
});
|
|
110
|
+
pendingSkipOperations.add(skipOperation);
|
|
111
|
+
void skipOperation
|
|
112
|
+
.finally(() => pendingSkipOperations.delete(skipOperation))
|
|
113
|
+
.catch(() => undefined);
|
|
114
|
+
},
|
|
115
|
+
onDestroyed: () => {
|
|
116
|
+
if (!targetRestorePending) {
|
|
117
|
+
targetSemanticsGuard?.restore(true);
|
|
118
|
+
}
|
|
119
|
+
targetSemanticsGuard = undefined;
|
|
120
|
+
targetRestorePending = false;
|
|
121
|
+
modalSemanticsGuard?.restore();
|
|
122
|
+
modalSemanticsGuard = undefined;
|
|
123
|
+
if (syncModalSemantics === syncCreatedModalSemantics) {
|
|
124
|
+
syncModalSemantics = undefined;
|
|
125
|
+
}
|
|
126
|
+
if (instance === created) {
|
|
127
|
+
instance = null;
|
|
128
|
+
clearTutorialState(stateHost);
|
|
129
|
+
reportActive(false);
|
|
130
|
+
}
|
|
131
|
+
if (!destroyed && activePresentation !== null) {
|
|
132
|
+
scheduleRefresh();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
onPopoverRender: ({ closeButton, nextButton, wrapper }) => {
|
|
136
|
+
closeButton.setAttribute('aria-label', presentation.copy.skip);
|
|
137
|
+
closeButton.title = presentation.copy.skip;
|
|
138
|
+
closeButton.textContent = presentation.copy.skip;
|
|
139
|
+
closeButton.setAttribute(tutorialDomAttributes.skip, 'true');
|
|
140
|
+
nextButton.setAttribute(tutorialDomAttributes.next, 'true');
|
|
141
|
+
wrapper.setAttribute(tutorialDomAttributes.popover, 'true');
|
|
142
|
+
popover = wrapper;
|
|
143
|
+
syncModalSemantics = syncCreatedModalSemantics;
|
|
144
|
+
syncCreatedModalSemantics();
|
|
145
|
+
if (presentation.step.advance.kind === 'acknowledge') {
|
|
146
|
+
const initialFocus = created.getActiveStep()?.advanceOnClick === true
|
|
147
|
+
? closeButton
|
|
148
|
+
: nextButton;
|
|
149
|
+
browserWindow.queueMicrotask(() => initialFocus.focus());
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
return created;
|
|
154
|
+
};
|
|
155
|
+
const driveActiveStep = () => {
|
|
156
|
+
const presentation = activePresentation;
|
|
157
|
+
const presentationKey = activePresentationKey;
|
|
158
|
+
if (presentation === null || presentationKey === null || instance?.isActive() === true) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const target = resolvePresentationTarget(presentation, presentationKey);
|
|
162
|
+
if (destroyed
|
|
163
|
+
|| activePresentation !== presentation
|
|
164
|
+
|| activePresentationKey !== presentationKey) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (presentation.step.target !== null && target === null) {
|
|
168
|
+
const policy = input.missingTarget ?? 'wait';
|
|
169
|
+
if (policy === 'error') {
|
|
170
|
+
reportMissingTarget(presentation, presentationKey);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (policy === 'wait') {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
missingTargetErrorKey = null;
|
|
179
|
+
}
|
|
180
|
+
const nextInstance = createInstance(presentation);
|
|
181
|
+
instance = nextInstance;
|
|
182
|
+
targetSemanticsGuard?.restore();
|
|
183
|
+
targetSemanticsGuard = target === null
|
|
184
|
+
? undefined
|
|
185
|
+
: preserveTutorialTargetSemantics(target, browserWindow);
|
|
186
|
+
try {
|
|
187
|
+
stateHost.setAttribute(tutorialDomAttributes.active, 'true');
|
|
188
|
+
stateHost.setAttribute(tutorialDomAttributes.step, presentation.step.id);
|
|
189
|
+
nextInstance.setSteps([
|
|
190
|
+
createDriveStep(input, presentation, target ?? undefined, ownerDocument),
|
|
191
|
+
]);
|
|
192
|
+
applyDriverTargetMutation(() => nextInstance.drive());
|
|
193
|
+
syncModalSemantics?.();
|
|
194
|
+
reportActive(true);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
dismissedPresentationKey = activePresentationKey;
|
|
198
|
+
try {
|
|
199
|
+
destroyInstancePreservingTarget(nextInstance);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
targetSemanticsGuard = undefined;
|
|
203
|
+
instance = null;
|
|
204
|
+
syncModalSemantics = undefined;
|
|
205
|
+
clearTutorialState(stateHost);
|
|
206
|
+
reportActive(false);
|
|
207
|
+
}
|
|
208
|
+
input.onError?.(error);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const resolvePresentationTarget = (presentation, presentationKey = activePresentationKey) => {
|
|
212
|
+
if (presentation.step.target === null) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
if (input.resolveTarget !== undefined) {
|
|
217
|
+
return input.resolveTarget(presentation.step.target, root);
|
|
218
|
+
}
|
|
219
|
+
return resolveVisibleTutorialTarget({
|
|
220
|
+
root,
|
|
221
|
+
target: presentation.step.target,
|
|
222
|
+
targetAttribute,
|
|
223
|
+
view: browserWindow,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (presentationKey !== null
|
|
228
|
+
&& presentationKey === activePresentationKey
|
|
229
|
+
&& missingTargetErrorKey !== presentationKey) {
|
|
230
|
+
missingTargetErrorKey = presentationKey;
|
|
231
|
+
input.onError?.(error);
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const observationRoot = root.nodeType === 9 ? ownerDocument.documentElement : root;
|
|
237
|
+
const mutationObserver = new browserWindow.MutationObserver((records) => {
|
|
238
|
+
if (activePresentation === null && instance === null) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (records.some((record) => !isDriverOwnedMutation(record))) {
|
|
242
|
+
scheduleRefresh();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
mutationObserver.observe(observationRoot, {
|
|
246
|
+
attributes: true,
|
|
247
|
+
childList: true,
|
|
248
|
+
subtree: true,
|
|
249
|
+
});
|
|
250
|
+
const resizeObserver = typeof browserWindow.ResizeObserver === 'function'
|
|
251
|
+
? new browserWindow.ResizeObserver(scheduleRefresh)
|
|
252
|
+
: null;
|
|
253
|
+
resizeObserver?.observe(observationRoot);
|
|
254
|
+
observationRoot.addEventListener('scroll', scheduleRefresh, true);
|
|
255
|
+
browserWindow.visualViewport?.addEventListener('resize', scheduleRefresh);
|
|
256
|
+
browserWindow.visualViewport?.addEventListener('scroll', scheduleRefresh);
|
|
257
|
+
browserWindow.addEventListener('orientationchange', scheduleRefresh);
|
|
258
|
+
return {
|
|
259
|
+
destroy() {
|
|
260
|
+
destroyed = true;
|
|
261
|
+
activePresentation = null;
|
|
262
|
+
activePresentationKey = null;
|
|
263
|
+
dismissedPresentationKey = null;
|
|
264
|
+
retryableDismissedKey = null;
|
|
265
|
+
missingTargetErrorKey = null;
|
|
266
|
+
mutationObserver.disconnect();
|
|
267
|
+
resizeObserver?.disconnect();
|
|
268
|
+
observationRoot.removeEventListener('scroll', scheduleRefresh, true);
|
|
269
|
+
browserWindow.visualViewport?.removeEventListener('resize', scheduleRefresh);
|
|
270
|
+
browserWindow.visualViewport?.removeEventListener('scroll', scheduleRefresh);
|
|
271
|
+
browserWindow.removeEventListener('orientationchange', scheduleRefresh);
|
|
272
|
+
if (frame !== undefined) {
|
|
273
|
+
browserWindow.cancelAnimationFrame(frame);
|
|
274
|
+
}
|
|
275
|
+
if (instance !== null) {
|
|
276
|
+
destroyInstancePreservingTarget(instance);
|
|
277
|
+
}
|
|
278
|
+
instance = null;
|
|
279
|
+
targetSemanticsGuard = undefined;
|
|
280
|
+
clearTutorialState(stateHost);
|
|
281
|
+
reportActive(false);
|
|
282
|
+
},
|
|
283
|
+
present(presentation) {
|
|
284
|
+
const nextKey = presentation === null
|
|
285
|
+
? null
|
|
286
|
+
: JSON.stringify([
|
|
287
|
+
presentation.step.id,
|
|
288
|
+
presentation.copy.title,
|
|
289
|
+
presentation.copy.description,
|
|
290
|
+
presentation.copy.next,
|
|
291
|
+
presentation.copy.done,
|
|
292
|
+
presentation.copy.skip,
|
|
293
|
+
presentation.acknowledgeOnTargetClick === true,
|
|
294
|
+
presentation.allowTargetInteraction === true,
|
|
295
|
+
presentation.finalStep === true,
|
|
296
|
+
]);
|
|
297
|
+
if (dismissedPresentationKey !== null
|
|
298
|
+
&& nextKey !== null
|
|
299
|
+
&& (dismissedPresentationKey !== nextKey || retryableDismissedKey === nextKey)) {
|
|
300
|
+
dismissedPresentationKey = null;
|
|
301
|
+
retryableDismissedKey = null;
|
|
302
|
+
}
|
|
303
|
+
if (activePresentationKey !== nextKey) {
|
|
304
|
+
missingTargetErrorKey = null;
|
|
305
|
+
}
|
|
306
|
+
if (activePresentationKey === nextKey) {
|
|
307
|
+
activePresentation = presentation;
|
|
308
|
+
scheduleRefresh();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
activePresentation = presentation;
|
|
312
|
+
activePresentationKey = nextKey;
|
|
313
|
+
const previousInstance = instance;
|
|
314
|
+
instance = null;
|
|
315
|
+
if (previousInstance !== null) {
|
|
316
|
+
destroyInstancePreservingTarget(previousInstance);
|
|
317
|
+
}
|
|
318
|
+
clearTutorialState(stateHost);
|
|
319
|
+
reportActive(false);
|
|
320
|
+
scheduleRefresh();
|
|
321
|
+
},
|
|
322
|
+
refresh: scheduleRefresh,
|
|
323
|
+
resetForReplay() {
|
|
324
|
+
dismissedPresentationKey = null;
|
|
325
|
+
retryableDismissedKey = null;
|
|
326
|
+
missingTargetErrorKey = null;
|
|
327
|
+
scheduleRefresh();
|
|
328
|
+
},
|
|
329
|
+
async waitForPendingSkip() {
|
|
330
|
+
while (pendingSkipOperations.size > 0) {
|
|
331
|
+
await Promise.allSettled(pendingSkipOperations);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
function rebindActiveTarget(currentInstance, presentation) {
|
|
336
|
+
if (presentation === null || presentation.step.target === null || !currentInstance.isActive()) {
|
|
337
|
+
applyDriverTargetMutation(() => currentInstance.refresh());
|
|
338
|
+
syncModalSemantics?.();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const presentationKey = activePresentationKey;
|
|
342
|
+
const target = resolvePresentationTarget(presentation, presentationKey);
|
|
343
|
+
if (destroyed
|
|
344
|
+
|| activePresentation !== presentation
|
|
345
|
+
|| activePresentationKey !== presentationKey
|
|
346
|
+
|| instance !== currentInstance
|
|
347
|
+
|| !currentInstance.isActive()) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (target === null) {
|
|
351
|
+
const policy = input.missingTarget ?? 'wait';
|
|
352
|
+
if (policy === 'unanchored') {
|
|
353
|
+
missingTargetErrorKey = null;
|
|
354
|
+
if (currentInstance.getActiveElement()?.id === 'driver-dummy-element') {
|
|
355
|
+
applyDriverTargetMutation(() => currentInstance.refresh());
|
|
356
|
+
syncModalSemantics?.();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const current = currentInstance.getActiveStep();
|
|
360
|
+
if (current === undefined) {
|
|
361
|
+
applyDriverTargetMutation(() => currentInstance.refresh());
|
|
362
|
+
syncModalSemantics?.();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
rebindTarget(currentInstance, presentation, undefined);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
destroyInstancePreservingTarget(currentInstance);
|
|
369
|
+
if (policy === 'error') {
|
|
370
|
+
reportMissingTarget(presentation, presentationKey);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
missingTargetErrorKey = null;
|
|
375
|
+
if (currentInstance.getActiveElement() === target) {
|
|
376
|
+
applyDriverTargetMutation(() => currentInstance.refresh());
|
|
377
|
+
syncModalSemantics?.();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const current = currentInstance.getActiveStep();
|
|
381
|
+
if (current === undefined) {
|
|
382
|
+
applyDriverTargetMutation(() => currentInstance.refresh());
|
|
383
|
+
syncModalSemantics?.();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
rebindTarget(currentInstance, presentation, target);
|
|
387
|
+
}
|
|
388
|
+
function rebindTarget(currentInstance, presentation, target) {
|
|
389
|
+
const previousTarget = currentInstance.getActiveElement();
|
|
390
|
+
targetSemanticsGuard?.restore();
|
|
391
|
+
const restorePreviousTarget = !(previousTarget instanceof HTMLElement)
|
|
392
|
+
|| previousTarget.id === 'driver-dummy-element'
|
|
393
|
+
? undefined
|
|
394
|
+
: captureTutorialTargetSemantics(previousTarget, target?.parentElement ?? null);
|
|
395
|
+
targetSemanticsGuard = target === undefined
|
|
396
|
+
? undefined
|
|
397
|
+
: preserveTutorialTargetSemantics(target, browserWindow);
|
|
398
|
+
const nextStep = createDriveStep(input, presentation, target, ownerDocument);
|
|
399
|
+
applyDriverTargetMutation(() => currentInstance.highlight(nextStep));
|
|
400
|
+
restorePreviousTarget?.();
|
|
401
|
+
syncModalSemantics?.();
|
|
402
|
+
}
|
|
403
|
+
function reportMissingTarget(presentation, presentationKey = activePresentationKey) {
|
|
404
|
+
if (presentationKey === null
|
|
405
|
+
|| presentationKey !== activePresentationKey
|
|
406
|
+
|| missingTargetErrorKey === presentationKey) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
missingTargetErrorKey = presentationKey;
|
|
410
|
+
input.onError?.(new Error(`Tutorial target was not found: ${String(presentation.step.target)}`));
|
|
411
|
+
}
|
|
412
|
+
function destroyInstancePreservingTarget(currentInstance) {
|
|
413
|
+
const target = currentInstance.getActiveElement();
|
|
414
|
+
targetSemanticsGuard?.restore();
|
|
415
|
+
const restoreAfterDestroy = !(target instanceof HTMLElement)
|
|
416
|
+
? undefined
|
|
417
|
+
: captureTutorialTargetSemantics(target);
|
|
418
|
+
targetRestorePending = true;
|
|
419
|
+
currentInstance.destroy();
|
|
420
|
+
targetRestorePending = false;
|
|
421
|
+
restoreAfterDestroy?.();
|
|
422
|
+
targetSemanticsGuard = undefined;
|
|
423
|
+
}
|
|
424
|
+
function applyDriverTargetMutation(mutation) {
|
|
425
|
+
return targetSemanticsGuard === undefined
|
|
426
|
+
? mutation()
|
|
427
|
+
: targetSemanticsGuard.applyDriverMutation(mutation);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
export function resolveVisibleTutorialTarget(input) {
|
|
431
|
+
const root = input.root ?? document;
|
|
432
|
+
const ownerDocument = root.nodeType === 9 ? root : root.ownerDocument;
|
|
433
|
+
if (ownerDocument === null) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const view = input.view ?? ownerDocument.defaultView;
|
|
437
|
+
if (view === null) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const attribute = input.targetAttribute ?? tutorialDomAttributes.target;
|
|
441
|
+
const selector = `[${attribute}="${escapeAttributeValue(input.target)}"]`;
|
|
442
|
+
const candidates = [...root.querySelectorAll(selector)];
|
|
443
|
+
return candidates.find((element) => isVisibleTutorialTarget(element, view))
|
|
444
|
+
?? candidates.find((element) => isRenderableTutorialTarget(element, view))
|
|
445
|
+
?? null;
|
|
446
|
+
}
|
|
447
|
+
export function isVisibleTutorialTarget(element, view = window) {
|
|
448
|
+
if (!isRenderableTutorialTarget(element, view)) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
let visible = intersectRect(element.getBoundingClientRect(), {
|
|
452
|
+
bottom: view.innerHeight,
|
|
453
|
+
left: 0,
|
|
454
|
+
right: view.innerWidth,
|
|
455
|
+
top: 0,
|
|
456
|
+
});
|
|
457
|
+
let ancestor = element.parentElement;
|
|
458
|
+
while (visible !== null && ancestor !== null) {
|
|
459
|
+
const style = view.getComputedStyle(ancestor);
|
|
460
|
+
if (style.display === 'none'
|
|
461
|
+
|| style.visibility === 'hidden'
|
|
462
|
+
|| Number.parseFloat(style.opacity || '1') <= 0) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if ([style.overflow, style.overflowX, style.overflowY].some((value) => value === 'auto' || value === 'clip' || value === 'hidden' || value === 'scroll')) {
|
|
466
|
+
visible = intersectRect(visible, ancestor.getBoundingClientRect());
|
|
467
|
+
}
|
|
468
|
+
ancestor = ancestor.parentElement;
|
|
469
|
+
}
|
|
470
|
+
return visible !== null && visible.right > visible.left && visible.bottom > visible.top;
|
|
471
|
+
}
|
|
472
|
+
export function bindTutorialReplayTrigger(input) {
|
|
473
|
+
let pending = false;
|
|
474
|
+
const listener = () => {
|
|
475
|
+
if (pending) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
pending = true;
|
|
479
|
+
void Promise.resolve()
|
|
480
|
+
.then(() => input.presenter.waitForPendingSkip())
|
|
481
|
+
.then(() => input.beforeReplay?.())
|
|
482
|
+
.then(() => input.presenter.waitForPendingSkip())
|
|
483
|
+
.then(() => input.director.replay())
|
|
484
|
+
.then(() => input.presenter.resetForReplay())
|
|
485
|
+
.catch((error) => input.onError?.(error))
|
|
486
|
+
.finally(() => {
|
|
487
|
+
pending = false;
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
input.element.addEventListener('click', listener);
|
|
491
|
+
return () => input.element.removeEventListener('click', listener);
|
|
492
|
+
}
|
|
493
|
+
function createDriveStep(input, presentation, target, ownerDocument) {
|
|
494
|
+
const { step } = presentation;
|
|
495
|
+
const actionGated = step.advance.kind === 'action' || step.advance.kind === 'signal';
|
|
496
|
+
const acknowledgeOnTargetClick = presentation.acknowledgeOnTargetClick === true
|
|
497
|
+
&& step.advance.kind === 'acknowledge'
|
|
498
|
+
&& step.target !== null
|
|
499
|
+
&& target !== undefined;
|
|
500
|
+
const targetInteraction = presentation.allowTargetInteraction === true
|
|
501
|
+
|| acknowledgeOnTargetClick
|
|
502
|
+
|| step.interaction === 'target'
|
|
503
|
+
|| step.interaction === 'gameplay';
|
|
504
|
+
return {
|
|
505
|
+
...(target === undefined ? {} : { element: target }),
|
|
506
|
+
advanceOnClick: acknowledgeOnTargetClick,
|
|
507
|
+
disableActiveInteraction: !targetInteraction,
|
|
508
|
+
popover: {
|
|
509
|
+
...(step.side === undefined ? {} : { side: step.side }),
|
|
510
|
+
...(step.align === undefined ? {} : { align: step.align }),
|
|
511
|
+
description: escapeTutorialText(ownerDocument, presentation.copy.description),
|
|
512
|
+
doneBtnText: presentation.finalStep === true
|
|
513
|
+
? escapeTutorialText(ownerDocument, presentation.copy.done)
|
|
514
|
+
: escapeTutorialText(ownerDocument, presentation.copy.next),
|
|
515
|
+
nextBtnText: presentation.finalStep === true
|
|
516
|
+
? escapeTutorialText(ownerDocument, presentation.copy.done)
|
|
517
|
+
: escapeTutorialText(ownerDocument, presentation.copy.next),
|
|
518
|
+
showButtons: actionGated || acknowledgeOnTargetClick
|
|
519
|
+
? ['close']
|
|
520
|
+
: ['next', 'close'],
|
|
521
|
+
title: escapeTutorialText(ownerDocument, presentation.copy.title),
|
|
522
|
+
onNextClick: () => input.onAcknowledge(step.id),
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function resolveUnderlyingTutorialModal(popover, interaction, activeElement, modalSelector, currentModal) {
|
|
527
|
+
const activeModal = activeElement?.closest(modalSelector) ?? null;
|
|
528
|
+
if (interaction !== 'blocked') {
|
|
529
|
+
return activeModal;
|
|
530
|
+
}
|
|
531
|
+
if (activeElement !== undefined
|
|
532
|
+
&& currentModal?.isConnected === true
|
|
533
|
+
&& currentModal.contains(activeElement)) {
|
|
534
|
+
return currentModal;
|
|
535
|
+
}
|
|
536
|
+
return activeModal
|
|
537
|
+
?? [...popover.ownerDocument.querySelectorAll(modalSelector)]
|
|
538
|
+
.find((candidate) => candidate !== popover && !popover.contains(candidate))
|
|
539
|
+
?? (currentModal?.isConnected === true ? currentModal : null)
|
|
540
|
+
?? null;
|
|
541
|
+
}
|
|
542
|
+
function configureTutorialModalSemantics(popover, interaction, underlyingModal) {
|
|
543
|
+
const ownedAttributes = new Map();
|
|
544
|
+
if (interaction === 'blocked') {
|
|
545
|
+
popover.setAttribute('role', 'dialog');
|
|
546
|
+
popover.setAttribute('aria-modal', 'true');
|
|
547
|
+
if (underlyingModal !== null) {
|
|
548
|
+
ownedAttributes.set('aria-modal', 'false');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
popover.setAttribute('role', 'region');
|
|
553
|
+
popover.removeAttribute('aria-modal');
|
|
554
|
+
if (underlyingModal !== null && popover.id !== '') {
|
|
555
|
+
const ownedIds = new Set((underlyingModal.getAttribute('aria-owns') ?? '').split(/\s+/u).filter(Boolean));
|
|
556
|
+
ownedIds.add(popover.id);
|
|
557
|
+
ownedAttributes.set('aria-owns', [...ownedIds].join(' '));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const restore = underlyingModal === null
|
|
561
|
+
? () => undefined
|
|
562
|
+
: preserveTutorialModalSemantics(underlyingModal, ownedAttributes, popover.ownerDocument);
|
|
563
|
+
return { modal: underlyingModal, popover, restore };
|
|
564
|
+
}
|
|
565
|
+
function preserveTutorialModalSemantics(element, ownedAttributes, ownerDocument) {
|
|
566
|
+
const view = ownerDocument.defaultView;
|
|
567
|
+
if (view === null || ownedAttributes.size === 0) {
|
|
568
|
+
return () => undefined;
|
|
569
|
+
}
|
|
570
|
+
const previous = new Map();
|
|
571
|
+
const hostValues = new Map();
|
|
572
|
+
const captureHostMutations = (records) => {
|
|
573
|
+
for (const record of records) {
|
|
574
|
+
const attribute = record.attributeName;
|
|
575
|
+
if (record.target === element && isTutorialModalAttribute(attribute)) {
|
|
576
|
+
hostValues.set(attribute, element.getAttribute(attribute));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
for (const [attribute, value] of ownedAttributes) {
|
|
581
|
+
const current = element.getAttribute(attribute);
|
|
582
|
+
if (current === value) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
previous.set(attribute, current);
|
|
586
|
+
element.setAttribute(attribute, value);
|
|
587
|
+
}
|
|
588
|
+
if (previous.size === 0) {
|
|
589
|
+
return () => undefined;
|
|
590
|
+
}
|
|
591
|
+
const observer = new view.MutationObserver(captureHostMutations);
|
|
592
|
+
observer.observe(element, {
|
|
593
|
+
attributeFilter: [...previous.keys()],
|
|
594
|
+
attributes: true,
|
|
595
|
+
});
|
|
596
|
+
return () => {
|
|
597
|
+
captureHostMutations(observer.takeRecords());
|
|
598
|
+
observer.disconnect();
|
|
599
|
+
for (const [attribute, previousValue] of previous) {
|
|
600
|
+
if (hostValues.has(attribute)) {
|
|
601
|
+
restoreAttribute(element, attribute, hostValues.get(attribute) ?? null);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
restoreOwnedAttribute(element, attribute, ownedAttributes.get(attribute) ?? null, previousValue);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function isTutorialModalAttribute(attribute) {
|
|
609
|
+
return attribute === 'aria-modal' || attribute === 'aria-owns';
|
|
610
|
+
}
|
|
611
|
+
function preserveTutorialTargetSemantics(element, view) {
|
|
612
|
+
const attributes = ['aria-controls', 'aria-expanded', 'aria-haspopup'];
|
|
613
|
+
const previous = Object.fromEntries(attributes.map((attribute) => [attribute, element.getAttribute(attribute)]));
|
|
614
|
+
const driverOwned = {
|
|
615
|
+
'aria-controls': 'driver-popover-content',
|
|
616
|
+
'aria-expanded': 'true',
|
|
617
|
+
'aria-haspopup': 'dialog',
|
|
618
|
+
};
|
|
619
|
+
const hostValues = new Map();
|
|
620
|
+
const decoratedParent = element.parentElement;
|
|
621
|
+
let active = true;
|
|
622
|
+
const captureHostMutations = (records) => {
|
|
623
|
+
for (const record of records) {
|
|
624
|
+
const attribute = record.attributeName;
|
|
625
|
+
if (record.target === element && isTutorialTargetAttribute(attribute)) {
|
|
626
|
+
hostValues.set(attribute, element.getAttribute(attribute));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const observer = new view.MutationObserver(captureHostMutations);
|
|
631
|
+
observer.observe(element, {
|
|
632
|
+
attributeFilter: [...attributes],
|
|
633
|
+
attributes: true,
|
|
634
|
+
});
|
|
635
|
+
return {
|
|
636
|
+
applyDriverMutation(mutation) {
|
|
637
|
+
captureHostMutations(observer.takeRecords());
|
|
638
|
+
try {
|
|
639
|
+
return mutation();
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
observer.takeRecords();
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
restore(driverAlreadyDestroyed = false) {
|
|
646
|
+
if (!active) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (driverAlreadyDestroyed) {
|
|
650
|
+
observer.takeRecords();
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
captureHostMutations(observer.takeRecords());
|
|
654
|
+
}
|
|
655
|
+
observer.disconnect();
|
|
656
|
+
active = false;
|
|
657
|
+
for (const attribute of attributes) {
|
|
658
|
+
if (hostValues.has(attribute)) {
|
|
659
|
+
restoreAttribute(element, attribute, hostValues.get(attribute) ?? null);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const current = element.getAttribute(attribute);
|
|
663
|
+
if (current === driverOwned[attribute] || (driverAlreadyDestroyed && current === null)) {
|
|
664
|
+
restoreAttribute(element, attribute, previous[attribute]);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
clearDetachedDriverTargetClasses(element, decoratedParent);
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function isTutorialTargetAttribute(attribute) {
|
|
672
|
+
return attribute === 'aria-controls'
|
|
673
|
+
|| attribute === 'aria-expanded'
|
|
674
|
+
|| attribute === 'aria-haspopup';
|
|
675
|
+
}
|
|
676
|
+
function captureTutorialTargetSemantics(element, preservedParent = null) {
|
|
677
|
+
const attributes = ['aria-controls', 'aria-expanded', 'aria-haspopup'];
|
|
678
|
+
const decoratedParent = element.parentElement;
|
|
679
|
+
const captured = Object.fromEntries(attributes.map((attribute) => [attribute, element.getAttribute(attribute)]));
|
|
680
|
+
return () => {
|
|
681
|
+
for (const attribute of attributes) {
|
|
682
|
+
restoreAttribute(element, attribute, captured[attribute]);
|
|
683
|
+
}
|
|
684
|
+
clearDetachedDriverTargetClasses(element, decoratedParent, preservedParent);
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function clearDetachedDriverTargetClasses(element, decoratedParent, preservedParent = null) {
|
|
688
|
+
element.classList.remove('driver-active-element', 'driver-no-interaction');
|
|
689
|
+
if (decoratedParent !== preservedParent) {
|
|
690
|
+
decoratedParent?.classList.remove('driver-active-element-parent', 'driver-active-element-parent-no-scroll');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function isRenderableTutorialTarget(element, view) {
|
|
694
|
+
const rect = element.getBoundingClientRect();
|
|
695
|
+
if (!element.isConnected || rect.width <= 0 || rect.height <= 0) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
let current = element;
|
|
699
|
+
while (current !== null) {
|
|
700
|
+
const style = view.getComputedStyle(current);
|
|
701
|
+
if (style.display === 'none'
|
|
702
|
+
|| style.visibility === 'hidden'
|
|
703
|
+
|| Number.parseFloat(style.opacity || '1') <= 0) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
current = current.parentElement;
|
|
707
|
+
}
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
function intersectRect(left, right) {
|
|
711
|
+
const intersection = {
|
|
712
|
+
bottom: Math.min(left.bottom, right.bottom),
|
|
713
|
+
left: Math.max(left.left, right.left),
|
|
714
|
+
right: Math.min(left.right, right.right),
|
|
715
|
+
top: Math.max(left.top, right.top),
|
|
716
|
+
};
|
|
717
|
+
return intersection.right > intersection.left && intersection.bottom > intersection.top
|
|
718
|
+
? intersection
|
|
719
|
+
: null;
|
|
720
|
+
}
|
|
721
|
+
function clearTutorialState(stateHost) {
|
|
722
|
+
stateHost.removeAttribute(tutorialDomAttributes.active);
|
|
723
|
+
stateHost.removeAttribute(tutorialDomAttributes.step);
|
|
724
|
+
}
|
|
725
|
+
function restoreAttribute(element, name, value) {
|
|
726
|
+
if (value === null) {
|
|
727
|
+
element.removeAttribute(name);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
element.setAttribute(name, value);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function restoreOwnedAttribute(element, name, ownedValue, previousValue) {
|
|
734
|
+
if (element.getAttribute(name) !== ownedValue) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
restoreAttribute(element, name, previousValue);
|
|
738
|
+
}
|
|
739
|
+
function isDriverOwnedMutation(record) {
|
|
740
|
+
const target = record.target;
|
|
741
|
+
if (target.nodeType !== 1) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
const element = target;
|
|
745
|
+
return element.matches('.driver-overlay, .driver-popover, .driver-popover *')
|
|
746
|
+
|| element.closest('.driver-overlay, .driver-popover') !== null;
|
|
747
|
+
}
|
|
748
|
+
function escapeAttributeValue(value) {
|
|
749
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
750
|
+
}
|
|
751
|
+
function escapeTutorialText(ownerDocument, value) {
|
|
752
|
+
const element = ownerDocument.createElement('span');
|
|
753
|
+
element.textContent = value;
|
|
754
|
+
return element.innerHTML;
|
|
755
|
+
}
|