@glowhop/core-tour 1.0.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/CHANGELOG.md +7 -0
- package/LICENSE +22 -0
- package/README.md +50 -0
- package/adapter.d.ts +35 -0
- package/adapter.js +84 -0
- package/builder/index.d.ts +148 -0
- package/config/from-config.d.ts +26 -0
- package/config/index.d.ts +6 -0
- package/config/index.js +999 -0
- package/config/types.d.ts +136 -0
- package/config/validate.d.ts +37 -0
- package/core.browser.d.ts +1 -0
- package/definition/index.d.ts +3 -0
- package/definition/step-props.d.ts +13 -0
- package/definition/types.d.ts +38 -0
- package/definition/workflow-definition.d.ts +28 -0
- package/dom/dom-mutation-lease.d.ts +13 -0
- package/dom/tour-view-driver.d.ts +113 -0
- package/elements/base.d.ts +62 -0
- package/elements/idle-presentation.d.ts +62 -0
- package/elements/overlay.d.ts +22 -0
- package/elements/pointer.d.ts +24 -0
- package/elements/popover-arrow-styles.d.ts +5 -0
- package/elements/popover.d.ts +33 -0
- package/index.d.ts +4 -0
- package/index.js +3889 -0
- package/options/validation.d.ts +3 -0
- package/package.json +47 -0
- package/runtime/abort.d.ts +2 -0
- package/runtime/active-step.d.ts +72 -0
- package/runtime/adapter-contract.d.ts +23 -0
- package/runtime/root-bridge.d.ts +6 -0
- package/runtime/step-props-store.d.ts +3 -0
- package/runtime/tour-controller.d.ts +107 -0
- package/state/focus-guard.d.ts +32 -0
- package/state/focusable.d.ts +6 -0
- package/state/scroll-lock.d.ts +18 -0
- package/types/index.d.ts +459 -0
- package/utils/options.d.ts +7 -0
- package/utils/utils.d.ts +28 -0
package/index.js
ADDED
|
@@ -0,0 +1,3889 @@
|
|
|
1
|
+
// packages/core/src/definition/step-props.ts
|
|
2
|
+
function cloneStepProps(props) {
|
|
3
|
+
return {
|
|
4
|
+
title: props.title,
|
|
5
|
+
content: props.content,
|
|
6
|
+
data: props.data === undefined ? undefined : structuredClone(props.data),
|
|
7
|
+
overlay: props.overlay && {
|
|
8
|
+
...props.overlay,
|
|
9
|
+
animation: props.overlay.animation && { ...props.overlay.animation }
|
|
10
|
+
},
|
|
11
|
+
popover: props.popover && {
|
|
12
|
+
...props.popover,
|
|
13
|
+
animation: props.popover.animation && { ...props.popover.animation },
|
|
14
|
+
arrow: props.popover.arrow && { ...props.popover.arrow },
|
|
15
|
+
keyboardShortcuts: props.popover.keyboardShortcuts && {
|
|
16
|
+
previous: props.popover.keyboardShortcuts.previous && [
|
|
17
|
+
...props.popover.keyboardShortcuts.previous
|
|
18
|
+
],
|
|
19
|
+
advance: props.popover.keyboardShortcuts.advance && [
|
|
20
|
+
...props.popover.keyboardShortcuts.advance
|
|
21
|
+
],
|
|
22
|
+
cancel: props.popover.keyboardShortcuts.cancel && [
|
|
23
|
+
...props.popover.keyboardShortcuts.cancel
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
placementTryOrder: props.popover.placementTryOrder && [...props.popover.placementTryOrder]
|
|
27
|
+
},
|
|
28
|
+
indicator: props.indicator && {
|
|
29
|
+
...props.indicator,
|
|
30
|
+
animation: props.indicator.animation && { ...props.indicator.animation },
|
|
31
|
+
placementTryOrder: props.indicator.placementTryOrder && [
|
|
32
|
+
...props.indicator.placementTryOrder
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function freezeStepProps(props) {
|
|
38
|
+
const cloned = cloneStepProps(props);
|
|
39
|
+
if (cloned.data)
|
|
40
|
+
Object.freeze(cloned.data);
|
|
41
|
+
if (cloned.overlay?.animation)
|
|
42
|
+
Object.freeze(cloned.overlay.animation);
|
|
43
|
+
if (cloned.overlay)
|
|
44
|
+
Object.freeze(cloned.overlay);
|
|
45
|
+
if (cloned.popover?.animation)
|
|
46
|
+
Object.freeze(cloned.popover.animation);
|
|
47
|
+
if (cloned.popover?.arrow)
|
|
48
|
+
Object.freeze(cloned.popover.arrow);
|
|
49
|
+
if (cloned.popover?.keyboardShortcuts?.previous)
|
|
50
|
+
Object.freeze(cloned.popover.keyboardShortcuts.previous);
|
|
51
|
+
if (cloned.popover?.keyboardShortcuts?.advance)
|
|
52
|
+
Object.freeze(cloned.popover.keyboardShortcuts.advance);
|
|
53
|
+
if (cloned.popover?.keyboardShortcuts?.cancel)
|
|
54
|
+
Object.freeze(cloned.popover.keyboardShortcuts.cancel);
|
|
55
|
+
if (cloned.popover?.keyboardShortcuts)
|
|
56
|
+
Object.freeze(cloned.popover.keyboardShortcuts);
|
|
57
|
+
if (cloned.popover?.placementTryOrder)
|
|
58
|
+
Object.freeze(cloned.popover.placementTryOrder);
|
|
59
|
+
if (cloned.popover)
|
|
60
|
+
Object.freeze(cloned.popover);
|
|
61
|
+
if (cloned.indicator?.animation)
|
|
62
|
+
Object.freeze(cloned.indicator.animation);
|
|
63
|
+
if (cloned.indicator?.placementTryOrder)
|
|
64
|
+
Object.freeze(cloned.indicator.placementTryOrder);
|
|
65
|
+
if (cloned.indicator)
|
|
66
|
+
Object.freeze(cloned.indicator);
|
|
67
|
+
return Object.freeze(cloned);
|
|
68
|
+
}
|
|
69
|
+
// packages/core/src/definition/workflow-definition.ts
|
|
70
|
+
function freezeRecord(value) {
|
|
71
|
+
return Object.freeze(value);
|
|
72
|
+
}
|
|
73
|
+
function freezeAnimation(options) {
|
|
74
|
+
return options && freezeRecord({ ...options });
|
|
75
|
+
}
|
|
76
|
+
function freezeOverlay(options) {
|
|
77
|
+
return options && freezeRecord({
|
|
78
|
+
...options,
|
|
79
|
+
animation: freezeAnimation(options.animation)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function freezePopover(options) {
|
|
83
|
+
return options && freezeRecord({
|
|
84
|
+
...options,
|
|
85
|
+
animation: freezeAnimation(options.animation),
|
|
86
|
+
arrow: options.arrow && freezeRecord({ ...options.arrow }),
|
|
87
|
+
keyboardShortcuts: options.keyboardShortcuts && freezeRecord({
|
|
88
|
+
previous: options.keyboardShortcuts.previous && freezeRecord([...options.keyboardShortcuts.previous]),
|
|
89
|
+
advance: options.keyboardShortcuts.advance && freezeRecord([...options.keyboardShortcuts.advance]),
|
|
90
|
+
cancel: options.keyboardShortcuts.cancel && freezeRecord([...options.keyboardShortcuts.cancel])
|
|
91
|
+
}),
|
|
92
|
+
placementTryOrder: options.placementTryOrder && freezeRecord([...options.placementTryOrder])
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function freezeIndicator(options) {
|
|
96
|
+
return options && freezeRecord({
|
|
97
|
+
...options,
|
|
98
|
+
animation: freezeAnimation(options.animation),
|
|
99
|
+
placementTryOrder: options.placementTryOrder && freezeRecord([...options.placementTryOrder])
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function freezeStep(draft) {
|
|
103
|
+
return freezeRecord({
|
|
104
|
+
id: draft.id,
|
|
105
|
+
target: draft.target,
|
|
106
|
+
resetPropsOnEnter: draft.resetPropsOnEnter,
|
|
107
|
+
props: freezeStepProps(draft.props),
|
|
108
|
+
behavior: draft.behavior && freezeRecord({
|
|
109
|
+
...draft.behavior,
|
|
110
|
+
scroll: draft.behavior.scroll && freezeRecord({ ...draft.behavior.scroll })
|
|
111
|
+
}),
|
|
112
|
+
actions: freezeRecord(draft.actions.map((action) => action)),
|
|
113
|
+
eventHandlers: freezeRecord(draft.eventHandlers.map((handler) => freezeRecord({ ...handler }))),
|
|
114
|
+
advanceAction: draft.advanceAction,
|
|
115
|
+
previousAction: draft.previousAction,
|
|
116
|
+
cancelAction: draft.cancelAction
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function freezeOptions(options) {
|
|
120
|
+
return freezeRecord({
|
|
121
|
+
...options,
|
|
122
|
+
overlay: freezeOverlay(options.overlay),
|
|
123
|
+
popover: freezePopover(options.popover),
|
|
124
|
+
indicator: freezeIndicator(options.indicator),
|
|
125
|
+
behavior: options.behavior && freezeRecord({
|
|
126
|
+
...options.behavior,
|
|
127
|
+
scroll: options.behavior.scroll && freezeRecord({ ...options.behavior.scroll })
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function cloneWorkflowStepDraft(definition) {
|
|
132
|
+
return {
|
|
133
|
+
...definition,
|
|
134
|
+
props: cloneStepProps(definition.props),
|
|
135
|
+
actions: definition.actions.map((action) => action),
|
|
136
|
+
eventHandlers: [...definition.eventHandlers]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function assertUniqueStepIds(name, drafts) {
|
|
140
|
+
const seen = new Map;
|
|
141
|
+
for (const [index, draft] of drafts.entries()) {
|
|
142
|
+
const label = `step ${index}${draft.props.title ? ` ("${String(draft.props.title)}")` : ""}`;
|
|
143
|
+
if (typeof draft.id !== "string" || draft.id.length === 0) {
|
|
144
|
+
throw new Error(`Workflow "${name}": ${label} is missing a non-empty "id".`);
|
|
145
|
+
}
|
|
146
|
+
const duplicate = seen.get(draft.id);
|
|
147
|
+
if (duplicate !== undefined) {
|
|
148
|
+
throw new Error(`Workflow "${name}": ${label} reuses the id "${draft.id}" already used by step ${duplicate}. Step ids must be unique.`);
|
|
149
|
+
}
|
|
150
|
+
seen.set(draft.id, index);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function createWorkflowDefinition(name, options, drafts) {
|
|
154
|
+
assertUniqueStepIds(name, drafts);
|
|
155
|
+
return freezeRecord({
|
|
156
|
+
name,
|
|
157
|
+
options: freezeOptions(options),
|
|
158
|
+
steps: freezeRecord(drafts.map(freezeStep))
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// packages/core/src/runtime/abort.ts
|
|
162
|
+
function abortError() {
|
|
163
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
164
|
+
}
|
|
165
|
+
function abortableDelay(delay, signal) {
|
|
166
|
+
if (signal.aborted)
|
|
167
|
+
throw abortError();
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
170
|
+
const onAbort = () => {
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
cleanup();
|
|
173
|
+
reject(abortError());
|
|
174
|
+
};
|
|
175
|
+
const timeoutId = setTimeout(() => {
|
|
176
|
+
cleanup();
|
|
177
|
+
resolve();
|
|
178
|
+
}, delay);
|
|
179
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// packages/core/src/builder/index.ts
|
|
184
|
+
var DEFAULT_WAIT_INTERVAL = 16;
|
|
185
|
+
var DEFAULT_WAIT_TIMEOUT = 3000;
|
|
186
|
+
var INACTIVE_STEP_ERROR = "WorkflowStepBuilder is no longer active";
|
|
187
|
+
var STEP_BUILDER_INTERNAL = Symbol("WorkflowStepBuilder.internal");
|
|
188
|
+
function assertTimingValue(name, value) {
|
|
189
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
190
|
+
throw new TypeError(`${name} must be a finite non-negative number`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function throwIfAborted(signal) {
|
|
194
|
+
if (signal.aborted)
|
|
195
|
+
throw abortError();
|
|
196
|
+
}
|
|
197
|
+
function waitTimeoutError(timeout) {
|
|
198
|
+
return new Error(`waitUntil timed out after ${timeout}ms`);
|
|
199
|
+
}
|
|
200
|
+
function waitForPredicate(predicate, remainingTime, timeout, signal) {
|
|
201
|
+
throwIfAborted(signal);
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
let settled = false;
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
clearTimeout(timeoutId);
|
|
206
|
+
signal.removeEventListener("abort", onAbort);
|
|
207
|
+
};
|
|
208
|
+
const settle = (callback) => {
|
|
209
|
+
if (settled)
|
|
210
|
+
return;
|
|
211
|
+
settled = true;
|
|
212
|
+
cleanup();
|
|
213
|
+
callback();
|
|
214
|
+
};
|
|
215
|
+
const onAbort = () => settle(() => reject(abortError()));
|
|
216
|
+
const timeoutId = setTimeout(() => settle(() => reject(waitTimeoutError(timeout))), remainingTime);
|
|
217
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
218
|
+
Promise.resolve().then(predicate).then((result) => settle(() => resolve(result)), (error) => settle(() => reject(error)));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function waitOptions(options = {}) {
|
|
222
|
+
const timeout = options.timeout ?? DEFAULT_WAIT_TIMEOUT;
|
|
223
|
+
const interval = options.interval ?? DEFAULT_WAIT_INTERVAL;
|
|
224
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
225
|
+
throw new TypeError("wait timeout must be a finite non-negative number");
|
|
226
|
+
}
|
|
227
|
+
if (!Number.isFinite(interval) || interval <= 0) {
|
|
228
|
+
throw new TypeError("wait interval must be a finite positive number");
|
|
229
|
+
}
|
|
230
|
+
return { interval, timeout };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class WorkflowBuilder {
|
|
234
|
+
name;
|
|
235
|
+
options;
|
|
236
|
+
steps = [];
|
|
237
|
+
currentStep = null;
|
|
238
|
+
definition = null;
|
|
239
|
+
constructor(name, options = {}) {
|
|
240
|
+
this.name = name;
|
|
241
|
+
this.options = options;
|
|
242
|
+
}
|
|
243
|
+
step(options) {
|
|
244
|
+
this.assertBuilding();
|
|
245
|
+
this.commitCurrentStep();
|
|
246
|
+
this.currentStep = new WorkflowStepBuilder(this, {
|
|
247
|
+
id: options.id,
|
|
248
|
+
target: options.target,
|
|
249
|
+
resetPropsOnEnter: options.resetPropsOnEnter,
|
|
250
|
+
props: {
|
|
251
|
+
title: options.title,
|
|
252
|
+
content: options.content,
|
|
253
|
+
data: cloneStepProps(options).data,
|
|
254
|
+
overlay: options.overlay,
|
|
255
|
+
popover: options.popover,
|
|
256
|
+
indicator: options.indicator
|
|
257
|
+
},
|
|
258
|
+
behavior: options.behavior,
|
|
259
|
+
actions: [],
|
|
260
|
+
eventHandlers: [],
|
|
261
|
+
advanceAction: null,
|
|
262
|
+
previousAction: null,
|
|
263
|
+
cancelAction: null
|
|
264
|
+
});
|
|
265
|
+
return this.currentStep;
|
|
266
|
+
}
|
|
267
|
+
append(workflow) {
|
|
268
|
+
this.assertBuilding();
|
|
269
|
+
const drafts = workflow.steps.map(cloneWorkflowStepDraft);
|
|
270
|
+
const lastStep = drafts.at(-1);
|
|
271
|
+
if (!lastStep)
|
|
272
|
+
throw new Error("Cannot append a workflow without steps");
|
|
273
|
+
this.commitCurrentStep();
|
|
274
|
+
this.steps.push(...drafts.slice(0, -1));
|
|
275
|
+
this.currentStep = new WorkflowStepBuilder(this, lastStep);
|
|
276
|
+
return this.currentStep;
|
|
277
|
+
}
|
|
278
|
+
build() {
|
|
279
|
+
if (this.definition)
|
|
280
|
+
return this.definition;
|
|
281
|
+
this.commitCurrentStep();
|
|
282
|
+
this.definition = createWorkflowDefinition(this.name, this.options, this.steps);
|
|
283
|
+
return this.definition;
|
|
284
|
+
}
|
|
285
|
+
assertBuilding() {
|
|
286
|
+
if (this.definition)
|
|
287
|
+
throw new Error("WorkflowBuilder is already finished");
|
|
288
|
+
}
|
|
289
|
+
commitCurrentStep() {
|
|
290
|
+
if (!this.currentStep)
|
|
291
|
+
return;
|
|
292
|
+
this.steps.push(this.currentStep[STEP_BUILDER_INTERNAL]());
|
|
293
|
+
this.currentStep = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class WorkflowStepBuilder {
|
|
298
|
+
owner;
|
|
299
|
+
draft;
|
|
300
|
+
active = true;
|
|
301
|
+
constructor(owner, draft) {
|
|
302
|
+
this.owner = owner;
|
|
303
|
+
this.draft = draft;
|
|
304
|
+
}
|
|
305
|
+
step(options) {
|
|
306
|
+
this.assertActive();
|
|
307
|
+
return this.owner.step(options);
|
|
308
|
+
}
|
|
309
|
+
append(workflow) {
|
|
310
|
+
this.assertActive();
|
|
311
|
+
return this.owner.append(workflow);
|
|
312
|
+
}
|
|
313
|
+
build() {
|
|
314
|
+
this.assertActive();
|
|
315
|
+
return this.owner.build();
|
|
316
|
+
}
|
|
317
|
+
clickTarget() {
|
|
318
|
+
return this.do(({ target }) => {
|
|
319
|
+
target?.click();
|
|
320
|
+
return true;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
focusTarget() {
|
|
324
|
+
return this.do(({ target }) => {
|
|
325
|
+
target?.focus();
|
|
326
|
+
return true;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
wait(timeMs) {
|
|
330
|
+
this.assertActive();
|
|
331
|
+
assertTimingValue("timeMs", timeMs);
|
|
332
|
+
this.draft.actions.push(timeMs);
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
waitUntil(predicate, options = {}) {
|
|
336
|
+
this.assertActive();
|
|
337
|
+
const { interval, timeout } = waitOptions(options);
|
|
338
|
+
this.draft.actions.push(async (context) => {
|
|
339
|
+
const startedAt = Date.now();
|
|
340
|
+
let attempted = false;
|
|
341
|
+
while (true) {
|
|
342
|
+
throwIfAborted(context.signal);
|
|
343
|
+
const elapsed = Date.now() - startedAt;
|
|
344
|
+
if (attempted && elapsed >= timeout)
|
|
345
|
+
throw waitTimeoutError(timeout);
|
|
346
|
+
attempted = true;
|
|
347
|
+
if (await waitForPredicate(() => predicate(context), Math.max(0, timeout - elapsed), timeout, context.signal))
|
|
348
|
+
return true;
|
|
349
|
+
const remainingTime = timeout - (Date.now() - startedAt);
|
|
350
|
+
if (remainingTime <= 0)
|
|
351
|
+
throw waitTimeoutError(timeout);
|
|
352
|
+
await abortableDelay(Math.min(interval, remainingTime), context.signal);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
waitUntilElement(selector, options) {
|
|
358
|
+
if (selector.length === 0)
|
|
359
|
+
throw new TypeError("selector must not be empty");
|
|
360
|
+
return this.waitUntil((context) => context.target.ownerDocument.querySelector(selector) !== null, options);
|
|
361
|
+
}
|
|
362
|
+
do(callback) {
|
|
363
|
+
this.assertActive();
|
|
364
|
+
this.draft.actions.push(callback);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
beforeAdvance(callback) {
|
|
368
|
+
this.assertActive();
|
|
369
|
+
this.draft.advanceAction = callback;
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
beforePrevious(callback) {
|
|
373
|
+
this.assertActive();
|
|
374
|
+
this.draft.previousAction = callback;
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
beforeCancel(callback) {
|
|
378
|
+
this.assertActive();
|
|
379
|
+
this.draft.cancelAction = callback;
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
onTargetEvent(eventOrEvents, callback) {
|
|
383
|
+
this.assertActive();
|
|
384
|
+
const events = typeof eventOrEvents === "string" ? [eventOrEvents] : eventOrEvents;
|
|
385
|
+
if (events.length === 0)
|
|
386
|
+
throw new TypeError("events must not be empty");
|
|
387
|
+
for (const event of events) {
|
|
388
|
+
if (event.length === 0)
|
|
389
|
+
throw new TypeError("event name must not be empty");
|
|
390
|
+
this.draft.eventHandlers.push({
|
|
391
|
+
event,
|
|
392
|
+
callback
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
[STEP_BUILDER_INTERNAL]() {
|
|
398
|
+
this.active = false;
|
|
399
|
+
return cloneWorkflowStepDraft(this.draft);
|
|
400
|
+
}
|
|
401
|
+
assertActive() {
|
|
402
|
+
if (!this.active)
|
|
403
|
+
throw new Error(INACTIVE_STEP_ERROR);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// packages/core/src/utils/utils.ts
|
|
408
|
+
function ownerWindow(element) {
|
|
409
|
+
return element?.ownerDocument?.defaultView ?? (typeof window === "undefined" ? null : window);
|
|
410
|
+
}
|
|
411
|
+
function ownerDocument(element) {
|
|
412
|
+
return element?.ownerDocument ?? (typeof document === "undefined" ? null : document);
|
|
413
|
+
}
|
|
414
|
+
function isHTMLElement(value, context) {
|
|
415
|
+
const HTMLElement2 = ownerWindow(context)?.HTMLElement ?? globalThis.HTMLElement;
|
|
416
|
+
return typeof HTMLElement2 === "function" && value instanceof HTMLElement2;
|
|
417
|
+
}
|
|
418
|
+
function isElement(value, context) {
|
|
419
|
+
const Element = ownerWindow(context)?.Element ?? globalThis.Element;
|
|
420
|
+
return typeof Element === "function" && value instanceof Element;
|
|
421
|
+
}
|
|
422
|
+
function isNode(value, context) {
|
|
423
|
+
const Node = ownerWindow(context)?.Node ?? globalThis.Node;
|
|
424
|
+
return typeof Node === "function" && value instanceof Node;
|
|
425
|
+
}
|
|
426
|
+
function viewportDimensions(context) {
|
|
427
|
+
const currentWindow = ownerWindow(context);
|
|
428
|
+
return {
|
|
429
|
+
width: currentWindow?.innerWidth ?? 1024,
|
|
430
|
+
height: currentWindow?.innerHeight ?? 768
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function isInViewport(rect, context) {
|
|
434
|
+
const viewport = viewportDimensions(context);
|
|
435
|
+
return rect.left >= 0 && rect.top >= 0 && rect.right <= viewport.width && rect.bottom <= viewport.height;
|
|
436
|
+
}
|
|
437
|
+
function roundByDPR(value, context) {
|
|
438
|
+
const dpr = ownerWindow(context)?.devicePixelRatio || 1;
|
|
439
|
+
return Math.round(value * dpr) / dpr;
|
|
440
|
+
}
|
|
441
|
+
function roundedRectPath(rect, viewport, options, context) {
|
|
442
|
+
const padding = options.padding;
|
|
443
|
+
const radius = options.radius;
|
|
444
|
+
const x = roundByDPR(rect.left - padding, context);
|
|
445
|
+
const y = roundByDPR(rect.top - padding, context);
|
|
446
|
+
const width = rect.width + padding * 2;
|
|
447
|
+
const height = rect.height + padding * 2;
|
|
448
|
+
const right = roundByDPR(rect.left + rect.width + padding, context);
|
|
449
|
+
const bottom = roundByDPR(rect.top + rect.height + padding, context);
|
|
450
|
+
const corner = roundByDPR(Math.max(0, Math.min(radius, width / 2, height / 2)), context);
|
|
451
|
+
return [
|
|
452
|
+
`M0,0 H${roundByDPR(viewport.width, context)} V${roundByDPR(viewport.height, context)} H0 Z`,
|
|
453
|
+
`M${x},${y + corner}`,
|
|
454
|
+
`Q${x},${y} ${x + corner},${y}`,
|
|
455
|
+
`H${right - corner}`,
|
|
456
|
+
`Q${right},${y} ${right},${y + corner}`,
|
|
457
|
+
`V${bottom - corner}`,
|
|
458
|
+
`Q${right},${bottom} ${right - corner},${bottom}`,
|
|
459
|
+
`H${x + corner}`,
|
|
460
|
+
`Q${x},${bottom} ${x},${bottom - corner}`,
|
|
461
|
+
"Z"
|
|
462
|
+
].join(" ");
|
|
463
|
+
}
|
|
464
|
+
async function resolveTargetElement(target, options, path = "target") {
|
|
465
|
+
const rootDocument = options.document;
|
|
466
|
+
if (typeof target === "string") {
|
|
467
|
+
const element = rootDocument ? rootDocument.querySelector(target) : typeof document === "undefined" ? null : document.querySelector(target);
|
|
468
|
+
return rootDocument ? validateTargetElement(element, rootDocument, path) : element;
|
|
469
|
+
} else if (typeof target === "function") {
|
|
470
|
+
const element = await target({ signal: options.signal });
|
|
471
|
+
return rootDocument ? validateTargetElement(element, rootDocument, path) : element;
|
|
472
|
+
}
|
|
473
|
+
if (rootDocument)
|
|
474
|
+
return validateTargetElement(target, rootDocument, path);
|
|
475
|
+
return typeof HTMLElement !== "undefined" && target instanceof HTMLElement ? target : null;
|
|
476
|
+
}
|
|
477
|
+
function validateTargetElement(candidate, rootDocument, path) {
|
|
478
|
+
if (candidate === null)
|
|
479
|
+
return null;
|
|
480
|
+
const HTMLElement2 = rootDocument.defaultView?.HTMLElement;
|
|
481
|
+
if (typeof HTMLElement2 !== "function" || !(candidate instanceof HTMLElement2) || candidate.ownerDocument !== rootDocument) {
|
|
482
|
+
throw new TypeError(`Invalid target: ${path} must resolve to an HTMLElement in the root document`);
|
|
483
|
+
}
|
|
484
|
+
return candidate.isConnected ? candidate : null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// packages/core/src/elements/base.ts
|
|
488
|
+
var DEFAULT_ANIMATION_DURATION = 180;
|
|
489
|
+
var DEFAULT_ANIMATION_EASING = "ease-out";
|
|
490
|
+
|
|
491
|
+
class GlowTourElement {
|
|
492
|
+
element;
|
|
493
|
+
options;
|
|
494
|
+
animations = new Set;
|
|
495
|
+
cancelledAnimations = new WeakSet;
|
|
496
|
+
released = false;
|
|
497
|
+
constructor(element, options) {
|
|
498
|
+
this.element = element;
|
|
499
|
+
this.options = options;
|
|
500
|
+
}
|
|
501
|
+
setAnimationOptions(options) {
|
|
502
|
+
this.options = options;
|
|
503
|
+
}
|
|
504
|
+
_getAnimationOptions() {
|
|
505
|
+
return {
|
|
506
|
+
duration: this.options?.disabled ? 0 : this.options?.duration ?? DEFAULT_ANIMATION_DURATION,
|
|
507
|
+
easing: this.options?.easing ?? DEFAULT_ANIMATION_EASING,
|
|
508
|
+
fill: "forwards"
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
_isAnimated() {
|
|
512
|
+
return this.options?.disabled !== true && this.options?.duration !== 0;
|
|
513
|
+
}
|
|
514
|
+
_startAnimation(keyframes, options, target = this.element) {
|
|
515
|
+
if (!this._isAnimated() || typeof target.animate !== "function")
|
|
516
|
+
return null;
|
|
517
|
+
try {
|
|
518
|
+
return target.animate(keyframes, options ?? this._getAnimationOptions());
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
_getDocument() {
|
|
524
|
+
const owner = this.element.ownerDocument;
|
|
525
|
+
return owner && typeof owner.addEventListener === "function" ? owner : null;
|
|
526
|
+
}
|
|
527
|
+
_finishWhileDocumentHidden(animation) {
|
|
528
|
+
const owner = this._getDocument();
|
|
529
|
+
if (!owner)
|
|
530
|
+
return () => {};
|
|
531
|
+
const finishIfHidden = () => {
|
|
532
|
+
if (owner.visibilityState !== "hidden")
|
|
533
|
+
return;
|
|
534
|
+
try {
|
|
535
|
+
animation.finish();
|
|
536
|
+
} catch {}
|
|
537
|
+
};
|
|
538
|
+
finishIfHidden();
|
|
539
|
+
owner.addEventListener("visibilitychange", finishIfHidden);
|
|
540
|
+
return () => owner.removeEventListener("visibilitychange", finishIfHidden);
|
|
541
|
+
}
|
|
542
|
+
async _waitForAnimation(animation) {
|
|
543
|
+
if (this.released) {
|
|
544
|
+
animation.cancel();
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
this.animations.add(animation);
|
|
548
|
+
const stopWatchingVisibility = this._finishWhileDocumentHidden(animation);
|
|
549
|
+
try {
|
|
550
|
+
await animation.finished;
|
|
551
|
+
return !this.released && !this.cancelledAnimations.has(animation);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (this.released || this.cancelledAnimations.has(animation))
|
|
554
|
+
return false;
|
|
555
|
+
throw error;
|
|
556
|
+
} finally {
|
|
557
|
+
stopWatchingVisibility();
|
|
558
|
+
this.animations.delete(animation);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
getElement() {
|
|
562
|
+
return this.released ? null : this.element;
|
|
563
|
+
}
|
|
564
|
+
disappear() {
|
|
565
|
+
return this._disappear();
|
|
566
|
+
}
|
|
567
|
+
release() {
|
|
568
|
+
if (this.released)
|
|
569
|
+
return;
|
|
570
|
+
this.released = true;
|
|
571
|
+
this.cancelAnimations();
|
|
572
|
+
this._release();
|
|
573
|
+
}
|
|
574
|
+
cancelAnimations() {
|
|
575
|
+
for (const animation of this.animations) {
|
|
576
|
+
this._cancelAnimation(animation);
|
|
577
|
+
}
|
|
578
|
+
this.animations.clear();
|
|
579
|
+
}
|
|
580
|
+
_cancelAnimation(animation) {
|
|
581
|
+
this.cancelledAnimations.add(animation);
|
|
582
|
+
animation.cancel();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// packages/core/src/elements/idle-presentation.ts
|
|
587
|
+
var OVERLAY_IDLE_STYLE = {
|
|
588
|
+
"clip-rule": "evenodd",
|
|
589
|
+
"fill-rule": "evenodd",
|
|
590
|
+
height: "100%",
|
|
591
|
+
left: "0px",
|
|
592
|
+
"pointer-events": "none",
|
|
593
|
+
position: "fixed",
|
|
594
|
+
"stroke-linejoin": "round",
|
|
595
|
+
"stroke-miterlimit": "2",
|
|
596
|
+
top: "0px",
|
|
597
|
+
width: "100%",
|
|
598
|
+
"z-index": "10000"
|
|
599
|
+
};
|
|
600
|
+
var OVERLAY_IDLE_ATTRIBUTES = {
|
|
601
|
+
"aria-hidden": "true",
|
|
602
|
+
"data-glow-tour-allow-interaction": "false",
|
|
603
|
+
inert: "true"
|
|
604
|
+
};
|
|
605
|
+
var POINTER_IDLE_STYLE = {
|
|
606
|
+
left: "0px",
|
|
607
|
+
opacity: "0",
|
|
608
|
+
"pointer-events": "none",
|
|
609
|
+
position: "fixed",
|
|
610
|
+
top: "0px",
|
|
611
|
+
"will-change": "top, left, transform, opacity",
|
|
612
|
+
"z-index": "10002"
|
|
613
|
+
};
|
|
614
|
+
var POINTER_IDLE_ATTRIBUTES = {
|
|
615
|
+
"aria-hidden": "true"
|
|
616
|
+
};
|
|
617
|
+
var POPOVER_IDLE_STYLE = {
|
|
618
|
+
left: "0px",
|
|
619
|
+
opacity: "0",
|
|
620
|
+
position: "fixed",
|
|
621
|
+
top: "0px",
|
|
622
|
+
"transform-origin": "center center",
|
|
623
|
+
"z-index": "10001"
|
|
624
|
+
};
|
|
625
|
+
var POPOVER_IDLE_ATTRIBUTES = {
|
|
626
|
+
"aria-hidden": "true",
|
|
627
|
+
inert: "true",
|
|
628
|
+
tabindex: "-1"
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// packages/core/src/elements/overlay.ts
|
|
632
|
+
var DEFAULT_OVERLAY_PADDING = 8;
|
|
633
|
+
var DEFAULT_OVERLAY_RADIUS = 8;
|
|
634
|
+
|
|
635
|
+
class OverlayElement extends GlowTourElement {
|
|
636
|
+
currentTransition = null;
|
|
637
|
+
visualState = null;
|
|
638
|
+
setInteractionAllowed(allowed) {
|
|
639
|
+
this.element.style.setProperty("pointer-events", allowed ? "none" : "auto");
|
|
640
|
+
this.element.setAttribute("data-glow-tour-allow-interaction", String(allowed));
|
|
641
|
+
}
|
|
642
|
+
async moveToTarget(nextPosition, step) {
|
|
643
|
+
const nextVisualState = this._getVisualState(step);
|
|
644
|
+
const path = this._getPathElement();
|
|
645
|
+
if (!path) {
|
|
646
|
+
this.visualState = nextVisualState;
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const keyframe = this.getRenderedTargetStyles(path, this._getNextStyles(nextPosition, step));
|
|
650
|
+
this.visualState = nextVisualState;
|
|
651
|
+
if (!path.style.getPropertyValue("d")) {
|
|
652
|
+
for (const [property, value] of Object.entries(keyframe)) {
|
|
653
|
+
if (property !== "opacity" && value != null) {
|
|
654
|
+
path.style.setProperty(property, String(value));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const opacity = keyframe.opacity == null ? "0.7" : String(keyframe.opacity);
|
|
658
|
+
path.style.setProperty("opacity", "0");
|
|
659
|
+
const animation2 = this._startAnimation([{ opacity: "0" }, { opacity }], {
|
|
660
|
+
...this._getAnimationOptions(),
|
|
661
|
+
fill: "none"
|
|
662
|
+
}, path);
|
|
663
|
+
if (!animation2 || await this._waitForAnimation(animation2))
|
|
664
|
+
this.applyStyles(path, keyframe);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const baseStyle = {
|
|
668
|
+
d: path.style.getPropertyValue("d") ?? "",
|
|
669
|
+
fill: path.style.getPropertyValue("fill") ?? "",
|
|
670
|
+
opacity: path.style.getPropertyValue("opacity") ?? "0"
|
|
671
|
+
};
|
|
672
|
+
const animation = this._startAnimation([baseStyle, keyframe], {
|
|
673
|
+
...this._getAnimationOptions(),
|
|
674
|
+
fill: "none"
|
|
675
|
+
}, path);
|
|
676
|
+
if (!animation || await this._waitForAnimation(animation))
|
|
677
|
+
this.applyStyles(path, keyframe);
|
|
678
|
+
}
|
|
679
|
+
async animateTo(position, step) {
|
|
680
|
+
const path = this._getPathElement();
|
|
681
|
+
if (!path)
|
|
682
|
+
return;
|
|
683
|
+
this.commitAndCancelCurrentTransition(path);
|
|
684
|
+
const from = this.getCurrentRenderedStyles(path);
|
|
685
|
+
const finalStyles = this.getRenderedTargetStyles(path, this._getNextStyles(position, step));
|
|
686
|
+
const animation = this._startAnimation([from, finalStyles], {
|
|
687
|
+
...this._getAnimationOptions(),
|
|
688
|
+
fill: "none"
|
|
689
|
+
}, path);
|
|
690
|
+
if (!animation) {
|
|
691
|
+
this.applyStyles(path, finalStyles);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.currentTransition = animation;
|
|
695
|
+
try {
|
|
696
|
+
const completed = await this._waitForAnimation(animation);
|
|
697
|
+
if (completed && this.currentTransition === animation) {
|
|
698
|
+
this.applyStyles(path, finalStyles);
|
|
699
|
+
}
|
|
700
|
+
} finally {
|
|
701
|
+
if (this.currentTransition === animation)
|
|
702
|
+
this.currentTransition = null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
_getNextStyles(position, step) {
|
|
706
|
+
const { padding, radius, color, opacity } = step.overlay || {};
|
|
707
|
+
const path = roundedRectPath(position, viewportDimensions(this.element), {
|
|
708
|
+
padding: padding ?? DEFAULT_OVERLAY_PADDING,
|
|
709
|
+
radius: radius ?? DEFAULT_OVERLAY_RADIUS
|
|
710
|
+
}, this.element);
|
|
711
|
+
return {
|
|
712
|
+
d: `path("${path}")`,
|
|
713
|
+
fill: color,
|
|
714
|
+
opacity: opacity != null ? String(opacity) : 0.7
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
initializeProps() {
|
|
718
|
+
const el = this.getElement();
|
|
719
|
+
if (!el) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const viewport = viewportDimensions(el);
|
|
723
|
+
for (const [property, value] of Object.entries(OVERLAY_IDLE_STYLE)) {
|
|
724
|
+
el.style.setProperty(property, value);
|
|
725
|
+
}
|
|
726
|
+
for (const [name, value] of Object.entries(OVERLAY_IDLE_ATTRIBUTES)) {
|
|
727
|
+
el.setAttribute(name, value);
|
|
728
|
+
}
|
|
729
|
+
el.setAttribute("viewBox", `0 0 ${viewport.width} ${viewport.height}`);
|
|
730
|
+
}
|
|
731
|
+
_getPathElement() {
|
|
732
|
+
return this.element.querySelector("path");
|
|
733
|
+
}
|
|
734
|
+
_release() {
|
|
735
|
+
this.currentTransition = null;
|
|
736
|
+
this.visualState = null;
|
|
737
|
+
const path = this._getPathElement();
|
|
738
|
+
path?.style.removeProperty("d");
|
|
739
|
+
path?.style.removeProperty("fill");
|
|
740
|
+
path?.style.setProperty("opacity", "0");
|
|
741
|
+
this.element.style.setProperty("pointer-events", "none");
|
|
742
|
+
}
|
|
743
|
+
updatePosition(nextPosition, step, animateChanges = false, onTransition) {
|
|
744
|
+
const path = this._getPathElement();
|
|
745
|
+
if (!path) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const nextVisualState = this._getVisualState(step);
|
|
749
|
+
const shouldAnimate = animateChanges && this.visualState !== null && !this._isSameVisualState(this.visualState, nextVisualState);
|
|
750
|
+
if (shouldAnimate) {
|
|
751
|
+
const transition = this.animateTo(nextPosition, step);
|
|
752
|
+
if (onTransition)
|
|
753
|
+
onTransition(transition);
|
|
754
|
+
else
|
|
755
|
+
transition.catch(() => {});
|
|
756
|
+
} else
|
|
757
|
+
this.applyStyles(path, this.getRenderedTargetStyles(path, this._getNextStyles(nextPosition, step)));
|
|
758
|
+
this.visualState = nextVisualState;
|
|
759
|
+
const viewport = viewportDimensions(this.element);
|
|
760
|
+
const viewBox = `0 0 ${viewport.width} ${viewport.height}`;
|
|
761
|
+
if (this.element.getAttribute("viewBox") !== viewBox) {
|
|
762
|
+
this.element.setAttribute("viewBox", viewBox);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
cancelAnimations() {
|
|
766
|
+
this.currentTransition = null;
|
|
767
|
+
super.cancelAnimations();
|
|
768
|
+
}
|
|
769
|
+
applyStyles(path, styles) {
|
|
770
|
+
for (const [property, value] of Object.entries(styles)) {
|
|
771
|
+
if (value == null) {
|
|
772
|
+
if (path.style.getPropertyValue(property))
|
|
773
|
+
path.style.removeProperty(property);
|
|
774
|
+
} else if (path.style.getPropertyValue(property) !== String(value)) {
|
|
775
|
+
path.style.setProperty(property, String(value));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
commitAndCancelCurrentTransition(path) {
|
|
780
|
+
const animation = this.currentTransition;
|
|
781
|
+
if (!animation)
|
|
782
|
+
return;
|
|
783
|
+
this.applyStyles(path, this.getCurrentRenderedStyles(path));
|
|
784
|
+
this.currentTransition = null;
|
|
785
|
+
this._cancelAnimation(animation);
|
|
786
|
+
}
|
|
787
|
+
getCurrentRenderedStyles(path) {
|
|
788
|
+
const computed = computedStyle(path);
|
|
789
|
+
return {
|
|
790
|
+
d: computed?.getPropertyValue("d") || path.style.getPropertyValue("d"),
|
|
791
|
+
fill: computed?.getPropertyValue("fill") || path.style.getPropertyValue("fill"),
|
|
792
|
+
opacity: computed?.getPropertyValue("opacity") || path.style.getPropertyValue("opacity")
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
getRenderedTargetStyles(path, styles) {
|
|
796
|
+
if (styles.fill != null)
|
|
797
|
+
return styles;
|
|
798
|
+
const inlineFill = path.style.getPropertyValue("fill");
|
|
799
|
+
if (inlineFill && this.visualState?.color === undefined) {
|
|
800
|
+
return { ...styles, fill: inlineFill };
|
|
801
|
+
}
|
|
802
|
+
path.style.removeProperty("fill");
|
|
803
|
+
const renderedFill = computedStyle(path)?.getPropertyValue("fill");
|
|
804
|
+
if (inlineFill)
|
|
805
|
+
path.style.setProperty("fill", inlineFill);
|
|
806
|
+
return { ...styles, fill: renderedFill ?? "" };
|
|
807
|
+
}
|
|
808
|
+
_getVisualState(step) {
|
|
809
|
+
return {
|
|
810
|
+
color: step.overlay?.color,
|
|
811
|
+
opacity: step.overlay?.opacity,
|
|
812
|
+
padding: step.overlay?.padding,
|
|
813
|
+
radius: step.overlay?.radius
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
_isSameVisualState(current, next) {
|
|
817
|
+
return current.color === next.color && current.opacity === next.opacity && current.padding === next.padding && current.radius === next.radius;
|
|
818
|
+
}
|
|
819
|
+
async _appear(position, step) {
|
|
820
|
+
const path = this._getPathElement();
|
|
821
|
+
if (!path) {
|
|
822
|
+
return Promise.resolve();
|
|
823
|
+
}
|
|
824
|
+
const finalStyles = this.getRenderedTargetStyles(path, this._getNextStyles(position, step));
|
|
825
|
+
const opacity = String(finalStyles.opacity ?? "0.7");
|
|
826
|
+
this.applyStyles(path, { ...finalStyles, opacity: "0" });
|
|
827
|
+
const animation = this._startAnimation([{ opacity: "0" }, { opacity }], {
|
|
828
|
+
...this._getAnimationOptions(),
|
|
829
|
+
fill: "none"
|
|
830
|
+
}, path);
|
|
831
|
+
if (!animation || await this._waitForAnimation(animation))
|
|
832
|
+
this.applyStyles(path, finalStyles);
|
|
833
|
+
}
|
|
834
|
+
async _disappear() {
|
|
835
|
+
const path = this._getPathElement();
|
|
836
|
+
if (!path) {
|
|
837
|
+
return Promise.resolve();
|
|
838
|
+
}
|
|
839
|
+
const animation = this._startAnimation({
|
|
840
|
+
opacity: "0"
|
|
841
|
+
}, { ...this._getAnimationOptions(), fill: "none" }, path);
|
|
842
|
+
if (animation && !await this._waitForAnimation(animation))
|
|
843
|
+
return;
|
|
844
|
+
path.style.removeProperty("d");
|
|
845
|
+
path.style.removeProperty("fill");
|
|
846
|
+
path.style.setProperty("opacity", "0");
|
|
847
|
+
this.element.style.setProperty("pointer-events", "none");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function computedStyle(element) {
|
|
851
|
+
const document2 = element.ownerDocument;
|
|
852
|
+
const currentWindow = document2 && "defaultView" in document2 ? document2.defaultView : ownerWindow(element);
|
|
853
|
+
return typeof currentWindow?.getComputedStyle === "function" ? currentWindow.getComputedStyle(element) : null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// packages/core/src/elements/pointer.ts
|
|
857
|
+
var DEFAULT_INDICATOR_GAP = 16;
|
|
858
|
+
var POINTER_ANIMATION_DISTANCE = 8;
|
|
859
|
+
var POINTER_ANIMATION_DURATION = 800;
|
|
860
|
+
var DEFAULT_TRY_ORDER = [
|
|
861
|
+
"left",
|
|
862
|
+
"right",
|
|
863
|
+
"top",
|
|
864
|
+
"bottom"
|
|
865
|
+
];
|
|
866
|
+
var POINTER_DIRECTION_SELECTOR = "[data-glow-tour-pointer-direction]";
|
|
867
|
+
var OPPOSITE_DIRECTION = {
|
|
868
|
+
bottom: "top",
|
|
869
|
+
left: "right",
|
|
870
|
+
right: "left",
|
|
871
|
+
top: "bottom"
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
class PointerElement extends GlowTourElement {
|
|
875
|
+
animation = null;
|
|
876
|
+
popoverPlacement;
|
|
877
|
+
directionNodes = null;
|
|
878
|
+
initializeProps() {
|
|
879
|
+
for (const [property, value] of Object.entries(POINTER_IDLE_STYLE)) {
|
|
880
|
+
this.element.style.setProperty(property, value);
|
|
881
|
+
}
|
|
882
|
+
for (const [name, value] of Object.entries(POINTER_IDLE_ATTRIBUTES)) {
|
|
883
|
+
this.element.setAttribute(name, value);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
_getNextStyles(position, step) {
|
|
887
|
+
const nextPosition = this._resolvePosition(position, step);
|
|
888
|
+
this._setPlacement(nextPosition.placement);
|
|
889
|
+
return {
|
|
890
|
+
left: `${roundByDPR(nextPosition.x, this.element)}px`,
|
|
891
|
+
top: `${roundByDPR(nextPosition.y, this.element)}px`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
async moveToTarget(nextPosition, step, appear, popoverPlacement) {
|
|
895
|
+
this.popoverPlacement = popoverPlacement;
|
|
896
|
+
if (!appear) {
|
|
897
|
+
await this._disappear();
|
|
898
|
+
}
|
|
899
|
+
await this._appear(nextPosition, step);
|
|
900
|
+
}
|
|
901
|
+
updatePosition(nextPosition, step, popoverPlacement) {
|
|
902
|
+
this.popoverPlacement = popoverPlacement;
|
|
903
|
+
const currentPlacement = this.element.getAttribute("data-glow-tour-placement");
|
|
904
|
+
const nextPositionState = this._resolvePosition(nextPosition, step);
|
|
905
|
+
const nextPlacement = nextPositionState.placement;
|
|
906
|
+
const left = `${roundByDPR(nextPositionState.x, this.element)}px`;
|
|
907
|
+
const top = `${roundByDPR(nextPositionState.y, this.element)}px`;
|
|
908
|
+
if (this.element.style.getPropertyValue("left") !== left) {
|
|
909
|
+
this.element.style.setProperty("left", left);
|
|
910
|
+
}
|
|
911
|
+
if (this.element.style.getPropertyValue("top") !== top) {
|
|
912
|
+
this.element.style.setProperty("top", top);
|
|
913
|
+
}
|
|
914
|
+
if (currentPlacement !== nextPlacement) {
|
|
915
|
+
this._setPlacement(nextPlacement);
|
|
916
|
+
}
|
|
917
|
+
if (nextPlacement !== currentPlacement && this.element.getAttribute("aria-hidden") !== "true") {
|
|
918
|
+
this._startPointerAnimation(nextPlacement);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
syncVisibility(visible, position, step, popoverPlacement) {
|
|
922
|
+
this.cancelAnimations();
|
|
923
|
+
this.popoverPlacement = popoverPlacement;
|
|
924
|
+
if (!visible) {
|
|
925
|
+
this.element.style.setProperty("opacity", "0");
|
|
926
|
+
this.element.setAttribute("aria-hidden", "true");
|
|
927
|
+
this._setPlacement(null);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const styles = this._getNextStyles(position, step);
|
|
931
|
+
for (const [property, value] of Object.entries(styles)) {
|
|
932
|
+
if (value != null)
|
|
933
|
+
this.element.style.setProperty(property, String(value));
|
|
934
|
+
}
|
|
935
|
+
this.element.style.setProperty("opacity", "1");
|
|
936
|
+
this.element.removeAttribute("aria-hidden");
|
|
937
|
+
const placement = this.element.getAttribute("data-glow-tour-placement");
|
|
938
|
+
this._startPointerAnimation(placement);
|
|
939
|
+
}
|
|
940
|
+
cancelAnimations() {
|
|
941
|
+
super.cancelAnimations();
|
|
942
|
+
this._stopAnimation();
|
|
943
|
+
}
|
|
944
|
+
async _appear(position, step) {
|
|
945
|
+
const styles = this._getNextStyles(position, step);
|
|
946
|
+
for (const [property, value] of Object.entries(styles)) {
|
|
947
|
+
if (value != null)
|
|
948
|
+
this.element.style.setProperty(property, String(value));
|
|
949
|
+
}
|
|
950
|
+
const animation = this._startAnimation({ opacity: 1 }, {
|
|
951
|
+
...this._getAnimationOptions(),
|
|
952
|
+
fill: "none"
|
|
953
|
+
});
|
|
954
|
+
if (animation && !await this._waitForAnimation(animation))
|
|
955
|
+
return;
|
|
956
|
+
this.element.style.setProperty("opacity", "1");
|
|
957
|
+
this.element.removeAttribute("aria-hidden");
|
|
958
|
+
const placement = this.element.getAttribute("data-glow-tour-placement");
|
|
959
|
+
this._startPointerAnimation(placement);
|
|
960
|
+
}
|
|
961
|
+
async _disappear() {
|
|
962
|
+
this._stopAnimation();
|
|
963
|
+
const animation = this._startAnimation({ opacity: 0 }, {
|
|
964
|
+
...this._getAnimationOptions(),
|
|
965
|
+
fill: "none"
|
|
966
|
+
});
|
|
967
|
+
if (animation && !await this._waitForAnimation(animation))
|
|
968
|
+
return;
|
|
969
|
+
this.element.style.setProperty("opacity", "0");
|
|
970
|
+
this.element.setAttribute("aria-hidden", "true");
|
|
971
|
+
this._setPlacement(null);
|
|
972
|
+
}
|
|
973
|
+
_resolvePosition(targetPosition, step) {
|
|
974
|
+
const pointerPosition = this.element.getBoundingClientRect();
|
|
975
|
+
const excludedPlacement = this.popoverPlacement === "center" ? undefined : this.popoverPlacement;
|
|
976
|
+
const configuredOrder = step.indicator?.placementTryOrder ?? [];
|
|
977
|
+
const tryOrder = [...new Set([...configuredOrder, ...DEFAULT_TRY_ORDER])].filter((placement2) => placement2 !== excludedPlacement);
|
|
978
|
+
const gap = Math.max(0, step.indicator?.gap ?? DEFAULT_INDICATOR_GAP);
|
|
979
|
+
const candidates = this._getCandidates(targetPosition, pointerPosition, gap);
|
|
980
|
+
for (const placement2 of tryOrder) {
|
|
981
|
+
const candidate2 = candidates[placement2];
|
|
982
|
+
if (isInViewport({
|
|
983
|
+
left: candidate2.x,
|
|
984
|
+
top: candidate2.y,
|
|
985
|
+
right: candidate2.x + pointerPosition.width,
|
|
986
|
+
bottom: candidate2.y + pointerPosition.height
|
|
987
|
+
}, this.element)) {
|
|
988
|
+
return { ...candidate2, placement: placement2 };
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const placement = tryOrder[0] ?? DEFAULT_TRY_ORDER[0];
|
|
992
|
+
const candidate = candidates[placement];
|
|
993
|
+
const viewport = viewportDimensions(this.element);
|
|
994
|
+
return {
|
|
995
|
+
placement,
|
|
996
|
+
x: Math.min(Math.max(candidate.x, 0), Math.max(viewport.width - pointerPosition.width, 0)),
|
|
997
|
+
y: Math.min(Math.max(candidate.y, 0), Math.max(viewport.height - pointerPosition.height, 0))
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
_getCandidates(targetPosition, pointerPosition, gap) {
|
|
1001
|
+
const centeredX = targetPosition.left + (targetPosition.width - pointerPosition.width) / 2;
|
|
1002
|
+
const centeredY = targetPosition.top + (targetPosition.height - pointerPosition.height) / 2;
|
|
1003
|
+
return {
|
|
1004
|
+
top: {
|
|
1005
|
+
x: centeredX,
|
|
1006
|
+
y: targetPosition.top - pointerPosition.height - gap
|
|
1007
|
+
},
|
|
1008
|
+
bottom: {
|
|
1009
|
+
x: centeredX,
|
|
1010
|
+
y: targetPosition.bottom + gap
|
|
1011
|
+
},
|
|
1012
|
+
left: {
|
|
1013
|
+
x: targetPosition.left - pointerPosition.width - gap,
|
|
1014
|
+
y: centeredY
|
|
1015
|
+
},
|
|
1016
|
+
right: {
|
|
1017
|
+
x: targetPosition.right + gap,
|
|
1018
|
+
y: centeredY
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
_startPointerAnimation(placement) {
|
|
1023
|
+
this._stopAnimation();
|
|
1024
|
+
this.animation = this._startAnimation([{ transform: "translate(0px, 0px)" }, { transform: this._getTargetTransform(placement) }], {
|
|
1025
|
+
direction: "alternate",
|
|
1026
|
+
duration: POINTER_ANIMATION_DURATION,
|
|
1027
|
+
easing: "ease-in-out",
|
|
1028
|
+
iterations: Number.POSITIVE_INFINITY
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
_stopAnimation() {
|
|
1032
|
+
this.animation?.cancel();
|
|
1033
|
+
this.animation = null;
|
|
1034
|
+
this.element.style.removeProperty("transform");
|
|
1035
|
+
}
|
|
1036
|
+
_release() {
|
|
1037
|
+
this._stopAnimation();
|
|
1038
|
+
this.element.style.setProperty("opacity", "0");
|
|
1039
|
+
this.element.setAttribute("aria-hidden", "true");
|
|
1040
|
+
this._setPlacement(null);
|
|
1041
|
+
}
|
|
1042
|
+
_setPlacement(placement) {
|
|
1043
|
+
if (placement)
|
|
1044
|
+
this.element.setAttribute("data-glow-tour-placement", placement);
|
|
1045
|
+
else
|
|
1046
|
+
this.element.removeAttribute("data-glow-tour-placement");
|
|
1047
|
+
this._syncDirectionVisibility(placement);
|
|
1048
|
+
}
|
|
1049
|
+
_syncDirectionVisibility(placement) {
|
|
1050
|
+
const nodes = this._ensureDirectionNodes();
|
|
1051
|
+
const pointingDirection = placement ? OPPOSITE_DIRECTION[placement] : null;
|
|
1052
|
+
for (const [direction, node] of Object.entries(nodes)) {
|
|
1053
|
+
node.style.setProperty("display", direction === pointingDirection ? "" : "none");
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
_ensureDirectionNodes() {
|
|
1057
|
+
if (this.directionNodes)
|
|
1058
|
+
return this.directionNodes;
|
|
1059
|
+
const nodes = {};
|
|
1060
|
+
if (typeof this.element.querySelectorAll === "function") {
|
|
1061
|
+
for (const node of Array.from(this.element.querySelectorAll(POINTER_DIRECTION_SELECTOR))) {
|
|
1062
|
+
const direction = node.getAttribute("data-glow-tour-pointer-direction");
|
|
1063
|
+
if (direction)
|
|
1064
|
+
nodes[direction] = node;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
this.directionNodes = nodes;
|
|
1068
|
+
return nodes;
|
|
1069
|
+
}
|
|
1070
|
+
_getTargetTransform(placement) {
|
|
1071
|
+
switch (placement) {
|
|
1072
|
+
case "top":
|
|
1073
|
+
return `translate(0px, ${POINTER_ANIMATION_DISTANCE}px)`;
|
|
1074
|
+
case "bottom":
|
|
1075
|
+
return `translate(0px, -${POINTER_ANIMATION_DISTANCE}px)`;
|
|
1076
|
+
case "left":
|
|
1077
|
+
return `translate(${POINTER_ANIMATION_DISTANCE}px, 0px)`;
|
|
1078
|
+
case "right":
|
|
1079
|
+
return `translate(-${POINTER_ANIMATION_DISTANCE}px, 0px)`;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// packages/core/src/dom/dom-mutation-lease.ts
|
|
1085
|
+
class DomMutationLease {
|
|
1086
|
+
element;
|
|
1087
|
+
attributes = new Map;
|
|
1088
|
+
styles = new Map;
|
|
1089
|
+
released = false;
|
|
1090
|
+
constructor(element) {
|
|
1091
|
+
this.element = element;
|
|
1092
|
+
}
|
|
1093
|
+
setAttribute(name, value) {
|
|
1094
|
+
if (this.released)
|
|
1095
|
+
return;
|
|
1096
|
+
const mutation = this.attributes.get(name);
|
|
1097
|
+
if (mutation)
|
|
1098
|
+
mutation.value = value;
|
|
1099
|
+
else
|
|
1100
|
+
this.attributes.set(name, { original: this.element.getAttribute(name), value });
|
|
1101
|
+
if (value === null)
|
|
1102
|
+
this.element.removeAttribute(name);
|
|
1103
|
+
else
|
|
1104
|
+
this.element.setAttribute(name, value);
|
|
1105
|
+
}
|
|
1106
|
+
setStyle(name, value, priority = "") {
|
|
1107
|
+
if (this.released)
|
|
1108
|
+
return;
|
|
1109
|
+
const mutation = this.styles.get(name);
|
|
1110
|
+
if (value === null) {
|
|
1111
|
+
if (mutation) {
|
|
1112
|
+
this.element.style.removeProperty(name);
|
|
1113
|
+
mutation.value = this.element.style.getPropertyValue(name);
|
|
1114
|
+
mutation.priority = this.element.style.getPropertyPriority(name);
|
|
1115
|
+
} else {
|
|
1116
|
+
const firstMutation2 = this.styleMutation(name);
|
|
1117
|
+
this.element.style.removeProperty(name);
|
|
1118
|
+
firstMutation2.value = this.element.style.getPropertyValue(name);
|
|
1119
|
+
firstMutation2.priority = this.element.style.getPropertyPriority(name);
|
|
1120
|
+
this.styles.set(name, firstMutation2);
|
|
1121
|
+
}
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const accepted = isStyleDeclarationAccepted(this.element, name, value, priority);
|
|
1125
|
+
if (mutation) {
|
|
1126
|
+
this.element.style.setProperty(name, value, priority);
|
|
1127
|
+
if (!accepted)
|
|
1128
|
+
return;
|
|
1129
|
+
mutation.value = this.element.style.getPropertyValue(name);
|
|
1130
|
+
mutation.priority = this.element.style.getPropertyPriority(name);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const firstMutation = this.styleMutation(name);
|
|
1134
|
+
this.element.style.setProperty(name, value, priority);
|
|
1135
|
+
if (!accepted)
|
|
1136
|
+
return;
|
|
1137
|
+
firstMutation.value = this.element.style.getPropertyValue(name);
|
|
1138
|
+
firstMutation.priority = this.element.style.getPropertyPriority(name);
|
|
1139
|
+
this.styles.set(name, firstMutation);
|
|
1140
|
+
}
|
|
1141
|
+
releaseStyle(name) {
|
|
1142
|
+
if (this.released)
|
|
1143
|
+
return;
|
|
1144
|
+
const mutation = this.styles.get(name);
|
|
1145
|
+
if (!mutation)
|
|
1146
|
+
return;
|
|
1147
|
+
try {
|
|
1148
|
+
this.restoreStyle(name, mutation);
|
|
1149
|
+
} finally {
|
|
1150
|
+
this.styles.delete(name);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
release() {
|
|
1154
|
+
if (this.released)
|
|
1155
|
+
return;
|
|
1156
|
+
this.released = true;
|
|
1157
|
+
let failed = false;
|
|
1158
|
+
let failure;
|
|
1159
|
+
for (const [name, mutation] of this.attributes) {
|
|
1160
|
+
try {
|
|
1161
|
+
if (this.element.getAttribute(name) !== mutation.value)
|
|
1162
|
+
continue;
|
|
1163
|
+
if (mutation.original === null)
|
|
1164
|
+
this.element.removeAttribute(name);
|
|
1165
|
+
else
|
|
1166
|
+
this.element.setAttribute(name, mutation.original);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (!failed) {
|
|
1169
|
+
failed = true;
|
|
1170
|
+
failure = error;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
for (const [name, mutation] of this.styles) {
|
|
1175
|
+
try {
|
|
1176
|
+
this.restoreStyle(name, mutation);
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
if (!failed) {
|
|
1179
|
+
failed = true;
|
|
1180
|
+
failure = error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (failed)
|
|
1185
|
+
throw failure;
|
|
1186
|
+
}
|
|
1187
|
+
styleMutation(name) {
|
|
1188
|
+
const original = this.element.style.getPropertyValue(name);
|
|
1189
|
+
return {
|
|
1190
|
+
original: original === "" ? null : original,
|
|
1191
|
+
originalPriority: this.element.style.getPropertyPriority(name),
|
|
1192
|
+
priority: this.element.style.getPropertyPriority(name),
|
|
1193
|
+
value: original
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
restoreStyle(name, mutation) {
|
|
1197
|
+
if (this.element.style.getPropertyValue(name) !== (mutation.value ?? "") || this.element.style.getPropertyPriority(name) !== mutation.priority) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (mutation.original === null)
|
|
1201
|
+
this.element.style.removeProperty(name);
|
|
1202
|
+
else
|
|
1203
|
+
this.element.style.setProperty(name, mutation.original, mutation.originalPriority);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function isStyleDeclarationAccepted(element, name, value, priority) {
|
|
1207
|
+
const probe = element.ownerDocument.createElement("div");
|
|
1208
|
+
probe.style.setProperty(name, value, priority);
|
|
1209
|
+
return value === "" || probe.style.getPropertyValue(name) !== "";
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// packages/core/src/elements/popover-arrow-styles.ts
|
|
1213
|
+
var ARROW_STYLES_SELECTOR = "style[data-glow-tour-core-arrow-styles]";
|
|
1214
|
+
var ARROW_STYLES = `
|
|
1215
|
+
:where([data-glow-tour-popover])::before {
|
|
1216
|
+
background-color: var(--glow-tour-arrow-color, var(--glow-tour-color-surface, #ffffff));
|
|
1217
|
+
border: var(--glow-tour-arrow-border-width, 1px) solid var(--glow-tour-arrow-border-color, var(--glow-tour-color-border, #dedee3));
|
|
1218
|
+
border-radius: var(--glow-tour-arrow-border-radius, 0px);
|
|
1219
|
+
box-sizing: border-box;
|
|
1220
|
+
content: "";
|
|
1221
|
+
height: var(--glow-tour-arrow-size, 12px);
|
|
1222
|
+
pointer-events: none;
|
|
1223
|
+
position: absolute;
|
|
1224
|
+
width: var(--glow-tour-arrow-size, 12px);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
:where([data-glow-tour-popover][data-glow-tour-placement="top"])::before {
|
|
1228
|
+
border-left: 0;
|
|
1229
|
+
border-top: 0;
|
|
1230
|
+
bottom: 0;
|
|
1231
|
+
left: var(--glow-tour-arrow-offset);
|
|
1232
|
+
transform: translate(-50%, 50%) rotate(45deg);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
:where([data-glow-tour-popover][data-glow-tour-placement="bottom"])::before {
|
|
1236
|
+
border-bottom: 0;
|
|
1237
|
+
border-right: 0;
|
|
1238
|
+
left: var(--glow-tour-arrow-offset);
|
|
1239
|
+
top: 0;
|
|
1240
|
+
transform: translate(-50%, -50%) rotate(45deg);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
:where([data-glow-tour-popover][data-glow-tour-placement="left"])::before {
|
|
1244
|
+
border-bottom: 0;
|
|
1245
|
+
border-left: 0;
|
|
1246
|
+
right: 0;
|
|
1247
|
+
top: var(--glow-tour-arrow-offset);
|
|
1248
|
+
transform: translate(50%, -50%) rotate(45deg);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
:where([data-glow-tour-popover][data-glow-tour-placement="right"])::before {
|
|
1252
|
+
border-right: 0;
|
|
1253
|
+
border-top: 0;
|
|
1254
|
+
left: 0;
|
|
1255
|
+
top: var(--glow-tour-arrow-offset);
|
|
1256
|
+
transform: translate(-50%, -50%) rotate(45deg);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
:where([data-glow-tour-popover][data-glow-tour-arrow-hidden])::before {
|
|
1260
|
+
display: none;
|
|
1261
|
+
}
|
|
1262
|
+
`;
|
|
1263
|
+
function ensurePopoverArrowStyles(element, options) {
|
|
1264
|
+
if (options?.disabled)
|
|
1265
|
+
return;
|
|
1266
|
+
if (typeof element.getRootNode !== "function")
|
|
1267
|
+
return;
|
|
1268
|
+
const root = element.getRootNode();
|
|
1269
|
+
if (root.nodeType !== 9 && root.nodeType !== 11)
|
|
1270
|
+
return;
|
|
1271
|
+
const styleRoot = root;
|
|
1272
|
+
if (styleRoot.querySelector(ARROW_STYLES_SELECTOR))
|
|
1273
|
+
return;
|
|
1274
|
+
const ownerDocument2 = root.nodeType === 9 ? root : element.ownerDocument;
|
|
1275
|
+
const style = ownerDocument2.createElement("style");
|
|
1276
|
+
style.setAttribute("data-glow-tour-core-arrow-styles", "");
|
|
1277
|
+
if (options?.nonce)
|
|
1278
|
+
style.nonce = options.nonce;
|
|
1279
|
+
style.textContent = ARROW_STYLES;
|
|
1280
|
+
if (root.nodeType === 9) {
|
|
1281
|
+
const document2 = root;
|
|
1282
|
+
(document2.head ?? document2.documentElement).append(style);
|
|
1283
|
+
} else {
|
|
1284
|
+
root.append(style);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// packages/core/src/elements/popover.ts
|
|
1289
|
+
var DEFAULT_POPOVER_GAP = 16;
|
|
1290
|
+
var DEFAULT_ARROW_EDGE_PADDING = 16;
|
|
1291
|
+
var DEFAULT_TRY_ORDER2 = ["bottom", "top", "right", "left"];
|
|
1292
|
+
var REPLACEMENT_DIFF = 50;
|
|
1293
|
+
var ARROW_STYLE_PROPERTIES = {
|
|
1294
|
+
borderRadius: "--glow-tour-arrow-border-radius",
|
|
1295
|
+
borderWidth: "--glow-tour-arrow-border-width",
|
|
1296
|
+
color: "--glow-tour-arrow-color",
|
|
1297
|
+
size: "--glow-tour-arrow-size"
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
class PopoverElement extends GlowTourElement {
|
|
1301
|
+
appliedPosition = null;
|
|
1302
|
+
pendingReposition = null;
|
|
1303
|
+
repositionPhase = "idle";
|
|
1304
|
+
repositionGeneration = 0;
|
|
1305
|
+
mutationLease = new DomMutationLease(this.element);
|
|
1306
|
+
_getNextStyles(position, step) {
|
|
1307
|
+
this._applyArrowStyles(step);
|
|
1308
|
+
const nextPosition = this.resolvePosition(position, step);
|
|
1309
|
+
this._applyPositionState(nextPosition);
|
|
1310
|
+
this.appliedPosition = nextPosition;
|
|
1311
|
+
return {
|
|
1312
|
+
transform: `translate(${roundByDPR(nextPosition.x, this.element)}px, ${roundByDPR(nextPosition.y, this.element)}px)`
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
resolvePosition(targetPosition, step) {
|
|
1316
|
+
const currentElement = this.getElement();
|
|
1317
|
+
if (!currentElement) {
|
|
1318
|
+
return {
|
|
1319
|
+
placement: "center",
|
|
1320
|
+
x: 0,
|
|
1321
|
+
y: 0,
|
|
1322
|
+
arrowOffset: null
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const gap = Math.max(0, step.popover?.gap ?? DEFAULT_POPOVER_GAP);
|
|
1326
|
+
const arrowDisabled = step.popover?.arrow?.disabled === true;
|
|
1327
|
+
const arrowEdgePadding = step.popover?.arrow?.edgePadding ?? DEFAULT_ARROW_EDGE_PADDING;
|
|
1328
|
+
const popoverPosition = currentElement.getBoundingClientRect();
|
|
1329
|
+
const viewport = viewportDimensions(currentElement);
|
|
1330
|
+
const minX = gap;
|
|
1331
|
+
const maxX = viewport.width - gap - popoverPosition.width;
|
|
1332
|
+
const minY = gap;
|
|
1333
|
+
const maxY = viewport.height - gap - popoverPosition.height;
|
|
1334
|
+
const targetCenterX = targetPosition.left + targetPosition.width / 2;
|
|
1335
|
+
const targetCenterY = targetPosition.top + targetPosition.height / 2;
|
|
1336
|
+
if (maxX < minX || maxY < minY) {
|
|
1337
|
+
return this._centerPosition(popoverPosition, viewport);
|
|
1338
|
+
}
|
|
1339
|
+
const centeredX = targetCenterX - popoverPosition.width / 2;
|
|
1340
|
+
const centeredY = targetCenterY - popoverPosition.height / 2;
|
|
1341
|
+
const candidates = {
|
|
1342
|
+
top: {
|
|
1343
|
+
x: clamp(centeredX, minX, maxX),
|
|
1344
|
+
y: targetPosition.top - popoverPosition.height - gap
|
|
1345
|
+
},
|
|
1346
|
+
bottom: {
|
|
1347
|
+
x: clamp(centeredX, minX, maxX),
|
|
1348
|
+
y: targetPosition.bottom + gap
|
|
1349
|
+
},
|
|
1350
|
+
left: {
|
|
1351
|
+
x: targetPosition.left - popoverPosition.width - gap,
|
|
1352
|
+
y: clamp(centeredY, minY, maxY)
|
|
1353
|
+
},
|
|
1354
|
+
right: {
|
|
1355
|
+
x: targetPosition.right + gap,
|
|
1356
|
+
y: clamp(centeredY, minY, maxY)
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const tryOrder = step.popover?.placementTryOrder ?? DEFAULT_TRY_ORDER2;
|
|
1360
|
+
for (const placement of tryOrder) {
|
|
1361
|
+
const candidate = candidates[placement];
|
|
1362
|
+
const isVisible = candidate.x >= minX && candidate.y >= minY && candidate.x + popoverPosition.width <= viewport.width - gap && candidate.y + popoverPosition.height <= viewport.height - gap;
|
|
1363
|
+
if (!isVisible) {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const arrowOffset = arrowDisabled ? null : placement === "top" || placement === "bottom" ? targetCenterX - candidate.x : targetCenterY - candidate.y;
|
|
1367
|
+
const arrowAxisSize = placement === "top" || placement === "bottom" ? popoverPosition.width : popoverPosition.height;
|
|
1368
|
+
if (arrowOffset !== null && (arrowOffset < arrowEdgePadding || arrowOffset > arrowAxisSize - arrowEdgePadding)) {
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
return { ...candidate, arrowOffset, placement };
|
|
1372
|
+
}
|
|
1373
|
+
return this._centerPosition(popoverPosition, viewport);
|
|
1374
|
+
}
|
|
1375
|
+
_centerPosition(popoverPosition, viewport) {
|
|
1376
|
+
return {
|
|
1377
|
+
arrowOffset: null,
|
|
1378
|
+
placement: "center",
|
|
1379
|
+
x: (viewport.width - popoverPosition.width) / 2,
|
|
1380
|
+
y: (viewport.height - popoverPosition.height) / 2
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
_applyPositionState(position) {
|
|
1384
|
+
if (this.element.getAttribute("data-glow-tour-placement") !== position.placement) {
|
|
1385
|
+
this.mutationLease.setAttribute("data-glow-tour-placement", position.placement);
|
|
1386
|
+
}
|
|
1387
|
+
const arrowHidden = position.arrowOffset === null;
|
|
1388
|
+
if (this.element.hasAttribute("data-glow-tour-arrow-hidden") !== arrowHidden) {
|
|
1389
|
+
this.mutationLease.setAttribute("data-glow-tour-arrow-hidden", arrowHidden ? "" : null);
|
|
1390
|
+
}
|
|
1391
|
+
if (position.arrowOffset === null) {
|
|
1392
|
+
if (this.element.style.getPropertyValue("--glow-tour-arrow-offset")) {
|
|
1393
|
+
this.mutationLease.setStyle("--glow-tour-arrow-offset", null);
|
|
1394
|
+
}
|
|
1395
|
+
} else {
|
|
1396
|
+
const arrowOffset = `${roundByDPR(position.arrowOffset, this.element)}px`;
|
|
1397
|
+
if (this.element.style.getPropertyValue("--glow-tour-arrow-offset") !== arrowOffset) {
|
|
1398
|
+
this.mutationLease.setStyle("--glow-tour-arrow-offset", arrowOffset);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async moveToTarget(nextPosition, step, appear, onChange) {
|
|
1403
|
+
if (!appear) {
|
|
1404
|
+
await this._disappear();
|
|
1405
|
+
}
|
|
1406
|
+
if (onChange)
|
|
1407
|
+
await onChange();
|
|
1408
|
+
await this._appear(nextPosition, step);
|
|
1409
|
+
}
|
|
1410
|
+
initializeProps() {
|
|
1411
|
+
const el = this.getElement();
|
|
1412
|
+
if (!el) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
for (const [property, value] of Object.entries(POPOVER_IDLE_STYLE)) {
|
|
1416
|
+
this.mutationLease.setStyle(property, value);
|
|
1417
|
+
}
|
|
1418
|
+
if (!el.hasAttribute("tabindex")) {
|
|
1419
|
+
this.mutationLease.setAttribute("tabindex", POPOVER_IDLE_ATTRIBUTES.tabindex);
|
|
1420
|
+
}
|
|
1421
|
+
this.mutationLease.setAttribute("aria-hidden", POPOVER_IDLE_ATTRIBUTES["aria-hidden"]);
|
|
1422
|
+
this.mutationLease.setAttribute("inert", POPOVER_IDLE_ATTRIBUTES.inert);
|
|
1423
|
+
}
|
|
1424
|
+
updatePosition(nextPosition, step, onReposition) {
|
|
1425
|
+
const nextCoordinates = this.resolvePosition(nextPosition, step);
|
|
1426
|
+
const appliedPosition = this.appliedPosition;
|
|
1427
|
+
if (!appliedPosition) {
|
|
1428
|
+
this._applyPositionState(nextCoordinates);
|
|
1429
|
+
this.appliedPosition = nextCoordinates;
|
|
1430
|
+
this._applyTransform(nextCoordinates);
|
|
1431
|
+
return nextCoordinates.placement;
|
|
1432
|
+
}
|
|
1433
|
+
if (this.repositionPhase === "fading-out") {
|
|
1434
|
+
this.pendingReposition = { position: nextPosition, step };
|
|
1435
|
+
return nextCoordinates.placement;
|
|
1436
|
+
}
|
|
1437
|
+
const shouldReposition = appliedPosition.placement !== nextCoordinates.placement || Math.abs(nextCoordinates.x - appliedPosition.x) > REPLACEMENT_DIFF || Math.abs(nextCoordinates.y - appliedPosition.y) > REPLACEMENT_DIFF;
|
|
1438
|
+
if (shouldReposition) {
|
|
1439
|
+
this.pendingReposition = { position: nextPosition, step };
|
|
1440
|
+
if (this.repositionPhase === "idle") {
|
|
1441
|
+
const reposition = this._flushReposition();
|
|
1442
|
+
if (onReposition)
|
|
1443
|
+
onReposition(reposition);
|
|
1444
|
+
else
|
|
1445
|
+
reposition.catch(() => {});
|
|
1446
|
+
}
|
|
1447
|
+
} else if (this.repositionPhase === "fading-in") {
|
|
1448
|
+
this.pendingReposition = null;
|
|
1449
|
+
} else {
|
|
1450
|
+
const stationaryPosition = this._stationaryPosition(appliedPosition, nextCoordinates, nextPosition, step);
|
|
1451
|
+
this._applyPositionState(stationaryPosition);
|
|
1452
|
+
this.appliedPosition = stationaryPosition;
|
|
1453
|
+
}
|
|
1454
|
+
return nextCoordinates.placement;
|
|
1455
|
+
}
|
|
1456
|
+
cancelAnimations() {
|
|
1457
|
+
this.repositionGeneration += 1;
|
|
1458
|
+
this.pendingReposition = null;
|
|
1459
|
+
this.repositionPhase = "idle";
|
|
1460
|
+
super.cancelAnimations();
|
|
1461
|
+
}
|
|
1462
|
+
async _flushReposition() {
|
|
1463
|
+
if (this.repositionPhase !== "idle")
|
|
1464
|
+
return;
|
|
1465
|
+
const generation = ++this.repositionGeneration;
|
|
1466
|
+
try {
|
|
1467
|
+
while (this.pendingReposition && generation === this.repositionGeneration) {
|
|
1468
|
+
let pending = this.pendingReposition;
|
|
1469
|
+
this.pendingReposition = null;
|
|
1470
|
+
this.repositionPhase = "fading-out";
|
|
1471
|
+
await this._disappear();
|
|
1472
|
+
if (generation !== this.repositionGeneration || !this.getElement())
|
|
1473
|
+
return;
|
|
1474
|
+
pending = this.pendingReposition ?? pending;
|
|
1475
|
+
this.pendingReposition = null;
|
|
1476
|
+
this.repositionPhase = "fading-in";
|
|
1477
|
+
await this._appear(pending.position, pending.step);
|
|
1478
|
+
}
|
|
1479
|
+
} finally {
|
|
1480
|
+
if (generation === this.repositionGeneration)
|
|
1481
|
+
this.repositionPhase = "idle";
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
_applyTransform(position) {
|
|
1485
|
+
const transform = `translate(${roundByDPR(position.x, this.element)}px, ${roundByDPR(position.y, this.element)}px)`;
|
|
1486
|
+
if (this.element.style.transform !== transform) {
|
|
1487
|
+
this.mutationLease.setStyle("transform", transform);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
_stationaryPosition(appliedPosition, nextPosition, targetPosition, step) {
|
|
1491
|
+
if (nextPosition.arrowOffset === null) {
|
|
1492
|
+
return { ...appliedPosition, arrowOffset: null };
|
|
1493
|
+
}
|
|
1494
|
+
const popoverRect = this.element.getBoundingClientRect();
|
|
1495
|
+
const horizontal = appliedPosition.placement === "top" || appliedPosition.placement === "bottom";
|
|
1496
|
+
const targetCenter = horizontal ? targetPosition.left + targetPosition.width / 2 : targetPosition.top + targetPosition.height / 2;
|
|
1497
|
+
const popoverStart = horizontal ? appliedPosition.x : appliedPosition.y;
|
|
1498
|
+
const popoverSize = horizontal ? popoverRect.width : popoverRect.height;
|
|
1499
|
+
const arrowEdgePadding = step.popover?.arrow?.edgePadding ?? DEFAULT_ARROW_EDGE_PADDING;
|
|
1500
|
+
return {
|
|
1501
|
+
...appliedPosition,
|
|
1502
|
+
arrowOffset: clamp(targetCenter - popoverStart, arrowEdgePadding, Math.max(arrowEdgePadding, popoverSize - arrowEdgePadding))
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
_applyArrowStyles(step) {
|
|
1506
|
+
const arrow = step.popover?.arrow;
|
|
1507
|
+
ensurePopoverArrowStyles(this.element, {
|
|
1508
|
+
disabled: arrow?.disableAutoStyles,
|
|
1509
|
+
nonce: arrow?.styleNonce
|
|
1510
|
+
});
|
|
1511
|
+
this._applyArrowStyle(ARROW_STYLE_PROPERTIES.color, arrow?.color);
|
|
1512
|
+
this._applyArrowStyle(ARROW_STYLE_PROPERTIES.size, toPixels(arrow?.size));
|
|
1513
|
+
this._applyArrowStyle(ARROW_STYLE_PROPERTIES.borderWidth, toPixels(arrow?.borderWidth));
|
|
1514
|
+
this._applyArrowStyle(ARROW_STYLE_PROPERTIES.borderRadius, toPixels(arrow?.borderRadius));
|
|
1515
|
+
}
|
|
1516
|
+
_applyArrowStyle(property, value) {
|
|
1517
|
+
if (value === undefined) {
|
|
1518
|
+
this.mutationLease.releaseStyle(property);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (this.element.style.getPropertyValue(property) !== value || this.element.style.getPropertyPriority(property)) {
|
|
1522
|
+
this.mutationLease.setStyle(property, value);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async _appear(position, step) {
|
|
1526
|
+
const defaultStyles = this._getNextStyles(position, step);
|
|
1527
|
+
for (const [key, value] of Object.entries(defaultStyles)) {
|
|
1528
|
+
if (value != null)
|
|
1529
|
+
this.mutationLease.setStyle(key, String(value));
|
|
1530
|
+
}
|
|
1531
|
+
const animation = this._startAnimation([
|
|
1532
|
+
{
|
|
1533
|
+
opacity: 1
|
|
1534
|
+
}
|
|
1535
|
+
], this._getAnimationOptions());
|
|
1536
|
+
if (animation && !await this._waitForAnimation(animation))
|
|
1537
|
+
return;
|
|
1538
|
+
this._applyVisibleState();
|
|
1539
|
+
}
|
|
1540
|
+
async _disappear() {
|
|
1541
|
+
const animation = this._startAnimation({
|
|
1542
|
+
opacity: 0
|
|
1543
|
+
}, this._getAnimationOptions());
|
|
1544
|
+
if (animation && !await this._waitForAnimation(animation))
|
|
1545
|
+
return;
|
|
1546
|
+
this._applyHiddenState();
|
|
1547
|
+
}
|
|
1548
|
+
_release() {
|
|
1549
|
+
this.mutationLease.release();
|
|
1550
|
+
}
|
|
1551
|
+
_applyVisibleState() {
|
|
1552
|
+
this.mutationLease.setStyle("opacity", "1");
|
|
1553
|
+
this.mutationLease.setAttribute("aria-hidden", null);
|
|
1554
|
+
this.mutationLease.setAttribute("inert", null);
|
|
1555
|
+
}
|
|
1556
|
+
_applyHiddenState() {
|
|
1557
|
+
this.mutationLease.setStyle("opacity", "0");
|
|
1558
|
+
this.mutationLease.setAttribute("aria-hidden", "true");
|
|
1559
|
+
this.mutationLease.setAttribute("inert", "true");
|
|
1560
|
+
this.mutationLease.setStyle("transform", null);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function toPixels(value) {
|
|
1564
|
+
return value === undefined ? undefined : `${Math.max(0, value)}px`;
|
|
1565
|
+
}
|
|
1566
|
+
function clamp(value, min, max) {
|
|
1567
|
+
return Math.min(Math.max(value, min), max);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// packages/core/src/state/focusable.ts
|
|
1571
|
+
var FOCUSABLE_SELECTOR = [
|
|
1572
|
+
"a[href]",
|
|
1573
|
+
"button:not([disabled])",
|
|
1574
|
+
"input:not([disabled])",
|
|
1575
|
+
"select:not([disabled])",
|
|
1576
|
+
"textarea:not([disabled])",
|
|
1577
|
+
"summary",
|
|
1578
|
+
"audio[controls]",
|
|
1579
|
+
"video[controls]",
|
|
1580
|
+
"[contenteditable]:not([contenteditable='false'])",
|
|
1581
|
+
"[tabindex]:not([tabindex='-1'])"
|
|
1582
|
+
].join(",");
|
|
1583
|
+
var TOUR_TRIGGER_SELECTOR = [
|
|
1584
|
+
"[data-glow-tour-previous-trigger]",
|
|
1585
|
+
"[data-glow-tour-cancel-trigger]",
|
|
1586
|
+
"[data-glow-tour-advance-trigger]"
|
|
1587
|
+
].join(",");
|
|
1588
|
+
function focusableElements(root) {
|
|
1589
|
+
return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(isFocusable);
|
|
1590
|
+
}
|
|
1591
|
+
function focusableElementsOwnedBy(root) {
|
|
1592
|
+
const ownerRoot = root.closest("[data-glow-tour-root]");
|
|
1593
|
+
return focusableElements(root).filter((element) => element.closest("[data-glow-tour-root]") === ownerRoot);
|
|
1594
|
+
}
|
|
1595
|
+
function focusableTourControls(root) {
|
|
1596
|
+
return focusableElementsOwnedBy(root).filter((element) => element.matches(TOUR_TRIGGER_SELECTOR));
|
|
1597
|
+
}
|
|
1598
|
+
function isFocusable(element) {
|
|
1599
|
+
if (element.hasAttribute("disabled") || element.hasAttribute("hidden") || element.getAttribute("aria-disabled") === "true" || element.getAttribute("aria-hidden") === "true" || element.closest("[hidden], [inert], [aria-hidden='true']") !== null)
|
|
1600
|
+
return false;
|
|
1601
|
+
for (let current = element;current; current = current.parentElement) {
|
|
1602
|
+
const document2 = current.ownerDocument;
|
|
1603
|
+
const currentWindow = document2 && "defaultView" in document2 ? document2.defaultView : ownerWindow(current);
|
|
1604
|
+
const style = typeof currentWindow?.getComputedStyle === "function" ? currentWindow.getComputedStyle(current) : null;
|
|
1605
|
+
if (!style)
|
|
1606
|
+
return false;
|
|
1607
|
+
if (style.display === "none" || style.visibility === "hidden")
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// packages/core/src/state/focus-guard.ts
|
|
1614
|
+
class FocusGuard {
|
|
1615
|
+
initialFocus = null;
|
|
1616
|
+
document = null;
|
|
1617
|
+
popover = null;
|
|
1618
|
+
allowedTarget = null;
|
|
1619
|
+
allowTargetInteraction = false;
|
|
1620
|
+
fallback = null;
|
|
1621
|
+
fallbackLease = null;
|
|
1622
|
+
direction = "advance";
|
|
1623
|
+
active = false;
|
|
1624
|
+
redirecting = false;
|
|
1625
|
+
handleFocusIn = (event) => {
|
|
1626
|
+
if (!this.active || this.redirecting) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const target = event.target;
|
|
1630
|
+
if (!isNode(target, this.popover) || this.isAllowed(target)) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
this.focusFallback();
|
|
1634
|
+
};
|
|
1635
|
+
activate(scope) {
|
|
1636
|
+
const document2 = ownerDocument(scope.popover);
|
|
1637
|
+
if (!document2)
|
|
1638
|
+
return;
|
|
1639
|
+
if (this.active && this.document !== document2)
|
|
1640
|
+
this.deactivate();
|
|
1641
|
+
if (!this.active) {
|
|
1642
|
+
this.document = document2;
|
|
1643
|
+
this.initialFocus = isHTMLElement(this.document.activeElement, scope.popover) ? this.document.activeElement : null;
|
|
1644
|
+
this.document.addEventListener("focusin", this.handleFocusIn, true);
|
|
1645
|
+
this.active = true;
|
|
1646
|
+
}
|
|
1647
|
+
this.update(scope);
|
|
1648
|
+
const currentFocus = this.document?.activeElement;
|
|
1649
|
+
if (scope.autoFocus !== false || !isNode(currentFocus, scope.popover) || !this.isAllowed(currentFocus)) {
|
|
1650
|
+
this.focusFallback();
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
update(scope) {
|
|
1654
|
+
this.popover = scope.popover;
|
|
1655
|
+
this.allowedTarget = scope.allowedTarget ?? null;
|
|
1656
|
+
this.allowTargetInteraction = scope.allowTargetInteraction ?? false;
|
|
1657
|
+
this.direction = scope.direction;
|
|
1658
|
+
this.setFallback(scope.fallback ?? null);
|
|
1659
|
+
}
|
|
1660
|
+
focus() {
|
|
1661
|
+
if (this.active)
|
|
1662
|
+
this.focusFallback();
|
|
1663
|
+
}
|
|
1664
|
+
deactivate() {
|
|
1665
|
+
if (!this.active) {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
this.document?.removeEventListener("focusin", this.handleFocusIn, true);
|
|
1669
|
+
this.document = null;
|
|
1670
|
+
this.active = false;
|
|
1671
|
+
this.popover = null;
|
|
1672
|
+
this.allowedTarget = null;
|
|
1673
|
+
this.allowTargetInteraction = false;
|
|
1674
|
+
this.restoreFallback();
|
|
1675
|
+
const focusToRestore = this.initialFocus;
|
|
1676
|
+
this.initialFocus = null;
|
|
1677
|
+
if (focusToRestore?.isConnected) {
|
|
1678
|
+
focusToRestore.focus();
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
isAllowed(target) {
|
|
1682
|
+
if (target === this.fallback)
|
|
1683
|
+
return true;
|
|
1684
|
+
const popover = this.popover;
|
|
1685
|
+
if (popover && belongsToScope(target, popover))
|
|
1686
|
+
return true;
|
|
1687
|
+
return this.allowTargetInteraction && !!this.allowedTarget && belongsToScope(target, this.allowedTarget);
|
|
1688
|
+
}
|
|
1689
|
+
focusFallback() {
|
|
1690
|
+
const popover = this.popover;
|
|
1691
|
+
if (!popover?.isConnected) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const nextFocus = this.findFocusable(popover, this.direction) ?? (isFocusable(popover) ? popover : this.fallback?.isConnected && isFocusable(this.fallback) ? this.fallback : null);
|
|
1695
|
+
if (!nextFocus)
|
|
1696
|
+
return;
|
|
1697
|
+
this.redirecting = true;
|
|
1698
|
+
nextFocus.focus();
|
|
1699
|
+
this.redirecting = false;
|
|
1700
|
+
}
|
|
1701
|
+
setFallback(fallback) {
|
|
1702
|
+
if (this.fallback === fallback)
|
|
1703
|
+
return;
|
|
1704
|
+
this.restoreFallback();
|
|
1705
|
+
this.fallback = fallback;
|
|
1706
|
+
if (!fallback)
|
|
1707
|
+
return;
|
|
1708
|
+
this.fallbackLease = new DomMutationLease(fallback);
|
|
1709
|
+
this.fallbackLease.setAttribute("tabindex", "-1");
|
|
1710
|
+
}
|
|
1711
|
+
restoreFallback() {
|
|
1712
|
+
this.fallbackLease?.release();
|
|
1713
|
+
this.fallbackLease = null;
|
|
1714
|
+
this.fallback = null;
|
|
1715
|
+
}
|
|
1716
|
+
findFocusable(root, direction) {
|
|
1717
|
+
const candidates = focusableTourControls(root);
|
|
1718
|
+
const selector = direction === "advance" ? "[data-glow-tour-advance-trigger]" : "[data-glow-tour-previous-trigger]";
|
|
1719
|
+
return candidates.find((candidate) => candidate.matches(selector)) ?? null;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function belongsToScope(target, scope) {
|
|
1723
|
+
if (!scope.contains(target))
|
|
1724
|
+
return false;
|
|
1725
|
+
const element = isHTMLElement(target, scope) ? target : target.parentElement;
|
|
1726
|
+
if (!element)
|
|
1727
|
+
return target === scope;
|
|
1728
|
+
return element.closest("[data-glow-tour-root]") === scope.closest("[data-glow-tour-root]");
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// packages/core/src/state/scroll-lock.ts
|
|
1732
|
+
class ScrollLock {
|
|
1733
|
+
document = null;
|
|
1734
|
+
previousOverflow = "";
|
|
1735
|
+
previousPaddingRight = "";
|
|
1736
|
+
active = false;
|
|
1737
|
+
activate(document2) {
|
|
1738
|
+
if (this.active || !document2)
|
|
1739
|
+
return;
|
|
1740
|
+
const body = document2.body;
|
|
1741
|
+
if (!body?.style)
|
|
1742
|
+
return;
|
|
1743
|
+
this.document = document2;
|
|
1744
|
+
this.previousOverflow = body.style.overflow;
|
|
1745
|
+
this.previousPaddingRight = body.style.paddingRight;
|
|
1746
|
+
this.active = true;
|
|
1747
|
+
body.style.overflow = "hidden";
|
|
1748
|
+
const scrollbarWidth = this.scrollbarWidth(document2);
|
|
1749
|
+
if (scrollbarWidth > 0) {
|
|
1750
|
+
const currentPaddingRight = this.computedPaddingRight(document2, body);
|
|
1751
|
+
body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
deactivate() {
|
|
1755
|
+
if (!this.active)
|
|
1756
|
+
return;
|
|
1757
|
+
const body = this.document?.body;
|
|
1758
|
+
this.active = false;
|
|
1759
|
+
this.document = null;
|
|
1760
|
+
if (!body?.style)
|
|
1761
|
+
return;
|
|
1762
|
+
body.style.overflow = this.previousOverflow;
|
|
1763
|
+
body.style.paddingRight = this.previousPaddingRight;
|
|
1764
|
+
this.previousOverflow = "";
|
|
1765
|
+
this.previousPaddingRight = "";
|
|
1766
|
+
}
|
|
1767
|
+
scrollbarWidth(document2) {
|
|
1768
|
+
const view = document2.defaultView;
|
|
1769
|
+
const documentElement = document2.documentElement;
|
|
1770
|
+
if (!view || !documentElement)
|
|
1771
|
+
return 0;
|
|
1772
|
+
const width = view.innerWidth - documentElement.clientWidth;
|
|
1773
|
+
return Number.isFinite(width) && width > 0 ? width : 0;
|
|
1774
|
+
}
|
|
1775
|
+
computedPaddingRight(document2, body) {
|
|
1776
|
+
const view = document2.defaultView;
|
|
1777
|
+
if (typeof view?.getComputedStyle !== "function")
|
|
1778
|
+
return 0;
|
|
1779
|
+
const value = Number.parseFloat(view.getComputedStyle(body).paddingRight || "0");
|
|
1780
|
+
return Number.isFinite(value) ? value : 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// packages/core/src/dom/tour-view-driver.ts
|
|
1785
|
+
var DEFAULT_SCROLL_END_TIMEOUT = 1000;
|
|
1786
|
+
var ACTIVE_MODAL_BY_DOCUMENT = new WeakMap;
|
|
1787
|
+
var DEFAULT_SHORTCUTS = {
|
|
1788
|
+
previous: ["ArrowLeft", "Backspace"],
|
|
1789
|
+
cancel: ["Escape"],
|
|
1790
|
+
advance: ["Enter", "ArrowRight"]
|
|
1791
|
+
};
|
|
1792
|
+
class DomTourViewDriver {
|
|
1793
|
+
focusGuard = new FocusGuard;
|
|
1794
|
+
scrollLock = new ScrollLock;
|
|
1795
|
+
modalToken = {};
|
|
1796
|
+
stepCleanups = [];
|
|
1797
|
+
commands;
|
|
1798
|
+
direction = "advance";
|
|
1799
|
+
currentStep = null;
|
|
1800
|
+
currentSignal = null;
|
|
1801
|
+
disposed = false;
|
|
1802
|
+
generation = 0;
|
|
1803
|
+
active = false;
|
|
1804
|
+
lastTargetRect = null;
|
|
1805
|
+
lastViewport = null;
|
|
1806
|
+
inertBranches = [];
|
|
1807
|
+
modalDocument = null;
|
|
1808
|
+
modalRoot = null;
|
|
1809
|
+
overlay = null;
|
|
1810
|
+
pendingKeyboardCommand = null;
|
|
1811
|
+
pointer = null;
|
|
1812
|
+
pendingFocusGeneration = null;
|
|
1813
|
+
popover = null;
|
|
1814
|
+
presentationDirty = false;
|
|
1815
|
+
rafId = null;
|
|
1816
|
+
rafCancel = null;
|
|
1817
|
+
root = null;
|
|
1818
|
+
scrollAbort = null;
|
|
1819
|
+
constructor(commands) {
|
|
1820
|
+
this.commands = commands ?? null;
|
|
1821
|
+
}
|
|
1822
|
+
setCommands(commands) {
|
|
1823
|
+
if (!this.disposed)
|
|
1824
|
+
this.commands = commands;
|
|
1825
|
+
}
|
|
1826
|
+
registerRoot(element) {
|
|
1827
|
+
if (this.disposed || this.root === element)
|
|
1828
|
+
return;
|
|
1829
|
+
if (this.active)
|
|
1830
|
+
this.releaseModality();
|
|
1831
|
+
this.root = element;
|
|
1832
|
+
this.refreshRegisteredElements();
|
|
1833
|
+
}
|
|
1834
|
+
registerOverlay(element) {
|
|
1835
|
+
if (this.disposed)
|
|
1836
|
+
return;
|
|
1837
|
+
if (this.overlay?.getElement() === element)
|
|
1838
|
+
return;
|
|
1839
|
+
this.overlay?.release();
|
|
1840
|
+
this.overlay = element ? new OverlayElement(element) : null;
|
|
1841
|
+
this.overlay?.initializeProps();
|
|
1842
|
+
this.refreshRegisteredElements();
|
|
1843
|
+
}
|
|
1844
|
+
registerPopover(element) {
|
|
1845
|
+
if (this.disposed)
|
|
1846
|
+
return;
|
|
1847
|
+
if (this.popover?.getElement() === element)
|
|
1848
|
+
return;
|
|
1849
|
+
if (!element && this.active) {
|
|
1850
|
+
this.releaseModality();
|
|
1851
|
+
this.focusGuard.deactivate();
|
|
1852
|
+
this.scrollLock.deactivate();
|
|
1853
|
+
}
|
|
1854
|
+
this.popover?.release();
|
|
1855
|
+
this.popover = element ? new PopoverElement(element) : null;
|
|
1856
|
+
this.popover?.initializeProps();
|
|
1857
|
+
if (element)
|
|
1858
|
+
this.refreshRegisteredElements();
|
|
1859
|
+
}
|
|
1860
|
+
registerPointer(element) {
|
|
1861
|
+
if (this.disposed)
|
|
1862
|
+
return;
|
|
1863
|
+
if (this.pointer?.getElement() === element)
|
|
1864
|
+
return;
|
|
1865
|
+
this.pointer?.release();
|
|
1866
|
+
this.pointer = element ? new PointerElement(element) : null;
|
|
1867
|
+
this.pointer?.initializeProps();
|
|
1868
|
+
this.refreshRegisteredElements();
|
|
1869
|
+
}
|
|
1870
|
+
async show(step, direction, signal, onBeforePopoverAppear) {
|
|
1871
|
+
this.throwIfAborted(signal);
|
|
1872
|
+
const generation = this.beginGeneration();
|
|
1873
|
+
const removeAbort = this.cancelAnimationsOnAbort(signal);
|
|
1874
|
+
const replaceVisiblePopover = this.active && onBeforePopoverAppear !== undefined;
|
|
1875
|
+
let removeTransitionKeydown = () => {};
|
|
1876
|
+
try {
|
|
1877
|
+
this.cleanupStepResources();
|
|
1878
|
+
this.throwIfStale(generation, signal);
|
|
1879
|
+
this.active = false;
|
|
1880
|
+
this.currentStep = step;
|
|
1881
|
+
this.currentSignal = signal;
|
|
1882
|
+
this.direction = direction;
|
|
1883
|
+
this.lastTargetRect = null;
|
|
1884
|
+
this.lastViewport = null;
|
|
1885
|
+
this.presentationDirty = false;
|
|
1886
|
+
if (replaceVisiblePopover) {
|
|
1887
|
+
const listener = (event) => this.queueTransitionKeydown(event, step, generation);
|
|
1888
|
+
const currentWindow = this.getWindow();
|
|
1889
|
+
if (typeof currentWindow?.addEventListener === "function") {
|
|
1890
|
+
currentWindow.addEventListener("keydown", listener);
|
|
1891
|
+
removeTransitionKeydown = () => currentWindow.removeEventListener("keydown", listener);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const target = step.target;
|
|
1895
|
+
if (!target)
|
|
1896
|
+
return;
|
|
1897
|
+
this.syncModality(step.behavior?.allowInteraction === true);
|
|
1898
|
+
await this.scrollTargetIntoView(step, target, signal);
|
|
1899
|
+
this.throwIfStale(generation, signal);
|
|
1900
|
+
this.initializeElements(step);
|
|
1901
|
+
const targetRect = target.getBoundingClientRect();
|
|
1902
|
+
await this.appear(targetRect, step, !replaceVisiblePopover, onBeforePopoverAppear);
|
|
1903
|
+
this.throwIfStale(generation, signal);
|
|
1904
|
+
this.lastTargetRect = snapshotRect(targetRect);
|
|
1905
|
+
this.lastViewport = snapshotViewport(target);
|
|
1906
|
+
this.active = true;
|
|
1907
|
+
this.activateFocus(step, target, direction, generation);
|
|
1908
|
+
this.syncScrollLock(step);
|
|
1909
|
+
this.throwIfStale(generation, signal);
|
|
1910
|
+
removeTransitionKeydown();
|
|
1911
|
+
removeTransitionKeydown = () => {};
|
|
1912
|
+
this.attachStepResources(step, target, generation, signal);
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
if (this.isCurrentGeneration(generation))
|
|
1915
|
+
this.releaseModality();
|
|
1916
|
+
throw error;
|
|
1917
|
+
} finally {
|
|
1918
|
+
removeTransitionKeydown();
|
|
1919
|
+
removeAbort();
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
async clear(signal) {
|
|
1923
|
+
if (signal.aborted || this.disposed)
|
|
1924
|
+
this.releaseModality();
|
|
1925
|
+
this.throwIfAborted(signal);
|
|
1926
|
+
const generation = this.beginGeneration();
|
|
1927
|
+
const removeAbort = this.cancelAnimationsOnAbort(signal);
|
|
1928
|
+
try {
|
|
1929
|
+
this.cleanupStepResources();
|
|
1930
|
+
this.releaseModality();
|
|
1931
|
+
this.focusGuard.deactivate();
|
|
1932
|
+
this.scrollLock.deactivate();
|
|
1933
|
+
this.throwIfStale(generation, signal);
|
|
1934
|
+
this.active = false;
|
|
1935
|
+
this.currentStep = null;
|
|
1936
|
+
this.currentSignal = null;
|
|
1937
|
+
this.lastTargetRect = null;
|
|
1938
|
+
this.lastViewport = null;
|
|
1939
|
+
this.popover?.getElement()?.removeAttribute("aria-modal");
|
|
1940
|
+
await Promise.allSettled([
|
|
1941
|
+
this.overlay?.disappear() ?? Promise.resolve(),
|
|
1942
|
+
this.popover?.disappear() ?? Promise.resolve(),
|
|
1943
|
+
this.pointer?.disappear() ?? Promise.resolve()
|
|
1944
|
+
]);
|
|
1945
|
+
this.throwIfStale(generation, signal);
|
|
1946
|
+
} finally {
|
|
1947
|
+
removeAbort();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
releaseMount() {
|
|
1951
|
+
if (this.disposed)
|
|
1952
|
+
return;
|
|
1953
|
+
this.beginGeneration();
|
|
1954
|
+
this.cleanupStepResources();
|
|
1955
|
+
this.releaseModality();
|
|
1956
|
+
this.focusGuard.deactivate();
|
|
1957
|
+
this.scrollLock.deactivate();
|
|
1958
|
+
this.active = false;
|
|
1959
|
+
this.currentStep = null;
|
|
1960
|
+
this.currentSignal = null;
|
|
1961
|
+
this.overlay?.release();
|
|
1962
|
+
this.popover?.release();
|
|
1963
|
+
this.pointer?.release();
|
|
1964
|
+
this.overlay = null;
|
|
1965
|
+
this.popover = null;
|
|
1966
|
+
this.pointer = null;
|
|
1967
|
+
this.root = null;
|
|
1968
|
+
}
|
|
1969
|
+
dispose() {
|
|
1970
|
+
if (this.disposed)
|
|
1971
|
+
return;
|
|
1972
|
+
this.releaseMount();
|
|
1973
|
+
this.disposed = true;
|
|
1974
|
+
this.commands = null;
|
|
1975
|
+
}
|
|
1976
|
+
refreshRegisteredElements() {
|
|
1977
|
+
if (!this.active || !this.currentStep || !this.lastTargetRect)
|
|
1978
|
+
return;
|
|
1979
|
+
const generation = this.beginGeneration();
|
|
1980
|
+
this.cleanupStepResources();
|
|
1981
|
+
this.activateRegisteredElements(generation).catch((error) => {
|
|
1982
|
+
if (!this.isCurrentGeneration(generation))
|
|
1983
|
+
return;
|
|
1984
|
+
return this.commands?.reportError(error);
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
async activateRegisteredElements(generation) {
|
|
1988
|
+
const step = this.currentStep;
|
|
1989
|
+
const targetRect = this.lastTargetRect;
|
|
1990
|
+
const target = step?.target;
|
|
1991
|
+
const signal = this.currentSignal;
|
|
1992
|
+
if (this.disposed || !step || !target || !targetRect || !signal)
|
|
1993
|
+
return;
|
|
1994
|
+
this.initializeElements(step);
|
|
1995
|
+
await this.appear(targetRect, step);
|
|
1996
|
+
this.throwIfStale(generation);
|
|
1997
|
+
this.activateFocus(step, target, this.direction, generation);
|
|
1998
|
+
this.syncScrollLock(step);
|
|
1999
|
+
this.throwIfStale(generation);
|
|
2000
|
+
this.attachStepResources(step, target, generation, signal);
|
|
2001
|
+
}
|
|
2002
|
+
initializeElements(step) {
|
|
2003
|
+
const interactionAllowed = step.behavior?.allowInteraction === true;
|
|
2004
|
+
this.overlay?.initializeProps();
|
|
2005
|
+
this.overlay?.setAnimationOptions(animationOptions(step, step.overlay, this.overlay.getElement()));
|
|
2006
|
+
this.overlay?.setInteractionAllowed(interactionAllowed);
|
|
2007
|
+
this.popover?.initializeProps();
|
|
2008
|
+
this.popover?.setAnimationOptions(animationOptions(step, step.popover, this.popover.getElement()));
|
|
2009
|
+
const popover = this.popover?.getElement();
|
|
2010
|
+
if (isHTMLElement(popover, this.root ?? popover) && !interactionAllowed)
|
|
2011
|
+
popover.setAttribute("aria-modal", "true");
|
|
2012
|
+
this.pointer?.initializeProps();
|
|
2013
|
+
this.pointer?.setAnimationOptions(animationOptions(step, step.indicator, this.pointer.getElement()));
|
|
2014
|
+
}
|
|
2015
|
+
syncModality(interactionAllowed) {
|
|
2016
|
+
if (interactionAllowed) {
|
|
2017
|
+
this.releaseModality();
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const root = this.root;
|
|
2021
|
+
if (!root)
|
|
2022
|
+
return;
|
|
2023
|
+
const document2 = root.ownerDocument;
|
|
2024
|
+
const owner = ACTIVE_MODAL_BY_DOCUMENT.get(document2);
|
|
2025
|
+
if (owner && owner !== this.modalToken) {
|
|
2026
|
+
throw new Error("GlowTour.js only supports one active modal tour per document");
|
|
2027
|
+
}
|
|
2028
|
+
ACTIVE_MODAL_BY_DOCUMENT.set(document2, this.modalToken);
|
|
2029
|
+
this.modalDocument = document2;
|
|
2030
|
+
if (this.modalRoot === root)
|
|
2031
|
+
return;
|
|
2032
|
+
this.restoreInertBranches();
|
|
2033
|
+
this.modalRoot = root;
|
|
2034
|
+
let branch = root;
|
|
2035
|
+
while (branch !== document2.body) {
|
|
2036
|
+
const parent = branch.parentElement;
|
|
2037
|
+
if (!parent)
|
|
2038
|
+
break;
|
|
2039
|
+
for (const sibling of Array.from(parent.children)) {
|
|
2040
|
+
if (!isHTMLElement(sibling, root) || sibling === branch)
|
|
2041
|
+
continue;
|
|
2042
|
+
this.inertBranches.push({ element: sibling, previous: sibling.getAttribute("inert") });
|
|
2043
|
+
sibling.setAttribute("inert", "");
|
|
2044
|
+
}
|
|
2045
|
+
branch = parent;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
releaseModality() {
|
|
2049
|
+
this.popover?.getElement()?.removeAttribute("aria-modal");
|
|
2050
|
+
this.restoreInertBranches();
|
|
2051
|
+
const document2 = this.modalDocument;
|
|
2052
|
+
if (document2 && ACTIVE_MODAL_BY_DOCUMENT.get(document2) === this.modalToken) {
|
|
2053
|
+
ACTIVE_MODAL_BY_DOCUMENT.delete(document2);
|
|
2054
|
+
}
|
|
2055
|
+
this.modalDocument = null;
|
|
2056
|
+
this.modalRoot = null;
|
|
2057
|
+
}
|
|
2058
|
+
restoreInertBranches() {
|
|
2059
|
+
for (const { element, previous } of this.inertBranches.splice(0)) {
|
|
2060
|
+
if (element.getAttribute("inert") !== "")
|
|
2061
|
+
continue;
|
|
2062
|
+
if (previous === null)
|
|
2063
|
+
element.removeAttribute("inert");
|
|
2064
|
+
else
|
|
2065
|
+
element.setAttribute("inert", previous);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
async appear(targetRect, step, appearPopover = true, onBeforePopoverAppear) {
|
|
2069
|
+
const pointerEnabled = this.isPointerEnabled(step);
|
|
2070
|
+
const popoverPlacement = this.popover?.resolvePosition(targetRect, step).placement;
|
|
2071
|
+
const commitStep = onBeforePopoverAppear ? async () => {
|
|
2072
|
+
await onBeforePopoverAppear();
|
|
2073
|
+
this.syncControlState(step);
|
|
2074
|
+
this.syncShortcutLabels(step);
|
|
2075
|
+
} : undefined;
|
|
2076
|
+
const popoverTransition = this.popover ? this.popover.moveToTarget(targetRect, step, appearPopover, commitStep) : Promise.resolve(commitStep?.());
|
|
2077
|
+
await Promise.all([
|
|
2078
|
+
this.overlay?.moveToTarget(targetRect, step) ?? Promise.resolve(),
|
|
2079
|
+
popoverTransition,
|
|
2080
|
+
pointerEnabled ? this.pointer?.moveToTarget(targetRect, step, true, popoverPlacement) ?? Promise.resolve() : this.pointer?.disappear() ?? Promise.resolve()
|
|
2081
|
+
]);
|
|
2082
|
+
}
|
|
2083
|
+
attachStepResources(step, target, generation, signal) {
|
|
2084
|
+
let initialPropsNotification = true;
|
|
2085
|
+
this.stepCleanups.push(step.props.subscribe(() => {
|
|
2086
|
+
if (!this.isCurrentGeneration(generation))
|
|
2087
|
+
return;
|
|
2088
|
+
if (initialPropsNotification) {
|
|
2089
|
+
initialPropsNotification = false;
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
this.presentationDirty = true;
|
|
2093
|
+
}));
|
|
2094
|
+
this.stepCleanups.push(this.commands?.subscribeCapabilities?.((active) => {
|
|
2095
|
+
if (!this.isCurrentGeneration(generation))
|
|
2096
|
+
return;
|
|
2097
|
+
this.syncControlState(step);
|
|
2098
|
+
if (active && this.pendingFocusGeneration === generation) {
|
|
2099
|
+
this.pendingFocusGeneration = null;
|
|
2100
|
+
this.focusGuard.focus();
|
|
2101
|
+
}
|
|
2102
|
+
if (active)
|
|
2103
|
+
this.flushPendingKeyboardCommand(step, generation);
|
|
2104
|
+
}) ?? (() => {}));
|
|
2105
|
+
for (const handler of step.definition.eventHandlers) {
|
|
2106
|
+
const listener = (event) => {
|
|
2107
|
+
if (!this.isCurrentGeneration(generation))
|
|
2108
|
+
return;
|
|
2109
|
+
const context = Object.freeze({
|
|
2110
|
+
advance: () => this.commandForStep("advance", step, signal),
|
|
2111
|
+
cancel: () => this.commandForStep("cancel", step, signal),
|
|
2112
|
+
previous: () => this.commandForStep("previous", step, signal),
|
|
2113
|
+
props: step.props,
|
|
2114
|
+
signal,
|
|
2115
|
+
target
|
|
2116
|
+
});
|
|
2117
|
+
Promise.resolve().then(() => handler.callback(event, context)).catch((error) => {
|
|
2118
|
+
if (signal.aborted || this.currentStep !== step)
|
|
2119
|
+
return;
|
|
2120
|
+
return this.commands?.reportError(error);
|
|
2121
|
+
});
|
|
2122
|
+
};
|
|
2123
|
+
this.listen(target, handler.event, listener);
|
|
2124
|
+
}
|
|
2125
|
+
const currentWindow = this.getWindow(target);
|
|
2126
|
+
if (typeof currentWindow?.addEventListener === "function") {
|
|
2127
|
+
this.listen(currentWindow, "keydown", (event) => {
|
|
2128
|
+
if (this.isCurrentGeneration(generation))
|
|
2129
|
+
this.handleKeydown(event);
|
|
2130
|
+
});
|
|
2131
|
+
this.listen(currentWindow, "click", (event) => {
|
|
2132
|
+
if (this.isCurrentGeneration(generation)) {
|
|
2133
|
+
this.handleOverlayClick(event, step, target);
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
this.attachButtonHandlers(step);
|
|
2138
|
+
this.observeControls(step, generation);
|
|
2139
|
+
this.syncControlState(step);
|
|
2140
|
+
this.syncShortcutLabels(step);
|
|
2141
|
+
this.schedulePosition(generation);
|
|
2142
|
+
}
|
|
2143
|
+
listen(target, type, listener, options) {
|
|
2144
|
+
target.addEventListener(type, listener, options);
|
|
2145
|
+
this.stepCleanups.push(() => target.removeEventListener(type, listener, options));
|
|
2146
|
+
}
|
|
2147
|
+
schedulePosition(generation = this.generation) {
|
|
2148
|
+
if (!this.isCurrentGeneration(generation) || !this.currentStep || this.rafId !== null)
|
|
2149
|
+
return;
|
|
2150
|
+
const owner = this.currentStep.target?.ownerDocument?.defaultView;
|
|
2151
|
+
const ownerRequest = owner?.requestAnimationFrame;
|
|
2152
|
+
const ownerCancel = owner?.cancelAnimationFrame;
|
|
2153
|
+
const ownerHasFrameCapability = typeof ownerRequest === "function" || typeof ownerCancel === "function";
|
|
2154
|
+
const request = ownerHasFrameCapability ? ownerRequest : globalThis.requestAnimationFrame;
|
|
2155
|
+
const cancel = ownerHasFrameCapability ? ownerCancel : globalThis.cancelAnimationFrame;
|
|
2156
|
+
if (typeof request !== "function" || typeof cancel !== "function")
|
|
2157
|
+
return;
|
|
2158
|
+
const frameWindow = ownerHasFrameCapability && owner ? owner : globalThis;
|
|
2159
|
+
this.rafCancel = (id) => cancel.call(frameWindow, id);
|
|
2160
|
+
this.rafId = request.call(frameWindow, () => {
|
|
2161
|
+
this.rafId = null;
|
|
2162
|
+
this.rafCancel = null;
|
|
2163
|
+
if (!this.isCurrentGeneration(generation))
|
|
2164
|
+
return;
|
|
2165
|
+
this.updatePosition(generation);
|
|
2166
|
+
this.schedulePosition(generation);
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
updatePosition(generation) {
|
|
2170
|
+
const step = this.currentStep;
|
|
2171
|
+
const target = step?.target;
|
|
2172
|
+
if (!this.isCurrentGeneration(generation) || !step || !target)
|
|
2173
|
+
return;
|
|
2174
|
+
if (!this.isCurrentTargetAvailable(target)) {
|
|
2175
|
+
this.stopForDisconnectedTarget(target, generation);
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
const targetRect = target.getBoundingClientRect();
|
|
2179
|
+
const targetSnapshot = snapshotRect(targetRect);
|
|
2180
|
+
const viewportSnapshot = snapshotViewport(target);
|
|
2181
|
+
const presentationChanged = this.presentationDirty;
|
|
2182
|
+
if (!presentationChanged && sameRect(targetSnapshot, this.lastTargetRect) && sameViewport(viewportSnapshot, this.lastViewport))
|
|
2183
|
+
return;
|
|
2184
|
+
if (presentationChanged) {
|
|
2185
|
+
this.overlay?.setAnimationOptions(animationOptions(step, step.overlay, this.overlay.getElement()));
|
|
2186
|
+
this.popover?.setAnimationOptions(animationOptions(step, step.popover, this.popover.getElement()));
|
|
2187
|
+
this.pointer?.setAnimationOptions(animationOptions(step, step.indicator, this.pointer.getElement()));
|
|
2188
|
+
this.syncControlState(step);
|
|
2189
|
+
this.syncShortcutLabels(step);
|
|
2190
|
+
}
|
|
2191
|
+
this.overlay?.updatePosition(targetRect, step, presentationChanged, (transition) => this.observeDynamicOperation(transition, generation));
|
|
2192
|
+
const popoverPlacement = this.popover?.updatePosition(targetRect, step, (reposition) => this.observeDynamicOperation(reposition, generation));
|
|
2193
|
+
if (presentationChanged) {
|
|
2194
|
+
this.pointer?.syncVisibility(this.isPointerEnabled(step), targetRect, step, popoverPlacement);
|
|
2195
|
+
} else if (this.isPointerEnabled(step)) {
|
|
2196
|
+
const pointer = this.pointer?.getElement();
|
|
2197
|
+
if (pointer?.getAttribute("aria-hidden") === "true") {
|
|
2198
|
+
this.observeDynamicOperation(this.pointer?.moveToTarget(targetRect, step, true, popoverPlacement), generation);
|
|
2199
|
+
} else {
|
|
2200
|
+
this.pointer?.updatePosition(targetRect, step, popoverPlacement);
|
|
2201
|
+
}
|
|
2202
|
+
} else if (this.pointer?.getElement()?.getAttribute("aria-hidden") !== "true") {
|
|
2203
|
+
this.observeDynamicOperation(this.pointer?.disappear(), generation);
|
|
2204
|
+
}
|
|
2205
|
+
this.lastTargetRect = targetSnapshot;
|
|
2206
|
+
this.lastViewport = viewportSnapshot;
|
|
2207
|
+
if (presentationChanged)
|
|
2208
|
+
this.presentationDirty = false;
|
|
2209
|
+
}
|
|
2210
|
+
observeDynamicOperation(operation, generation) {
|
|
2211
|
+
if (!operation)
|
|
2212
|
+
return;
|
|
2213
|
+
operation.catch((error) => {
|
|
2214
|
+
if (!this.isCurrentGeneration(generation))
|
|
2215
|
+
return;
|
|
2216
|
+
try {
|
|
2217
|
+
const reported = this.commands?.reportError(error);
|
|
2218
|
+
reported?.catch(() => {});
|
|
2219
|
+
} catch {}
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
handleKeydown(event) {
|
|
2223
|
+
const step = this.currentStep;
|
|
2224
|
+
if (!step || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey)
|
|
2225
|
+
return;
|
|
2226
|
+
if (event.key === "Tab" && step.behavior?.allowInteraction !== true) {
|
|
2227
|
+
this.loopFocus(event);
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
const shortcuts = step.popover?.keyboardShortcuts;
|
|
2231
|
+
if ((shortcuts?.cancel ?? DEFAULT_SHORTCUTS.cancel).includes(event.key) && this.canCommand("cancel", step)) {
|
|
2232
|
+
event.preventDefault();
|
|
2233
|
+
this.command("cancel", "keyboard");
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (isEditable(event.target, this.root))
|
|
2237
|
+
return;
|
|
2238
|
+
if ((shortcuts?.advance ?? DEFAULT_SHORTCUTS.advance).includes(event.key) && this.canCommand("advance", step)) {
|
|
2239
|
+
event.preventDefault();
|
|
2240
|
+
this.command("advance", "keyboard");
|
|
2241
|
+
} else if ((shortcuts?.previous ?? DEFAULT_SHORTCUTS.previous).includes(event.key) && this.canCommand("previous", step)) {
|
|
2242
|
+
event.preventDefault();
|
|
2243
|
+
this.command("previous", "keyboard");
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
handleOverlayClick(event, step, target) {
|
|
2247
|
+
if (this.currentStep !== step || event.defaultPrevented)
|
|
2248
|
+
return;
|
|
2249
|
+
if (step.behavior?.allowInteraction === true)
|
|
2250
|
+
return;
|
|
2251
|
+
const path = event.composedPath();
|
|
2252
|
+
const popover = this.popover?.getElement();
|
|
2253
|
+
const pointer = this.pointer?.getElement();
|
|
2254
|
+
if (path.includes(target) || popover && path.includes(popover) || pointer && path.includes(pointer))
|
|
2255
|
+
return;
|
|
2256
|
+
const overlayClick = step.behavior?.overlayClick ?? "none";
|
|
2257
|
+
if (overlayClick === "advance" && this.canCommand("advance", step)) {
|
|
2258
|
+
this.command("advance", "overlay");
|
|
2259
|
+
} else if (overlayClick === "cancel" && this.canCommand("cancel", step)) {
|
|
2260
|
+
this.command("cancel", "overlay");
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
queueTransitionKeydown(event, step, generation) {
|
|
2264
|
+
if (!this.isCurrentGeneration(generation) || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey)
|
|
2265
|
+
return;
|
|
2266
|
+
const shortcuts = step.popover?.keyboardShortcuts;
|
|
2267
|
+
let command = null;
|
|
2268
|
+
if ((shortcuts?.cancel ?? DEFAULT_SHORTCUTS.cancel).includes(event.key))
|
|
2269
|
+
command = "cancel";
|
|
2270
|
+
else if (!isEditable(event.target, this.root)) {
|
|
2271
|
+
if ((shortcuts?.advance ?? DEFAULT_SHORTCUTS.advance).includes(event.key) && step.props.get().popover?.disableAdvanceButton !== true)
|
|
2272
|
+
command = "advance";
|
|
2273
|
+
else if ((shortcuts?.previous ?? DEFAULT_SHORTCUTS.previous).includes(event.key) && step.props.get().popover?.disablePreviousButton !== true)
|
|
2274
|
+
command = "previous";
|
|
2275
|
+
}
|
|
2276
|
+
if (!command)
|
|
2277
|
+
return;
|
|
2278
|
+
event.preventDefault();
|
|
2279
|
+
this.pendingKeyboardCommand ??= { command, generation };
|
|
2280
|
+
}
|
|
2281
|
+
flushPendingKeyboardCommand(step, generation) {
|
|
2282
|
+
const pending = this.pendingKeyboardCommand;
|
|
2283
|
+
if (!pending || pending.generation !== generation)
|
|
2284
|
+
return;
|
|
2285
|
+
this.pendingKeyboardCommand = null;
|
|
2286
|
+
queueMicrotask(() => {
|
|
2287
|
+
if (!this.isCurrentGeneration(generation) || this.currentStep !== step || !this.canCommand(pending.command, step))
|
|
2288
|
+
return;
|
|
2289
|
+
this.commandForGeneration(pending.command, generation, "keyboard");
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
loopFocus(event) {
|
|
2293
|
+
const popover = this.popover?.getElement();
|
|
2294
|
+
if (!isHTMLElement(popover, this.root ?? popover))
|
|
2295
|
+
return;
|
|
2296
|
+
const focusable = focusableElementsOwnedBy(popover);
|
|
2297
|
+
if (focusable.length === 0) {
|
|
2298
|
+
event.preventDefault();
|
|
2299
|
+
popover.focus();
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const current = popover.ownerDocument.activeElement;
|
|
2303
|
+
const index = isHTMLElement(current, popover) ? focusable.indexOf(current) : -1;
|
|
2304
|
+
if (event.shiftKey && (index <= 0 || !popover.contains(current))) {
|
|
2305
|
+
event.preventDefault();
|
|
2306
|
+
focusable.at(-1)?.focus();
|
|
2307
|
+
} else if (!event.shiftKey && (index === focusable.length - 1 || !popover.contains(current))) {
|
|
2308
|
+
event.preventDefault();
|
|
2309
|
+
focusable[0]?.focus();
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
activateFocus(step, target, direction, generation) {
|
|
2313
|
+
const popover = this.popover?.getElement();
|
|
2314
|
+
if (!isHTMLElement(popover, this.root ?? popover))
|
|
2315
|
+
return;
|
|
2316
|
+
const autoFocus = step.behavior?.disableAutoFocus !== true;
|
|
2317
|
+
const deferFocus = autoFocus && this.commands?.subscribeCapabilities !== undefined;
|
|
2318
|
+
if (deferFocus)
|
|
2319
|
+
this.pendingFocusGeneration = generation;
|
|
2320
|
+
this.focusGuard.activate({
|
|
2321
|
+
allowedTarget: target,
|
|
2322
|
+
allowTargetInteraction: step.behavior?.allowInteraction === true,
|
|
2323
|
+
autoFocus: autoFocus && !deferFocus,
|
|
2324
|
+
direction,
|
|
2325
|
+
fallback: this.root ?? popover.parentElement,
|
|
2326
|
+
popover
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
syncScrollLock(step) {
|
|
2330
|
+
if (step.allowScroll) {
|
|
2331
|
+
this.scrollLock.deactivate();
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
this.scrollLock.activate(this.root?.ownerDocument ?? this.popover?.getElement()?.ownerDocument);
|
|
2335
|
+
}
|
|
2336
|
+
syncShortcutLabels(step) {
|
|
2337
|
+
for (const advance of this.findTriggers("advance")) {
|
|
2338
|
+
syncKeyShortcuts(advance, step.popover?.keyboardShortcuts?.advance ?? DEFAULT_SHORTCUTS.advance);
|
|
2339
|
+
}
|
|
2340
|
+
for (const previous of this.findTriggers("previous")) {
|
|
2341
|
+
syncKeyShortcuts(previous, step.popover?.keyboardShortcuts?.previous ?? DEFAULT_SHORTCUTS.previous);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
attachButtonHandlers(step) {
|
|
2345
|
+
const generation = this.generation;
|
|
2346
|
+
const scope = this.root ?? this.popover?.getElement();
|
|
2347
|
+
if (!isHTMLElement(scope, scope) || typeof scope.addEventListener !== "function")
|
|
2348
|
+
return;
|
|
2349
|
+
this.listen(scope, "click", (event) => {
|
|
2350
|
+
if (!isElement(event.target, scope))
|
|
2351
|
+
return;
|
|
2352
|
+
const match = this.findClickedTrigger(event.target, scope);
|
|
2353
|
+
if (!match)
|
|
2354
|
+
return;
|
|
2355
|
+
this.deferTriggerCommand(match.command, event, step, generation, match.trigger);
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
findClickedTrigger(target, scope) {
|
|
2359
|
+
for (const [command, direction] of [
|
|
2360
|
+
["advance", "advance"],
|
|
2361
|
+
["previous", "previous"],
|
|
2362
|
+
["cancel", "cancel"]
|
|
2363
|
+
]) {
|
|
2364
|
+
const trigger = target.closest(`[data-glow-tour-${direction}-trigger]`);
|
|
2365
|
+
if (trigger && this.ownsTrigger(trigger, scope)) {
|
|
2366
|
+
return { command, trigger };
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
observeControls(step, generation) {
|
|
2372
|
+
const scope = this.root ?? this.popover?.getElement();
|
|
2373
|
+
if (!isHTMLElement(scope, scope))
|
|
2374
|
+
return;
|
|
2375
|
+
const MutationObserver = this.getWindow(scope)?.MutationObserver ?? globalThis.MutationObserver;
|
|
2376
|
+
if (typeof MutationObserver !== "function")
|
|
2377
|
+
return;
|
|
2378
|
+
let queued = false;
|
|
2379
|
+
const observer = new MutationObserver(() => {
|
|
2380
|
+
if (queued)
|
|
2381
|
+
return;
|
|
2382
|
+
queued = true;
|
|
2383
|
+
queueMicrotask(() => {
|
|
2384
|
+
queued = false;
|
|
2385
|
+
if (!this.isCurrentGeneration(generation) || this.currentStep !== step)
|
|
2386
|
+
return;
|
|
2387
|
+
this.syncControlState(step);
|
|
2388
|
+
this.syncShortcutLabels(step);
|
|
2389
|
+
});
|
|
2390
|
+
});
|
|
2391
|
+
observer.observe(scope, { childList: true, subtree: true });
|
|
2392
|
+
this.stepCleanups.push(() => observer.disconnect());
|
|
2393
|
+
}
|
|
2394
|
+
deferTriggerCommand(command, event, step, generation, trigger) {
|
|
2395
|
+
if (event.defaultPrevented || this.isLiveDisabled(trigger) || !this.canCommand(command, step, trigger))
|
|
2396
|
+
return;
|
|
2397
|
+
queueMicrotask(() => {
|
|
2398
|
+
if (event.defaultPrevented || this.isLiveDisabled(trigger) || !this.isCurrentGeneration(generation) || this.currentStep !== step || !this.canCommand(command, step, trigger))
|
|
2399
|
+
return;
|
|
2400
|
+
this.commandForGeneration(command, generation, "trigger");
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
syncControlState(step) {
|
|
2404
|
+
for (const advance of this.findTriggers("advance")) {
|
|
2405
|
+
this.syncControl(advance, this.commands?.isAdvanceDisabled() === true || step.props.get().popover?.disableAdvanceButton === true);
|
|
2406
|
+
}
|
|
2407
|
+
for (const previous of this.findTriggers("previous")) {
|
|
2408
|
+
this.syncControl(previous, this.commands?.isPreviousDisabled() === true || step.props.get().popover?.disablePreviousButton === true);
|
|
2409
|
+
}
|
|
2410
|
+
for (const cancel of this.findTriggers("cancel")) {
|
|
2411
|
+
this.syncControl(cancel, this.commands?.isCancelDisabled() === true);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
findTriggers(direction) {
|
|
2415
|
+
const scope = this.root ?? this.popover?.getElement();
|
|
2416
|
+
if (!isHTMLElement(scope, scope) || typeof scope.querySelectorAll !== "function")
|
|
2417
|
+
return [];
|
|
2418
|
+
return Array.from(scope.querySelectorAll(`[data-glow-tour-${direction}-trigger]`)).filter((trigger) => this.ownsTrigger(trigger, scope));
|
|
2419
|
+
}
|
|
2420
|
+
ownsTrigger(trigger, scope) {
|
|
2421
|
+
if (!scope.contains(trigger))
|
|
2422
|
+
return false;
|
|
2423
|
+
const owner = this.root ?? scope.closest("[data-glow-tour-root]");
|
|
2424
|
+
return trigger.closest("[data-glow-tour-root]") === owner;
|
|
2425
|
+
}
|
|
2426
|
+
async command(command, source) {
|
|
2427
|
+
if (this.disposed)
|
|
2428
|
+
return;
|
|
2429
|
+
if (command === "advance")
|
|
2430
|
+
await this.commands?.advance(source);
|
|
2431
|
+
else if (command === "previous")
|
|
2432
|
+
await this.commands?.previous(source);
|
|
2433
|
+
else
|
|
2434
|
+
await this.commands?.cancel(source);
|
|
2435
|
+
}
|
|
2436
|
+
commandForGeneration(command, generation, source) {
|
|
2437
|
+
return this.isCurrentGeneration(generation) ? this.command(command, source) : Promise.resolve();
|
|
2438
|
+
}
|
|
2439
|
+
commandForStep(command, step, signal) {
|
|
2440
|
+
return !signal.aborted && this.currentStep === step ? this.command(command, "api") : Promise.resolve();
|
|
2441
|
+
}
|
|
2442
|
+
canCommand(command, step, trigger) {
|
|
2443
|
+
if (this.isConsumerDisabled(trigger ?? null))
|
|
2444
|
+
return false;
|
|
2445
|
+
if (command === "advance") {
|
|
2446
|
+
return (this.commands?.canAdvance?.() ?? true) && step.props.get().popover?.disableAdvanceButton !== true;
|
|
2447
|
+
}
|
|
2448
|
+
if (command === "previous") {
|
|
2449
|
+
return (this.commands?.canPrevious?.() ?? true) && step.props.get().popover?.disablePreviousButton !== true;
|
|
2450
|
+
}
|
|
2451
|
+
return this.commands?.canCancel?.() ?? true;
|
|
2452
|
+
}
|
|
2453
|
+
syncControl(element, disabled) {
|
|
2454
|
+
if (!element)
|
|
2455
|
+
return;
|
|
2456
|
+
if (element.hasAttribute("data-glow-tour-control-managed"))
|
|
2457
|
+
return;
|
|
2458
|
+
const isDisabled = disabled || this.isConsumerDisabled(element);
|
|
2459
|
+
if (element.disabled !== isDisabled)
|
|
2460
|
+
element.disabled = isDisabled;
|
|
2461
|
+
if (element.getAttribute("aria-disabled") !== String(isDisabled)) {
|
|
2462
|
+
element.setAttribute("aria-disabled", String(isDisabled));
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
isConsumerDisabled(element) {
|
|
2466
|
+
return element?.getAttribute("data-glow-tour-consumer-disabled") === "true";
|
|
2467
|
+
}
|
|
2468
|
+
isLiveDisabled(element) {
|
|
2469
|
+
return element.disabled || element.getAttribute("aria-disabled") === "true" || this.isConsumerDisabled(element);
|
|
2470
|
+
}
|
|
2471
|
+
isPointerEnabled(step) {
|
|
2472
|
+
return step.behavior?.allowInteraction === true && step.indicator?.disabled !== true;
|
|
2473
|
+
}
|
|
2474
|
+
cleanupStepResources() {
|
|
2475
|
+
this.scrollAbort?.abort();
|
|
2476
|
+
this.scrollAbort = null;
|
|
2477
|
+
this.presentationDirty = false;
|
|
2478
|
+
if (this.rafId !== null)
|
|
2479
|
+
this.rafCancel?.(this.rafId);
|
|
2480
|
+
this.rafId = null;
|
|
2481
|
+
this.rafCancel = null;
|
|
2482
|
+
for (const cleanup of this.stepCleanups.splice(0))
|
|
2483
|
+
cleanup();
|
|
2484
|
+
}
|
|
2485
|
+
isCurrentTargetAvailable(target) {
|
|
2486
|
+
const rootDocument = this.root?.ownerDocument;
|
|
2487
|
+
return target.isConnected && (!rootDocument || target.ownerDocument === rootDocument);
|
|
2488
|
+
}
|
|
2489
|
+
stopForDisconnectedTarget(target, generation) {
|
|
2490
|
+
if (!this.isCurrentGeneration(generation))
|
|
2491
|
+
return;
|
|
2492
|
+
this.beginGeneration();
|
|
2493
|
+
this.cleanupStepResources();
|
|
2494
|
+
this.releaseModality();
|
|
2495
|
+
this.focusGuard.deactivate();
|
|
2496
|
+
this.scrollLock.deactivate();
|
|
2497
|
+
this.active = false;
|
|
2498
|
+
this.currentSignal = null;
|
|
2499
|
+
this.currentStep = null;
|
|
2500
|
+
this.lastTargetRect = null;
|
|
2501
|
+
this.lastViewport = null;
|
|
2502
|
+
Promise.resolve(this.commands?.targetDisconnected(target)).catch((error) => {
|
|
2503
|
+
this.commands?.reportError(error).catch(() => {});
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
beginGeneration() {
|
|
2507
|
+
this.generation += 1;
|
|
2508
|
+
this.pendingKeyboardCommand = null;
|
|
2509
|
+
this.pendingFocusGeneration = null;
|
|
2510
|
+
this.cancelElementAnimations();
|
|
2511
|
+
return this.generation;
|
|
2512
|
+
}
|
|
2513
|
+
cancelAnimationsOnAbort(signal) {
|
|
2514
|
+
const onAbort = () => this.cancelElementAnimations();
|
|
2515
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2516
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
2517
|
+
}
|
|
2518
|
+
cancelElementAnimations() {
|
|
2519
|
+
this.overlay?.cancelAnimations();
|
|
2520
|
+
this.popover?.cancelAnimations();
|
|
2521
|
+
this.pointer?.cancelAnimations();
|
|
2522
|
+
}
|
|
2523
|
+
isCurrentGeneration(generation) {
|
|
2524
|
+
return !this.disposed && generation === this.generation;
|
|
2525
|
+
}
|
|
2526
|
+
async scrollTargetIntoView(step, target, signal) {
|
|
2527
|
+
this.throwIfAborted(signal);
|
|
2528
|
+
if (step.behavior?.disableAutoScroll || isInViewport(target.getBoundingClientRect(), target))
|
|
2529
|
+
return;
|
|
2530
|
+
const currentWindow = this.getWindow(target);
|
|
2531
|
+
if (!currentWindow)
|
|
2532
|
+
return;
|
|
2533
|
+
const controller = new AbortController;
|
|
2534
|
+
this.scrollAbort = controller;
|
|
2535
|
+
await new Promise((resolve, reject) => {
|
|
2536
|
+
let timeout = null;
|
|
2537
|
+
const abort = () => finish(abortError2());
|
|
2538
|
+
const finish = (error) => {
|
|
2539
|
+
currentWindow.removeEventListener("scrollend", complete);
|
|
2540
|
+
signal.removeEventListener("abort", abort);
|
|
2541
|
+
controller.signal.removeEventListener("abort", abort);
|
|
2542
|
+
if (timeout !== null)
|
|
2543
|
+
clearTimeout(timeout);
|
|
2544
|
+
if (this.scrollAbort === controller)
|
|
2545
|
+
this.scrollAbort = null;
|
|
2546
|
+
if (error)
|
|
2547
|
+
reject(error);
|
|
2548
|
+
else
|
|
2549
|
+
resolve();
|
|
2550
|
+
};
|
|
2551
|
+
const complete = () => finish();
|
|
2552
|
+
currentWindow.addEventListener("scrollend", complete, { once: true });
|
|
2553
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2554
|
+
controller.signal.addEventListener("abort", abort, { once: true });
|
|
2555
|
+
timeout = setTimeout(complete, DEFAULT_SCROLL_END_TIMEOUT);
|
|
2556
|
+
try {
|
|
2557
|
+
target.scrollIntoView({
|
|
2558
|
+
behavior: prefersReducedMotion(target) ? "instant" : step.behavior?.scroll?.behavior ?? "smooth",
|
|
2559
|
+
block: step.behavior?.scroll?.block ?? "center",
|
|
2560
|
+
inline: step.behavior?.scroll?.inline ?? "nearest"
|
|
2561
|
+
});
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
throwIfAborted(signal) {
|
|
2568
|
+
if (signal.aborted || this.disposed)
|
|
2569
|
+
throw abortError2();
|
|
2570
|
+
}
|
|
2571
|
+
throwIfStale(generation, signal) {
|
|
2572
|
+
if (!this.isCurrentGeneration(generation) || signal?.aborted)
|
|
2573
|
+
throw abortError2();
|
|
2574
|
+
}
|
|
2575
|
+
getWindow(element) {
|
|
2576
|
+
return ownerWindow(element ?? this.root ?? this.popover?.getElement() ?? this.pointer?.getElement() ?? this.overlay?.getElement());
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function animationOptions(step, options, element) {
|
|
2580
|
+
return {
|
|
2581
|
+
disabled: step.animated === false || options?.animated === false || prefersReducedMotion(element),
|
|
2582
|
+
duration: options?.animation?.duration,
|
|
2583
|
+
easing: options?.animation?.easing
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
function abortError2() {
|
|
2587
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
2588
|
+
}
|
|
2589
|
+
function snapshotRect(rect) {
|
|
2590
|
+
const left = finite(rect.left);
|
|
2591
|
+
const top = finite(rect.top);
|
|
2592
|
+
const width = finite(rect.width);
|
|
2593
|
+
const height = finite(rect.height);
|
|
2594
|
+
return {
|
|
2595
|
+
bottom: finite(rect.bottom, top + height),
|
|
2596
|
+
height,
|
|
2597
|
+
left,
|
|
2598
|
+
right: finite(rect.right, left + width),
|
|
2599
|
+
top,
|
|
2600
|
+
width,
|
|
2601
|
+
x: finite(rect.x, left),
|
|
2602
|
+
y: finite(rect.y, top)
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function snapshotViewport(context) {
|
|
2606
|
+
const viewport = viewportDimensions(context);
|
|
2607
|
+
return {
|
|
2608
|
+
height: finite(viewport.height),
|
|
2609
|
+
width: finite(viewport.width)
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
function sameRect(left, right) {
|
|
2613
|
+
return right !== null && left.bottom === right.bottom && left.height === right.height && left.left === right.left && left.right === right.right && left.top === right.top && left.width === right.width && left.x === right.x && left.y === right.y;
|
|
2614
|
+
}
|
|
2615
|
+
function sameViewport(left, right) {
|
|
2616
|
+
return right !== null && left.height === right.height && left.width === right.width;
|
|
2617
|
+
}
|
|
2618
|
+
function finite(value, fallback = 0) {
|
|
2619
|
+
return Number.isFinite(value) ? value : fallback;
|
|
2620
|
+
}
|
|
2621
|
+
function isEditable(target, context) {
|
|
2622
|
+
if (!isHTMLElement(target, context))
|
|
2623
|
+
return false;
|
|
2624
|
+
if (target.matches("input, textarea, select"))
|
|
2625
|
+
return true;
|
|
2626
|
+
let current = target;
|
|
2627
|
+
while (current) {
|
|
2628
|
+
const contentEditable = current.getAttribute("contenteditable");
|
|
2629
|
+
if (contentEditable !== null)
|
|
2630
|
+
return contentEditable.toLowerCase() !== "false";
|
|
2631
|
+
current = current.parentElement;
|
|
2632
|
+
}
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
function prefersReducedMotion(context) {
|
|
2636
|
+
const currentWindow = ownerWindow(context);
|
|
2637
|
+
return typeof currentWindow?.matchMedia === "function" && currentWindow.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2638
|
+
}
|
|
2639
|
+
function syncKeyShortcuts(element, shortcuts) {
|
|
2640
|
+
if (!element)
|
|
2641
|
+
return;
|
|
2642
|
+
if (shortcuts.length === 0)
|
|
2643
|
+
element.removeAttribute("aria-keyshortcuts");
|
|
2644
|
+
else
|
|
2645
|
+
element.setAttribute("aria-keyshortcuts", shortcuts.join(" "));
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// packages/core/src/options/validation.ts
|
|
2649
|
+
function invalidOption(path) {
|
|
2650
|
+
return new TypeError(`Invalid option: ${path}`);
|
|
2651
|
+
}
|
|
2652
|
+
function validateFiniteNonNegative(path, value) {
|
|
2653
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0))
|
|
2654
|
+
throw invalidOption(path);
|
|
2655
|
+
}
|
|
2656
|
+
function validateAnimation(path, animation) {
|
|
2657
|
+
if (animation?.duration === undefined && animation !== undefined)
|
|
2658
|
+
throw invalidOption(`${path}.duration`);
|
|
2659
|
+
validateFiniteNonNegative(`${path}.duration`, animation?.duration);
|
|
2660
|
+
}
|
|
2661
|
+
function validateOverlay(path, overlay) {
|
|
2662
|
+
if (!overlay)
|
|
2663
|
+
return;
|
|
2664
|
+
validateAnimation(`${path}.animation`, overlay.animation);
|
|
2665
|
+
validateFiniteNonNegative(`${path}.padding`, overlay.padding);
|
|
2666
|
+
validateFiniteNonNegative(`${path}.radius`, overlay.radius);
|
|
2667
|
+
const opacity = overlay.opacity;
|
|
2668
|
+
if (opacity !== undefined && (!Number.isFinite(opacity) || opacity < 0 || opacity > 1)) {
|
|
2669
|
+
throw invalidOption(`${path}.opacity`);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
function validatePopover(path, popover) {
|
|
2673
|
+
if (!popover)
|
|
2674
|
+
return;
|
|
2675
|
+
validateAnimation(`${path}.animation`, popover.animation);
|
|
2676
|
+
validateFiniteNonNegative(`${path}.gap`, popover.gap);
|
|
2677
|
+
if (!popover.arrow)
|
|
2678
|
+
return;
|
|
2679
|
+
validateFiniteNonNegative(`${path}.arrow.size`, popover.arrow.size);
|
|
2680
|
+
validateFiniteNonNegative(`${path}.arrow.borderWidth`, popover.arrow.borderWidth);
|
|
2681
|
+
validateFiniteNonNegative(`${path}.arrow.borderRadius`, popover.arrow.borderRadius);
|
|
2682
|
+
validateFiniteNonNegative(`${path}.arrow.edgePadding`, popover.arrow.edgePadding);
|
|
2683
|
+
}
|
|
2684
|
+
function validateIndicator(path, indicator) {
|
|
2685
|
+
if (!indicator)
|
|
2686
|
+
return;
|
|
2687
|
+
validateAnimation(`${path}.animation`, indicator.animation);
|
|
2688
|
+
validateFiniteNonNegative(`${path}.gap`, indicator.gap);
|
|
2689
|
+
}
|
|
2690
|
+
function validateBehavior(path, behavior) {
|
|
2691
|
+
validateFiniteNonNegative(`${path}.targetTimeout`, behavior?.targetTimeout);
|
|
2692
|
+
}
|
|
2693
|
+
function validateStepProps(path, props) {
|
|
2694
|
+
validateOverlay(`${path}.overlay`, props.overlay);
|
|
2695
|
+
validatePopover(`${path}.popover`, props.popover);
|
|
2696
|
+
validateIndicator(`${path}.indicator`, props.indicator);
|
|
2697
|
+
}
|
|
2698
|
+
function validateWorkflowOptions(workflow) {
|
|
2699
|
+
validateOverlay("options.overlay", workflow.options.overlay);
|
|
2700
|
+
validatePopover("options.popover", workflow.options.popover);
|
|
2701
|
+
validateIndicator("options.indicator", workflow.options.indicator);
|
|
2702
|
+
validateBehavior("options.behavior", workflow.options.behavior);
|
|
2703
|
+
for (const [index, step] of workflow.steps.entries()) {
|
|
2704
|
+
const path = `steps[${index}]`;
|
|
2705
|
+
validateBehavior(`${path}.behavior`, step.behavior);
|
|
2706
|
+
validateStepProps(path, step.props);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// packages/core/src/utils/options.ts
|
|
2711
|
+
function mergeOverlayOptions(defaults, overrides) {
|
|
2712
|
+
if (!defaults && !overrides) {
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
return {
|
|
2716
|
+
...mergeBaseOptions(defaults, overrides),
|
|
2717
|
+
color: overrides?.color ?? defaults?.color,
|
|
2718
|
+
opacity: overrides?.opacity ?? defaults?.opacity,
|
|
2719
|
+
padding: overrides?.padding ?? defaults?.padding,
|
|
2720
|
+
radius: overrides?.radius ?? defaults?.radius
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
function mergeIndicatorOptions(defaults, overrides) {
|
|
2724
|
+
if (!defaults && !overrides) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
return {
|
|
2728
|
+
...mergeBaseOptions(defaults, overrides),
|
|
2729
|
+
disabled: overrides?.disabled ?? defaults?.disabled,
|
|
2730
|
+
gap: overrides?.gap ?? defaults?.gap,
|
|
2731
|
+
placementTryOrder: cloneArray(overrides?.placementTryOrder ?? defaults?.placementTryOrder)
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
function mergePopoverOptions(defaults, overrides) {
|
|
2735
|
+
if (!defaults && !overrides) {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
const placementTryOrder = overrides?.placementTryOrder ?? defaults?.placementTryOrder;
|
|
2739
|
+
const hasArrow = !!defaults?.arrow || !!overrides?.arrow;
|
|
2740
|
+
const hasKeyboardShortcuts = !!defaults?.keyboardShortcuts || !!overrides?.keyboardShortcuts;
|
|
2741
|
+
return {
|
|
2742
|
+
...mergeBaseOptions(defaults, overrides),
|
|
2743
|
+
arrow: hasArrow ? {
|
|
2744
|
+
disabled: overrides?.arrow?.disabled ?? defaults?.arrow?.disabled,
|
|
2745
|
+
color: overrides?.arrow?.color ?? defaults?.arrow?.color,
|
|
2746
|
+
size: overrides?.arrow?.size ?? defaults?.arrow?.size,
|
|
2747
|
+
borderWidth: overrides?.arrow?.borderWidth ?? defaults?.arrow?.borderWidth,
|
|
2748
|
+
borderRadius: overrides?.arrow?.borderRadius ?? defaults?.arrow?.borderRadius,
|
|
2749
|
+
edgePadding: overrides?.arrow?.edgePadding ?? defaults?.arrow?.edgePadding,
|
|
2750
|
+
styleNonce: overrides?.arrow?.styleNonce ?? defaults?.arrow?.styleNonce,
|
|
2751
|
+
disableAutoStyles: overrides?.arrow?.disableAutoStyles ?? defaults?.arrow?.disableAutoStyles
|
|
2752
|
+
} : undefined,
|
|
2753
|
+
disableAdvanceButton: overrides?.disableAdvanceButton ?? defaults?.disableAdvanceButton,
|
|
2754
|
+
disablePreviousButton: overrides?.disablePreviousButton ?? defaults?.disablePreviousButton,
|
|
2755
|
+
gap: overrides?.gap ?? defaults?.gap,
|
|
2756
|
+
hideAdvanceButton: overrides?.hideAdvanceButton ?? defaults?.hideAdvanceButton,
|
|
2757
|
+
hideFooter: overrides?.hideFooter ?? defaults?.hideFooter,
|
|
2758
|
+
hidePreviousButton: overrides?.hidePreviousButton ?? defaults?.hidePreviousButton,
|
|
2759
|
+
placementTryOrder: cloneArray(placementTryOrder),
|
|
2760
|
+
keyboardShortcuts: hasKeyboardShortcuts ? {
|
|
2761
|
+
previous: cloneArray(overrides?.keyboardShortcuts?.previous ?? defaults?.keyboardShortcuts?.previous),
|
|
2762
|
+
advance: cloneArray(overrides?.keyboardShortcuts?.advance ?? defaults?.keyboardShortcuts?.advance),
|
|
2763
|
+
cancel: cloneArray(overrides?.keyboardShortcuts?.cancel ?? defaults?.keyboardShortcuts?.cancel)
|
|
2764
|
+
} : undefined
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
function mergeScrollOptions(defaults, overrides) {
|
|
2768
|
+
if (!defaults && !overrides) {
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
return {
|
|
2772
|
+
behavior: overrides?.behavior ?? defaults?.behavior,
|
|
2773
|
+
block: overrides?.block ?? defaults?.block,
|
|
2774
|
+
inline: overrides?.inline ?? defaults?.inline
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function mergeAnimationOptions(defaults, overrides) {
|
|
2778
|
+
if (!defaults) {
|
|
2779
|
+
return overrides ? { ...overrides } : undefined;
|
|
2780
|
+
}
|
|
2781
|
+
if (!overrides) {
|
|
2782
|
+
return { ...defaults };
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
duration: overrides.duration ?? defaults.duration,
|
|
2786
|
+
easing: overrides.easing ?? defaults.easing
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
function mergeBaseOptions(defaults, overrides) {
|
|
2790
|
+
return {
|
|
2791
|
+
animated: overrides?.animated ?? defaults?.animated,
|
|
2792
|
+
animation: mergeAnimationOptions(defaults?.animation, overrides?.animation)
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
function mergeStepBehavior(defaults, overrides) {
|
|
2796
|
+
if (!defaults && !overrides) {
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
return {
|
|
2800
|
+
allowInteraction: overrides?.allowInteraction ?? defaults?.allowInteraction,
|
|
2801
|
+
disableAutoFocus: overrides?.disableAutoFocus ?? defaults?.disableAutoFocus,
|
|
2802
|
+
disableAutoScroll: overrides?.disableAutoScroll ?? defaults?.disableAutoScroll,
|
|
2803
|
+
missingTargetStrategy: overrides?.missingTargetStrategy ?? defaults?.missingTargetStrategy,
|
|
2804
|
+
scroll: mergeScrollOptions(defaults?.scroll, overrides?.scroll),
|
|
2805
|
+
targetTimeout: overrides?.targetTimeout ?? defaults?.targetTimeout,
|
|
2806
|
+
overlayClick: overrides?.overlayClick ?? defaults?.overlayClick
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
function cloneArray(value) {
|
|
2810
|
+
return value ? [...value] : undefined;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// packages/core/src/runtime/step-props-store.ts
|
|
2814
|
+
function createStepPropsStore(initialProps, reportListenerError, path = "steps[0]") {
|
|
2815
|
+
let current = freezeStepProps(initialProps);
|
|
2816
|
+
const listeners = new Set;
|
|
2817
|
+
const notify = (listener, props) => {
|
|
2818
|
+
try {
|
|
2819
|
+
listener(props);
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
try {
|
|
2822
|
+
reportListenerError(error);
|
|
2823
|
+
} catch {}
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
return Object.freeze({
|
|
2827
|
+
get: () => current,
|
|
2828
|
+
set: (update) => {
|
|
2829
|
+
const next = typeof update === "function" ? update(current) : update;
|
|
2830
|
+
validateStepProps(path, next);
|
|
2831
|
+
current = freezeStepProps(next);
|
|
2832
|
+
const published = current;
|
|
2833
|
+
for (const listener of Array.from(listeners))
|
|
2834
|
+
notify(listener, published);
|
|
2835
|
+
},
|
|
2836
|
+
subscribe: (listener) => {
|
|
2837
|
+
notify(listener, current);
|
|
2838
|
+
listeners.add(listener);
|
|
2839
|
+
let subscribed = true;
|
|
2840
|
+
return () => {
|
|
2841
|
+
if (!subscribed)
|
|
2842
|
+
return;
|
|
2843
|
+
subscribed = false;
|
|
2844
|
+
listeners.delete(listener);
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// packages/core/src/runtime/active-step.ts
|
|
2851
|
+
class ActiveStep {
|
|
2852
|
+
definition;
|
|
2853
|
+
path;
|
|
2854
|
+
rootDocument;
|
|
2855
|
+
initialProps;
|
|
2856
|
+
props;
|
|
2857
|
+
behavior;
|
|
2858
|
+
animated;
|
|
2859
|
+
allowScroll;
|
|
2860
|
+
target = null;
|
|
2861
|
+
constructor(definition, defaults, reportSubscriberError = () => {}, path = "steps[0]", rootDocument) {
|
|
2862
|
+
this.definition = definition;
|
|
2863
|
+
this.path = path;
|
|
2864
|
+
this.rootDocument = rootDocument;
|
|
2865
|
+
this.initialProps = freezeStepProps({
|
|
2866
|
+
title: definition.props.title,
|
|
2867
|
+
content: definition.props.content,
|
|
2868
|
+
data: definition.props.data,
|
|
2869
|
+
overlay: mergeOverlayOptions(defaults.overlay, definition.props.overlay),
|
|
2870
|
+
popover: mergePopoverOptions(defaults.popover, definition.props.popover),
|
|
2871
|
+
indicator: mergeIndicatorOptions(defaults.indicator, definition.props.indicator)
|
|
2872
|
+
});
|
|
2873
|
+
this.props = createStepPropsStore(this.initialProps, reportSubscriberError, path);
|
|
2874
|
+
this.behavior = mergeStepBehavior(defaults.behavior, definition.behavior);
|
|
2875
|
+
this.animated = defaults.animated;
|
|
2876
|
+
this.allowScroll = defaults.allowScroll === true;
|
|
2877
|
+
}
|
|
2878
|
+
reset() {
|
|
2879
|
+
this.props.set(this.initialProps);
|
|
2880
|
+
}
|
|
2881
|
+
get overlay() {
|
|
2882
|
+
return this.props.get().overlay;
|
|
2883
|
+
}
|
|
2884
|
+
get popover() {
|
|
2885
|
+
return this.props.get().popover;
|
|
2886
|
+
}
|
|
2887
|
+
get indicator() {
|
|
2888
|
+
return this.props.get().indicator;
|
|
2889
|
+
}
|
|
2890
|
+
async resolveTarget(signal) {
|
|
2891
|
+
return await resolveTargetElement(this.definition.target, { document: this.rootDocument, signal }, this.path);
|
|
2892
|
+
}
|
|
2893
|
+
snapshot() {
|
|
2894
|
+
return Object.freeze({
|
|
2895
|
+
id: this.definition.id,
|
|
2896
|
+
initialProps: freezeStepProps(this.initialProps),
|
|
2897
|
+
currentProps: freezeStepProps(this.props.get()),
|
|
2898
|
+
target: this.target
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// packages/core/src/runtime/adapter-contract.ts
|
|
2904
|
+
var ADAPTER_BRIDGE_SYMBOL = Symbol.for("@glowhop/core-tour/adapter-bridge/v1");
|
|
2905
|
+
var ADAPTER_BRIDGE_VERSION = 1;
|
|
2906
|
+
|
|
2907
|
+
// packages/core/src/runtime/root-bridge.ts
|
|
2908
|
+
var ROOT_OWNER_SYMBOL = Symbol.for("@glowhop/core-tour/root-owner/v1");
|
|
2909
|
+
var DOCUMENT_PREFIX_RESERVATIONS_SYMBOL = Symbol.for("@glowhop/core-tour/id-prefix-reservations/v1");
|
|
2910
|
+
var DEFAULT_ID_PREFIX = "glow-tour";
|
|
2911
|
+
var ID_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
2912
|
+
|
|
2913
|
+
class TourRootBinding {
|
|
2914
|
+
driver;
|
|
2915
|
+
root;
|
|
2916
|
+
ids;
|
|
2917
|
+
reservation;
|
|
2918
|
+
mutations;
|
|
2919
|
+
onMountReleaseStart;
|
|
2920
|
+
onMountReleaseComplete;
|
|
2921
|
+
onRelease;
|
|
2922
|
+
released = false;
|
|
2923
|
+
overlay = null;
|
|
2924
|
+
pointer = null;
|
|
2925
|
+
popover = null;
|
|
2926
|
+
constructor(driver, root, ids, reservation, mutations, onMountReleaseStart, onMountReleaseComplete, onRelease) {
|
|
2927
|
+
this.driver = driver;
|
|
2928
|
+
this.root = root;
|
|
2929
|
+
this.ids = ids;
|
|
2930
|
+
this.reservation = reservation;
|
|
2931
|
+
this.mutations = mutations;
|
|
2932
|
+
this.onMountReleaseStart = onMountReleaseStart;
|
|
2933
|
+
this.onMountReleaseComplete = onMountReleaseComplete;
|
|
2934
|
+
this.onRelease = onRelease;
|
|
2935
|
+
}
|
|
2936
|
+
bindOverlay(element) {
|
|
2937
|
+
this.assertLiveElement(element);
|
|
2938
|
+
this.overlay = element;
|
|
2939
|
+
this.driver.registerOverlay(element);
|
|
2940
|
+
return () => {
|
|
2941
|
+
if (this.released || this.overlay !== element)
|
|
2942
|
+
return;
|
|
2943
|
+
this.overlay = null;
|
|
2944
|
+
this.driver.registerOverlay(null);
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
bindPointer(element) {
|
|
2948
|
+
this.assertLiveElement(element);
|
|
2949
|
+
this.pointer = element;
|
|
2950
|
+
this.driver.registerPointer(element);
|
|
2951
|
+
return () => {
|
|
2952
|
+
if (this.released || this.pointer !== element)
|
|
2953
|
+
return;
|
|
2954
|
+
this.pointer = null;
|
|
2955
|
+
this.driver.registerPointer(null);
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
bindPopover(element) {
|
|
2959
|
+
this.assertLiveElement(element);
|
|
2960
|
+
this.popover = element;
|
|
2961
|
+
this.driver.registerPopover(element);
|
|
2962
|
+
return () => {
|
|
2963
|
+
if (this.released || this.popover !== element)
|
|
2964
|
+
return;
|
|
2965
|
+
this.popover = null;
|
|
2966
|
+
this.driver.registerPopover(null);
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
hasPopover() {
|
|
2970
|
+
return this.popover !== null;
|
|
2971
|
+
}
|
|
2972
|
+
get document() {
|
|
2973
|
+
return this.root.ownerDocument;
|
|
2974
|
+
}
|
|
2975
|
+
release() {
|
|
2976
|
+
if (this.released)
|
|
2977
|
+
return;
|
|
2978
|
+
this.released = true;
|
|
2979
|
+
this.overlay = null;
|
|
2980
|
+
this.pointer = null;
|
|
2981
|
+
this.popover = null;
|
|
2982
|
+
try {
|
|
2983
|
+
runCleanup([
|
|
2984
|
+
this.onMountReleaseStart,
|
|
2985
|
+
() => this.driver.releaseMount(),
|
|
2986
|
+
() => this.mutations.release(),
|
|
2987
|
+
() => releaseRootOwner(this.root, this.reservation),
|
|
2988
|
+
() => releasePrefix(this.root.ownerDocument, this.ids.root.slice(0, -"-root".length), this.reservation)
|
|
2989
|
+
]);
|
|
2990
|
+
} finally {
|
|
2991
|
+
this.onRelease(this);
|
|
2992
|
+
this.onMountReleaseComplete();
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
assertLiveElement(element) {
|
|
2996
|
+
if (this.released)
|
|
2997
|
+
throw new Error("Glow tour root binding has been released");
|
|
2998
|
+
if (element === this.root || !this.root.contains(element)) {
|
|
2999
|
+
throw new Error("Glow tour elements must be descendants of the claimed root");
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function attachRootBridge(tour, driver, isDisposed, onMountReleaseStart, onMountReleaseComplete) {
|
|
3004
|
+
let binding = null;
|
|
3005
|
+
let pending = null;
|
|
3006
|
+
const bridge = Object.freeze({
|
|
3007
|
+
version: ADAPTER_BRIDGE_VERSION,
|
|
3008
|
+
connectRoot(options) {
|
|
3009
|
+
if (isDisposed())
|
|
3010
|
+
throw new Error("Cannot connect a root to a disposed glow tour");
|
|
3011
|
+
if (binding || pending)
|
|
3012
|
+
throw new Error("Glow tour already has a live root lease");
|
|
3013
|
+
const root = options.root;
|
|
3014
|
+
if (!root?.ownerDocument)
|
|
3015
|
+
throw new Error("Glow tour root must belong to a document");
|
|
3016
|
+
if (Reflect.has(root, ROOT_OWNER_SYMBOL)) {
|
|
3017
|
+
throw new Error("Glow tour root is already owned by another tour");
|
|
3018
|
+
}
|
|
3019
|
+
const reservation = {};
|
|
3020
|
+
const mutations = new DomMutationLease(root);
|
|
3021
|
+
let prefix = null;
|
|
3022
|
+
let ids = null;
|
|
3023
|
+
let ownsPrefix = false;
|
|
3024
|
+
let ownsRoot = false;
|
|
3025
|
+
let registeredRoot = false;
|
|
3026
|
+
let releasing = false;
|
|
3027
|
+
const rollback = () => {
|
|
3028
|
+
runCleanup([
|
|
3029
|
+
() => {
|
|
3030
|
+
if (!registeredRoot)
|
|
3031
|
+
return;
|
|
3032
|
+
registeredRoot = false;
|
|
3033
|
+
driver.releaseMount();
|
|
3034
|
+
},
|
|
3035
|
+
() => mutations.release(),
|
|
3036
|
+
() => {
|
|
3037
|
+
if (!ownsRoot)
|
|
3038
|
+
return;
|
|
3039
|
+
ownsRoot = false;
|
|
3040
|
+
releaseRootOwner(root, reservation);
|
|
3041
|
+
},
|
|
3042
|
+
() => {
|
|
3043
|
+
if (!ownsPrefix || !prefix)
|
|
3044
|
+
return;
|
|
3045
|
+
ownsPrefix = false;
|
|
3046
|
+
releasePrefix(root.ownerDocument, prefix, reservation);
|
|
3047
|
+
}
|
|
3048
|
+
]);
|
|
3049
|
+
};
|
|
3050
|
+
const pendingLease = {
|
|
3051
|
+
release: () => {
|
|
3052
|
+
if (pending !== pendingLease || releasing)
|
|
3053
|
+
return;
|
|
3054
|
+
releasing = true;
|
|
3055
|
+
try {
|
|
3056
|
+
rollback();
|
|
3057
|
+
} finally {
|
|
3058
|
+
if (pending === pendingLease)
|
|
3059
|
+
pending = null;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
};
|
|
3063
|
+
pending = pendingLease;
|
|
3064
|
+
try {
|
|
3065
|
+
prefix = reservePrefix(root.ownerDocument, options.idPrefix, root);
|
|
3066
|
+
claimRootOwner(root, reservation);
|
|
3067
|
+
ownsRoot = true;
|
|
3068
|
+
reservePrefixOwnership(root.ownerDocument, prefix, reservation);
|
|
3069
|
+
ownsPrefix = true;
|
|
3070
|
+
ids = idsFor(prefix);
|
|
3071
|
+
mutations.setAttribute("id", ids.root);
|
|
3072
|
+
mutations.setAttribute("data-glow-tour-id-prefix", prefix);
|
|
3073
|
+
registeredRoot = true;
|
|
3074
|
+
driver.registerRoot(root);
|
|
3075
|
+
if (isDisposed())
|
|
3076
|
+
throw new Error("Cannot connect a root to a disposed glow tour");
|
|
3077
|
+
if (pending !== pendingLease)
|
|
3078
|
+
throw new Error("Glow tour root lease was released while connecting");
|
|
3079
|
+
const live = new TourRootBinding(driver, root, ids, reservation, mutations, onMountReleaseStart, onMountReleaseComplete, (released) => {
|
|
3080
|
+
if (binding === released)
|
|
3081
|
+
binding = null;
|
|
3082
|
+
});
|
|
3083
|
+
binding = live;
|
|
3084
|
+
pending = null;
|
|
3085
|
+
return live;
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
try {
|
|
3088
|
+
if (pending === pendingLease)
|
|
3089
|
+
pendingLease.release();
|
|
3090
|
+
else
|
|
3091
|
+
rollback();
|
|
3092
|
+
} catch {}
|
|
3093
|
+
throw error;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
Object.defineProperty(tour, ADAPTER_BRIDGE_SYMBOL, {
|
|
3098
|
+
configurable: false,
|
|
3099
|
+
enumerable: false,
|
|
3100
|
+
value: bridge,
|
|
3101
|
+
writable: false
|
|
3102
|
+
});
|
|
3103
|
+
return {
|
|
3104
|
+
assertCanRun(workflow) {
|
|
3105
|
+
if (!binding)
|
|
3106
|
+
throw new Error("Glow tour requires a connected root before run()");
|
|
3107
|
+
if (workflow.steps.length > 0 && !binding.hasPopover()) {
|
|
3108
|
+
throw new Error("Glow tour requires a connected popover before run()");
|
|
3109
|
+
}
|
|
3110
|
+
return binding.document;
|
|
3111
|
+
},
|
|
3112
|
+
release() {
|
|
3113
|
+
binding?.release();
|
|
3114
|
+
pending?.release();
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
function runCleanup(cleanups) {
|
|
3119
|
+
let failed = false;
|
|
3120
|
+
let failure;
|
|
3121
|
+
for (const cleanup of cleanups) {
|
|
3122
|
+
try {
|
|
3123
|
+
cleanup();
|
|
3124
|
+
} catch (error) {
|
|
3125
|
+
if (!failed) {
|
|
3126
|
+
failed = true;
|
|
3127
|
+
failure = error;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (failed)
|
|
3132
|
+
throw failure;
|
|
3133
|
+
}
|
|
3134
|
+
function claimRootOwner(root, reservation) {
|
|
3135
|
+
Object.defineProperty(root, ROOT_OWNER_SYMBOL, {
|
|
3136
|
+
configurable: true,
|
|
3137
|
+
enumerable: false,
|
|
3138
|
+
value: reservation
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
function releaseRootOwner(root, reservation) {
|
|
3142
|
+
if (Reflect.get(root, ROOT_OWNER_SYMBOL) === reservation) {
|
|
3143
|
+
Reflect.deleteProperty(root, ROOT_OWNER_SYMBOL);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
function idsFor(prefix) {
|
|
3147
|
+
return Object.freeze({
|
|
3148
|
+
description: `${prefix}-description`,
|
|
3149
|
+
popover: `${prefix}-popover`,
|
|
3150
|
+
root: `${prefix}-root`,
|
|
3151
|
+
title: `${prefix}-title`
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
function reservePrefix(document2, requestedPrefix, root) {
|
|
3155
|
+
if (requestedPrefix !== undefined && !ID_PREFIX_PATTERN.test(requestedPrefix)) {
|
|
3156
|
+
throw new Error("Glow tour idPrefix must match [A-Za-z][A-Za-z0-9_-]*");
|
|
3157
|
+
}
|
|
3158
|
+
if (requestedPrefix !== undefined) {
|
|
3159
|
+
if (prefixIsAvailable(document2, requestedPrefix, root))
|
|
3160
|
+
return requestedPrefix;
|
|
3161
|
+
throw new Error(`Glow tour idPrefix is already in use: ${requestedPrefix}`);
|
|
3162
|
+
}
|
|
3163
|
+
for (let suffix = 1;; suffix += 1) {
|
|
3164
|
+
const prefix = suffix === 1 ? DEFAULT_ID_PREFIX : `${DEFAULT_ID_PREFIX}-${suffix}`;
|
|
3165
|
+
if (prefixIsAvailable(document2, prefix, root))
|
|
3166
|
+
return prefix;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
function prefixIsAvailable(document2, prefix, root) {
|
|
3170
|
+
const reservations = prefixReservations(document2);
|
|
3171
|
+
if (reservations.has(prefix))
|
|
3172
|
+
return false;
|
|
3173
|
+
return Object.values(idsFor(prefix)).every((id) => Array.from(document2.querySelectorAll("[id]")).filter((element) => element.id === id).every((element) => root?.contains(element) === true));
|
|
3174
|
+
}
|
|
3175
|
+
function reservePrefixOwnership(document2, prefix, reservation) {
|
|
3176
|
+
prefixReservations(document2).set(prefix, reservation);
|
|
3177
|
+
}
|
|
3178
|
+
function releasePrefix(document2, prefix, reservation) {
|
|
3179
|
+
const reservations = prefixReservations(document2);
|
|
3180
|
+
if (reservations.get(prefix) === reservation)
|
|
3181
|
+
reservations.delete(prefix);
|
|
3182
|
+
}
|
|
3183
|
+
function prefixReservations(document2) {
|
|
3184
|
+
const current = Reflect.get(document2, DOCUMENT_PREFIX_RESERVATIONS_SYMBOL);
|
|
3185
|
+
if (current instanceof Map)
|
|
3186
|
+
return current;
|
|
3187
|
+
const reservations = new Map;
|
|
3188
|
+
Object.defineProperty(document2, DOCUMENT_PREFIX_RESERVATIONS_SYMBOL, {
|
|
3189
|
+
configurable: true,
|
|
3190
|
+
enumerable: false,
|
|
3191
|
+
value: reservations
|
|
3192
|
+
});
|
|
3193
|
+
return reservations;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// packages/core/src/runtime/tour-controller.ts
|
|
3197
|
+
var DEFAULT_TARGET_TIMEOUT = 3000;
|
|
3198
|
+
var DISPOSED_ERROR_MESSAGE = "Tour controller is disposed";
|
|
3199
|
+
function resolveStartIndex(workflow, startAt) {
|
|
3200
|
+
if (startAt === undefined)
|
|
3201
|
+
return 0;
|
|
3202
|
+
const index = workflow.steps.findIndex((step) => step.id === startAt);
|
|
3203
|
+
if (index === -1) {
|
|
3204
|
+
throw new Error(`Workflow "${workflow.name}" has no step with id "${startAt}" to start at.`);
|
|
3205
|
+
}
|
|
3206
|
+
return index;
|
|
3207
|
+
}
|
|
3208
|
+
function normalizedError(error) {
|
|
3209
|
+
if (error instanceof Error)
|
|
3210
|
+
return error;
|
|
3211
|
+
try {
|
|
3212
|
+
return new Error(String(error));
|
|
3213
|
+
} catch {
|
|
3214
|
+
return new Error("Unknown error");
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
class TourController {
|
|
3219
|
+
driver;
|
|
3220
|
+
options;
|
|
3221
|
+
snapshot;
|
|
3222
|
+
workflow = null;
|
|
3223
|
+
steps = [];
|
|
3224
|
+
index = -1;
|
|
3225
|
+
direction = "advance";
|
|
3226
|
+
status = "idle";
|
|
3227
|
+
error = null;
|
|
3228
|
+
operationToken = 0;
|
|
3229
|
+
publicationRevision = 0;
|
|
3230
|
+
operation = null;
|
|
3231
|
+
disposed = false;
|
|
3232
|
+
retainedPresentation = null;
|
|
3233
|
+
stateListeners = new Set;
|
|
3234
|
+
stepPropsSubscriptions = [];
|
|
3235
|
+
tourStartedAt = 0;
|
|
3236
|
+
stepEnteredAt = 0;
|
|
3237
|
+
commandSource = "api";
|
|
3238
|
+
state = Object.freeze({
|
|
3239
|
+
get: () => this.snapshot,
|
|
3240
|
+
subscribe: (listener) => {
|
|
3241
|
+
if (this.disposed)
|
|
3242
|
+
return () => {};
|
|
3243
|
+
this.notifyStateListener(listener, this.snapshot);
|
|
3244
|
+
if (this.disposed)
|
|
3245
|
+
return () => {};
|
|
3246
|
+
this.stateListeners.add(listener);
|
|
3247
|
+
return () => {
|
|
3248
|
+
this.stateListeners.delete(listener);
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
constructor(driver, options = {}) {
|
|
3253
|
+
this.driver = driver;
|
|
3254
|
+
this.options = options;
|
|
3255
|
+
this.snapshot = this.createSnapshot();
|
|
3256
|
+
this.driver.setCommands?.({
|
|
3257
|
+
advance: (source) => this.advance(source),
|
|
3258
|
+
canAdvance: () => this.canNavigate("advance"),
|
|
3259
|
+
canCancel: () => this.status === "active" && this.isCancelAvailable(),
|
|
3260
|
+
canPrevious: () => this.canNavigate("previous"),
|
|
3261
|
+
cancel: (source) => this.cancel(source),
|
|
3262
|
+
isAdvanceDisabled: () => !this.isPresentedAdvanceAvailable(),
|
|
3263
|
+
isCancelDisabled: () => !this.isPresentedCancelAvailable(),
|
|
3264
|
+
isPreviousDisabled: () => !this.isPresentedPreviousAvailable(),
|
|
3265
|
+
previous: (source) => this.previous(source),
|
|
3266
|
+
reportError: async (error) => {
|
|
3267
|
+
if (this.disposed || this.status === "idle")
|
|
3268
|
+
return;
|
|
3269
|
+
const operation = this.beginOperation();
|
|
3270
|
+
try {
|
|
3271
|
+
await this.handleFailure(error, operation);
|
|
3272
|
+
} catch {}
|
|
3273
|
+
},
|
|
3274
|
+
targetDisconnected: async (target) => {
|
|
3275
|
+
await this.recoverDisconnectedTarget(target);
|
|
3276
|
+
},
|
|
3277
|
+
subscribeCapabilities: (listener) => this.state.subscribe((state) => listener(state.status === "active"))
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
create(name, options = {}) {
|
|
3281
|
+
return new WorkflowBuilder(name, options);
|
|
3282
|
+
}
|
|
3283
|
+
async run(workflow, runOptions = {}) {
|
|
3284
|
+
this.assertNotDisposed();
|
|
3285
|
+
validateWorkflowOptions(workflow);
|
|
3286
|
+
const startIndex = resolveStartIndex(workflow, runOptions.startAt);
|
|
3287
|
+
const rootDocument = this.options.assertCanRun?.(workflow) ?? undefined;
|
|
3288
|
+
const retainedPresentation = this.capturePresentation();
|
|
3289
|
+
const operation = this.beginOperation();
|
|
3290
|
+
this.workflow = workflow;
|
|
3291
|
+
this.releaseStepPropsSubscriptions();
|
|
3292
|
+
this.steps = workflow.steps.map((step, index) => new ActiveStep(step, workflow.options, (error) => this.reportSubscriberError(error), `steps[${index}]`, rootDocument));
|
|
3293
|
+
for (const step of this.steps) {
|
|
3294
|
+
this.stepPropsSubscriptions.push(step.props.subscribe(() => {
|
|
3295
|
+
if (!this.disposed && this.currentStep() === step)
|
|
3296
|
+
this.publish();
|
|
3297
|
+
}));
|
|
3298
|
+
}
|
|
3299
|
+
this.index = -1;
|
|
3300
|
+
this.error = null;
|
|
3301
|
+
this.commandSource = "api";
|
|
3302
|
+
this.retainedPresentation = retainedPresentation;
|
|
3303
|
+
try {
|
|
3304
|
+
this.setStatus("starting");
|
|
3305
|
+
this.assertCurrent(operation);
|
|
3306
|
+
const { context: startContext, isAborted: isStartAborted } = this.createLifecycleHookContext(this.steps[startIndex] ?? null);
|
|
3307
|
+
await workflow.options.onStart?.(startContext);
|
|
3308
|
+
this.assertCurrent(operation);
|
|
3309
|
+
if (isStartAborted()) {
|
|
3310
|
+
this.resetToIdle();
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
this.tourStartedAt = Date.now();
|
|
3314
|
+
this.emit("tour:start", this.steps[startIndex] ?? null, 0);
|
|
3315
|
+
if (this.steps.length === 0) {
|
|
3316
|
+
await this.finish(operation);
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
await this.enter(startIndex, "advance", operation);
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
await this.handleFailure(error, operation);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
async advance(source = "api") {
|
|
3325
|
+
this.assertNotDisposed();
|
|
3326
|
+
await this.transitionFromPublic("advance", undefined, source);
|
|
3327
|
+
}
|
|
3328
|
+
async previous(source = "api") {
|
|
3329
|
+
this.assertNotDisposed();
|
|
3330
|
+
await this.transitionFromPublic("previous", undefined, source);
|
|
3331
|
+
}
|
|
3332
|
+
async goToStep(index) {
|
|
3333
|
+
this.assertNotDisposed();
|
|
3334
|
+
if (this.status === "starting" || this.status === "transitioning")
|
|
3335
|
+
return;
|
|
3336
|
+
if (index < 0 || index >= this.steps.length) {
|
|
3337
|
+
throw new Error(`Step index ${index} is out of bounds`);
|
|
3338
|
+
}
|
|
3339
|
+
const direction = index > this.index ? "advance" : "previous";
|
|
3340
|
+
await this.transitionFromPublic(direction, index, "api");
|
|
3341
|
+
}
|
|
3342
|
+
async cancel(source = "api") {
|
|
3343
|
+
this.assertNotDisposed();
|
|
3344
|
+
if (!this.workflow || !this.canCancel())
|
|
3345
|
+
return;
|
|
3346
|
+
this.commandSource = source;
|
|
3347
|
+
const operation = this.beginOperation();
|
|
3348
|
+
try {
|
|
3349
|
+
await this.cancelCurrent(operation);
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
await this.handleFailure(error, operation);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
dispose() {
|
|
3355
|
+
if (this.disposed)
|
|
3356
|
+
return;
|
|
3357
|
+
this.disposed = true;
|
|
3358
|
+
this.invalidateOperation();
|
|
3359
|
+
this.releaseStepPropsSubscriptions();
|
|
3360
|
+
this.steps = [];
|
|
3361
|
+
this.workflow = null;
|
|
3362
|
+
this.index = -1;
|
|
3363
|
+
this.direction = "advance";
|
|
3364
|
+
this.error = null;
|
|
3365
|
+
this.retainedPresentation = null;
|
|
3366
|
+
this.status = "disposed";
|
|
3367
|
+
this.publish(true);
|
|
3368
|
+
this.stateListeners.clear();
|
|
3369
|
+
this.options.onDispose?.();
|
|
3370
|
+
this.driver.dispose();
|
|
3371
|
+
}
|
|
3372
|
+
isDisposed() {
|
|
3373
|
+
return this.disposed;
|
|
3374
|
+
}
|
|
3375
|
+
beginMountRelease() {
|
|
3376
|
+
if (this.disposed)
|
|
3377
|
+
return;
|
|
3378
|
+
this.invalidateOperation();
|
|
3379
|
+
this.workflow = null;
|
|
3380
|
+
this.releaseStepPropsSubscriptions();
|
|
3381
|
+
this.steps = [];
|
|
3382
|
+
this.index = -1;
|
|
3383
|
+
this.direction = "advance";
|
|
3384
|
+
this.error = null;
|
|
3385
|
+
this.retainedPresentation = null;
|
|
3386
|
+
this.status = "idle";
|
|
3387
|
+
}
|
|
3388
|
+
completeMountRelease() {
|
|
3389
|
+
if (!this.disposed)
|
|
3390
|
+
this.publish();
|
|
3391
|
+
}
|
|
3392
|
+
async enter(index, direction, operation) {
|
|
3393
|
+
this.direction = direction;
|
|
3394
|
+
this.emitStepLeave(this.currentStep());
|
|
3395
|
+
this.setStatus("transitioning");
|
|
3396
|
+
this.assertCurrent(operation);
|
|
3397
|
+
const step = this.steps[index];
|
|
3398
|
+
if (!step)
|
|
3399
|
+
throw new Error(`Step index ${index} is out of bounds`);
|
|
3400
|
+
if (step.definition.resetPropsOnEnter !== false)
|
|
3401
|
+
step.reset();
|
|
3402
|
+
const target = await this.resolveTarget(step, operation);
|
|
3403
|
+
this.assertCurrent(operation);
|
|
3404
|
+
if (!target) {
|
|
3405
|
+
await this.advancePastMissingTarget(index, direction, operation);
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
step.target = target;
|
|
3409
|
+
let committed = false;
|
|
3410
|
+
const commitStep = () => {
|
|
3411
|
+
this.assertCurrent(operation);
|
|
3412
|
+
if (committed)
|
|
3413
|
+
return;
|
|
3414
|
+
committed = true;
|
|
3415
|
+
this.index = index;
|
|
3416
|
+
this.retainedPresentation = null;
|
|
3417
|
+
this.publish();
|
|
3418
|
+
};
|
|
3419
|
+
await this.driver.show(step, direction, this.signalFor(operation), commitStep);
|
|
3420
|
+
this.assertCurrent(operation);
|
|
3421
|
+
commitStep();
|
|
3422
|
+
this.setStatus("active");
|
|
3423
|
+
this.assertCurrent(operation);
|
|
3424
|
+
this.stepEnteredAt = Date.now();
|
|
3425
|
+
this.emit("step:enter", step, 0);
|
|
3426
|
+
await this.runActions(operation);
|
|
3427
|
+
}
|
|
3428
|
+
async transitionFromPublic(direction, destination, source = "api") {
|
|
3429
|
+
if (!this.canNavigate(direction, destination))
|
|
3430
|
+
return;
|
|
3431
|
+
this.commandSource = source;
|
|
3432
|
+
const operation = this.beginOperation();
|
|
3433
|
+
try {
|
|
3434
|
+
await this.transition(direction, operation, destination);
|
|
3435
|
+
} catch (error) {
|
|
3436
|
+
await this.handleFailure(error, operation);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
async transition(direction, operation, destination) {
|
|
3440
|
+
if (!this.canNavigate(direction, destination))
|
|
3441
|
+
return;
|
|
3442
|
+
const step = this.currentStep();
|
|
3443
|
+
if (!step)
|
|
3444
|
+
return;
|
|
3445
|
+
this.setStatus("transitioning");
|
|
3446
|
+
this.assertCurrent(operation);
|
|
3447
|
+
const hook = direction === "advance" ? step.definition.advanceAction : step.definition.previousAction;
|
|
3448
|
+
await hook?.(this.createBeforeActionStepContext(step));
|
|
3449
|
+
this.assertCurrent(operation);
|
|
3450
|
+
if (destination !== undefined) {
|
|
3451
|
+
await this.enter(destination, direction, operation);
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
if (direction === "advance") {
|
|
3455
|
+
const nextIndex = this.index + 1;
|
|
3456
|
+
if (nextIndex >= this.steps.length)
|
|
3457
|
+
await this.finish(operation);
|
|
3458
|
+
else
|
|
3459
|
+
await this.enter(nextIndex, direction, operation);
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
if (this.index === 0) {
|
|
3463
|
+
if (this.canCancel())
|
|
3464
|
+
await this.cancelCurrent(operation);
|
|
3465
|
+
else
|
|
3466
|
+
this.setStatus("active");
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
await this.enter(this.index - 1, direction, operation);
|
|
3470
|
+
}
|
|
3471
|
+
async runActions(operation) {
|
|
3472
|
+
const step = this.currentStep();
|
|
3473
|
+
if (!step)
|
|
3474
|
+
return;
|
|
3475
|
+
for (const action of step.definition.actions) {
|
|
3476
|
+
this.assertCurrent(operation);
|
|
3477
|
+
if (typeof action === "number") {
|
|
3478
|
+
await abortableDelay(action, this.signalFor(operation));
|
|
3479
|
+
this.assertCurrent(operation);
|
|
3480
|
+
continue;
|
|
3481
|
+
}
|
|
3482
|
+
let controlInvoked = false;
|
|
3483
|
+
const shouldContinue = await action(this.createStepContext(step, operation, () => {
|
|
3484
|
+
controlInvoked = true;
|
|
3485
|
+
}));
|
|
3486
|
+
this.assertCurrent(operation);
|
|
3487
|
+
if (controlInvoked || shouldContinue === false)
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
async resolveTarget(step, operation) {
|
|
3492
|
+
const signal = this.signalFor(operation);
|
|
3493
|
+
const strategy = step.behavior?.missingTargetStrategy ?? "error";
|
|
3494
|
+
const timeout = step.behavior?.targetTimeout ?? DEFAULT_TARGET_TIMEOUT;
|
|
3495
|
+
const startedAt = Date.now();
|
|
3496
|
+
while (true) {
|
|
3497
|
+
const target = await step.resolveTarget(signal);
|
|
3498
|
+
this.assertCurrent(operation);
|
|
3499
|
+
if (target)
|
|
3500
|
+
return target;
|
|
3501
|
+
if (strategy === "skip")
|
|
3502
|
+
return null;
|
|
3503
|
+
if (strategy !== "wait" || Date.now() - startedAt >= timeout) {
|
|
3504
|
+
throw this.missingTargetError(step);
|
|
3505
|
+
}
|
|
3506
|
+
await abortableDelay(16, signal);
|
|
3507
|
+
this.assertCurrent(operation);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
async recoverDisconnectedTarget(target) {
|
|
3511
|
+
if (this.disposed || this.status !== "active")
|
|
3512
|
+
return;
|
|
3513
|
+
const step = this.currentStep();
|
|
3514
|
+
if (!step || step.target !== target)
|
|
3515
|
+
return;
|
|
3516
|
+
const index = this.index;
|
|
3517
|
+
const direction = this.direction;
|
|
3518
|
+
const operation = this.beginOperation();
|
|
3519
|
+
try {
|
|
3520
|
+
this.setStatus("transitioning");
|
|
3521
|
+
this.assertCurrent(operation);
|
|
3522
|
+
await this.driver.clear(this.signalFor(operation));
|
|
3523
|
+
this.assertCurrent(operation);
|
|
3524
|
+
const recoveredTarget = await this.resolveTarget(step, operation);
|
|
3525
|
+
this.assertCurrent(operation);
|
|
3526
|
+
if (!recoveredTarget) {
|
|
3527
|
+
await this.advancePastRecoveryMissingTarget(step, index, direction, operation);
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
step.target = recoveredTarget;
|
|
3531
|
+
await this.driver.show(step, direction, this.signalFor(operation));
|
|
3532
|
+
this.assertCurrent(operation);
|
|
3533
|
+
this.setStatus("active");
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
try {
|
|
3536
|
+
await this.handleFailure(error, operation);
|
|
3537
|
+
} catch {}
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
async advancePastMissingTarget(index, direction, operation) {
|
|
3541
|
+
const nextIndex = direction === "advance" ? index + 1 : index - 1;
|
|
3542
|
+
if (nextIndex >= this.steps.length)
|
|
3543
|
+
await this.finish(operation);
|
|
3544
|
+
else if (nextIndex < 0) {
|
|
3545
|
+
if (this.canCancel())
|
|
3546
|
+
await this.cancelCurrent(operation);
|
|
3547
|
+
else
|
|
3548
|
+
this.setStatus("active");
|
|
3549
|
+
} else
|
|
3550
|
+
await this.enter(nextIndex, direction, operation);
|
|
3551
|
+
}
|
|
3552
|
+
async advancePastRecoveryMissingTarget(step, index, direction, operation) {
|
|
3553
|
+
const nextIndex = direction === "advance" ? index + 1 : index - 1;
|
|
3554
|
+
if (nextIndex >= this.steps.length)
|
|
3555
|
+
await this.finish(operation);
|
|
3556
|
+
else if (nextIndex < 0) {
|
|
3557
|
+
if (this.canCancel())
|
|
3558
|
+
await this.cancelCurrent(operation);
|
|
3559
|
+
else
|
|
3560
|
+
throw this.missingTargetError(step);
|
|
3561
|
+
} else
|
|
3562
|
+
await this.enter(nextIndex, direction, operation);
|
|
3563
|
+
}
|
|
3564
|
+
missingTargetError(step) {
|
|
3565
|
+
return new Error(`Missing target at ${step.path}: ${String(step.definition.target)}`);
|
|
3566
|
+
}
|
|
3567
|
+
async finish(operation) {
|
|
3568
|
+
this.assertCurrent(operation);
|
|
3569
|
+
const step = this.currentStep();
|
|
3570
|
+
const { context, isAborted } = this.createLifecycleHookContext(step);
|
|
3571
|
+
await this.workflow?.options.onFinish?.(context);
|
|
3572
|
+
this.assertCurrent(operation);
|
|
3573
|
+
if (isAborted()) {
|
|
3574
|
+
if (step)
|
|
3575
|
+
this.setStatus("active");
|
|
3576
|
+
else
|
|
3577
|
+
this.resetToIdle();
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
this.emitStepLeave(step);
|
|
3581
|
+
await this.driver.clear(this.signalFor(operation));
|
|
3582
|
+
this.assertCurrent(operation);
|
|
3583
|
+
this.retainedPresentation = null;
|
|
3584
|
+
this.setStatus("finished");
|
|
3585
|
+
this.emit("tour:complete", step, Date.now() - this.tourStartedAt);
|
|
3586
|
+
this.assertCurrent(operation);
|
|
3587
|
+
}
|
|
3588
|
+
async cancelCurrent(operation) {
|
|
3589
|
+
const step = this.currentStep();
|
|
3590
|
+
if (step?.definition.cancelAction) {
|
|
3591
|
+
await step.definition.cancelAction(this.createBeforeActionStepContext(step));
|
|
3592
|
+
}
|
|
3593
|
+
this.assertCurrent(operation);
|
|
3594
|
+
const { context, isAborted } = this.createLifecycleHookContext(step);
|
|
3595
|
+
await this.workflow?.options.onCancel?.(context);
|
|
3596
|
+
this.assertCurrent(operation);
|
|
3597
|
+
if (isAborted()) {
|
|
3598
|
+
this.setStatus("active");
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
this.emitStepLeave(step);
|
|
3602
|
+
await this.driver.clear(this.signalFor(operation));
|
|
3603
|
+
this.assertCurrent(operation);
|
|
3604
|
+
this.retainedPresentation = null;
|
|
3605
|
+
this.setStatus("cancelled");
|
|
3606
|
+
this.emit("tour:cancel", step, Date.now() - this.tourStartedAt);
|
|
3607
|
+
this.assertCurrent(operation);
|
|
3608
|
+
}
|
|
3609
|
+
createLifecycleHookContext(step) {
|
|
3610
|
+
let aborted = false;
|
|
3611
|
+
const context = Object.freeze({
|
|
3612
|
+
step: step?.snapshot() ?? null,
|
|
3613
|
+
abort: () => {
|
|
3614
|
+
aborted = true;
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
return { context, isAborted: () => aborted };
|
|
3618
|
+
}
|
|
3619
|
+
resetToIdle() {
|
|
3620
|
+
this.workflow = null;
|
|
3621
|
+
this.releaseStepPropsSubscriptions();
|
|
3622
|
+
this.steps = [];
|
|
3623
|
+
this.index = -1;
|
|
3624
|
+
this.retainedPresentation = null;
|
|
3625
|
+
this.setStatus("idle");
|
|
3626
|
+
}
|
|
3627
|
+
async handleFailure(reason, operation) {
|
|
3628
|
+
if (!this.isCurrent(operation))
|
|
3629
|
+
return;
|
|
3630
|
+
const error = normalizedError(reason);
|
|
3631
|
+
this.error = error;
|
|
3632
|
+
this.retainedPresentation = null;
|
|
3633
|
+
this.setStatus("error");
|
|
3634
|
+
this.emit("tour:error", this.currentStep(), Date.now() - this.tourStartedAt, error);
|
|
3635
|
+
if (!this.isCurrent(operation))
|
|
3636
|
+
throw error;
|
|
3637
|
+
try {
|
|
3638
|
+
await this.driver.clear(this.signalFor(operation));
|
|
3639
|
+
} catch {}
|
|
3640
|
+
throw error;
|
|
3641
|
+
}
|
|
3642
|
+
beginOperation() {
|
|
3643
|
+
this.invalidateOperation();
|
|
3644
|
+
this.operation = new AbortController;
|
|
3645
|
+
return this.operationToken;
|
|
3646
|
+
}
|
|
3647
|
+
createStepContext(step, operation, onControl) {
|
|
3648
|
+
if (!step.target)
|
|
3649
|
+
throw new Error("Cannot create a step context without a target");
|
|
3650
|
+
return Object.freeze({
|
|
3651
|
+
advance: async () => {
|
|
3652
|
+
onControl?.();
|
|
3653
|
+
this.assertCurrent(operation);
|
|
3654
|
+
await this.transition("advance", operation);
|
|
3655
|
+
},
|
|
3656
|
+
cancel: async () => {
|
|
3657
|
+
onControl?.();
|
|
3658
|
+
this.assertCurrent(operation);
|
|
3659
|
+
if (this.canCancel())
|
|
3660
|
+
await this.cancelCurrent(operation);
|
|
3661
|
+
},
|
|
3662
|
+
previous: async () => {
|
|
3663
|
+
onControl?.();
|
|
3664
|
+
this.assertCurrent(operation);
|
|
3665
|
+
await this.transition("previous", operation);
|
|
3666
|
+
},
|
|
3667
|
+
props: step.props,
|
|
3668
|
+
signal: this.signalFor(operation),
|
|
3669
|
+
target: step.target
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
createBeforeActionStepContext(step) {
|
|
3673
|
+
if (!step.target)
|
|
3674
|
+
throw new Error("Cannot create a step context without a target");
|
|
3675
|
+
return Object.freeze({
|
|
3676
|
+
...step.props.get(),
|
|
3677
|
+
target: step.target
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
invalidateOperation() {
|
|
3681
|
+
this.operationToken += 1;
|
|
3682
|
+
this.operation?.abort();
|
|
3683
|
+
this.operation = null;
|
|
3684
|
+
}
|
|
3685
|
+
signalFor(operation) {
|
|
3686
|
+
this.assertCurrent(operation);
|
|
3687
|
+
const signal = this.operation?.signal;
|
|
3688
|
+
if (!signal)
|
|
3689
|
+
throw abortError();
|
|
3690
|
+
return signal;
|
|
3691
|
+
}
|
|
3692
|
+
assertCurrent(operation) {
|
|
3693
|
+
if (!this.isCurrent(operation))
|
|
3694
|
+
throw abortError();
|
|
3695
|
+
}
|
|
3696
|
+
isCurrent(operation) {
|
|
3697
|
+
return !this.disposed && operation === this.operationToken && this.operation?.signal.aborted === false;
|
|
3698
|
+
}
|
|
3699
|
+
assertNotDisposed() {
|
|
3700
|
+
if (this.disposed)
|
|
3701
|
+
throw new Error(DISPOSED_ERROR_MESSAGE);
|
|
3702
|
+
}
|
|
3703
|
+
currentStep() {
|
|
3704
|
+
return this.index >= 0 ? this.steps[this.index] ?? null : null;
|
|
3705
|
+
}
|
|
3706
|
+
releaseStepPropsSubscriptions() {
|
|
3707
|
+
for (const unsubscribe of this.stepPropsSubscriptions.splice(0))
|
|
3708
|
+
unsubscribe();
|
|
3709
|
+
}
|
|
3710
|
+
canNavigate(direction, destination) {
|
|
3711
|
+
if (this.status !== "active" || !this.currentStep())
|
|
3712
|
+
return false;
|
|
3713
|
+
if (destination !== undefined) {
|
|
3714
|
+
return destination >= 0 && destination < this.steps.length && destination !== this.index;
|
|
3715
|
+
}
|
|
3716
|
+
return direction === "advance" || this.index > 0;
|
|
3717
|
+
}
|
|
3718
|
+
isAdvanceButtonAvailable() {
|
|
3719
|
+
const props = this.currentStep()?.props.get();
|
|
3720
|
+
return props !== undefined && props.popover?.disableAdvanceButton !== true;
|
|
3721
|
+
}
|
|
3722
|
+
isPreviousButtonAvailable() {
|
|
3723
|
+
const props = this.currentStep()?.props.get();
|
|
3724
|
+
return props !== undefined && props.popover?.disablePreviousButton !== true && this.index > 0;
|
|
3725
|
+
}
|
|
3726
|
+
isCancelAvailable() {
|
|
3727
|
+
return this.currentStep() !== null && this.canCancel();
|
|
3728
|
+
}
|
|
3729
|
+
isPresentedAdvanceAvailable() {
|
|
3730
|
+
return this.currentStep() ? this.isAdvanceButtonAvailable() : this.retainedPresentation?.advanceButtonAvailable ?? false;
|
|
3731
|
+
}
|
|
3732
|
+
isPresentedPreviousAvailable() {
|
|
3733
|
+
return this.currentStep() ? this.isPreviousButtonAvailable() : this.retainedPresentation?.previousButtonAvailable ?? false;
|
|
3734
|
+
}
|
|
3735
|
+
isPresentedCancelAvailable() {
|
|
3736
|
+
return this.currentStep() ? this.isCancelAvailable() : this.retainedPresentation?.canCancel ?? false;
|
|
3737
|
+
}
|
|
3738
|
+
canCancel() {
|
|
3739
|
+
return (this.workflow?.options.cancellable ?? true) && this.status !== "finished" && this.status !== "cancelled" && this.status !== "error";
|
|
3740
|
+
}
|
|
3741
|
+
setStatus(status) {
|
|
3742
|
+
this.status = status;
|
|
3743
|
+
this.publish();
|
|
3744
|
+
}
|
|
3745
|
+
publish(allowDisposed = false) {
|
|
3746
|
+
const revision = ++this.publicationRevision;
|
|
3747
|
+
const state = this.createSnapshot();
|
|
3748
|
+
this.snapshot = state;
|
|
3749
|
+
for (const listener of Array.from(this.stateListeners)) {
|
|
3750
|
+
if (!allowDisposed && this.disposed || revision !== this.publicationRevision)
|
|
3751
|
+
break;
|
|
3752
|
+
this.notifyStateListener(listener, state);
|
|
3753
|
+
if (!allowDisposed && this.disposed || revision !== this.publicationRevision)
|
|
3754
|
+
break;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
notifyStateListener(listener, state) {
|
|
3758
|
+
try {
|
|
3759
|
+
listener(state);
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
this.reportSubscriberError(error);
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
hasEventListeners() {
|
|
3765
|
+
return Boolean(this.options.onEvent ?? this.workflow?.options.onEvent);
|
|
3766
|
+
}
|
|
3767
|
+
emit(type, step, durationMs, error = null) {
|
|
3768
|
+
const instanceListener = this.options.onEvent;
|
|
3769
|
+
const workflowListener = this.workflow?.options.onEvent;
|
|
3770
|
+
if (!instanceListener && !workflowListener)
|
|
3771
|
+
return;
|
|
3772
|
+
const index = step ? this.steps.indexOf(step) : -1;
|
|
3773
|
+
const event = Object.freeze({
|
|
3774
|
+
direction: this.direction,
|
|
3775
|
+
durationMs,
|
|
3776
|
+
error,
|
|
3777
|
+
source: this.commandSource,
|
|
3778
|
+
stepCount: this.steps.length,
|
|
3779
|
+
stepId: step?.definition.id ?? null,
|
|
3780
|
+
stepIndex: index,
|
|
3781
|
+
timestamp: Date.now(),
|
|
3782
|
+
type,
|
|
3783
|
+
workflowName: this.workflow?.name ?? ""
|
|
3784
|
+
});
|
|
3785
|
+
this.notifyEventListener(instanceListener, event);
|
|
3786
|
+
this.notifyEventListener(workflowListener, event);
|
|
3787
|
+
}
|
|
3788
|
+
notifyEventListener(listener, event) {
|
|
3789
|
+
if (!listener)
|
|
3790
|
+
return;
|
|
3791
|
+
try {
|
|
3792
|
+
listener(event);
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
this.reportSubscriberError(error);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
emitStepLeave(step) {
|
|
3798
|
+
if (!step || !this.hasEventListeners())
|
|
3799
|
+
return;
|
|
3800
|
+
this.emit("step:leave", step, Date.now() - this.stepEnteredAt);
|
|
3801
|
+
}
|
|
3802
|
+
reportSubscriberError(reason) {
|
|
3803
|
+
const error = normalizedError(reason);
|
|
3804
|
+
const onSubscriberError = this.options.onSubscriberError;
|
|
3805
|
+
if (!onSubscriberError) {
|
|
3806
|
+
this.reportUnhandledError(error);
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
try {
|
|
3810
|
+
Promise.resolve(onSubscriberError(error)).catch((hookError) => {
|
|
3811
|
+
this.reportUnhandledError(normalizedError(hookError));
|
|
3812
|
+
});
|
|
3813
|
+
} catch (hookError) {
|
|
3814
|
+
this.reportUnhandledError(normalizedError(hookError));
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
reportUnhandledError(error) {
|
|
3818
|
+
const reporter = this.options.reportUnhandledError ?? ((reason) => {
|
|
3819
|
+
throw reason;
|
|
3820
|
+
});
|
|
3821
|
+
queueMicrotask(() => reporter(error));
|
|
3822
|
+
}
|
|
3823
|
+
createSnapshot() {
|
|
3824
|
+
const currentStep = this.currentStep();
|
|
3825
|
+
const retained = currentStep ? null : this.retainedPresentation;
|
|
3826
|
+
const currentStepIndex = retained?.currentStepIndex ?? this.index;
|
|
3827
|
+
const totalSteps = retained?.totalSteps ?? this.steps.length;
|
|
3828
|
+
const isFirstStep = retained?.isFirstStep ?? currentStepIndex === 0;
|
|
3829
|
+
const isLastStep = retained?.isLastStep ?? (currentStepIndex === totalSteps - 1 && currentStepIndex >= 0);
|
|
3830
|
+
return Object.freeze({
|
|
3831
|
+
name: this.workflow?.name ?? "",
|
|
3832
|
+
totalSteps,
|
|
3833
|
+
currentStepIndex,
|
|
3834
|
+
currentStep: currentStep?.snapshot() ?? retained?.currentStep ?? null,
|
|
3835
|
+
direction: this.direction,
|
|
3836
|
+
canAdvance: this.canNavigate("advance"),
|
|
3837
|
+
canPrevious: this.canNavigate("previous"),
|
|
3838
|
+
canCancel: this.isPresentedCancelAvailable(),
|
|
3839
|
+
isFirstStep,
|
|
3840
|
+
isLastStep,
|
|
3841
|
+
status: this.status,
|
|
3842
|
+
error: this.error
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
capturePresentation() {
|
|
3846
|
+
if (this.retainedPresentation)
|
|
3847
|
+
return this.retainedPresentation;
|
|
3848
|
+
if (this.status !== "active" && this.status !== "transitioning")
|
|
3849
|
+
return null;
|
|
3850
|
+
const state = this.createSnapshot();
|
|
3851
|
+
if (!state.currentStep)
|
|
3852
|
+
return null;
|
|
3853
|
+
return {
|
|
3854
|
+
advanceButtonAvailable: this.isAdvanceButtonAvailable(),
|
|
3855
|
+
canCancel: state.canCancel,
|
|
3856
|
+
currentStep: state.currentStep,
|
|
3857
|
+
currentStepIndex: state.currentStepIndex,
|
|
3858
|
+
isFirstStep: state.isFirstStep,
|
|
3859
|
+
isLastStep: state.isLastStep,
|
|
3860
|
+
previousButtonAvailable: this.isPreviousButtonAvailable(),
|
|
3861
|
+
totalSteps: state.totalSteps
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
function createGlowTour(options = {}) {
|
|
3866
|
+
const driver = new DomTourViewDriver;
|
|
3867
|
+
let bridge;
|
|
3868
|
+
const controller = new TourController(driver, {
|
|
3869
|
+
assertCanRun: (workflow) => bridge.assertCanRun(workflow),
|
|
3870
|
+
onDispose: () => bridge.release(),
|
|
3871
|
+
onEvent: options.onEvent,
|
|
3872
|
+
onSubscriberError: options.onSubscriberError
|
|
3873
|
+
});
|
|
3874
|
+
const tour = {
|
|
3875
|
+
advance: () => controller.advance(),
|
|
3876
|
+
cancel: () => controller.cancel(),
|
|
3877
|
+
create: (name, options2) => controller.create(name, options2),
|
|
3878
|
+
dispose: () => controller.dispose(),
|
|
3879
|
+
goToStep: (index) => controller.goToStep(index),
|
|
3880
|
+
previous: () => controller.previous(),
|
|
3881
|
+
run: (workflow, runOptions) => controller.run(workflow, runOptions),
|
|
3882
|
+
state: controller.state
|
|
3883
|
+
};
|
|
3884
|
+
bridge = attachRootBridge(tour, driver, () => controller.isDisposed(), () => controller.beginMountRelease(), () => controller.completeMountRelease());
|
|
3885
|
+
return Object.freeze(tour);
|
|
3886
|
+
}
|
|
3887
|
+
export {
|
|
3888
|
+
createGlowTour
|
|
3889
|
+
};
|