vanilla-jui 1.4.8 → 1.5.1
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/README.md +10 -4
- package/dist/index.d.ts +402 -977
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/components/accordion.ts +467 -349
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +199 -326
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +281 -361
- package/src/components/pagination.ts +207 -374
- package/src/components/swiper.ts +688 -1168
- package/src/components/tabs.ts +468 -788
- package/src/components/toc.ts +191 -244
- package/src/core/component.ts +332 -0
- package/src/core/motion.ts +298 -0
- package/src/core/presence.ts +233 -0
- package/src/core/scheduler.ts +91 -0
- package/src/core/view.ts +31 -0
- package/src/css/accordion.css +94 -3
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +5 -17
- package/src/css/swiper.css +2 -1
- package/src/css/tabs.css +8 -17
- package/src/css/themes/default/accordion.css +0 -48
- package/src/css/themes/default/swiper.css +1 -1
- package/src/css/themes/default/tabs.css +4 -6
- package/src/css/toast.css +1 -17
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -3
- package/src/locales/index.ts +3 -0
- package/src/primitives/drop.ts +253 -262
- package/src/primitives/loading.ts +22 -4
- package/src/primitives/parabola.ts +123 -164
- package/src/primitives/sticky.ts +297 -0
- package/src/primitives/theme.ts +164 -222
- package/src/primitives/toast.ts +309 -284
- package/src/primitives/tooltip.ts +65 -66
- package/src/utilities/browser.ts +38 -0
- package/src/utilities/dom.ts +0 -54
- package/src/utilities/refs.ts +61 -0
- package/src/utilities/state.ts +73 -0
- package/src/utilities/timer.ts +11 -31
- package/src/validation/validator.ts +289 -389
- package/src/components/sticky.ts +0 -297
- package/src/core/Component.ts +0 -377
- package/src/utilities/scheduler.ts +0 -156
package/src/components/flow.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createDeepStore,
|
|
3
|
-
createRoot,
|
|
4
|
-
flushSync,
|
|
5
|
-
jsx,
|
|
6
|
-
onCleanup,
|
|
7
|
-
render,
|
|
8
|
-
} from 'vanilla-signal';
|
|
1
|
+
import { createDeepStore, flushSync, jsx } from 'vanilla-signal';
|
|
9
2
|
|
|
3
|
+
import {
|
|
4
|
+
type FunctionalComponent,
|
|
5
|
+
defineComponent,
|
|
6
|
+
} from '../core/component.ts';
|
|
10
7
|
import { type RenderableContent } from '../utilities/dom.ts';
|
|
11
8
|
import { randomId } from '../utilities/id.ts';
|
|
12
9
|
import { isPlainObject } from '../utilities/object.ts';
|
|
@@ -23,12 +20,8 @@ export type FlowBusyStrategy = 'ignore' | 'throw';
|
|
|
23
20
|
export type FlowDirection = string;
|
|
24
21
|
export type FlowSlotName = 'renderHeader' | 'renderBody' | 'renderFooter';
|
|
25
22
|
export type FlowCleanup = () => void;
|
|
26
|
-
export type
|
|
27
|
-
|
|
28
|
-
| {
|
|
29
|
-
id: string;
|
|
30
|
-
data?: FlowPayload;
|
|
31
|
-
};
|
|
23
|
+
export type FlowTarget = string | number;
|
|
24
|
+
export type FlowStepResult = string | { id: string; data?: FlowPayload };
|
|
32
25
|
|
|
33
26
|
export interface FlowClassNames {
|
|
34
27
|
root: string;
|
|
@@ -90,6 +83,27 @@ export interface FlowSnapshot {
|
|
|
90
83
|
error: unknown;
|
|
91
84
|
}
|
|
92
85
|
|
|
86
|
+
export interface FlowState extends Record<string, unknown> {
|
|
87
|
+
id: string;
|
|
88
|
+
currentId: string;
|
|
89
|
+
currentIndex: number;
|
|
90
|
+
previousId: string | null;
|
|
91
|
+
previousIndex: number | null;
|
|
92
|
+
direction: FlowDirection | null;
|
|
93
|
+
history: string[];
|
|
94
|
+
data: FlowData;
|
|
95
|
+
stepData: Record<string, FlowData>;
|
|
96
|
+
loading: boolean;
|
|
97
|
+
error: unknown;
|
|
98
|
+
busyAction: FlowAction | null;
|
|
99
|
+
version: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FlowGoToOptions {
|
|
103
|
+
direction?: FlowDirection;
|
|
104
|
+
internal?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
export interface FlowContext {
|
|
94
108
|
flow: Flow;
|
|
95
109
|
step: FlowStep;
|
|
@@ -174,7 +188,6 @@ export type FlowSubscriber = (
|
|
|
174
188
|
flow: Flow,
|
|
175
189
|
previous: FlowSnapshot | null
|
|
176
190
|
) => void;
|
|
177
|
-
export type FlowTarget = string | number;
|
|
178
191
|
|
|
179
192
|
export interface FlowText {
|
|
180
193
|
back: string;
|
|
@@ -234,62 +247,50 @@ interface ResolvedFlowProps extends Record<string, unknown> {
|
|
|
234
247
|
onBusy: FlowBusyHook | null;
|
|
235
248
|
}
|
|
236
249
|
|
|
237
|
-
export interface FlowState extends Record<string, unknown> {
|
|
238
|
-
id: string;
|
|
239
|
-
currentId: string;
|
|
240
|
-
currentIndex: number;
|
|
241
|
-
previousId: string | null;
|
|
242
|
-
previousIndex: number | null;
|
|
243
|
-
direction: FlowDirection | null;
|
|
244
|
-
history: string[];
|
|
245
|
-
data: FlowData;
|
|
246
|
-
stepData: Record<string, FlowData>;
|
|
247
|
-
loading: boolean;
|
|
248
|
-
error: unknown;
|
|
249
|
-
busyAction: FlowAction | null;
|
|
250
|
-
version: number;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
interface InitialFlowData {
|
|
254
|
-
stepData: Record<string, FlowData>;
|
|
255
|
-
global: FlowData;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
interface FlowDOM {
|
|
259
|
-
root: HTMLElement | null;
|
|
260
|
-
header: HTMLElement | null;
|
|
261
|
-
body: HTMLElement | null;
|
|
262
|
-
footer: HTMLElement | null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
250
|
interface FlowRuntime {
|
|
266
251
|
built: boolean;
|
|
252
|
+
mounted: boolean;
|
|
267
253
|
destroyed: boolean;
|
|
268
254
|
activeAction: FlowAction | null;
|
|
269
255
|
actionController: AbortController | null;
|
|
270
256
|
}
|
|
271
257
|
|
|
272
|
-
interface
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
258
|
+
interface FlowActions {
|
|
259
|
+
subscribe(handler: FlowSubscriber): FlowCleanup;
|
|
260
|
+
snapshot(): FlowSnapshot;
|
|
261
|
+
next(payload?: FlowPayload): Promise<FlowSnapshot | null>;
|
|
262
|
+
back(payload?: FlowPayload): Promise<FlowSnapshot | null>;
|
|
263
|
+
goTo(
|
|
264
|
+
target: FlowTarget,
|
|
265
|
+
payload?: FlowPayload,
|
|
266
|
+
options?: FlowGoToOptions
|
|
267
|
+
): Promise<FlowSnapshot | null>;
|
|
268
|
+
setData(data: FlowPayload): Flow;
|
|
269
|
+
setStepData(
|
|
270
|
+
stepId: string,
|
|
271
|
+
data: FlowPayload,
|
|
272
|
+
options?: { silent?: boolean }
|
|
273
|
+
): Flow;
|
|
274
|
+
getStepData(stepId: string): FlowData;
|
|
275
|
+
reset(): Flow;
|
|
276
|
+
finish(payload?: FlowPayload): Promise<FlowSnapshot | null>;
|
|
285
277
|
}
|
|
286
278
|
|
|
287
|
-
interface
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
279
|
+
export interface Flow extends FlowActions {
|
|
280
|
+
readonly props: ResolvedFlowProps;
|
|
281
|
+
readonly steps: FlowStep[];
|
|
282
|
+
readonly state: FlowState;
|
|
283
|
+
readonly runtime: FlowRuntime;
|
|
284
|
+
readonly element: HTMLElement | null;
|
|
285
|
+
readonly currentStep: FlowStep;
|
|
286
|
+
readonly currentData: FlowData;
|
|
287
|
+
readonly canBack: boolean;
|
|
288
|
+
readonly canNext: boolean;
|
|
289
|
+
readonly isLast: boolean;
|
|
290
|
+
build(): this;
|
|
291
|
+
mount(container: Element | DocumentFragment): this;
|
|
292
|
+
unmount(): this;
|
|
293
|
+
destroy(): void;
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
interface CapturedFlowState {
|
|
@@ -329,25 +330,23 @@ const DEFAULT_CLASS_NAMES: FlowClassNames = {
|
|
|
329
330
|
next: 'is-primary flow-next',
|
|
330
331
|
};
|
|
331
332
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
333
|
+
const FLOW_STEP_RULE = {
|
|
334
|
+
type: 'plainObject',
|
|
335
|
+
shape: { id: { type: 'string', nonEmpty: true } },
|
|
336
|
+
};
|
|
337
|
+
const FLOW_CONTENT_RULE = { type: 'renderable' };
|
|
338
|
+
const FLOW_PAYLOAD_RULE = { types: ['plainObject', 'null', 'undefined'] };
|
|
335
339
|
|
|
336
340
|
function clonePlainObject(value: unknown): FlowData {
|
|
337
341
|
return isPlainObject(value) ? { ...(value as FlowData) } : {};
|
|
338
342
|
}
|
|
339
343
|
|
|
340
|
-
function cloneArray<T>(value: unknown): T[] {
|
|
341
|
-
return Array.isArray(value) ? [...value] : [];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
344
|
function cloneSteps(steps: unknown): FlowStep[] {
|
|
345
345
|
if (!Array.isArray(steps)) return [];
|
|
346
346
|
return steps.map((step) => {
|
|
347
347
|
const source = { ...(step as FlowStep & Record<string, unknown>) };
|
|
348
348
|
delete source.description;
|
|
349
349
|
delete source.view;
|
|
350
|
-
|
|
351
350
|
return {
|
|
352
351
|
...source,
|
|
353
352
|
data: clonePlainObject(source.data),
|
|
@@ -359,107 +358,44 @@ function cloneSteps(steps: unknown): FlowStep[] {
|
|
|
359
358
|
});
|
|
360
359
|
}
|
|
361
360
|
|
|
362
|
-
function normalizeStepResult(
|
|
363
|
-
result: FlowStepResult | FlowPayload | void,
|
|
364
|
-
fallbackId: string
|
|
365
|
-
): { id: string; data?: FlowPayload } {
|
|
366
|
-
if (typeof result === 'string') return { id: result };
|
|
367
|
-
if (isPlainObject(result)) {
|
|
368
|
-
const source = result as { id?: unknown; data?: unknown };
|
|
369
|
-
if (typeof source.id !== 'string') return { id: fallbackId };
|
|
370
|
-
const data: FlowPayload | undefined = isPlainObject(source.data)
|
|
371
|
-
? (source.data as FlowData)
|
|
372
|
-
: source.data == null
|
|
373
|
-
? source.data
|
|
374
|
-
: undefined;
|
|
375
|
-
return {
|
|
376
|
-
id: source.id,
|
|
377
|
-
data,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
return { id: fallbackId };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
361
|
function normalizeClassNames(value: unknown): FlowClassNames {
|
|
384
362
|
if (typeof value === 'string') {
|
|
385
|
-
const extra = value.trim();
|
|
386
363
|
return {
|
|
387
364
|
...DEFAULT_CLASS_NAMES,
|
|
388
|
-
root:
|
|
389
|
-
? `${DEFAULT_CLASS_NAMES.root} ${extra}`
|
|
390
|
-
: DEFAULT_CLASS_NAMES.root,
|
|
365
|
+
root: [DEFAULT_CLASS_NAMES.root, value.trim()].filter(Boolean).join(' '),
|
|
391
366
|
};
|
|
392
367
|
}
|
|
393
|
-
|
|
394
368
|
return {
|
|
395
369
|
...DEFAULT_CLASS_NAMES,
|
|
396
|
-
...(isPlainObject(value) ? (value as
|
|
370
|
+
...(isPlainObject(value) ? (value as FlowClassNameConfig) : {}),
|
|
397
371
|
};
|
|
398
372
|
}
|
|
399
373
|
|
|
400
|
-
function
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
374
|
+
function normalizeStepResult(
|
|
375
|
+
result: FlowStepResult | FlowPayload | void,
|
|
376
|
+
fallbackId: string
|
|
377
|
+
): { id: string; data?: FlowPayload } {
|
|
378
|
+
if (typeof result === 'string') return { id: result };
|
|
379
|
+
if (!isPlainObject(result)) return { id: fallbackId };
|
|
380
|
+
const source = result as { id?: unknown; data?: unknown };
|
|
381
|
+
if (typeof source.id !== 'string') return { id: fallbackId };
|
|
382
|
+
return {
|
|
383
|
+
id: source.id,
|
|
384
|
+
data:
|
|
385
|
+
isPlainObject(source.data) || source.data == null
|
|
386
|
+
? (source.data as FlowPayload)
|
|
387
|
+
: undefined,
|
|
388
|
+
};
|
|
404
389
|
}
|
|
405
390
|
|
|
406
|
-
const FLOW_STEP_RULE = {
|
|
407
|
-
type: 'plainObject',
|
|
408
|
-
shape: {
|
|
409
|
-
id: { type: 'string', nonEmpty: true },
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
const FLOW_CONTENT_RULE = {
|
|
414
|
-
type: 'renderable',
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const FLOW_STEPS_RULE = {
|
|
418
|
-
type: 'array',
|
|
419
|
-
nonEmpty: true,
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const FLOW_PAYLOAD_RULE = {
|
|
423
|
-
types: ['plainObject', 'null', 'undefined'],
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
const FLOW_RENDER_SLOT_RULE = {
|
|
427
|
-
validate: isFlowRenderSlot,
|
|
428
|
-
message: 'expects function, false or null.',
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const FLOW_TEXT_RULE = {
|
|
432
|
-
default: {},
|
|
433
|
-
type: 'plainObject',
|
|
434
|
-
normalize: (value: unknown): FlowText => {
|
|
435
|
-
const text = (isPlainObject(value) ? value : {}) as Partial<FlowText> &
|
|
436
|
-
Record<string, unknown>;
|
|
437
|
-
return {
|
|
438
|
-
...Object.fromEntries(
|
|
439
|
-
Object.entries(text).filter(([, item]) => typeof item === 'string')
|
|
440
|
-
),
|
|
441
|
-
back: typeof text.back === 'string' ? text.back : 'Back',
|
|
442
|
-
next: typeof text.next === 'string' ? text.next : 'Next',
|
|
443
|
-
finish: typeof text.finish === 'string' ? text.finish : 'Finish',
|
|
444
|
-
reset: typeof text.reset === 'string' ? text.reset : 'Reset',
|
|
445
|
-
};
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
|
|
449
391
|
const FLOW_PROPS_SCHEMA = {
|
|
450
392
|
id: {
|
|
451
393
|
default: null,
|
|
452
394
|
types: ['string', 'null'],
|
|
453
|
-
normalize: (value: unknown) =>
|
|
454
|
-
|
|
455
|
-
const id = value.trim();
|
|
456
|
-
return id || randomId();
|
|
457
|
-
}
|
|
458
|
-
if (value == null) return randomId();
|
|
459
|
-
return value;
|
|
460
|
-
},
|
|
395
|
+
normalize: (value: unknown) =>
|
|
396
|
+
typeof value === 'string' ? value.trim() || randomId() : randomId(),
|
|
461
397
|
},
|
|
462
|
-
steps: { default: [],
|
|
398
|
+
steps: { default: [], type: 'array', nonEmpty: true },
|
|
463
399
|
initial: { default: null, types: ['string', 'number', 'null'] },
|
|
464
400
|
cache: { default: true, type: 'boolean' },
|
|
465
401
|
linear: { default: true, type: 'boolean' },
|
|
@@ -473,15 +409,39 @@ const FLOW_PROPS_SCHEMA = {
|
|
|
473
409
|
showBack: { default: true, type: 'boolean' },
|
|
474
410
|
showNext: { default: true, type: 'boolean' },
|
|
475
411
|
showReset: { default: false, type: 'boolean' },
|
|
476
|
-
text:
|
|
412
|
+
text: {
|
|
413
|
+
default: {},
|
|
414
|
+
type: 'plainObject',
|
|
415
|
+
normalize: (value: unknown) => {
|
|
416
|
+
const text = isPlainObject(value) ? (value as Partial<FlowText>) : {};
|
|
417
|
+
return {
|
|
418
|
+
back: typeof text.back === 'string' ? text.back : 'Back',
|
|
419
|
+
next: typeof text.next === 'string' ? text.next : 'Next',
|
|
420
|
+
finish: typeof text.finish === 'string' ? text.finish : 'Finish',
|
|
421
|
+
reset: typeof text.reset === 'string' ? text.reset : 'Reset',
|
|
422
|
+
};
|
|
423
|
+
},
|
|
424
|
+
},
|
|
477
425
|
className: {
|
|
478
426
|
default: DEFAULT_CLASS_NAMES,
|
|
479
427
|
types: ['object', 'string'],
|
|
480
428
|
normalize: normalizeClassNames,
|
|
481
429
|
},
|
|
482
|
-
renderHeader: {
|
|
483
|
-
|
|
484
|
-
|
|
430
|
+
renderHeader: {
|
|
431
|
+
default: null,
|
|
432
|
+
validate: (value: unknown) =>
|
|
433
|
+
value == null || value === false || typeof value === 'function',
|
|
434
|
+
},
|
|
435
|
+
renderBody: {
|
|
436
|
+
default: null,
|
|
437
|
+
validate: (value: unknown) =>
|
|
438
|
+
value == null || value === false || typeof value === 'function',
|
|
439
|
+
},
|
|
440
|
+
renderFooter: {
|
|
441
|
+
default: null,
|
|
442
|
+
validate: (value: unknown) =>
|
|
443
|
+
value == null || value === false || typeof value === 'function',
|
|
444
|
+
},
|
|
485
445
|
onChange: { default: null, types: ['function', 'null'] },
|
|
486
446
|
onNext: { default: null, types: ['function', 'null'] },
|
|
487
447
|
onBack: { default: null, types: ['function', 'null'] },
|
|
@@ -495,7 +455,7 @@ function normalizeProps(input: FlowProps): ResolvedFlowProps {
|
|
|
495
455
|
return {
|
|
496
456
|
id: props.id as string,
|
|
497
457
|
steps: cloneSteps(props.steps),
|
|
498
|
-
initial: props.initial as
|
|
458
|
+
initial: props.initial as string | number | null,
|
|
499
459
|
cache: props.cache as boolean,
|
|
500
460
|
linear: props.linear as boolean,
|
|
501
461
|
render: props.render as boolean,
|
|
@@ -518,1026 +478,712 @@ function normalizeProps(input: FlowProps): ResolvedFlowProps {
|
|
|
518
478
|
};
|
|
519
479
|
}
|
|
520
480
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
private stepMap: Map<string, number>;
|
|
533
|
-
private initialStepId: string;
|
|
534
|
-
private initialData: InitialFlowData;
|
|
535
|
-
private subscribers: Set<FlowSubscriber>;
|
|
536
|
-
private renderDispose: FlowCleanup | null;
|
|
537
|
-
private cleanupTasks: Set<FlowCleanup>;
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* 创建 Flow 实例。
|
|
541
|
-
* @param {FlowProps} [props={}] Flow 配置。
|
|
542
|
-
*/
|
|
543
|
-
constructor(props: FlowProps = {}) {
|
|
544
|
-
this.props = normalizeProps(props);
|
|
545
|
-
this.steps = this.props.steps;
|
|
546
|
-
this.validateSteps(this.steps);
|
|
547
|
-
this.stepMap = new Map(this.steps.map((step, index) => [step.id, index]));
|
|
548
|
-
this.initialStepId = this.resolveInitialStepId(this.props.initial);
|
|
549
|
-
this.initialData = this.createInitialStepData();
|
|
550
|
-
this.subscribers = new Set();
|
|
551
|
-
this.renderDispose = null;
|
|
552
|
-
this.cleanupTasks = new Set();
|
|
553
|
-
this.dom = {
|
|
554
|
-
root: null,
|
|
555
|
-
header: null,
|
|
556
|
-
body: null,
|
|
557
|
-
footer: null,
|
|
558
|
-
};
|
|
559
|
-
this.runtime = {
|
|
560
|
-
built: false,
|
|
561
|
-
destroyed: false,
|
|
562
|
-
activeAction: null,
|
|
563
|
-
actionController: null,
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
this.state = createDeepStore({
|
|
567
|
-
id: this.props.id,
|
|
568
|
-
currentId: this.initialStepId,
|
|
569
|
-
currentIndex: this.stepMap.get(this.initialStepId) ?? 0,
|
|
570
|
-
previousId: null,
|
|
571
|
-
previousIndex: null,
|
|
572
|
-
direction: null,
|
|
573
|
-
history: [this.initialStepId],
|
|
574
|
-
data: clonePlainObject(this.initialData.global),
|
|
575
|
-
stepData: clonePlainObject(this.initialData.stepData) as Record<
|
|
576
|
-
string,
|
|
577
|
-
FlowData
|
|
578
|
-
>,
|
|
579
|
-
loading: false,
|
|
580
|
-
error: null,
|
|
581
|
-
busyAction: null,
|
|
582
|
-
version: 0,
|
|
583
|
-
}) as FlowState;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* 当前步骤。
|
|
588
|
-
* @returns {FlowStep}
|
|
589
|
-
*/
|
|
590
|
-
get currentStep(): FlowStep {
|
|
591
|
-
return this.steps[this.state.currentIndex];
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* 当前步骤数据。
|
|
596
|
-
* @returns {object}
|
|
597
|
-
*/
|
|
598
|
-
get currentData(): FlowData {
|
|
599
|
-
return this.getStepData(this.state.currentId);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* 是否可以返回上一步。
|
|
604
|
-
* @returns {boolean}
|
|
605
|
-
*/
|
|
606
|
-
get canBack(): boolean {
|
|
607
|
-
return this.state.currentIndex > 0 && !this.state.loading;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* 是否可以前进。
|
|
612
|
-
* @returns {boolean}
|
|
613
|
-
*/
|
|
614
|
-
get canNext(): boolean {
|
|
615
|
-
return (
|
|
616
|
-
this.state.currentIndex < this.steps.length - 1 && !this.state.loading
|
|
481
|
+
export function createFlow(input: FlowProps = {}): Flow {
|
|
482
|
+
const props = normalizeProps(input);
|
|
483
|
+
const steps = props.steps;
|
|
484
|
+
const stepMap = new Map<string, number>();
|
|
485
|
+
for (const [index, step] of steps.entries()) {
|
|
486
|
+
validateParam(String(index), step, FLOW_STEP_RULE, 'Flow.props.steps');
|
|
487
|
+
validateParam(
|
|
488
|
+
'content',
|
|
489
|
+
step.content ?? null,
|
|
490
|
+
FLOW_CONTENT_RULE,
|
|
491
|
+
`Flow.props.steps.${index}`
|
|
617
492
|
);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* 是否处于最后一步。
|
|
622
|
-
* @returns {boolean}
|
|
623
|
-
*/
|
|
624
|
-
get isLast(): boolean {
|
|
625
|
-
return this.state.currentIndex === this.steps.length - 1;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* 订阅状态变化。
|
|
630
|
-
* @param {Function} handler 订阅函数。
|
|
631
|
-
* @returns {Function} 取消订阅函数。
|
|
632
|
-
*/
|
|
633
|
-
subscribe(handler: FlowSubscriber): FlowCleanup {
|
|
634
|
-
this.assertActive('subscribe');
|
|
635
|
-
if (typeof handler !== 'function') {
|
|
636
|
-
throw new Error('Flow.subscribe: handler expects a function.');
|
|
493
|
+
if (stepMap.has(step.id)) {
|
|
494
|
+
throw new Error(`Flow.props.steps: duplicated step id "${step.id}".`);
|
|
637
495
|
}
|
|
638
|
-
|
|
639
|
-
handler(this.snapshot(), this, null);
|
|
640
|
-
return () => this.subscribers.delete(handler);
|
|
496
|
+
stepMap.set(step.id, index);
|
|
641
497
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
currentStep: this.publicStep(currentStep),
|
|
664
|
-
canBack: this.canBack,
|
|
665
|
-
canNext: this.canNext,
|
|
666
|
-
isLast: this.isLast,
|
|
667
|
-
loading: this.state.loading,
|
|
668
|
-
busyAction: this.state.busyAction,
|
|
669
|
-
error: this.state.error,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* 构建默认 Flow UI。
|
|
675
|
-
* @returns {Flow}
|
|
676
|
-
*/
|
|
677
|
-
build(): this {
|
|
678
|
-
this.assertActive('build');
|
|
679
|
-
if (this.runtime.built) return this;
|
|
680
|
-
|
|
681
|
-
this.runtime.built = true;
|
|
682
|
-
if (!this.props.render) return this;
|
|
683
|
-
|
|
684
|
-
this.dom.root = this.buildRoot();
|
|
685
|
-
this.mountView();
|
|
686
|
-
return this;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
private teardownView(): void {
|
|
690
|
-
this.renderDispose?.();
|
|
691
|
-
this.renderDispose = null;
|
|
692
|
-
this.dom.root?.remove();
|
|
693
|
-
this.dom.root = null;
|
|
694
|
-
this.dom.header = null;
|
|
695
|
-
this.dom.body = null;
|
|
696
|
-
this.dom.footer = null;
|
|
697
|
-
this.runtime.built = false;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* 前进一步。
|
|
702
|
-
* @param {object|null} [payload=null] 当前步骤需要缓存的数据。
|
|
703
|
-
* @returns {Promise<object>} 切换后的快照。
|
|
704
|
-
*/
|
|
705
|
-
async next(payload: FlowPayload = null): Promise<FlowSnapshot | null> {
|
|
706
|
-
this.assertActive('next');
|
|
707
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.next');
|
|
708
|
-
const busySnapshot = this.handleBusy('next');
|
|
709
|
-
if (busySnapshot) return busySnapshot;
|
|
710
|
-
|
|
711
|
-
return this.runAction('next', async () => {
|
|
712
|
-
if (this.isLast) return this.finish(payload, { internal: true });
|
|
713
|
-
|
|
714
|
-
const fromStep = this.currentStep;
|
|
715
|
-
const fallbackId = this.steps[this.state.currentIndex + 1]?.id;
|
|
716
|
-
const result = await this.runMoveHook(
|
|
717
|
-
'next',
|
|
718
|
-
fromStep,
|
|
719
|
-
payload,
|
|
720
|
-
fallbackId
|
|
721
|
-
);
|
|
722
|
-
const { id, data } = normalizeStepResult(result, fallbackId);
|
|
723
|
-
return this.goTo(id, data ?? payload, {
|
|
724
|
-
direction: 'next',
|
|
725
|
-
internal: true,
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* 返回上一步。
|
|
732
|
-
* @param {object|null} [payload=null] 当前步骤需要缓存的数据。
|
|
733
|
-
* @returns {Promise<object>} 切换后的快照。
|
|
734
|
-
*/
|
|
735
|
-
async back(payload: FlowPayload = null): Promise<FlowSnapshot | null> {
|
|
736
|
-
this.assertActive('back');
|
|
737
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.back');
|
|
738
|
-
const busySnapshot = this.handleBusy('back');
|
|
739
|
-
if (busySnapshot) return busySnapshot;
|
|
740
|
-
|
|
741
|
-
if (!this.canBack) return this.snapshot();
|
|
742
|
-
|
|
743
|
-
return this.runAction('back', async () => {
|
|
744
|
-
const fromStep = this.currentStep;
|
|
745
|
-
const fallbackId = this.steps[this.state.currentIndex - 1]?.id;
|
|
746
|
-
const result = await this.runMoveHook(
|
|
747
|
-
'back',
|
|
748
|
-
fromStep,
|
|
749
|
-
payload,
|
|
750
|
-
fallbackId
|
|
751
|
-
);
|
|
752
|
-
const { id, data } = normalizeStepResult(result, fallbackId);
|
|
753
|
-
return this.goTo(id, data ?? payload, {
|
|
754
|
-
direction: 'back',
|
|
755
|
-
internal: true,
|
|
756
|
-
});
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* 跳转到指定步骤。
|
|
762
|
-
* @param {string|number} target 目标步骤 id 或索引。
|
|
763
|
-
* @param {object|null} [payload=null] 当前步骤需要缓存的数据。
|
|
764
|
-
* @param {{direction?:string, internal?:boolean}} [options={}] 跳转选项。
|
|
765
|
-
* @returns {Promise<object>} 切换后的快照。
|
|
766
|
-
*/
|
|
767
|
-
async goTo(
|
|
768
|
-
target: FlowTarget,
|
|
769
|
-
payload: FlowPayload = null,
|
|
770
|
-
options: FlowGoToOptions = {}
|
|
771
|
-
): Promise<FlowSnapshot | null> {
|
|
772
|
-
this.assertActive('goTo');
|
|
773
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.goTo');
|
|
774
|
-
const busySnapshot = options.internal ? null : this.handleBusy('goTo');
|
|
775
|
-
if (busySnapshot) return busySnapshot;
|
|
776
|
-
|
|
777
|
-
return this.runAction(
|
|
778
|
-
'goTo',
|
|
779
|
-
async () => {
|
|
780
|
-
const toIndex = this.resolveStepIndex(target);
|
|
781
|
-
const toStep = this.steps[toIndex];
|
|
782
|
-
const fromStep = this.currentStep;
|
|
783
|
-
|
|
784
|
-
if (!toStep || toStep.id === this.state.currentId) {
|
|
785
|
-
if (payload) this.setStepData(this.state.currentId, payload);
|
|
786
|
-
return this.snapshot();
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
await this.transitionTo(toStep, {
|
|
790
|
-
direction: options.direction || 'go',
|
|
791
|
-
payload,
|
|
792
|
-
fromStep,
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
return this.snapshot();
|
|
796
|
-
},
|
|
797
|
-
{ internal: options.internal }
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* 合并全局数据。
|
|
803
|
-
* @param {object} data 数据补丁。
|
|
804
|
-
* @returns {Flow}
|
|
805
|
-
*/
|
|
806
|
-
setData(data: FlowPayload): this {
|
|
807
|
-
this.assertActive('setData');
|
|
808
|
-
validateParam('data', data, FLOW_PAYLOAD_RULE, 'Flow.setData');
|
|
809
|
-
if (!data) return this;
|
|
810
|
-
|
|
811
|
-
flushSync(() => {
|
|
812
|
-
Object.assign(this.state.data, data);
|
|
813
|
-
this.state.version += 1;
|
|
814
|
-
});
|
|
815
|
-
this.emitChange();
|
|
816
|
-
return this;
|
|
498
|
+
const initialStepId = (() => {
|
|
499
|
+
if (typeof props.initial === 'number') {
|
|
500
|
+
const step = steps[props.initial];
|
|
501
|
+
if (!step) throw new Error('Flow.props.initial index is out of range.');
|
|
502
|
+
return step.id;
|
|
503
|
+
}
|
|
504
|
+
if (typeof props.initial === 'string') {
|
|
505
|
+
if (!stepMap.has(props.initial)) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Flow.props.initial step "${props.initial}" does not exist.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return props.initial;
|
|
511
|
+
}
|
|
512
|
+
return steps[0].id;
|
|
513
|
+
})();
|
|
514
|
+
const initialStepData: Record<string, FlowData> = {};
|
|
515
|
+
const initialGlobal: FlowData = {};
|
|
516
|
+
for (const step of steps) {
|
|
517
|
+
initialStepData[step.id] = clonePlainObject(step.data);
|
|
518
|
+
Object.assign(initialGlobal, initialStepData[step.id]);
|
|
817
519
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
520
|
+
const state = createDeepStore({
|
|
521
|
+
id: props.id,
|
|
522
|
+
currentId: initialStepId,
|
|
523
|
+
currentIndex: stepMap.get(initialStepId) ?? 0,
|
|
524
|
+
previousId: null,
|
|
525
|
+
previousIndex: null,
|
|
526
|
+
direction: null,
|
|
527
|
+
history: [initialStepId],
|
|
528
|
+
data: clonePlainObject(initialGlobal),
|
|
529
|
+
stepData: clonePlainObject(initialStepData) as Record<string, FlowData>,
|
|
530
|
+
loading: false,
|
|
531
|
+
error: null,
|
|
532
|
+
busyAction: null,
|
|
533
|
+
version: 0,
|
|
534
|
+
}) as FlowState;
|
|
535
|
+
const runtime: FlowRuntime = {
|
|
536
|
+
built: false,
|
|
537
|
+
mounted: false,
|
|
538
|
+
destroyed: false,
|
|
539
|
+
activeAction: null,
|
|
540
|
+
actionController: null,
|
|
541
|
+
};
|
|
542
|
+
const subscribers = new Set<FlowSubscriber>();
|
|
543
|
+
const cleanupTasks = new Set<FlowCleanup>();
|
|
544
|
+
let ui: FunctionalComponent<
|
|
545
|
+
Record<string, unknown>,
|
|
546
|
+
FlowState,
|
|
547
|
+
HTMLElement,
|
|
548
|
+
object
|
|
549
|
+
> | null = null;
|
|
550
|
+
let flow!: Flow;
|
|
551
|
+
|
|
552
|
+
const assertActive = (method: string): void => {
|
|
553
|
+
if (runtime.destroyed) {
|
|
554
|
+
throw new Error(`Flow.${method}: instance has been destroyed.`);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const currentStep = (): FlowStep => steps[state.currentIndex];
|
|
558
|
+
const getStepData = (stepId: string): FlowData =>
|
|
559
|
+
clonePlainObject(state.stepData[stepId]);
|
|
560
|
+
const publicStep = (
|
|
561
|
+
step: FlowStep | null | undefined
|
|
562
|
+
): PublicFlowStep | null => {
|
|
563
|
+
if (!step) return null;
|
|
564
|
+
const {
|
|
565
|
+
onEnter: _onEnter,
|
|
566
|
+
onLeave: _onLeave,
|
|
567
|
+
onNext: _onNext,
|
|
568
|
+
onBack: _onBack,
|
|
569
|
+
canEnter: _canEnter,
|
|
570
|
+
canLeave: _canLeave,
|
|
571
|
+
...result
|
|
572
|
+
} = step;
|
|
573
|
+
return result;
|
|
574
|
+
};
|
|
575
|
+
const snapshot = (): FlowSnapshot => ({
|
|
576
|
+
id: state.id,
|
|
577
|
+
currentId: state.currentId,
|
|
578
|
+
currentIndex: state.currentIndex,
|
|
579
|
+
previousId: state.previousId,
|
|
580
|
+
previousIndex: state.previousIndex,
|
|
581
|
+
direction: state.direction,
|
|
582
|
+
history: [...state.history],
|
|
583
|
+
data: clonePlainObject(state.data),
|
|
584
|
+
stepData: clonePlainObject(state.stepData) as Record<string, FlowData>,
|
|
585
|
+
currentData: getStepData(state.currentId),
|
|
586
|
+
currentStep: publicStep(currentStep()),
|
|
587
|
+
canBack: state.currentIndex > 0 && !state.loading,
|
|
588
|
+
canNext: state.currentIndex < steps.length - 1 && !state.loading,
|
|
589
|
+
isLast: state.currentIndex === steps.length - 1,
|
|
590
|
+
loading: state.loading,
|
|
591
|
+
busyAction: state.busyAction,
|
|
592
|
+
error: state.error,
|
|
593
|
+
});
|
|
594
|
+
const emitChange = (previous: FlowSnapshot | null = null): void => {
|
|
595
|
+
const next = snapshot();
|
|
596
|
+
for (const subscriber of subscribers) subscriber(next, flow, previous);
|
|
597
|
+
props.onChange?.(next, flow, previous);
|
|
598
|
+
};
|
|
599
|
+
const replaceObject = (target: FlowData, source: FlowData): void => {
|
|
600
|
+
for (const key of Object.keys(target)) delete target[key];
|
|
601
|
+
Object.assign(target, source);
|
|
602
|
+
};
|
|
603
|
+
const setStepData = (
|
|
826
604
|
stepId: string,
|
|
827
605
|
data: FlowPayload,
|
|
828
606
|
options: { silent?: boolean } = {}
|
|
829
|
-
):
|
|
830
|
-
|
|
607
|
+
): Flow => {
|
|
608
|
+
assertActive('setStepData');
|
|
831
609
|
validateParam('stepId', stepId, { type: 'string' }, 'Flow.setStepData');
|
|
832
610
|
validateParam('data', data, FLOW_PAYLOAD_RULE, 'Flow.setStepData');
|
|
833
|
-
if (!data || !
|
|
834
|
-
|
|
611
|
+
if (!data || !stepMap.has(stepId)) return flow;
|
|
835
612
|
flushSync(() => {
|
|
836
|
-
|
|
837
|
-
...clonePlainObject(
|
|
613
|
+
state.stepData[stepId] = {
|
|
614
|
+
...clonePlainObject(state.stepData[stepId]),
|
|
838
615
|
...data,
|
|
839
616
|
};
|
|
840
|
-
if (
|
|
841
|
-
|
|
617
|
+
if (props.cache) Object.assign(state.data, data);
|
|
618
|
+
state.version += 1;
|
|
842
619
|
});
|
|
843
|
-
if (!options.silent)
|
|
844
|
-
return
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
* @returns {object}
|
|
851
|
-
*/
|
|
852
|
-
getStepData(stepId: string): FlowData {
|
|
853
|
-
return clonePlainObject(this.state.stepData[stepId]);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* 重置流程。
|
|
858
|
-
* @returns {Flow}
|
|
859
|
-
*/
|
|
860
|
-
reset(): this {
|
|
861
|
-
this.assertActive('reset');
|
|
620
|
+
if (!options.silent) emitChange();
|
|
621
|
+
return flow;
|
|
622
|
+
};
|
|
623
|
+
const setData = (data: FlowPayload): Flow => {
|
|
624
|
+
assertActive('setData');
|
|
625
|
+
validateParam('data', data, FLOW_PAYLOAD_RULE, 'Flow.setData');
|
|
626
|
+
if (!data) return flow;
|
|
862
627
|
flushSync(() => {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.state.previousId = null;
|
|
866
|
-
this.state.previousIndex = null;
|
|
867
|
-
this.state.direction = null;
|
|
868
|
-
this.state.history.splice(
|
|
869
|
-
0,
|
|
870
|
-
this.state.history.length,
|
|
871
|
-
this.initialStepId
|
|
872
|
-
);
|
|
873
|
-
this.replaceObject(
|
|
874
|
-
this.state.data,
|
|
875
|
-
clonePlainObject(this.initialData.global)
|
|
876
|
-
);
|
|
877
|
-
this.replaceObject(
|
|
878
|
-
this.state.stepData,
|
|
879
|
-
clonePlainObject(this.initialData.stepData)
|
|
880
|
-
);
|
|
881
|
-
this.state.loading = false;
|
|
882
|
-
this.state.busyAction = null;
|
|
883
|
-
this.state.error = null;
|
|
884
|
-
this.state.version += 1;
|
|
628
|
+
Object.assign(state.data, data);
|
|
629
|
+
state.version += 1;
|
|
885
630
|
});
|
|
886
|
-
|
|
887
|
-
return
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
* @param {object|null} [payload=null] 最后一步需要缓存的数据。
|
|
893
|
-
* @returns {Promise<object>} 当前快照。
|
|
894
|
-
*/
|
|
895
|
-
async finish(
|
|
896
|
-
payload: FlowPayload = null,
|
|
897
|
-
options: FlowRunActionOptions = {}
|
|
898
|
-
): Promise<FlowSnapshot | null> {
|
|
899
|
-
this.assertActive('finish');
|
|
900
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.finish');
|
|
901
|
-
const busySnapshot = options.internal ? null : this.handleBusy('finish');
|
|
902
|
-
if (busySnapshot) return busySnapshot;
|
|
903
|
-
|
|
904
|
-
return this.runAction(
|
|
905
|
-
'finish',
|
|
906
|
-
async () => {
|
|
907
|
-
if (payload) this.setStepData(this.state.currentId, payload);
|
|
908
|
-
await this.callHook(this.props.onFinish, [this.snapshot(), this]);
|
|
909
|
-
return this.snapshot();
|
|
910
|
-
},
|
|
911
|
-
{ internal: options.internal }
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* 销毁 Flow 实例。
|
|
917
|
-
* @returns {void}
|
|
918
|
-
*/
|
|
919
|
-
destroy(): void {
|
|
920
|
-
if (this.runtime.destroyed) return;
|
|
921
|
-
this.runtime.destroyed = true;
|
|
922
|
-
this.abortActiveAction();
|
|
923
|
-
this.teardownView();
|
|
924
|
-
for (const cleanup of Array.from(this.cleanupTasks)) {
|
|
925
|
-
cleanup();
|
|
926
|
-
}
|
|
927
|
-
this.cleanupTasks.clear();
|
|
928
|
-
this.subscribers.clear();
|
|
929
|
-
this.steps = [];
|
|
930
|
-
this.stepMap.clear();
|
|
931
|
-
this.runtime.activeAction = null;
|
|
932
|
-
this.runtime.actionController = null;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
private resolveInitialStepId(initial: string | number | null): string {
|
|
936
|
-
if (typeof initial === 'number') {
|
|
937
|
-
const step = this.steps[initial];
|
|
938
|
-
if (!step) throw new Error('Flow.props.initial index is out of range.');
|
|
939
|
-
return step.id;
|
|
940
|
-
}
|
|
941
|
-
if (typeof initial === 'string') {
|
|
942
|
-
if (!this.stepMap.has(initial)) {
|
|
943
|
-
throw new Error(`Flow.props.initial step "${initial}" does not exist.`);
|
|
944
|
-
}
|
|
945
|
-
return initial;
|
|
946
|
-
}
|
|
947
|
-
return this.steps[0].id;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
private validateSteps(steps: FlowStep[]): void {
|
|
951
|
-
const ids = new Set<string>();
|
|
952
|
-
for (const [index, step] of steps.entries()) {
|
|
953
|
-
validateParam(String(index), step, FLOW_STEP_RULE, 'Flow.props.steps');
|
|
954
|
-
validateParam(
|
|
955
|
-
'content',
|
|
956
|
-
step.content ?? null,
|
|
957
|
-
FLOW_CONTENT_RULE,
|
|
958
|
-
`Flow.props.steps.${index}`
|
|
959
|
-
);
|
|
960
|
-
if (ids.has(step.id)) {
|
|
961
|
-
throw new Error(`Flow.props.steps: duplicated step id "${step.id}".`);
|
|
962
|
-
}
|
|
963
|
-
ids.add(step.id);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
private resolveStepIndex(target: FlowTarget): number {
|
|
968
|
-
if (typeof target === 'number') {
|
|
969
|
-
if (!this.steps[target])
|
|
970
|
-
throw new Error('Flow.goTo target is out of range.');
|
|
971
|
-
return target;
|
|
972
|
-
}
|
|
973
|
-
if (typeof target === 'string') {
|
|
974
|
-
const index = this.stepMap.get(target);
|
|
975
|
-
if (index == null) {
|
|
976
|
-
throw new Error(`Flow.goTo target "${target}" does not exist.`);
|
|
977
|
-
}
|
|
978
|
-
return index;
|
|
979
|
-
}
|
|
980
|
-
throw new Error('Flow.goTo target expects string or number.');
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
private createInitialStepData(): InitialFlowData {
|
|
984
|
-
const stepData: Record<string, FlowData> = {};
|
|
985
|
-
const global: FlowData = {};
|
|
986
|
-
for (const step of this.steps) {
|
|
987
|
-
stepData[step.id] = clonePlainObject(step.data);
|
|
988
|
-
Object.assign(global, stepData[step.id]);
|
|
989
|
-
}
|
|
990
|
-
return { stepData, global };
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
private async runMoveHook(
|
|
994
|
-
type: 'next' | 'back',
|
|
995
|
-
step: FlowStep,
|
|
996
|
-
payload: FlowPayload,
|
|
997
|
-
fallbackId: string
|
|
998
|
-
): Promise<FlowStepResult | FlowPayload | void> {
|
|
999
|
-
const globalHook = type === 'back' ? this.props.onBack : this.props.onNext;
|
|
1000
|
-
const stepHook = type === 'back' ? step.onBack : step.onNext;
|
|
1001
|
-
const context = this.createContext({ payload, targetId: fallbackId });
|
|
1002
|
-
|
|
1003
|
-
if (typeof stepHook === 'function') {
|
|
1004
|
-
const result = await this.callHook(stepHook, [context]);
|
|
1005
|
-
if (result != null) return result;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
if (typeof globalHook === 'function') {
|
|
1009
|
-
const result = await this.callHook(globalHook, [context]);
|
|
1010
|
-
if (result != null) return result;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
return fallbackId;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
private async transitionTo(
|
|
1017
|
-
toStep: FlowStep,
|
|
1018
|
-
{ direction, payload, fromStep }: FlowTransitionOptions
|
|
1019
|
-
): Promise<void> {
|
|
1020
|
-
const fromSnapshot = this.snapshot();
|
|
1021
|
-
const rollbackState = this.captureState();
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
if (payload) this.setStepData(fromStep.id, payload, { silent: true });
|
|
1025
|
-
|
|
1026
|
-
await this.assertCanLeave(fromStep, toStep, direction);
|
|
1027
|
-
await this.assertCanEnter(toStep, fromStep, direction);
|
|
1028
|
-
|
|
1029
|
-
if (typeof fromStep.onLeave === 'function') {
|
|
1030
|
-
await this.callHook(fromStep.onLeave, [
|
|
1031
|
-
this.createContext({
|
|
1032
|
-
direction,
|
|
1033
|
-
step: fromStep,
|
|
1034
|
-
targetId: toStep.id,
|
|
1035
|
-
}),
|
|
1036
|
-
]);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
const previousId = this.state.currentId;
|
|
1040
|
-
const previousIndex = this.state.currentIndex;
|
|
1041
|
-
const currentIndex = this.stepMap.get(toStep.id) ?? 0;
|
|
1042
|
-
|
|
1043
|
-
flushSync(() => {
|
|
1044
|
-
this.state.previousId = previousId;
|
|
1045
|
-
this.state.previousIndex = previousIndex;
|
|
1046
|
-
this.state.currentId = toStep.id;
|
|
1047
|
-
this.state.currentIndex = currentIndex;
|
|
1048
|
-
this.state.direction = direction;
|
|
1049
|
-
if (direction === 'back') this.state.history.pop();
|
|
1050
|
-
else this.state.history.push(toStep.id);
|
|
1051
|
-
this.state.error = null;
|
|
1052
|
-
this.state.version += 1;
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
if (typeof toStep.onEnter === 'function') {
|
|
1056
|
-
await this.callHook(toStep.onEnter, [
|
|
1057
|
-
this.createContext({
|
|
1058
|
-
direction,
|
|
1059
|
-
fromId: previousId,
|
|
1060
|
-
step: toStep,
|
|
1061
|
-
}),
|
|
1062
|
-
]);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
this.emitChange(fromSnapshot);
|
|
1066
|
-
} catch (error) {
|
|
1067
|
-
if (this.runtime.destroyed) throw error;
|
|
1068
|
-
if (this.props.rollbackOnError) {
|
|
1069
|
-
this.restoreState(rollbackState, { keepLoading: true });
|
|
1070
|
-
}
|
|
1071
|
-
this.handleError(error, fromSnapshot);
|
|
1072
|
-
throw error;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
private async assertCanLeave(
|
|
1077
|
-
fromStep: FlowStep,
|
|
1078
|
-
toStep: FlowStep,
|
|
1079
|
-
direction: FlowDirection
|
|
1080
|
-
): Promise<void> {
|
|
1081
|
-
if (typeof fromStep.canLeave !== 'function') return;
|
|
1082
|
-
const result = await this.callHook(fromStep.canLeave, [
|
|
1083
|
-
this.createContext({ direction, targetId: toStep.id }),
|
|
1084
|
-
]);
|
|
1085
|
-
if (result === false) {
|
|
1086
|
-
throw new Error(`Flow: step "${fromStep.id}" blocked leaving.`);
|
|
631
|
+
emitChange();
|
|
632
|
+
return flow;
|
|
633
|
+
};
|
|
634
|
+
const addCleanup = (cleanup: FlowCleanup): FlowCleanup => {
|
|
635
|
+
if (typeof cleanup !== 'function') {
|
|
636
|
+
throw new Error('Flow.addCleanup: cleanup expects a function.');
|
|
1087
637
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
toStep: FlowStep,
|
|
1092
|
-
fromStep: FlowStep,
|
|
1093
|
-
direction: FlowDirection
|
|
1094
|
-
): Promise<void> {
|
|
1095
|
-
if (typeof toStep.canEnter !== 'function') return;
|
|
1096
|
-
const result = await this.callHook(toStep.canEnter, [
|
|
1097
|
-
this.createContext({ direction, fromId: fromStep.id }),
|
|
1098
|
-
]);
|
|
1099
|
-
if (result === false) {
|
|
1100
|
-
throw new Error(`Flow: step "${toStep.id}" blocked entering.`);
|
|
638
|
+
if (runtime.destroyed) {
|
|
639
|
+
cleanup();
|
|
640
|
+
return () => {};
|
|
1101
641
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const
|
|
642
|
+
cleanupTasks.add(cleanup);
|
|
643
|
+
return () => cleanupTasks.delete(cleanup);
|
|
644
|
+
};
|
|
645
|
+
const createAbortError = (): FlowError => {
|
|
646
|
+
const error = new Error('Flow: active action was aborted.') as FlowError;
|
|
647
|
+
error.code = 'FLOW_ABORTED';
|
|
648
|
+
return error;
|
|
649
|
+
};
|
|
650
|
+
const callHook = async <TResult>(
|
|
651
|
+
hook: ((...args: never[]) => TResult | Promise<TResult>) | null | undefined,
|
|
652
|
+
args: unknown[]
|
|
653
|
+
): Promise<TResult | undefined> => {
|
|
654
|
+
if (typeof hook !== 'function') return undefined;
|
|
655
|
+
const result = await (
|
|
656
|
+
hook as (...values: unknown[]) => TResult | Promise<TResult>
|
|
657
|
+
)(...args);
|
|
658
|
+
if (runtime.destroyed) throw createAbortError();
|
|
659
|
+
return result;
|
|
660
|
+
};
|
|
661
|
+
const createContext = (
|
|
662
|
+
extra: Partial<
|
|
663
|
+
Pick<
|
|
664
|
+
FlowContext,
|
|
665
|
+
'step' | 'payload' | 'direction' | 'fromId' | 'targetId'
|
|
666
|
+
>
|
|
667
|
+
> = {}
|
|
668
|
+
): FlowContext => {
|
|
669
|
+
const step = extra.step || currentStep();
|
|
1107
670
|
return {
|
|
1108
671
|
...extra,
|
|
1109
|
-
flow
|
|
672
|
+
flow,
|
|
1110
673
|
step,
|
|
1111
|
-
state
|
|
1112
|
-
signal:
|
|
1113
|
-
snapshot,
|
|
1114
|
-
data: clonePlainObject(
|
|
1115
|
-
currentData:
|
|
1116
|
-
setData
|
|
1117
|
-
setStepData: (
|
|
1118
|
-
getStepData
|
|
1119
|
-
next: (payload) =>
|
|
1120
|
-
back: (payload) =>
|
|
1121
|
-
goTo: (target, payload, options) =>
|
|
1122
|
-
addCleanup
|
|
674
|
+
state,
|
|
675
|
+
signal: runtime.actionController?.signal || null,
|
|
676
|
+
snapshot: snapshot(),
|
|
677
|
+
data: clonePlainObject(state.data),
|
|
678
|
+
currentData: getStepData(step.id),
|
|
679
|
+
setData,
|
|
680
|
+
setStepData: (id, data) => setStepData(id, data),
|
|
681
|
+
getStepData,
|
|
682
|
+
next: (payload) => flow.next(payload),
|
|
683
|
+
back: (payload) => flow.back(payload),
|
|
684
|
+
goTo: (target, payload, options) => flow.goTo(target, payload, options),
|
|
685
|
+
addCleanup,
|
|
1123
686
|
};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
|
|
687
|
+
};
|
|
688
|
+
const handleError = (
|
|
689
|
+
error: unknown,
|
|
690
|
+
previous: FlowSnapshot | null = null
|
|
691
|
+
): void => {
|
|
692
|
+
flushSync(() => {
|
|
693
|
+
state.error = error;
|
|
694
|
+
state.version += 1;
|
|
695
|
+
});
|
|
696
|
+
props.onError?.(error, snapshot(), flow, previous);
|
|
697
|
+
};
|
|
698
|
+
const setLoading = (value: boolean, action: FlowAction | null): void => {
|
|
699
|
+
const previous = snapshot();
|
|
700
|
+
if (state.loading === value && state.busyAction === action) return;
|
|
701
|
+
flushSync(() => {
|
|
702
|
+
state.loading = value;
|
|
703
|
+
state.busyAction = value ? action : null;
|
|
704
|
+
state.version += 1;
|
|
705
|
+
});
|
|
706
|
+
emitChange(previous);
|
|
707
|
+
};
|
|
708
|
+
const handleBusy = (action: FlowAction): FlowSnapshot | null => {
|
|
709
|
+
if (!state.loading) return null;
|
|
710
|
+
const error = new Error(
|
|
711
|
+
`Flow: action "${action}" ignored while loading.`
|
|
712
|
+
) as FlowError;
|
|
713
|
+
error.code = 'FLOW_BUSY';
|
|
714
|
+
props.onBusy?.(action, snapshot(), flow);
|
|
715
|
+
if (props.busyStrategy === 'throw') throw error;
|
|
716
|
+
return snapshot();
|
|
717
|
+
};
|
|
718
|
+
const runAction = async (
|
|
1127
719
|
action: FlowAction,
|
|
1128
720
|
task: () => Promise<FlowSnapshot | null>,
|
|
1129
|
-
|
|
1130
|
-
): Promise<FlowSnapshot | null> {
|
|
1131
|
-
const
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
this.setLoading(true, action);
|
|
721
|
+
internal = false
|
|
722
|
+
): Promise<FlowSnapshot | null> => {
|
|
723
|
+
const outer = !internal && !state.loading;
|
|
724
|
+
if (outer) {
|
|
725
|
+
runtime.actionController = new AbortController();
|
|
726
|
+
runtime.activeAction = action;
|
|
727
|
+
setLoading(true, action);
|
|
1137
728
|
}
|
|
1138
|
-
|
|
1139
729
|
try {
|
|
1140
730
|
return await task();
|
|
1141
731
|
} catch (error) {
|
|
1142
|
-
if (
|
|
1143
|
-
if (
|
|
732
|
+
if (runtime.destroyed) return null;
|
|
733
|
+
if (state.error !== error) handleError(error);
|
|
1144
734
|
throw error;
|
|
1145
735
|
} finally {
|
|
1146
|
-
if (
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
736
|
+
if (outer && !runtime.destroyed) {
|
|
737
|
+
setLoading(false, null);
|
|
738
|
+
runtime.activeAction = null;
|
|
739
|
+
runtime.actionController = null;
|
|
1150
740
|
}
|
|
1151
741
|
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
return this.snapshot();
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
private abortActiveAction(): void {
|
|
1172
|
-
this.runtime.actionController?.abort();
|
|
1173
|
-
this.runtime.actionController = null;
|
|
1174
|
-
this.runtime.activeAction = null;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
private async callHook<TResult>(
|
|
1178
|
-
hook: ((...args: never[]) => TResult | Promise<TResult>) | null | undefined,
|
|
1179
|
-
args: unknown[]
|
|
1180
|
-
): Promise<TResult | undefined> {
|
|
1181
|
-
if (typeof hook !== 'function') return undefined;
|
|
1182
|
-
const result = await (
|
|
1183
|
-
hook as (...args: unknown[]) => TResult | Promise<TResult>
|
|
1184
|
-
)(...args);
|
|
1185
|
-
if (this.runtime.destroyed) throw this.createAbortError();
|
|
1186
|
-
return result;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
private createAbortError(): FlowError {
|
|
1190
|
-
const error = new Error('Flow: active action was aborted.') as FlowError;
|
|
1191
|
-
error.code = 'FLOW_ABORTED';
|
|
1192
|
-
return error;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
private setLoading(
|
|
1196
|
-
value: boolean,
|
|
1197
|
-
action: FlowAction | null = this.state.busyAction
|
|
1198
|
-
): void {
|
|
1199
|
-
const previous = this.snapshot();
|
|
1200
|
-
const busyAction = value ? action : null;
|
|
1201
|
-
if (this.state.loading === value && this.state.busyAction === busyAction)
|
|
1202
|
-
return;
|
|
1203
|
-
|
|
1204
|
-
flushSync(() => {
|
|
1205
|
-
this.state.loading = value;
|
|
1206
|
-
this.state.busyAction = busyAction;
|
|
1207
|
-
this.state.version += 1;
|
|
1208
|
-
});
|
|
1209
|
-
this.emitChange(previous);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
private handleError(
|
|
1213
|
-
error: unknown,
|
|
1214
|
-
previous: FlowSnapshot | null = null
|
|
1215
|
-
): void {
|
|
742
|
+
};
|
|
743
|
+
const captureState = (): CapturedFlowState => ({
|
|
744
|
+
currentId: state.currentId,
|
|
745
|
+
currentIndex: state.currentIndex,
|
|
746
|
+
previousId: state.previousId,
|
|
747
|
+
previousIndex: state.previousIndex,
|
|
748
|
+
direction: state.direction,
|
|
749
|
+
history: [...state.history],
|
|
750
|
+
data: clonePlainObject(state.data),
|
|
751
|
+
stepData: clonePlainObject(state.stepData) as Record<string, FlowData>,
|
|
752
|
+
loading: state.loading,
|
|
753
|
+
busyAction: state.busyAction,
|
|
754
|
+
error: state.error,
|
|
755
|
+
version: state.version,
|
|
756
|
+
});
|
|
757
|
+
const restoreState = (captured: CapturedFlowState): void => {
|
|
1216
758
|
flushSync(() => {
|
|
1217
|
-
|
|
1218
|
-
|
|
759
|
+
state.currentId = captured.currentId;
|
|
760
|
+
state.currentIndex = captured.currentIndex;
|
|
761
|
+
state.previousId = captured.previousId;
|
|
762
|
+
state.previousIndex = captured.previousIndex;
|
|
763
|
+
state.direction = captured.direction;
|
|
764
|
+
state.history.splice(0, state.history.length, ...captured.history);
|
|
765
|
+
replaceObject(state.data, captured.data);
|
|
766
|
+
replaceObject(state.stepData, captured.stepData);
|
|
767
|
+
state.loading = true;
|
|
768
|
+
state.busyAction = captured.busyAction;
|
|
769
|
+
state.error = captured.error;
|
|
770
|
+
state.version = captured.version + 1;
|
|
1219
771
|
});
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
private emitChange(previous: FlowSnapshot | null = null): void {
|
|
1226
|
-
const next = this.snapshot();
|
|
1227
|
-
for (const handler of Array.from(this.subscribers)) {
|
|
1228
|
-
handler(next, this, previous);
|
|
772
|
+
};
|
|
773
|
+
const resolveStepIndex = (target: FlowTarget): number => {
|
|
774
|
+
if (typeof target === 'number') {
|
|
775
|
+
if (!steps[target]) throw new Error('Flow.goTo target is out of range.');
|
|
776
|
+
return target;
|
|
1229
777
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
778
|
+
const index = stepMap.get(target);
|
|
779
|
+
if (index == null)
|
|
780
|
+
throw new Error(`Flow.goTo target "${target}" does not exist.`);
|
|
781
|
+
return index;
|
|
782
|
+
};
|
|
783
|
+
const transitionTo = async (
|
|
784
|
+
toStep: FlowStep,
|
|
785
|
+
direction: FlowDirection,
|
|
786
|
+
payload: FlowPayload,
|
|
787
|
+
fromStep: FlowStep
|
|
788
|
+
): Promise<void> => {
|
|
789
|
+
const previous = snapshot();
|
|
790
|
+
const rollback = captureState();
|
|
791
|
+
try {
|
|
792
|
+
if (payload) setStepData(fromStep.id, payload, { silent: true });
|
|
793
|
+
if (fromStep.canLeave) {
|
|
794
|
+
const allowed = await callHook(fromStep.canLeave, [
|
|
795
|
+
createContext({ direction, targetId: toStep.id }),
|
|
796
|
+
]);
|
|
797
|
+
if (allowed === false) {
|
|
798
|
+
throw new Error(`Flow: step "${fromStep.id}" blocked leaving.`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (toStep.canEnter) {
|
|
802
|
+
const allowed = await callHook(toStep.canEnter, [
|
|
803
|
+
createContext({ direction, fromId: fromStep.id, step: toStep }),
|
|
804
|
+
]);
|
|
805
|
+
if (allowed === false) {
|
|
806
|
+
throw new Error(`Flow: step "${toStep.id}" blocked entering.`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
await callHook(fromStep.onLeave, [
|
|
810
|
+
createContext({ direction, step: fromStep, targetId: toStep.id }),
|
|
811
|
+
]);
|
|
812
|
+
const previousId = state.currentId;
|
|
813
|
+
const previousIndex = state.currentIndex;
|
|
814
|
+
flushSync(() => {
|
|
815
|
+
state.previousId = previousId;
|
|
816
|
+
state.previousIndex = previousIndex;
|
|
817
|
+
state.currentId = toStep.id;
|
|
818
|
+
state.currentIndex = stepMap.get(toStep.id) ?? 0;
|
|
819
|
+
state.direction = direction;
|
|
820
|
+
if (direction === 'back') state.history.pop();
|
|
821
|
+
else state.history.push(toStep.id);
|
|
822
|
+
state.error = null;
|
|
823
|
+
state.version += 1;
|
|
824
|
+
});
|
|
825
|
+
await callHook(toStep.onEnter, [
|
|
826
|
+
createContext({ direction, fromId: previousId, step: toStep }),
|
|
827
|
+
]);
|
|
828
|
+
emitChange(previous);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
if (runtime.destroyed) throw error;
|
|
831
|
+
if (props.rollbackOnError) restoreState(rollback);
|
|
832
|
+
handleError(error, previous);
|
|
833
|
+
throw error;
|
|
1232
834
|
}
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
835
|
+
};
|
|
836
|
+
const runMoveHook = async (
|
|
837
|
+
type: 'next' | 'back',
|
|
838
|
+
step: FlowStep,
|
|
839
|
+
payload: FlowPayload,
|
|
840
|
+
fallbackId: string
|
|
841
|
+
): Promise<FlowStepResult | FlowPayload | void> => {
|
|
842
|
+
const stepHook = type === 'back' ? step.onBack : step.onNext;
|
|
843
|
+
const globalHook = type === 'back' ? props.onBack : props.onNext;
|
|
844
|
+
const context = createContext({ payload, targetId: fallbackId });
|
|
845
|
+
const stepResult = await callHook(stepHook, [context]);
|
|
846
|
+
if (stepResult != null) return stepResult;
|
|
847
|
+
const globalResult = await callHook(globalHook, [context]);
|
|
848
|
+
return globalResult ?? fallbackId;
|
|
849
|
+
};
|
|
1239
850
|
|
|
1240
|
-
|
|
851
|
+
const createRenderContext = (
|
|
852
|
+
fallback: () => RenderableContent<FlowRenderContext>
|
|
853
|
+
): FlowRenderContext => {
|
|
854
|
+
const current = snapshot();
|
|
1241
855
|
return {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
data:
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
busyAction: this.state.busyAction,
|
|
1255
|
-
error: this.state.error,
|
|
1256
|
-
version: this.state.version,
|
|
856
|
+
flow,
|
|
857
|
+
snapshot: current,
|
|
858
|
+
state,
|
|
859
|
+
steps: steps.map(publicStep),
|
|
860
|
+
currentStep: current.currentStep,
|
|
861
|
+
currentData: current.currentData,
|
|
862
|
+
data: current.data,
|
|
863
|
+
fallback,
|
|
864
|
+
next: (payload) => flow.next(payload),
|
|
865
|
+
back: (payload) => flow.back(payload),
|
|
866
|
+
goTo: (target, payload, options) => flow.goTo(target, payload, options),
|
|
867
|
+
reset: () => flow.reset(),
|
|
1257
868
|
};
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
private restoreState(
|
|
1261
|
-
snapshot: CapturedFlowState,
|
|
1262
|
-
options: { keepLoading?: boolean } = {}
|
|
1263
|
-
): void {
|
|
1264
|
-
flushSync(() => {
|
|
1265
|
-
this.state.currentId = snapshot.currentId;
|
|
1266
|
-
this.state.currentIndex = snapshot.currentIndex;
|
|
1267
|
-
this.state.previousId = snapshot.previousId;
|
|
1268
|
-
this.state.previousIndex = snapshot.previousIndex;
|
|
1269
|
-
this.state.direction = snapshot.direction;
|
|
1270
|
-
this.state.history.splice(
|
|
1271
|
-
0,
|
|
1272
|
-
this.state.history.length,
|
|
1273
|
-
...snapshot.history
|
|
1274
|
-
);
|
|
1275
|
-
this.replaceObject(this.state.data, snapshot.data);
|
|
1276
|
-
this.replaceObject(this.state.stepData, snapshot.stepData);
|
|
1277
|
-
this.state.loading = options.keepLoading ? true : snapshot.loading;
|
|
1278
|
-
this.state.busyAction = snapshot.busyAction;
|
|
1279
|
-
this.state.error = snapshot.error;
|
|
1280
|
-
this.state.version = snapshot.version + 1;
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
private addCleanup(cleanup: FlowCleanup): FlowCleanup {
|
|
1285
|
-
if (typeof cleanup !== 'function') {
|
|
1286
|
-
throw new Error('Flow.addCleanup: cleanup expects a function.');
|
|
1287
|
-
}
|
|
1288
|
-
if (this.runtime.destroyed) {
|
|
1289
|
-
cleanup();
|
|
1290
|
-
return () => {};
|
|
1291
|
-
}
|
|
1292
|
-
this.cleanupTasks.add(cleanup);
|
|
1293
|
-
return () => this.cleanupTasks.delete(cleanup);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
private assertActive(method: string): void {
|
|
1297
|
-
if (this.runtime.destroyed) {
|
|
1298
|
-
throw new Error(`Flow.${method}: instance has been destroyed.`);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
private publicStep(step: FlowStep | null | undefined): PublicFlowStep | null {
|
|
1303
|
-
if (!step) return null;
|
|
1304
|
-
const {
|
|
1305
|
-
onEnter: _onEnter,
|
|
1306
|
-
onLeave: _onLeave,
|
|
1307
|
-
onNext: _onNext,
|
|
1308
|
-
onBack: _onBack,
|
|
1309
|
-
canEnter: _canEnter,
|
|
1310
|
-
canLeave: _canLeave,
|
|
1311
|
-
...publicStep
|
|
1312
|
-
} = step;
|
|
1313
|
-
return publicStep;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
private buildRoot(): HTMLElement {
|
|
1317
|
-
return jsx('div', {
|
|
1318
|
-
className: this.props.className.root,
|
|
1319
|
-
id: this.props.id,
|
|
1320
|
-
role: 'group',
|
|
1321
|
-
'data-flow': 'root',
|
|
1322
|
-
'aria-busy': () => (this.state.loading ? 'true' : 'false'),
|
|
1323
|
-
}) as HTMLElement;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
private mountView(): void {
|
|
1327
|
-
if (this.renderDispose || !this.dom.root) return;
|
|
1328
|
-
this.renderDispose = createRoot((dispose: FlowCleanup) => {
|
|
1329
|
-
const viewDispose = render(
|
|
1330
|
-
() => this.view(),
|
|
1331
|
-
this.dom.root as HTMLElement
|
|
1332
|
-
);
|
|
1333
|
-
onCleanup(viewDispose);
|
|
1334
|
-
return dispose;
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
private view(): RenderableContent<FlowRenderContext> {
|
|
1339
|
-
const snapshot = this.snapshot();
|
|
1340
|
-
return [
|
|
1341
|
-
this.headerView(snapshot),
|
|
1342
|
-
this.bodyView(snapshot),
|
|
1343
|
-
this.footerView(snapshot),
|
|
1344
|
-
];
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
private renderSlot(
|
|
869
|
+
};
|
|
870
|
+
const renderSlot = (
|
|
1348
871
|
name: FlowSlotName,
|
|
1349
|
-
snapshot: FlowSnapshot,
|
|
1350
872
|
fallback: () => RenderableContent<FlowRenderContext>
|
|
1351
|
-
): RenderableContent<FlowRenderContext> {
|
|
1352
|
-
const slot =
|
|
873
|
+
): RenderableContent<FlowRenderContext> => {
|
|
874
|
+
const slot = props[name];
|
|
1353
875
|
if (slot === false) return null;
|
|
1354
|
-
if (typeof slot
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
private createRenderContext(
|
|
1361
|
-
snapshot: FlowSnapshot,
|
|
1362
|
-
fallback: () => RenderableContent<FlowRenderContext>
|
|
1363
|
-
): FlowRenderContext {
|
|
1364
|
-
return {
|
|
1365
|
-
flow: this,
|
|
1366
|
-
snapshot,
|
|
1367
|
-
state: this.state,
|
|
1368
|
-
steps: this.steps.map((step) => this.publicStep(step)),
|
|
1369
|
-
currentStep: snapshot.currentStep,
|
|
1370
|
-
currentData: snapshot.currentData,
|
|
1371
|
-
data: snapshot.data,
|
|
1372
|
-
fallback,
|
|
1373
|
-
next: (payload) => this.next(payload),
|
|
1374
|
-
back: (payload) => this.back(payload),
|
|
1375
|
-
goTo: (target, payload, options) => this.goTo(target, payload, options),
|
|
1376
|
-
reset: () => this.reset(),
|
|
876
|
+
if (typeof slot !== 'function') return fallback();
|
|
877
|
+
return () => {
|
|
878
|
+
const version = state.version;
|
|
879
|
+
return version >= 0 ? slot(createRenderContext(fallback)) : null;
|
|
1377
880
|
};
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
return
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
'data-flow-step': step.id,
|
|
1403
|
-
'data-flow-step-index': String(index),
|
|
1404
|
-
'aria-current': index === snapshot.currentIndex ? 'step' : undefined,
|
|
881
|
+
};
|
|
882
|
+
const contentView = (): RenderableContent<FlowRenderContext> => {
|
|
883
|
+
const content = currentStep().content;
|
|
884
|
+
return typeof content === 'function'
|
|
885
|
+
? (content(createContext()) as RenderableContent<FlowRenderContext>)
|
|
886
|
+
: ((content ?? '') as RenderableContent<FlowRenderContext>);
|
|
887
|
+
};
|
|
888
|
+
const buildUi = (): FunctionalComponent<
|
|
889
|
+
Record<string, unknown>,
|
|
890
|
+
FlowState,
|
|
891
|
+
HTMLElement,
|
|
892
|
+
object
|
|
893
|
+
> =>
|
|
894
|
+
defineComponent({
|
|
895
|
+
name: 'FlowView',
|
|
896
|
+
props: {},
|
|
897
|
+
state,
|
|
898
|
+
view: () =>
|
|
899
|
+
jsx('div', {
|
|
900
|
+
className: props.className.root,
|
|
901
|
+
id: props.id,
|
|
902
|
+
role: 'group',
|
|
903
|
+
'data-flow': 'root',
|
|
904
|
+
'aria-busy': () => (state.loading ? 'true' : 'false'),
|
|
1405
905
|
children: [
|
|
1406
|
-
jsx('
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
:
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
906
|
+
jsx('div', {
|
|
907
|
+
className: props.className.header,
|
|
908
|
+
'data-flow-header': '',
|
|
909
|
+
children: renderSlot('renderHeader', () =>
|
|
910
|
+
jsx('ol', {
|
|
911
|
+
className: props.className.steps,
|
|
912
|
+
'data-flow-steps': '',
|
|
913
|
+
role: 'list',
|
|
914
|
+
'aria-label': 'Flow steps',
|
|
915
|
+
children: steps.map((step, index) =>
|
|
916
|
+
jsx('li', {
|
|
917
|
+
className: () =>
|
|
918
|
+
[
|
|
919
|
+
props.className.step,
|
|
920
|
+
index === state.currentIndex
|
|
921
|
+
? props.className.active
|
|
922
|
+
: '',
|
|
923
|
+
index < state.currentIndex
|
|
924
|
+
? props.className.complete
|
|
925
|
+
: '',
|
|
926
|
+
]
|
|
927
|
+
.filter(Boolean)
|
|
928
|
+
.join(' '),
|
|
929
|
+
'data-flow-step': step.id,
|
|
930
|
+
'data-flow-step-index': String(index),
|
|
931
|
+
'aria-current': () =>
|
|
932
|
+
index === state.currentIndex ? 'step' : null,
|
|
933
|
+
children: jsx('button', {
|
|
934
|
+
type: 'button',
|
|
935
|
+
className: props.className.stepButton,
|
|
936
|
+
disabled: () =>
|
|
937
|
+
props.linear && index > state.currentIndex,
|
|
938
|
+
'aria-current': () =>
|
|
939
|
+
index === state.currentIndex ? 'step' : null,
|
|
940
|
+
onClick: () => {
|
|
941
|
+
if (index !== state.currentIndex)
|
|
942
|
+
void flow.goTo(index);
|
|
943
|
+
},
|
|
944
|
+
children: [
|
|
945
|
+
jsx('span', {
|
|
946
|
+
className: props.className.stepIndex,
|
|
947
|
+
'data-flow-step-number': '',
|
|
948
|
+
children: index + 1,
|
|
949
|
+
}),
|
|
950
|
+
jsx('span', {
|
|
951
|
+
className: props.className.stepTitle,
|
|
952
|
+
'data-flow-step-title': '',
|
|
953
|
+
children: step.title || step.id,
|
|
954
|
+
}),
|
|
955
|
+
],
|
|
956
|
+
}),
|
|
957
|
+
})
|
|
958
|
+
),
|
|
959
|
+
})
|
|
960
|
+
),
|
|
961
|
+
}),
|
|
962
|
+
jsx('div', {
|
|
963
|
+
className: props.className.body,
|
|
964
|
+
'data-flow-body': '',
|
|
965
|
+
role: 'region',
|
|
966
|
+
'aria-live': 'polite',
|
|
967
|
+
'aria-busy': () => (state.loading ? 'true' : 'false'),
|
|
968
|
+
children: renderSlot('renderBody', () => () => {
|
|
969
|
+
const version = state.version;
|
|
970
|
+
return version >= 0 ? contentView() : null;
|
|
971
|
+
}),
|
|
972
|
+
}),
|
|
973
|
+
jsx('div', {
|
|
974
|
+
className: props.className.footer,
|
|
975
|
+
'data-flow-footer': '',
|
|
976
|
+
children: renderSlot('renderFooter', () => [
|
|
977
|
+
props.showReset
|
|
978
|
+
? jsx('button', {
|
|
979
|
+
type: 'button',
|
|
980
|
+
className: [props.className.button, props.className.reset]
|
|
981
|
+
.filter(Boolean)
|
|
982
|
+
.join(' '),
|
|
983
|
+
'data-action': 'reset',
|
|
984
|
+
onClick: () => flow.reset(),
|
|
985
|
+
disabled: () => state.loading,
|
|
986
|
+
children: props.text.reset,
|
|
987
|
+
})
|
|
988
|
+
: null,
|
|
989
|
+
props.showBack
|
|
990
|
+
? jsx('button', {
|
|
991
|
+
type: 'button',
|
|
992
|
+
className: [props.className.button, props.className.back]
|
|
993
|
+
.filter(Boolean)
|
|
994
|
+
.join(' '),
|
|
995
|
+
'data-action': 'back',
|
|
996
|
+
onClick: () => void flow.back(),
|
|
997
|
+
disabled: () => !flow.canBack,
|
|
998
|
+
children: props.text.back,
|
|
999
|
+
})
|
|
1000
|
+
: null,
|
|
1001
|
+
props.showNext
|
|
1002
|
+
? jsx('button', {
|
|
1003
|
+
type: 'button',
|
|
1004
|
+
className: [props.className.button, props.className.next]
|
|
1005
|
+
.filter(Boolean)
|
|
1006
|
+
.join(' '),
|
|
1007
|
+
'data-action': 'next',
|
|
1008
|
+
onClick: () => void flow.next(),
|
|
1009
|
+
disabled: () => state.loading,
|
|
1010
|
+
children: () =>
|
|
1011
|
+
flow.isLast ? props.text.finish : props.text.next,
|
|
1012
|
+
})
|
|
1013
|
+
: null,
|
|
1014
|
+
]),
|
|
1434
1015
|
}),
|
|
1435
1016
|
],
|
|
1436
|
-
})
|
|
1437
|
-
|
|
1438
|
-
}) as HTMLOListElement;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
private bodyView(snapshot: FlowSnapshot): HTMLElement {
|
|
1442
|
-
return jsx('div', {
|
|
1443
|
-
className: this.props.className.body,
|
|
1444
|
-
'data-flow-body': '',
|
|
1445
|
-
role: 'region',
|
|
1446
|
-
'aria-live': 'polite',
|
|
1447
|
-
'aria-busy': snapshot.loading ? 'true' : 'false',
|
|
1448
|
-
ref: (element: HTMLElement) => {
|
|
1449
|
-
this.dom.body = element;
|
|
1450
|
-
},
|
|
1451
|
-
children: this.renderSlot('renderBody', snapshot, () =>
|
|
1452
|
-
this.contentView(this.currentStep.content)
|
|
1453
|
-
),
|
|
1454
|
-
}) as HTMLElement;
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
private footerView(snapshot: FlowSnapshot): HTMLElement {
|
|
1458
|
-
return jsx('div', {
|
|
1459
|
-
className: this.props.className.footer,
|
|
1460
|
-
'data-flow-footer': '',
|
|
1461
|
-
ref: (element: HTMLElement) => {
|
|
1462
|
-
this.dom.footer = element;
|
|
1463
|
-
},
|
|
1464
|
-
children: this.renderSlot('renderFooter', snapshot, () => [
|
|
1465
|
-
this.props.showReset
|
|
1466
|
-
? jsx('button', {
|
|
1467
|
-
type: 'button',
|
|
1468
|
-
className: joinClasses(
|
|
1469
|
-
this.props.className.button,
|
|
1470
|
-
this.props.className.reset
|
|
1471
|
-
),
|
|
1472
|
-
'data-action': 'reset',
|
|
1473
|
-
onClick: () => this.reset(),
|
|
1474
|
-
disabled: snapshot.loading,
|
|
1475
|
-
'aria-disabled': snapshot.loading ? 'true' : 'false',
|
|
1476
|
-
children: this.props.text.reset,
|
|
1477
|
-
})
|
|
1478
|
-
: null,
|
|
1479
|
-
this.props.showBack
|
|
1480
|
-
? jsx('button', {
|
|
1481
|
-
type: 'button',
|
|
1482
|
-
className: joinClasses(
|
|
1483
|
-
this.props.className.button,
|
|
1484
|
-
this.props.className.back
|
|
1485
|
-
),
|
|
1486
|
-
'data-action': 'back',
|
|
1487
|
-
onClick: () => void this.back(),
|
|
1488
|
-
disabled: !snapshot.canBack,
|
|
1489
|
-
'aria-disabled': !snapshot.canBack ? 'true' : 'false',
|
|
1490
|
-
children: this.props.text.back,
|
|
1491
|
-
})
|
|
1492
|
-
: null,
|
|
1493
|
-
this.props.showNext
|
|
1494
|
-
? jsx('button', {
|
|
1495
|
-
type: 'button',
|
|
1496
|
-
className: joinClasses(
|
|
1497
|
-
this.props.className.button,
|
|
1498
|
-
this.props.className.next
|
|
1499
|
-
),
|
|
1500
|
-
'data-action': 'next',
|
|
1501
|
-
onClick: () => void this.next(),
|
|
1502
|
-
disabled: snapshot.loading,
|
|
1503
|
-
'aria-disabled': snapshot.loading ? 'true' : 'false',
|
|
1504
|
-
children: snapshot.isLast
|
|
1505
|
-
? this.props.text.finish
|
|
1506
|
-
: this.props.text.next,
|
|
1507
|
-
})
|
|
1508
|
-
: null,
|
|
1509
|
-
]),
|
|
1510
|
-
}) as HTMLElement;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
private contentView(
|
|
1514
|
-
content: FlowStep['content']
|
|
1515
|
-
): RenderableContent<FlowRenderContext> {
|
|
1516
|
-
if (typeof content === 'function') {
|
|
1517
|
-
return content(
|
|
1518
|
-
this.createContext()
|
|
1519
|
-
) as RenderableContent<FlowRenderContext>;
|
|
1520
|
-
}
|
|
1521
|
-
return (content ?? '') as RenderableContent<FlowRenderContext>;
|
|
1522
|
-
}
|
|
1017
|
+
}) as HTMLElement,
|
|
1018
|
+
});
|
|
1523
1019
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
)
|
|
1533
|
-
|
|
1534
|
-
}
|
|
1020
|
+
flow = {
|
|
1021
|
+
props,
|
|
1022
|
+
steps,
|
|
1023
|
+
state,
|
|
1024
|
+
runtime,
|
|
1025
|
+
get element() {
|
|
1026
|
+
return ui?.element || null;
|
|
1027
|
+
},
|
|
1028
|
+
get currentStep() {
|
|
1029
|
+
return currentStep();
|
|
1030
|
+
},
|
|
1031
|
+
get currentData() {
|
|
1032
|
+
return getStepData(state.currentId);
|
|
1033
|
+
},
|
|
1034
|
+
get canBack() {
|
|
1035
|
+
return state.currentIndex > 0 && !state.loading;
|
|
1036
|
+
},
|
|
1037
|
+
get canNext() {
|
|
1038
|
+
return state.currentIndex < steps.length - 1 && !state.loading;
|
|
1039
|
+
},
|
|
1040
|
+
get isLast() {
|
|
1041
|
+
return state.currentIndex === steps.length - 1;
|
|
1042
|
+
},
|
|
1043
|
+
build() {
|
|
1044
|
+
assertActive('build');
|
|
1045
|
+
if (runtime.built) return flow;
|
|
1046
|
+
if (props.render) {
|
|
1047
|
+
ui ||= buildUi();
|
|
1048
|
+
ui.build();
|
|
1049
|
+
}
|
|
1050
|
+
runtime.built = true;
|
|
1051
|
+
return flow;
|
|
1052
|
+
},
|
|
1053
|
+
mount(container) {
|
|
1054
|
+
assertActive('mount');
|
|
1055
|
+
flow.build();
|
|
1056
|
+
if (ui) ui.mount(container);
|
|
1057
|
+
runtime.mounted = !!ui;
|
|
1058
|
+
return flow;
|
|
1059
|
+
},
|
|
1060
|
+
unmount() {
|
|
1061
|
+
assertActive('unmount');
|
|
1062
|
+
ui?.unmount();
|
|
1063
|
+
runtime.mounted = false;
|
|
1064
|
+
return flow;
|
|
1065
|
+
},
|
|
1066
|
+
subscribe(handler) {
|
|
1067
|
+
assertActive('subscribe');
|
|
1068
|
+
if (typeof handler !== 'function') {
|
|
1069
|
+
throw new Error('Flow.subscribe: handler expects a function.');
|
|
1070
|
+
}
|
|
1071
|
+
subscribers.add(handler);
|
|
1072
|
+
handler(snapshot(), flow, null);
|
|
1073
|
+
return () => subscribers.delete(handler);
|
|
1074
|
+
},
|
|
1075
|
+
snapshot,
|
|
1076
|
+
async next(payload = null) {
|
|
1077
|
+
assertActive('next');
|
|
1078
|
+
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.next');
|
|
1079
|
+
const busy = handleBusy('next');
|
|
1080
|
+
if (busy) return busy;
|
|
1081
|
+
return runAction('next', async () => {
|
|
1082
|
+
if (flow.isLast) {
|
|
1083
|
+
if (payload) setStepData(state.currentId, payload);
|
|
1084
|
+
await callHook(props.onFinish, [snapshot(), flow]);
|
|
1085
|
+
return snapshot();
|
|
1086
|
+
}
|
|
1087
|
+
const from = currentStep();
|
|
1088
|
+
const fallback = steps[state.currentIndex + 1]?.id;
|
|
1089
|
+
const result = await runMoveHook('next', from, payload, fallback);
|
|
1090
|
+
const target = normalizeStepResult(result, fallback);
|
|
1091
|
+
return flow.goTo(target.id, target.data ?? payload, {
|
|
1092
|
+
direction: 'next',
|
|
1093
|
+
internal: true,
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
},
|
|
1097
|
+
async back(payload = null) {
|
|
1098
|
+
assertActive('back');
|
|
1099
|
+
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.back');
|
|
1100
|
+
const busy = handleBusy('back');
|
|
1101
|
+
if (busy) return busy;
|
|
1102
|
+
if (!flow.canBack) return snapshot();
|
|
1103
|
+
return runAction('back', async () => {
|
|
1104
|
+
const from = currentStep();
|
|
1105
|
+
const fallback = steps[state.currentIndex - 1]?.id;
|
|
1106
|
+
const result = await runMoveHook('back', from, payload, fallback);
|
|
1107
|
+
const target = normalizeStepResult(result, fallback);
|
|
1108
|
+
return flow.goTo(target.id, target.data ?? payload, {
|
|
1109
|
+
direction: 'back',
|
|
1110
|
+
internal: true,
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
},
|
|
1114
|
+
async goTo(target, payload = null, options = {}) {
|
|
1115
|
+
assertActive('goTo');
|
|
1116
|
+
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.goTo');
|
|
1117
|
+
const busy = options.internal ? null : handleBusy('goTo');
|
|
1118
|
+
if (busy) return busy;
|
|
1119
|
+
return runAction(
|
|
1120
|
+
'goTo',
|
|
1121
|
+
async () => {
|
|
1122
|
+
const index = resolveStepIndex(target);
|
|
1123
|
+
const to = steps[index];
|
|
1124
|
+
const from = currentStep();
|
|
1125
|
+
if (to.id === state.currentId) {
|
|
1126
|
+
if (payload) setStepData(state.currentId, payload);
|
|
1127
|
+
return snapshot();
|
|
1128
|
+
}
|
|
1129
|
+
await transitionTo(to, options.direction || 'go', payload, from);
|
|
1130
|
+
return snapshot();
|
|
1131
|
+
},
|
|
1132
|
+
!!options.internal
|
|
1133
|
+
);
|
|
1134
|
+
},
|
|
1135
|
+
setData,
|
|
1136
|
+
setStepData,
|
|
1137
|
+
getStepData,
|
|
1138
|
+
reset() {
|
|
1139
|
+
assertActive('reset');
|
|
1140
|
+
flushSync(() => {
|
|
1141
|
+
state.currentId = initialStepId;
|
|
1142
|
+
state.currentIndex = stepMap.get(initialStepId) ?? 0;
|
|
1143
|
+
state.previousId = null;
|
|
1144
|
+
state.previousIndex = null;
|
|
1145
|
+
state.direction = null;
|
|
1146
|
+
state.history.splice(0, state.history.length, initialStepId);
|
|
1147
|
+
replaceObject(state.data, clonePlainObject(initialGlobal));
|
|
1148
|
+
replaceObject(
|
|
1149
|
+
state.stepData,
|
|
1150
|
+
clonePlainObject(initialStepData) as Record<string, FlowData>
|
|
1151
|
+
);
|
|
1152
|
+
state.loading = false;
|
|
1153
|
+
state.busyAction = null;
|
|
1154
|
+
state.error = null;
|
|
1155
|
+
state.version += 1;
|
|
1156
|
+
});
|
|
1157
|
+
emitChange();
|
|
1158
|
+
return flow;
|
|
1159
|
+
},
|
|
1160
|
+
async finish(payload = null) {
|
|
1161
|
+
assertActive('finish');
|
|
1162
|
+
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.finish');
|
|
1163
|
+
const busy = handleBusy('finish');
|
|
1164
|
+
if (busy) return busy;
|
|
1165
|
+
return runAction('finish', async () => {
|
|
1166
|
+
if (payload) setStepData(state.currentId, payload);
|
|
1167
|
+
await callHook(props.onFinish, [snapshot(), flow]);
|
|
1168
|
+
return snapshot();
|
|
1169
|
+
});
|
|
1170
|
+
},
|
|
1171
|
+
destroy() {
|
|
1172
|
+
if (runtime.destroyed) return;
|
|
1173
|
+
runtime.destroyed = true;
|
|
1174
|
+
runtime.actionController?.abort();
|
|
1175
|
+
runtime.actionController = null;
|
|
1176
|
+
runtime.activeAction = null;
|
|
1177
|
+
ui?.destroy();
|
|
1178
|
+
ui = null;
|
|
1179
|
+
for (const cleanup of cleanupTasks) cleanup();
|
|
1180
|
+
cleanupTasks.clear();
|
|
1181
|
+
subscribers.clear();
|
|
1182
|
+
runtime.built = false;
|
|
1183
|
+
runtime.mounted = false;
|
|
1184
|
+
stepMap.clear();
|
|
1185
|
+
},
|
|
1186
|
+
};
|
|
1535
1187
|
|
|
1536
|
-
|
|
1537
|
-
* 创建 Flow 实例。
|
|
1538
|
-
* @param {FlowProps} props Flow 配置。
|
|
1539
|
-
* @returns {Flow}
|
|
1540
|
-
*/
|
|
1541
|
-
export function createFlow(props: FlowProps = {}): Flow {
|
|
1542
|
-
return new Flow(props);
|
|
1188
|
+
return flow;
|
|
1543
1189
|
}
|