@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/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
+ }