@iyulab/router 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.cjs.js DELETED
@@ -1,819 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const lit = require("lit");
4
- const decorators_js = require("lit/decorators.js");
5
- const react = require("@lit/react");
6
- const React = require("react");
7
- const client = require("react-dom/client");
8
- function absolutePath(...paths) {
9
- paths = paths.map((p) => p.replace(/^\/|\/$/g, "")).filter((p) => p.length > 0);
10
- if (paths.length === 0) return "/";
11
- return "/" + paths.join("/");
12
- }
13
- function parseURL(url, basepath) {
14
- let urlObj;
15
- basepath = catchBasepath(basepath);
16
- if (url.startsWith("http")) {
17
- urlObj = new URL(url);
18
- } else if (url.startsWith("/")) {
19
- urlObj = new URL(url, window.location.origin);
20
- } else if (url.startsWith("?")) {
21
- urlObj = new URL(window.location.pathname + url, window.location.origin);
22
- } else if (url.startsWith("#")) {
23
- urlObj = new URL(window.location.pathname + window.location.search + url, window.location.origin);
24
- } else {
25
- urlObj = new URL(absolutePath(basepath, url), window.location.origin);
26
- }
27
- return {
28
- href: urlObj.href,
29
- origin: urlObj.origin,
30
- basepath,
31
- path: urlObj.href.replace(urlObj.origin, ""),
32
- pathname: urlObj.pathname,
33
- query: new URLSearchParams(urlObj.search),
34
- hash: urlObj.hash,
35
- params: {}
36
- };
37
- }
38
- function catchBasepath(basepath) {
39
- if (basepath === "/") return basepath;
40
- let pattern = new URLPattern({ pathname: basepath + "/*" });
41
- let match = pattern.exec({ pathname: window.location.pathname });
42
- if (match) {
43
- const rawPath = match.pathname.input;
44
- const restPath = match.pathname.groups?.["0"];
45
- return restPath ? rawPath.replace("/" + restPath, "") : rawPath.slice(0, -1);
46
- }
47
- pattern = new URLPattern({ pathname: `${basepath}{/}?` });
48
- match = pattern.exec({ pathname: window.location.pathname });
49
- if (match) {
50
- return match.pathname.input;
51
- }
52
- return basepath;
53
- }
54
- function getRandomID() {
55
- return window.isSecureContext ? window.crypto.randomUUID() : window.crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
56
- }
57
- class RouteError extends Error {
58
- constructor(code, message, original) {
59
- super(message);
60
- this.name = "RouteError";
61
- this.code = code;
62
- this.original = original;
63
- this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
64
- if (Error.captureStackTrace) {
65
- Error.captureStackTrace(this, RouteError);
66
- }
67
- }
68
- }
69
- class NotFoundRouteError extends RouteError {
70
- constructor(path, original) {
71
- super(404, `Page not found: ${path}`, original);
72
- this.name = "NotFoundError";
73
- if (Error.captureStackTrace) {
74
- Error.captureStackTrace(this, NotFoundRouteError);
75
- }
76
- }
77
- }
78
- class RouteEvent extends Event {
79
- constructor(type, routeInfo, cancelable = false) {
80
- super(type, { bubbles: true, composed: true, cancelable });
81
- this.routeInfo = routeInfo;
82
- this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
83
- }
84
- /** 이벤트가 취소되었는지 확인 */
85
- get cancelled() {
86
- return this.defaultPrevented;
87
- }
88
- /** 이벤트 취소 */
89
- cancel() {
90
- if (this.cancelable) {
91
- this.preventDefault();
92
- }
93
- }
94
- }
95
- class RouteBeginEvent extends RouteEvent {
96
- constructor(routeInfo) {
97
- super("route-begin", routeInfo, false);
98
- }
99
- }
100
- class RouteDoneEvent extends RouteEvent {
101
- constructor(routeInfo) {
102
- super("route-done", routeInfo, false);
103
- }
104
- }
105
- class RouteErrorEvent extends RouteEvent {
106
- constructor(error, routeInfo) {
107
- super("route-error", routeInfo, false);
108
- this.error = error;
109
- }
110
- }
111
- const styles = lit.css`
112
- :host {
113
- display: flex;
114
- justify-content: center;
115
- align-items: center;
116
- min-height: 100vh;
117
- width: 100%;
118
- padding: 2rem;
119
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
120
- background: var(--route-error-background, linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%));
121
- color: var(--route-error-color, #2d3748);
122
- line-height: 1.6;
123
- }
124
-
125
- .container {
126
- max-width: 520px;
127
- margin: 0 auto;
128
- text-align: center;
129
- background: var(--route-error-container-bg, rgba(255, 255, 255, 0.95));
130
- backdrop-filter: blur(10px);
131
- border-radius: var(--route-error-border-radius, 24px);
132
- padding: 3rem 2rem;
133
- box-shadow: var(--route-error-box-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
134
- border: 1px solid var(--route-error-border, rgba(255, 255, 255, 0.2));
135
- animation: slideUp 0.6s ease-out;
136
- }
137
-
138
- @keyframes slideUp {
139
- from {
140
- opacity: 0;
141
- transform: translateY(30px);
142
- }
143
- to {
144
- opacity: 1;
145
- transform: translateY(0);
146
- }
147
- }
148
-
149
- .icon {
150
- font-size: 5rem;
151
- margin-bottom: 1.5rem;
152
- animation: bounce 0.8s ease-out 0.2s both;
153
- filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
154
- }
155
-
156
- @keyframes bounce {
157
- 0%, 20%, 53%, 80%, 100% {
158
- transform: translate3d(0, 0, 0);
159
- }
160
- 40%, 43% {
161
- transform: translate3d(0, -10px, 0);
162
- }
163
- 70% {
164
- transform: translate3d(0, -5px, 0);
165
- }
166
- 90% {
167
- transform: translate3d(0, -2px, 0);
168
- }
169
- }
170
-
171
- .code {
172
- font-size: 2rem;
173
- font-weight: 700;
174
- margin-bottom: 1rem;
175
- color: var(--route-error-code-color, #4a5568);
176
- letter-spacing: -0.025em;
177
- }
178
-
179
- .message {
180
- font-size: 1.125rem;
181
- margin-bottom: 2.5rem;
182
- color: var(--route-error-message-color, #718096);
183
- font-weight: 400;
184
- max-width: 400px;
185
- margin-left: auto;
186
- margin-right: auto;
187
- }
188
-
189
- .actions {
190
- display: flex;
191
- gap: 1rem;
192
- justify-content: center;
193
- flex-wrap: wrap;
194
- }
195
-
196
- .button {
197
- position: relative;
198
- padding: 0.875rem 2rem;
199
- border: none;
200
- border-radius: 12px;
201
- font-size: 0.95rem;
202
- font-weight: 600;
203
- cursor: pointer;
204
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
205
- text-decoration: none;
206
- display: inline-flex;
207
- align-items: center;
208
- justify-content: center;
209
- gap: 0.5rem;
210
- font-family: inherit;
211
- min-width: 120px;
212
- overflow: hidden;
213
- user-select: none;
214
- -webkit-tap-highlight-color: transparent;
215
- }
216
-
217
- .button:first-child {
218
- background: var(--route-error-primary-button-bg, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
219
- color: var(--route-error-primary-button-color, white);
220
- box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.4);
221
- }
222
-
223
- .button:first-child:hover {
224
- transform: translateY(-2px);
225
- box-shadow: 0 8px 25px 0 rgba(102, 126, 234, 0.5);
226
- }
227
-
228
- .button:first-child:active {
229
- transform: translateY(0);
230
- }
231
-
232
- .button:last-child {
233
- background: var(--route-error-secondary-button-bg, rgba(255, 255, 255, 0.9));
234
- color: var(--route-error-secondary-button-color, #4a5568);
235
- border: 2px solid var(--route-error-secondary-button-border, rgba(74, 85, 104, 0.2));
236
- backdrop-filter: blur(10px);
237
- }
238
-
239
- .button:last-child:hover {
240
- background: var(--route-error-secondary-button-hover-bg, rgba(255, 255, 255, 1));
241
- border-color: var(--route-error-secondary-button-hover-border, rgba(74, 85, 104, 0.4));
242
- transform: translateY(-1px);
243
- }
244
-
245
- .button:focus-visible {
246
- outline: 2px solid var(--route-error-focus-color, #667eea);
247
- outline-offset: 2px;
248
- }
249
-
250
- .button::before {
251
- content: '';
252
- position: absolute;
253
- top: 0;
254
- left: -100%;
255
- width: 100%;
256
- height: 100%;
257
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
258
- transition: left 0.5s;
259
- }
260
-
261
- .button:hover::before {
262
- left: 100%;
263
- }
264
-
265
- @media (max-width: 640px) {
266
- :host {
267
- padding: 1rem;
268
- }
269
-
270
- .container {
271
- padding: 2rem 1.5rem;
272
- border-radius: 20px;
273
- }
274
-
275
- .icon {
276
- font-size: 4rem;
277
- }
278
-
279
- .code {
280
- font-size: 1.75rem;
281
- }
282
-
283
- .message {
284
- font-size: 1rem;
285
- margin-bottom: 2rem;
286
- }
287
-
288
- .actions {
289
- flex-direction: column;
290
- align-items: center;
291
- gap: 0.75rem;
292
- }
293
-
294
- .button {
295
- width: 100%;
296
- max-width: 280px;
297
- padding: 1rem 2rem;
298
- }
299
- }
300
-
301
- @media (max-width: 480px) {
302
- .container {
303
- margin: 1rem;
304
- padding: 1.5rem 1rem;
305
- }
306
-
307
- .icon {
308
- font-size: 3.5rem;
309
- }
310
- }
311
-
312
- @media (prefers-color-scheme: dark) {
313
- :host {
314
- background: var(--route-error-dark-background, linear-gradient(135deg, #1a202c 0%, #2d3748 100%));
315
- color: var(--route-error-dark-color, #e2e8f0);
316
- }
317
-
318
- .container {
319
- background: var(--route-error-dark-container-bg, rgba(45, 55, 72, 0.95));
320
- border: 1px solid var(--route-error-dark-border, rgba(255, 255, 255, 0.1));
321
- }
322
-
323
- .code {
324
- color: var(--route-error-dark-code-color, #f7fafc);
325
- }
326
-
327
- .message {
328
- color: var(--route-error-dark-message-color, #a0aec0);
329
- }
330
-
331
- .button:first-child {
332
- background: var(--route-error-dark-primary-button-bg, linear-gradient(135deg, #553c9a 0%, #764ba2 100%));
333
- box-shadow: 0 4px 15px 0 rgba(85, 60, 154, 0.4);
334
- }
335
-
336
- .button:first-child:hover {
337
- box-shadow: 0 8px 25px 0 rgba(85, 60, 154, 0.5);
338
- }
339
-
340
- .button:last-child {
341
- background: var(--route-error-dark-secondary-button-bg, rgba(74, 85, 104, 0.3));
342
- color: var(--route-error-dark-secondary-button-color, #e2e8f0);
343
- border: 2px solid var(--route-error-dark-secondary-button-border, rgba(226, 232, 240, 0.2));
344
- }
345
-
346
- .button:last-child:hover {
347
- background: var(--route-error-dark-secondary-button-hover-bg, rgba(74, 85, 104, 0.5));
348
- border-color: var(--route-error-dark-secondary-button-hover-border, rgba(226, 232, 240, 0.4));
349
- }
350
- }
351
-
352
- @media (prefers-reduced-motion: reduce) {
353
- .container {
354
- animation: none;
355
- }
356
-
357
- .icon {
358
- animation: none;
359
- }
360
-
361
- .button::before {
362
- display: none;
363
- }
364
-
365
- .button {
366
- transition: none;
367
- }
368
- }
369
- `;
370
- var __defProp$1 = Object.defineProperty;
371
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
372
- var __decorateClass$1 = (decorators, target, key, kind) => {
373
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
374
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
375
- if (decorator = decorators[i])
376
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
377
- if (kind && result) __defProp$1(target, key, result);
378
- return result;
379
- };
380
- let ErrorPage = class extends lit.LitElement {
381
- render() {
382
- const error = this.error || this.getDefaultError();
383
- const icon = this.getErrorIcon(error.code);
384
- return lit.html`
385
- <div class="container" role="alert" aria-live="polite">
386
- <div class="icon" aria-hidden="true">${icon}</div>
387
- <div class="code" aria-label="Error code">${error.code}</div>
388
- <div class="message">${error.message}</div>
389
-
390
- <div class="actions">
391
- <button
392
- class="button"
393
- @click=${this.handleGoBack}
394
- title="Go back to previous page"
395
- aria-label="Go back to previous page">
396
- ← Go Back
397
- </button>
398
-
399
- <button
400
- class="button"
401
- @click=${this.handleRefresh}
402
- title="Refresh the current page"
403
- aria-label="Refresh the current page">
404
- 🔄 Refresh
405
- </button>
406
- </div>
407
- </div>
408
- `;
409
- }
410
- /** 기본 에러 정보 반환 */
411
- getDefaultError() {
412
- return new RouteError(500, "Something went wrong. Please try again or contact support if the problem persists.");
413
- }
414
- /** 에러 코드에 따른 기본 아이콘 반환 */
415
- getErrorIcon(code) {
416
- const numericCode = typeof code === "string" ? parseInt(code) : code;
417
- switch (numericCode) {
418
- case 404:
419
- return "🔍";
420
- case 403:
421
- return "🔒";
422
- case 401:
423
- return "🔑";
424
- case 429:
425
- return "⏱️";
426
- case 503:
427
- return "🛠️";
428
- case 500:
429
- default:
430
- return "⚠️";
431
- }
432
- }
433
- /** 뒤로가기 */
434
- handleGoBack() {
435
- window.history.back();
436
- }
437
- /** 새로고침 */
438
- handleRefresh() {
439
- window.location.reload();
440
- }
441
- };
442
- ErrorPage.styles = styles;
443
- __decorateClass$1([
444
- decorators_js.property({ type: Object })
445
- ], ErrorPage.prototype, "error", 2);
446
- ErrorPage = __decorateClass$1([
447
- decorators_js.customElement("u-error-page")
448
- ], ErrorPage);
449
- class Router {
450
- constructor(config) {
451
- this._counter = 0;
452
- this.handlePopstate = async () => {
453
- const href = window.location.href;
454
- await this.go(href);
455
- };
456
- this._rootElement = config.root;
457
- this._basepath = absolutePath(config.basepath || "/");
458
- this._routes = this.setRoutes(config.routes, this._basepath);
459
- window.removeEventListener("popstate", this.handlePopstate);
460
- window.addEventListener("popstate", this.handlePopstate);
461
- this.initiate();
462
- }
463
- get basepath() {
464
- return this._basepath;
465
- }
466
- get routes() {
467
- return this._routes;
468
- }
469
- get routeInfo() {
470
- return this._routeInfo;
471
- }
472
- /**
473
- * 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
474
- * @param href 이동할 경로
475
- */
476
- async go(href) {
477
- const requestID = getRandomID();
478
- this._requestID = requestID;
479
- const routeInfo = parseURL(href, this._basepath);
480
- if (routeInfo.href === this._routeInfo?.href) return;
481
- try {
482
- if (this._requestID !== requestID) return;
483
- window.dispatchEvent(new RouteBeginEvent(routeInfo));
484
- const routes = this.getRoutes(routeInfo.pathname);
485
- const lastRoute = routes[routes.length - 1];
486
- if (lastRoute && "path" in lastRoute && lastRoute.path instanceof URLPattern) {
487
- routeInfo.params = lastRoute.path.exec({ pathname: routeInfo.pathname })?.pathname.groups || {};
488
- }
489
- this._routeInfo = routeInfo;
490
- window.route = routeInfo;
491
- if (this._requestID !== requestID) return;
492
- if (routes.length === 0) {
493
- throw new NotFoundRouteError(routeInfo.href);
494
- }
495
- let outlet = this.findOutletOrThrow(this._rootElement);
496
- let title = void 0;
497
- for (const route of routes) {
498
- if (this._requestID !== requestID) return;
499
- const content = route.render(routeInfo);
500
- const element = await outlet.renderContent({ id: route.id, content, force: route.force });
501
- outlet = this.findOutlet(element) || outlet;
502
- title = route.title || title;
503
- }
504
- document.title = title || document.title;
505
- if (this._requestID !== requestID) return;
506
- if (routeInfo.href !== window.location.href) {
507
- window.history.pushState({ basepath: routeInfo.basepath }, "", routeInfo.href);
508
- } else {
509
- window.history.replaceState({ basepath: routeInfo.basepath }, "", routeInfo.href);
510
- }
511
- window.dispatchEvent(new RouteDoneEvent(routeInfo));
512
- } catch (error) {
513
- const routeError = new RouteError(
514
- error.status || error.code || "UNKNOWN_ERROR",
515
- error.message || "An unexpected error occurred",
516
- error
517
- );
518
- window.dispatchEvent(new RouteErrorEvent(routeError, routeInfo));
519
- console.error("Routing error:", error);
520
- try {
521
- const errorEl = new ErrorPage();
522
- errorEl.error = routeError;
523
- document.body.innerHTML = "";
524
- document.body.appendChild(errorEl);
525
- } catch (pageError) {
526
- console.error("Failed to render error component:", pageError);
527
- console.error("Original error:", error);
528
- }
529
- }
530
- }
531
- /** 초기 라우팅 처리, TODO: 제거 */
532
- async initiate() {
533
- let outlet = await this.findOutlet(this._rootElement);
534
- while (!outlet && this._counter < 20) {
535
- await new Promise((resolve) => setTimeout(resolve, 50));
536
- this._counter++;
537
- outlet = await this.findOutlet(this._rootElement);
538
- }
539
- this._counter = 0;
540
- this.handlePopstate();
541
- }
542
- /** 라우트를 재설정합니다. */
543
- setRoutes(routes, basepath) {
544
- for (const route of routes) {
545
- route.id ||= getRandomID();
546
- if ("index" in route && route.index) {
547
- route.path = new URLPattern({ pathname: `${basepath}{/}?` });
548
- } else if ("path" in route && route.path) {
549
- if (typeof route.path === "string") {
550
- const absolutePathStr = absolutePath(basepath, route.path);
551
- route.path = new URLPattern({ pathname: `${absolutePathStr}{/}?` });
552
- }
553
- } else {
554
- throw new Error('Route must have either "index" or "path" property defined.');
555
- }
556
- if (route.children && route.children.length > 0) {
557
- let childBasepath;
558
- if ("index" in route) {
559
- childBasepath = basepath;
560
- } else {
561
- if (typeof route.path === "string") {
562
- childBasepath = absolutePath(basepath, route.path);
563
- } else {
564
- childBasepath = route.path.pathname.replace("{/}?", "");
565
- }
566
- }
567
- route.children = this.setRoutes(route.children, childBasepath);
568
- route.force ||= false;
569
- } else {
570
- route.force ||= true;
571
- }
572
- }
573
- return routes;
574
- }
575
- /** URLPattern을 사용하여 경로와 일치하는 라우트들을 자식 라우트까지 포함하여 반환합니다. */
576
- getRoutes(pathname, routes = this._routes) {
577
- for (const route of routes) {
578
- if (route.children) {
579
- const childRoutes = this.getRoutes(pathname, route.children);
580
- if (childRoutes.length > 0) {
581
- return [route, ...childRoutes];
582
- }
583
- }
584
- let matches = false;
585
- if ("index" in route && route.index && route.path) {
586
- matches = route.path.test({ pathname });
587
- } else if ("path" in route && route.path instanceof URLPattern) {
588
- matches = route.path.test({ pathname });
589
- }
590
- if (matches) {
591
- return [route];
592
- }
593
- }
594
- return [];
595
- }
596
- /** Outlet 엘리먼트를 찾아 반환합니다. */
597
- findOutlet(element) {
598
- let outlet = void 0;
599
- if (element.shadowRoot) {
600
- outlet = element.shadowRoot.querySelector("u-outlet");
601
- if (outlet) return outlet;
602
- for (const child of Array.from(element.shadowRoot.children)) {
603
- outlet = this.findOutlet(child);
604
- if (outlet) return outlet;
605
- }
606
- } else {
607
- outlet = element.querySelector("u-outlet");
608
- if (outlet) return outlet;
609
- for (const child of Array.from(element.children)) {
610
- outlet = this.findOutlet(child);
611
- if (outlet) return outlet;
612
- }
613
- }
614
- return void 0;
615
- }
616
- /** Outlet 엘리먼트를 찾아 반환합니다. 없으면 에러를 던집니다. */
617
- findOutletOrThrow(element) {
618
- const outlet = this.findOutlet(element);
619
- if (!outlet) {
620
- throw new Error("No Outlet component found in the root element.");
621
- }
622
- return outlet;
623
- }
624
- }
625
- var __defProp = Object.defineProperty;
626
- var __decorateClass = (decorators, target, key, kind) => {
627
- var result = void 0;
628
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
629
- if (decorator = decorators[i])
630
- result = decorator(target, key, result) || result;
631
- if (result) __defProp(target, key, result);
632
- return result;
633
- };
634
- const EXTERNAL_LINK_PATTERNS = [
635
- /^http/,
636
- /^\/\//,
637
- /^mailto:/,
638
- /^tel:/,
639
- /^javascript:/,
640
- /^ftp:/,
641
- /^data:/,
642
- /^ws:/,
643
- /^wss:/
644
- ];
645
- const _Link = class _Link extends lit.LitElement {
646
- constructor() {
647
- super(...arguments);
648
- this.isExternal = false;
649
- this.anchorHref = "#";
650
- this.handleMouseDown = (event) => {
651
- const isNonNavigationClick = event.button === 2 || event.metaKey || event.shiftKey || event.altKey;
652
- if (event.defaultPrevented || isNonNavigationClick) return;
653
- event.preventDefault();
654
- event.stopPropagation();
655
- const basepath = window.history.state?.basepath || "";
656
- if (event.button === 1 || event.ctrlKey) {
657
- window.open(this.anchorHref, "_blank");
658
- } else if (!this.href) {
659
- this.dispatchPopstate(basepath, basepath);
660
- } else if (this.isExternal || this.href.startsWith("/") && !this.href.startsWith(basepath)) {
661
- window.location.href = this.href;
662
- } else if (this.href.startsWith("#")) {
663
- const url = window.location.pathname + window.location.search + this.href;
664
- this.dispatchHashchange(basepath, url);
665
- } else if (this.href.startsWith("?")) {
666
- const url = window.location.pathname + this.href;
667
- this.dispatchPopstate(basepath, url);
668
- } else {
669
- const url = absolutePath(basepath, this.href);
670
- this.dispatchPopstate(basepath, url);
671
- }
672
- };
673
- this.preventClickEvent = (event) => {
674
- event.preventDefault();
675
- event.stopPropagation();
676
- };
677
- }
678
- connectedCallback() {
679
- super.connectedCallback();
680
- this.addEventListener("mousedown", this.handleMouseDown);
681
- }
682
- disconnectedCallback() {
683
- this.removeEventListener("mousedown", this.handleMouseDown);
684
- super.disconnectedCallback();
685
- }
686
- async updated(changedProperties) {
687
- super.updated(changedProperties);
688
- await this.updateComplete;
689
- if (changedProperties.has("href")) {
690
- this.isExternal = this.checkExternalLink(this.href || "");
691
- this.anchorHref = this.getAnchorHref(this.href);
692
- }
693
- }
694
- render() {
695
- return lit.html`
696
- <a href=${this.anchorHref} @click=${this.preventClickEvent}>
697
- <slot></slot>
698
- </a>
699
- `;
700
- }
701
- /** 클라이언트 라우팅을 위해 popstate 이벤트를 발생시킵니다. */
702
- dispatchPopstate(basepath, url) {
703
- window.history.pushState({ basepath }, "", url);
704
- window.dispatchEvent(new PopStateEvent("popstate"));
705
- }
706
- /** 클라이언트 라우팅을 위해 hashchange 이벤트를 발생시킵니다. */
707
- dispatchHashchange(basepath, url) {
708
- window.history.pushState({ basepath }, "", url);
709
- window.dispatchEvent(new HashChangeEvent("hashchange"));
710
- }
711
- /** 외부 링크인지 확인합니다. */
712
- checkExternalLink(href) {
713
- return EXTERNAL_LINK_PATTERNS.some((pattern) => pattern.test(href));
714
- }
715
- /** a 태그의 href 값을 계산합니다. */
716
- getAnchorHref(href) {
717
- const basepath = window.history.state?.basepath || "";
718
- if (!href) {
719
- return window.location.origin + basepath;
720
- }
721
- if (this.isExternal || href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) {
722
- return href;
723
- }
724
- return absolutePath(basepath, href);
725
- }
726
- };
727
- _Link.styles = lit.css`
728
- :host {
729
- display: inline-flex;
730
- cursor: pointer;
731
- }
732
-
733
- a {
734
- display: contents;
735
- text-decoration: none;
736
- color: inherit;
737
- }
738
- `;
739
- let Link = _Link;
740
- __decorateClass([
741
- decorators_js.state()
742
- ], Link.prototype, "anchorHref");
743
- __decorateClass([
744
- decorators_js.property({ type: String })
745
- ], Link.prototype, "href");
746
- class Outlet extends lit.LitElement {
747
- /** 쉐도우를 사용하지 않고, 직접 렌더링합니다. */
748
- createRenderRoot() {
749
- return this;
750
- }
751
- render() {
752
- return lit.html`${this.container}`;
753
- }
754
- /**
755
- * render 함수의 결과를 렌더링합니다.
756
- * - HTMLElement, ReactElement, TemplateResult를 모두 처리할 수 있습니다.
757
- */
758
- async renderContent({ id, content, force }) {
759
- if (this.routeId === id && force === false && this.container) {
760
- return this.container;
761
- }
762
- this.routeId = id;
763
- this.clear();
764
- if (!this.container) {
765
- throw new Error("DOM이 초기화되지 않았습니다.");
766
- }
767
- if (content instanceof HTMLElement) {
768
- this.container.appendChild(content);
769
- } else if ("_$litType$" in content) {
770
- this.content = lit.render(content, this.container);
771
- } else if ("$$typeof" in content) {
772
- this.content = client.createRoot(this.container);
773
- this.content.render(content);
774
- } else {
775
- throw new Error("not supported content type for Outlet rendering.");
776
- }
777
- this.requestUpdate();
778
- await this.updateComplete;
779
- return this.container;
780
- }
781
- /**
782
- * 기존 DOM을 라이프 사이클에 맞게 제거합니다.
783
- */
784
- clear() {
785
- if (this.content) {
786
- if ("unmount" in this.content) {
787
- this.content.unmount();
788
- }
789
- if ("setConnected" in this.content) {
790
- this.content.setConnected(false);
791
- }
792
- this.content = void 0;
793
- }
794
- this.container = document.createElement("div");
795
- this.container.style.display = "contents";
796
- }
797
- }
798
- customElements.define("u-link", Link);
799
- customElements.define("u-outlet", Outlet);
800
- const ULink = react.createComponent({
801
- react: React,
802
- tagName: "u-link",
803
- elementClass: Link
804
- });
805
- const UOutlet = react.createComponent({
806
- react: React,
807
- tagName: "u-outlet",
808
- elementClass: Outlet
809
- });
810
- exports.Link = Link;
811
- exports.NotFoundRouteError = NotFoundRouteError;
812
- exports.Outlet = Outlet;
813
- exports.RouteBeginEvent = RouteBeginEvent;
814
- exports.RouteDoneEvent = RouteDoneEvent;
815
- exports.RouteError = RouteError;
816
- exports.RouteErrorEvent = RouteErrorEvent;
817
- exports.Router = Router;
818
- exports.ULink = ULink;
819
- exports.UOutlet = UOutlet;