@iyulab/router 0.3.0 → 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.
package/dist/main.cjs.js CHANGED
@@ -1,18 +1,52 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const React = require("react");
3
4
  const lit = require("lit");
4
5
  const decorators_js = require("lit/decorators.js");
5
- const react = require("@lit/react");
6
- const React = require("react");
7
6
  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("/");
7
+ const e = /* @__PURE__ */ new Set(["children", "localName", "ref", "style", "className"]), n = /* @__PURE__ */ new WeakMap(), t = (e2, t2, o2, l, a) => {
8
+ const s = a?.[t2];
9
+ void 0 === s ? (e2[t2] = o2, null == o2 && t2 in HTMLElement.prototype && e2.removeAttribute(t2)) : o2 !== l && ((e3, t3, o3) => {
10
+ let l2 = n.get(e3);
11
+ void 0 === l2 && n.set(e3, l2 = /* @__PURE__ */ new Map());
12
+ let a2 = l2.get(t3);
13
+ void 0 !== o3 ? void 0 === a2 ? (l2.set(t3, a2 = { handleEvent: o3 }), e3.addEventListener(t3, a2)) : a2.handleEvent = o3 : void 0 !== a2 && (l2.delete(t3), e3.removeEventListener(t3, a2));
14
+ })(e2, s, o2);
15
+ }, o = ({ react: n2, tagName: o2, elementClass: l, events: a, displayName: s }) => {
16
+ const c = new Set(Object.keys(a ?? {})), r = n2.forwardRef(((s2, r2) => {
17
+ const i = n2.useRef(/* @__PURE__ */ new Map()), d = n2.useRef(null), f = {}, u = {};
18
+ for (const [n3, t2] of Object.entries(s2)) e.has(n3) ? f["className" === n3 ? "class" : n3] = t2 : c.has(n3) || n3 in l.prototype ? u[n3] = t2 : f[n3] = t2;
19
+ return n2.useLayoutEffect((() => {
20
+ if (null === d.current) return;
21
+ const e2 = /* @__PURE__ */ new Map();
22
+ for (const n3 in u) t(d.current, n3, s2[n3], i.current.get(n3), a), i.current.delete(n3), e2.set(n3, s2[n3]);
23
+ for (const [e3, n3] of i.current) t(d.current, e3, void 0, n3, a);
24
+ i.current = e2;
25
+ })), n2.useLayoutEffect((() => {
26
+ d.current?.removeAttribute("defer-hydration");
27
+ }), []), f.suppressHydrationWarning = true, n2.createElement(o2, { ...f, ref: n2.useCallback(((e2) => {
28
+ d.current = e2, "function" == typeof r2 ? r2(e2) : null !== r2 && (r2.current = e2);
29
+ }), [r2]) });
30
+ }));
31
+ return r.displayName = s ?? l.name, r;
32
+ };
33
+ function isExternalUrl(url) {
34
+ if (!url) return false;
35
+ const s = url.trim();
36
+ if (/^(?:mailto:|tel:|javascript:)/i.test(s)) return true;
37
+ if (s.startsWith("//")) return true;
38
+ try {
39
+ const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
40
+ const parsed = new URL(s, base);
41
+ if (/^(?:ftp:|ftps:|data:|ws:|wss:)/i.test(parsed.protocol)) return true;
42
+ return parsed.origin !== new URL(base).origin;
43
+ } catch {
44
+ return false;
45
+ }
12
46
  }
