@yoyaflow/yoya-ui 0.3.2 → 0.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.
Files changed (42) hide show
  1. package/README.md +407 -124
  2. package/README.zh-CN.md +375 -131
  3. package/dist/yoya.core.chunk.js +4275 -0
  4. package/dist/yoya.core.chunk.min.js +9 -0
  5. package/dist/yoya.core.js +3 -1779
  6. package/dist/yoya.core.min.js +1 -0
  7. package/dist/yoya.devtools.js +3 -0
  8. package/dist/yoya.devtools.min.js +1 -0
  9. package/dist/yoya.echart.js +182 -592
  10. package/dist/yoya.echart.min.js +1 -0
  11. package/dist/yoya.router.full.js +5271 -0
  12. package/dist/yoya.router.full.min.js +43 -0
  13. package/dist/yoya.router.js +1330 -0
  14. package/dist/yoya.router.min.js +35 -0
  15. package/dist/yoya.three.js +360 -0
  16. package/dist/yoya.three.min.js +1 -0
  17. package/dist/yoya.ui-router.full.js +21843 -0
  18. package/dist/yoya.ui-router.full.min.js +43 -0
  19. package/dist/yoya.ui-router.umd.js +43 -0
  20. package/dist/yoya.ui-router.umd.min.js +43 -0
  21. package/dist/yoya.ui.css +80 -0
  22. package/dist/yoya.ui.full.js +20169 -0
  23. package/dist/yoya.ui.full.min.js +1 -0
  24. package/dist/yoya.ui.js +12227 -9083
  25. package/dist/yoya.ui.min.js +1 -0
  26. package/package.json +27 -11
  27. package/types/core.d.ts +122 -1
  28. package/types/data-display.d.ts +44 -2
  29. package/types/devtools.d.ts +156 -0
  30. package/types/feedback.d.ts +13 -0
  31. package/types/form.d.ts +1 -1
  32. package/types/index.d.ts +1 -1
  33. package/types/ssr.d.ts +27 -7
  34. package/types/three.d.ts +77 -0
  35. package/types/yoya.devtools.d.ts +5 -0
  36. package/types/yoya.router.d.ts +7 -0
  37. package/types/yoya.three.d.ts +4 -0
  38. package/types/yoya.ui-router.d.ts +19 -0
  39. package/types/yoya.ui.d.ts +2 -7
  40. package/dist/yoya-ui.umd.js +0 -35
  41. package/dist/yoya.ssr.js +0 -2856
  42. package/types/yoya.ssr.d.ts +0 -9
