@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/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;
@@ -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;
@@ -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;