olum 0.2.4 → 0.5.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/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright 2021 Eissa Saber
3
+ Copyright 2026 Eissa Saber
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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">
package/dist/olum.js CHANGED
@@ -1,272 +1,547 @@
1
1
  /**
2
2
  * @name Olum.js
3
- * @version 0.2.4
4
- * @copyright 2021
3
+ * @version 0.5.1
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.html(value)` (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.__olumHtml === 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.html(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
+ html(value) {
120
+ return { __olumHtml: true, html: value == null ? "" : String(value) };
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()");
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
+ );
179
+
180
+ item.removeAttribute("data-o-event");
181
+ item.removeAttribute("data-o-event-mode");
182
+ // item.removeAttribute("data-child-of");
76
183
  }
77
- }
78
184
 
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!");
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 + "; ";
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
+ }
92
216
  }
93
- return this;
94
- }
95
217
 
96
- function buildTree(entry) {
97
- var compsArr = [];
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
98
248
 
99
- function recursive(comp) {
100
- if (comp.hasOwnProperty("components") && isFullArr(comp.components)) {
101
-
102
- for (var i = 0; i < comp.components.length; i++) {
103
-
104
- var item = comp.components[i];
249
+ // bind children components
250
+ const recursive = (comp) => {
251
+ if (this.isFullArr(comp.__OLUM__.components)) {
252
+ comp.__OLUM__.components.forEach((item) => {
105
253
  var key = Object.keys(item)[0];
106
- var obj = {};
107
- addProp(obj, "parent", comp.name);
108
- addProp(obj, "child", {});
109
- addProp(obj.child, "name", key);
110
- var instance = new item[key]();
111
- addProp(obj.child, "olumCompData", instance.olumCompData());
112
- compsArr.push(obj);
113
-
114
- if (obj.hasOwnProperty("child") && obj.child.hasOwnProperty("olumCompData")) {
115
- var nextEntry = obj.child.olumCompData;
116
- recursive(nextEntry);
117
- }
118
-
119
- }
120
-
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
+ });
121
262
  }
122
263
  };
123
264
  recursive(entry);
124
- return compsArr;
125
- }
126
265
 
127
- function buildStyles(css) {
128
- var id = "olum_style_tag";
129
- var styleTag = document.getElementById(id);
130
- if (!styleTag) {
131
- styleTag = document.createElement("style");
132
- styleTag.id = id;
133
- 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
+ });
134
343
  }
135
- styleTag.innerHTML = css + "\n [to] * {pointer-events: none;}";
136
- }
137
344
 
138
- function merge(tree) {
139
- var entry = tree.entry;
140
- var compsArr = tree.compsArr;
141
-
142
- // parent (view)
143
- var template = entry.template || "";
144
- var style = entry.style || "";
145
- var script = {};
146
- !!(entry.render) ? (script[0] = entry.render) : null;
147
-
148
- // children
149
- if (isFullArr(compsArr)) {
150
- for(var i = 0; i < compsArr.length; i++) {
151
- var olumCompData = compsArr[i].child.olumCompData;
152
- var html = olumCompData.template || "";
153
- var css = olumCompData.style || "";
154
- var js = olumCompData.render || null;
155
- var name = prefix ? prefix + "-" + olumCompData.name : olumCompData.name;
156
- var regex = new RegExp("<(" + name + "\\s{0,})\\/>", "gi"); // detect components e.g. <App-AddTodo /> or <AddTodo />
157
-
158
- template = template.replace(regex, html);
159
- style += css;
160
- if (js !== null) script[i + 1] = js;
161
- }
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
+ });
162
355
  }
163
356
 
164
- return {
165
- template,
166
- style,
167
- script,
168
- };
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;
169
379
  }
170
-
171
- function labelView(root, arr) {
172
- var compAttrRegex = /(olum-component=[\"\']([^\"|\']*)[\"\'])|(olum-component)/gi;
173
- var openingSelfClosingTagRegex = /<[a-z]+(>|.*?[^?]>)/gi;
174
- var greaterCharRegex = /\>/gi;
175
- var compsArr = Array.from(arr);
176
- var entry = root;
177
-
178
- // children
179
- for(var i = 0; i < compsArr.length; i++) {
180
- var name = compsArr[i].child.olumCompData.name || "undefined";
181
- if (compsArr[i].child && compsArr[i].child.olumCompData && compsArr[i].child.olumCompData.hasOwnProperty("template")) {
182
- var olumCompData = compsArr[i].child.olumCompData;
183
- // clean
184
- olumCompData.template = olumCompData.template.replace(compAttrRegex, "");
185
- // labeling
186
- var compWrapper = olumCompData.template.match(openingSelfClosingTagRegex);
187
- if (isFullArr(compWrapper)) {
188
- 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;
189
405
  }
190
406
  }
191
- }
407
+ });
408
+ }
192
409
 
193
- // parent (view)
194
- if (entry.template) {
195
- var name = entry.name || "undefined";
196
- // clean
197
- entry.template = entry.template.replace(compAttrRegex, "");
198
- // labeling
199
- var compWrapper = entry.template.match(openingSelfClosingTagRegex);
200
- if (isFullArr(compWrapper)) {
201
- entry.template = entry.template.replace(
202
- compWrapper[0],
203
- compWrapper[0].replace(greaterCharRegex, " olum-component='"+name+"' olum-view='"+name+"'>")
204
- );
205
- }
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;
206
419
  }
420
+ return path;
421
+ }
207
422
 
208
- // root (placeholder)
209
- rootElm.setAttribute("olum-component", rootCompName);
210
-
211
- return {
212
- entry,
213
- compsArr,
214
- };
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;
215
431
  }
216
432
 
217
- function useComponent() {
218
- debug("use component");
219
- var view = new which.cb();
220
- var entry = view.olumCompData();
221
- var compsArr = buildTree(entry);
222
- // labeling components
223
- var label = isDev() ? labelView(entry, compsArr) : { entry: entry, compsArr: compsArr };
224
- // final component (View)
225
- var viewObj = merge(label);
226
- // css
227
- buildStyles(viewObj.style);
228
- // html
229
- rootElm.innerHTML = viewObj.template;
230
- // js
231
- setTimeout(function () {
232
- for (var key in viewObj.script) {
233
- 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;
234
443
  }
235
- }, 0);
236
- }
237
-
238
- function useRouter() {
239
- debug("use OlumRouter");
240
- var router = which.cb;
241
- // share core functionalites with router
242
- // props
243
- router.__proto__.rootElm = rootElm;
244
- // methods
245
- router.__proto__.buildStyles = buildStyles;
246
- router.__proto__.buildTree = buildTree;
247
- router.__proto__.labelView = labelView;
248
- router.__proto__.merge = merge;
249
-
250
- if (router.isReady) router.listen();
251
- }
252
-
253
- function mount() {
254
- debug({mount:"mount()",rootElm, which});
255
-
256
- if (which && which.type && which.cb) {
257
- if (which.type === "router") useRouter();
258
- else if (which.type === "component") useComponent();
259
- } else {
260
- throw new Error(debugStr + " Can't mount, Missing component or router @use()");
444
+ return hash;
261
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
+ });
262
533
  }
263
534
 
264
- function init() {
265
- if (!rootElm) throw new Error(debugStr + " Root Element is not found in DOM!");
266
- 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 };
267
543
  }
268
-
269
544
  }
270
545
 
271
546
  return Olum;
272
- });
547
+ })();
package/dist/olum.min.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @name olum
3
- * @version 0.2.4
4
- * @copyright 2021
3
+ * @version 0.5.1
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 t(e){return null!==e&&"object"==typeof e}function f(e){return t(e)&&Array.isArray(e)&&e.length}function u(e,t,n){Object.defineProperty(e,t,{value:n,writable:!0,configurable:!0})}function d(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 a=[];return function e(t){if(t.hasOwnProperty("components")&&f(t.components))for(var n=0;n<t.components.length;n++){var o=t.components[n],r=Object.keys(o)[0],l={};u(l,"parent",t.name),u(l,"child",{}),u(l.child,"name",r),r=new o[r],u(l.child,"olumCompData",r.olumCompData()),a.push(l),l.hasOwnProperty("child")&&l.child.hasOwnProperty("olumCompData")&&e(l.child.olumCompData)}}(e),a}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(d({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(){d("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(){d("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.__olumHtml?e.html:String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")},html(e){return{__olumHtml:!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.4",
3
+ "version": "0.5.1",
4
4
  "description": "The VanillaJS developer’s platform.",
5
5
  "main": "dist/olum.js",
6
6
  "directories": {