@zyrab/domo 1.1.1 → 1.2.1
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/package.json +39 -36
- package/src/base.js +80 -0
- package/src/builder.js +43 -0
- package/src/children.js +188 -0
- package/src/classes.js +62 -0
- package/src/domo.js +77 -0
- package/src/events.js +166 -0
- package/src/properties.js +162 -0
- package/README.md +0 -144
- package/index.js +0 -35
- package/src/core/Domo.js +0 -373
- package/src/core/DomoSVG.js +0 -65
- package/src/core/Router.js +0 -206
package/src/core/Domo.js
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domo - Lightweight helper class for creating and manipulating DOM elements fluently.
|
|
3
|
-
* @typedef {import('./Domo').default} Domo
|
|
4
|
-
*/
|
|
5
|
-
class Domo {
|
|
6
|
-
/**
|
|
7
|
-
* @param {string} [el='div'] - The tag name of the element to create.
|
|
8
|
-
*/
|
|
9
|
-
constructor(el = "div") {
|
|
10
|
-
this.element = this.el(el);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a DOM element.
|
|
15
|
-
* @param {string} el - Element tag name.
|
|
16
|
-
* @returns {HTMLElement}
|
|
17
|
-
*/
|
|
18
|
-
el(el) {
|
|
19
|
-
return document.createElement(String(el || "div").toLowerCase());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Provides direct reference to the created element.
|
|
24
|
-
* @param {(el: HTMLElement) => void} callBack
|
|
25
|
-
* @returns {Domo}
|
|
26
|
-
*/
|
|
27
|
-
ref(callBack) {
|
|
28
|
-
if (typeof callBack === "function") callBack(this.element);
|
|
29
|
-
return this;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Sets a property on the element if the value is not undefined.
|
|
34
|
-
* @private
|
|
35
|
-
* @param {string} key
|
|
36
|
-
* @param {*} val
|
|
37
|
-
* @returns {Domo}
|
|
38
|
-
*/
|
|
39
|
-
_set(key, val) {
|
|
40
|
-
if (val !== undefined) this.element[key] = val;
|
|
41
|
-
return this;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** @param {string} id */
|
|
45
|
-
id(id) {
|
|
46
|
-
return this._set("id", id);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** @param {string} value */
|
|
50
|
-
val(value) {
|
|
51
|
-
return this._set("value", value);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** @param {string} text */
|
|
55
|
-
txt(text) {
|
|
56
|
-
return this._set("textContent", text);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Normalizes a class list input.
|
|
61
|
-
* @private
|
|
62
|
-
* @param {string|string[]} input
|
|
63
|
-
* @returns {string[]}
|
|
64
|
-
*/
|
|
65
|
-
_parseClassList(input) {
|
|
66
|
-
return Array.isArray(input) ? input.filter(Boolean) : String(input).split(" ").filter(Boolean);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Adds classes
|
|
71
|
-
* accepted types:
|
|
72
|
-
* string
|
|
73
|
-
* string[]
|
|
74
|
-
* @param {string|string[]} classes
|
|
75
|
-
*/
|
|
76
|
-
cls(classes) {
|
|
77
|
-
if (classes) {
|
|
78
|
-
this.element.classList.add(...this._parseClassList(classes));
|
|
79
|
-
}
|
|
80
|
-
return this;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Removes classes
|
|
85
|
-
* accepted types:
|
|
86
|
-
* string
|
|
87
|
-
* string[]
|
|
88
|
-
* @param {string|string[]} classes
|
|
89
|
-
*/
|
|
90
|
-
rmvCls(classes) {
|
|
91
|
-
if (classes) {
|
|
92
|
-
this.element.classList.remove(...this._parseClassList(classes));
|
|
93
|
-
}
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Toggles a class.
|
|
99
|
-
* @param {string} className
|
|
100
|
-
* @param {boolean} [force]
|
|
101
|
-
*/
|
|
102
|
-
tgglCls(className, force) {
|
|
103
|
-
this.element.classList.toggle(className, force);
|
|
104
|
-
return this;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Sets attributes (skips event attributes).
|
|
109
|
-
* @param {Record<string, any>} attributes
|
|
110
|
-
*/
|
|
111
|
-
attr(attributes = {}) {
|
|
112
|
-
Object.entries(attributes).forEach(([key, value]) => {
|
|
113
|
-
if (key.startsWith("on")) return;
|
|
114
|
-
if (typeof value === "boolean") {
|
|
115
|
-
if (value) this.element.setAttribute(key, "");
|
|
116
|
-
else this.element.removeAttribute(key);
|
|
117
|
-
} else if (value != null) {
|
|
118
|
-
this.element.setAttribute(key, value);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
return this;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Toggles an attribute.
|
|
126
|
-
* @param {string} attrName
|
|
127
|
-
* @param {boolean} [force]
|
|
128
|
-
*/
|
|
129
|
-
tgglAttr(attrName, force) {
|
|
130
|
-
if (attrName.startsWith("on")) return;
|
|
131
|
-
if (typeof force === "boolean") {
|
|
132
|
-
if (force) {
|
|
133
|
-
this.element.setAttribute(attrName, "");
|
|
134
|
-
} else {
|
|
135
|
-
this.element.removeAttribute(attrName);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
if (this.element.hasAttribute(attrName)) {
|
|
139
|
-
this.element.removeAttribute(attrName);
|
|
140
|
-
} else {
|
|
141
|
-
this.element.setAttribute(attrName, "");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return this;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Sets data-* attributes.
|
|
149
|
-
* @param {Record<string, string>} data
|
|
150
|
-
*/
|
|
151
|
-
data(data = {}) {
|
|
152
|
-
Object.entries(data).forEach(([key, val]) => {
|
|
153
|
-
this.element.dataset[key] = val;
|
|
154
|
-
});
|
|
155
|
-
return this;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Sets CSS styles.
|
|
160
|
-
* @param {Partial<CSSStyleDeclaration>} styles
|
|
161
|
-
*/
|
|
162
|
-
css(styles = {}) {
|
|
163
|
-
Object.assign(this.element.style, styles);
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Adds one or multiple event listeners to the element.
|
|
169
|
-
*
|
|
170
|
-
* Supports both:
|
|
171
|
-
* - A single event: `on('click', handler, options)`
|
|
172
|
-
* - Multiple events via object map: `on({ click: handler, mouseenter: [handler, options] })`
|
|
173
|
-
*
|
|
174
|
-
* @param {string | Record<string, Function | [Function, AddEventListenerOptions]>} eventMapOrName
|
|
175
|
-
* Either an event name string, or an object mapping event names to handlers (or [handler, options] tuples).
|
|
176
|
-
* @param {EventListenerOrEventListenerObject} [callback]
|
|
177
|
-
* Callback to execute when the event is triggered (used when first param is a string).
|
|
178
|
-
* @param {AddEventListenerOptions} [options={}]
|
|
179
|
-
* Options for `addEventListener` (used when first param is a string).
|
|
180
|
-
* @returns {this} The current instance for chaining.
|
|
181
|
-
*/
|
|
182
|
-
|
|
183
|
-
on(eventMapOrName, callback, options = {}) {
|
|
184
|
-
if (typeof eventMapOrName === "object" && eventMapOrName !== null) {
|
|
185
|
-
for (const [event, value] of Object.entries(eventMapOrName)) {
|
|
186
|
-
if (typeof value === "function") {
|
|
187
|
-
this.element.addEventListener(event, value);
|
|
188
|
-
} else if (Array.isArray(value)) {
|
|
189
|
-
const [cb, opts] = value;
|
|
190
|
-
this.element.addEventListener(event, cb, opts);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
this.element.addEventListener(eventMapOrName, callback, options);
|
|
195
|
-
}
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** @private */
|
|
200
|
-
_handleClosest(e, map) {
|
|
201
|
-
for (const [selector, handler] of Object.entries(map)) {
|
|
202
|
-
const match = e.target.closest(selector);
|
|
203
|
-
if (match) handler(e, match);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Delegates events to closest matching ancestor.
|
|
209
|
-
* accepts a map of selectors to event handlers
|
|
210
|
-
* @param {string} event
|
|
211
|
-
* @param {Record<string, (e: Event, target: Element) => void>} selectors
|
|
212
|
-
* @param {AddEventListenerOptions} [options]
|
|
213
|
-
*/
|
|
214
|
-
onClosest(event, selectors = {}, options = {}) {
|
|
215
|
-
return this.on(event, (e) => this._handleClosest(e, selectors), options);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** @private */
|
|
219
|
-
_handleMatches(e, map) {
|
|
220
|
-
for (const [selector, handler] of Object.entries(map)) {
|
|
221
|
-
if (e.target.matches(selector)) handler(e, e.target);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Delegates events using element.matches.
|
|
227
|
-
* accepts a map of selectors to event handlers
|
|
228
|
-
* @param {string} event
|
|
229
|
-
* @param {Record<string, (e: Event, target: Element) => void>} selectors
|
|
230
|
-
* @param {AddEventListenerOptions} [options]
|
|
231
|
-
*/
|
|
232
|
-
onMatch(event, selectors = {}, options = {}) {
|
|
233
|
-
return this.on(event, (e) => this._handleMatches(e, selectors), options);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** @private */
|
|
237
|
-
_handleElementInstance(element) {
|
|
238
|
-
if (element instanceof Domo) return element.build();
|
|
239
|
-
if (element instanceof DocumentFragment) return element;
|
|
240
|
-
if (element instanceof Node) return element;
|
|
241
|
-
if (typeof element === "string" || typeof element === "number") {
|
|
242
|
-
return document.createTextNode(element);
|
|
243
|
-
}
|
|
244
|
-
return document.createTextNode(`⚠ Invalid child: ${String(element)}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Appends children to the element.
|
|
249
|
-
* Accepts:
|
|
250
|
-
* - DOM nodes
|
|
251
|
-
* - Domo instances
|
|
252
|
-
* - DocumentFragments
|
|
253
|
-
* - Primitive strings/numbers (as text nodes)
|
|
254
|
-
* - Nested arrays of above
|
|
255
|
-
* @param {(Node|string|number|Domo|DocumentFragment|Array<any>)[]} children
|
|
256
|
-
* @returns {Domo}
|
|
257
|
-
*/
|
|
258
|
-
child(children = []) {
|
|
259
|
-
const flattenedChildren = children.flat();
|
|
260
|
-
flattenedChildren.forEach((child) => {
|
|
261
|
-
this.element.appendChild(this._handleElementInstance(child));
|
|
262
|
-
});
|
|
263
|
-
return this;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Appends children to the element.
|
|
267
|
-
* Alias for `child`.
|
|
268
|
-
* Accepts:
|
|
269
|
-
* - DOM nodes
|
|
270
|
-
* - Domo instances
|
|
271
|
-
* - DocumentFragments
|
|
272
|
-
* - Primitive strings/numbers (as text nodes)
|
|
273
|
-
* - Nested arrays of above
|
|
274
|
-
* @param {(Node|string|number|Domo|DocumentFragment|Array<any>)[]} children
|
|
275
|
-
* @returns {Domo}
|
|
276
|
-
*/
|
|
277
|
-
append(children = []) {
|
|
278
|
-
return this.child(children);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Appends the current element to a target parent.
|
|
283
|
-
* Accepts:
|
|
284
|
-
* - HTMLElement
|
|
285
|
-
* - Domo instance
|
|
286
|
-
* - DocumentFragment
|
|
287
|
-
* @param {HTMLElement|Domo|DocumentFragment} target
|
|
288
|
-
* @returns {Domo}
|
|
289
|
-
*/
|
|
290
|
-
appendTo(target) {
|
|
291
|
-
if (_handleElementInstance(target)) parent.appendChild(this.element);
|
|
292
|
-
return this;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Appends the current element to a target parent.
|
|
297
|
-
* Alias for `appendTo`.
|
|
298
|
-
* @param {HTMLElement|Domo|DocumentFragment} target
|
|
299
|
-
* @returns {Domo}
|
|
300
|
-
*/
|
|
301
|
-
parent(target) {
|
|
302
|
-
return this.appendTo(target);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
parent(parent) {
|
|
306
|
-
parent.appendChild(this.element);
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Removes all children.
|
|
310
|
-
*/
|
|
311
|
-
clear() {
|
|
312
|
-
this.element.replaceChildren();
|
|
313
|
-
return this;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Replaces a child element or self with a new one.
|
|
318
|
-
* * Accepts:
|
|
319
|
-
* - DOM nodes
|
|
320
|
-
* - Domo instances
|
|
321
|
-
* - DocumentFragments
|
|
322
|
-
* - Primitive strings/numbers (as text nodes)
|
|
323
|
-
* - Nested arrays of above
|
|
324
|
-
*
|
|
325
|
-
* @param {Node} child
|
|
326
|
-
* @param {(Node|string|number|Domo|DocumentFragment|Array<any>)[]} newChild
|
|
327
|
-
*/
|
|
328
|
-
replace(child, newChild) {
|
|
329
|
-
const resolvedNew = this._handleElementInstance(newChild);
|
|
330
|
-
const resolvedOld = this._handleElementInstance(child);
|
|
331
|
-
|
|
332
|
-
if (resolvedOld === this.element) {
|
|
333
|
-
this.element.replaceWith(resolvedNew);
|
|
334
|
-
this.element = resolvedNew;
|
|
335
|
-
} else if (this.element.contains(resolvedOld)) {
|
|
336
|
-
resolvedOld.replaceWith(resolvedNew);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return this;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Shows or hides the element.
|
|
344
|
-
* @param {boolean} [visible=true]
|
|
345
|
-
* @param {string} [displayValue='block']
|
|
346
|
-
*/
|
|
347
|
-
show(visible = true, displayValue = "block") {
|
|
348
|
-
this.element.style.display = visible ? displayValue : "none";
|
|
349
|
-
return this;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Conditionally render element, or return dummy hidden placeholder.
|
|
354
|
-
* @param {boolean} condition
|
|
355
|
-
* @returns {Domo}
|
|
356
|
-
*/
|
|
357
|
-
if(condition) {
|
|
358
|
-
if (!condition) {
|
|
359
|
-
return new Domo("if").attr({ hidden: true }).data({ if: this.element.tagName.toLowerCase() });
|
|
360
|
-
}
|
|
361
|
-
return this;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Returns the constructed DOM element.
|
|
366
|
-
* @returns {HTMLElement}
|
|
367
|
-
*/
|
|
368
|
-
build() {
|
|
369
|
-
return this.element;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export default Domo;
|
package/src/core/DomoSVG.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import Domo from "./Domo.js";
|
|
2
|
-
class DomoSVG extends Domo {
|
|
3
|
-
constructor(tag = "svg") {
|
|
4
|
-
super(tag); // Call Domo constructor with tag
|
|
5
|
-
if (!this.isSVGTag(tag)) {
|
|
6
|
-
throw new Error(`Invalid SVG tag: ${tag}`);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check if the tag is a valid SVG element.
|
|
12
|
-
* @param {string} tag
|
|
13
|
-
* @returns {boolean}
|
|
14
|
-
*/
|
|
15
|
-
isSVGTag(tag) {
|
|
16
|
-
const svgTags = [
|
|
17
|
-
"svg",
|
|
18
|
-
"path",
|
|
19
|
-
"circle",
|
|
20
|
-
"rect",
|
|
21
|
-
"line",
|
|
22
|
-
"ellipse",
|
|
23
|
-
"polygon",
|
|
24
|
-
"g",
|
|
25
|
-
"text",
|
|
26
|
-
"use",
|
|
27
|
-
"defs",
|
|
28
|
-
"clipPath",
|
|
29
|
-
"marker",
|
|
30
|
-
"mask",
|
|
31
|
-
"style",
|
|
32
|
-
"linearGradient",
|
|
33
|
-
"radialGradient",
|
|
34
|
-
"stop",
|
|
35
|
-
];
|
|
36
|
-
return svgTags.includes(tag);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Override the el method to handle SVG namespace creation.
|
|
41
|
-
* @param {string} tag
|
|
42
|
-
* @returns {HTMLElement}
|
|
43
|
-
*/
|
|
44
|
-
el(tag) {
|
|
45
|
-
return this.isSVGTag(tag)
|
|
46
|
-
? document.createElementNS("http://www.w3.org/2000/svg", tag)
|
|
47
|
-
: super.el(tag); // Fallback to regular DOM creation
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Set attributes specific to SVG elements.
|
|
52
|
-
* @param {Record<string, any>} attributes
|
|
53
|
-
* @returns {DomoSVG}
|
|
54
|
-
*/
|
|
55
|
-
attr(attributes = {}) {
|
|
56
|
-
Object.entries(attributes).forEach(([key, value]) => {
|
|
57
|
-
if (key.startsWith("on")) return;
|
|
58
|
-
if (value != null) {
|
|
59
|
-
this.element.setAttributeNS(null, key, value); // Set using SVG-specific namespace
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
export default DomoSVG;
|
package/src/core/Router.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
let _routes = {};
|
|
2
|
-
let _listeners = [];
|
|
3
|
-
const _scrollPositions = {};
|
|
4
|
-
let _previousUrl = "";
|
|
5
|
-
|
|
6
|
-
let _root;
|
|
7
|
-
|
|
8
|
-
function init() {
|
|
9
|
-
["DOMContentLoaded", "popstate"].forEach((event) =>
|
|
10
|
-
window.addEventListener(event, async () => {
|
|
11
|
-
if (!_root) {
|
|
12
|
-
_root = document.createElement("main");
|
|
13
|
-
_root.id = "main";
|
|
14
|
-
}
|
|
15
|
-
saveScroll(_previousUrl);
|
|
16
|
-
const url = path();
|
|
17
|
-
load(url);
|
|
18
|
-
_previousUrl = url;
|
|
19
|
-
restoreScroll();
|
|
20
|
-
})
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function render({ component, meta }, params) {
|
|
25
|
-
try {
|
|
26
|
-
const content = await component(params);
|
|
27
|
-
_root?.replaceChildren();
|
|
28
|
-
|
|
29
|
-
if (content instanceof HTMLElement) {
|
|
30
|
-
_root.appendChild(content);
|
|
31
|
-
} else if (typeof content === "string") {
|
|
32
|
-
const wrapper = document.createElement("div");
|
|
33
|
-
wrapper.textContent = content;
|
|
34
|
-
_root.appendChild(wrapper);
|
|
35
|
-
} else {
|
|
36
|
-
throw new Error("Unsupported component output type");
|
|
37
|
-
}
|
|
38
|
-
if (meta) {
|
|
39
|
-
document.title = meta?.title;
|
|
40
|
-
document.querySelector("meta[name='description']").setAttribute("content", meta?.description);
|
|
41
|
-
}
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error("Rendering error:", error);
|
|
44
|
-
const fallback = _routes["*"]?.component?.({ error: error.message });
|
|
45
|
-
if (fallback) {
|
|
46
|
-
_root.replaceChildren();
|
|
47
|
-
_root.appendChild(fallback);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
async function goTo(path) {
|
|
52
|
-
saveScroll(_previousUrl);
|
|
53
|
-
await load(path);
|
|
54
|
-
_previousUrl = path;
|
|
55
|
-
restoreScroll();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const path = () => window.location.pathname + window.location.hash;
|
|
59
|
-
|
|
60
|
-
function info() {
|
|
61
|
-
const { segments } = parseUrl(path());
|
|
62
|
-
const { routeData, params } = match(segments);
|
|
63
|
-
return { meta: routeData.meta || {}, params, segments };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseUrl(url) {
|
|
67
|
-
// Remove the hash from the URL
|
|
68
|
-
const pureUrl = url.includes("#") ? url.split("#")[0] : url;
|
|
69
|
-
// Split the URL into segments keepinig '/' for nested routes
|
|
70
|
-
const segments = pureUrl.split(/(?=\/)/g).filter(Boolean);
|
|
71
|
-
return {
|
|
72
|
-
segments,
|
|
73
|
-
pureUrl,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function match(segments) {
|
|
78
|
-
if (!segments.length) return { routeData: _routes["/"] || _routes["*"] };
|
|
79
|
-
|
|
80
|
-
let current = _routes;
|
|
81
|
-
let params = {};
|
|
82
|
-
|
|
83
|
-
for (const segment of segments) {
|
|
84
|
-
if (current[segment]) {
|
|
85
|
-
// exact match found go deeper
|
|
86
|
-
current = current[segment].children || current[segment];
|
|
87
|
-
} else {
|
|
88
|
-
// look for dynamic route
|
|
89
|
-
const dynamic = Object.keys(current).find((k) => k.includes(":"));
|
|
90
|
-
if (!dynamic) return { routeData: _routes["*"], params: {} };
|
|
91
|
-
|
|
92
|
-
const parmName = dynamic.split(":")[1];
|
|
93
|
-
params = { ...params, [parmName]: segment.split("/")[1] };
|
|
94
|
-
current = current[dynamic].children || current[dynamic];
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// we send for rendering default child if component doesnt exists
|
|
98
|
-
const final = current.component ? current : current["/"] || _routes["*"];
|
|
99
|
-
|
|
100
|
-
return { params, routeData: final };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function load(url) {
|
|
104
|
-
const { segments, pureUrl } = parseUrl(url);
|
|
105
|
-
const { routeData, params } = match(segments);
|
|
106
|
-
|
|
107
|
-
if (path() !== url) {
|
|
108
|
-
history.pushState(null, null, pureUrl);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (url === _previousUrl) return;
|
|
112
|
-
|
|
113
|
-
await render(routeData, params);
|
|
114
|
-
if (_listeners.length > 0) notify(info());
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function notify(info) {
|
|
118
|
-
_listeners.forEach((cb) => cb(info));
|
|
119
|
-
}
|
|
120
|
-
function saveScroll(path = window?.location?.pathname) {
|
|
121
|
-
_scrollPositions[path] = window?.scrollY;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function restoreScroll() {
|
|
125
|
-
const pos = _scrollPositions[window?.location?.pathname];
|
|
126
|
-
window?.scrollTo(0, pos || 0);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const Router = {
|
|
130
|
-
/**
|
|
131
|
-
* Mount point of the router, where components will be rendered.
|
|
132
|
-
* @returns {HTMLElement} The root DOM element used by the router.
|
|
133
|
-
*/
|
|
134
|
-
mount: () => _root,
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Initializes the router by setting up event listeners and loading the initial route.
|
|
138
|
-
*/
|
|
139
|
-
init,
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Programmatically navigate to a new route.
|
|
143
|
-
* @param {string} path - The path to navigate to.
|
|
144
|
-
* @returns {Promise<void>}
|
|
145
|
-
*/
|
|
146
|
-
goTo,
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Navigate one step back in browser history.
|
|
150
|
-
*/
|
|
151
|
-
back: () => history.back(),
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get the current full path including hash.
|
|
155
|
-
* @returns {string} The current path.
|
|
156
|
-
*/
|
|
157
|
-
path,
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Get the previous URL path.
|
|
161
|
-
* @returns {string}
|
|
162
|
-
*/
|
|
163
|
-
prev: () => _previousUrl || "/",
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Returns the base (first) segment of the current path.
|
|
167
|
-
* @returns {string}
|
|
168
|
-
*/
|
|
169
|
-
base: () => parseUrl(path()).segments[0],
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Returns info about the current route.
|
|
173
|
-
* @returns {{ meta: object, params: object, segments: string[] }}
|
|
174
|
-
*/
|
|
175
|
-
info,
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Define your route structure.
|
|
179
|
-
*
|
|
180
|
-
* @param {Object} config - Route config object. Supports nested and dynamic routes.
|
|
181
|
-
* @example
|
|
182
|
-
* Router.routes({
|
|
183
|
-
* '/': { component: Home, meta: { title: "Home", description: "Welcome" } },
|
|
184
|
-
* '/blog': {
|
|
185
|
-
* children: {
|
|
186
|
-
* '/:slug': { component: BlogPost },
|
|
187
|
-
* '/': { component: Blog }
|
|
188
|
-
* }
|
|
189
|
-
* },
|
|
190
|
-
* '*': { component: NotFound }
|
|
191
|
-
* });
|
|
192
|
-
*/
|
|
193
|
-
routes: (config) => {
|
|
194
|
-
_routes = config;
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Register a listener for route changes.
|
|
199
|
-
* @param {(info: ReturnType<typeof info>) => void} fn - Listener callback.
|
|
200
|
-
*/
|
|
201
|
-
listen: (fn) => {
|
|
202
|
-
if (typeof fn === "function") _listeners.push(fn);
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
export default Router;
|