olum 0.2.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <p align="center"><img width="100" src="https://olumjs.github.io/logo.png" alt="Olum logo"></p>
1
+ <p align="center"><img width="100" src="https://github.com/olumjs.png" alt="Olum logo"></p>
2
2
  <p align="center">
3
3
  <a href="https://www.npmjs.com/package/olum" target="_blank"><img src="https://img.shields.io/npm/v/olum" alt="npm"></a>
4
4
  <img src="https://img.shields.io/npm/dm/olum" alt="npm">
@@ -8,21 +8,4 @@
8
8
  # Olumjs
9
9
  The VanillaJS developer’s platform.
10
10
 
11
- See [Documentation](https://olumjs.github.io/docs)
12
-
13
- Olum is a simple lightweight frontend library for providing spa experience, </br>
14
- You don't need to learn a new library or a framework if you already know vanilla js, and this is the reason behind Olumjs creation.
15
-
16
- ### Pros
17
- * Reusable Components
18
- * State Management System
19
- * Routing SPA
20
- * A size of 14kb including router, state management, other helpers
21
- * Coding with Vanilla JS, the best feature
22
-
23
- ### Ecosystem
24
- * Routing (SPA)
25
- * State Management system
26
- * Olum-CLI [npm](https://www.npmjs.com/package/olum-cli)
27
- * Devtool that works anywhere [download](https://github.com/olumjs/olum-devtool)
28
- * VSC extension [marketplace](https://marketplace.visualstudio.com/items?itemName=eissapk.olum)
11
+ See [Documentation](https://olumjs.github.io/docs)
package/dist/olum.js CHANGED
@@ -1,266 +1,547 @@
1
1
  /**
2
2
  * @name Olum.js
3
- * @version 0.2.3
4
- * @copyright 2021
3
+ * @version 0.5.0
4
+ * @copyright 2026
5
5
  * @author Eissa Saber
6
6
  * @license MIT
7
7
  */
8
- (function (root, factory) {
9
- if (typeof module === "object" && module.exports) module.exports = factory();
10
- else if (typeof define === "function" && define.amd) define(factory);
11
- else root.Olum = factory();
12
- })(typeof self !== "undefined" ? self : this, function () {
13
- "use strict";
14
- /* helpers */
15
- var global = typeof self !== "undefined" ? self : this;
16
- var debugStr = "Olum [warn]:";
17
- var isDebugging = false;
18
-
19
- function isDef(val) {
20
- return (val !== undefined && val !== null);
21
- }
8
+ export default (function () {
9
+ var olum = {
10
+ app: {}, // this is a must to access live changes
11
+ $emit(event, data) {
12
+ this.dispatchEvent(event, data);
13
+ },
14
+ dispatchEvent(event, data) {
15
+ window.dispatchEvent(new CustomEvent(event, { detail: data }));
16
+ },
17
+ mkElm(type, compName, compId) {
18
+ const el = document.createElement(type);
19
+ if (compName && compId) el.setAttribute("data-olum", JSON.stringify({ compName, compId }));
20
+ return el;
21
+ },
22
+ injectStyle(compName, cssContent) {
23
+ if (!cssContent || !cssContent.trim()) return;
24
+ const id = "olum-style-" + compName;
25
+ if (document.getElementById(id)) return;
26
+ const tag = document.createElement("style");
27
+ tag.id = id;
28
+ tag.textContent = cssContent;
29
+ document.head.appendChild(tag);
30
+ },
31
+ proxyHandler(obj, watcher, el) {
32
+ function mkHash(str) {
33
+ var hash = 0;
34
+ var i;
35
+ var char;
36
+ if (str.length === 0) return hash;
37
+ for (i = 0; i < str.length; i++) {
38
+ char = str.charCodeAt(i);
39
+ hash = (hash << 5) - hash + char;
40
+ hash |= 0; // Convert to 32bit integer
41
+ }
42
+ return hash;
43
+ }
44
+ var handler = {
45
+ get: function (obj, key) {
46
+ return obj[key];
47
+ },
48
+ set: function (obj, key, newVal) {
49
+ if (key === "__olum__") return false;
50
+ const oldVal = obj[key];
51
+ if (oldVal === newVal) return true;
52
+ obj[key] = newVal;
53
+ if (watcher && watcher[key] && typeof watcher[key] === "function") watcher[key](oldVal, newVal);
54
+ // todo prefix compName with instance e.g. you may have one component used twice so compName-1 & compName-2
55
+ const dataObj = { compName: obj.__olum__.compName, compId: obj.__olum__.compId };
56
+ const hash = mkHash(dataObj.compName + dataObj.compId);
57
+ dataObj.hash = hash;
22
58
 
23
- function isDev() {
24
- return ["localhost", "127.0.0.1"].indexOf(global.location.hostname) !== -1;
25
- }
59
+ window.olum.$emit("updateOlumComp", dataObj);
60
+ return true;
61
+ },
62
+ deleteProperty: function (obj, key) {
63
+ if (key === "__olum__") return false;
64
+ delete obj[key];
65
+ // todo prefix compName with instance e.g. you may have one component used twice so compName-1 & compName-2
66
+ const dataObj = { compName: obj.__olum__.compName, compId: obj.__olum__.compId };
67
+ const hash = mkHash(dataObj.compName + dataObj.compId);
68
+ dataObj.hash = hash;
26
69
 
27
- function isObj(obj) {
28
- return (obj !== null && typeof obj === "object");
29
- }
70
+ window.olum.$emit("updateOlumComp", dataObj);
71
+ return true;
72
+ },
73
+ };
74
+ return new Proxy(obj, handler);
75
+ },
76
+ proxyHandlerForStore(obj, originalProxy) {
77
+ const handler = {
78
+ get: function (obj, key) {
79
+ return originalProxy[key];
80
+ },
81
+ set: function (obj, key, val) {
82
+ obj[key] = val;
83
+ originalProxy[key] = val;
84
+ return true;
85
+ },
86
+ deleteProperty: function (obj, key) {
87
+ delete obj[key];
88
+ delete originalProxy[key];
89
+ return true;
90
+ },
91
+ };
92
+ return new Proxy(obj, handler);
93
+ },
94
+ clean(fragment) {
95
+ const str = String(fragment).trim();
96
+ if (str === "null") return null;
97
+ return str;
98
+ },
99
+ // #2 (escape-by-default): HTML-escape a value before it is interpolated into a template.
100
+ // The compiler wraps every text/attribute interpolation `{expr}` in `olum.esc(expr)` so a
101
+ // user-supplied string (a todo title, a comment, anything) can't inject markup/scripts (XSS)
102
+ // or visually break rendering when it contains <, >, &, or quotes.
103
+ // - null/undefined render as "" (instead of the literal text "null"/"undefined").
104
+ // - To render trusted HTML on purpose, opt out explicitly with `olum.raw(html)` (below);
105
+ // esc() detects the marker it returns and passes the HTML through unescaped.
106
+ esc(value) {
107
+ if (value === null || value === undefined) return "";
108
+ if (value && value.__olumRaw === true) return value.html; // explicit raw-HTML opt-in
109
+ return String(value)
110
+ .replace(/&/g, "&amp;") // must run first so the entities below aren't double-escaped
111
+ .replace(/</g, "&lt;")
112
+ .replace(/>/g, "&gt;")
113
+ .replace(/"/g, "&quot;")
114
+ .replace(/'/g, "&#39;");
115
+ },
116
+ // #2 opt-in escape hatch: mark a string as trusted raw HTML so `esc()` leaves it untouched.
117
+ // Usage in a template: {olum.raw(props.richText)}. Use it ONLY on HTML you control or have
118
+ // already sanitized — this is the deliberate, greppable way to bypass auto-escaping.
119
+ raw(html) {
120
+ return { __olumRaw: true, html: html == null ? "" : String(html) };
121
+ },
122
+ eventsHandler(el, nodes, compName, methodsRefObj) {
123
+ function event(item, str, modifiers) {
124
+ const eventName = str.split("|")[0];
125
+ str = str.split("|").slice(1).join();
30
126
 
31
- function isFullArr(arr) {
32
- return !!(isObj(arr) && Array.isArray(arr) && arr.length);
33
- }
127
+ let methods = str.split("&");
128
+ const data = methods.map((chunk) => {
129
+ const chunks = chunk.split("=");
130
+ const name = chunks[0];
131
+ const args = JSON.parse(chunks.slice(1).join(""));
132
+ return { args, methodName: name };
133
+ });
34
134
 
35
- function isFullObj(obj) {
36
- return !!(isObj(obj) && Array.isArray(Object.keys(obj)) && Object.keys(obj).length);
37
- }
135
+ const opts = {
136
+ once: false,
137
+ passive: false,
138
+ capture: false,
139
+ };
38
140
 
39
- function addProp(obj, key, val) {
40
- Object.defineProperty(obj, key, {
41
- value: val,
42
- writable: true,
43
- configurable: true,
44
- });
45
- }
46
- String.prototype.cap = function () {
47
- return this.toLowerCase().split(" ").map(function (word) {
48
- return word.charAt(0).toUpperCase() + word.slice(1);
49
- }).join(" ");
50
- };
141
+ if (modifiers && modifiers.length) {
142
+ if (modifiers.includes("once")) opts.once = true;
143
+ if (modifiers.includes("passive")) opts.passive = true;
144
+ if (modifiers.includes("capture")) opts.capture = true;
145
+ }
51
146
 
52
- function debug(args, level) {
53
- if (!isDef(level)) level = "log";
54
- level = level == "err" ? "error" : level;
55
- if (isDebugging) Array.isArray(args) ? console[level].apply(console, args) : console[level](args);
56
- }
147
+ item.addEventListener(
148
+ eventName.slice(2),
149
+ (e) => {
150
+ if (modifiers && modifiers.length) {
151
+ if (modifiers.includes("prevent")) e.preventDefault();
152
+ if (modifiers.includes("stop")) e.stopPropagation();
153
+ }
57
154
 
58
- function Olum(config) {
59
- if (!(this instanceof Olum)) throw new Error("can't invoke 'Olum' without 'new' keyword");
60
- var $this = this;
61
-
62
- // set defaults
63
- var rootElm = null;
64
- var rootCompName = null;
65
- var which = null;
66
- var prefix = config && config.prefix ? config.prefix : null;
67
-
68
- this.use = function (arg) {
69
- if (arg) {
70
- if (arg.name && typeof arg.name === "function" && arg.name() === "OlumRouter") which = { type: "router", cb: arg };
71
- else which = { type: "component", cb: arg };
72
- if (which) init();
73
- else throw new Error(debugStr + " Root component or router are not defined!");
74
- } else {
75
- throw new Error(debugStr + " Missing component or router @use()");
76
- }
77
- }
155
+ function init() {
156
+ data.forEach((obj) => {
157
+ if (methodsRefObj[obj.methodName]) {
158
+ if (obj.args.length) {
159
+ const eventIndex = obj.args.indexOf("$event");
160
+ if (eventIndex !== -1) obj.args.splice(eventIndex, 1, e);
161
+ methodsRefObj[obj.methodName](...obj.args);
162
+ } else {
163
+ methodsRefObj[obj.methodName](e);
164
+ }
165
+ } else {
166
+ console.warn("olum: can't access the method");
167
+ }
168
+ });
169
+ }
170
+
171
+ if (modifiers && modifiers.length && modifiers.includes("self")) {
172
+ if (e.target === item) init();
173
+ return;
174
+ }
175
+ init();
176
+ },
177
+ opts
178
+ );
78
179
 
79
- this.$ = function (selector) {
80
- if (selector) {
81
- var delimiter;
82
- if (selector.indexOf(".") !== -1) delimiter = ".";
83
- else if (selector.indexOf("#") !== -1) delimiter = "#";
84
- selector = selector.replace(/\#|\./, "");
85
- rootCompName = selector.cap();
86
- rootElm =
87
- delimiter === "." ? document.getElementsByClassName(selector)[0] :
88
- delimiter === "#" ? document.getElementById(selector) :
89
- document.getElementsByTagName(selector)[0];
90
- } else {
91
- throw new Error(debugStr + " Root Element is not found in DOM!");
180
+ item.removeAttribute("data-o-event");
181
+ item.removeAttribute("data-o-event-mode");
182
+ // item.removeAttribute("data-child-of");
92
183
  }
93
- return this;
94
- }
95
184
 
96
- function buildTree(entry) {
97
- var compsArr = [];
98
-
99
- function recursive(comp) {
100
- if (comp.hasOwnProperty("components") && isFullObj(comp.components)) {
101
- for (var key in comp.components) {
102
- var obj = {};
103
- addProp(obj, "parent", comp.name);
104
- addProp(obj, "child", {});
105
- addProp(obj.child, "name", key);
106
- var instance = new comp.components[key]();
107
- addProp(obj.child, "olumCompData", instance.olumCompData());
108
- compsArr.push(obj);
109
-
110
- if (obj.hasOwnProperty("child") && obj.child.hasOwnProperty("olumCompData")) {
111
- var nextEntry = obj.child.olumCompData;
112
- recursive(nextEntry);
113
- }
185
+ nodes.forEach((node) => {
186
+ const hasEvent = olum.clean(node.getAttribute("data-o-event"));
187
+ const hasMode = olum.clean(node.getAttribute("data-o-event-mode"));
188
+ const mode = hasMode && hasMode.trim() !== "" ? hasMode.split(".") : [];
189
+ if (hasEvent) event(node, hasEvent, mode);
190
+ });
191
+ },
192
+ stylesHandler(el, nodes, compName) {
193
+ function setStyles(item, obj, keys) {
194
+ let str = "";
195
+ keys.forEach((key) => {
196
+ const val = obj[key];
197
+ if (val) {
198
+ // console.log({ key, val });
199
+ str += key + ": " + val + "; ";
114
200
  }
201
+ });
202
+ str = str.trim();
203
+ // console.log(str);
204
+ if (str !== "") item.setAttribute("style", str);
205
+ item.removeAttribute("data-o-style");
206
+ // item.removeAttribute("data-child-of");
207
+ }
208
+
209
+ function style(item, str) {
210
+ const obj = JSON.parse(str);
211
+ const keys = Object.keys(obj);
212
+ if (keys.length) {
213
+ // console.log("directObj: ", obj);
214
+ setStyles(item, obj, keys);
215
+ }
216
+ }
217
+
218
+ nodes.forEach((node) => {
219
+ const hasStyle = olum.clean(node.getAttribute("data-o-style"));
220
+ if (hasStyle) style(node, hasStyle);
221
+ });
222
+ },
223
+ handleMarkup(compName, id, el, methods) {
224
+ el.setAttribute("data-child-of", compName);
225
+ el.setAttribute("data-o-" + id, "");
226
+ var nodes = el.querySelectorAll("*");
227
+ nodes.forEach((node) => {
228
+ node.setAttribute("data-child-of", compName);
229
+ node.setAttribute("data-o-" + id, "");
230
+ });
231
+ this.eventsHandler(el, nodes, compName, methods);
232
+ this.stylesHandler(el, nodes, compName);
233
+ return el;
234
+ },
235
+ isObj(obj) {
236
+ return obj !== null && typeof obj === "object";
237
+ },
238
+ isFullArr(arr) {
239
+ return !!(this.isObj(arr) && Array.isArray(arr) && arr.length);
240
+ },
241
+ isFullObj(obj) {
242
+ return !!(this.isObj(obj) && Array.isArray(Object.keys(obj)) && Object.keys(obj).length);
243
+ },
244
+ createStore(entry) {
245
+ const store = {};
246
+ const map = [];
247
+ store[entry.__OLUM__.compName] = entry; // bind root component
248
+
249
+ // bind children components
250
+ const recursive = (comp) => {
251
+ if (this.isFullArr(comp.__OLUM__.components)) {
252
+ comp.__OLUM__.components.forEach((item) => {
253
+ var key = Object.keys(item)[0];
254
+ var newComp = item[key]();
255
+ if (!store[key]) store[key] = newComp;
256
+
257
+ const obj = { name: key, children: newComp.__OLUM__.components.map((obj) => Object.keys(obj)[0]) };
258
+ map.push(obj);
259
+
260
+ if (this.isFullArr(obj.children)) recursive(newComp);
261
+ });
115
262
  }
116
263
  };
117
264
  recursive(entry);
118
- return compsArr;
119
- }
120
265
 
121
- function buildStyles(css) {
122
- var id = "olum_style_tag";
123
- var styleTag = document.getElementById(id);
124
- if (!styleTag) {
125
- styleTag = document.createElement("style");
126
- styleTag.id = id;
127
- document.head.append(styleTag);
266
+ return { store, map };
267
+ },
268
+
269
+ // direct <olum> placeholders of a container: those with no <olum> ancestor inside the container.
270
+ // (deeper placeholders belong to a nested component and are handled when that component renders)
271
+ directOlums(container) {
272
+ return Array.prototype.slice.call(container.querySelectorAll("olum")).filter((p) => {
273
+ const anc = p.parentElement && p.parentElement.closest && p.parentElement.closest("olum");
274
+ return !anc || !container.contains(anc);
275
+ });
276
+ },
277
+ buildTree(comp, store, compKey) {
278
+ const rootElm = comp.__OLUM__.getElm;
279
+ if (!rootElm) return null;
280
+ const self = this;
281
+ // lazy global factory registry: name -> factory. Accumulated from every instance's components map
282
+ // as we descend, so slot-passed and cross-file components resolve regardless of where they were declared.
283
+ const registry = window.olum.app.registry || (window.olum.app.registry = {});
284
+
285
+ function renderChildren(containerComp, containerKey, containerElm) {
286
+ if (containerComp.__OLUM__.components) Object.assign(registry, containerComp.__OLUM__.components);
287
+ const placeholders = self.directOlums(containerElm);
288
+ const occ = {}; // per-name occurrence counter -> positional instance keys, stable across re-renders
289
+ placeholders.forEach((placeholder) => {
290
+ const name = placeholder.getAttribute("name");
291
+ const factory = registry[name] || (containerComp.__OLUM__.components && containerComp.__OLUM__.components[name]);
292
+ if (!factory) {
293
+ console.warn("olum: couldn't find " + name + " Component while building the tree!");
294
+ return;
295
+ }
296
+ // #3 (keyed reconciliation): a placeholder rendered inside a keyed <for> carries data-o-key
297
+ // (the evaluated `key={...}` value for that item). Keyed instances are stored under
298
+ // "...Name@<key>", so the SAME instance — and therefore its state and DOM — is reused for a
299
+ // given item no matter where it moves, is inserted, or is removed in the list. Without a key
300
+ // we fall back to positional "...Name#<i>" keys, where state follows position, not identity
301
+ // (delete the middle item of a list and the survivors inherit the wrong neighbour's state).
302
+ const keyVal = placeholder.getAttribute("data-o-key");
303
+ let instanceKey;
304
+ if (keyVal !== null && keyVal !== "") {
305
+ instanceKey = containerKey + ">" + name + "@" + keyVal;
306
+ } else {
307
+ occ[name] = occ[name] === undefined ? 0 : occ[name] + 1;
308
+ instanceKey = containerKey + ">" + name + "#" + occ[name];
309
+ }
310
+
311
+ // reuse the instance from a previous render (preserves its state) or mint a fresh one
312
+ let child = store[instanceKey];
313
+ if (!child) {
314
+ child = factory(instanceKey);
315
+ store[instanceKey] = child;
316
+ }
317
+
318
+ // handle props
319
+ child.parentCompName = containerKey;
320
+ const propsJson = placeholder.getAttribute("data-o-props");
321
+ child.incomingProps = propsJson ? JSON.parse(decodeURIComponent(propsJson)) : {};
322
+ const srcStr = placeholder.getAttribute("data-o-props-src") || "";
323
+ child.incomingPropSources = {};
324
+ if (srcStr)
325
+ srcStr.split("|").forEach((pair) => {
326
+ const parts = pair.split(":"); // propKey:kind:srcKey
327
+ const propKey = parts[0],
328
+ kind = parts[1],
329
+ srcKey = parts[2];
330
+ if (propKey && kind && srcKey) child.incomingPropSources[propKey] = { kind, key: srcKey };
331
+ });
332
+
333
+ // handle slot/children — must be set before getElm so ${children} resolves in the child's template
334
+ child.children = placeholder.innerHTML.trim();
335
+
336
+ const elm = child.__OLUM__.getElm;
337
+ if (elm) {
338
+ elm.setAttribute("data-o-if", placeholder.getAttribute("if") ? placeholder.getAttribute("if") : "olum-no-condition"); // display if condition value (truthy, falsy)
339
+ placeholder.replaceWith(elm);
340
+ renderChildren(child, instanceKey, elm); // recurse into this child's own subtree
341
+ }
342
+ });
128
343
  }
129
- styleTag.innerHTML = css + "\n [to] * {pointer-events: none;}";
130
- }
131
344
 
132
- function merge(tree) {
133
- var entry = tree.entry;
134
- var compsArr = tree.compsArr;
135
-
136
- // parent (view)
137
- var template = entry.template || "";
138
- var style = entry.style || "";
139
- var script = {};
140
- !!(entry.render) ? (script[0] = entry.render) : null;
141
-
142
- // children
143
- if (isFullArr(compsArr)) {
144
- for(var i = 0; i < compsArr.length; i++) {
145
- var olumCompData = compsArr[i].child.olumCompData;
146
- var html = olumCompData.template || "";
147
- var css = olumCompData.style || "";
148
- var js = olumCompData.render || null;
149
- var name = prefix ? prefix + "-" + olumCompData.name : olumCompData.name;
150
- var regex = new RegExp("<(" + name + "\\s{0,})\\/>", "gi"); // detect components e.g. <App-AddTodo /> or <AddTodo />
151
-
152
- template = template.replace(regex, html);
153
- style += css;
154
- if (js !== null) script[i + 1] = js;
155
- }
345
+ renderChildren(comp, compKey, rootElm);
346
+ return rootElm;
347
+ },
348
+ getInnerNames(entry) {
349
+ const comps = [];
350
+ const map = window.olum.app.map;
351
+ if (map) {
352
+ map.find((obj) => {
353
+ if (obj.name == entry) obj.children.forEach((child) => comps.push(child));
354
+ });
156
355
  }
157
356
 
158
- return {
159
- template,
160
- style,
161
- script,
162
- };
357
+ function recursive(num) {
358
+ const child = comps[num];
359
+ map.forEach((obj) => {
360
+ if (obj.name == child) obj.children.forEach((item) => comps.push(item));
361
+ });
362
+ if (num + 1 <= comps.length) recursive(num + 1);
363
+ }
364
+
365
+ if (comps.length) recursive(0);
366
+
367
+ // console.warn("innerComps: ", comps);
368
+ return comps;
369
+ },
370
+ };
371
+
372
+ if (typeof window !== "undefined") window.olum = olum;
373
+
374
+ class Olum {
375
+ root = null;
376
+ $(s) {
377
+ this.root = document.querySelector(s);
378
+ return this;
163
379
  }
164
-
165
- function labelView(root, arr) {
166
- var compAttrRegex = /(olum-component=[\"\']([^\"|\']*)[\"\'])|(olum-component)/gi;
167
- var openingSelfClosingTagRegex = /<[a-z]+(>|.*?[^?]>)/gi;
168
- var greaterCharRegex = /\>/gi;
169
- var compsArr = Array.from(arr);
170
- var entry = root;
171
-
172
- // children
173
- for(var i = 0; i < compsArr.length; i++) {
174
- var name = compsArr[i].child.olumCompData.name || "undefined";
175
- if (compsArr[i].child && compsArr[i].child.olumCompData && compsArr[i].child.olumCompData.hasOwnProperty("template")) {
176
- var olumCompData = compsArr[i].child.olumCompData;
177
- // clean
178
- olumCompData.template = olumCompData.template.replace(compAttrRegex, "");
179
- // labeling
180
- var compWrapper = olumCompData.template.match(openingSelfClosingTagRegex);
181
- if (isFullArr(compWrapper)) {
182
- olumCompData.template = olumCompData.template.replace(compWrapper[0], compWrapper[0].replace(greaterCharRegex, " olum-component='"+name+"'>"));
380
+
381
+ use(comp) {
382
+ const entry = comp();
383
+ const { store, rootKey } = this.share(entry);
384
+ const tree = window.olum.buildTree(entry, store, rootKey);
385
+ if (!tree) return console.warn("olum: couldn't build tree!");
386
+
387
+ this.setupListeners(store); // unmounted hook & state system
388
+
389
+ this.root.append(tree); // bind full comps tree
390
+ // handle mounted hook
391
+ if (entry.hooks.mounted) entry.hooks.mounted(); // force mounting parent component (entry point comp) regardless of the data-o-if value
392
+ entry.hooks.isMounted = true;
393
+ // mount every instance created during buildTree (keyed by runtime instance key)
394
+ Object.keys(store).forEach((key) => {
395
+ if (key === rootKey) return;
396
+ const c = store[key];
397
+ if (!c || !c.el) return;
398
+ const ifConValue = c.el.getAttribute("data-o-if");
399
+ if (!ifConValue) {
400
+ // don't render comp because it has falsy value
401
+ } else {
402
+ if (["olum-no-condition", "true"].includes(ifConValue)) {
403
+ if (c.hooks.mounted) c.hooks.mounted();
404
+ c.hooks.isMounted = true;
183
405
  }
184
406
  }
185
- }
407
+ });
408
+ }
186
409
 
187
- // parent (view)
188
- if (entry.template) {
189
- var name = entry.name || "undefined";
190
- // clean
191
- entry.template = entry.template.replace(compAttrRegex, "");
192
- // labeling
193
- var compWrapper = entry.template.match(openingSelfClosingTagRegex);
194
- if (isFullArr(compWrapper)) {
195
- entry.template = entry.template.replace(
196
- compWrapper[0],
197
- compWrapper[0].replace(greaterCharRegex, " olum-component='"+name+"' olum-view='"+name+"'>")
198
- );
199
- }
410
+ getPath(el, root) {
411
+ const path = [];
412
+ let cur = el;
413
+ while (cur && cur !== root) {
414
+ const parent = cur.parentElement;
415
+ if (!parent) break;
416
+ const siblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
417
+ path.unshift({ tag: cur.tagName, index: siblings.indexOf(cur) });
418
+ cur = parent;
200
419
  }
420
+ return path;
421
+ }
201
422
 
202
- // root (placeholder)
203
- rootElm.setAttribute("olum-component", rootCompName);
204
-
205
- return {
206
- entry,
207
- compsArr,
208
- };
423
+ findByPath(root, path) {
424
+ let cur = root;
425
+ for (const step of path) {
426
+ const siblings = Array.from(cur.children).filter((c) => c.tagName === step.tag);
427
+ cur = siblings[step.index];
428
+ if (!cur) return null;
429
+ }
430
+ return cur;
209
431
  }
210
432
 
211
- function useComponent() {
212
- debug("use component");
213
- var view = new which.cb();
214
- var entry = view.olumCompData();
215
- var compsArr = buildTree(entry);
216
- // labeling components
217
- var label = isDev() ? labelView(entry, compsArr) : { entry: entry, compsArr: compsArr };
218
- // final component (View)
219
- var viewObj = merge(label);
220
- // css
221
- buildStyles(viewObj.style);
222
- // html
223
- rootElm.innerHTML = viewObj.template;
224
- // js
225
- setTimeout(function () {
226
- for (var key in viewObj.script) {
227
- viewObj.script[key]();
433
+ setupListeners(store) {
434
+ function mkHash(str) {
435
+ var hash = 0;
436
+ var i;
437
+ var char;
438
+ if (str.length === 0) return hash;
439
+ for (i = 0; i < str.length; i++) {
440
+ char = str.charCodeAt(i);
441
+ hash = (hash << 5) - hash + char;
442
+ hash |= 0;
228
443
  }
229
- }, 0);
230
- }
231
-
232
- function useRouter() {
233
- debug("use OlumRouter");
234
- var router = which.cb;
235
- // share core functionalites with router
236
- // props
237
- router.__proto__.rootElm = rootElm;
238
- // methods
239
- router.__proto__.buildStyles = buildStyles;
240
- router.__proto__.buildTree = buildTree;
241
- router.__proto__.labelView = labelView;
242
- router.__proto__.merge = merge;
243
-
244
- if (router.isReady) router.listen();
245
- }
246
-
247
- function mount() {
248
- debug({mount:"mount()",rootElm, which});
249
-
250
- if (which && which.type && which.cb) {
251
- if (which.type === "router") useRouter();
252
- else if (which.type === "component") useComponent();
253
- } else {
254
- throw new Error(debugStr + " Can't mount, Missing component or router @use()");
444
+ return hash;
255
445
  }
446
+
447
+ // const snapshots = {}; // persists across re-renders: { [compName]: Map<pathKey, entry> }
448
+
449
+ window.addEventListener("updateOlumComp", (e) => {
450
+ if (e && e.detail && e.detail.compName && e.detail.compId && e.detail.hash) {
451
+ if (e.detail.hash !== mkHash(e.detail.compName + e.detail.compId)) return;
452
+ const comp = store[e.detail.compName];
453
+ if (!comp || !comp.el) return;
454
+ if (!document.body.contains(comp.el)) return; // comp was unmounted; ignore stale state updates
455
+
456
+ const compName = e.detail.compName;
457
+
458
+ // snapshot input values of child components currently in DOM
459
+ // only child components (data-child-of !== compName) so we don't overwrite state inputs with stale values
460
+ // entries not updated here (hidden components) keep their previous snapshot value intact
461
+ // if (!snapshots[compName]) snapshots[compName] = new Map();
462
+ // comp.el.querySelectorAll("input, textarea, select").forEach(field => {
463
+ // const childOf = field.getAttribute("data-child-of");
464
+ // if (childOf && childOf !== compName) {
465
+ // const path = this.getPath(field, comp.el);
466
+ // const entry = { path };
467
+ // if (field.type === "checkbox" || field.type === "radio") entry.checked = field.checked;
468
+ // else entry.value = field.value;
469
+ // snapshots[compName].set(JSON.stringify(path), entry);
470
+ // }
471
+ // });
472
+
473
+ const innerNames = Object.keys(store).filter((name) => name !== compName);
474
+ const prevMounted = {};
475
+ innerNames.forEach((name) => {
476
+ const c = store[name];
477
+ if (c) prevMounted[name] = c.hooks.isMounted;
478
+ });
479
+
480
+ // get current active element location/path before re-render (buildTree)
481
+ const activeEl = document.activeElement;
482
+ const selStart = activeEl && activeEl.selectionStart != null ? activeEl.selectionStart : null;
483
+ const selEnd = activeEl && activeEl.selectionEnd != null ? activeEl.selectionEnd : null;
484
+ const activePath = activeEl && comp.el.contains(activeEl) ? this.getPath(activeEl, comp.el) : null;
485
+
486
+ const treeElm = window.olum.buildTree(comp, store, compName);
487
+ if (!treeElm) return console.warn("olum: couldn't build tree!");
488
+ comp.el.replaceWith(treeElm);
489
+
490
+ // restore child component input values after rebuild
491
+ // snapshots[compName].forEach(({ path, value, checked }) => {
492
+ // const field = this.findByPath(treeElm, path);
493
+ // if (!field) return; // component is hidden in this render, skip
494
+ // if (checked !== undefined) field.checked = checked;
495
+ // else if (value !== undefined) field.value = value;
496
+ // });
497
+
498
+ // load prev active element after re-render (buildTree) to focus (e.g. input) or restore its data (e.g. forms)
499
+ if (activePath && activePath.length) {
500
+ const toRestore = this.findByPath(treeElm, activePath);
501
+ if (toRestore) {
502
+ if (toRestore.focus && typeof toRestore.focus == "function") toRestore.focus();
503
+ if (selStart !== null && toRestore.setSelectionRange && typeof toRestore.setSelectionRange == "function")
504
+ toRestore.setSelectionRange(selStart, selEnd);
505
+ }
506
+ }
507
+
508
+ // after rebuild, call lifecycle hooks for any inner component that changed visibility.
509
+ // recompute keys here so instances created during this rebuild (e.g. a grown loop) are included.
510
+ const afterNames = Object.keys(store).filter((name) => name !== compName);
511
+ afterNames.forEach((name) => {
512
+ const c = store[name];
513
+ if (!c) return;
514
+ const isInDOM = document.body.contains(c.el);
515
+ if (prevMounted[name] && !isInDOM) {
516
+ // was mounted, now removed from DOM → unMounted
517
+ if (c.hooks.unMounted && !c.hooks.isUnMounted) {
518
+ c.hooks.unMounted();
519
+ c.hooks.isUnMounted = true;
520
+ c.hooks.isMounted = false;
521
+ }
522
+ } else if (!prevMounted[name] && isInDOM) {
523
+ // was not mounted, now in DOM → mounted
524
+ if (c.hooks.mounted && !c.hooks.isMounted) {
525
+ c.hooks.mounted();
526
+ c.hooks.isMounted = true;
527
+ c.hooks.isUnMounted = false;
528
+ }
529
+ }
530
+ });
531
+ }
532
+ });
256
533
  }
257
534
 
258
- function init() {
259
- if (!rootElm) throw new Error(debugStr + " Root Element is not found in DOM!");
260
- else mount();
535
+ share(entry) {
536
+ // store now holds live instances keyed by runtime instance key; buildTree populates it lazily.
537
+ // registry holds name->factory and is accumulated as the tree is walked.
538
+ const store = {};
539
+ const rootKey = entry.__OLUM__.compName;
540
+ store[rootKey] = entry;
541
+ Object.assign(window.olum.app, { store, registry: {} }); // this is a must to access live changes
542
+ return { store, rootKey };
261
543
  }
262
-
263
544
  }
264
545
 
265
546
  return Olum;
266
- });
547
+ })();
package/dist/olum.min.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @name Olum.js
3
- * @version 0.2.2
4
- * @copyright 2021
2
+ * @name olum
3
+ * @version 0.5.0
4
+ * @copyright 2026
5
5
  * @author Eissa Saber
6
6
  * @license MIT
7
7
  */
8
- !function(e,t){"object"==typeof module&&module.exports?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Olum=t()}("undefined"!=typeof self?self:this,function(){"use strict";var m="undefined"!=typeof self?self:this,n="Olum [warn]:",o=!1;function u(e){return null!==e&&"object"==typeof e}function f(e){return u(e)&&Array.isArray(e)&&e.length}function d(e,t,n){Object.defineProperty(e,t,{value:n,writable:!0,configurable:!0})}function h(e,t){t="err"==(t=null==t?"log":t)?"error":t,o&&(Array.isArray(e)?console[t].apply(console,e):console[t](e))}return String.prototype.cap=function(){return this.toLowerCase().split(" ").map(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}).join(" ")},function e(t){if(!(this instanceof e))throw new Error("can't invoke 'Olum' without 'new' keyword");var c=null,p=null,o=null,s=t&&t.prefix?t.prefix:null;function r(e){var l=[];return function e(t){if(t.hasOwnProperty("components")&&u(r=t.components)&&Array.isArray(Object.keys(r))&&Object.keys(r).length)for(var n in t.components){var o={};d(o,"parent",t.name),d(o,"child",{}),d(o.child,"name",n),n=new t.components[n],d(o.child,"olumCompData",n.olumCompData()),l.push(o),o.hasOwnProperty("child")&&o.child.hasOwnProperty("olumCompData")&&e(o.child.olumCompData)}var r}(e),l}function l(e){var t="olum_style_tag",n=document.getElementById(t);n||((n=document.createElement("style")).id=t,document.head.append(n)),n.innerHTML=e+"\n [to] * {pointer-events: none;}"}function a(e){var t=e.entry,n=e.compsArr,o=t.template||"",r=t.style||"",l={};if(t.render&&(l[0]=t.render),f(n))for(var a=0;a<n.length;a++){var i=(c=n[a].child.olumCompData).template||"",m=c.style||"",u=c.render||null,c=s?s+"-"+c.name:c.name,c=new RegExp("<("+c+"\\s{0,})\\/>","gi"),o=o.replace(c,i);r+=m,null!==u&&(l[a+1]=u)}return{template:o,style:r,script:l}}function i(e,t){for(var n=/(olum-component=[\"\']([^\"|\']*)[\"\'])|(olum-component)/gi,o=/<[a-z]+(>|.*?[^?]>)/gi,r=/\>/gi,l=Array.from(t),e=e,a=0;a<l.length;a++){var i,m,u=l[a].child.olumCompData.name||"undefined";l[a].child&&l[a].child.olumCompData&&l[a].child.olumCompData.hasOwnProperty("template")&&((i=l[a].child.olumCompData).template=i.template.replace(n,""),f(m=i.template.match(o))&&(i.template=i.template.replace(m[0],m[0].replace(r," olum-component='"+u+"'>"))))}return e.template&&(u=e.name||"undefined",e.template=e.template.replace(n,""),f(m=e.template.match(o))&&(e.template=e.template.replace(m[0],m[0].replace(r," olum-component='"+u+"' olum-view='"+u+"'>")))),c.setAttribute("olum-component",p),{entry:e,compsArr:l}}this.use=function(e){if(!e)throw new Error(n+" Missing component or router @use()");if(!(o=e.name&&"function"==typeof e.name&&"OlumRouter"===e.name()?{type:"router",cb:e}:{type:"component",cb:e}))throw new Error(n+" Root component or router are not defined!");!function(){if(!c)throw new Error(n+" Root Element is not found in DOM!");!function(){if(h({mount:"mount()",rootElm:c,which:o}),!(o&&o.type&&o.cb))throw new Error(n+" Can't mount, Missing component or router @use()");"router"===o.type?function(){h("use OlumRouter");var e=o.cb;e.__proto__.rootElm=c,e.__proto__.buildStyles=l,e.__proto__.buildTree=r,e.__proto__.labelView=i,e.__proto__.merge=a,e.isReady&&e.listen()}():"component"===o.type&&function(){h("use component");var e=(new o.cb).olumCompData(),t=r(e),n=a(-1!==["localhost","127.0.0.1"].indexOf(m.location.hostname)?i(e,t):{entry:e,compsArr:t});l(n.style),c.innerHTML=n.template,setTimeout(function(){for(var e in n.script)n.script[e]()},0)}()}()}()},this.$=function(e){if(!e)throw new Error(n+" Root Element is not found in DOM!");var t;return-1!==e.indexOf(".")?t=".":-1!==e.indexOf("#")&&(t="#"),e=e.replace(/\#|\./,""),p=e.cap(),c="."===t?document.getElementsByClassName(e)[0]:"#"===t?document.getElementById(e):document.getElementsByTagName(e)[0],this}}});
8
+ export default(()=>{var i={app:{},$emit(e,t){this.dispatchEvent(e,t)},dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:t}))},mkElm(e,t,o){e=document.createElement(e);return t&&o&&e.setAttribute("data-olum",JSON.stringify({compName:t,compId:o})),e},injectStyle(e,t){var o;t&&t.trim()&&(e="olum-style-"+e,document.getElementById(e)||((o=document.createElement("style")).id=e,o.textContent=t,document.head.appendChild(o)))},proxyHandler(e,r,t){function l(e){var t,o=0;if(0!==e.length)for(t=0;t<e.length;t++)o=(o<<5)-o+e.charCodeAt(t),o|=0;return o}return new Proxy(e,{get:function(e,t){return e[t]},set:function(e,t,o){var n;return"__olum__"!==t&&((n=e[t])!==o&&(e[t]=o,r&&r[t]&&"function"==typeof r[t]&&r[t](n,o),n=l((t={compName:e.__olum__.compName,compId:e.__olum__.compId}).compName+t.compId),t.hash=n,window.olum.$emit("updateOlumComp",t)),!0)},deleteProperty:function(e,t){if("__olum__"===t)return!1;delete e[t];t={compName:e.__olum__.compName,compId:e.__olum__.compId},e=l(t.compName+t.compId);return t.hash=e,window.olum.$emit("updateOlumComp",t),!0}})},proxyHandlerForStore(e,n){return new Proxy(e,{get:function(e,t){return n[t]},set:function(e,t,o){return e[t]=o,n[t]=o,!0},deleteProperty:function(e,t){return delete e[t],delete n[t],!0}})},clean(e){e=String(e).trim();return"null"===e?null:e},esc(e){return null==e?"":e&&!0===e.__olumRaw?e.html:String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")},raw(e){return{__olumRaw:!0,html:null==e?"":String(e)}},eventsHandler(e,t,o,a){t.forEach(e=>{var o=i.clean(e.getAttribute("data-o-event")),n=i.clean(e.getAttribute("data-o-event-mode")),n=n&&""!==n.trim()?n.split("."):[];if(o){var r=e,e=o,l=n,o=e.split("|")[0];let t=(e=e.split("|").slice(1).join()).split("&").map(e=>{var e=e.split("="),t=e[0];return{args:JSON.parse(e.slice(1).join("")),methodName:t}});e={once:!1,passive:!1,capture:!1},l&&l.length&&(l.includes("once")&&(e.once=!0),l.includes("passive")&&(e.passive=!0),l.includes("capture"))&&(e.capture=!0),r.addEventListener(o.slice(2),o=>{function e(){t.forEach(e=>{var t;a[e.methodName]?e.args.length?(-1!==(t=e.args.indexOf("$event"))&&e.args.splice(t,1,o),a[e.methodName](...e.args)):a[e.methodName](o):console.warn("olum: can't access the method")})}l&&l.length&&(l.includes("prevent")&&o.preventDefault(),l.includes("stop"))&&o.stopPropagation(),l&&l.length&&l.includes("self")&&o.target!==r||e()},e),r.removeAttribute("data-o-event"),r.removeAttribute("data-o-event-mode")}})},stylesHandler(e,t,o){t.forEach(e=>{var t=i.clean(e.getAttribute("data-o-style"));if(t){var t=JSON.parse(t),n=Object.keys(t);if(n.length){var r=t;t=n;let o="";t.forEach(e=>{var t=r[e];t&&(o+=e+": "+t+"; ")}),""!==(o=o.trim())&&e.setAttribute("style",o),e.removeAttribute("data-o-style")}}})},handleMarkup(t,o,e,n){e.setAttribute("data-child-of",t),e.setAttribute("data-o-"+o,"");var r=e.querySelectorAll("*");return r.forEach(e=>{e.setAttribute("data-child-of",t),e.setAttribute("data-o-"+o,"")}),this.eventsHandler(e,r,t,n),this.stylesHandler(e,r,t),e},isObj(e){return null!==e&&"object"==typeof e},isFullArr(e){return!!(this.isObj(e)&&Array.isArray(e)&&e.length)},isFullObj(e){return!!(this.isObj(e)&&Array.isArray(Object.keys(e))&&Object.keys(e).length)},createStore(e){let o={},n=[],r=(o[e.__OLUM__.compName]=e,e=>{this.isFullArr(e.__OLUM__.components)&&e.__OLUM__.components.forEach(e=>{var t=Object.keys(e)[0],e=e[t](),t=(o[t]||(o[t]=e),{name:t,children:e.__OLUM__.components.map(e=>Object.keys(e)[0])});n.push(t),this.isFullArr(t.children)&&r(e)})});return r(e),{store:o,map:n}},directOlums(t){return Array.prototype.slice.call(t.querySelectorAll("olum")).filter(e=>{e=e.parentElement&&e.parentElement.closest&&e.parentElement.closest("olum");return!e||!t.contains(e)})},buildTree(e,u,t){var o=e.__OLUM__.getElm;if(!o)return null;let n=this,c=window.olum.app.registry||(window.olum.app.registry={});return function a(e,i,t){e.__OLUM__.components&&Object.assign(c,e.__OLUM__.components);t=n.directOlums(t);let s={};t.forEach(t=>{var o=t.getAttribute("name"),r=c[o]||e.__OLUM__.components&&e.__OLUM__.components[o];if(r){var l=t.getAttribute("data-o-key");let e,n=(e=null!==l&&""!==l?i+">"+o+"@"+l:(s[o]=void 0===s[o]?0:s[o]+1,i+">"+o+"#"+s[o]),u[e]);n||(n=r(e),u[e]=n),n.parentCompName=i,l=t.getAttribute("data-o-props"),n.incomingProps=l?JSON.parse(decodeURIComponent(l)):{},r=t.getAttribute("data-o-props-src")||"",n.incomingPropSources={},r&&r.split("|").forEach(e=>{var t=(e=e.split(":"))[0],o=e[1],e=e[2];t&&o&&e&&(n.incomingPropSources[t]={kind:o,key:e})}),n.children=t.innerHTML.trim(),(l=n.__OLUM__.getElm)&&(l.setAttribute("data-o-if",t.getAttribute("if")?t.getAttribute("if"):"olum-no-condition"),t.replaceWith(l),a(n,e,l))}else console.warn("olum: couldn't find "+o+" Component while building the tree!")})}(e,t,o),o},getInnerNames(t){let n=[],r=window.olum.app.map;return r&&r.find(e=>{e.name==t&&e.children.forEach(e=>n.push(e))}),n.length&&function e(t){let o=n[t];r.forEach(e=>{e.name==o&&e.children.forEach(e=>n.push(e))}),t+1<=n.length&&e(t+1)}(0),n}};"undefined"!=typeof window&&(window.olum=i);class e{root=null;$(e){return this.root=document.querySelector(e),this}use(e){e=e();let{store:o,rootKey:n}=this.share(e);var t=window.olum.buildTree(e,o,n);if(!t)return console.warn("olum: couldn't build tree!");this.setupListeners(o),this.root.append(t),e.hooks.mounted&&e.hooks.mounted(),e.hooks.isMounted=!0,Object.keys(o).forEach(e=>{var t;e!==n&&(e=o[e])&&e.el&&(t=e.el.getAttribute("data-o-if"))&&["olum-no-condition","true"].includes(t)&&(e.hooks.mounted&&e.hooks.mounted(),e.hooks.isMounted=!0)})}getPath(e,t){var o=[];let n=e;for(;n&&n!==t;){var r=n.parentElement;if(!r)break;var l=Array.from(r.children).filter(e=>e.tagName===n.tagName);o.unshift({tag:n.tagName,index:l.indexOf(n)}),n=r}return o}findByPath(e,o){let n=e;for(let t of o){var r=Array.from(n.children).filter(e=>e.tagName===t.tag);if(!(n=r[t.index]))return null}return n}setupListeners(i){window.addEventListener("updateOlumComp",e=>{if(e&&e.detail&&e.detail.compName&&e.detail.compId&&e.detail.hash&&e.detail.hash===(e=>{var t,o=0;if(0!==e.length)for(t=0;t<e.length;t++)o=(o<<5)-o+e.charCodeAt(t),o|=0;return o})(e.detail.compName+e.detail.compId)){var o=i[e.detail.compName];if(o&&o.el&&document.body.contains(o.el)){let t=e.detail.compName;e=Object.keys(i).filter(e=>e!==t);let n={};e.forEach(e=>{var t=i[e];t&&(n[e]=t.hooks.isMounted)});var e=document.activeElement,r=e&&null!=e.selectionStart?e.selectionStart:null,l=e&&null!=e.selectionEnd?e.selectionEnd:null,e=e&&o.el.contains(e)?this.getPath(e,o.el):null,a=window.olum.buildTree(o,i,t);if(!a)return console.warn("olum: couldn't build tree!");o.el.replaceWith(a),e&&e.length&&(o=this.findByPath(a,e))&&(o.focus&&"function"==typeof o.focus&&o.focus(),null!==r)&&o.setSelectionRange&&"function"==typeof o.setSelectionRange&&o.setSelectionRange(r,l),Object.keys(i).filter(e=>e!==t).forEach(e=>{var t,o=i[e];o&&(t=document.body.contains(o.el),n[e]&&!t?o.hooks.unMounted&&!o.hooks.isUnMounted&&(o.hooks.unMounted(),o.hooks.isUnMounted=!0,o.hooks.isMounted=!1):!n[e]&&t&&o.hooks.mounted&&!o.hooks.isMounted&&(o.hooks.mounted(),o.hooks.isMounted=!0,o.hooks.isUnMounted=!1))})}}})}share(e){var t={},o=e.__OLUM__.compName;return t[o]=e,Object.assign(window.olum.app,{store:t,registry:{}}),{store:t,rootKey:o}}}return e})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "olum",
3
- "version": "0.2.3",
3
+ "version": "0.5.0",
4
4
  "description": "The VanillaJS developer’s platform.",
5
5
  "main": "dist/olum.js",
6
6
  "directories": {