@xihan-ui/web-components 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4919 -6
- package/README.md +10 -3
- package/custom-elements.json +38698 -19900
- package/dist/backgrounds.d.ts +7 -7
- package/dist/backgrounds.d.ts.map +1 -1
- package/dist/backgrounds.js +8 -8
- package/dist/backgrounds.js.map +1 -1
- package/dist/{config-Cz16-P4i.d.ts → config-etfwIorI.d.ts} +17 -8
- package/dist/config-etfwIorI.d.ts.map +1 -0
- package/dist/define.d.ts +8641 -4081
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +11420 -5706
- package/dist/define.js.map +1 -1
- package/dist/{element-ib25-l85.js → element-BKQR3ce3.js} +4 -4
- package/dist/element-BKQR3ce3.js.map +1 -0
- package/dist/{element-DRoFjrYO.d.ts → element-DFbmhZLP.d.ts} +3 -3
- package/dist/{element-DRoFjrYO.d.ts.map → element-DFbmhZLP.d.ts.map} +1 -1
- package/dist/index.d.ts +11 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{machine-controller-DE6edQbK.js → machine-controller-C1ffftux.js} +35 -29
- package/dist/machine-controller-C1ffftux.js.map +1 -0
- package/dist/registry-DR6I2_zF.js.map +1 -1
- package/dist/services.d.ts +184 -0
- package/dist/services.d.ts.map +1 -0
- package/dist/services.js +701 -0
- package/dist/services.js.map +1 -0
- package/dist/toast-BTlM12YF.js +1405 -0
- package/dist/toast-BTlM12YF.js.map +1 -0
- package/dist/{types-BvWWqMd_.d.ts → types-C1Bybm6P.d.ts} +2 -2
- package/dist/{types-BvWWqMd_.d.ts.map → types-C1Bybm6P.d.ts.map} +1 -1
- package/package.json +23 -16
- package/dist/config-Cz16-P4i.d.ts.map +0 -1
- package/dist/element-ib25-l85.js.map +0 -1
- package/dist/machine-controller-DE6edQbK.js.map +0 -1
package/dist/services.js
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { m as withXhConfig, n as createLitRuntime, s as wcNormalize } from "./machine-controller-C1ffftux.js";
|
|
2
|
+
import { t as defineElement } from "./registry-DR6I2_zF.js";
|
|
3
|
+
import { n as version } from "./element-BKQR3ce3.js";
|
|
4
|
+
import { a as XhDialogElement, i as XhLoadingBarElement, n as XhNotificationElement, r as XhNotificationItemElement, s as XhButtonElement, t as XhToastElement } from "./toast-BTlM12YF.js";
|
|
5
|
+
import { NOTIFICATION_PLACEMENT, NOTIFICATION_PLACEMENTS, TOAST_GAP, TOAST_MAX, TOAST_PLACEMENT, connectNotification, createDialogServiceController, createFeedbackServiceController, createLoadingBarServiceController, createToastStackController, dialogServiceBadgeTone, notificationMachine, resolveFeedbackServiceTitle, resolveToastServiceItem, toastAnatomy, visibleNotifications } from "@xihan-ui/headless";
|
|
6
|
+
import { DATA_INERT_EXEMPT, DIAGNOSTIC_CODES, createService, ensurePortalRoot, reportDiagnostic } from "@xihan-ui/core";
|
|
7
|
+
//#region src/services/glyph.ts
|
|
8
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
9
|
+
/** 旋转的加载弧线;转动动画由外层容器(如按钮的 indicator 部件)提供。 */
|
|
10
|
+
function spinArc(size = "1em") {
|
|
11
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
12
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
13
|
+
svg.setAttribute("width", size);
|
|
14
|
+
svg.setAttribute("height", size);
|
|
15
|
+
svg.setAttribute("aria-hidden", "true");
|
|
16
|
+
svg.style.display = "block";
|
|
17
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
18
|
+
circle.setAttribute("cx", "8");
|
|
19
|
+
circle.setAttribute("cy", "8");
|
|
20
|
+
circle.setAttribute("r", "6.5");
|
|
21
|
+
circle.setAttribute("fill", "none");
|
|
22
|
+
circle.setAttribute("stroke", "currentColor");
|
|
23
|
+
circle.setAttribute("stroke-width", "2");
|
|
24
|
+
circle.setAttribute("stroke-linecap", "round");
|
|
25
|
+
circle.setAttribute("stroke-dasharray", "30");
|
|
26
|
+
circle.setAttribute("stroke-dashoffset", "22");
|
|
27
|
+
svg.appendChild(circle);
|
|
28
|
+
return svg;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/services/host.ts
|
|
32
|
+
/**
|
|
33
|
+
* 命令式服务的宿主容器。未提供 target 时在浮层落点下新建一个,dispose 时连同容器一起移除。
|
|
34
|
+
*/
|
|
35
|
+
function createServiceHolder(target) {
|
|
36
|
+
if (target) return {
|
|
37
|
+
holder: target,
|
|
38
|
+
release: () => {}
|
|
39
|
+
};
|
|
40
|
+
const holder = document.createElement("div");
|
|
41
|
+
ensurePortalRoot(document).appendChild(holder);
|
|
42
|
+
return {
|
|
43
|
+
holder,
|
|
44
|
+
release: () => holder.remove()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 供没有元素的状态机使用的最小反应宿主。
|
|
49
|
+
*
|
|
50
|
+
* 轻提示的堆叠在库中没有对应的自定义元素(堆叠位置是整个服务的口径,
|
|
51
|
+
* 不是逐页各挂一份),因此队列状态机没有可依附的元素。状态机只要求宿主在状态变化时
|
|
52
|
+
* 被唤醒一次,这里就只兑现这一件事:把重绘排进微任务,同一拍内的多次写入合并为一次。
|
|
53
|
+
*/
|
|
54
|
+
function createServiceReactiveHost(render) {
|
|
55
|
+
let scheduled = false;
|
|
56
|
+
let settle = null;
|
|
57
|
+
let pending = null;
|
|
58
|
+
const flush = () => {
|
|
59
|
+
scheduled = false;
|
|
60
|
+
const done = settle;
|
|
61
|
+
settle = null;
|
|
62
|
+
pending = null;
|
|
63
|
+
render();
|
|
64
|
+
done?.();
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
addController: () => {},
|
|
68
|
+
removeController: () => {},
|
|
69
|
+
requestUpdate: () => {
|
|
70
|
+
if (scheduled) return;
|
|
71
|
+
scheduled = true;
|
|
72
|
+
queueMicrotask(flush);
|
|
73
|
+
},
|
|
74
|
+
get updateComplete() {
|
|
75
|
+
if (!scheduled) return Promise.resolve(true);
|
|
76
|
+
pending ??= new Promise((resolve) => {
|
|
77
|
+
settle = () => resolve(!scheduled);
|
|
78
|
+
});
|
|
79
|
+
return pending;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 服务无法建立时发一条诊断并返回 false,由调用方整体惰化。
|
|
85
|
+
*
|
|
86
|
+
* 这几个服务是从路由守卫、请求拦截器等位置懒建的:这些位置抛异常,
|
|
87
|
+
* 后果不是提示未弹出而是整次导航失败、整站白屏。一条轻提示不应有这种权力。
|
|
88
|
+
*/
|
|
89
|
+
function reportServiceFailure(service, error) {
|
|
90
|
+
reportDiagnostic({
|
|
91
|
+
code: DIAGNOSTIC_CODES.warn,
|
|
92
|
+
level: "warn",
|
|
93
|
+
message: `[xh] ${service} 的宿主没建起来,这个服务本次退化成空操作。`,
|
|
94
|
+
detail: {
|
|
95
|
+
service,
|
|
96
|
+
error
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/** 建一个带角色标记的节点:作者在声明式用法里手写的那一份,服务自己生成。 */
|
|
102
|
+
function partNode(tag, part) {
|
|
103
|
+
const el = document.createElement(tag);
|
|
104
|
+
el.dataset.xhPart = part;
|
|
105
|
+
return el;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/services/register.ts
|
|
109
|
+
/**
|
|
110
|
+
* 服务用到的那几个元素在服务建起来时注册。
|
|
111
|
+
*
|
|
112
|
+
* 服务自己生成节点,作者不写标签,因此不能要求他先 import `/define`;
|
|
113
|
+
* 注册表是幂等的,已经注册过的原样跳过。
|
|
114
|
+
*/
|
|
115
|
+
function defineFeedbackElements() {
|
|
116
|
+
defineElement("xh-toast", XhToastElement, version);
|
|
117
|
+
defineElement("xh-notification", XhNotificationElement, version);
|
|
118
|
+
defineElement("xh-notification-item", XhNotificationItemElement, version);
|
|
119
|
+
defineElement("xh-dialog", XhDialogElement, version);
|
|
120
|
+
defineElement("xh-button", XhButtonElement, version);
|
|
121
|
+
defineElement("xh-loading-bar", XhLoadingBarElement, version);
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/services/dialog-service.ts
|
|
125
|
+
function createDialogService(options = {}) {
|
|
126
|
+
if (typeof document === "undefined") throw new Error("createDialogService 需要 document;SSR 里请等到客户端再创建");
|
|
127
|
+
const defaults = {
|
|
128
|
+
okText: options.okText ?? "OK",
|
|
129
|
+
cancelText: options.cancelText ?? "Cancel"
|
|
130
|
+
};
|
|
131
|
+
const holder = options.target ?? document.createElement("div");
|
|
132
|
+
const release = () => {
|
|
133
|
+
if (!options.target) holder.remove();
|
|
134
|
+
};
|
|
135
|
+
let hostFailure = null;
|
|
136
|
+
try {
|
|
137
|
+
defineFeedbackElements();
|
|
138
|
+
if (!options.target) ensurePortalRoot(document).appendChild(holder);
|
|
139
|
+
if (holder.nodeType !== 1 || holder.ownerDocument !== document || !holder.isConnected) throw new Error("DialogService target 必须是当前文档中已连接的元素");
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
hostFailure = { cause };
|
|
142
|
+
}
|
|
143
|
+
const dialog = document.createElement("xh-dialog");
|
|
144
|
+
dialog.setAttribute("role", "alertdialog");
|
|
145
|
+
dialog.modal = true;
|
|
146
|
+
const backdrop = partNode("div", "backdrop");
|
|
147
|
+
const positioner = partNode("div", "positioner");
|
|
148
|
+
const content = partNode("div", "content");
|
|
149
|
+
const header = partNode("div", "header");
|
|
150
|
+
const indicator = partNode("div", "indicator");
|
|
151
|
+
const title = partNode("div", "title");
|
|
152
|
+
const body = partNode("div", "body");
|
|
153
|
+
const description = partNode("div", "description");
|
|
154
|
+
const errorMessage = document.createElement("p");
|
|
155
|
+
errorMessage.setAttribute("role", "alert");
|
|
156
|
+
errorMessage.style.color = "var(--xh-fg-danger)";
|
|
157
|
+
errorMessage.hidden = true;
|
|
158
|
+
const footer = partNode("div", "footer");
|
|
159
|
+
const cancel = document.createElement("xh-button");
|
|
160
|
+
const cancelRoot = partNode("button", "root");
|
|
161
|
+
const cancelLabel = partNode("span", "label");
|
|
162
|
+
const ok = document.createElement("xh-button");
|
|
163
|
+
const okRoot = partNode("button", "root");
|
|
164
|
+
const okIndicator = partNode("span", "indicator");
|
|
165
|
+
const okLabel = partNode("span", "label");
|
|
166
|
+
header.append(indicator, title);
|
|
167
|
+
body.append(description, errorMessage);
|
|
168
|
+
cancelRoot.append(cancelLabel);
|
|
169
|
+
cancel.append(cancelRoot);
|
|
170
|
+
cancel.variant = "ghost";
|
|
171
|
+
okRoot.append(okIndicator, okLabel);
|
|
172
|
+
ok.append(okRoot);
|
|
173
|
+
ok.variant = "solid";
|
|
174
|
+
footer.append(cancel, ok);
|
|
175
|
+
content.append(header, body, footer);
|
|
176
|
+
positioner.append(content);
|
|
177
|
+
dialog.append(backdrop, positioner);
|
|
178
|
+
const controller = createDialogServiceController({ onStateChange: () => paint() });
|
|
179
|
+
if (!hostFailure) try {
|
|
180
|
+
holder.appendChild(dialog);
|
|
181
|
+
} catch (cause) {
|
|
182
|
+
hostFailure = { cause };
|
|
183
|
+
release();
|
|
184
|
+
}
|
|
185
|
+
function paintContent() {
|
|
186
|
+
const serviceState = controller.state;
|
|
187
|
+
const spec = serviceState.current?.spec;
|
|
188
|
+
if (!spec) return;
|
|
189
|
+
title.textContent = spec.title;
|
|
190
|
+
indicator.hidden = spec.badge == null;
|
|
191
|
+
if (spec.badge) indicator.setAttribute("data-tone", dialogServiceBadgeTone(spec.badge));
|
|
192
|
+
else indicator.removeAttribute("data-tone");
|
|
193
|
+
if (typeof spec.content === "function") {
|
|
194
|
+
description.replaceChildren();
|
|
195
|
+
spec.content(description);
|
|
196
|
+
} else description.textContent = spec.content ?? "";
|
|
197
|
+
cancel.hidden = !spec.showCancel;
|
|
198
|
+
cancel.disabled = serviceState.busy;
|
|
199
|
+
cancelLabel.textContent = spec.cancelText;
|
|
200
|
+
ok.tone = spec.tone;
|
|
201
|
+
ok.loading = serviceState.busy;
|
|
202
|
+
okIndicator.replaceChildren(...serviceState.busy ? [spinArc()] : []);
|
|
203
|
+
okLabel.textContent = serviceState.busy ? `${spec.okText}…` : spec.okText;
|
|
204
|
+
errorMessage.hidden = serviceState.actionError === null;
|
|
205
|
+
errorMessage.textContent = serviceState.actionError ? options.actionErrorText ?? "Action failed. Please try again." : "";
|
|
206
|
+
dialog.closeOnEscape = !serviceState.busy;
|
|
207
|
+
}
|
|
208
|
+
function failHost(cause) {
|
|
209
|
+
controller.fail(cause);
|
|
210
|
+
}
|
|
211
|
+
let exitRequest = null;
|
|
212
|
+
let exitListener = null;
|
|
213
|
+
function syncExitListener() {
|
|
214
|
+
const request = controller.state.exiting;
|
|
215
|
+
if (request === exitRequest) return;
|
|
216
|
+
if (exitListener) dialog.removeEventListener("exit-complete", exitListener);
|
|
217
|
+
exitRequest = request;
|
|
218
|
+
exitListener = request ? () => void controller.finishExit(request) : null;
|
|
219
|
+
if (exitListener) dialog.addEventListener("exit-complete", exitListener, { once: true });
|
|
220
|
+
}
|
|
221
|
+
function paint() {
|
|
222
|
+
try {
|
|
223
|
+
paintContent();
|
|
224
|
+
syncExitListener();
|
|
225
|
+
dialog.open = controller.state.open;
|
|
226
|
+
} catch (cause) {
|
|
227
|
+
failHost(cause);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (hostFailure) controller.fail(hostFailure.cause);
|
|
231
|
+
const onOpenChange = (event) => {
|
|
232
|
+
if (event.detail?.open) return;
|
|
233
|
+
controller.close(false);
|
|
234
|
+
};
|
|
235
|
+
dialog.addEventListener("open-change", onOpenChange);
|
|
236
|
+
const onCancel = () => {
|
|
237
|
+
if (!controller.state.busy) controller.close(false);
|
|
238
|
+
};
|
|
239
|
+
const onOk = () => {
|
|
240
|
+
controller.confirmCurrent();
|
|
241
|
+
};
|
|
242
|
+
cancelRoot.addEventListener("click", onCancel);
|
|
243
|
+
okRoot.addEventListener("click", onOk);
|
|
244
|
+
const alert = (badge, tone) => async (opts) => {
|
|
245
|
+
await controller.request({
|
|
246
|
+
title: opts.title,
|
|
247
|
+
content: opts.content,
|
|
248
|
+
tone,
|
|
249
|
+
okText: opts.okText ?? defaults.okText,
|
|
250
|
+
cancelText: defaults.cancelText,
|
|
251
|
+
showCancel: false,
|
|
252
|
+
badge,
|
|
253
|
+
onOk: opts.onOk,
|
|
254
|
+
onActionError: opts.onActionError
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
get actionError() {
|
|
259
|
+
return controller.state.actionError;
|
|
260
|
+
},
|
|
261
|
+
confirm: (opts) => controller.request({
|
|
262
|
+
title: opts.title,
|
|
263
|
+
content: opts.content,
|
|
264
|
+
tone: opts.tone ?? "brand",
|
|
265
|
+
okText: opts.okText ?? defaults.okText,
|
|
266
|
+
cancelText: opts.cancelText ?? defaults.cancelText,
|
|
267
|
+
showCancel: true,
|
|
268
|
+
badge: opts.badge,
|
|
269
|
+
onOk: opts.onOk,
|
|
270
|
+
onActionError: opts.onActionError
|
|
271
|
+
}),
|
|
272
|
+
info: alert("info", "info"),
|
|
273
|
+
success: alert("success", "success"),
|
|
274
|
+
warning: alert("warning", "warning"),
|
|
275
|
+
error: alert("error", "danger"),
|
|
276
|
+
dispose: () => {
|
|
277
|
+
controller.dispose();
|
|
278
|
+
dialog.removeEventListener("open-change", onOpenChange);
|
|
279
|
+
if (exitListener) dialog.removeEventListener("exit-complete", exitListener);
|
|
280
|
+
cancelRoot.removeEventListener("click", onCancel);
|
|
281
|
+
okRoot.removeEventListener("click", onOk);
|
|
282
|
+
dialog.remove();
|
|
283
|
+
release();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/services/loading-bar-service.ts
|
|
289
|
+
function createLoadingBarService(options = {}) {
|
|
290
|
+
if (typeof document === "undefined") throw new Error("createLoadingBarService 需要 document;SSR 里请等到客户端再创建");
|
|
291
|
+
defineFeedbackElements();
|
|
292
|
+
const { target, tone = "brand", errorTone = "danger", translations, ...barProps } = options;
|
|
293
|
+
const { holder, release } = createServiceHolder(target);
|
|
294
|
+
const bar = document.createElement("xh-loading-bar");
|
|
295
|
+
const root = partNode("div", "root");
|
|
296
|
+
const track = partNode("div", "track");
|
|
297
|
+
const range = partNode("div", "range");
|
|
298
|
+
track.appendChild(range);
|
|
299
|
+
root.appendChild(track);
|
|
300
|
+
bar.appendChild(root);
|
|
301
|
+
bar.height = barProps.height;
|
|
302
|
+
bar.trickle = barProps.trickle;
|
|
303
|
+
bar.trickleSpeed = barProps.trickleSpeed;
|
|
304
|
+
bar.minimum = barProps.minimum;
|
|
305
|
+
bar.fadeDuration = barProps.fadeDuration;
|
|
306
|
+
bar.translations = translations;
|
|
307
|
+
let mounted = true;
|
|
308
|
+
try {
|
|
309
|
+
holder.appendChild(bar);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
mounted = reportServiceFailure("loading-bar", error);
|
|
312
|
+
release();
|
|
313
|
+
}
|
|
314
|
+
const apply = (state) => {
|
|
315
|
+
bar.tone = state.tone;
|
|
316
|
+
bar.value = state.value;
|
|
317
|
+
bar.loading = state.pending > 0;
|
|
318
|
+
};
|
|
319
|
+
const controller = createLoadingBarServiceController({
|
|
320
|
+
tone,
|
|
321
|
+
errorTone,
|
|
322
|
+
onStateChange: apply
|
|
323
|
+
});
|
|
324
|
+
if (!mounted) controller.dispose();
|
|
325
|
+
return {
|
|
326
|
+
start: controller.start,
|
|
327
|
+
finish: controller.finish,
|
|
328
|
+
error: controller.error,
|
|
329
|
+
finishAll: controller.finishAll,
|
|
330
|
+
set: controller.set,
|
|
331
|
+
dispose: () => {
|
|
332
|
+
controller.dispose();
|
|
333
|
+
bar.remove();
|
|
334
|
+
release();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/services/notification-service.ts
|
|
340
|
+
function createNotificationService(options = {}) {
|
|
341
|
+
if (typeof document === "undefined") throw new Error("createNotificationService 需要 document;SSR 里请等到客户端再创建");
|
|
342
|
+
defineFeedbackElements();
|
|
343
|
+
const { target, translations, ...queueProps } = options;
|
|
344
|
+
const { holder, release } = createServiceHolder(target);
|
|
345
|
+
const queue = document.createElement("xh-notification");
|
|
346
|
+
const root = partNode("div", "root");
|
|
347
|
+
queue.appendChild(root);
|
|
348
|
+
const groups = /* @__PURE__ */ new Map();
|
|
349
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
350
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
351
|
+
let mounted = true;
|
|
352
|
+
const controller = createFeedbackServiceController({
|
|
353
|
+
name: "notification",
|
|
354
|
+
dismissOnUnmounted: false,
|
|
355
|
+
onStateChange: () => render()
|
|
356
|
+
});
|
|
357
|
+
queue.placement = queueProps.placement;
|
|
358
|
+
queue.max = queueProps.max;
|
|
359
|
+
queue.dedupe = queueProps.dedupe;
|
|
360
|
+
queue.gap = queueProps.gap;
|
|
361
|
+
queue.duration = queueProps.duration;
|
|
362
|
+
queue.removeDelay = queueProps.removeDelay;
|
|
363
|
+
queue.pauseOnPageIdle = queueProps.pauseOnPageIdle;
|
|
364
|
+
queue.translations = translations;
|
|
365
|
+
ensureGroup(queueProps.placement ?? NOTIFICATION_PLACEMENT).hidden = true;
|
|
366
|
+
try {
|
|
367
|
+
holder.appendChild(queue);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
mounted = reportServiceFailure("notification", error);
|
|
370
|
+
release();
|
|
371
|
+
}
|
|
372
|
+
if (mounted) controller.attach({
|
|
373
|
+
create: (opts) => queue.create(opts),
|
|
374
|
+
update: (id, opts) => queue.updateItem(id, opts),
|
|
375
|
+
dismiss: (id) => queue.dismiss(id),
|
|
376
|
+
dismissAll: () => queue.dismissAll()
|
|
377
|
+
});
|
|
378
|
+
/**
|
|
379
|
+
* 堆叠区。每个位置一个,按九宫格固定顺序插入 root:
|
|
380
|
+
* 不按出现次序,否则同一批通知更换先后就会让整块界面重排。
|
|
381
|
+
*/
|
|
382
|
+
function ensureGroup(placement) {
|
|
383
|
+
const existing = groups.get(placement);
|
|
384
|
+
if (existing) return existing;
|
|
385
|
+
const group = partNode("div", "group");
|
|
386
|
+
group.setAttribute("placement", placement);
|
|
387
|
+
groups.set(placement, group);
|
|
388
|
+
const order = NOTIFICATION_PLACEMENTS.indexOf(placement);
|
|
389
|
+
const after = NOTIFICATION_PLACEMENTS.slice(order + 1).map((name) => groups.get(name)).find((node) => node != null);
|
|
390
|
+
root.insertBefore(group, after ?? null);
|
|
391
|
+
return group;
|
|
392
|
+
}
|
|
393
|
+
function ensureNode(item) {
|
|
394
|
+
let node = nodes.get(item.id);
|
|
395
|
+
if (!node) {
|
|
396
|
+
node = document.createElement("xh-notification-item");
|
|
397
|
+
nodes.set(item.id, node);
|
|
398
|
+
}
|
|
399
|
+
const shape = `${item.closable ? "c" : ""}${item.actionLabel ? "a" : ""}`;
|
|
400
|
+
if (shapes.get(item.id) !== shape) {
|
|
401
|
+
shapes.set(item.id, shape);
|
|
402
|
+
const card = partNode("div", "item");
|
|
403
|
+
card.appendChild(partNode("div", "item-indicator"));
|
|
404
|
+
card.appendChild(partNode("div", "item-title"));
|
|
405
|
+
card.appendChild(partNode("div", "item-description"));
|
|
406
|
+
if (item.actionLabel) card.appendChild(partNode("button", "item-action-trigger"));
|
|
407
|
+
if (item.closable) card.appendChild(partNode("button", "item-close-trigger"));
|
|
408
|
+
node.replaceChildren(card);
|
|
409
|
+
}
|
|
410
|
+
const action = node.querySelector("[data-xh-part=\"item-action-trigger\"]");
|
|
411
|
+
if (action && action.textContent !== (item.actionLabel ?? "")) action.textContent = item.actionLabel ?? "";
|
|
412
|
+
node.itemId = item.id;
|
|
413
|
+
node.titleText = resolveFeedbackServiceTitle(item);
|
|
414
|
+
node.description = item.description;
|
|
415
|
+
node.tone = item.tone;
|
|
416
|
+
node.loading = item.loading;
|
|
417
|
+
node.duration = item.duration;
|
|
418
|
+
node.removeDelay = item.removeDelay;
|
|
419
|
+
node.closable = item.closable;
|
|
420
|
+
node.pauseOnPageIdle = item.pauseOnPageIdle;
|
|
421
|
+
node.paused = controller.state.paused;
|
|
422
|
+
node.translations = translations;
|
|
423
|
+
return node;
|
|
424
|
+
}
|
|
425
|
+
function render() {
|
|
426
|
+
if (!mounted) return;
|
|
427
|
+
const items = queue.visibleNotifications;
|
|
428
|
+
controller.syncItems(items.map((item) => item.id));
|
|
429
|
+
const living = new Set(items.map((item) => item.id));
|
|
430
|
+
for (const [id, node] of nodes) {
|
|
431
|
+
if (living.has(id)) continue;
|
|
432
|
+
node.remove();
|
|
433
|
+
nodes.delete(id);
|
|
434
|
+
shapes.delete(id);
|
|
435
|
+
}
|
|
436
|
+
for (const placement of queue.placements) ensureGroup(placement);
|
|
437
|
+
for (const [placement, group] of groups) {
|
|
438
|
+
const list = queue.getItemsByPlacement(placement);
|
|
439
|
+
group.hidden = list.length === 0;
|
|
440
|
+
let previous = null;
|
|
441
|
+
for (const item of list) {
|
|
442
|
+
const node = ensureNode(item);
|
|
443
|
+
const anchor = previous ? previous.nextElementSibling : group.firstElementChild;
|
|
444
|
+
if (anchor !== node) group.insertBefore(node, anchor);
|
|
445
|
+
previous = node;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** 队列一变化就重绘。元素自己发出该事件,服务不必再监视状态机。 */
|
|
450
|
+
const onItems = () => render();
|
|
451
|
+
/** 走完退场的那条从队列里删掉;元素自己也收这条,这里只清回调表。 */
|
|
452
|
+
const onStatus = (event) => {
|
|
453
|
+
const detail = event.detail;
|
|
454
|
+
if (detail?.status === "unmounted") controller.unmounted(detail.id);
|
|
455
|
+
};
|
|
456
|
+
const onPress = (event) => {
|
|
457
|
+
const el = event.target;
|
|
458
|
+
if (el?.tagName.toLowerCase() !== "xh-notification-item") return;
|
|
459
|
+
const id = el.itemId;
|
|
460
|
+
if (id) controller.invokeAction(id);
|
|
461
|
+
};
|
|
462
|
+
queue.addEventListener("items-change", onItems);
|
|
463
|
+
queue.addEventListener("status-change", onStatus);
|
|
464
|
+
queue.addEventListener("action", onPress);
|
|
465
|
+
/** 入队一条;回调另存一张表,队列记录里只留文案。 */
|
|
466
|
+
const create = (opts = {}) => {
|
|
467
|
+
const { onAction, ...record } = opts;
|
|
468
|
+
const id = controller.create(record, onAction);
|
|
469
|
+
render();
|
|
470
|
+
return id;
|
|
471
|
+
};
|
|
472
|
+
const sugar = (tone) => (title, opts = {}) => create({
|
|
473
|
+
...opts,
|
|
474
|
+
tone,
|
|
475
|
+
title
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
create,
|
|
479
|
+
update: (id, opts) => {
|
|
480
|
+
controller.update(id, opts);
|
|
481
|
+
render();
|
|
482
|
+
},
|
|
483
|
+
dismiss: (id) => {
|
|
484
|
+
controller.dismiss(id);
|
|
485
|
+
render();
|
|
486
|
+
},
|
|
487
|
+
dismissAll: () => {
|
|
488
|
+
controller.dismissAll();
|
|
489
|
+
render();
|
|
490
|
+
},
|
|
491
|
+
info: sugar("info"),
|
|
492
|
+
success: sugar("success"),
|
|
493
|
+
warning: sugar("warning"),
|
|
494
|
+
danger: sugar("danger"),
|
|
495
|
+
pauseAll: controller.pauseAll,
|
|
496
|
+
resumeAll: controller.resumeAll,
|
|
497
|
+
dispose: () => {
|
|
498
|
+
mounted = false;
|
|
499
|
+
queue.removeEventListener("items-change", onItems);
|
|
500
|
+
queue.removeEventListener("status-change", onStatus);
|
|
501
|
+
queue.removeEventListener("action", onPress);
|
|
502
|
+
controller.dispose();
|
|
503
|
+
nodes.clear();
|
|
504
|
+
shapes.clear();
|
|
505
|
+
groups.clear();
|
|
506
|
+
queue.remove();
|
|
507
|
+
release();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/services/toast-service.ts
|
|
513
|
+
const parts = toastAnatomy.build();
|
|
514
|
+
function createToastService(options = {}) {
|
|
515
|
+
if (typeof document === "undefined") throw new Error("createToastService 需要 document;SSR 里请等到客户端再创建");
|
|
516
|
+
defineFeedbackElements();
|
|
517
|
+
const { target, placement = TOAST_PLACEMENT, gap = TOAST_GAP, max = TOAST_MAX, dedupe, toastTranslations, ...defaults } = options;
|
|
518
|
+
const serviceDefaults = {
|
|
519
|
+
...defaults,
|
|
520
|
+
pauseOnPageIdle: defaults.pauseOnPageIdle ?? true
|
|
521
|
+
};
|
|
522
|
+
const { holder, release } = createServiceHolder(target);
|
|
523
|
+
const group = document.createElement("div");
|
|
524
|
+
for (const [name, value] of Object.entries(parts.group.attrs)) group.setAttribute(name, String(value));
|
|
525
|
+
group.setAttribute("data-placement", placement);
|
|
526
|
+
group.setAttribute(DATA_INERT_EXEMPT, "");
|
|
527
|
+
holder.appendChild(group);
|
|
528
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
529
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
530
|
+
const controller = createFeedbackServiceController({
|
|
531
|
+
name: "toast",
|
|
532
|
+
idPrefix: "toast",
|
|
533
|
+
onStateChange: () => render()
|
|
534
|
+
});
|
|
535
|
+
const stack = createToastStackController({
|
|
536
|
+
group,
|
|
537
|
+
gap,
|
|
538
|
+
placement,
|
|
539
|
+
onInteractionChange: (active) => active ? controller.pauseAll() : controller.resumeAll()
|
|
540
|
+
});
|
|
541
|
+
const machineProps = () => withXhConfig("notification", {
|
|
542
|
+
placement,
|
|
543
|
+
max,
|
|
544
|
+
dedupe,
|
|
545
|
+
duration: serviceDefaults.duration,
|
|
546
|
+
removeDelay: serviceDefaults.removeDelay,
|
|
547
|
+
pauseOnPageIdle: serviceDefaults.pauseOnPageIdle
|
|
548
|
+
}, holder);
|
|
549
|
+
const runtime = createLitRuntime(createServiceReactiveHost(() => render()));
|
|
550
|
+
let service = null;
|
|
551
|
+
try {
|
|
552
|
+
service = createService(notificationMachine, {
|
|
553
|
+
props: machineProps,
|
|
554
|
+
runtime
|
|
555
|
+
});
|
|
556
|
+
runtime.mount();
|
|
557
|
+
} catch (error) {
|
|
558
|
+
reportServiceFailure("toast", error);
|
|
559
|
+
service = null;
|
|
560
|
+
release();
|
|
561
|
+
}
|
|
562
|
+
const api = () => service ? connectNotification(service, wcNormalize) : null;
|
|
563
|
+
if (service) controller.attach({
|
|
564
|
+
create: (opts) => api().create(opts),
|
|
565
|
+
update: (id, opts) => api().update(id, opts),
|
|
566
|
+
dismiss: (id) => api().dismiss(id),
|
|
567
|
+
dismissAll: () => api().dismissAll()
|
|
568
|
+
});
|
|
569
|
+
function ensureNode(item) {
|
|
570
|
+
const resolved = resolveToastServiceItem(item, serviceDefaults);
|
|
571
|
+
let node = nodes.get(item.id);
|
|
572
|
+
if (!node) {
|
|
573
|
+
node = document.createElement("xh-toast");
|
|
574
|
+
nodes.set(item.id, node);
|
|
575
|
+
}
|
|
576
|
+
const shape = `${resolved.closable ? "c" : ""}${resolved.actionLabel ? "a" : ""}${resolved.description ? "d" : ""}`;
|
|
577
|
+
if (shapes.get(item.id) !== shape) {
|
|
578
|
+
shapes.set(item.id, shape);
|
|
579
|
+
const root = partNode("div", "root");
|
|
580
|
+
root.appendChild(partNode("span", "indicator"));
|
|
581
|
+
const content = partNode("div", "content");
|
|
582
|
+
content.appendChild(partNode("div", "title"));
|
|
583
|
+
if (resolved.description) content.appendChild(partNode("div", "description"));
|
|
584
|
+
root.appendChild(content);
|
|
585
|
+
if (resolved.actionLabel) root.appendChild(partNode("button", "action-trigger"));
|
|
586
|
+
if (resolved.closable) root.appendChild(partNode("button", "close-trigger"));
|
|
587
|
+
node.replaceChildren(root);
|
|
588
|
+
}
|
|
589
|
+
const action = node.querySelector("[data-xh-part=\"action-trigger\"]");
|
|
590
|
+
if (action && action.textContent !== (resolved.actionLabel ?? "")) action.textContent = resolved.actionLabel ?? "";
|
|
591
|
+
node.toastId = resolved.id;
|
|
592
|
+
node.titleText = resolved.title;
|
|
593
|
+
node.descriptionText = resolved.description;
|
|
594
|
+
node.tone = resolved.tone;
|
|
595
|
+
node.loading = resolved.loading;
|
|
596
|
+
node.duration = resolved.duration;
|
|
597
|
+
node.removeDelay = resolved.removeDelay;
|
|
598
|
+
node.closable = resolved.closable;
|
|
599
|
+
node.pauseOnPageIdle = resolved.pauseOnPageIdle;
|
|
600
|
+
node.paused = controller.state.paused;
|
|
601
|
+
node.translations = toastTranslations;
|
|
602
|
+
return node;
|
|
603
|
+
}
|
|
604
|
+
/** 队列里没有的节点撤掉,剩下的按记录刷一遍属性并排到该在的位置。 */
|
|
605
|
+
function render() {
|
|
606
|
+
if (!service) return;
|
|
607
|
+
const items = visibleNotifications(service.context.get("items"), max, placement);
|
|
608
|
+
controller.syncItems(items.map((item) => item.id));
|
|
609
|
+
const living = new Set(items.map((item) => item.id));
|
|
610
|
+
for (const [id, node] of nodes) {
|
|
611
|
+
if (living.has(id)) continue;
|
|
612
|
+
node.remove();
|
|
613
|
+
nodes.delete(id);
|
|
614
|
+
shapes.delete(id);
|
|
615
|
+
}
|
|
616
|
+
group.setAttribute("data-count", String(items.length));
|
|
617
|
+
group.toggleAttribute("hidden", items.length === 0);
|
|
618
|
+
let previous = null;
|
|
619
|
+
for (const item of items) {
|
|
620
|
+
const node = ensureNode(item);
|
|
621
|
+
const anchor = previous ? previous.nextElementSibling : group.firstElementChild;
|
|
622
|
+
if (anchor !== node) group.insertBefore(node, anchor);
|
|
623
|
+
previous = node;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/** 走完退场的那条从队列里删掉。 */
|
|
627
|
+
const onStatus = (event) => {
|
|
628
|
+
if (event.target?.tagName.toLowerCase() !== "xh-toast") return;
|
|
629
|
+
const detail = event.detail;
|
|
630
|
+
if (detail?.status === "unmounted") controller.unmounted(detail.id);
|
|
631
|
+
};
|
|
632
|
+
/** 行内动作按 id 现查那张回调表。 */
|
|
633
|
+
const onPress = (event) => {
|
|
634
|
+
if (event.target?.tagName.toLowerCase() !== "xh-toast") return;
|
|
635
|
+
const detail = event.detail;
|
|
636
|
+
if (detail?.id) controller.invokeAction(detail.id);
|
|
637
|
+
};
|
|
638
|
+
group.addEventListener("status-change", onStatus);
|
|
639
|
+
group.addEventListener("action", onPress);
|
|
640
|
+
/** 入队一条;回调另存一张表,队列记录里只留文案。 */
|
|
641
|
+
const create = (opts = {}) => {
|
|
642
|
+
const { onAction, ...record } = opts;
|
|
643
|
+
return controller.create(record, onAction);
|
|
644
|
+
};
|
|
645
|
+
const sugar = (tone) => (message, opts = {}) => create({
|
|
646
|
+
...opts,
|
|
647
|
+
tone,
|
|
648
|
+
title: message
|
|
649
|
+
});
|
|
650
|
+
return {
|
|
651
|
+
create,
|
|
652
|
+
update: controller.update,
|
|
653
|
+
dismiss: controller.dismiss,
|
|
654
|
+
dismissAll: controller.dismissAll,
|
|
655
|
+
info: sugar("info"),
|
|
656
|
+
success: sugar("success"),
|
|
657
|
+
warning: sugar("warning"),
|
|
658
|
+
danger: sugar("danger"),
|
|
659
|
+
loading: (message, opts = {}) => create({
|
|
660
|
+
...opts,
|
|
661
|
+
loading: true,
|
|
662
|
+
title: message
|
|
663
|
+
}),
|
|
664
|
+
promise: (input, opts) => {
|
|
665
|
+
const { loading, success, error, ...rest } = opts;
|
|
666
|
+
const running = typeof input === "function" ? input() : input;
|
|
667
|
+
const { onAction, ...record } = rest;
|
|
668
|
+
return controller.trackPromise(running, {
|
|
669
|
+
...record,
|
|
670
|
+
loading: true,
|
|
671
|
+
title: loading
|
|
672
|
+
}, (value) => ({
|
|
673
|
+
loading: false,
|
|
674
|
+
tone: "success",
|
|
675
|
+
title: typeof success === "function" ? success(value) : success
|
|
676
|
+
}), (reason) => ({
|
|
677
|
+
loading: false,
|
|
678
|
+
tone: "danger",
|
|
679
|
+
title: typeof error === "function" ? error(reason) : error
|
|
680
|
+
}), onAction);
|
|
681
|
+
},
|
|
682
|
+
pauseAll: controller.pauseAll,
|
|
683
|
+
resumeAll: controller.resumeAll,
|
|
684
|
+
dispose: () => {
|
|
685
|
+
stack.dispose();
|
|
686
|
+
group.removeEventListener("status-change", onStatus);
|
|
687
|
+
group.removeEventListener("action", onPress);
|
|
688
|
+
controller.dispose();
|
|
689
|
+
runtime.unmount();
|
|
690
|
+
service = null;
|
|
691
|
+
nodes.clear();
|
|
692
|
+
shapes.clear();
|
|
693
|
+
group.remove();
|
|
694
|
+
release();
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
//#endregion
|
|
699
|
+
export { createDialogService, createLoadingBarService, createNotificationService, createToastService };
|
|
700
|
+
|
|
701
|
+
//# sourceMappingURL=services.js.map
|