@@ -0,0 +1,1330 @@
1
+ import { A as themeBorder, Di as applyElementOptions, E as replaceChildren, Ei as ViewNode, Mi as registerChildFactories, Pi as vText, _i as bindDocumentEvent, a as parseState, b as normalizeChildren, c as resolveLocale, i as mount, j as themeValue, ji as normalizeSetupArguments, l as serializeState, n as hydrate, o as renderPage, r as hydrateOrMount, s as renderToString, t as PageDocumentNode, vi as bindWindowEvent, wi as ElementNode, yi as injectDocumentStyle } from "./yoya.core.chunk.js";
2
+
3
+ //#region src/router/router.js
4
+ const maxVisibleTitles = 8;
5
+ let scrollbarStyle = null;
6
+ function ensureScrollbarStyle() {
7
+ if (scrollbarStyle) return;
8
+ scrollbarStyle = injectDocumentStyle(`.yoya-vrouter-views-titlebar,
9
+ .yoya-vrouter-views-popup {
10
+ -ms-overflow-style: none;
11
+ scrollbar-width: none;
12
+ }
13
+ .yoya-vrouter-views-titlebar::-webkit-scrollbar,
14
+ .yoya-vrouter-views-popup::-webkit-scrollbar {
15
+ display: none;
16
+ height: 0;
17
+ width: 0;
18
+ }
19
+ .yoya-vrouter-views-popup-item:hover {
20
+ background: var(--yoya-color-surface-hover, #eef3f9);
21
+ }
22
+ .yoya-vrouter-views-popup-item[aria-current='true'] {
23
+ background: var(--yoya-color-primary-subtle, #e8f0fe);
24
+ color: var(--yoya-color-primary, #1f6feb);
25
+ }
26
+ .yoya-vrouter-views-popup-close {
27
+ color: var(--yoya-color-text-secondary, #57606a);
28
+ opacity: 0.65;
29
+ }
30
+ .yoya-vrouter-views-popup-item:hover .yoya-vrouter-views-popup-close,
31
+ .yoya-vrouter-views-popup-close:hover {
32
+ color: var(--yoya-color-text-danger, #b91c1c);
33
+ opacity: 1;
34
+ }
35
+ .yoya-vrouter-views-context-item:hover {
36
+ background: var(--yoya-color-surface-hover, #f6f8fa);
37
+ }
38
+ .yoya-vrouter-views-context-separator {
39
+ background: var(--yoya-color-border, #d0d7de);
40
+ height: 1px;
41
+ margin: 4px 6px;
42
+ }`, "data-yoya-vrouter-views-popup-style");
43
+ }
44
+ function createRouterViewsStorageKey(routerInstance) {
45
+ const source = [routerInstance._defaultPath || "/", routerInstance._routes.map((route) => route.pattern).join("|")].join("::");
46
+ let hash = 0;
47
+ for (let index = 0; index < source.length; index += 1) hash = hash * 31 + source.charCodeAt(index) | 0;
48
+ return `yoya-ui:router-views:${(hash >>> 0).toString(36)}`;
49
+ }
50
+ function readSavedTabs(storageKey) {
51
+ if (typeof localStorage === "undefined") return null;
52
+ try {
53
+ const saved = JSON.parse(localStorage.getItem(storageKey) || "null");
54
+ return Array.isArray(saved?.paths) ? saved : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Router 是一个很轻的 hash 路由出口。
61
+ * 它负责路径匹配、参数提取、守卫和把路由视图渲染到自身节点内。
62
+ */
63
+ var Router = class extends ElementNode {
64
+ constructor(setup = null) {
65
+ super("div");
66
+ this._routes = [];
67
+ this._defaultPath = null;
68
+ this._notFoundView = null;
69
+ this._beforeEach = null;
70
+ this._currentPath = "/";
71
+ this._currentParams = {};
72
+ this._currentQuery = {};
73
+ this._currentRoute = null;
74
+ this._currentView = null;
75
+ this._loadingView = null;
76
+ this._errorView = null;
77
+ this._navigationGeneration = 0;
78
+ this._outlet = this;
79
+ this._subscribers = /* @__PURE__ */ new Set();
80
+ this._ignoreNextHashPath = null;
81
+ this._mode = "hash";
82
+ this._started = false;
83
+ this._onHashChange = () => this._handleHashChange();
84
+ this._onPopState = () => this._handlePopState();
85
+ this.attr("data-yoya-router", "");
86
+ if (setup !== null) this.setup(setup);
87
+ }
88
+ setup(setup) {
89
+ if (typeof setup === "function") {
90
+ setup(this);
91
+ return this;
92
+ }
93
+ return super.setup(setup);
94
+ }
95
+ /**
96
+ * 设置无 hash 时进入的默认路径。
97
+ */
98
+ default(path) {
99
+ this._defaultPath = normalizePath(path);
100
+ return this;
101
+ }
102
+ mode(value) {
103
+ if (value === void 0) return this._mode;
104
+ const nextMode = value === "history" ? "history" : "hash";
105
+ if (nextMode === this._mode) return this;
106
+ const wasStarted = this._started;
107
+ if (wasStarted) this.stop();
108
+ this._mode = nextMode;
109
+ if (wasStarted) this.start();
110
+ return this;
111
+ }
112
+ /**
113
+ * 添加路由。config 可以是视图函数,也可以是 { view/component, beforeEnter }。
114
+ */
115
+ route(pattern, config) {
116
+ const route = normalizeRoute(pattern, config);
117
+ this._routes.push(route);
118
+ return this;
119
+ }
120
+ /**
121
+ * 设置未匹配路由的视图函数。
122
+ */
123
+ notFound(view) {
124
+ this._notFoundView = view;
125
+ return this;
126
+ }
127
+ /**
128
+ * 设置异步路由视图加载中的默认视图。支持 ViewNode、文本或 (context) => view 函数。
129
+ */
130
+ loading(view) {
131
+ this._loadingView = view;
132
+ return this;
133
+ }
134
+ /**
135
+ * 设置异步路由视图加载失败的默认视图。支持 ViewNode、文本或 (error, context) => view 函数。
136
+ */
137
+ error(view) {
138
+ this._errorView = view;
139
+ return this;
140
+ }
141
+ /**
142
+ * 设置全局前置守卫,返回 false 时阻止导航。
143
+ */
144
+ beforeEach(guard) {
145
+ this._beforeEach = guard;
146
+ return this;
147
+ }
148
+ start() {
149
+ if (!this._started) {
150
+ if (this._mode === "history") this._stopListening = bindWindowEvent("popstate", this._onPopState);
151
+ else this._stopListening = bindWindowEvent("hashchange", this._onHashChange);
152
+ this._started = true;
153
+ }
154
+ const currentPath = readPath(this._mode);
155
+ if ((this._mode === "history" ? currentPath === "/" || currentPath === "/index.html" : !window.location.hash) && this._defaultPath) return this.navigate(this._defaultPath, { replace: true });
156
+ return this.refresh();
157
+ }
158
+ stop() {
159
+ if (this._started) {
160
+ this._stopListening?.();
161
+ this._stopListening = null;
162
+ this._started = false;
163
+ }
164
+ return this;
165
+ }
166
+ /**
167
+ * 导航到指定路径。replace 为 true 时不新增浏览器历史记录。
168
+ */
169
+ navigate(path, options = {}) {
170
+ const nextPath = normalizePath(path);
171
+ const resolved = this._resolve(nextPath);
172
+ if (!this._canEnter(resolved.context)) return this;
173
+ if (writePath(nextPath, options, this._mode) && !options.replace && this._mode === "hash") this._ignoreNextHashPath = nextPath;
174
+ this._renderResolved(resolved);
175
+ return this;
176
+ }
177
+ refresh() {
178
+ const nextPath = readPath(this._mode);
179
+ const resolved = this._resolve(nextPath);
180
+ if (!this._canEnter(resolved.context)) return this;
181
+ this._renderResolved(resolved);
182
+ return this;
183
+ }
184
+ /**
185
+ * 服务端渲染入口:按路径解析并渲染匹配视图到自身节点,不依赖 window。
186
+ * 守卫返回 false 时不提交;异步视图在服务端序列化其 loading 视图。
187
+ */
188
+ renderPath(path) {
189
+ const resolved = this._resolve(path);
190
+ if (!this._canEnter(resolved.context)) return this;
191
+ this._renderResolved(resolved);
192
+ return this;
193
+ }
194
+ currentPath() {
195
+ return this._currentPath;
196
+ }
197
+ currentParams() {
198
+ return { ...this._currentParams };
199
+ }
200
+ currentQuery() {
201
+ return { ...this._currentQuery };
202
+ }
203
+ currentRoute() {
204
+ return this._currentRoute;
205
+ }
206
+ currentView() {
207
+ return this._currentView;
208
+ }
209
+ outlet(value) {
210
+ if (value === void 0) return this._outlet;
211
+ if (!(value instanceof ElementNode)) throw new TypeError("Router outlet must be an ElementNode");
212
+ this._outlet = value;
213
+ return this;
214
+ }
215
+ subscribe(listener) {
216
+ if (typeof listener !== "function") throw new TypeError("Router subscriber must be a function");
217
+ this._subscribers.add(listener);
218
+ return () => this._subscribers.delete(listener);
219
+ }
220
+ go(delta) {
221
+ window.history.go(delta);
222
+ return this;
223
+ }
224
+ back() {
225
+ return this.go(-1);
226
+ }
227
+ forward() {
228
+ return this.go(1);
229
+ }
230
+ destroy() {
231
+ this.stop();
232
+ this._navigationGeneration += 1;
233
+ this._destroyCurrentView();
234
+ this._subscribers.clear();
235
+ return super.destroy();
236
+ }
237
+ _resolve(path) {
238
+ const routeMatch = this._routes.map((route) => ({
239
+ route,
240
+ match: matchRoute(route.pattern, path)
241
+ })).find(({ match }) => match);
242
+ if (routeMatch) return {
243
+ context: createRouteContext(this, path, routeMatch.route, routeMatch.match),
244
+ route: routeMatch.route,
245
+ view: routeMatch.route.view
246
+ };
247
+ const parsed = parsePath(path);
248
+ return {
249
+ context: createRouteContext(this, path, null, {
250
+ params: {},
251
+ pathname: parsed.pathname,
252
+ query: parsed.query
253
+ }),
254
+ route: null,
255
+ view: this._notFoundView
256
+ };
257
+ }
258
+ _handleHashChange() {
259
+ const nextPath = readHashPath();
260
+ if (this._ignoreNextHashPath === nextPath) {
261
+ this._ignoreNextHashPath = null;
262
+ return this;
263
+ }
264
+ this._ignoreNextHashPath = null;
265
+ if (nextPath === this._currentPath) return this;
266
+ return this.refresh();
267
+ }
268
+ _handlePopState() {
269
+ this._ignoreNextHashPath = null;
270
+ return this.refresh();
271
+ }
272
+ _canEnter(to) {
273
+ const from = {
274
+ params: this.currentParams(),
275
+ path: this._currentPath,
276
+ query: this.currentQuery(),
277
+ route: this._currentRoute
278
+ };
279
+ if (this._beforeEach && this._beforeEach(to, from, this) === false) return false;
280
+ if (to.route?.beforeEnter && to.route.beforeEnter(to, from, this) === false) return false;
281
+ return true;
282
+ }
283
+ _renderResolved(resolved) {
284
+ const { context, route, view } = resolved;
285
+ const result = typeof view === "function" ? view(context) : view;
286
+ this._navigationGeneration += 1;
287
+ if (!isPromiseLike(result)) {
288
+ this._commitView(resolved, normalizeRouteView(result, context));
289
+ return;
290
+ }
291
+ const generation = this._navigationGeneration;
292
+ this._commitView(resolved, this._buildLoadingView(route, context));
293
+ Promise.resolve(result).then((value) => {
294
+ if (generation !== this._navigationGeneration || this._deleted) return value;
295
+ let nextView;
296
+ try {
297
+ nextView = normalizeRouteView(value, context);
298
+ } catch (error) {
299
+ this._commitView(resolved, this._buildErrorView(route, context, error));
300
+ return value;
301
+ }
302
+ this._commitView(resolved, nextView);
303
+ return value;
304
+ }, (error) => {
305
+ if (generation !== this._navigationGeneration || this._deleted) return error;
306
+ this._commitView(resolved, this._buildErrorView(route, context, error));
307
+ return error;
308
+ });
309
+ }
310
+ _commitView(resolved, nextView) {
311
+ const { context, route } = resolved;
312
+ const outlet = this._outlet;
313
+ this._destroyCurrentView();
314
+ outlet._children = [];
315
+ if (outlet._el) outlet._el.replaceChildren();
316
+ outlet.child(nextView);
317
+ this._currentPath = context.path;
318
+ this._currentParams = context.params;
319
+ this._currentQuery = context.query;
320
+ this._currentRoute = route;
321
+ this._currentView = nextView;
322
+ this._subscribers.forEach((listener) => listener(context, this));
323
+ }
324
+ _buildLoadingView(route, context) {
325
+ return resolveRouteEntry(route?.loading ?? this._loadingView ?? defaultLoadingView, [context]);
326
+ }
327
+ _buildErrorView(route, context, error) {
328
+ return resolveRouteEntry(route?.error ?? this._errorView ?? defaultErrorView, [error, context]);
329
+ }
330
+ _destroyCurrentView() {
331
+ if (this._currentView?.destroy) this._currentView.destroy();
332
+ this._currentView = null;
333
+ }
334
+ };
335
+ function createRouter(first = null, second = null, third = null) {
336
+ const args = normalizeSetupArguments(first, second, third);
337
+ const node = new Router(args.first);
338
+ applyElementOptions(node, args.options);
339
+ if (typeof args.callback === "function") args.callback(node);
340
+ return node;
341
+ }
342
+ const router = createRouter;
343
+ function vRoute(pattern, config) {
344
+ return {
345
+ config,
346
+ pattern
347
+ };
348
+ }
349
+ function vRouter(first = null, second = null, third = null) {
350
+ const args = normalizeSetupArguments(first, second, third);
351
+ const node = new Router();
352
+ node.vRoute = (pattern, config) => {
353
+ node.route(pattern, config);
354
+ return node;
355
+ };
356
+ applyDeclarativeRouterSetup(node, args.first);
357
+ applyElementOptions(node, args.options);
358
+ if (typeof args.callback === "function") args.callback(node);
359
+ return node;
360
+ }
361
+ function vLink(routerInstance, setup = null, callback = null) {
362
+ assertRouter(routerInstance);
363
+ const node = new ElementNode("a");
364
+ const state = {
365
+ exact: true,
366
+ label: null,
367
+ params: {},
368
+ query: {},
369
+ replace: false,
370
+ to: "/"
371
+ };
372
+ const labelNode = new ElementNode("span").className("yoya-vlink-label");
373
+ node.className("yoya-vlink");
374
+ node.attr("data-router-link", "true");
375
+ node.child(labelNode);
376
+ node.to = (value) => updateLinkValue(node, state, "to", value);
377
+ node.params = (value) => updateLinkValue(node, state, "params", value || {});
378
+ node.query = (value) => updateLinkValue(node, state, "query", value || {});
379
+ node.replace = (value) => updateLinkValue(node, state, "replace", Boolean(value));
380
+ node.exact = (value) => updateLinkValue(node, state, "exact", Boolean(value));
381
+ node.label = (value) => {
382
+ if (value === void 0) return state.label;
383
+ state.label = value;
384
+ replaceChildren(labelNode, normalizeChildren(value));
385
+ return node;
386
+ };
387
+ applyLinkSetup(node, state, setup);
388
+ updateLink(node, state, routerInstance);
389
+ node.on("click", (event) => {
390
+ if (!shouldHandleLinkClick(event, node)) return;
391
+ event.preventDefault();
392
+ routerInstance.navigate(buildLinkPath(state.to, state.params, state.query), { replace: state.replace });
393
+ });
394
+ const unsubscribe = routerInstance.subscribe(() => updateLink(node, state, routerInstance));
395
+ const destroy = node.destroy.bind(node);
396
+ node.destroy = () => {
397
+ unsubscribe();
398
+ return destroy();
399
+ };
400
+ if (typeof callback === "function") callback(node);
401
+ return node;
402
+ }
403
+ function vRouterView(routerInstance, setup = null, callback = null) {
404
+ assertRouter(routerInstance);
405
+ const node = new ElementNode("div");
406
+ node.className("yoya-vrouter-view");
407
+ node.attr("data-router-view", "true");
408
+ if (typeof setup === "function") setup(node);
409
+ else if (setup) node.setup(setup);
410
+ routerInstance.outlet(node);
411
+ const destroy = node.destroy.bind(node);
412
+ node.destroy = () => {
413
+ if (routerInstance.outlet() === node) routerInstance.outlet(routerInstance);
414
+ return destroy();
415
+ };
416
+ if (typeof callback === "function") callback(node);
417
+ return node;
418
+ }
419
+ function vRouterViews(routerInstance, setup = null, callback = null) {
420
+ assertRouter(routerInstance);
421
+ const node = new ElementNode("div");
422
+ const titleNode = new ElementNode("header").className("yoya-vrouter-views-titlebar").attr({
423
+ role: "tablist",
424
+ "aria-label": "已打开页面"
425
+ });
426
+ const contentNode = new ElementNode("div").className("yoya-vrouter-views-content");
427
+ const moreButtonText = vText("⋯");
428
+ const moreButton = new ElementNode("button").className("yoya-vrouter-views-expand").attr({
429
+ type: "button",
430
+ "aria-expanded": "false",
431
+ "aria-label": "展开全部标签"
432
+ }).styles({
433
+ alignItems: "center",
434
+ background: "transparent",
435
+ border: "0",
436
+ color: themeValue("color-text-secondary", "#57606a"),
437
+ cursor: "pointer",
438
+ display: "none",
439
+ flexShrink: "0",
440
+ font: "inherit",
441
+ gap: "6px",
442
+ height: "24px",
443
+ justifyContent: "center",
444
+ lineHeight: "1",
445
+ marginBottom: "0",
446
+ marginLeft: "auto",
447
+ minWidth: "24px",
448
+ padding: "0",
449
+ position: "sticky",
450
+ right: "8px",
451
+ zIndex: "2"
452
+ }).child(moreButtonText);
453
+ const popup = new ElementNode("div").className("yoya-vrouter-views-popup").attr({
454
+ role: "menu",
455
+ "aria-label": "已打开页面"
456
+ }).styles({
457
+ background: themeValue("color-surface", "#ffffff"),
458
+ border: themeBorder("color-border", "#d0d7de"),
459
+ borderRadius: "8px",
460
+ boxShadow: "0 12px 28px rgba(15, 23, 42, 0.18)",
461
+ display: "none",
462
+ maxHeight: "280px",
463
+ maxWidth: "260px",
464
+ minWidth: "180px",
465
+ msOverflowStyle: "none",
466
+ overflowY: "auto",
467
+ padding: "6px",
468
+ position: "fixed",
469
+ scrollbarWidth: "none",
470
+ zIndex: "100"
471
+ });
472
+ const state = {
473
+ lockTitle: false,
474
+ overflow: false,
475
+ persist: true,
476
+ popupOpen: false,
477
+ storageKey: createRouterViewsStorageKey(routerInstance),
478
+ suppressPersist: false,
479
+ tabs: /* @__PURE__ */ new Map(),
480
+ title: "工作区",
481
+ titleResolver: null,
482
+ titlePosition: "top"
483
+ };
484
+ node.className("yoya-vrouter-views");
485
+ node.styles({
486
+ border: themeBorder("color-border", "#d0d7de"),
487
+ boxSizing: "border-box",
488
+ overflow: "hidden"
489
+ });
490
+ titleNode.styles({
491
+ background: themeValue("color-surface-hover", "#f6f8fa"),
492
+ borderBottom: themeBorder("color-border", "#d0d7de"),
493
+ boxSizing: "border-box",
494
+ color: themeValue("color-text-secondary", "#57606a"),
495
+ display: "flex",
496
+ flexWrap: "nowrap",
497
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
498
+ fontSize: "13px",
499
+ gap: "0px",
500
+ msOverflowStyle: "none",
501
+ overflowX: "auto",
502
+ overflowY: "hidden",
503
+ padding: "0px 0px",
504
+ scrollbarWidth: "none",
505
+ width: "100%"
506
+ });
507
+ contentNode.className("yoya-vrouter-views-content");
508
+ contentNode.styles({
509
+ minHeight: "120px",
510
+ padding: "16px"
511
+ });
512
+ node.child(titleNode, contentNode, popup);
513
+ const applyLockedTitle = () => {
514
+ const vertical = state.titlePosition !== "top";
515
+ if (state.lockTitle) {
516
+ node.styles({
517
+ display: "flex",
518
+ flexDirection: vertical ? "row" : "column",
519
+ height: "100%",
520
+ minHeight: "0",
521
+ overflow: "hidden"
522
+ });
523
+ titleNode.style("flex", "0 0 auto");
524
+ contentNode.styles({
525
+ flex: "1 1 auto",
526
+ minHeight: "0",
527
+ minWidth: "0",
528
+ overflow: "auto"
529
+ });
530
+ return;
531
+ }
532
+ node.styles({
533
+ flexDirection: null,
534
+ height: null,
535
+ minHeight: null,
536
+ overflow: "hidden"
537
+ });
538
+ titleNode.style("flex", null);
539
+ contentNode.styles({
540
+ flex: vertical ? "1 1 auto" : null,
541
+ minHeight: "120px",
542
+ minWidth: vertical ? "0" : null,
543
+ overflow: null
544
+ });
545
+ };
546
+ const applyTitlePosition = (position) => {
547
+ const vertical = position !== "top";
548
+ node.styles({
549
+ alignItems: vertical ? "stretch" : null,
550
+ display: vertical ? "flex" : null
551
+ });
552
+ titleNode.styles({
553
+ background: themeValue("color-surface-hover", "#f6f8fa"),
554
+ borderBottom: vertical ? null : themeBorder("color-border", "#d0d7de"),
555
+ borderLeft: position === "right" ? "1px solid" : null,
556
+ borderLeftColor: position === "right" ? themeValue("color-border", "#d0d7de") : null,
557
+ borderLeftStyle: position === "right" ? "solid" : null,
558
+ borderLeftWidth: position === "right" ? "1px" : null,
559
+ borderRight: position === "left" ? "1px solid" : null,
560
+ borderRightColor: position === "left" ? themeValue("color-border", "#d0d7de") : null,
561
+ borderRightStyle: position === "left" ? "solid" : null,
562
+ borderRightWidth: position === "left" ? "1px" : null,
563
+ boxSizing: "border-box",
564
+ color: themeValue("color-text-secondary", "#57606a"),
565
+ display: "flex",
566
+ flexDirection: vertical ? "column" : "row",
567
+ flexShrink: vertical ? "0" : null,
568
+ flexWrap: "nowrap",
569
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
570
+ fontSize: "13px",
571
+ gap: "0px",
572
+ msOverflowStyle: "none",
573
+ overflowX: vertical ? "hidden" : "auto",
574
+ overflowY: vertical ? "auto" : "hidden",
575
+ padding: vertical ? "10px 8px" : "0px 0px",
576
+ scrollbarWidth: "none",
577
+ width: vertical ? null : "100%"
578
+ });
579
+ contentNode.styles({
580
+ flex: vertical ? "1 1 auto" : null,
581
+ minWidth: vertical ? "0" : null
582
+ });
583
+ const orderedChildren = position === "right" ? [
584
+ contentNode,
585
+ titleNode,
586
+ popup
587
+ ] : [
588
+ titleNode,
589
+ contentNode,
590
+ popup
591
+ ];
592
+ node.clearChildren().child(orderedChildren);
593
+ if (node._el) orderedChildren.forEach((child) => {
594
+ const childElement = child.renderDom();
595
+ if (childElement) node._el.appendChild(childElement);
596
+ });
597
+ applyLockedTitle();
598
+ };
599
+ const resolveTitle = (context = {}) => {
600
+ let title = state.title;
601
+ const routeTitle = context.route?.title;
602
+ if (routeTitle !== void 0) title = typeof routeTitle === "function" ? routeTitle(context) : routeTitle;
603
+ if (state.titleResolver) {
604
+ const resolved = state.titleResolver(context);
605
+ if (resolved !== void 0 && resolved !== null) title = typeof resolved === "function" ? resolved(context) : resolved;
606
+ }
607
+ return title ?? "";
608
+ };
609
+ const styleTitleTab = (entry, active) => {
610
+ const tab = entry.tab;
611
+ const position = state.titlePosition;
612
+ const vertical = position !== "top";
613
+ entry.label.attr("aria-selected", String(active));
614
+ tab.styles({
615
+ background: active ? themeValue("color-surface", "#ffffff") : themeValue("color-surface-active", "#eaeef2"),
616
+ border: themeBorder("color-border", "#d0d7de"),
617
+ borderBottomColor: position === "top" ? active ? themeValue("color-surface", "#ffffff") : themeValue("color-border", "#d0d7de") : themeValue("color-border", "#d0d7de"),
618
+ borderLeftColor: "transparent",
619
+ borderRightColor: position === "left" ? active ? themeValue("color-surface", "#ffffff") : themeValue("color-border", "#d0d7de") : themeValue("color-border", "#d0d7de"),
620
+ color: active ? themeValue("color-text", "#24292f") : themeValue("color-text-secondary", "#57606a"),
621
+ fontWeight: active ? "600" : "400",
622
+ marginBottom: position === "top" ? "-9px" : "0",
623
+ marginLeft: position === "right" ? "-9px" : "0",
624
+ marginRight: position === "left" ? "-9px" : "0",
625
+ padding: vertical ? "8px 10px" : "7px 14px 8px"
626
+ });
627
+ };
628
+ const setOverflowButtonVisible = (visible) => {
629
+ state.overflow = Boolean(visible);
630
+ node.attr("data-title-overflow", state.overflow ? "true" : null);
631
+ moreButton.style("display", state.overflow ? "inline-flex" : "none");
632
+ };
633
+ const updateOverflow = () => {
634
+ if (state.titlePosition !== "top" || state.tabs.size <= maxVisibleTitles) {
635
+ setOverflowButtonVisible(false);
636
+ return;
637
+ }
638
+ setOverflowButtonVisible(true);
639
+ };
640
+ const syncMoreButton = () => {
641
+ const tabEntries = Array.from(state.tabs.entries());
642
+ const children = tabEntries.slice(0, maxVisibleTitles).map(([, entry]) => entry.tab).filter((child) => child !== moreButton);
643
+ if (!(tabEntries.length > maxVisibleTitles && state.titlePosition === "top")) {
644
+ if (state.popupOpen) closePopup();
645
+ titleNode._children = children;
646
+ titleNode._childrenDirty = true;
647
+ if (titleNode._el) titleNode._el.replaceChildren(...children.map((child) => child.renderDom()).filter(Boolean));
648
+ else moreButton._el?.remove();
649
+ setOverflowButtonVisible(false);
650
+ return;
651
+ }
652
+ titleNode._children = [...children, moreButton];
653
+ titleNode._childrenDirty = true;
654
+ if (titleNode._el) {
655
+ const childElements = children.map((child) => child.renderDom()).filter(Boolean);
656
+ titleNode._el.replaceChildren(...childElements, moreButton.renderDom());
657
+ }
658
+ updateOverflow();
659
+ };
660
+ const persistTabs = () => {
661
+ if (!state.persist || state.suppressPersist || typeof localStorage === "undefined") return;
662
+ try {
663
+ localStorage.setItem(state.storageKey, JSON.stringify({
664
+ activePath: routerInstance.currentPath(),
665
+ paths: Array.from(state.tabs.keys())
666
+ }));
667
+ } catch {}
668
+ };
669
+ let popupCleanup = null;
670
+ const closePopup = () => {
671
+ state.popupOpen = false;
672
+ node.attr("data-title-popup", null);
673
+ moreButton.attr("aria-expanded", "false");
674
+ popup.style("display", "none");
675
+ if (popupCleanup) {
676
+ popupCleanup();
677
+ popupCleanup = null;
678
+ }
679
+ };
680
+ const buildPopup = () => {
681
+ popup.clearChildren();
682
+ if (popup._el) popup._el.replaceChildren();
683
+ Array.from(state.tabs.entries()).slice(maxVisibleTitles).forEach(([path, entry]) => {
684
+ const title = entry.text.textContent();
685
+ const item = new ElementNode("div").className("yoya-vrouter-views-popup-item").attr({
686
+ role: "menuitem",
687
+ tabIndex: "0",
688
+ "data-router-view-path": path
689
+ }).styles({
690
+ alignItems: "center",
691
+ borderRadius: "6px",
692
+ color: themeValue("color-text", "#24292f"),
693
+ cursor: "pointer",
694
+ display: "flex",
695
+ fontSize: "13px",
696
+ gap: "8px",
697
+ padding: "7px 8px",
698
+ width: "100%"
699
+ });
700
+ const titleSpan = new ElementNode("span").className("yoya-vrouter-views-popup-title").styles({
701
+ flex: "1",
702
+ minWidth: "0",
703
+ overflow: "hidden",
704
+ textOverflow: "ellipsis",
705
+ whiteSpace: "nowrap"
706
+ }).text(title);
707
+ const closeButton = new ElementNode("button").className("yoya-vrouter-views-popup-close").attr({
708
+ type: "button",
709
+ "aria-label": `关闭 ${title}`
710
+ }).styles({
711
+ background: "transparent",
712
+ border: "0",
713
+ color: "inherit",
714
+ cursor: "pointer",
715
+ flexShrink: "0",
716
+ font: "inherit",
717
+ lineHeight: "1",
718
+ padding: "2px 4px"
719
+ }).text("×");
720
+ if (path === routerInstance.currentPath()) {
721
+ item.attr("aria-current", "true");
722
+ item.styles({
723
+ background: themeValue("color-primary-subtle", "#e8f0fe"),
724
+ color: themeValue("color-primary", "#1f6feb")
725
+ });
726
+ }
727
+ const activate = () => {
728
+ closePopup();
729
+ routerInstance.navigate(path);
730
+ };
731
+ item.on("click", activate);
732
+ item.on("keydown", (event) => {
733
+ if (event.key === "Enter" || event.key === " ") {
734
+ event.preventDefault();
735
+ activate();
736
+ }
737
+ });
738
+ closeButton.on("click", (event) => {
739
+ event.stopPropagation();
740
+ closeTitleTab(path, event);
741
+ if (state.popupOpen) buildPopup();
742
+ });
743
+ item.child(titleSpan, closeButton);
744
+ popup.child(item);
745
+ });
746
+ };
747
+ const openPopup = () => {
748
+ if (state.tabs.size === 0) return;
749
+ ensureScrollbarStyle();
750
+ buildPopup();
751
+ state.popupOpen = true;
752
+ node.attr("data-title-popup", "true");
753
+ moreButton.attr("aria-expanded", "true");
754
+ popup.styles({ display: "block" });
755
+ placePopup();
756
+ const handleDocumentClick = (event) => {
757
+ if (!popup._el?.contains(event.target) && !moreButton._el?.contains(event.target)) closePopup();
758
+ };
759
+ const handleKeydown = (event) => {
760
+ if (event.key === "Escape") closePopup();
761
+ };
762
+ const handleScroll = () => placePopup();
763
+ const handleResize = () => placePopup();
764
+ const unbindClick = bindDocumentEvent("click", handleDocumentClick);
765
+ const unbindKeydown = bindDocumentEvent("keydown", handleKeydown);
766
+ const unbindScroll = bindWindowEvent("scroll", handleScroll, true);
767
+ const unbindResize = bindWindowEvent("resize", handleResize);
768
+ popupCleanup = () => {
769
+ unbindClick();
770
+ unbindKeydown();
771
+ unbindScroll();
772
+ unbindResize();
773
+ };
774
+ function placePopup() {
775
+ const buttonRect = moreButton._el?.getBoundingClientRect();
776
+ const popupRect = popup._el?.getBoundingClientRect();
777
+ if (!buttonRect || !popupRect) return;
778
+ const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || 0;
779
+ const viewportHeight = window.innerHeight || document.documentElement?.clientHeight || 0;
780
+ const edge = 8;
781
+ let left = buttonRect.right - popupRect.width;
782
+ left = Math.max(edge, Math.min(left, viewportWidth - popupRect.width - edge));
783
+ let top = buttonRect.bottom + 4;
784
+ if (top + popupRect.height + edge > viewportHeight) top = Math.max(edge, buttonRect.top - popupRect.height - 4);
785
+ popup.styles({
786
+ left: `${left}px`,
787
+ top: `${top}px`
788
+ });
789
+ }
790
+ };
791
+ moreButton.on("click", () => {
792
+ if (state.popupOpen) closePopup();
793
+ else openPopup();
794
+ });
795
+ const renderDom = node.renderDom.bind(node);
796
+ node.renderDom = () => {
797
+ ensureScrollbarStyle();
798
+ const element = renderDom();
799
+ updateOverflow();
800
+ return element;
801
+ };
802
+ node.updateOverflow = updateOverflow;
803
+ node.titlePosition = (value) => {
804
+ if (value === void 0) return state.titlePosition;
805
+ const next = normalizeTitlePosition(value);
806
+ state.titlePosition = next;
807
+ if (next !== "top" && state.popupOpen) closePopup();
808
+ node.attr("data-title-position", state.titlePosition);
809
+ titleNode.attr("aria-orientation", state.titlePosition === "top" ? "horizontal" : "vertical");
810
+ applyTitlePosition(state.titlePosition);
811
+ syncMoreButton();
812
+ state.tabs.forEach((entry, path) => {
813
+ styleTitleTab(entry, path === routerInstance.currentPath());
814
+ });
815
+ updateOverflow();
816
+ return node;
817
+ };
818
+ node.titlePosition(state.titlePosition);
819
+ node.lockTitle = (value = true) => {
820
+ if (value === void 0) return state.lockTitle;
821
+ state.lockTitle = Boolean(value);
822
+ node.attr("data-title-locked", state.lockTitle ? "true" : null);
823
+ applyTitlePosition(state.titlePosition);
824
+ updateOverflow();
825
+ return node;
826
+ };
827
+ node.titleLocked = node.lockTitle;
828
+ const closeTitleTab = (path, event = null) => {
829
+ if (event) event.stopPropagation();
830
+ const closingIndex = Array.from(state.tabs.entries()).findIndex(([tabPath]) => tabPath === path);
831
+ const entry = state.tabs.get(path);
832
+ if (!entry) return;
833
+ const wasActive = entry.label.attr("aria-selected") === "true";
834
+ state.tabs.delete(path);
835
+ titleNode._children = titleNode.children().filter((child) => child !== entry.tab);
836
+ titleNode._childrenDirty = true;
837
+ entry.tab.destroy();
838
+ syncMoreButton();
839
+ persistTabs();
840
+ if (!wasActive) return;
841
+ const remainingPaths = Array.from(state.tabs.keys());
842
+ if (remainingPaths.length > 0) {
843
+ const nextPath = remainingPaths[Math.min(closingIndex, remainingPaths.length - 1)];
844
+ routerInstance.navigate(nextPath, { replace: true });
845
+ return;
846
+ }
847
+ contentNode.clearChildren().commit();
848
+ routerInstance._navigationGeneration += 1;
849
+ routerInstance._currentView = null;
850
+ };
851
+ const titleContextMenu = new ElementNode("div").className("yoya-vrouter-views-context").attr({
852
+ role: "menu",
853
+ "aria-label": "标签页操作"
854
+ }).styles({
855
+ background: themeValue("color-surface", "#ffffff"),
856
+ border: themeBorder("color-border", "#d0d7de"),
857
+ borderRadius: "8px",
858
+ boxShadow: "0 12px 28px rgba(15, 23, 42, 0.18)",
859
+ display: "none",
860
+ minWidth: "180px",
861
+ padding: "6px",
862
+ position: "fixed",
863
+ zIndex: "100"
864
+ });
865
+ let contextMenuCleanup = null;
866
+ const closeTabContextMenu = () => {
867
+ titleContextMenu.styles({ display: "none" });
868
+ titleContextMenu.clearChildren();
869
+ titleContextMenu._el?.remove();
870
+ if (contextMenuCleanup) {
871
+ contextMenuCleanup();
872
+ contextMenuCleanup = null;
873
+ }
874
+ };
875
+ const closeTabs = (pathsToClose, activatePath = null) => {
876
+ const closing = new Set(pathsToClose);
877
+ const entries = Array.from(state.tabs.entries());
878
+ const closingIndex = entries.findIndex(([tabPath]) => closing.has(tabPath));
879
+ entries.forEach(([tabPath, entry]) => {
880
+ if (!closing.has(tabPath)) return;
881
+ state.tabs.delete(tabPath);
882
+ titleNode._children = titleNode.children().filter((child) => child !== entry.tab);
883
+ titleNode._childrenDirty = true;
884
+ entry.tab.destroy();
885
+ });
886
+ syncMoreButton();
887
+ persistTabs();
888
+ if (state.popupOpen) buildPopup();
889
+ if (!closing.has(routerInstance.currentPath())) return;
890
+ const remaining = Array.from(state.tabs.keys());
891
+ if (remaining.length === 0) {
892
+ contentNode.clearChildren().commit();
893
+ routerInstance._navigationGeneration += 1;
894
+ routerInstance._currentView = null;
895
+ return;
896
+ }
897
+ const target = activatePath && state.tabs.has(activatePath) ? activatePath : remaining[Math.min(Math.max(closingIndex, 0), remaining.length - 1)];
898
+ routerInstance.navigate(target, { replace: true });
899
+ };
900
+ const copyTabUrl = (path) => {
901
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
902
+ const origin = typeof window === "undefined" ? "" : window.location.origin;
903
+ const pathname = typeof window === "undefined" ? "" : window.location.pathname;
904
+ const url = routerInstance.mode() === "history" ? `${origin}${pathname}${path}` : `${origin}${pathname}#${path}`;
905
+ navigator.clipboard.writeText(url).catch(() => {});
906
+ };
907
+ const openTabContextMenu = (path, event) => {
908
+ if (state.popupOpen) closePopup();
909
+ closeTabContextMenu();
910
+ const entries = Array.from(state.tabs.entries());
911
+ const clickedIndex = entries.findIndex(([tabPath]) => tabPath === path);
912
+ const closeOthers = () => closeTabs(entries.filter(([tabPath]) => tabPath !== path).map(([tabPath]) => tabPath), path);
913
+ const closeLeft = () => closeTabs(entries.slice(0, clickedIndex).map(([tabPath]) => tabPath), path);
914
+ const closeRight = () => closeTabs(entries.slice(clickedIndex + 1).map(([tabPath]) => tabPath), path);
915
+ const closeAll = () => closeTabs(entries.map(([tabPath]) => tabPath));
916
+ const addItem = (label, action, danger = false, disabled = false) => {
917
+ const item = new ElementNode("div").className("yoya-vrouter-views-context-item").attr({
918
+ role: "menuitem",
919
+ tabIndex: "0"
920
+ }).text(label).styles({
921
+ alignItems: "center",
922
+ borderRadius: "6px",
923
+ color: danger ? themeValue("color-text-danger", "#b91c1c") : themeValue("color-text", "#24292f"),
924
+ cursor: disabled ? "default" : "pointer",
925
+ display: "flex",
926
+ fontSize: "13px",
927
+ gap: "8px",
928
+ padding: "7px 10px",
929
+ width: "100%"
930
+ });
931
+ if (disabled) {
932
+ item.attr("aria-disabled", "true");
933
+ item.styles({ opacity: "0.4" });
934
+ } else item.on("click", () => {
935
+ closeTabContextMenu();
936
+ action();
937
+ });
938
+ titleContextMenu.child(item);
939
+ return item;
940
+ };
941
+ const addSeparator = () => titleContextMenu.child(new ElementNode("div").className("yoya-vrouter-views-context-separator"));
942
+ addItem("刷新", () => routerInstance.navigate(path, { replace: true }));
943
+ addItem("复制链接", () => copyTabUrl(path));
944
+ addSeparator();
945
+ addItem("关闭", () => closeTitleTab(path), true);
946
+ addItem("关闭其他", closeOthers, false, entries.length <= 1);
947
+ addItem("关闭左侧", closeLeft, false, clickedIndex <= 0);
948
+ addItem("关闭右侧", closeRight, false, clickedIndex === entries.length - 1);
949
+ addSeparator();
950
+ addItem("关闭全部", closeAll, true, entries.length === 0);
951
+ titleContextMenu.bindTo(document.body);
952
+ titleContextMenu.styles({ display: "block" });
953
+ const rect = titleContextMenu._el.getBoundingClientRect();
954
+ const viewportWidth = typeof window === "undefined" ? 0 : window.innerWidth || 0;
955
+ const viewportHeight = typeof window === "undefined" ? 0 : window.innerHeight || 0;
956
+ const edge = 8;
957
+ const left = Math.max(edge, Math.min(event.clientX || 0, viewportWidth - rect.width - edge));
958
+ const top = Math.max(edge, Math.min(event.clientY || 0, viewportHeight - rect.height - edge));
959
+ titleContextMenu.styles({
960
+ left: `${left}px`,
961
+ top: `${top}px`
962
+ });
963
+ const handlePointerDown = (pointerEvent) => {
964
+ if (!titleContextMenu._el?.contains(pointerEvent.target)) closeTabContextMenu();
965
+ };
966
+ const handleKeydown = (keyEvent) => {
967
+ if (keyEvent.key === "Escape") closeTabContextMenu();
968
+ };
969
+ const handleScroll = () => closeTabContextMenu();
970
+ const unbindPointerDown = bindDocumentEvent("mousedown", handlePointerDown);
971
+ const unbindKeydown = bindDocumentEvent("keydown", handleKeydown);
972
+ const unbindScroll = bindWindowEvent("scroll", handleScroll, true);
973
+ const unbindResize = bindWindowEvent("resize", handleScroll);
974
+ contextMenuCleanup = () => {
975
+ unbindPointerDown();
976
+ unbindKeydown();
977
+ unbindScroll();
978
+ unbindResize();
979
+ };
980
+ };
981
+ const updateTitle = (context = {}) => {
982
+ const path = context.path || routerInstance.currentPath();
983
+ const visiblePaths = new Set(state.titlePosition === "top" ? Array.from(state.tabs.keys()).slice(0, maxVisibleTitles) : Array.from(state.tabs.keys()));
984
+ let entry = state.tabs.get(path);
985
+ if (!entry) {
986
+ const tab = new ElementNode("div").className("yoya-vrouter-views-title");
987
+ const text = vText(resolveTitle(context));
988
+ const label = new ElementNode("button").className("yoya-vrouter-views-label").child(text);
989
+ const closeButton = new ElementNode("button").className("yoya-vrouter-views-close");
990
+ tab.attr({ "data-router-view-path": path });
991
+ tab.styles({
992
+ alignItems: "center",
993
+ cursor: "pointer",
994
+ display: "inline-flex",
995
+ font: "inherit",
996
+ gap: "8px",
997
+ whiteSpace: "nowrap"
998
+ });
999
+ label.attr({
1000
+ role: "tab",
1001
+ type: "button"
1002
+ });
1003
+ label.styles({
1004
+ background: "transparent",
1005
+ border: "0",
1006
+ color: "inherit",
1007
+ cursor: "pointer",
1008
+ font: "inherit",
1009
+ padding: "0"
1010
+ });
1011
+ label.on("click", () => routerInstance.navigate(path));
1012
+ closeButton.attr({
1013
+ type: "button",
1014
+ "aria-label": `关闭 ${resolveTitle(context)}`
1015
+ });
1016
+ closeButton.styles({
1017
+ background: "transparent",
1018
+ border: "0",
1019
+ color: "inherit",
1020
+ cursor: "pointer",
1021
+ font: "inherit",
1022
+ lineHeight: "1",
1023
+ padding: "0"
1024
+ });
1025
+ closeButton.text("×");
1026
+ closeButton.on("click", (event) => closeTitleTab(path, event));
1027
+ tab.on("contextmenu", (event) => {
1028
+ event.preventDefault();
1029
+ event.stopPropagation();
1030
+ openTabContextMenu(path, event);
1031
+ });
1032
+ tab.child(label, closeButton);
1033
+ titleNode.child(tab);
1034
+ entry = {
1035
+ closeButton,
1036
+ label,
1037
+ tab,
1038
+ text
1039
+ };
1040
+ state.tabs.set(path, entry);
1041
+ styleTitleTab(entry, false);
1042
+ } else {
1043
+ const title = resolveTitle(context);
1044
+ entry.text.textContent(title);
1045
+ entry.closeButton.attr("aria-label", `关闭 ${title}`);
1046
+ }
1047
+ if (!visiblePaths.has(path)) {
1048
+ state.tabs.delete(path);
1049
+ state.tabs = new Map([[path, entry], ...state.tabs]);
1050
+ }
1051
+ state.tabs.forEach((entry, tabPath) => styleTitleTab(entry, tabPath === path));
1052
+ syncMoreButton();
1053
+ if (state.popupOpen) buildPopup();
1054
+ persistTabs();
1055
+ };
1056
+ const restoreTabs = () => {
1057
+ if (!state.persist) return;
1058
+ const saved = readSavedTabs(state.storageKey);
1059
+ if (!saved) return;
1060
+ state.suppressPersist = true;
1061
+ try {
1062
+ saved.paths.slice().reverse().forEach((path) => {
1063
+ const resolved = routerInstance._resolve(path);
1064
+ updateTitle({
1065
+ ...resolved.context,
1066
+ path
1067
+ });
1068
+ });
1069
+ } finally {
1070
+ state.suppressPersist = false;
1071
+ persistTabs();
1072
+ }
1073
+ };
1074
+ if (typeof setup === "function") setup(node);
1075
+ else if (setup && typeof setup === "object") {
1076
+ const { persist, storageKey, title, titlePosition, titleResolver, ...elementConfig } = setup;
1077
+ if (title !== void 0) state.title = title;
1078
+ if (persist !== void 0) state.persist = Boolean(persist);
1079
+ if (storageKey !== void 0) state.storageKey = storageKey;
1080
+ if (titlePosition !== void 0) node.titlePosition(titlePosition);
1081
+ if (typeof titleResolver === "function") state.titleResolver = titleResolver;
1082
+ if (Object.keys(elementConfig).length > 0) node.setup(elementConfig);
1083
+ }
1084
+ routerInstance.outlet(contentNode);
1085
+ restoreTabs();
1086
+ if (routerInstance.currentRoute() || routerInstance.currentPath() !== "/") updateTitle({
1087
+ params: routerInstance.currentParams(),
1088
+ path: routerInstance.currentPath(),
1089
+ query: routerInstance.currentQuery(),
1090
+ route: routerInstance.currentRoute(),
1091
+ router: routerInstance
1092
+ });
1093
+ const unsubscribe = routerInstance.subscribe((context) => updateTitle(context));
1094
+ const handleResize = () => updateOverflow();
1095
+ const unbindResize = bindWindowEvent("resize", handleResize);
1096
+ const destroy = node.destroy.bind(node);
1097
+ node.destroy = () => {
1098
+ if (state.popupOpen) closePopup();
1099
+ closeTabContextMenu();
1100
+ unbindResize();
1101
+ unsubscribe();
1102
+ if (routerInstance.outlet() === contentNode) routerInstance.outlet(routerInstance);
1103
+ return destroy();
1104
+ };
1105
+ if (typeof callback === "function") callback(node);
1106
+ return node;
1107
+ }
1108
+ registerChildFactories(ElementNode, {
1109
+ vLink,
1110
+ vRouter,
1111
+ vRouterView,
1112
+ vRouterViews
1113
+ });
1114
+ function normalizeRoute(pattern, config) {
1115
+ const route = {
1116
+ beforeEnter: null,
1117
+ error: null,
1118
+ loading: null,
1119
+ pattern: normalizePattern(pattern),
1120
+ title: null,
1121
+ view: null
1122
+ };
1123
+ if (typeof config === "function" || config instanceof ViewNode || isTextLike(config)) {
1124
+ route.view = config;
1125
+ return route;
1126
+ }
1127
+ if (config && typeof config === "object") {
1128
+ route.beforeEnter = config.beforeEnter || null;
1129
+ route.error = config.error ?? null;
1130
+ route.loading = config.loading ?? null;
1131
+ route.title = config.title ?? null;
1132
+ route.view = config.view || config.component || null;
1133
+ }
1134
+ return route;
1135
+ }
1136
+ function assertRouter(routerInstance) {
1137
+ if (!(routerInstance instanceof Router)) throw new TypeError("vLink and vRouterView require a Router instance");
1138
+ }
1139
+ function normalizeTitlePosition(value) {
1140
+ return value === "left" || value === "right" ? value : "top";
1141
+ }
1142
+ function applyDeclarativeRouterSetup(node, setup) {
1143
+ if (typeof setup === "function") {
1144
+ setup(node);
1145
+ return node;
1146
+ }
1147
+ if (!setup || typeof setup !== "object") return node;
1148
+ const { beforeEach, default: defaultPath, error, loading, notFound, routes = [], ...elementConfig } = setup;
1149
+ if (Object.keys(elementConfig).length) node.setup(elementConfig);
1150
+ if (defaultPath !== void 0) node.default(defaultPath);
1151
+ if (beforeEach) node.beforeEach(beforeEach);
1152
+ if (loading !== void 0) node.loading(loading);
1153
+ if (error !== void 0) node.error(error);
1154
+ routes.forEach((declaration) => {
1155
+ if (declaration?.pattern !== void 0) node.vRoute(declaration.pattern, declaration.config);
1156
+ });
1157
+ if (notFound !== void 0) node.notFound(notFound);
1158
+ return node;
1159
+ }
1160
+ function applyLinkSetup(node, state, setup) {
1161
+ if (typeof setup === "function") {
1162
+ setup(node);
1163
+ return;
1164
+ }
1165
+ if (typeof setup === "string") {
1166
+ node.to(setup);
1167
+ node.label(setup);
1168
+ return;
1169
+ }
1170
+ if (!setup) return;
1171
+ const { exact, label, params, query, replace, to, ...elementConfig } = setup;
1172
+ if (Object.keys(elementConfig).length) node.setup(elementConfig);
1173
+ if (to !== void 0) state.to = to;
1174
+ if (params !== void 0) state.params = params || {};
1175
+ if (query !== void 0) state.query = query || {};
1176
+ if (replace !== void 0) state.replace = Boolean(replace);
1177
+ if (exact !== void 0) state.exact = Boolean(exact);
1178
+ state.label = label ?? state.to;
1179
+ node.label(state.label);
1180
+ }
1181
+ function updateLinkValue(node, state, key, value) {
1182
+ if (value === void 0) return state[key];
1183
+ state[key] = value;
1184
+ if (key === "to" && state.label === null) replaceChildren(node.children()[0], normalizeChildren(value));
1185
+ const routerInstance = node._routerInstance;
1186
+ if (routerInstance) updateLink(node, state, routerInstance);
1187
+ return node;
1188
+ }
1189
+ function updateLink(node, state, routerInstance) {
1190
+ node._routerInstance = routerInstance;
1191
+ const target = buildLinkPath(state.to, state.params, state.query);
1192
+ const active = isLinkActive(routerInstance.currentPath(), target, state.exact);
1193
+ const classes = new Set(String(node.attr("class") || "").split(/\s+/).filter(Boolean));
1194
+ classes.add("yoya-vlink");
1195
+ if (active) classes.add("is-active");
1196
+ else classes.delete("is-active");
1197
+ node.attr({
1198
+ "aria-current": active ? "page" : null,
1199
+ class: [...classes].join(" "),
1200
+ href: routerInstance.mode() === "history" ? target : `#${target}`
1201
+ });
1202
+ return node;
1203
+ }
1204
+ function buildLinkPath(to, params, query) {
1205
+ const [pathTemplate, existingQuery = ""] = normalizePath(to).split("?");
1206
+ const pathname = pathTemplate.replace(/:([A-Za-z0-9_]+)/g, (match, name) => params[name] === void 0 ? match : encodeURIComponent(params[name]));
1207
+ const search = new URLSearchParams(existingQuery);
1208
+ Object.entries(query || {}).forEach(([name, value]) => {
1209
+ search.delete(name);
1210
+ if (Array.isArray(value)) value.forEach((item) => search.append(name, item));
1211
+ else if (value !== null && value !== void 0) search.set(name, value);
1212
+ });
1213
+ const queryString = search.toString();
1214
+ return queryString ? `${pathname}?${queryString}` : pathname;
1215
+ }
1216
+ function isLinkActive(currentPath, targetPath, exact) {
1217
+ const current = normalizePath(currentPath);
1218
+ const target = normalizePath(targetPath);
1219
+ if (exact) return current === target;
1220
+ const currentPathname = parsePath(current).pathname;
1221
+ const targetPathname = parsePath(target).pathname;
1222
+ return currentPathname === targetPathname || currentPathname.startsWith(`${targetPathname}/`);
1223
+ }
1224
+ function shouldHandleLinkClick(event, node) {
1225
+ return !event.defaultPrevented && event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey && (!node.attr("target") || node.attr("target") === "_self");
1226
+ }
1227
+ function normalizeRouteView(view, context) {
1228
+ if (view instanceof ViewNode) return view;
1229
+ if (view === null || view === void 0) return vText("");
1230
+ if (isTextLike(view)) return vText(view);
1231
+ if (view !== null && typeof view === "object" && typeof view.default !== "undefined" && typeof view.render !== "function") return normalizeRouteView(view.default, context);
1232
+ if (typeof view === "function") return normalizeRouteView(view(context), context);
1233
+ if (typeof view === "object" && typeof view.render === "function") return normalizeRouteView(view.render(), context);
1234
+ throw new TypeError("Router route view must be a ViewNode, string, number, null, undefined, a render() component object, a factory returning one, or a module with a default export");
1235
+ }
1236
+ const defaultLoadingView = () => "加载中…";
1237
+ const defaultErrorView = (error) => `加载失败:${error?.message ?? String(error)}`;
1238
+ function isPromiseLike(value) {
1239
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
1240
+ }
1241
+ function resolveRouteEntry(entry, args) {
1242
+ return normalizeRouteView(typeof entry === "function" ? entry(...args) : entry, args[args.length - 1]);
1243
+ }
1244
+ function matchRoute(pattern, path) {
1245
+ const parsedPattern = parsePath(pattern);
1246
+ const parsedPath = parsePath(path);
1247
+ const patternParts = splitPath(parsedPattern.pathname);
1248
+ const pathParts = splitPath(parsedPath.pathname);
1249
+ if (patternParts.length !== pathParts.length) return null;
1250
+ const params = {};
1251
+ for (let index = 0; index < patternParts.length; index += 1) {
1252
+ const patternPart = patternParts[index];
1253
+ const pathPart = pathParts[index];
1254
+ if (patternPart.startsWith(":")) {
1255
+ params[patternPart.slice(1)] = decodeURIComponent(pathPart);
1256
+ continue;
1257
+ }
1258
+ if (patternPart !== pathPart) return null;
1259
+ }
1260
+ return {
1261
+ params,
1262
+ pathname: parsedPath.pathname,
1263
+ query: parsedPath.query
1264
+ };
1265
+ }
1266
+ function createRouteContext(routerInstance, path, route, match) {
1267
+ return {
1268
+ params: { ...match.params },
1269
+ path,
1270
+ pathname: match.pathname,
1271
+ query: { ...match.query },
1272
+ route,
1273
+ router: routerInstance
1274
+ };
1275
+ }
1276
+ function readHashPath() {
1277
+ if (typeof window === "undefined") return "/";
1278
+ return normalizePath(window.location.hash.replace(/^#/, "") || "/");
1279
+ }
1280
+ function readPath(mode = "hash") {
1281
+ if (typeof window === "undefined") return "/";
1282
+ if (mode === "history") return normalizePath(`${window.location.pathname}${window.location.search}`);
1283
+ return readHashPath();
1284
+ }
1285
+ function writeHashPath(path, options = {}) {
1286
+ if (typeof window === "undefined") return false;
1287
+ const hash = `#${path}`;
1288
+ if (options.replace) {
1289
+ window.history.replaceState(null, "", hash);
1290
+ return true;
1291
+ }
1292
+ if (window.location.hash === hash) return false;
1293
+ window.location.hash = hash;
1294
+ return true;
1295
+ }
1296
+ function writePath(path, options = {}, mode = "hash") {
1297
+ if (typeof window === "undefined") return false;
1298
+ if (mode !== "history") return writeHashPath(path, options);
1299
+ const target = normalizePath(path);
1300
+ const currentPath = readPath("history");
1301
+ if (!options.replace && currentPath === target) return false;
1302
+ if (options.replace) window.history.replaceState(null, "", target);
1303
+ else window.history.pushState(null, "", target);
1304
+ return true;
1305
+ }
1306
+ function normalizePattern(pattern) {
1307
+ return normalizePath(pattern).split("?")[0] || "/";
1308
+ }
1309
+ function normalizePath(path) {
1310
+ const [pathname, query = ""] = String(path || "/").replace(/^#/, "").split("?");
1311
+ const withSlash = pathname.startsWith("/") ? pathname : `/${pathname}`;
1312
+ const cleanPathname = withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
1313
+ return query ? `${cleanPathname}?${query}` : cleanPathname;
1314
+ }
1315
+ function parsePath(path) {
1316
+ const [pathname, queryString = ""] = normalizePath(path).split("?");
1317
+ return {
1318
+ pathname,
1319
+ query: Object.fromEntries(new URLSearchParams(queryString))
1320
+ };
1321
+ }
1322
+ function splitPath(pathname) {
1323
+ return pathname.split("/").filter(Boolean);
1324
+ }
1325
+ function isTextLike(value) {
1326
+ return typeof value === "string" || typeof value === "number";
1327
+ }
1328
+
1329
+ //#endregion
1330
+ export { PageDocumentNode, Router, createRouter, hydrate, hydrateOrMount, mount, parseState, renderPage, renderToString, resolveLocale, router, serializeState, vLink, vRoute, vRouter, vRouterView, vRouterViews };