vanilla-jui 1.3.11 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2927 -0
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -3
- package/src/components/accordion.ts +522 -0
- package/src/components/drop.ts +497 -0
- package/src/components/flow.ts +1579 -0
- package/src/components/form.ts +953 -0
- package/src/components/{icons.js → icons.ts} +24 -11
- package/src/components/menu.ts +615 -0
- package/src/components/{modal.js → modal.ts} +560 -237
- package/src/components/offcanvas.ts +496 -0
- package/src/components/{pagination.js → pagination.ts} +251 -76
- package/src/components/parabola.ts +335 -0
- package/src/components/{sticky.js → sticky.ts} +123 -98
- package/src/components/{swiper.js → swiper.ts} +510 -232
- package/src/components/tabs.ts +984 -0
- package/src/components/theme.ts +437 -0
- package/src/components/toast.ts +371 -0
- package/src/components/toc.ts +385 -0
- package/src/components/tooltip.ts +179 -0
- package/src/components/validator.ts +596 -0
- package/src/core/{Component.js → Component.ts} +134 -27
- package/src/css/animation.css +31 -1
- package/src/css/card.css +1 -1
- package/src/css/color.css +863 -1013
- package/src/css/color_p3.css +822 -975
- package/src/css/components.css +1 -0
- package/src/css/form.css +1 -1
- package/src/css/icon.css +17 -1
- package/src/css/index.css +2 -2
- package/src/css/menu.css +1 -1
- package/src/css/tabs.css +1 -0
- package/src/css/tokens.css +1 -1
- package/src/index.ts +37 -0
- package/src/locales/{index.js → index.ts} +1 -2
- package/src/types.d.ts +1 -0
- package/src/utilities/browser.ts +131 -0
- package/src/utilities/cache.ts +57 -0
- package/src/utilities/core.ts +614 -0
- package/src/utilities/dom.ts +468 -0
- package/src/utilities/events.ts +106 -0
- package/src/utilities/http.ts +35 -0
- package/src/utilities/storage.ts +46 -0
- package/src/components/accordion.js +0 -373
- package/src/components/drop.js +0 -437
- package/src/components/flow.js +0 -1133
- package/src/components/form.js +0 -585
- package/src/components/menu.js +0 -422
- package/src/components/offcanvas.js +0 -332
- package/src/components/parabola.js +0 -295
- package/src/components/tabs.js +0 -746
- package/src/components/theme.js +0 -353
- package/src/components/toast.js +0 -329
- package/src/components/toc.js +0 -226
- package/src/components/tooltip.js +0 -86
- package/src/components/validator.js +0 -586
- package/src/index.js +0 -45
- package/src/locales/index.min.js +0 -1
- package/src/utilities/browser.js +0 -111
- package/src/utilities/cache.js +0 -159
- package/src/utilities/core.js +0 -502
- package/src/utilities/debug.js +0 -103
- package/src/utilities/dom.js +0 -313
- package/src/utilities/events.js +0 -69
- package/src/utilities/http.js +0 -30
- package/src/utilities/legacy-dom.js +0 -179
- package/src/utilities/storage.js +0 -46
- /package/src/{components/components.js → css/themes/index.css} +0 -0
- /package/src/icons/{index.js → index.ts} +0 -0
package/src/components/flow.js
DELETED
|
@@ -1,1133 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createDeepStore,
|
|
3
|
-
createRoot,
|
|
4
|
-
flushSync,
|
|
5
|
-
jsx,
|
|
6
|
-
onCleanup,
|
|
7
|
-
render,
|
|
8
|
-
} from 'vanilla-signal';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
isPlainObject,
|
|
12
|
-
randomId,
|
|
13
|
-
resolveProps,
|
|
14
|
-
validateParam,
|
|
15
|
-
} from '../utilities/core.js';
|
|
16
|
-
import { requireRenderDOM, resolveElement } from '../utilities/dom.js';
|
|
17
|
-
|
|
18
|
-
function isFlowRenderContent(content) {
|
|
19
|
-
return (
|
|
20
|
-
content == null ||
|
|
21
|
-
typeof content === 'string' ||
|
|
22
|
-
typeof content === 'function' ||
|
|
23
|
-
Array.isArray(content) ||
|
|
24
|
-
(typeof Node !== 'undefined' && content instanceof Node)
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isFlowStep(step) {
|
|
29
|
-
return !!step && typeof step === 'object' && typeof step.id === 'string';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isFlowRenderSlot(slot) {
|
|
33
|
-
return slot == null || slot === false || typeof slot === 'function';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function clonePlainObject(value) {
|
|
37
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
38
|
-
? { ...value }
|
|
39
|
-
: {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function cloneArray(value) {
|
|
43
|
-
return Array.isArray(value) ? [...value] : [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function cloneSteps(steps) {
|
|
47
|
-
if (!Array.isArray(steps)) return [];
|
|
48
|
-
return steps.map((step) => ({
|
|
49
|
-
...step,
|
|
50
|
-
data: clonePlainObject(step.data),
|
|
51
|
-
modal: clonePlainObject(step.modal),
|
|
52
|
-
view: clonePlainObject(step.view),
|
|
53
|
-
}));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeStepResult(result, fallbackId) {
|
|
57
|
-
if (typeof result === 'string') return { id: result };
|
|
58
|
-
if (result && typeof result === 'object') return result;
|
|
59
|
-
return { id: fallbackId };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const FLOW_STEP_RULE = {
|
|
63
|
-
validate: isFlowStep,
|
|
64
|
-
message: 'expects a step object with a string id.',
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const FLOW_CONTENT_RULE = {
|
|
68
|
-
validate: isFlowRenderContent,
|
|
69
|
-
message: 'expects string, Node, array, function or null.',
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const FLOW_STEPS_RULE = {
|
|
73
|
-
validate: (value) => Array.isArray(value) && value.length > 0,
|
|
74
|
-
message: 'expects a non-empty steps array.',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const FLOW_PAYLOAD_RULE = {
|
|
78
|
-
validate: (value) =>
|
|
79
|
-
value == null || (typeof value === 'object' && !Array.isArray(value)),
|
|
80
|
-
message: 'expects an object or null.',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const FLOW_RENDER_SLOT_RULE = {
|
|
84
|
-
validate: isFlowRenderSlot,
|
|
85
|
-
message: 'expects function, false or null.',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const FLOW_TEXT_RULE = {
|
|
89
|
-
default: {},
|
|
90
|
-
validate: (value) => isPlainObject(value),
|
|
91
|
-
message: 'expects an object with text fields.',
|
|
92
|
-
normalize: (value) => {
|
|
93
|
-
const text = isPlainObject(value) ? value : {};
|
|
94
|
-
return {
|
|
95
|
-
...text,
|
|
96
|
-
back: typeof text.back === 'string' ? text.back : 'Back',
|
|
97
|
-
next: typeof text.next === 'string' ? text.next : 'Next',
|
|
98
|
-
finish: typeof text.finish === 'string' ? text.finish : 'Finish',
|
|
99
|
-
reset: typeof text.reset === 'string' ? text.reset : 'Reset',
|
|
100
|
-
};
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const FLOW_OPTIONS_SCHEMA = {
|
|
105
|
-
id: {
|
|
106
|
-
default: null,
|
|
107
|
-
types: ['string', 'null'],
|
|
108
|
-
normalize: (value) => {
|
|
109
|
-
if (typeof value === 'string')
|
|
110
|
-
return value.trim() ? value.trim() : randomId();
|
|
111
|
-
if (value == null) return randomId();
|
|
112
|
-
return value;
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
steps: { default: [], ...FLOW_STEPS_RULE },
|
|
116
|
-
initial: { default: null, types: ['string', 'number', 'null'] },
|
|
117
|
-
cache: { default: true, type: 'boolean' },
|
|
118
|
-
linear: { default: true, type: 'boolean' },
|
|
119
|
-
render: { default: true, type: 'boolean' },
|
|
120
|
-
rollbackOnError: { default: true, type: 'boolean' },
|
|
121
|
-
busyStrategy: {
|
|
122
|
-
default: 'ignore',
|
|
123
|
-
type: 'string',
|
|
124
|
-
enum: ['ignore', 'throw'],
|
|
125
|
-
},
|
|
126
|
-
showHeader: { default: true, type: 'boolean' },
|
|
127
|
-
showFooter: { default: true, type: 'boolean' },
|
|
128
|
-
showSteps: { default: true, type: 'boolean' },
|
|
129
|
-
showBack: { default: true, type: 'boolean' },
|
|
130
|
-
showNext: { default: true, type: 'boolean' },
|
|
131
|
-
showReset: { default: false, type: 'boolean' },
|
|
132
|
-
text: FLOW_TEXT_RULE,
|
|
133
|
-
className: { default: '', type: 'string' },
|
|
134
|
-
renderHeader: { default: null, ...FLOW_RENDER_SLOT_RULE },
|
|
135
|
-
renderSteps: { default: null, ...FLOW_RENDER_SLOT_RULE },
|
|
136
|
-
renderBody: { default: null, ...FLOW_RENDER_SLOT_RULE },
|
|
137
|
-
renderFooter: { default: null, ...FLOW_RENDER_SLOT_RULE },
|
|
138
|
-
onChange: { default: null, types: ['function', 'null'] },
|
|
139
|
-
onNext: { default: null, types: ['function', 'null'] },
|
|
140
|
-
onBack: { default: null, types: ['function', 'null'] },
|
|
141
|
-
onFinish: { default: null, types: ['function', 'null'] },
|
|
142
|
-
onError: { default: null, types: ['function', 'null'] },
|
|
143
|
-
onBusy: { default: null, types: ['function', 'null'] },
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* @typedef {object} FlowStep
|
|
148
|
-
* @property {string} id 步骤 id。
|
|
149
|
-
* @property {string} [title] 步骤标题。
|
|
150
|
-
* @property {string} [description] 步骤描述。
|
|
151
|
-
* @property {string|Node|Node[]|Function|null} [content] 默认 UI 内容。
|
|
152
|
-
* @property {object} [data] 步骤初始缓存数据。
|
|
153
|
-
* @property {object|Function|null} [modal] 供 Modal 消费的显式 UI 配置;可返回 Partial<ModalOptions>。
|
|
154
|
-
* @property {object} [view] 供外部组件消费的视图配置,如 modal 配置。
|
|
155
|
-
* @property {Function|null} [onEnter] 进入步骤时触发。
|
|
156
|
-
* @property {Function|null} [onLeave] 离开步骤前触发。
|
|
157
|
-
* @property {Function|null} [onNext] 当前步骤 next 时触发。
|
|
158
|
-
* @property {Function|null} [onBack] 当前步骤 back 时触发。
|
|
159
|
-
* @property {Function|null} [canEnter] 是否允许进入步骤。
|
|
160
|
-
* @property {Function|null} [canLeave] 是否允许离开步骤。
|
|
161
|
-
*/
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @typedef {object} FlowOptions
|
|
165
|
-
* @property {string|null} [id] Flow 根 id。
|
|
166
|
-
* @property {FlowStep[]} steps 步骤列表。
|
|
167
|
-
* @property {string|number|null} [initial] 初始步骤 id 或索引。
|
|
168
|
-
* @property {boolean} [cache=true] 是否缓存每一步的数据。
|
|
169
|
-
* @property {boolean} [linear=true] 是否按步骤顺序限制 next/back。
|
|
170
|
-
* @property {boolean} [render=true] 是否启用默认 UI 渲染。
|
|
171
|
-
* @property {boolean} [rollbackOnError=true] transition 失败时是否回滚状态。
|
|
172
|
-
* @property {"ignore"|"throw"} [busyStrategy="ignore"] loading 中重复触发动作时的处理策略。
|
|
173
|
-
* @property {boolean} [showHeader=true] 是否显示头部。
|
|
174
|
-
* @property {boolean} [showFooter=true] 是否显示底部。
|
|
175
|
-
* @property {boolean} [showSteps=true] 是否显示步骤条。
|
|
176
|
-
* @property {boolean} [showBack=true] 是否显示返回按钮。
|
|
177
|
-
* @property {boolean} [showNext=true] 是否显示下一步按钮。
|
|
178
|
-
* @property {boolean} [showReset=false] 是否显示重置按钮。
|
|
179
|
-
* @property {object} [text={}] 文案配置,支持 `back`、`next`、`finish`、`reset`。
|
|
180
|
-
* @property {string} [className=""] 根节点附加类名。
|
|
181
|
-
* @property {Function|null|false} [renderHeader] 自定义头部渲染。
|
|
182
|
-
* @property {Function|null|false} [renderSteps] 自定义步骤条渲染。
|
|
183
|
-
* @property {Function|null|false} [renderBody] 自定义内容区渲染。
|
|
184
|
-
* @property {Function|null|false} [renderFooter] 自定义底部渲染。
|
|
185
|
-
* @property {Function|null} [onChange] 步骤切换后触发。
|
|
186
|
-
* @property {Function|null} [onNext] 全局 next 时触发。
|
|
187
|
-
* @property {Function|null} [onBack] 全局 back 时触发。
|
|
188
|
-
* @property {Function|null} [onFinish] 完成时触发。
|
|
189
|
-
* @property {Function|null} [onError] 生命周期错误时触发。
|
|
190
|
-
* @property {Function|null} [onBusy] 重复触发动作时触发。
|
|
191
|
-
*/
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Headless 流程控制器,带可选默认 UI。
|
|
195
|
-
*
|
|
196
|
-
* 适合在 Modal、Offcanvas、页面表单或任意业务组件中复用 next/back/goTo、步骤缓存和生命周期。
|
|
197
|
-
*/
|
|
198
|
-
export class Flow {
|
|
199
|
-
/**
|
|
200
|
-
* 创建 Flow 实例。
|
|
201
|
-
* @param {FlowOptions} [options={}] Flow 配置。
|
|
202
|
-
*/
|
|
203
|
-
constructor(options = {}) {
|
|
204
|
-
this.options = resolveProps(options, FLOW_OPTIONS_SCHEMA, 'Flow.options');
|
|
205
|
-
this._init(this.options);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
_init(options) {
|
|
209
|
-
this.steps = cloneSteps(options.steps);
|
|
210
|
-
this._validateSteps(this.steps);
|
|
211
|
-
this._stepMap = new Map(this.steps.map((step, index) => [step.id, index]));
|
|
212
|
-
this._initialStepId = this._resolveInitialStepId(options.initial);
|
|
213
|
-
this._initialData = this._createInitialStepData();
|
|
214
|
-
this._subscribers = new Set();
|
|
215
|
-
this._renderDispose = null;
|
|
216
|
-
this._cleanupTasks = new Set();
|
|
217
|
-
this._nodes = {};
|
|
218
|
-
this._destroyed = false;
|
|
219
|
-
this._activeAction = null;
|
|
220
|
-
this._actionController = null;
|
|
221
|
-
this.root = null;
|
|
222
|
-
|
|
223
|
-
this.state = createDeepStore({
|
|
224
|
-
id: options.id,
|
|
225
|
-
currentId: this._initialStepId,
|
|
226
|
-
currentIndex: this._stepMap.get(this._initialStepId),
|
|
227
|
-
previousId: null,
|
|
228
|
-
previousIndex: null,
|
|
229
|
-
direction: null,
|
|
230
|
-
history: [this._initialStepId],
|
|
231
|
-
data: clonePlainObject(this._initialData.global),
|
|
232
|
-
stepData: clonePlainObject(this._initialData.stepData),
|
|
233
|
-
loading: false,
|
|
234
|
-
error: null,
|
|
235
|
-
busyAction: null,
|
|
236
|
-
version: 0,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* 当前步骤。
|
|
242
|
-
* @returns {FlowStep}
|
|
243
|
-
*/
|
|
244
|
-
get currentStep() {
|
|
245
|
-
return this.steps[this.state.currentIndex];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* 当前步骤数据。
|
|
250
|
-
* @returns {object}
|
|
251
|
-
*/
|
|
252
|
-
get currentData() {
|
|
253
|
-
return this.getStepData(this.state.currentId);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* 是否可以返回上一步。
|
|
258
|
-
* @returns {boolean}
|
|
259
|
-
*/
|
|
260
|
-
get canBack() {
|
|
261
|
-
return this.state.currentIndex > 0 && !this.state.loading;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* 是否可以前进。
|
|
266
|
-
* @returns {boolean}
|
|
267
|
-
*/
|
|
268
|
-
get canNext() {
|
|
269
|
-
return (
|
|
270
|
-
this.state.currentIndex < this.steps.length - 1 && !this.state.loading
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* 是否处于最后一步。
|
|
276
|
-
* @returns {boolean}
|
|
277
|
-
*/
|
|
278
|
-
get isLast() {
|
|
279
|
-
return this.state.currentIndex === this.steps.length - 1;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* 订阅状态变化。
|
|
284
|
-
* @param {Function} handler 订阅函数。
|
|
285
|
-
* @returns {Function} 取消订阅函数。
|
|
286
|
-
*/
|
|
287
|
-
subscribe(handler) {
|
|
288
|
-
this._assertActive('subscribe');
|
|
289
|
-
if (typeof handler !== 'function') {
|
|
290
|
-
throw new Error('Flow.subscribe: handler expects a function.');
|
|
291
|
-
}
|
|
292
|
-
this._subscribers.add(handler);
|
|
293
|
-
handler(this.snapshot(), this);
|
|
294
|
-
return () => this._subscribers.delete(handler);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* 获取不可变快照。
|
|
299
|
-
* @returns {object}
|
|
300
|
-
*/
|
|
301
|
-
snapshot() {
|
|
302
|
-
if (!this.state) return null;
|
|
303
|
-
const currentStep = this.currentStep;
|
|
304
|
-
return {
|
|
305
|
-
id: this.state.id,
|
|
306
|
-
currentId: this.state.currentId,
|
|
307
|
-
currentIndex: this.state.currentIndex,
|
|
308
|
-
previousId: this.state.previousId,
|
|
309
|
-
previousIndex: this.state.previousIndex,
|
|
310
|
-
direction: this.state.direction,
|
|
311
|
-
history: [...this.state.history],
|
|
312
|
-
data: clonePlainObject(this.state.data),
|
|
313
|
-
stepData: clonePlainObject(this.state.stepData),
|
|
314
|
-
currentData: this.currentData,
|
|
315
|
-
currentStep: this._publicStep(currentStep),
|
|
316
|
-
canBack: this.canBack,
|
|
317
|
-
canNext: this.canNext,
|
|
318
|
-
isLast: this.isLast,
|
|
319
|
-
loading: this.state.loading,
|
|
320
|
-
busyAction: this.state.busyAction,
|
|
321
|
-
error: this.state.error,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* 挂载默认 Flow UI。
|
|
327
|
-
* @param {Element|Node|string|Array} container DOM 容器、选择器或 JSX/h 返回节点。
|
|
328
|
-
* @returns {Flow}
|
|
329
|
-
*/
|
|
330
|
-
mount(container) {
|
|
331
|
-
this._assertActive('mount');
|
|
332
|
-
if (!this.options.render) return this;
|
|
333
|
-
requireRenderDOM('Flow.mount');
|
|
334
|
-
|
|
335
|
-
const target = resolveElement(container, 'Flow.mount.container');
|
|
336
|
-
|
|
337
|
-
this.unmount();
|
|
338
|
-
this.root = this._buildRoot();
|
|
339
|
-
target.appendChild(this.root);
|
|
340
|
-
this._mountView();
|
|
341
|
-
return this;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* 卸载默认 UI。
|
|
346
|
-
* @returns {Flow}
|
|
347
|
-
*/
|
|
348
|
-
unmount() {
|
|
349
|
-
this._renderDispose?.();
|
|
350
|
-
this._renderDispose = null;
|
|
351
|
-
if (this.root?.parentNode) this.root.parentNode.removeChild(this.root);
|
|
352
|
-
this.root = null;
|
|
353
|
-
this._nodes = {};
|
|
354
|
-
return this;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* 前进一步。
|
|
359
|
-
* @param {object|null} [payload=null] 当前步骤需要缓存的数据。
|
|
360
|
-
* @returns {Promise<object>} 切换后的快照。
|
|
361
|
-
*/
|
|
362
|
-
async next(payload = null) {
|
|
363
|
-
this._assertActive('next');
|
|
364
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.next');
|
|
365
|
-
const busySnapshot = this._handleBusy('next');
|
|
366
|
-
if (busySnapshot) return busySnapshot;
|
|
367
|
-
|
|
368
|
-
return this._runAction('next', async () => {
|
|
369
|
-
if (this.isLast) return this.finish(payload, { internal: true });
|
|
370
|
-
|
|
371
|
-
const fromStep = this.currentStep;
|
|
372
|
-
const fallbackId = this.steps[this.state.currentIndex + 1]?.id;
|
|
373
|
-
const result = await this._runMoveHook(
|
|
374
|
-
'next',
|
|
375
|
-
fromStep,
|
|
376
|
-
payload,
|
|
377
|
-
fallbackId
|
|
378
|
-
);
|
|
379
|
-
const { id, data } = normalizeStepResult(result, fallbackId);
|
|
380
|
-
return this.goTo(id, data ?? payload, {
|
|
381
|
-
direction: 'next',
|
|
382
|
-
internal: true,
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* 返回上一步。
|
|
389
|
-
* @param {object|null} [payload=null] 当前步骤需要缓存的数据。
|
|
390
|
-
* @returns {Promise<object>} 切换后的快照。
|
|
391
|
-
*/
|
|
392
|
-
async back(payload = null) {
|
|
393
|
-
this._assertActive('back');
|
|
394
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.back');
|
|
395
|
-
const busySnapshot = this._handleBusy('back');
|
|
396
|
-
if (busySnapshot) return busySnapshot;
|
|
397
|
-
|
|
398
|
-
if (!this.canBack) return this.snapshot();
|
|
399
|
-
|
|
400
|
-
return this._runAction('back', async () => {
|
|
401
|
-
const fromStep = this.currentStep;
|
|
402
|
-
const fallbackId = this.steps[this.state.currentIndex - 1]?.id;
|
|
403
|
-
const result = await this._runMoveHook(
|
|
404
|
-
'back',
|
|
405
|
-
fromStep,
|
|
406
|
-
payload,
|
|
407
|
-
fallbackId
|
|
408
|
-
);
|
|
409
|
-
const { id, data } = normalizeStepResult(result, fallbackId);
|
|
410
|
-
return this.goTo(id, data ?? payload, {
|
|
411
|
-
direction: 'back',
|
|
412
|
-
internal: true,
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* 跳转到指定步骤。
|
|
419
|
-
* @param {string|number} target 目标步骤 id 或索引。
|
|
420
|
-
* @param {object|null} [payload=null] 当前步骤需要缓存的数据。
|
|
421
|
-
* @param {{direction?:string, internal?:boolean}} [options={}] 跳转选项。
|
|
422
|
-
* @returns {Promise<object>} 切换后的快照。
|
|
423
|
-
*/
|
|
424
|
-
async goTo(target, payload = null, options = {}) {
|
|
425
|
-
this._assertActive('goTo');
|
|
426
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.goTo');
|
|
427
|
-
const busySnapshot = options.internal ? null : this._handleBusy('goTo');
|
|
428
|
-
if (busySnapshot) return busySnapshot;
|
|
429
|
-
|
|
430
|
-
return this._runAction(
|
|
431
|
-
'goTo',
|
|
432
|
-
async () => {
|
|
433
|
-
const toIndex = this._resolveStepIndex(target);
|
|
434
|
-
const toStep = this.steps[toIndex];
|
|
435
|
-
const fromStep = this.currentStep;
|
|
436
|
-
|
|
437
|
-
if (!toStep || toStep.id === this.state.currentId) {
|
|
438
|
-
if (payload) this.setStepData(this.state.currentId, payload);
|
|
439
|
-
return this.snapshot();
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
await this._transitionTo(toStep, {
|
|
443
|
-
direction: options.direction || 'go',
|
|
444
|
-
payload,
|
|
445
|
-
fromStep,
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
return this.snapshot();
|
|
449
|
-
},
|
|
450
|
-
{ internal: options.internal }
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* 合并全局数据。
|
|
456
|
-
* @param {object} data 数据补丁。
|
|
457
|
-
* @returns {Flow}
|
|
458
|
-
*/
|
|
459
|
-
setData(data) {
|
|
460
|
-
this._assertActive('setData');
|
|
461
|
-
validateParam('data', data, FLOW_PAYLOAD_RULE, 'Flow.setData');
|
|
462
|
-
if (!data) return this;
|
|
463
|
-
|
|
464
|
-
flushSync(() => {
|
|
465
|
-
Object.assign(this.state.data, data);
|
|
466
|
-
this.state.version += 1;
|
|
467
|
-
});
|
|
468
|
-
this._emitChange();
|
|
469
|
-
return this;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* 合并指定步骤缓存数据。
|
|
474
|
-
* @param {string} stepId 步骤 id。
|
|
475
|
-
* @param {object|null} data 数据补丁。
|
|
476
|
-
* @returns {Flow}
|
|
477
|
-
*/
|
|
478
|
-
setStepData(stepId, data, options = {}) {
|
|
479
|
-
this._assertActive('setStepData');
|
|
480
|
-
validateParam('stepId', stepId, { type: 'string' }, 'Flow.setStepData');
|
|
481
|
-
validateParam('data', data, FLOW_PAYLOAD_RULE, 'Flow.setStepData');
|
|
482
|
-
if (!data || !this._stepMap.has(stepId)) return this;
|
|
483
|
-
|
|
484
|
-
flushSync(() => {
|
|
485
|
-
this.state.stepData[stepId] = {
|
|
486
|
-
...clonePlainObject(this.state.stepData[stepId]),
|
|
487
|
-
...data,
|
|
488
|
-
};
|
|
489
|
-
if (this.options.cache) Object.assign(this.state.data, data);
|
|
490
|
-
this.state.version += 1;
|
|
491
|
-
});
|
|
492
|
-
if (!options.silent) this._emitChange();
|
|
493
|
-
return this;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* 获取指定步骤缓存数据。
|
|
498
|
-
* @param {string} stepId 步骤 id。
|
|
499
|
-
* @returns {object}
|
|
500
|
-
*/
|
|
501
|
-
getStepData(stepId) {
|
|
502
|
-
return clonePlainObject(this.state.stepData[stepId]);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* 重置流程。
|
|
507
|
-
* @returns {Flow}
|
|
508
|
-
*/
|
|
509
|
-
reset() {
|
|
510
|
-
this._assertActive('reset');
|
|
511
|
-
flushSync(() => {
|
|
512
|
-
this.state.currentId = this._initialStepId;
|
|
513
|
-
this.state.currentIndex = this._stepMap.get(this._initialStepId);
|
|
514
|
-
this.state.previousId = null;
|
|
515
|
-
this.state.previousIndex = null;
|
|
516
|
-
this.state.direction = null;
|
|
517
|
-
this.state.history.splice(
|
|
518
|
-
0,
|
|
519
|
-
this.state.history.length,
|
|
520
|
-
this._initialStepId
|
|
521
|
-
);
|
|
522
|
-
this._replaceObject(
|
|
523
|
-
this.state.data,
|
|
524
|
-
clonePlainObject(this._initialData.global)
|
|
525
|
-
);
|
|
526
|
-
this._replaceObject(
|
|
527
|
-
this.state.stepData,
|
|
528
|
-
clonePlainObject(this._initialData.stepData)
|
|
529
|
-
);
|
|
530
|
-
this.state.loading = false;
|
|
531
|
-
this.state.busyAction = null;
|
|
532
|
-
this.state.error = null;
|
|
533
|
-
this.state.version += 1;
|
|
534
|
-
});
|
|
535
|
-
this._emitChange();
|
|
536
|
-
return this;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* 完成流程。
|
|
541
|
-
* @param {object|null} [payload=null] 最后一步需要缓存的数据。
|
|
542
|
-
* @returns {Promise<object>} 当前快照。
|
|
543
|
-
*/
|
|
544
|
-
async finish(payload = null, options = {}) {
|
|
545
|
-
this._assertActive('finish');
|
|
546
|
-
validateParam('payload', payload, FLOW_PAYLOAD_RULE, 'Flow.finish');
|
|
547
|
-
const busySnapshot = options.internal ? null : this._handleBusy('finish');
|
|
548
|
-
if (busySnapshot) return busySnapshot;
|
|
549
|
-
|
|
550
|
-
return this._runAction(
|
|
551
|
-
'finish',
|
|
552
|
-
async () => {
|
|
553
|
-
if (payload) this.setStepData(this.state.currentId, payload);
|
|
554
|
-
await this._callHook(this.options.onFinish, [this.snapshot(), this]);
|
|
555
|
-
return this.snapshot();
|
|
556
|
-
},
|
|
557
|
-
{ internal: options.internal }
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* 销毁 Flow 实例。
|
|
563
|
-
* @returns {void}
|
|
564
|
-
*/
|
|
565
|
-
destroy() {
|
|
566
|
-
if (this._destroyed) return;
|
|
567
|
-
this._destroyed = true;
|
|
568
|
-
this._abortActiveAction();
|
|
569
|
-
this.unmount();
|
|
570
|
-
for (const cleanup of Array.from(this._cleanupTasks)) {
|
|
571
|
-
cleanup();
|
|
572
|
-
}
|
|
573
|
-
this._cleanupTasks.clear();
|
|
574
|
-
this._subscribers.clear();
|
|
575
|
-
this.steps = null;
|
|
576
|
-
this.options = null;
|
|
577
|
-
this.state = null;
|
|
578
|
-
this._stepMap = null;
|
|
579
|
-
this._initialData = null;
|
|
580
|
-
this._activeAction = null;
|
|
581
|
-
this._actionController = null;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
_resolveInitialStepId(initial) {
|
|
585
|
-
if (typeof initial === 'number') {
|
|
586
|
-
const step = this.steps[initial];
|
|
587
|
-
if (!step) throw new Error('Flow.options.initial index is out of range.');
|
|
588
|
-
return step.id;
|
|
589
|
-
}
|
|
590
|
-
if (typeof initial === 'string') {
|
|
591
|
-
if (!this._stepMap.has(initial)) {
|
|
592
|
-
throw new Error(
|
|
593
|
-
`Flow.options.initial step "${initial}" does not exist.`
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
return initial;
|
|
597
|
-
}
|
|
598
|
-
return this.steps[0].id;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
_validateSteps(steps) {
|
|
602
|
-
const ids = new Set();
|
|
603
|
-
for (const [index, step] of steps.entries()) {
|
|
604
|
-
validateParam(String(index), step, FLOW_STEP_RULE, 'Flow.options.steps');
|
|
605
|
-
validateParam(
|
|
606
|
-
'content',
|
|
607
|
-
step.content ?? null,
|
|
608
|
-
FLOW_CONTENT_RULE,
|
|
609
|
-
`Flow.options.steps.${index}`
|
|
610
|
-
);
|
|
611
|
-
if (ids.has(step.id)) {
|
|
612
|
-
throw new Error(`Flow.options.steps: duplicated step id "${step.id}".`);
|
|
613
|
-
}
|
|
614
|
-
ids.add(step.id);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
_resolveStepIndex(target) {
|
|
619
|
-
if (typeof target === 'number') {
|
|
620
|
-
if (!this.steps[target])
|
|
621
|
-
throw new Error('Flow.goTo target is out of range.');
|
|
622
|
-
return target;
|
|
623
|
-
}
|
|
624
|
-
if (typeof target === 'string') {
|
|
625
|
-
if (!this._stepMap.has(target)) {
|
|
626
|
-
throw new Error(`Flow.goTo target "${target}" does not exist.`);
|
|
627
|
-
}
|
|
628
|
-
return this._stepMap.get(target);
|
|
629
|
-
}
|
|
630
|
-
throw new Error('Flow.goTo target expects string or number.');
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
_createInitialStepData() {
|
|
634
|
-
const stepData = {};
|
|
635
|
-
const global = {};
|
|
636
|
-
for (const step of this.steps) {
|
|
637
|
-
stepData[step.id] = clonePlainObject(step.data);
|
|
638
|
-
Object.assign(global, stepData[step.id]);
|
|
639
|
-
}
|
|
640
|
-
return { stepData, global };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
async _runMoveHook(type, step, payload, fallbackId) {
|
|
644
|
-
const globalHook =
|
|
645
|
-
type === 'back' ? this.options.onBack : this.options.onNext;
|
|
646
|
-
const stepHook = type === 'back' ? step.onBack : step.onNext;
|
|
647
|
-
const context = this._createContext({ payload, targetId: fallbackId });
|
|
648
|
-
|
|
649
|
-
if (typeof stepHook === 'function') {
|
|
650
|
-
const result = await this._callHook(stepHook, [context]);
|
|
651
|
-
if (result != null) return result;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (typeof globalHook === 'function') {
|
|
655
|
-
const result = await this._callHook(globalHook, [context]);
|
|
656
|
-
if (result != null) return result;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return fallbackId;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async _transitionTo(toStep, { direction, payload, fromStep }) {
|
|
663
|
-
const fromSnapshot = this.snapshot();
|
|
664
|
-
const rollbackState = this._captureState();
|
|
665
|
-
|
|
666
|
-
try {
|
|
667
|
-
if (payload) this.setStepData(fromStep.id, payload, { silent: true });
|
|
668
|
-
|
|
669
|
-
await this._assertCanLeave(fromStep, toStep, direction);
|
|
670
|
-
await this._assertCanEnter(toStep, fromStep, direction);
|
|
671
|
-
|
|
672
|
-
if (typeof fromStep.onLeave === 'function') {
|
|
673
|
-
await this._callHook(fromStep.onLeave, [
|
|
674
|
-
this._createContext({
|
|
675
|
-
direction,
|
|
676
|
-
step: fromStep,
|
|
677
|
-
targetId: toStep.id,
|
|
678
|
-
}),
|
|
679
|
-
]);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const previousId = this.state.currentId;
|
|
683
|
-
const previousIndex = this.state.currentIndex;
|
|
684
|
-
const currentIndex = this._stepMap.get(toStep.id);
|
|
685
|
-
|
|
686
|
-
flushSync(() => {
|
|
687
|
-
this.state.previousId = previousId;
|
|
688
|
-
this.state.previousIndex = previousIndex;
|
|
689
|
-
this.state.currentId = toStep.id;
|
|
690
|
-
this.state.currentIndex = currentIndex;
|
|
691
|
-
this.state.direction = direction;
|
|
692
|
-
if (direction === 'back') this.state.history.pop();
|
|
693
|
-
else this.state.history.push(toStep.id);
|
|
694
|
-
this.state.error = null;
|
|
695
|
-
this.state.version += 1;
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
if (typeof toStep.onEnter === 'function') {
|
|
699
|
-
await this._callHook(toStep.onEnter, [
|
|
700
|
-
this._createContext({
|
|
701
|
-
direction,
|
|
702
|
-
fromId: previousId,
|
|
703
|
-
step: toStep,
|
|
704
|
-
}),
|
|
705
|
-
]);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
this._emitChange(fromSnapshot);
|
|
709
|
-
} catch (error) {
|
|
710
|
-
if (this._destroyed || !this.state || !this.options) {
|
|
711
|
-
throw error;
|
|
712
|
-
}
|
|
713
|
-
if (this.options.rollbackOnError) {
|
|
714
|
-
this._restoreState(rollbackState, { keepLoading: true });
|
|
715
|
-
}
|
|
716
|
-
this._handleError(error, fromSnapshot);
|
|
717
|
-
throw error;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
async _assertCanLeave(fromStep, toStep, direction) {
|
|
722
|
-
if (typeof fromStep.canLeave !== 'function') return;
|
|
723
|
-
const result = await this._callHook(fromStep.canLeave, [
|
|
724
|
-
this._createContext({ direction, targetId: toStep.id }),
|
|
725
|
-
]);
|
|
726
|
-
if (result === false) {
|
|
727
|
-
throw new Error(`Flow: step "${fromStep.id}" blocked leaving.`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
async _assertCanEnter(toStep, fromStep, direction) {
|
|
732
|
-
if (typeof toStep.canEnter !== 'function') return;
|
|
733
|
-
const result = await this._callHook(toStep.canEnter, [
|
|
734
|
-
this._createContext({ direction, fromId: fromStep.id }),
|
|
735
|
-
]);
|
|
736
|
-
if (result === false) {
|
|
737
|
-
throw new Error(`Flow: step "${toStep.id}" blocked entering.`);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
_createContext(extra = {}) {
|
|
742
|
-
const step = extra.step || this.currentStep;
|
|
743
|
-
const snapshot = this.snapshot();
|
|
744
|
-
return {
|
|
745
|
-
...extra,
|
|
746
|
-
flow: this,
|
|
747
|
-
step,
|
|
748
|
-
state: this.state,
|
|
749
|
-
signal: this._actionController?.signal || null,
|
|
750
|
-
snapshot,
|
|
751
|
-
data: clonePlainObject(this.state.data),
|
|
752
|
-
currentData: this.getStepData(step.id),
|
|
753
|
-
setData: (data) => this.setData(data),
|
|
754
|
-
setStepData: (stepId, data) => this.setStepData(stepId, data),
|
|
755
|
-
getStepData: (stepId) => this.getStepData(stepId),
|
|
756
|
-
next: (payload) => this.next(payload),
|
|
757
|
-
back: (payload) => this.back(payload),
|
|
758
|
-
goTo: (target, payload, options) => this.goTo(target, payload, options),
|
|
759
|
-
addCleanup: (cleanup) => this._addCleanup(cleanup),
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
async _runAction(action, task, options = {}) {
|
|
764
|
-
const isOuterAction = !options.internal && !this.state.loading;
|
|
765
|
-
if (isOuterAction) {
|
|
766
|
-
this._actionController =
|
|
767
|
-
typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
768
|
-
this._activeAction = action;
|
|
769
|
-
this._setLoading(true, action);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
try {
|
|
773
|
-
return await task();
|
|
774
|
-
} catch (error) {
|
|
775
|
-
if (this._destroyed || !this.state) {
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
if (!this._destroyed && this.state?.error !== error) {
|
|
779
|
-
this._handleError(error);
|
|
780
|
-
}
|
|
781
|
-
throw error;
|
|
782
|
-
} finally {
|
|
783
|
-
if (isOuterAction && !this._destroyed) {
|
|
784
|
-
this._setLoading(false, null);
|
|
785
|
-
this._activeAction = null;
|
|
786
|
-
this._actionController = null;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
_handleBusy(action) {
|
|
792
|
-
if (!this.state.loading) return null;
|
|
793
|
-
|
|
794
|
-
const error = new Error(`Flow: action "${action}" ignored while loading.`);
|
|
795
|
-
error.code = 'FLOW_BUSY';
|
|
796
|
-
|
|
797
|
-
if (typeof this.options.onBusy === 'function') {
|
|
798
|
-
this.options.onBusy(action, this.snapshot(), this);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (this.options.busyStrategy === 'throw') {
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
return this.snapshot();
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
_abortActiveAction() {
|
|
809
|
-
this._actionController?.abort?.();
|
|
810
|
-
this._actionController = null;
|
|
811
|
-
this._activeAction = null;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
async _callHook(hook, args) {
|
|
815
|
-
if (typeof hook !== 'function') return undefined;
|
|
816
|
-
const result = await hook(...args);
|
|
817
|
-
if (this._destroyed || !this.state) {
|
|
818
|
-
throw this._createAbortError();
|
|
819
|
-
}
|
|
820
|
-
return result;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
_createAbortError() {
|
|
824
|
-
const error = new Error('Flow: active action was aborted.');
|
|
825
|
-
error.code = 'FLOW_ABORTED';
|
|
826
|
-
return error;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
_setLoading(value, action = this.state?.busyAction || null) {
|
|
830
|
-
if (!this.state) return;
|
|
831
|
-
flushSync(() => {
|
|
832
|
-
this.state.loading = value;
|
|
833
|
-
this.state.busyAction = value ? action : null;
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
_handleError(error, previous = null) {
|
|
838
|
-
if (!this.state) return;
|
|
839
|
-
flushSync(() => {
|
|
840
|
-
this.state.error = error;
|
|
841
|
-
this.state.version += 1;
|
|
842
|
-
});
|
|
843
|
-
if (typeof this.options.onError === 'function') {
|
|
844
|
-
this.options.onError(error, this.snapshot(), this, previous);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
_emitChange(previous = null) {
|
|
849
|
-
const next = this.snapshot();
|
|
850
|
-
for (const handler of Array.from(this._subscribers)) {
|
|
851
|
-
handler(next, this, previous);
|
|
852
|
-
}
|
|
853
|
-
if (typeof this.options.onChange === 'function') {
|
|
854
|
-
this.options.onChange(next, this, previous);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
_replaceObject(target, source) {
|
|
859
|
-
for (const key of Object.keys(target)) delete target[key];
|
|
860
|
-
Object.assign(target, source);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
_captureState() {
|
|
864
|
-
return {
|
|
865
|
-
currentId: this.state.currentId,
|
|
866
|
-
currentIndex: this.state.currentIndex,
|
|
867
|
-
previousId: this.state.previousId,
|
|
868
|
-
previousIndex: this.state.previousIndex,
|
|
869
|
-
direction: this.state.direction,
|
|
870
|
-
history: cloneArray(this.state.history),
|
|
871
|
-
data: clonePlainObject(this.state.data),
|
|
872
|
-
stepData: clonePlainObject(this.state.stepData),
|
|
873
|
-
loading: this.state.loading,
|
|
874
|
-
busyAction: this.state.busyAction,
|
|
875
|
-
error: this.state.error,
|
|
876
|
-
version: this.state.version,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
_restoreState(snapshot, options = {}) {
|
|
881
|
-
flushSync(() => {
|
|
882
|
-
this.state.currentId = snapshot.currentId;
|
|
883
|
-
this.state.currentIndex = snapshot.currentIndex;
|
|
884
|
-
this.state.previousId = snapshot.previousId;
|
|
885
|
-
this.state.previousIndex = snapshot.previousIndex;
|
|
886
|
-
this.state.direction = snapshot.direction;
|
|
887
|
-
this.state.history.splice(
|
|
888
|
-
0,
|
|
889
|
-
this.state.history.length,
|
|
890
|
-
...snapshot.history
|
|
891
|
-
);
|
|
892
|
-
this._replaceObject(this.state.data, snapshot.data);
|
|
893
|
-
this._replaceObject(this.state.stepData, snapshot.stepData);
|
|
894
|
-
this.state.loading = options.keepLoading ? true : snapshot.loading;
|
|
895
|
-
this.state.busyAction = snapshot.busyAction;
|
|
896
|
-
this.state.error = snapshot.error;
|
|
897
|
-
this.state.version = snapshot.version + 1;
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
_addCleanup(cleanup) {
|
|
902
|
-
if (typeof cleanup !== 'function') {
|
|
903
|
-
throw new Error('Flow.addCleanup: cleanup expects a function.');
|
|
904
|
-
}
|
|
905
|
-
if (this._destroyed) {
|
|
906
|
-
cleanup();
|
|
907
|
-
return () => {};
|
|
908
|
-
}
|
|
909
|
-
this._cleanupTasks.add(cleanup);
|
|
910
|
-
return () => this._cleanupTasks.delete(cleanup);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
_assertActive(method) {
|
|
914
|
-
if (this._destroyed || !this.state) {
|
|
915
|
-
throw new Error(`Flow.${method}: instance has been destroyed.`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
_publicStep(step) {
|
|
920
|
-
if (!step) return null;
|
|
921
|
-
const publicStep = { ...step };
|
|
922
|
-
delete publicStep.onEnter;
|
|
923
|
-
delete publicStep.onLeave;
|
|
924
|
-
delete publicStep.onNext;
|
|
925
|
-
delete publicStep.onBack;
|
|
926
|
-
delete publicStep.canEnter;
|
|
927
|
-
delete publicStep.canLeave;
|
|
928
|
-
return publicStep;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
_buildRoot() {
|
|
932
|
-
return jsx('div', {
|
|
933
|
-
className: `j-flow ${this.options.className || ''}`.trim(),
|
|
934
|
-
id: this.options.id,
|
|
935
|
-
role: 'group',
|
|
936
|
-
'aria-busy': () => (this.state.loading ? 'true' : 'false'),
|
|
937
|
-
'aria-labelledby': `${this.options.id}-title`,
|
|
938
|
-
ref: (element) => {
|
|
939
|
-
this._nodes.root = element;
|
|
940
|
-
},
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
_mountView() {
|
|
945
|
-
if (this._renderDispose || !this.root) return;
|
|
946
|
-
this._renderDispose = createRoot((dispose) => {
|
|
947
|
-
const viewDispose = render(() => this._view(), this.root);
|
|
948
|
-
onCleanup(viewDispose);
|
|
949
|
-
return dispose;
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
_view() {
|
|
954
|
-
const snapshot = this.snapshot();
|
|
955
|
-
return [
|
|
956
|
-
this._renderSlot('renderHeader', snapshot, () =>
|
|
957
|
-
this.options.showHeader ? this._headerView(snapshot) : null
|
|
958
|
-
),
|
|
959
|
-
this._renderSlot('renderSteps', snapshot, () =>
|
|
960
|
-
this.options.showSteps ? this._stepsView(snapshot) : null
|
|
961
|
-
),
|
|
962
|
-
this._renderSlot('renderBody', snapshot, () => this._bodyView(snapshot)),
|
|
963
|
-
this._renderSlot('renderFooter', snapshot, () =>
|
|
964
|
-
this.options.showFooter ? this._footerView(snapshot) : null
|
|
965
|
-
),
|
|
966
|
-
];
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
_renderSlot(name, snapshot, fallback) {
|
|
970
|
-
const slot = this.options[name];
|
|
971
|
-
if (slot === false) return null;
|
|
972
|
-
if (typeof slot === 'function') {
|
|
973
|
-
return slot(this._createRenderContext(snapshot, fallback));
|
|
974
|
-
}
|
|
975
|
-
return fallback();
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
_createRenderContext(snapshot, fallback) {
|
|
979
|
-
return {
|
|
980
|
-
flow: this,
|
|
981
|
-
snapshot,
|
|
982
|
-
state: this.state,
|
|
983
|
-
steps: this.steps.map((step) => this._publicStep(step)),
|
|
984
|
-
currentStep: snapshot.currentStep,
|
|
985
|
-
currentData: snapshot.currentData,
|
|
986
|
-
data: snapshot.data,
|
|
987
|
-
fallback,
|
|
988
|
-
next: (payload) => this.next(payload),
|
|
989
|
-
back: (payload) => this.back(payload),
|
|
990
|
-
goTo: (target, payload, options) => this.goTo(target, payload, options),
|
|
991
|
-
reset: () => this.reset(),
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
_headerView(snapshot) {
|
|
996
|
-
return jsx('div', {
|
|
997
|
-
className: 'flow-header',
|
|
998
|
-
children: [
|
|
999
|
-
jsx('div', {
|
|
1000
|
-
id: `${this.options.id}-title`,
|
|
1001
|
-
className: 'flow-title',
|
|
1002
|
-
children:
|
|
1003
|
-
snapshot.currentStep?.title || snapshot.currentStep?.id || '',
|
|
1004
|
-
}),
|
|
1005
|
-
snapshot.currentStep?.description
|
|
1006
|
-
? jsx('div', {
|
|
1007
|
-
className: 'flow-description',
|
|
1008
|
-
children: snapshot.currentStep.description,
|
|
1009
|
-
})
|
|
1010
|
-
: null,
|
|
1011
|
-
],
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
_stepsView(snapshot) {
|
|
1016
|
-
return jsx('ol', {
|
|
1017
|
-
className: 'flow-steps',
|
|
1018
|
-
role: 'list',
|
|
1019
|
-
'aria-label': 'Flow steps',
|
|
1020
|
-
children: this.steps.map((step, index) =>
|
|
1021
|
-
jsx('li', {
|
|
1022
|
-
className: this._stepClass(index, snapshot.currentIndex),
|
|
1023
|
-
'aria-current': index === snapshot.currentIndex ? 'step' : undefined,
|
|
1024
|
-
children: [
|
|
1025
|
-
jsx('button', {
|
|
1026
|
-
type: 'button',
|
|
1027
|
-
className: 'flow-step-button',
|
|
1028
|
-
disabled: this.options.linear && index > snapshot.currentIndex,
|
|
1029
|
-
'aria-current':
|
|
1030
|
-
index === snapshot.currentIndex ? 'step' : undefined,
|
|
1031
|
-
'aria-disabled':
|
|
1032
|
-
this.options.linear && index > snapshot.currentIndex
|
|
1033
|
-
? 'true'
|
|
1034
|
-
: 'false',
|
|
1035
|
-
'aria-label': `${index + 1}. ${step.title || step.id}`,
|
|
1036
|
-
onClick: () => {
|
|
1037
|
-
if (index === snapshot.currentIndex) return;
|
|
1038
|
-
void this.goTo(index);
|
|
1039
|
-
},
|
|
1040
|
-
children: [
|
|
1041
|
-
jsx('span', {
|
|
1042
|
-
className: 'flow-step-index',
|
|
1043
|
-
children: index + 1,
|
|
1044
|
-
}),
|
|
1045
|
-
jsx('span', {
|
|
1046
|
-
className: 'flow-step-title',
|
|
1047
|
-
children: step.title || step.id,
|
|
1048
|
-
}),
|
|
1049
|
-
],
|
|
1050
|
-
}),
|
|
1051
|
-
],
|
|
1052
|
-
})
|
|
1053
|
-
),
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
_bodyView(snapshot) {
|
|
1058
|
-
return jsx('div', {
|
|
1059
|
-
className: 'flow-body',
|
|
1060
|
-
role: 'region',
|
|
1061
|
-
'aria-live': 'polite',
|
|
1062
|
-
'aria-busy': snapshot.loading ? 'true' : 'false',
|
|
1063
|
-
children: this._contentView(snapshot.currentStep?.content),
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
_footerView(snapshot) {
|
|
1068
|
-
return jsx('div', {
|
|
1069
|
-
className: 'flow-footer',
|
|
1070
|
-
children: [
|
|
1071
|
-
this.options.showReset
|
|
1072
|
-
? jsx('button', {
|
|
1073
|
-
type: 'button',
|
|
1074
|
-
className: 'j-button is-ghost flow-reset',
|
|
1075
|
-
onClick: () => this.reset(),
|
|
1076
|
-
disabled: snapshot.loading,
|
|
1077
|
-
'aria-disabled': snapshot.loading ? 'true' : 'false',
|
|
1078
|
-
children: this.options.text.reset,
|
|
1079
|
-
})
|
|
1080
|
-
: null,
|
|
1081
|
-
this.options.showBack
|
|
1082
|
-
? jsx('button', {
|
|
1083
|
-
type: 'button',
|
|
1084
|
-
className: 'j-button is-ghost flow-back',
|
|
1085
|
-
'data-action': 'back',
|
|
1086
|
-
onClick: () => void this.back(),
|
|
1087
|
-
disabled: !snapshot.canBack,
|
|
1088
|
-
'aria-disabled': !snapshot.canBack ? 'true' : 'false',
|
|
1089
|
-
children: this.options.text.back,
|
|
1090
|
-
})
|
|
1091
|
-
: null,
|
|
1092
|
-
this.options.showNext
|
|
1093
|
-
? jsx('button', {
|
|
1094
|
-
type: 'button',
|
|
1095
|
-
className: 'j-button is-primary flow-next',
|
|
1096
|
-
'data-action': 'next',
|
|
1097
|
-
onClick: () => void this.next(),
|
|
1098
|
-
disabled: snapshot.loading,
|
|
1099
|
-
'aria-disabled': snapshot.loading ? 'true' : 'false',
|
|
1100
|
-
children: snapshot.isLast
|
|
1101
|
-
? this.options.text.finish
|
|
1102
|
-
: this.options.text.next,
|
|
1103
|
-
})
|
|
1104
|
-
: null,
|
|
1105
|
-
],
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
_contentView(content) {
|
|
1110
|
-
if (typeof content === 'function') return content(this._createContext());
|
|
1111
|
-
if (Array.isArray(content)) return content;
|
|
1112
|
-
if (typeof Node !== 'undefined' && content instanceof Node) return content;
|
|
1113
|
-
if (content == null) return '';
|
|
1114
|
-
if (typeof content !== 'string') return '';
|
|
1115
|
-
return content;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
_stepClass(index, currentIndex = this.state.currentIndex) {
|
|
1119
|
-
const classes = ['flow-step'];
|
|
1120
|
-
if (index === currentIndex) classes.push('is-active');
|
|
1121
|
-
if (index < currentIndex) classes.push('is-complete');
|
|
1122
|
-
return classes.join(' ');
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* 创建 Flow 实例。
|
|
1128
|
-
* @param {FlowOptions} options Flow 配置。
|
|
1129
|
-
* @returns {Flow}
|
|
1130
|
-
*/
|
|
1131
|
-
export function createFlow(options) {
|
|
1132
|
-
return new Flow(options);
|
|
1133
|
-
}
|