@lmjs/core 2.5.4 → 2.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lumenjs-core.js +4 -4
- package/package.json +1 -1
- package/src/_re.js +158 -5
- package/src/lstnrs.js +5 -1
- package/vendor/reconnecting-websocket.js +51 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmjs/core",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.5",
|
|
4
4
|
"description": "LumenJS reactive core runtime — the reactive engine, HTML/JS AST compiler, realtime socket client, and delegated event listeners powering LumenJS applications.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
package/src/_re.js
CHANGED
|
@@ -668,6 +668,14 @@ function _evalEventScopedExpr(ownerRe, expr, locals) {
|
|
|
668
668
|
}
|
|
669
669
|
return undefined;
|
|
670
670
|
}
|
|
671
|
+
// `ev` and `el` are the handler's documented locals. The others evalEvAttr passes (`n`, the event's name, and
|
|
672
|
+
// `arg1`..`arg3`) are internal, so a page or loop variable with that name wins: `:for="names as n"` with
|
|
673
|
+
// `@click="n = n + '!'"` used to read and write the event name instead of the row's `n` (2026-10-08).
|
|
674
|
+
function isLocal(key) {
|
|
675
|
+
if (!locals.hasOwnProperty(key)) return false;
|
|
676
|
+
if (key === 'ev' || key === 'el' || key.indexOf('__lm') === 0 || !hasOwner) return true;
|
|
677
|
+
return !(levelOf(key) || ownFn(key));
|
|
678
|
+
}
|
|
671
679
|
let scopeProxy = new Proxy({}, {
|
|
672
680
|
has(target, key) {
|
|
673
681
|
if (typeof key === 'symbol') return false;
|
|
@@ -677,7 +685,7 @@ function _evalEventScopedExpr(ownerRe, expr, locals) {
|
|
|
677
685
|
return !!levelOf(key) || widgetsVars.hasOwnProperty(key) || _vt.Global.vars.hasOwnProperty(key);
|
|
678
686
|
},
|
|
679
687
|
get(target, key) {
|
|
680
|
-
if (
|
|
688
|
+
if (isLocal(key)) return locals[key];
|
|
681
689
|
const f = ownFn(key);
|
|
682
690
|
if (f) return f;
|
|
683
691
|
const lv = levelOf(key);
|
|
@@ -688,7 +696,7 @@ function _evalEventScopedExpr(ownerRe, expr, locals) {
|
|
|
688
696
|
return _vt.Global.vars[key];
|
|
689
697
|
},
|
|
690
698
|
set(target, key, value) {
|
|
691
|
-
if (
|
|
699
|
+
if (isLocal(key)) { locals[key] = value; return true; }
|
|
692
700
|
try {
|
|
693
701
|
const lv = levelOf(key);
|
|
694
702
|
if (lv) {
|
|
@@ -711,7 +719,112 @@ function _evalEventScopedExpr(ownerRe, expr, locals) {
|
|
|
711
719
|
function _runWithScope(expr) {
|
|
712
720
|
with (scopeProxy) { return eval(expr); }
|
|
713
721
|
}
|
|
714
|
-
return _runWithScope(expr);
|
|
722
|
+
if (locals.__lmNoWriteBack) return _runWithScope(expr);
|
|
723
|
+
let rows = _lmRowSnapshots(levels);
|
|
724
|
+
let result = _runWithScope(expr);
|
|
725
|
+
if (rows.length) {
|
|
726
|
+
_lmRowWriteBack(rows);
|
|
727
|
+
// `@click="save(t)"` with an async save(): changes made after its first await are saved when it settles.
|
|
728
|
+
if (result && typeof result.then === 'function') result.then(function () { _lmRowWriteBack(rows); }, function () { _lmRowWriteBack(rows); });
|
|
729
|
+
}
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// 2026-10-08: a :for row's variables are copies of the list's items (the loop renders from a clone, which its
|
|
734
|
+
// diff needs). `@click="t.done = !t.done"`, `@click="toggle(t)"` or, in a bare `:for="todos"`, `done = !done` used
|
|
735
|
+
// to change only that copy, so nothing happened. V1 behaved the same; V2 now saves the change into the real item,
|
|
736
|
+
// as a Vue developer would expect: after a row's handler runs, each property of the row's item (alias, or a spread
|
|
737
|
+
// key) that changed is written to `<list>[index].<prop>` in the scope that owns the list, which re-renders it.
|
|
738
|
+
// Only changed properties are written, so values a copy can't carry (Dates, class instances) stay intact. A list
|
|
739
|
+
// that isn't a plain variable path (`todos.filter(...)`, a number) can't be written to; a dev-mode warning says so.
|
|
740
|
+
var _lmRowPathRe = /^\s*[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*|\[\s*\d+\s*\])*\s*$/;
|
|
741
|
+
var _lmRowWarned = {};
|
|
742
|
+
function _lmRowJson(v) {
|
|
743
|
+
try { return JSON.stringify(v === undefined ? null : v); } catch (e) { return null; }
|
|
744
|
+
}
|
|
745
|
+
function _lmRowSnapshots(levels) {
|
|
746
|
+
let rows = [];
|
|
747
|
+
for (let i = 0; i < levels.length; i++) {
|
|
748
|
+
let re = levels[i].re, info = re && re._lmRow;
|
|
749
|
+
if (!info) continue;
|
|
750
|
+
let vars = levels[i].vars, forX = info.forX || {}, as = forX.as || {};
|
|
751
|
+
let names = [];
|
|
752
|
+
if (info.isObj) {
|
|
753
|
+
let vName = as.v ? as.v : (as.k ? '' : 'value');
|
|
754
|
+
if (vName) names.push({ name: vName, whole: true });
|
|
755
|
+
} else {
|
|
756
|
+
if (as.k) names.push({ name: as.k, whole: true });
|
|
757
|
+
// The item's own keys are spread into the row's scope as well.
|
|
758
|
+
let item = as.k ? vars[as.k] : null;
|
|
759
|
+
let src = item && typeof item === 'object' && !Array.isArray(item) ? item : null;
|
|
760
|
+
for (let k in vars) {
|
|
761
|
+
if (!Object.prototype.hasOwnProperty.call(vars, k) || k === as.k || k === forX.dx || k === 'index' || k === 'key' || k === 'value') continue;
|
|
762
|
+
if (src ? Object.prototype.hasOwnProperty.call(src, k) : !as.k) names.push({ name: k, whole: false });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
let snap = {};
|
|
766
|
+
for (let n = 0; n < names.length; n++) snap[names[n].name] = _lmRowJson(vars[names[n].name]);
|
|
767
|
+
rows.push({ re: re, vars: vars, info: info, names: names, snap: snap });
|
|
768
|
+
}
|
|
769
|
+
return rows;
|
|
770
|
+
}
|
|
771
|
+
function _lmRowWriteBack(rows) {
|
|
772
|
+
for (let r = 0; r < rows.length; r++) {
|
|
773
|
+
let row = rows[r], vars = row.vars, info = row.info, js = info.forX && info.forX.js;
|
|
774
|
+
let owner = row.re.view && row.re.view._pv;
|
|
775
|
+
let pos = info.isObj ? vars.key : vars.index;
|
|
776
|
+
for (let n = 0; n < row.names.length; n++) {
|
|
777
|
+
let name = row.names[n].name, now = _lmRowJson(vars[name]);
|
|
778
|
+
if (now === row.snap[name]) continue;
|
|
779
|
+
let before = row.snap[name];
|
|
780
|
+
row.snap[name] = now;
|
|
781
|
+
if (!owner || typeof js !== 'string' || !_lmRowPathRe.test(js) || pos === undefined) {
|
|
782
|
+
if (_lumenDevMode && !_lmRowWarned[js]) {
|
|
783
|
+
_lmRowWarned[js] = true;
|
|
784
|
+
console.warn('[LumenJS] A change to a row of :for="' + js + '" was not saved: the list is not a variable. ' +
|
|
785
|
+
'Loop over a variable, or change the item in the list itself (list[i].prop = value).');
|
|
786
|
+
}
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
let value = vars[name];
|
|
790
|
+
try {
|
|
791
|
+
if (!row.names[n].whole) {
|
|
792
|
+
_evalEventScopedExpr(owner, '(' + js + ')[__lmPos][__lmProp] = __lmVal', { __lmPos: pos, __lmProp: name, __lmVal: clone(value), __lmNoWriteBack: true });
|
|
793
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
794
|
+
let old = null;
|
|
795
|
+
try { old = JSON.parse(before); } catch (e) { old = null; }
|
|
796
|
+
if (!old || typeof old !== 'object' || Array.isArray(old)) {
|
|
797
|
+
_evalEventScopedExpr(owner, '(' + js + ')[__lmPos] = __lmVal', { __lmPos: pos, __lmVal: clone(value), __lmNoWriteBack: true });
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
for (let k in value) {
|
|
801
|
+
if (!Object.prototype.hasOwnProperty.call(value, k)) continue;
|
|
802
|
+
if (_lmRowJson(value[k]) === _lmRowJson(old[k])) continue;
|
|
803
|
+
_evalEventScopedExpr(owner, '(' + js + ')[__lmPos][__lmProp] = __lmVal', { __lmPos: pos, __lmProp: k, __lmVal: clone(value[k]), __lmNoWriteBack: true });
|
|
804
|
+
}
|
|
805
|
+
for (let k in old) {
|
|
806
|
+
if (Object.prototype.hasOwnProperty.call(old, k) && !Object.prototype.hasOwnProperty.call(value, k))
|
|
807
|
+
_evalEventScopedExpr(owner, 'delete (' + js + ')[__lmPos][__lmProp]', { __lmPos: pos, __lmProp: k, __lmNoWriteBack: true });
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
_evalEventScopedExpr(owner, '(' + js + ')[__lmPos] = __lmVal', { __lmPos: pos, __lmVal: clone(value), __lmNoWriteBack: true });
|
|
811
|
+
}
|
|
812
|
+
_lmRowTouch(owner, js);
|
|
813
|
+
} catch (e) { cl(e); }
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// A nested write is re-rendered by the reactive proxy when the list lives in a view; a list held by an enclosing
|
|
818
|
+
// :for row (a nested loop) is a plain copy, so that row is told to update.
|
|
819
|
+
function _lmRowTouch(owner, js) {
|
|
820
|
+
let root = /^\s*([A-Za-z_$][\w$]*)/.exec(js);
|
|
821
|
+
if (!root) return;
|
|
822
|
+
root = root[1];
|
|
823
|
+
for (let e = owner, n = 0; e && e.view && n < 50; e = e.view._pv, n++) {
|
|
824
|
+
if (!e.view.vars || !Object.prototype.hasOwnProperty.call(e.view.vars, root)) continue;
|
|
825
|
+
if (e._lmRow) { try { e.update(root); } catch (err) { cl(err); } }
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
715
828
|
}
|
|
716
829
|
|
|
717
830
|
// concatVarsAtLevel() below merges via object spread, not the loop-with-
|
|
@@ -2217,7 +2330,7 @@ class _lm {
|
|
|
2217
2330
|
// exists for the first time (unlike :for/:if's before-render,
|
|
2218
2331
|
// which runs against DOM that's already connected).
|
|
2219
2332
|
if (events && events['@after-render'] && !(attrs && (attrs.hasOwnProperty(':for') || attrs.hasOwnProperty(':if') || attrs.hasOwnProperty(':else-if') || attrs.hasOwnProperty(':else')))) {
|
|
2220
|
-
|
|
2333
|
+
_queueMountHook(doc, _el);
|
|
2221
2334
|
}
|
|
2222
2335
|
|
|
2223
2336
|
// 2026-09-16: V1's real automatic plugin-init pass, ported
|
|
@@ -2826,6 +2939,8 @@ class _lm {
|
|
|
2826
2939
|
// The row element was created by the loop's owner (above); its @-handlers must resolve against THIS row's
|
|
2827
2940
|
// engine (row alias, index) and fall back to the enclosing view through the engine chain.
|
|
2828
2941
|
if (el && scsc) Object.defineProperty(el, '_ownerRe', { value: scsc, enumerable: false, configurable: true, writable: true });
|
|
2942
|
+
// Which list this row comes from, so a handler's change to the row's copy is saved into it (_lmRowWriteBack).
|
|
2943
|
+
if (scsc && typeN === 'for' && forX) scsc._lmRow = { forX: forX, isObj: isObj };
|
|
2829
2944
|
// cl(23);
|
|
2830
2945
|
// cl(scsc);
|
|
2831
2946
|
scsc._re = scsc;
|
|
@@ -3060,6 +3175,44 @@ async function fireRenderHook(cx, n, containerEl, extra) {
|
|
|
3060
3175
|
return result;
|
|
3061
3176
|
}
|
|
3062
3177
|
|
|
3178
|
+
// 2026-10-08: a mount `@after-render` used to fire inside createEl(), while the element was still detached and
|
|
3179
|
+
// before the view's own <script> had run, so `@after-render="setup"` found no `setup` and was silently skipped (and
|
|
3180
|
+
// `el.offsetWidth`, a chart or a slider saw a detached node). It now fires once the element is in the document and,
|
|
3181
|
+
// for a plain function name, once that function exists: checked on the next tasks, for up to ~2s.
|
|
3182
|
+
var _pendingMountHooks = [];
|
|
3183
|
+
var _mountHookTimer = null;
|
|
3184
|
+
function _mountHookReady(doc, el) {
|
|
3185
|
+
if (!el.isConnected) return false;
|
|
3186
|
+
let v = doc && doc.evs && doc.evs['@after-render'];
|
|
3187
|
+
if (typeof v !== 'string' || !/^\s*[A-Za-z_$][\w$]*\s*$/.test(v)) return true;
|
|
3188
|
+
v = v.trim();
|
|
3189
|
+
if (typeof window[v] === 'function') return true;
|
|
3190
|
+
for (let e = el._ownerRe, n = 0; e && e.view && n < 50; e = e.view._pv, n++) {
|
|
3191
|
+
if (e.view.fns && typeof e.view.fns[v] === 'function') return true;
|
|
3192
|
+
}
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
function _queueMountHook(doc, el) {
|
|
3196
|
+
_pendingMountHooks.push([doc, el, 0]);
|
|
3197
|
+
if (!_mountHookTimer) _mountHookTimer = setTimeout(_flushMountHooks, 0);
|
|
3198
|
+
}
|
|
3199
|
+
function _flushMountHooks() {
|
|
3200
|
+
_mountHookTimer = null;
|
|
3201
|
+
let waiting = [];
|
|
3202
|
+
let due = _pendingMountHooks;
|
|
3203
|
+
_pendingMountHooks = [];
|
|
3204
|
+
for (let i = 0; i < due.length; i++) {
|
|
3205
|
+
let doc = due[i][0], el = due[i][1], tries = due[i][2];
|
|
3206
|
+
if (_mountHookReady(doc, el)) fireRenderHook({ doc: doc }, 'after-render', el, {});
|
|
3207
|
+
else if (tries < 40) waiting.push([doc, el, tries + 1]);
|
|
3208
|
+
// Still not ready after ~2s: connected means the name is not a function (fire, so the usual error shows);
|
|
3209
|
+
// detached means the element was thrown away before it was ever shown.
|
|
3210
|
+
else if (el.isConnected) fireRenderHook({ doc: doc }, 'after-render', el, {});
|
|
3211
|
+
}
|
|
3212
|
+
for (let i = 0; i < waiting.length; i++) _pendingMountHooks.push(waiting[i]);
|
|
3213
|
+
if (_pendingMountHooks.length && !_mountHookTimer) _mountHookTimer = setTimeout(_flushMountHooks, waiting.length === _pendingMountHooks.length ? 50 : 0);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3063
3216
|
// 2026-09-16: V1's real automatic plugin-init pass — [sl] (Select2),
|
|
3064
3217
|
// [color] (bootstrap-colorpicker), [time]/[date]/[datetime]
|
|
3065
3218
|
// (bootstrap-datetimepicker via dtp()) — ported faithfully from bea.js's
|
|
@@ -3509,7 +3662,7 @@ async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["V
|
|
|
3509
3662
|
let fileKey = btoa(filePath);
|
|
3510
3663
|
if (!isSub) {
|
|
3511
3664
|
View.props = d ?? {};
|
|
3512
|
-
var _queryParams = window.location.href.split("?");
|
|
3665
|
+
var _queryParams = window.location.href.split("#")[0].split("?");
|
|
3513
3666
|
var nn = _queryParams.shift();
|
|
3514
3667
|
View.params = paraToObj(_queryParams) ?? {};
|
|
3515
3668
|
}
|
package/src/lstnrs.js
CHANGED
|
@@ -1221,10 +1221,14 @@ $("body").on("click touch", "a,area", function (e) {
|
|
|
1221
1221
|
// alert(url);
|
|
1222
1222
|
|
|
1223
1223
|
if (local && !cntrlon) {
|
|
1224
|
+
// 2026-10-08: a link to a #fragment of the page already showing
|
|
1225
|
+
// (`href="#faq"`, or `/guide#faq` while on /guide) is left to the
|
|
1226
|
+
// browser: it scrolls, and the view is not re-rendered.
|
|
1227
|
+
if (l.hash && l.pathname == location.pathname && l.search == location.search) return;
|
|
1224
1228
|
e.preventDefault();
|
|
1225
1229
|
if (el.hasAttr("stop")) e.stopPropagation();
|
|
1226
1230
|
|
|
1227
|
-
pushURL(url + (l.search != "" ? l.search : "") ?? "home");
|
|
1231
|
+
pushURL(url + (l.search != "" ? l.search : "") + (l.hash || "") ?? "home");
|
|
1228
1232
|
} else {
|
|
1229
1233
|
}
|
|
1230
1234
|
}
|
|
@@ -552,6 +552,30 @@ var miniTB = [
|
|
|
552
552
|
// })();
|
|
553
553
|
|
|
554
554
|
var XHRs = new Array();
|
|
555
|
+
// 2026-10-08: where web storage is blocked (a sandboxed iframe such as lumenjs.com's playground or an embed,
|
|
556
|
+
// cookies disabled, some privacy modes) reading `localStorage` throws, and the line below used to stop the whole
|
|
557
|
+
// runtime before anything rendered. Fall back to in-memory storage for that page load instead: `globals` and the
|
|
558
|
+
// app's own storage calls keep working, they just aren't kept after a reload.
|
|
559
|
+
(function () {
|
|
560
|
+
function memoryStorage() {
|
|
561
|
+
var d = {};
|
|
562
|
+
return {
|
|
563
|
+
getItem: function (k) { return Object.prototype.hasOwnProperty.call(d, k) ? d[k] : null; },
|
|
564
|
+
setItem: function (k, v) { d[k] = String(v); },
|
|
565
|
+
removeItem: function (k) { delete d[k]; },
|
|
566
|
+
clear: function () { d = {}; },
|
|
567
|
+
key: function (i) { return Object.keys(d)[i] || null; },
|
|
568
|
+
get length() { return Object.keys(d).length; }
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
["localStorage", "sessionStorage"].forEach(function (name) {
|
|
572
|
+
try {
|
|
573
|
+
var s = window[name];
|
|
574
|
+
if (s) { s.getItem("globals"); return; }
|
|
575
|
+
} catch (e) { }
|
|
576
|
+
try { Object.defineProperty(window, name, { value: memoryStorage(), configurable: true }); } catch (e) { }
|
|
577
|
+
});
|
|
578
|
+
})();
|
|
555
579
|
if (localStorage.getItem("globals") === null)
|
|
556
580
|
localStorage.setItem("globals", JSON.stringify({}));
|
|
557
581
|
if (localStorage.getItem("_rx") === null)
|
|
@@ -1294,8 +1318,31 @@ function getFormData(form) {
|
|
|
1294
1318
|
return indexed_array;
|
|
1295
1319
|
}
|
|
1296
1320
|
|
|
1321
|
+
// 2026-10-08: a URL's #fragment is not part of the route. `/page#part` renders
|
|
1322
|
+
// `page`, and once it has rendered the browser is asked to scroll to `#part`.
|
|
1323
|
+
// A popstate that changes only the fragment (an in-page anchor, or Back to
|
|
1324
|
+
// one) leaves the view alone, as a normal web page would, instead of
|
|
1325
|
+
// re-rendering it from scratch. V1 read the fragment as part of the view name.
|
|
1326
|
+
var _lumenRouteKey = null;
|
|
1327
|
+
function _lumenScrollToHash() {
|
|
1328
|
+
var h = location.hash;
|
|
1329
|
+
if (!h || h.length < 2) return;
|
|
1330
|
+
var id;
|
|
1331
|
+
try { id = decodeURIComponent(h.slice(1)); } catch (e) { id = h.slice(1); }
|
|
1332
|
+
var tries = 0;
|
|
1333
|
+
(function go() {
|
|
1334
|
+
var t = document.getElementById(id) || document.getElementsByName(id)[0];
|
|
1335
|
+
if (t) t.scrollIntoView();
|
|
1336
|
+
else if (++tries < 40) setTimeout(go, 50);
|
|
1337
|
+
})();
|
|
1338
|
+
}
|
|
1297
1339
|
$(window).on("popstate", function () {
|
|
1298
|
-
if (history && history.pushState)
|
|
1340
|
+
if (!(history && history.pushState)) return;
|
|
1341
|
+
if (_lumenRouteKey !== null && location.pathname + location.search === _lumenRouteKey) {
|
|
1342
|
+
_lumenScrollToHash();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
goToNode();
|
|
1299
1346
|
});
|
|
1300
1347
|
|
|
1301
1348
|
var getLocation = function (href) {
|
|
@@ -1325,12 +1372,14 @@ function getURLNodes() {
|
|
|
1325
1372
|
.split(appSettings.Base)
|
|
1326
1373
|
.join("")
|
|
1327
1374
|
.replace(new RegExp("^[" + "/" + "]+"), "")
|
|
1375
|
+
.split("#")[0]
|
|
1328
1376
|
.split("?")[0]
|
|
1329
1377
|
.split("/");
|
|
1330
1378
|
|
|
1331
1379
|
// let authFunc = (appSettings.auth && typeof appSettings.auth === "function") ? appSettings.auth : (typeof Auth === "function" ? Auth : null);
|
|
1332
1380
|
// if (typeof authFunc === 'function') authFunc();
|
|
1333
1381
|
if (!_nodes[0]) _nodes[0] = appSettings.defaultView ?? "home";
|
|
1382
|
+
_lumenRouteKey = location.pathname + location.search;
|
|
1334
1383
|
|
|
1335
1384
|
if (historyStack[historyStack.length - 1]?.url != window.location.href)
|
|
1336
1385
|
historyStack.push({
|
|
@@ -1346,7 +1395,7 @@ function goToNode(d) {
|
|
|
1346
1395
|
if (_initialized) {
|
|
1347
1396
|
prevNodes = clone(nodes);
|
|
1348
1397
|
nodes = getURLNodes();
|
|
1349
|
-
renderView(nodes[0], null, d);
|
|
1398
|
+
Promise.resolve(renderView(nodes[0], null, d)).then(_lumenScrollToHash);
|
|
1350
1399
|
} else {
|
|
1351
1400
|
setTimeout(function () {
|
|
1352
1401
|
goToNode(d);
|