@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 +367 -304
- package/dist/main.d.ts +36 -16
- package/dist/{main.es.js → main.js} +368 -305
- package/package.json +10 -9
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
47
|
+
function parseUrl(url, basepath) {
|
|
14
48
|
let urlObj;
|
|
15
|
-
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
576
|
+
var __defProp = Object.defineProperty;
|
|
371
577
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
372
|
-
var __decorateClass
|
|
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
|
|
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
|
|
649
|
+
__decorateClass([
|
|
444
650
|
decorators_js.property({ type: Object })
|
|
445
651
|
], ErrorPage.prototype, "error", 2);
|
|
446
|
-
ErrorPage = __decorateClass
|
|
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.
|
|
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 =
|
|
459
|
-
|
|
460
|
-
window.
|
|
461
|
-
this.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|