@wcstack/router 1.3.11

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 (118) hide show
  1. package/README.ja.md +488 -0
  2. package/README.md +486 -0
  3. package/dist/GuardCancel.d.ts +6 -0
  4. package/dist/GuardCancel.d.ts.map +1 -0
  5. package/dist/GuardCancel.js +8 -0
  6. package/dist/GuardCancel.js.map +1 -0
  7. package/dist/Navigation.d.ts +7 -0
  8. package/dist/Navigation.d.ts.map +1 -0
  9. package/dist/Navigation.js +11 -0
  10. package/dist/Navigation.js.map +1 -0
  11. package/dist/applyRoute.d.ts +3 -0
  12. package/dist/applyRoute.d.ts.map +1 -0
  13. package/dist/applyRoute.js +40 -0
  14. package/dist/applyRoute.js.map +1 -0
  15. package/dist/assignParams.d.ts +2 -0
  16. package/dist/assignParams.d.ts.map +1 -0
  17. package/dist/assignParams.js +52 -0
  18. package/dist/assignParams.js.map +1 -0
  19. package/dist/auto.js +3 -0
  20. package/dist/auto.min.js +3 -0
  21. package/dist/bootstrapRouter.d.ts +8 -0
  22. package/dist/bootstrapRouter.d.ts.map +1 -0
  23. package/dist/bootstrapRouter.js +14 -0
  24. package/dist/bootstrapRouter.js.map +1 -0
  25. package/dist/builtinParamTypes.d.ts +38 -0
  26. package/dist/builtinParamTypes.d.ts.map +1 -0
  27. package/dist/builtinParamTypes.js +79 -0
  28. package/dist/builtinParamTypes.js.map +1 -0
  29. package/dist/components/Head.d.ts +29 -0
  30. package/dist/components/Head.d.ts.map +1 -0
  31. package/dist/components/Head.js +173 -0
  32. package/dist/components/Head.js.map +1 -0
  33. package/dist/components/Layout.d.ts +15 -0
  34. package/dist/components/Layout.d.ts.map +1 -0
  35. package/dist/components/Layout.js +86 -0
  36. package/dist/components/Layout.js.map +1 -0
  37. package/dist/components/LayoutOutlet.d.ts +15 -0
  38. package/dist/components/LayoutOutlet.d.ts.map +1 -0
  39. package/dist/components/LayoutOutlet.js +101 -0
  40. package/dist/components/LayoutOutlet.js.map +1 -0
  41. package/dist/components/Link.d.ts +25 -0
  42. package/dist/components/Link.d.ts.map +1 -0
  43. package/dist/components/Link.js +155 -0
  44. package/dist/components/Link.js.map +1 -0
  45. package/dist/components/Outlet.d.ts +16 -0
  46. package/dist/components/Outlet.d.ts.map +1 -0
  47. package/dist/components/Outlet.js +46 -0
  48. package/dist/components/Outlet.js.map +1 -0
  49. package/dist/components/Route.d.ts +61 -0
  50. package/dist/components/Route.d.ts.map +1 -0
  51. package/dist/components/Route.js +348 -0
  52. package/dist/components/Route.js.map +1 -0
  53. package/dist/components/Router.d.ts +62 -0
  54. package/dist/components/Router.d.ts.map +1 -0
  55. package/dist/components/Router.js +237 -0
  56. package/dist/components/Router.js.map +1 -0
  57. package/dist/components/types.d.ts +86 -0
  58. package/dist/components/types.d.ts.map +1 -0
  59. package/dist/components/types.js +2 -0
  60. package/dist/components/types.js.map +1 -0
  61. package/dist/config.d.ts +5 -0
  62. package/dist/config.d.ts.map +1 -0
  63. package/dist/config.js +53 -0
  64. package/dist/config.js.map +1 -0
  65. package/dist/exports.d.ts +3 -0
  66. package/dist/exports.d.ts.map +1 -0
  67. package/dist/exports.js +2 -0
  68. package/dist/exports.js.map +1 -0
  69. package/dist/getCustomTagName.d.ts +2 -0
  70. package/dist/getCustomTagName.d.ts.map +1 -0
  71. package/dist/getCustomTagName.js +12 -0
  72. package/dist/getCustomTagName.js.map +1 -0
  73. package/dist/getUUID.d.ts +2 -0
  74. package/dist/getUUID.d.ts.map +1 -0
  75. package/dist/getUUID.js +11 -0
  76. package/dist/getUUID.js.map +1 -0
  77. package/dist/hideRoute.d.ts +3 -0
  78. package/dist/hideRoute.d.ts.map +1 -0
  79. package/dist/hideRoute.js +7 -0
  80. package/dist/hideRoute.js.map +1 -0
  81. package/dist/index.d.ts +24 -0
  82. package/dist/index.esm.js +1682 -0
  83. package/dist/index.esm.js.map +1 -0
  84. package/dist/index.esm.min.js +2 -0
  85. package/dist/index.esm.min.js.map +1 -0
  86. package/dist/matchRoutes.d.ts +3 -0
  87. package/dist/matchRoutes.d.ts.map +1 -0
  88. package/dist/matchRoutes.js +52 -0
  89. package/dist/matchRoutes.js.map +1 -0
  90. package/dist/parse.d.ts +3 -0
  91. package/dist/parse.d.ts.map +1 -0
  92. package/dist/parse.js +77 -0
  93. package/dist/parse.js.map +1 -0
  94. package/dist/raiseError.d.ts +2 -0
  95. package/dist/raiseError.d.ts.map +1 -0
  96. package/dist/raiseError.js +4 -0
  97. package/dist/raiseError.js.map +1 -0
  98. package/dist/registerComponents.d.ts +2 -0
  99. package/dist/registerComponents.d.ts.map +1 -0
  100. package/dist/registerComponents.js +33 -0
  101. package/dist/registerComponents.js.map +1 -0
  102. package/dist/showRoute.d.ts +3 -0
  103. package/dist/showRoute.d.ts.map +1 -0
  104. package/dist/showRoute.js +38 -0
  105. package/dist/showRoute.js.map +1 -0
  106. package/dist/showRouteContent.d.ts +3 -0
  107. package/dist/showRouteContent.d.ts.map +1 -0
  108. package/dist/showRouteContent.js +38 -0
  109. package/dist/showRouteContent.js.map +1 -0
  110. package/dist/testPath.d.ts +3 -0
  111. package/dist/testPath.d.ts.map +1 -0
  112. package/dist/testPath.js +85 -0
  113. package/dist/testPath.js.map +1 -0
  114. package/dist/types.d.ts +38 -0
  115. package/dist/types.d.ts.map +1 -0
  116. package/dist/types.js +2 -0
  117. package/dist/types.js.map +1 -0
  118. package/package.json +74 -0