13
- function parseURL(url, basepath) {
47
+ function parseUrl(url, basepath) {
14
48
  let urlObj;
15
- basepath = catchBasepath(basepath);
49
+ basepath = catchBasePath(basepath);
16
50
  if (url.startsWith("http")) {
17
51
  urlObj = new URL(url);
18
52
  } else if (url.startsWith("/")) {
@@ -35,7 +69,12 @@ function parseURL(url, basepath) {
35
69
  params: {}
36
70
  };
37
71
  }
38
- function catchBasepath(basepath) {
72
+ function absolutePath(...paths) {
73
+ paths = paths.map((p) => p.replace(/^\/|\/$/g, "")).filter((p) => p.length > 0);
74
+ if (paths.length === 0) return "/";
75
+ return "/" + paths.join("/");
76
+ }
77
+ function catchBasePath(basepath) {
39
78
  if (basepath === "/") return basepath;
40
79
  let pattern = new URLPattern({ pathname: basepath + "/*" });
41
80
  let match = pattern.exec({ pathname: window.location.pathname });
@@ -51,9 +90,176 @@ function catchBasepath(basepath) {
51
90
  }
52
91
  return basepath;
53
92
  }
54
- function getRandomID() {
55
- return window.isSecureContext ? window.crypto.randomUUID() : window.crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
93
+ var __defProp$1 = Object.defineProperty;
94
+ var __decorateClass$1 = (decorators, target, key, kind) => {
95
+ var result = void 0;
96
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
97
+ if (decorator = decorators[i])
98
+ result = decorator(target, key, result) || result;
99
+ if (result) __defProp$1(target, key, result);
100
+ return result;
101
+ };
102
+ const _Link = class _Link extends lit.LitElement {
103
+ constructor() {
104
+ super(...arguments);
105
+ this.isExternal = false;
106
+ this.anchorHref = "#";
107
+ this.handleMouseDown = (event) => {
108
+ const isNonNavigationClick = event.button === 2 || event.metaKey || event.shiftKey || event.altKey;
109
+ if (event.defaultPrevented || isNonNavigationClick) return;
110
+ event.preventDefault();
111
+ event.stopPropagation();
112
+ const basepath = window.history.state?.basepath || "";
113
+ if (event.button === 1 || event.ctrlKey) {
114
+ window.open(this.anchorHref, "_blank");
115
+ } else if (!this.href) {
116
+ this.dispatchPopstate(basepath, basepath);
117
+ } else if (this.isExternal || this.href.startsWith("/") && !this.href.startsWith(basepath)) {
118
+ window.location.href = this.href;
119
+ } else if (this.href.startsWith("#")) {
120
+ const url = window.location.pathname + window.location.search + this.href;
121
+ this.dispatchHashchange(basepath, url);
122
+ } else if (this.href.startsWith("?")) {
123
+ const url = window.location.pathname + this.href;
124
+ this.dispatchPopstate(basepath, url);
125
+ } else {
126
+ const url = absolutePath(basepath, this.href);
127
+ this.dispatchPopstate(basepath, url);
128
+ }
129
+ };
130
+ this.preventClickEvent = (event) => {
131
+ event.preventDefault();
132
+ event.stopPropagation();
133
+ };
134
+ }
135
+ connectedCallback() {
136
+ super.connectedCallback();
137
+ this.addEventListener("mousedown", this.handleMouseDown);
138
+ }
139
+ disconnectedCallback() {
140
+ this.removeEventListener("mousedown", this.handleMouseDown);
141
+ super.disconnectedCallback();
142
+ }
143
+ async updated(changedProperties) {
144
+ super.updated(changedProperties);
145
+ await this.updateComplete;
146
+ if (changedProperties.has("href")) {
147
+ this.isExternal = isExternalUrl(this.href || "");
148
+ this.anchorHref = this.getAnchorHref(this.href);
149
+ }
150
+ }
151
+ render() {
152
+ return lit.html`
153
+ <a href=${this.anchorHref} @click=${this.preventClickEvent}>
154
+ <slot></slot>
155
+ </a>
156
+ `;
157
+ }
158
+ /** 클라이언트 라우팅을 위해 popstate 이벤트를 발생시킵니다. */
159
+ dispatchPopstate(basepath, url) {
160
+ window.history.pushState({ basepath }, "", url);
161
+ window.dispatchEvent(new PopStateEvent("popstate"));
162
+ }
163
+ /** 클라이언트 라우팅을 위해 hashchange 이벤트를 발생시킵니다. */
164
+ dispatchHashchange(basepath, url) {
165
+ window.history.pushState({ basepath }, "", url);
166
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
167
+ }
168
+ /** a 태그에 주입할 href 값을 계산합니다. */
169
+ getAnchorHref(href) {
170
+ const basepath = window.history.state?.basepath || "";
171
+ if (!href) {
172
+ return window.location.origin + basepath;
173
+ }
174
+ if (this.isExternal || href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) {
175
+ return href;
176
+ }
177
+ return absolutePath(basepath, href);
178
+ }
179
+ };
180
+ _Link.styles = lit.css`
181
+ :host {
182
+ display: inline-flex;
183
+ cursor: pointer;
184
+ }
185
+
186
+ a {
187
+ display: contents;
188
+ text-decoration: none;
189
+ color: inherit;
190
+ }
191
+ `;
192
+ let Link = _Link;
193
+ __decorateClass$1([
194
+ decorators_js.state()
195
+ ], Link.prototype, "anchorHref");
196
+ __decorateClass$1([
197
+ decorators_js.property({ type: String })
198
+ ], Link.prototype, "href");
199
+ class Outlet extends lit.LitElement {
200
+ /** 쉐도우를 사용하지 않고, 직접 렌더링합니다. */
201
+ createRenderRoot() {
202
+ return this;
203
+ }
204
+ render() {
205
+ return lit.html`${this.container}`;
206
+ }
207
+ /**
208
+ * render 함수의 결과를 렌더링합니다.
209
+ * - HTMLElement, ReactElement, TemplateResult를 모두 처리할 수 있습니다.
210
+ */
211
+ async renderContent({ id, content, force }) {
212
+ if (this.routeId === id && force === false && this.container) {
213
+ return this.container;
214
+ }
215
+ this.routeId = id;
216
+ this.clear();
217
+ if (!this.container) {
218
+ throw new Error("DOM이 초기화되지 않았습니다.");
219
+ }
220
+ if (content instanceof HTMLElement) {
221
+ this.container.appendChild(content);
222
+ } else if ("_$litType$" in content) {
223
+ this.content = lit.render(content, this.container);
224
+ } else if ("$$typeof" in content) {
225
+ this.content = client.createRoot(this.container);
226
+ this.content.render(content);
227
+ } else {
228
+ throw new Error("not supported content type for Outlet rendering.");
229
+ }
230
+ this.requestUpdate();
231
+ await this.updateComplete;
232
+ return this.container;
233
+ }
234
+ /**
235
+ * 기존 DOM을 라이프 사이클에 맞게 제거합니다.
236
+ */
237
+ clear() {
238
+ if (this.content) {
239
+ if ("unmount" in this.content) {
240
+ this.content.unmount();
241
+ }
242
+ if ("setConnected" in this.content) {
243
+ this.content.setConnected(false);
244
+ }
245
+ this.content = void 0;
246
+ }
247
+ this.container = document.createElement("div");
248
+ this.container.style.display = "contents";
249
+ }
56
250
  }
251
+ customElements.define("u-link", Link);
252
+ customElements.define("u-outlet", Outlet);
253
+ const ULink = o({
254
+ react: React,
255
+ tagName: "u-link",
256
+ elementClass: Link
257
+ });
258
+ const UOutlet = o({
259
+ react: React,
260
+ tagName: "u-outlet",
261
+ elementClass: Outlet
262
+ });
57
263
  class RouteError extends Error {
58
264
  constructor(code, message, original) {
59
265
  super(message);
@@ -367,14 +573,14 @@ const styles = lit.css`
367
573
  }
368
574
  }
369
575
  `;
370
- var __defProp$1 = Object.defineProperty;
576
+ var __defProp = Object.defineProperty;
371
577
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
372
- var __decorateClass$1 = (decorators, target, key, kind) => {
578
+ var __decorateClass = (decorators, target, key, kind) => {
373
579
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
374
580
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
375
581
  if (decorator = decorators[i])
376
582
  result = (kind ? decorator(target, key, result) : decorator(result)) || result;
377
- if (kind && result) __defProp$1(target, key, result);
583
+ if (kind && result) __defProp(target, key, result);
378
584
  return result;
379
585
  };
380
586
  let ErrorPage = class extends lit.LitElement {
@@ -440,32 +646,167 @@ let ErrorPage = class extends lit.LitElement {
440
646
  }
441
647
  };
442
648
  ErrorPage.styles = styles;
443
- __decorateClass$1([
649
+ __decorateClass([
444
650
  decorators_js.property({ type: Object })
445
651
  ], ErrorPage.prototype, "error", 2);
446
- ErrorPage = __decorateClass$1([
652
+ ErrorPage = __decorateClass([
447
653
  decorators_js.customElement("u-error-page")
448
654
  ], ErrorPage);
655
+ function getRandomID() {
656
+ return window.isSecureContext ? window.crypto.randomUUID() : window.crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
657
+ }
658
+ function findOutlet(element) {
659
+ let outlet = void 0;
660
+ if (element.shadowRoot) {
661
+ outlet = element.shadowRoot.querySelector("u-outlet");
662
+ if (outlet) return outlet;
663
+ for (const child of Array.from(element.shadowRoot.children)) {
664
+ outlet = findOutlet(child);
665
+ if (outlet) return outlet;
666
+ }
667
+ } else {
668
+ outlet = element.querySelector("u-outlet");
669
+ if (outlet) return outlet;
670
+ for (const child of Array.from(element.children)) {
671
+ outlet = findOutlet(child);
672
+ if (outlet) return outlet;
673
+ }
674
+ }
675
+ return void 0;
676
+ }
677
+ function findOutletOrThrow(element) {
678
+ const outlet = findOutlet(element);
679
+ if (!outlet) {
680
+ throw new Error("No Outlet component found in the root element.");
681
+ }
682
+ return outlet;
683
+ }
684
+ function findAnchorFromEvent(e2) {
685
+ const targets = e2.composedPath() || [];
686
+ if (targets && targets.length) {
687
+ for (const node of targets) {
688
+ if (!(node instanceof Element)) continue;
689
+ if (node.tagName === "A") return node;
690
+ }
691
+ }
692
+ const tgt = e2.target;
693
+ if (!tgt) return null;
694
+ const anchor = tgt.closest("a");
695
+ return anchor;
696
+ }
697
+ function setRoutes(routes, basepath) {
698
+ for (const route of routes) {
699
+ route.id ||= getRandomID();
700
+ if ("index" in route && route.index) {
701
+ route.path = new URLPattern({ pathname: `${basepath}{/}?` });
702
+ } else if ("path" in route && route.path) {
703
+ if (typeof route.path === "string") {
704
+ const absolutePathStr = absolutePath(basepath, route.path);
705
+ route.path = new URLPattern({ pathname: `${absolutePathStr}{/}?` });
706
+ }
707
+ } else {
708
+ throw new Error('Route must have either "index" or "path" property defined.');
709
+ }
710
+ if (route.children && route.children.length > 0) {
711
+ let childBasepath;
712
+ if ("index" in route) {
713
+ childBasepath = basepath;
714
+ } else {
715
+ if (typeof route.path === "string") {
716
+ childBasepath = absolutePath(basepath, route.path);
717
+ } else {
718
+ childBasepath = route.path.pathname.replace("{/}?", "");
719
+ }
720
+ }
721
+ route.children = setRoutes(route.children, childBasepath);
722
+ route.force ||= false;
723
+ } else {
724
+ route.force ||= true;
725
+ }
726
+ }
727
+ return routes;
728
+ }
729
+ function getRoutes(pathname, routes) {
730
+ for (const route of routes) {
731
+ if (route.children) {
732
+ const childRoutes = getRoutes(pathname, route.children);
733
+ if (childRoutes.length > 0) {
734
+ return [route, ...childRoutes];
735
+ }
736
+ }
737
+ let matches = false;
738
+ if ("index" in route && route.index && route.path) {
739
+ matches = route.path.test({ pathname });
740
+ } else if ("path" in route && route.path instanceof URLPattern) {
741
+ matches = route.path.test({ pathname });
742
+ }
743
+ if (matches) {
744
+ return [route];
745
+ }
746
+ }
747
+ return [];
748
+ }
449
749
  class Router {
450
750
  constructor(config) {
451
- this._counter = 0;
452
- this.handlePopstate = async () => {
751
+ this.handleWindowPopstate = async () => {
453
752
  const href = window.location.href;
454
753
  await this.go(href);
455
754
  };
755
+ this.handleRootClick = (e2) => {
756
+ try {
757
+ if (e2.defaultPrevented) return;
758
+ if (e2.button !== 0 || e2.metaKey || e2.ctrlKey || e2.shiftKey) return;
759
+ const anchor = findAnchorFromEvent(e2);
760
+ if (!anchor) return;
761
+ const href = anchor.getAttribute("href") || anchor.href;
762
+ if (!href) return;
763
+ if (isExternalUrl(href)) return;
764
+ if (anchor.hasAttribute("download")) return;
765
+ if (anchor.getAttribute("rel") === "external") return;
766
+ if (anchor.target && anchor.target !== "") return;
767
+ e2.preventDefault();
768
+ void this.go(anchor.href);
769
+ } catch {
770
+ }
771
+ };
456
772
  this._rootElement = config.root;
457
773
  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();
774
+ this._routes = setRoutes(config.routes, this._basepath);
775
+ this.waitConnected();
776
+ window.removeEventListener("popstate", this.handleWindowPopstate);
777
+ window.addEventListener("popstate", this.handleWindowPopstate);
778
+ if (config.useIntercept !== false) {
779
+ this._rootElement.removeEventListener("click", this.handleRootClick);
780
+ this._rootElement.addEventListener("click", this.handleRootClick);
781
+ }
462
782
  }
783
+ /** 초기 라우팅 처리, TODO: 제거 */
784
+ async waitConnected() {
785
+ let outlet = findOutlet(this._rootElement);
786
+ let count = 0;
787
+ while (!outlet && count < 20) {
788
+ await new Promise((resolve) => setTimeout(resolve, 50));
789
+ outlet = findOutlet(this._rootElement);
790
+ count++;
791
+ }
792
+ this.handleWindowPopstate();
793
+ }
794
+ /** 객체를 정리하고 이벤트 리스너를 제거합니다. */
795
+ destroy() {
796
+ window.removeEventListener("popstate", this.handleWindowPopstate);
797
+ this._rootElement.removeEventListener("click", this.handleRootClick);
798
+ this._routeInfo = void 0;
799
+ this._requestID = void 0;
800
+ }
801
+ /** 라우터의 기본 경로 반환 */
463
802
  get basepath() {
464
803
  return this._basepath;
465
804
  }
805
+ /** 등록된 라우트 반환 */
466
806
  get routes() {
467
807
  return this._routes;
468
808
  }
809
+ /** 현재 라우팅 정보 반환 */
469
810
  get routeInfo() {
470
811
  return this._routeInfo;
471
812
  }
@@ -476,12 +817,12 @@ class Router {
476
817
  async go(href) {
477
818
  const requestID = getRandomID();
478
819
  this._requestID = requestID;
479
- const routeInfo = parseURL(href, this._basepath);
820
+ const routeInfo = parseUrl(href, this._basepath);
480
821
  if (routeInfo.href === this._routeInfo?.href) return;
481
822
  try {
482
823
  if (this._requestID !== requestID) return;
483
824
  window.dispatchEvent(new RouteBeginEvent(routeInfo));
484
- const routes = this.getRoutes(routeInfo.pathname);
825
+ const routes = getRoutes(routeInfo.pathname, this._routes);
485
826
  const lastRoute = routes[routes.length - 1];
486
827
  if (lastRoute && "path" in lastRoute && lastRoute.path instanceof URLPattern) {
487
828
  routeInfo.params = lastRoute.path.exec({ pathname: routeInfo.pathname })?.pathname.groups || {};
@@ -492,13 +833,13 @@ class Router {
492
833
  if (routes.length === 0) {
493
834
  throw new NotFoundRouteError(routeInfo.href);
494
835
  }
495
- let outlet = this.findOutletOrThrow(this._rootElement);
836
+ let outlet = findOutletOrThrow(this._rootElement);
496
837
  let title = void 0;
497
838
  for (const route of routes) {
498
839
  if (this._requestID !== requestID) return;
499
840
  const content = route.render(routeInfo);
500
841
  const element = await outlet.renderContent({ id: route.id, content, force: route.force });
501
- outlet = this.findOutlet(element) || outlet;
842
+ outlet = findOutlet(element) || outlet;
502
843
  title = route.title || title;
503
844
  }
504
845
  document.title = title || document.title;
@@ -528,285 +869,7 @@ class Router {
528
869
  }
529
870
  }
530
871
  }
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
872
  }
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
873
  exports.Link = Link;
811
874
  exports.NotFoundRouteError = NotFoundRouteError;
812
875
  exports.Outlet = Outlet;