@@ -0,0 +1,1682 @@
1
+ const _config = {
2
+ tagNames: {
3
+ route: "wcs-route",
4
+ router: "wcs-router",
5
+ outlet: "wcs-outlet",
6
+ layout: "wcs-layout",
7
+ layoutOutlet: "wcs-layout-outlet",
8
+ link: "wcs-link",
9
+ head: "wcs-head"
10
+ },
11
+ enableShadowRoot: false,
12
+ basenameFileExtensions: [".html"]
13
+ };
14
+ // 後方互換のため config もエクスポート(読み取り専用として使用)
15
+ const config = _config;
16
+ function setConfig(partialConfig) {
17
+ if (partialConfig.tagNames) {
18
+ Object.assign(_config.tagNames, partialConfig.tagNames);
19
+ }
20
+ if (typeof partialConfig.enableShadowRoot === "boolean") {
21
+ _config.enableShadowRoot = partialConfig.enableShadowRoot;
22
+ }
23
+ if (Array.isArray(partialConfig.basenameFileExtensions)) {
24
+ _config.basenameFileExtensions = partialConfig.basenameFileExtensions;
25
+ }
26
+ }
27
+
28
+ function getUUID() {
29
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
30
+ return crypto.randomUUID();
31
+ }
32
+ // Simple UUID generator
33
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
34
+ const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8;
35
+ return v.toString(16);
36
+ });
37
+ }
38
+
39
+ function raiseError(message) {
40
+ throw new Error(`[@wcstack/router] ${message}`);
41
+ }
42
+
43
+ class GuardCancel extends Error {
44
+ fallbackPath;
45
+ constructor(message, fallbackPath) {
46
+ super(message);
47
+ this.fallbackPath = fallbackPath;
48
+ }
49
+ }
50
+
51
+ const builtinParamTypes = {
52
+ "int": {
53
+ typeName: "int",
54
+ pattern: /^-?\d+$/,
55
+ parse(value) {
56
+ if (!this.pattern.test(value)) {
57
+ return undefined;
58
+ }
59
+ return parseInt(value, 10);
60
+ }
61
+ },
62
+ "float": {
63
+ typeName: "float",
64
+ pattern: /^-?\d+(?:\.\d+)?$/,
65
+ parse(value) {
66
+ if (!this.pattern.test(value)) {
67
+ return undefined;
68
+ }
69
+ return parseFloat(value);
70
+ }
71
+ },
72
+ "bool": {
73
+ typeName: "bool",
74
+ pattern: /^(true|false|0|1)$/,
75
+ parse(value) {
76
+ if (!this.pattern.test(value)) {
77
+ return undefined;
78
+ }
79
+ return value === "true" || value === "1";
80
+ }
81
+ },
82
+ "uuid": {
83
+ typeName: "uuid",
84
+ pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
85
+ parse(value) {
86
+ if (!this.pattern.test(value)) {
87
+ return undefined;
88
+ }
89
+ return value;
90
+ }
91
+ },
92
+ "slug": {
93
+ typeName: "slug",
94
+ pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
95
+ parse(value) {
96
+ if (!this.pattern.test(value)) {
97
+ return undefined;
98
+ }
99
+ return value;
100
+ }
101
+ },
102
+ "isoDate": {
103
+ typeName: "isoDate",
104
+ pattern: /^\d{4}-\d{2}-\d{2}$/,
105
+ parse(value) {
106
+ if (!this.pattern.test(value)) {
107
+ return undefined;
108
+ }
109
+ const [year, month, day] = value.split("-").map(Number);
110
+ const date = new Date(year, month - 1, day);
111
+ // 元の値と一致するか確認(補正されていないか)
112
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
113
+ return undefined;
114
+ }
115
+ return date;
116
+ }
117
+ },
118
+ "any": {
119
+ typeName: "any",
120
+ pattern: /^.+$/,
121
+ parse(value) {
122
+ if (!this.pattern.test(value)) {
123
+ return undefined;
124
+ }
125
+ return value;
126
+ }
127
+ },
128
+ };
129
+
130
+ const weights = {
131
+ 'static': 2,
132
+ 'param': 1,
133
+ 'catch-all': 0
134
+ };
135
+ class Route extends HTMLElement {
136
+ _name = '';
137
+ _path = '';
138
+ _routeParentNode = null;
139
+ _routeChildNodes = [];
140
+ _routerNode = null;
141
+ _uuid = getUUID();
142
+ _placeHolder = document.createComment(`@@route:${this._uuid}`);
143
+ _childNodeArray;
144
+ _paramNames;
145
+ _absoluteParamNames;
146
+ _params = {};
147
+ _typedParams = {};
148
+ _weight;
149
+ _absoluteWeight;
150
+ _childIndex = 0;
151
+ _hasGuard = false;
152
+ _guardHandler = null;
153
+ _waitForSetGuardHandler = null;
154
+ _resolveSetGuardHandler = null;
155
+ _guardFallbackPath = '';
156
+ _initialized = false;
157
+ _isFallbackRoute = false;
158
+ _segmentCount;
159
+ _segmentInfos = [];
160
+ _absoluteSegmentInfos;
161
+ constructor() {
162
+ super();
163
+ }
164
+ get routeParentNode() {
165
+ return this._routeParentNode;
166
+ }
167
+ get routeChildNodes() {
168
+ return this._routeChildNodes;
169
+ }
170
+ get routerNode() {
171
+ if (!this._routerNode) {
172
+ raiseError(`${config.tagNames.route} has no routerNode.`);
173
+ }
174
+ return this._routerNode;
175
+ }
176
+ get path() {
177
+ return this._path;
178
+ }
179
+ get isRelative() {
180
+ return !this._path.startsWith('/');
181
+ }
182
+ _checkParentNode(hasParentCallback, noParentCallback) {
183
+ // fallbackはルーター直下のみ許可されるため、相対パスチェックはスキップ
184
+ if (!this._isFallbackRoute) {
185
+ if (this.isRelative && !this._routeParentNode) {
186
+ raiseError(`${config.tagNames.route} is relative but has no parent route.`);
187
+ }
188
+ if (!this.isRelative && this._routeParentNode) {
189
+ raiseError(`${config.tagNames.route} is absolute but has a parent route.`);
190
+ }
191
+ }
192
+ if (this.isRelative && this._routeParentNode) {
193
+ return hasParentCallback(this._routeParentNode);
194
+ }
195
+ else {
196
+ return noParentCallback();
197
+ }
198
+ }
199
+ get absolutePath() {
200
+ return this._checkParentNode((routeParentNode) => {
201
+ const parentPath = routeParentNode.absolutePath;
202
+ return parentPath.endsWith('/')
203
+ ? parentPath + this._path
204
+ : parentPath + '/' + this._path;
205
+ }, () => {
206
+ return this._path;
207
+ });
208
+ }
209
+ get uuid() {
210
+ return this._uuid;
211
+ }
212
+ get placeHolder() {
213
+ return this._placeHolder;
214
+ }
215
+ get childNodeArray() {
216
+ if (typeof this._childNodeArray === 'undefined') {
217
+ this._childNodeArray = Array.from(this.childNodes);
218
+ }
219
+ return this._childNodeArray;
220
+ }
221
+ get routes() {
222
+ if (this.routeParentNode) {
223
+ return this.routeParentNode.routes.concat(this);
224
+ }
225
+ else {
226
+ return [this];
227
+ }
228
+ }
229
+ get segmentInfos() {
230
+ return this._segmentInfos;
231
+ }
232
+ // indexの場合、{ type: 'static', segmentText: '' }となる、indexが複数連続する場合もある
233
+ get absoluteSegmentInfos() {
234
+ if (typeof this._absoluteSegmentInfos === 'undefined') {
235
+ this._absoluteSegmentInfos = this._checkParentNode((routeParentNode) => {
236
+ return [
237
+ ...routeParentNode.absoluteSegmentInfos,
238
+ ...this._segmentInfos
239
+ ];
240
+ }, () => {
241
+ return [...this._segmentInfos];
242
+ });
243
+ }
244
+ return this._absoluteSegmentInfos;
245
+ }
246
+ get params() {
247
+ return this._params;
248
+ }
249
+ get typedParams() {
250
+ return this._typedParams;
251
+ }
252
+ get paramNames() {
253
+ if (typeof this._paramNames === 'undefined') {
254
+ const names = [];
255
+ for (const info of this._segmentInfos) {
256
+ if (info.paramName) {
257
+ names.push(info.paramName);
258
+ }
259
+ }
260
+ this._paramNames = names;
261
+ }
262
+ return this._paramNames;
263
+ }
264
+ get absoluteParamNames() {
265
+ if (typeof this._absoluteParamNames === 'undefined') {
266
+ this._absoluteParamNames = this._checkParentNode((routeParentNode) => {
267
+ return [
268
+ ...routeParentNode.absoluteParamNames,
269
+ ...this.paramNames
270
+ ];
271
+ }, () => {
272
+ return [...this.paramNames];
273
+ });
274
+ }
275
+ return this._absoluteParamNames;
276
+ }
277
+ get weight() {
278
+ if (typeof this._weight === 'undefined') {
279
+ let weight = 0;
280
+ for (const info of this._segmentInfos) {
281
+ weight += weights[info.type];
282
+ }
283
+ this._weight = weight;
284
+ }
285
+ return this._weight;
286
+ }
287
+ get absoluteWeight() {
288
+ if (typeof this._absoluteWeight === 'undefined') {
289
+ this._absoluteWeight = this._checkParentNode((routeParentNode) => {
290
+ return routeParentNode.absoluteWeight + this.weight;
291
+ }, () => {
292
+ return this.weight;
293
+ });
294
+ }
295
+ return this._absoluteWeight;
296
+ }
297
+ get childIndex() {
298
+ return this._childIndex;
299
+ }
300
+ get name() {
301
+ return this._name;
302
+ }
303
+ async guardCheck(matchResult) {
304
+ if (this._hasGuard && this._waitForSetGuardHandler) {
305
+ await this._waitForSetGuardHandler;
306
+ }
307
+ if (this._guardHandler) {
308
+ const toPath = matchResult.path;
309
+ const fromPath = matchResult.lastPath;
310
+ const allowed = await this._guardHandler(toPath, fromPath);
311
+ if (!allowed) {
312
+ throw new GuardCancel('Navigation cancelled by guard.', this._guardFallbackPath);
313
+ }
314
+ }
315
+ }
316
+ shouldChange(newParams) {
317
+ for (const key of this.paramNames) {
318
+ if (this.params[key] !== newParams[key]) {
319
+ return true;
320
+ }
321
+ }
322
+ return false;
323
+ }
324
+ get guardHandler() {
325
+ if (!this._guardHandler) {
326
+ raiseError(`${config.tagNames.route} has no guardHandler.`);
327
+ }
328
+ return this._guardHandler;
329
+ }
330
+ set guardHandler(value) {
331
+ this._resolveSetGuardHandler?.();
332
+ this._guardHandler = value;
333
+ }
334
+ initialize(routerNode, routeParentNode) {
335
+ if (this._initialized) {
336
+ return;
337
+ }
338
+ this._initialized = true;
339
+ // 単独で影響のないものから設定していく
340
+ if (this.hasAttribute('path')) {
341
+ this._path = this.getAttribute('path') || '';
342
+ }
343
+ else if (this.hasAttribute('index')) {
344
+ this._path = '';
345
+ }
346
+ else if (this.hasAttribute('fallback')) {
347
+ this._path = '';
348
+ this._isFallbackRoute = true;
349
+ }
350
+ else {
351
+ raiseError(`${config.tagNames.route} should have a "path" or "index" attribute.`);
352
+ }
353
+ this._name = this.getAttribute('name') || '';
354
+ this._routerNode = routerNode;
355
+ this._routeParentNode = routeParentNode;
356
+ const routeChildContainer = routeParentNode || routerNode;
357
+ routeChildContainer.routeChildNodes.push(this);
358
+ this._childIndex = routeChildContainer.routeChildNodes.length - 1;
359
+ if (this._isFallbackRoute) {
360
+ if (routeParentNode) {
361
+ raiseError(`${config.tagNames.route} with fallback attribute must be a direct child of ${config.tagNames.router}.`);
362
+ }
363
+ if (routerNode.fallbackRoute) {
364
+ raiseError(`${config.tagNames.router} can have only one fallback route.`);
365
+ }
366
+ routerNode.fallbackRoute = this;
367
+ }
368
+ // index属性の場合は特別扱い(セグメントを消費しない)
369
+ if (this.hasAttribute('index')) {
370
+ this._segmentInfos.push({
371
+ type: 'static',
372
+ segmentText: '',
373
+ paramName: null,
374
+ pattern: /^$/,
375
+ isIndex: true
376
+ });
377
+ }
378
+ const segments = this._path.split('/');
379
+ for (let idx = 0; idx < segments.length; idx++) {
380
+ const segment = segments[idx];
381
+ // 末尾の空セグメントはスキップ(/parent/ のような場合)
382
+ if (segment === '' && idx === segments.length - 1 && idx > 0) {
383
+ continue;
384
+ }
385
+ if (segment === '*') {
386
+ this._segmentInfos.push({
387
+ type: 'catch-all',
388
+ segmentText: segment,
389
+ paramName: '*',
390
+ pattern: new RegExp('^(.*)$')
391
+ });
392
+ // Catch-all: matches remaining path segments
393
+ break; // Ignore subsequent segments
394
+ }
395
+ else if (segment.startsWith(':')) {
396
+ const matchType = segment.match(/^:([^()]+)(\(([^)]+)\))?$/);
397
+ let paramName;
398
+ let typeName = 'any';
399
+ if (matchType) {
400
+ paramName = matchType[1];
401
+ if (matchType[3] && Object.keys(builtinParamTypes).includes(matchType[3])) {
402
+ typeName = matchType[3];
403
+ }
404
+ }
405
+ else {
406
+ paramName = segment.substring(1);
407
+ }
408
+ this._segmentInfos.push({
409
+ type: 'param',
410
+ segmentText: segment,
411
+ paramName: paramName,
412
+ pattern: new RegExp('^([^\\/]+)$'),
413
+ paramType: typeName
414
+ });
415
+ }
416
+ else if (segment !== '' || !this.hasAttribute('index')) {
417
+ // 空セグメントはindex以外の場合のみ追加(絶対パスの先頭 '' など)
418
+ this._segmentInfos.push({
419
+ type: 'static',
420
+ segmentText: segment,
421
+ paramName: null,
422
+ pattern: new RegExp(`^${segment}$`)
423
+ });
424
+ }
425
+ }
426
+ this._hasGuard = this.hasAttribute('guard');
427
+ if (this._hasGuard) {
428
+ this._guardFallbackPath = this.getAttribute('guard') || '/';
429
+ this._waitForSetGuardHandler = new Promise((resolve) => {
430
+ this._resolveSetGuardHandler = resolve;
431
+ });
432
+ }
433
+ this.setAttribute('fullpath', this.absolutePath);
434
+ }
435
+ get fullpath() {
436
+ return this.absolutePath;
437
+ }
438
+ get segmentCount() {
439
+ if (typeof this._segmentCount === 'undefined') {
440
+ let count = 0;
441
+ for (const info of this._segmentInfos) {
442
+ if (info.type !== 'catch-all') {
443
+ count++;
444
+ }
445
+ }
446
+ this._segmentCount = this._path === "" ? 0 : count;
447
+ }
448
+ return this._segmentCount;
449
+ }
450
+ get absoluteSegmentCount() {
451
+ return this._checkParentNode((routeParentNode) => {
452
+ return routeParentNode.absoluteSegmentCount + this.segmentCount;
453
+ }, () => {
454
+ return this.segmentCount;
455
+ });
456
+ }
457
+ testAncestorNode(ancestorNode) {
458
+ let currentNode = this._routeParentNode;
459
+ while (currentNode) {
460
+ if (currentNode === ancestorNode) {
461
+ return true;
462
+ }
463
+ currentNode = currentNode.routeParentNode;
464
+ }
465
+ return false;
466
+ }
467
+ clearParams() {
468
+ this._params = {};
469
+ this._typedParams = {};
470
+ }
471
+ }
472
+
473
+ const cache = new Map();
474
+ class Layout extends HTMLElement {
475
+ _uuid = getUUID();
476
+ _initialized = false;
477
+ constructor() {
478
+ super();
479
+ }
480
+ async _loadTemplateFromSource(source) {
481
+ try {
482
+ const response = await fetch(source);
483
+ if (!response.ok) {
484
+ raiseError(`${config.tagNames.layout} failed to fetch layout from source: ${source}, status: ${response.status}`);
485
+ }
486
+ const templateContent = await response.text();
487
+ cache.set(source, templateContent);
488
+ return templateContent;
489
+ }
490
+ catch (error) {
491
+ raiseError(`${config.tagNames.layout} failed to load layout from source: ${source}, error: ${error}`);
492
+ }
493
+ }
494
+ _loadTemplateFromDocument(id) {
495
+ const element = document.getElementById(`${id}`);
496
+ if (element) {
497
+ if (element instanceof HTMLTemplateElement) {
498
+ return element.innerHTML;
499
+ }
500
+ }
501
+ return null;
502
+ }
503
+ async loadTemplate() {
504
+ const source = this.getAttribute('src');
505
+ const layoutId = this.getAttribute('layout');
506
+ if (source && layoutId) {
507
+ console.warn(`${config.tagNames.layout} have both "src" and "layout" attributes.`);
508
+ }
509
+ const template = document.createElement('template');
510
+ if (source) {
511
+ if (cache.has(source)) {
512
+ template.innerHTML = cache.get(source) || '';
513
+ }
514
+ else {
515
+ template.innerHTML = await this._loadTemplateFromSource(source) || '';
516
+ cache.set(source, template.innerHTML);
517
+ }
518
+ }
519
+ else if (layoutId) {
520
+ const templateContent = this._loadTemplateFromDocument(layoutId);
521
+ if (templateContent) {
522
+ template.innerHTML = templateContent;
523
+ }
524
+ else {
525
+ console.warn(`${config.tagNames.layout} could not find template with id "${layoutId}".`);
526
+ }
527
+ }
528
+ return template;
529
+ }
530
+ get uuid() {
531
+ return this._uuid;
532
+ }
533
+ get enableShadowRoot() {
534
+ if (this.hasAttribute('enable-shadow-root')) {
535
+ return true;
536
+ }
537
+ else if (this.hasAttribute('disable-shadow-root')) {
538
+ return false;
539
+ }
540
+ return config.enableShadowRoot;
541
+ }
542
+ get name() {
543
+ // Layout 要素が DOM に挿入されないケース(parseで置換)でも name を取れるようにする
544
+ return this.getAttribute('name') || '';
545
+ }
546
+ _initialize() {
547
+ this._initialized = true;
548
+ }
549
+ connectedCallback() {
550
+ if (!this._initialized) {
551
+ this._initialize();
552
+ }
553
+ }
554
+ }
555
+
556
+ class Outlet extends HTMLElement {
557
+ _routesNode = null;
558
+ _lastRoutes = [];
559
+ _initialized = false;
560
+ constructor() {
561
+ super();
562
+ }
563
+ get routesNode() {
564
+ if (!this._routesNode) {
565
+ raiseError(`${config.tagNames.outlet} has no routesNode.`);
566
+ }
567
+ return this._routesNode;
568
+ }
569
+ set routesNode(value) {
570
+ this._routesNode = value;
571
+ }
572
+ get rootNode() {
573
+ if (this.shadowRoot) {
574
+ return this.shadowRoot;
575
+ }
576
+ return this;
577
+ }
578
+ get lastRoutes() {
579
+ return this._lastRoutes;
580
+ }
581
+ set lastRoutes(value) {
582
+ this._lastRoutes = [...value];
583
+ }
584
+ _initialize() {
585
+ if (config.enableShadowRoot) {
586
+ this.attachShadow({ mode: 'open' });
587
+ }
588
+ this._initialized = true;
589
+ }
590
+ connectedCallback() {
591
+ if (!this._initialized) {
592
+ this._initialize();
593
+ }
594
+ }
595
+ }
596
+ function createOutlet() {
597
+ return document.createElement(config.tagNames.outlet);
598
+ }
599
+
600
+ function getCustomTagName(element) {
601
+ const tagName = element.tagName.toLowerCase();
602
+ if (tagName.includes("-")) {
603
+ return tagName;
604
+ }
605
+ const isAttr = element.getAttribute("is");
606
+ if (isAttr && isAttr.includes("-")) {
607
+ return isAttr;
608
+ }
609
+ return null;
610
+ }
611
+
612
+ const bindTypeSet = new Set(["props", "states", "attr", ""]);
613
+ function _assignParams(element, params, bindType) {
614
+ for (const [key, value] of Object.entries(params)) {
615
+ switch (bindType) {
616
+ case "props":
617
+ element.props = {
618
+ ...element.props,
619
+ [key]: value
620
+ };
621
+ break;
622
+ case "states":
623
+ element.states = {
624
+ ...element.states,
625
+ [key]: value
626
+ };
627
+ break;
628
+ case "attr":
629
+ element.setAttribute(key, value);
630
+ break;
631
+ case "":
632
+ element[key] = value;
633
+ break;
634
+ }
635
+ }
636
+ }
637
+ function assignParams(element, params) {
638
+ if (!element.hasAttribute('data-bind')) {
639
+ raiseError(`${element.tagName} has no 'data-bind' attribute.`);
640
+ }
641
+ const bindTypeText = element.getAttribute('data-bind') || '';
642
+ if (!bindTypeSet.has(bindTypeText)) {
643
+ raiseError(`${element.tagName} has invalid 'data-bind' attribute: ${bindTypeText}`);
644
+ }
645
+ const bindType = bindTypeText;
646
+ const customTagName = getCustomTagName(element);
647
+ if (customTagName && customElements.get(customTagName) === undefined) {
648
+ customElements.whenDefined(customTagName).then(() => {
649
+ if (element.isConnected) {
650
+ // 要素が削除されていない場合のみ割り当てを行う
651
+ _assignParams(element, params, bindType);
652
+ }
653
+ }).catch(() => {
654
+ raiseError(`Failed to define custom element: ${customTagName}`);
655
+ });
656
+ }
657
+ else {
658
+ _assignParams(element, params, bindType);
659
+ }
660
+ }
661
+
662
+ class LayoutOutlet extends HTMLElement {
663
+ _layout = null;
664
+ _initialized = false;
665
+ _layoutChildNodes = [];
666
+ constructor() {
667
+ super();
668
+ }
669
+ get layout() {
670
+ if (!this._layout) {
671
+ raiseError(`${config.tagNames.layoutOutlet} has no layout.`);
672
+ }
673
+ return this._layout;
674
+ }
675
+ set layout(value) {
676
+ this._layout = value;
677
+ this.setAttribute('name', value.name);
678
+ }
679
+ get name() {
680
+ return this.layout.name;
681
+ }
682
+ async _initialize() {
683
+ this._initialized = true;
684
+ if (this.layout.enableShadowRoot) {
685
+ this.attachShadow({ mode: 'open' });
686
+ }
687
+ const template = await this.layout.loadTemplate();
688
+ if (this.shadowRoot) {
689
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
690
+ for (const childNode of Array.from(this.layout.childNodes)) {
691
+ this._layoutChildNodes.push(childNode);
692
+ this.appendChild(childNode);
693
+ }
694
+ }
695
+ else {
696
+ const fragmentForTemplate = template.content.cloneNode(true);
697
+ const slotElementBySlotName = new Map();
698
+ fragmentForTemplate.querySelectorAll('slot').forEach((slotElement) => {
699
+ const slotName = slotElement.getAttribute('name') || '';
700
+ if (!slotElementBySlotName.has(slotName)) {
701
+ slotElementBySlotName.set(slotName, slotElement);
702
+ }
703
+ else {
704
+ console.warn(`${config.tagNames.layoutOutlet} duplicate slot name "${slotName}" in layout template.`);
705
+ }
706
+ });
707
+ const fragmentBySlotName = new Map();
708
+ const fragmentForChildNodes = document.createDocumentFragment();
709
+ for (const childNode of Array.from(this.layout.childNodes)) {
710
+ this._layoutChildNodes.push(childNode);
711
+ if (childNode instanceof Element) {
712
+ const slotName = childNode.getAttribute('slot') || '';
713
+ if (slotName.length > 0 && slotElementBySlotName.has(slotName)) {
714
+ if (!fragmentBySlotName.has(slotName)) {
715
+ fragmentBySlotName.set(slotName, document.createDocumentFragment());
716
+ }
717
+ fragmentBySlotName.get(slotName)?.appendChild(childNode);
718
+ continue;
719
+ }
720
+ }
721
+ fragmentForChildNodes.appendChild(childNode);
722
+ }
723
+ for (const [slotName, slotElement] of slotElementBySlotName) {
724
+ const fragment = fragmentBySlotName.get(slotName);
725
+ if (fragment) {
726
+ slotElement.replaceWith(fragment);
727
+ }
728
+ }
729
+ const defaultSlot = slotElementBySlotName.get('');
730
+ if (defaultSlot) {
731
+ defaultSlot.replaceWith(fragmentForChildNodes);
732
+ }
733
+ this.appendChild(fragmentForTemplate);
734
+ }
735
+ }
736
+ async connectedCallback() {
737
+ if (!this._initialized) {
738
+ await this._initialize();
739
+ }
740
+ }
741
+ assignParams(params) {
742
+ for (const childNode of this._layoutChildNodes) {
743
+ if (childNode instanceof Element) {
744
+ childNode.querySelectorAll('[data-bind]').forEach((e) => {
745
+ // 子要素にパラメータを割り当て
746
+ assignParams(e, params);
747
+ });
748
+ if (childNode.hasAttribute('data-bind')) {
749
+ // 子要素にパラメータを割り当て
750
+ assignParams(childNode, params);
751
+ }
752
+ }
753
+ }
754
+ }
755
+ }
756
+ function createLayoutOutlet() {
757
+ return document.createElement(config.tagNames.layoutOutlet);
758
+ }
759
+
760
+ function _duplicateCheck(routesByPath, route) {
761
+ let routes = routesByPath.get(route.absolutePath);
762
+ if (!routes) {
763
+ routes = [];
764
+ }
765
+ for (const existingRoute of routes) {
766
+ if (!route.testAncestorNode(existingRoute)) {
767
+ console.warn(`Duplicate route path detected: '${route.absolutePath}' (defined as '${route.path}')`);
768
+ break;
769
+ }
770
+ }
771
+ routes.push(route);
772
+ if (routes.length === 1) {
773
+ routesByPath.set(route.absolutePath, routes);
774
+ }
775
+ }
776
+ async function _parseNode(routerNode, node, routes, map, routesByPath) {
777
+ const routeParentNode = routes.length > 0 ? routes[routes.length - 1] : null;
778
+ const fragment = document.createDocumentFragment();
779
+ const childNodes = Array.from(node.childNodes);
780
+ for (const childNode of childNodes) {
781
+ if (childNode.nodeType === Node.ELEMENT_NODE) {
782
+ let appendNode = childNode;
783
+ let element = childNode;
784
+ const tagName = element.tagName.toLowerCase();
785
+ if (tagName === config.tagNames.route) {
786
+ const childFragment = document.createDocumentFragment();
787
+ // Move child nodes to fragment to avoid duplication of
788
+ for (const childNode of Array.from(element.childNodes)) {
789
+ childFragment.appendChild(childNode);
790
+ }
791
+ const cloneElement = document.importNode(element, true);
792
+ customElements.upgrade(cloneElement);
793
+ cloneElement.appendChild(childFragment);
794
+ const route = cloneElement;
795
+ route.initialize(routerNode, routeParentNode);
796
+ _duplicateCheck(routesByPath, route);
797
+ routes.push(route);
798
+ map.set(route.uuid, route);
799
+ appendNode = route.placeHolder;
800
+ element = route;
801
+ }
802
+ else if (tagName === config.tagNames.layout) {
803
+ const childFragment = document.createDocumentFragment();
804
+ // Move child nodes to fragment to avoid duplication of
805
+ for (const childNode of Array.from(element.childNodes)) {
806
+ childFragment.appendChild(childNode);
807
+ }
808
+ const cloneElement = document.importNode(element, true);
809
+ customElements.upgrade(cloneElement);
810
+ cloneElement.appendChild(childFragment);
811
+ const layout = cloneElement;
812
+ const layoutOutlet = createLayoutOutlet();
813
+ layoutOutlet.layout = layout;
814
+ appendNode = layoutOutlet;
815
+ element = cloneElement;
816
+ }
817
+ const children = await _parseNode(routerNode, element, routes, map, routesByPath);
818
+ element.innerHTML = "";
819
+ element.appendChild(children);
820
+ fragment.appendChild(appendNode);
821
+ }
822
+ else {
823
+ fragment.appendChild(childNode);
824
+ }
825
+ }
826
+ return fragment;
827
+ }
828
+ async function parse(routerNode) {
829
+ const map = new Map();
830
+ const routesByPath = new Map();
831
+ const fr = await _parseNode(routerNode, routerNode.template.content, [], map, routesByPath);
832
+ return fr;
833
+ }
834
+
835
+ function testPath(route, path, segments) {
836
+ const params = {};
837
+ const typedParams = {};
838
+ let testResult = true;
839
+ let catchAllFound = false;
840
+ let i = 0, segIndex = 0;
841
+ while (i < route.absoluteSegmentInfos.length) {
842
+ const segmentInfo = route.absoluteSegmentInfos[i];
843
+ // index属性のルートはセグメントを消費しないのでスキップ
844
+ if (segmentInfo.isIndex) {
845
+ i++;
846
+ continue;
847
+ }
848
+ // 先頭の空セグメント(絶対パスの /)はsegmentsから除外されているのでスキップ
849
+ if (i === 0 && segmentInfo.segmentText === '' && segmentInfo.type === 'static') {
850
+ i++;
851
+ continue;
852
+ }
853
+ const segment = segments[segIndex];
854
+ if (segment === undefined) {
855
+ // セグメントが足りない
856
+ testResult = false;
857
+ break;
858
+ }
859
+ let match = false;
860
+ if (segmentInfo.type === "param") {
861
+ const paramType = segmentInfo.paramType || 'any';
862
+ const builtinParamType = builtinParamTypes[paramType];
863
+ const value = builtinParamType.parse(segment);
864
+ if (typeof value !== 'undefined') {
865
+ if (segmentInfo.paramName) {
866
+ params[segmentInfo.paramName] = segment;
867
+ typedParams[segmentInfo.paramName] = value;
868
+ }
869
+ match = true;
870
+ }
871
+ }
872
+ else {
873
+ match = segmentInfo.pattern.exec(segment) !== null;
874
+ }
875
+ if (match) {
876
+ if (segmentInfo.type === 'catch-all') {
877
+ // Catch-all: match remaining segments
878
+ const remainingSegments = segments.slice(segIndex).join('/');
879
+ params['*'] = remainingSegments;
880
+ typedParams['*'] = remainingSegments;
881
+ catchAllFound = true;
882
+ break; // No more segments to process
883
+ }
884
+ }
885
+ else {
886
+ testResult = false;
887
+ break;
888
+ }
889
+ i++;
890
+ segIndex++;
891
+ }
892
+ let finalResult = false;
893
+ if (testResult) {
894
+ if (catchAllFound) {
895
+ // catch-all は残り全部マッチ済み
896
+ finalResult = true;
897
+ }
898
+ else if (i === route.absoluteSegmentInfos.length && segIndex === segments.length) {
899
+ // 全セグメントが消費された
900
+ finalResult = true;
901
+ }
902
+ else if (i === route.absoluteSegmentInfos.length && segIndex === segments.length - 1 && segments.at(-1) === '') {
903
+ // 末尾スラッシュ対応: /users/ -> ['', 'users', '']
904
+ finalResult = true;
905
+ }
906
+ }
907
+ if (finalResult) {
908
+ return {
909
+ path: path,
910
+ routes: route.routes,
911
+ params: params,
912
+ typedParams: typedParams,
913
+ lastPath: ""
914
+ };
915
+ }
916
+ return null;
917
+ }
918
+
919
+ function _matchRoutes(routerNode, routeNode, routes, normalizedPath, segments, results) {
920
+ const nextRoutes = routes.concat(routeNode);
921
+ const matchResult = testPath(routeNode, normalizedPath, segments);
922
+ if (matchResult) {
923
+ results.push(matchResult);
924
+ }
925
+ for (const childRoute of routeNode.routeChildNodes) {
926
+ _matchRoutes(routerNode, childRoute, nextRoutes, normalizedPath, segments, results);
927
+ }
928
+ }
929
+ function matchRoutes(routerNode, normalizedPath) {
930
+ const routes = [];
931
+ const topLevelRoutes = routerNode.routeChildNodes;
932
+ const results = [];
933
+ // セグメント配列を作成(先頭の/は除去せずにそのまま分割)
934
+ // '/' => ['', ''] → filter → ['']
935
+ // '/home' => ['', 'home'] → filter → ['home']
936
+ // '/home/about' => ['', 'home', 'about'] → filter → ['home', 'about']
937
+ // '' => ['']
938
+ const rawSegments = normalizedPath.split('/');
939
+ // 先頭の空セグメント(絶対パスの/)と末尾の空セグメント(/で終わるパス)を除去
940
+ const segments = rawSegments.filter((s, i) => {
941
+ if (i === 0 && s === '')
942
+ return false; // 先頭の空セグメントをスキップ
943
+ if (i === rawSegments.length - 1 && s === '' && rawSegments.length > 1)
944
+ return false; // 末尾の空セグメントをスキップ
945
+ return true;
946
+ });
947
+ for (const route of topLevelRoutes) {
948
+ _matchRoutes(routerNode, route, routes, normalizedPath, segments, results);
949
+ }
950
+ results.sort((a, b) => {
951
+ const lastRouteA = a.routes.at(-1);
952
+ const lastRouteB = b.routes.at(-1);
953
+ const diffSegmentCount = lastRouteA.absoluteSegmentCount - lastRouteB.absoluteSegmentCount;
954
+ if (diffSegmentCount !== 0) {
955
+ return -diffSegmentCount;
956
+ }
957
+ const diffWeight = lastRouteA.absoluteWeight - lastRouteB.absoluteWeight;
958
+ if (diffWeight !== 0) {
959
+ return -diffWeight;
960
+ }
961
+ const diffIndex = lastRouteA.childIndex - lastRouteB.childIndex;
962
+ return diffIndex;
963
+ });
964
+ if (results.length > 0) {
965
+ return results[0];
966
+ }
967
+ return null;
968
+ }
969
+
970
+ function hideRoute(route) {
971
+ route.clearParams();
972
+ for (const node of route.childNodeArray) {
973
+ node.parentNode?.removeChild(node);
974
+ }
975
+ }
976
+
977
+ function showRoute(route, matchResult) {
978
+ route.clearParams();
979
+ for (const key of route.paramNames) {
980
+ route.params[key] = matchResult.params[key];
981
+ route.typedParams[key] = matchResult.typedParams[key];
982
+ }
983
+ const parentNode = route.placeHolder.parentNode;
984
+ const nextSibling = route.placeHolder.nextSibling;
985
+ for (const node of route.childNodeArray) {
986
+ // connectedCallbackが呼ばれる前に、プロパティにパラメータを割り当てる
987
+ // connectedCallbackを実行するときにパラメータはすでに設定されている必要があるため
988
+ if (node.nodeType === Node.ELEMENT_NODE) {
989
+ const element = node;
990
+ element.querySelectorAll('[data-bind]').forEach((e) => {
991
+ assignParams(e, route.typedParams);
992
+ });
993
+ if (element.hasAttribute('data-bind')) {
994
+ assignParams(element, route.typedParams);
995
+ }
996
+ element.querySelectorAll(config.tagNames.layoutOutlet).forEach((layoutOutlet) => {
997
+ layoutOutlet.assignParams(route.typedParams);
998
+ });
999
+ if (element.tagName.toLowerCase() === config.tagNames.layoutOutlet) {
1000
+ element.assignParams(route.typedParams);
1001
+ }
1002
+ }
1003
+ if (nextSibling) {
1004
+ parentNode?.insertBefore(node, nextSibling);
1005
+ }
1006
+ else {
1007
+ parentNode?.appendChild(node);
1008
+ }
1009
+ }
1010
+ return true;
1011
+ }
1012
+
1013
+ async function showRouteContent(routerNode, matchResult, lastRoutes) {
1014
+ // Hide previous routes
1015
+ const routesSet = new Set(matchResult.routes);
1016
+ for (const route of lastRoutes) {
1017
+ if (!routesSet.has(route)) {
1018
+ hideRoute(route);
1019
+ }
1020
+ }
1021
+ try {
1022
+ for (const route of matchResult.routes) {
1023
+ await route.guardCheck(matchResult);
1024
+ }
1025
+ }
1026
+ catch (e) {
1027
+ const err = e;
1028
+ if ("fallbackPath" in err) {
1029
+ const guardCancel = err;
1030
+ console.warn(`Navigation cancelled: ${err.message}. Redirecting to ${guardCancel.fallbackPath}`);
1031
+ queueMicrotask(() => {
1032
+ routerNode.navigate(guardCancel.fallbackPath);
1033
+ });
1034
+ return;
1035
+ }
1036
+ else {
1037
+ throw e;
1038
+ }
1039
+ }
1040
+ const lastRouteSet = new Set(lastRoutes);
1041
+ let force = false;
1042
+ for (const route of matchResult.routes) {
1043
+ if (!lastRouteSet.has(route) || route.shouldChange(matchResult.params) || force) {
1044
+ force = showRoute(route, matchResult);
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ async function applyRoute(routerNode, outlet, fullPath, lastPath) {
1050
+ const basename = routerNode.basename;
1051
+ let sliced = fullPath;
1052
+ if (basename !== "") {
1053
+ if (fullPath === basename) {
1054
+ sliced = "";
1055
+ }
1056
+ else if (fullPath.startsWith(basename + "/")) {
1057
+ sliced = fullPath.slice(basename.length);
1058
+ }
1059
+ }
1060
+ // when fullPath === basename (e.g. "/app"), treat it as root "/"
1061
+ const path = sliced === "" ? "/" : sliced;
1062
+ let matchResult = matchRoutes(routerNode, path);
1063
+ if (!matchResult) {
1064
+ if (routerNode.fallbackRoute) {
1065
+ matchResult = {
1066
+ routes: [routerNode.fallbackRoute],
1067
+ params: {},
1068
+ typedParams: {},
1069
+ path: path,
1070
+ lastPath: lastPath
1071
+ };
1072
+ }
1073
+ else {
1074
+ raiseError(`${config.tagNames.router} No route matched for path: ${path}`);
1075
+ }
1076
+ }
1077
+ matchResult.lastPath = lastPath;
1078
+ const lastRoutes = outlet.lastRoutes;
1079
+ await showRouteContent(routerNode, matchResult, lastRoutes);
1080
+ // if successful, update router and outlet state
1081
+ routerNode.path = path;
1082
+ outlet.lastRoutes = matchResult.routes;
1083
+ }
1084
+
1085
+ function getNavigation() {
1086
+ const nav = window.navigation;
1087
+ if (!nav) {
1088
+ return null;
1089
+ }
1090
+ if (typeof nav.addEventListener !== "function" || typeof nav.removeEventListener !== "function") {
1091
+ return null;
1092
+ }
1093
+ return nav;
1094
+ }
1095
+
1096
+ /**
1097
+ * AppRoutes - Root component for @wcstack/router
1098
+ *
1099
+ * Container element that manages route definitions and navigation.
1100
+ */
1101
+ class Router extends HTMLElement {
1102
+ static _instance = null;
1103
+ _outlet = null;
1104
+ _template = null;
1105
+ _routeChildNodes = [];
1106
+ _basename = '';
1107
+ _path = '';
1108
+ _initialized = false;
1109
+ _fallbackRoute = null;
1110
+ _listeningPopState = false;
1111
+ constructor() {
1112
+ super();
1113
+ if (Router._instance) {
1114
+ raiseError(`${config.tagNames.router} can only be instantiated once.`);
1115
+ }
1116
+ Router._instance = this;
1117
+ }
1118
+ /**
1119
+ * Normalize a URL pathname to a route path.
1120
+ * - ensure leading slash
1121
+ * - collapse multiple slashes
1122
+ * - treat trailing file extensions (e.g. .html) as directory root
1123
+ * - remove trailing slash except root "/"
1124
+ */
1125
+ _normalizePathname(_path) {
1126
+ let path = _path || "/";
1127
+ if (!path.startsWith("/"))
1128
+ path = "/" + path;
1129
+ path = path.replace(/\/{2,}/g, "/");
1130
+ // e.g. "/app/index.html" -> "/app"
1131
+ const exts = config.basenameFileExtensions;
1132
+ if (exts.length > 0) {
1133
+ const extPattern = new RegExp(`\\/[^/]+(?:${exts.map(e => e.replace(/\./g, '\\.')).join('|')})$`, 'i');
1134
+ path = path.replace(extPattern, "");
1135
+ }
1136
+ if (path === "")
1137
+ path = "/";
1138
+ if (path.length > 1 && path.endsWith("/"))
1139
+ path = path.slice(0, -1);
1140
+ return path;
1141
+ }
1142
+ /**
1143
+ * Normalize basename.
1144
+ * - "" or "/" -> ""
1145
+ * - "/app/" -> "/app"
1146
+ * - "/app/index.html" -> "/app"
1147
+ */
1148
+ _normalizeBasename(_path) {
1149
+ let path = _path || "";
1150
+ if (!path)
1151
+ return "";
1152
+ if (!path.startsWith("/"))
1153
+ path = "/" + path;
1154
+ path = path.replace(/\/{2,}/g, "/");
1155
+ const exts = config.basenameFileExtensions;
1156
+ if (exts.length > 0) {
1157
+ const extPattern = new RegExp(`\\/[^/]+(?:${exts.map(e => e.replace(/\./g, '\\.')).join('|')})$`, 'i');
1158
+ path = path.replace(extPattern, "");
1159
+ }
1160
+ if (path.length > 1 && path.endsWith("/"))
1161
+ path = path.slice(0, -1);
1162
+ if (path === "/")
1163
+ return "";
1164
+ return path;
1165
+ }
1166
+ _joinInternalPath(basename, to) {
1167
+ const base = this._normalizeBasename(basename);
1168
+ // accept "about" as "/about"
1169
+ let path = to.startsWith("/") ? to : "/" + to;
1170
+ path = this._normalizePathname(path);
1171
+ if (!base)
1172
+ return path;
1173
+ // keep "/app/" for root
1174
+ if (path === "/")
1175
+ return base + "/";
1176
+ return base + path;
1177
+ }
1178
+ _notifyLocationChange() {
1179
+ // For environments without Navigation API (and for Link active-state updates)
1180
+ window.dispatchEvent(new CustomEvent("wcs:navigate"));
1181
+ }
1182
+ _getBasename() {
1183
+ const base = new URL(document.baseURI);
1184
+ let path = base.pathname || "/";
1185
+ if (path === "/") {
1186
+ return "";
1187
+ }
1188
+ return this._normalizeBasename(path);
1189
+ }
1190
+ static get instance() {
1191
+ if (!Router._instance) {
1192
+ raiseError(`${config.tagNames.router} has not been instantiated.`);
1193
+ }
1194
+ return Router._instance;
1195
+ }
1196
+ static navigate(path) {
1197
+ Router.instance.navigate(path);
1198
+ }
1199
+ get basename() {
1200
+ return this._basename;
1201
+ }
1202
+ _getOutlet() {
1203
+ let outlet = document.querySelector(config.tagNames.outlet);
1204
+ if (!outlet) {
1205
+ outlet = createOutlet();
1206
+ document.body.appendChild(outlet);
1207
+ }
1208
+ return outlet;
1209
+ }
1210
+ _getTemplate() {
1211
+ const template = this.querySelector("template");
1212
+ return template;
1213
+ }
1214
+ get outlet() {
1215
+ if (!this._outlet) {
1216
+ raiseError(`${config.tagNames.router} has no outlet.`);
1217
+ }
1218
+ return this._outlet;
1219
+ }
1220
+ get template() {
1221
+ if (!this._template) {
1222
+ raiseError(`${config.tagNames.router} has no template.`);
1223
+ }
1224
+ return this._template;
1225
+ }
1226
+ get routeChildNodes() {
1227
+ return this._routeChildNodes;
1228
+ }
1229
+ get path() {
1230
+ return this._path;
1231
+ }
1232
+ /**
1233
+ * applyRoute 内で設定される値です。
1234
+ */
1235
+ set path(value) {
1236
+ this._path = value;
1237
+ }
1238
+ get fallbackRoute() {
1239
+ return this._fallbackRoute;
1240
+ }
1241
+ /**
1242
+ * Routeのfallback属性がある場合にそのルートを設定します。
1243
+ */
1244
+ set fallbackRoute(value) {
1245
+ this._fallbackRoute = value;
1246
+ }
1247
+ async navigate(path) {
1248
+ const fullPath = this._joinInternalPath(this._basename, path);
1249
+ const navigation = getNavigation();
1250
+ if (navigation?.navigate) {
1251
+ navigation.navigate(fullPath);
1252
+ }
1253
+ else {
1254
+ history.pushState(null, '', fullPath);
1255
+ await applyRoute(this, this.outlet, fullPath, this._path);
1256
+ this._notifyLocationChange();
1257
+ }
1258
+ }
1259
+ _onNavigateFunc(navEvent) {
1260
+ if (!navEvent.canIntercept ||
1261
+ navEvent.hashChange ||
1262
+ navEvent.downloadRequest !== null) {
1263
+ return;
1264
+ }
1265
+ const routesNode = this;
1266
+ navEvent.intercept({
1267
+ handler: async () => {
1268
+ const url = new URL(navEvent.destination.url);
1269
+ const fullPath = routesNode._normalizePathname(url.pathname);
1270
+ await applyRoute(routesNode, routesNode.outlet, fullPath, routesNode.path);
1271
+ },
1272
+ });
1273
+ }
1274
+ _onNavigate = this._onNavigateFunc.bind(this);
1275
+ _onPopState = async () => {
1276
+ // back/forward for environments without Navigation API
1277
+ const fullPath = this._normalizePathname(window.location.pathname);
1278
+ await applyRoute(this, this.outlet, fullPath, this._path);
1279
+ this._notifyLocationChange();
1280
+ };
1281
+ async _initialize() {
1282
+ this._initialized = true;
1283
+ this._basename = this._normalizeBasename(this.getAttribute("basename") || this._getBasename() || "");
1284
+ const hasBaseTag = document.querySelector('base[href]') !== null;
1285
+ const url = new URL(window.location.href);
1286
+ if (this._basename === "" && !hasBaseTag && url.pathname !== "/") {
1287
+ raiseError(`${config.tagNames.router} basename is empty, but current path is not "/".`);
1288
+ }
1289
+ this._outlet = this._getOutlet();
1290
+ this._outlet.routesNode = this;
1291
+ this._template = this._getTemplate();
1292
+ if (!this._template) {
1293
+ raiseError(`${config.tagNames.router} should have a <template> child element.`);
1294
+ }
1295
+ const fragment = await parse(this);
1296
+ this._outlet.rootNode.appendChild(fragment);
1297
+ if (this.routeChildNodes.length === 0) {
1298
+ raiseError(`${config.tagNames.router} has no route definitions.`);
1299
+ }
1300
+ const fullPath = this._normalizePathname(window.location.pathname);
1301
+ await applyRoute(this, this.outlet, fullPath, this._path);
1302
+ this._notifyLocationChange();
1303
+ }
1304
+ async connectedCallback() {
1305
+ if (!this._initialized) {
1306
+ await this._initialize();
1307
+ }
1308
+ getNavigation()?.addEventListener("navigate", this._onNavigate);
1309
+ // Fallback for browsers without Navigation API
1310
+ if (!getNavigation()?.addEventListener && !this._listeningPopState) {
1311
+ window.addEventListener("popstate", this._onPopState);
1312
+ this._listeningPopState = true;
1313
+ }
1314
+ }
1315
+ disconnectedCallback() {
1316
+ getNavigation()?.removeEventListener("navigate", this._onNavigate);
1317
+ if (this._listeningPopState) {
1318
+ window.removeEventListener("popstate", this._onPopState);
1319
+ this._listeningPopState = false;
1320
+ }
1321
+ if (Router._instance === this) {
1322
+ Router._instance = null;
1323
+ }
1324
+ }
1325
+ }
1326
+
1327
+ class Link extends HTMLElement {
1328
+ static get observedAttributes() {
1329
+ return ['to'];
1330
+ }
1331
+ _childNodeArray = [];
1332
+ _uuid = getUUID();
1333
+ _path = "";
1334
+ _router = null;
1335
+ _anchorElement = null;
1336
+ _initialized = false;
1337
+ _onClick;
1338
+ constructor() {
1339
+ super();
1340
+ }
1341
+ get uuid() {
1342
+ return this._uuid;
1343
+ }
1344
+ get router() {
1345
+ if (this._router) {
1346
+ return this._router;
1347
+ }
1348
+ const router = document.querySelector(config.tagNames.router);
1349
+ if (router) {
1350
+ return (this._router = router);
1351
+ }
1352
+ raiseError(`${config.tagNames.link} is not connected to a router.`);
1353
+ }
1354
+ _initialize() {
1355
+ this.style.display = "none";
1356
+ this._childNodeArray = Array.from(this.childNodes);
1357
+ this._path = this.getAttribute('to') || '';
1358
+ this._initialized = true;
1359
+ }
1360
+ _normalizePathname(path) {
1361
+ let p = path || "/";
1362
+ if (!p.startsWith("/"))
1363
+ p = "/" + p;
1364
+ p = p.replace(/\/{2,}/g, "/");
1365
+ if (p.length > 1 && p.endsWith("/"))
1366
+ p = p.slice(0, -1);
1367
+ return p;
1368
+ }
1369
+ _joinInternalPath(basename, to) {
1370
+ const base = (basename || "").replace(/\/{2,}/g, "/").replace(/\/$/, "");
1371
+ const internal = to.startsWith("/") ? to : "/" + to;
1372
+ const path = this._normalizePathname(internal);
1373
+ if (!base)
1374
+ return path;
1375
+ if (path === "/")
1376
+ return base + "/";
1377
+ return base + path;
1378
+ }
1379
+ _setAnchorHref(anchor, path) {
1380
+ if (path.startsWith('/')) {
1381
+ anchor.href = this._joinInternalPath(this.router.basename, path);
1382
+ }
1383
+ else {
1384
+ try {
1385
+ anchor.href = new URL(path).toString();
1386
+ }
1387
+ catch {
1388
+ raiseError(`[${config.tagNames.link}] Invalid URL in 'to' attribute: ${path}`);
1389
+ }
1390
+ }
1391
+ }
1392
+ connectedCallback() {
1393
+ if (!this._initialized) {
1394
+ this._initialize();
1395
+ }
1396
+ const parentNode = this.parentNode;
1397
+ if (!parentNode) {
1398
+ // should not happen if connected
1399
+ return;
1400
+ }
1401
+ const nextSibling = this.nextSibling;
1402
+ const link = document.createElement('a');
1403
+ this._setAnchorHref(link, this._path);
1404
+ for (const childNode of this._childNodeArray) {
1405
+ link.appendChild(childNode);
1406
+ }
1407
+ if (nextSibling) {
1408
+ parentNode.insertBefore(link, nextSibling);
1409
+ }
1410
+ else {
1411
+ parentNode.appendChild(link);
1412
+ }
1413
+ this._anchorElement = link;
1414
+ // ロケーション変更を監視
1415
+ getNavigation()?.addEventListener('currententrychange', this._updateActiveState);
1416
+ window.addEventListener('wcs:navigate', this._updateActiveState);
1417
+ window.addEventListener('popstate', this._updateActiveState);
1418
+ // Navigation API が無い場合は、クリックで router.navigate にフォールバック
1419
+ if (this._path.startsWith('/') && !getNavigation()?.navigate) {
1420
+ this._onClick = async (e) => {
1421
+ // only left-click without modifiers
1422
+ if (e.defaultPrevented)
1423
+ return;
1424
+ if (e.button !== 0)
1425
+ return;
1426
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
1427
+ return;
1428
+ e.preventDefault();
1429
+ await this.router.navigate(this._path);
1430
+ this._updateActiveState();
1431
+ };
1432
+ link.addEventListener('click', this._onClick);
1433
+ }
1434
+ this._updateActiveState();
1435
+ }
1436
+ disconnectedCallback() {
1437
+ getNavigation()?.removeEventListener('currententrychange', this._updateActiveState);
1438
+ window.removeEventListener('wcs:navigate', this._updateActiveState);
1439
+ window.removeEventListener('popstate', this._updateActiveState);
1440
+ if (this._anchorElement) {
1441
+ if (this._onClick) {
1442
+ this._anchorElement.removeEventListener('click', this._onClick);
1443
+ this._onClick = undefined;
1444
+ }
1445
+ this._anchorElement.remove();
1446
+ this._anchorElement = null;
1447
+ }
1448
+ for (const childNode of this._childNodeArray) {
1449
+ childNode.parentNode?.removeChild(childNode);
1450
+ }
1451
+ }
1452
+ attributeChangedCallback(name, oldValue, newValue) {
1453
+ if (name === 'to' && oldValue !== newValue) {
1454
+ this._path = newValue || '';
1455
+ if (this._anchorElement) {
1456
+ this._setAnchorHref(this._anchorElement, this._path);
1457
+ this._updateActiveState();
1458
+ }
1459
+ }
1460
+ }
1461
+ _updateActiveState = () => {
1462
+ const currentPath = this._normalizePathname(new URL(window.location.href).pathname);
1463
+ const linkPath = this._normalizePathname(this._path.startsWith('/') ? this._joinInternalPath(this.router.basename, this._path) : this._path);
1464
+ if (this._anchorElement) {
1465
+ if (currentPath === linkPath) {
1466
+ this._anchorElement.classList.add('active');
1467
+ }
1468
+ else {
1469
+ this._anchorElement.classList.remove('active');
1470
+ }
1471
+ }
1472
+ };
1473
+ get anchorElement() {
1474
+ return this._anchorElement;
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * グローバルHeadスタック
1480
+ * 最後に接続されたHeadが優先される
1481
+ */
1482
+ const headStack = [];
1483
+ /**
1484
+ * 初期の<head>内容を記憶(最初のHead接続時に保存)
1485
+ */
1486
+ const initialHeadValues = new Map();
1487
+ let initialHeadCaptured = false;
1488
+ class Head extends HTMLElement {
1489
+ _initialized = false;
1490
+ _childElementArray = [];
1491
+ constructor() {
1492
+ super();
1493
+ this.style.display = 'none';
1494
+ }
1495
+ _initialize() {
1496
+ if (this._initialized) {
1497
+ return;
1498
+ }
1499
+ this._initialized = true;
1500
+ this._childElementArray = Array.from(this.children);
1501
+ for (const child of this._childElementArray) {
1502
+ this.removeChild(child);
1503
+ }
1504
+ }
1505
+ connectedCallback() {
1506
+ this._initialize();
1507
+ // 初回のみ初期状態を保存
1508
+ if (!initialHeadCaptured) {
1509
+ this._captureInitialHead();
1510
+ initialHeadCaptured = true;
1511
+ }
1512
+ // スタックに追加
1513
+ headStack.push(this);
1514
+ // headを再適用
1515
+ this._reapplyHead();
1516
+ }
1517
+ disconnectedCallback() {
1518
+ // スタックから削除
1519
+ const index = headStack.indexOf(this);
1520
+ if (index !== -1) {
1521
+ headStack.splice(index, 1);
1522
+ }
1523
+ // headを再適用(スタックが空なら初期状態に戻す)
1524
+ this._reapplyHead();
1525
+ }
1526
+ get childElementArray() {
1527
+ if (!this._initialized) {
1528
+ raiseError('Head component is not initialized yet.');
1529
+ }
1530
+ return this._childElementArray;
1531
+ }
1532
+ /**
1533
+ * 要素の一意キーを生成
1534
+ */
1535
+ _getKey(el) {
1536
+ const tag = el.tagName.toLowerCase();
1537
+ if (tag === 'title') {
1538
+ return 'title';
1539
+ }
1540
+ if (tag === 'meta') {
1541
+ const name = el.getAttribute('name') || '';
1542
+ const property = el.getAttribute('property') || '';
1543
+ const httpEquiv = el.getAttribute('http-equiv') || '';
1544
+ const charset = el.hasAttribute('charset') ? 'charset' : '';
1545
+ const media = el.getAttribute('media') || '';
1546
+ return `meta:${name}:${property}:${httpEquiv}:${charset}:${media}`;
1547
+ }
1548
+ if (tag === 'link') {
1549
+ const rel = el.getAttribute('rel') || '';
1550
+ const href = el.getAttribute('href') || '';
1551
+ const media = el.getAttribute('media') || '';
1552
+ return `link:${rel}:${href}:${media}`;
1553
+ }
1554
+ if (tag === 'base') {
1555
+ return 'base';
1556
+ }
1557
+ // script, style等はouterHTMLの先頭で識別(フォールバック)
1558
+ return `${tag}:${el.outerHTML.slice(0, 100)}`;
1559
+ }
1560
+ /**
1561
+ * head内で指定のキーに一致する要素を検索
1562
+ */
1563
+ _findInHead(key) {
1564
+ const head = document.head;
1565
+ for (const el of Array.from(head.children)) {
1566
+ if (this._getKey(el) === key) {
1567
+ return el;
1568
+ }
1569
+ }
1570
+ return null;
1571
+ }
1572
+ /**
1573
+ * 初期の<head>状態をキャプチャ
1574
+ * document.head内の全ての要素をスキャンして保存する
1575
+ */
1576
+ _captureInitialHead() {
1577
+ const head = document.head;
1578
+ for (const child of Array.from(head.children)) {
1579
+ const key = this._getKey(child);
1580
+ if (!initialHeadValues.has(key)) {
1581
+ initialHeadValues.set(key, child.cloneNode(true));
1582
+ }
1583
+ }
1584
+ }
1585
+ /**
1586
+ * スタック全体からheadを再構築
1587
+ * 後のHeadが優先される(上書き)
1588
+ */
1589
+ _reapplyHead() {
1590
+ // 全スタックのHeadが扱うキーを収集
1591
+ const allKeys = new Set();
1592
+ for (const head of headStack) {
1593
+ for (const child of head._childElementArray) {
1594
+ allKeys.add(this._getKey(child));
1595
+ }
1596
+ }
1597
+ // 初期値にあるキーも追加
1598
+ for (const key of initialHeadValues.keys()) {
1599
+ allKeys.add(key);
1600
+ }
1601
+ // 現在のheadにある要素のキーも追加(管理下から外れたものを削除するため)
1602
+ for (const child of Array.from(document.head.children)) {
1603
+ allKeys.add(this._getKey(child));
1604
+ }
1605
+ // 各キーについて、最も優先度の高い値を決定
1606
+ for (const key of allKeys) {
1607
+ // スタックを逆順に見て、最初に見つかった値を使用
1608
+ let targetElement = null;
1609
+ for (let i = headStack.length - 1; i >= 0; i--) {
1610
+ const head = headStack[i];
1611
+ for (const child of head._childElementArray) {
1612
+ if (this._getKey(child) === key) {
1613
+ targetElement = child.cloneNode(true);
1614
+ break;
1615
+ }
1616
+ }
1617
+ if (targetElement)
1618
+ break;
1619
+ }
1620
+ // スタックに該当がなければ初期値を使用
1621
+ if (!targetElement && initialHeadValues.has(key)) {
1622
+ const initial = initialHeadValues.get(key);
1623
+ // initialHeadValuesにはnullを保存しないため、has(key)がtrueならinitialは必ず存在しElementである
1624
+ targetElement = initial.cloneNode(true);
1625
+ }
1626
+ // headを更新
1627
+ const current = this._findInHead(key);
1628
+ if (targetElement) {
1629
+ if (current) {
1630
+ current.replaceWith(targetElement);
1631
+ }
1632
+ else {
1633
+ document.head.appendChild(targetElement);
1634
+ }
1635
+ }
1636
+ else {
1637
+ // 初期値もスタックにもない場合は削除
1638
+ current?.remove();
1639
+ }
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ function registerComponents() {
1645
+ // Register custom element
1646
+ if (!customElements.get(config.tagNames.layout)) {
1647
+ customElements.define(config.tagNames.layout, Layout);
1648
+ }
1649
+ if (!customElements.get(config.tagNames.layoutOutlet)) {
1650
+ customElements.define(config.tagNames.layoutOutlet, LayoutOutlet);
1651
+ }
1652
+ if (!customElements.get(config.tagNames.outlet)) {
1653
+ customElements.define(config.tagNames.outlet, Outlet);
1654
+ }
1655
+ if (!customElements.get(config.tagNames.route)) {
1656
+ customElements.define(config.tagNames.route, Route);
1657
+ }
1658
+ if (!customElements.get(config.tagNames.router)) {
1659
+ customElements.define(config.tagNames.router, Router);
1660
+ }
1661
+ if (!customElements.get(config.tagNames.link)) {
1662
+ customElements.define(config.tagNames.link, Link);
1663
+ }
1664
+ if (!customElements.get(config.tagNames.head)) {
1665
+ customElements.define(config.tagNames.head, Head);
1666
+ }
1667
+ }
1668
+
1669
+ /**
1670
+ * Initialize the router with optional configuration.
1671
+ * This is the main entry point for setting up the router.
1672
+ * @param config - Optional partial configuration to override defaults
1673
+ */
1674
+ function bootstrapRouter(config) {
1675
+ if (config) {
1676
+ setConfig(config);
1677
+ }
1678
+ registerComponents();
1679
+ }
1680
+
1681
+ export { bootstrapRouter };
1682
+ //# sourceMappingURL=index.esm.js.map