micra.js 2.2.0 → 2.3.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/dist/micra.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.2.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.3.0 — https://github.com/micra-js/micra — MIT */
2
2
  "use strict";
3
3
  var Micra = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -176,27 +176,32 @@ var Micra = (() => {
176
176
  return false;
177
177
  }
178
178
  function evalExpr(expr, state) {
179
- if (SIMPLE_PATH.test(expr)) {
180
- const parts = expr.split(".");
181
- if (!safeStateHas(state, parts[0])) return void 0;
182
- return parts.reduce(
179
+ let cached = exprCache.get(expr);
180
+ if (!cached) {
181
+ if (SIMPLE_PATH.test(expr)) {
182
+ cached = { kind: "path", parts: expr.split(".") };
183
+ } else {
184
+ try {
185
+ cached = {
186
+ kind: "fn",
187
+ fn: new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
188
+ };
189
+ } catch {
190
+ warn(`invalid expression "${expr}"`);
191
+ cached = { kind: "fn", fn: () => void 0 };
192
+ }
193
+ }
194
+ exprCache.set(expr, cached);
195
+ }
196
+ if (cached.kind === "path") {
197
+ if (!safeStateHas(state, cached.parts[0])) return void 0;
198
+ return cached.parts.reduce(
183
199
  (obj, key) => obj != null ? obj[key] : void 0,
184
200
  state
185
201
  );
186
202
  }
187
- if (!exprCache.has(expr)) {
188
- try {
189
- exprCache.set(
190
- expr,
191
- new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
192
- );
193
- } catch {
194
- warn(`invalid expression "${expr}"`);
195
- exprCache.set(expr, () => void 0);
196
- }
197
- }
198
203
  try {
199
- return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
204
+ return cached.fn(safeStateWrap(state), SAFE_OUTER);
200
205
  } catch (e) {
201
206
  if (!warnedRuntime.has(expr)) {
202
207
  warnedRuntime.add(expr);
@@ -222,8 +227,9 @@ var Micra = (() => {
222
227
  set.delete(handler);
223
228
  if (set.size === 0) _bus.delete(event);
224
229
  }
225
- function emit(event, payload) {
230
+ function emit(event, ...args) {
226
231
  var _a;
232
+ const payload = args[0];
227
233
  (_a = _bus.get(event)) == null ? void 0 : _a.forEach((h) => {
228
234
  try {
229
235
  h(payload);
@@ -234,11 +240,12 @@ var Micra = (() => {
234
240
  }
235
241
 
236
242
  // src/core/reactive.ts
237
- function createReactiveState(obj, schedule) {
243
+ function createReactiveState(obj, schedule, onKey) {
238
244
  return new Proxy(obj, {
239
245
  set(target, key, value) {
240
246
  ;
241
247
  target[key] = value;
248
+ onKey == null ? void 0 : onKey(key);
242
249
  schedule();
243
250
  return true;
244
251
  }
@@ -264,7 +271,8 @@ var Micra = (() => {
264
271
  }
265
272
  function applyHtml(el, expr, state) {
266
273
  var _a;
267
- el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
274
+ const html = String((_a = evalExpr(expr, state)) != null ? _a : "");
275
+ if (el.innerHTML !== html) el.innerHTML = html;
268
276
  }
269
277
  function applyIf(binding, state) {
270
278
  const el = binding.el;
@@ -281,7 +289,9 @@ var Micra = (() => {
281
289
  }
282
290
  }
283
291
  function applyShow(el, expr, state) {
284
- el.style.display = evalExpr(expr, state) ? "" : "none";
292
+ const desired = evalExpr(expr, state) ? "" : "none";
293
+ const htmlEl = el;
294
+ if (htmlEl.style.display !== desired) htmlEl.style.display = desired;
285
295
  }
286
296
  function applyBind(el, pairs, state) {
287
297
  for (const [attr, valExpr] of pairs) {
@@ -526,23 +536,9 @@ var Micra = (() => {
526
536
  }
527
537
  return scan;
528
538
  }
529
- function scanFragment(frag) {
530
- const scan = emptyScan();
531
- const walker = document.createTreeWalker(
532
- frag,
533
- NodeFilter.SHOW_ELEMENT,
534
- NESTED_COMPONENT_FILTER
535
- );
536
- let node = walker.nextNode();
537
- while (node) {
538
- classify(node, scan);
539
- node = walker.nextNode();
540
- }
541
- return scan;
542
- }
543
539
 
544
540
  // src/dom/each.ts
545
- function renderList(templates, state, rawState, instance) {
541
+ function renderList(templates, state, rawState, instance, triggerKey) {
546
542
  var _a;
547
543
  for (const tmplEl of templates) {
548
544
  if (tmplEl.tagName !== "TEMPLATE") continue;
@@ -559,22 +555,40 @@ var Micra = (() => {
559
555
  }
560
556
  const marker = tmpl.__micraMarker;
561
557
  const keyMap = tmpl.__micraNodes;
562
- const parent = marker.parentNode;
563
- if (!parent) continue;
558
+ if (!marker.parentNode) continue;
564
559
  if (!Array.isArray(items)) {
565
560
  tmpl.__micraList.forEach((n) => n.remove());
566
561
  tmpl.__micraList = [];
567
562
  keyMap.clear();
568
563
  continue;
569
564
  }
565
+ const canSkipUnchanged = triggerKey !== null && triggerKey !== "MULTIPLE" && triggerKey === itemsExpr;
570
566
  if (keyAttr) {
571
- renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
567
+ renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
572
568
  } else {
573
- renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
569
+ renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged);
574
570
  }
575
571
  }
576
572
  }
577
- function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
573
+ function createRowNode(tmpl, state, instance) {
574
+ const frag = tmpl.content.cloneNode(true);
575
+ let node;
576
+ if (frag.childNodes.length === 1) {
577
+ node = frag.firstElementChild;
578
+ } else {
579
+ node = document.createElement("micra-each-item");
580
+ node.style.display = "contents";
581
+ node.append(frag);
582
+ }
583
+ const rowScan = scanComponent(node);
584
+ node.__micraScan = rowScan;
585
+ node._itemState = Object.create(state);
586
+ bindDataOn(rowScan.on, instance);
587
+ bindAtEvents(rowScan.atEvents, instance);
588
+ bindModels(rowScan.model, instance);
589
+ return node;
590
+ }
591
+ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
578
592
  var _a;
579
593
  const nextKeys = /* @__PURE__ */ new Set();
580
594
  const nextNodes = [];
@@ -593,26 +607,19 @@ var Micra = (() => {
593
607
  nextKeys.add(key);
594
608
  let node = keyMap.get(key);
595
609
  if (!node) {
596
- const frag = tmpl.content.cloneNode(true);
597
- if (frag.childNodes.length === 1) {
598
- node = frag.firstElementChild;
599
- } else {
600
- node = document.createElement("micra-each-item");
601
- node.style.display = "contents";
602
- node.append(frag);
603
- }
610
+ node = createRowNode(tmpl, state, instance);
604
611
  node.__micraKey = key;
605
612
  keyMap.set(key, node);
606
- const rowScan2 = scanComponent(node);
607
- node.__micraScan = rowScan2;
608
- bindDataOn(rowScan2.on, instance);
609
- bindAtEvents(rowScan2.atEvents, instance);
610
- bindModels(rowScan2.model, instance);
613
+ } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
614
+ nextNodes.push(node);
615
+ continue;
611
616
  }
612
- const itemState = Object.assign(
613
- Object.create(state),
614
- { item, index, $index: index }
615
- );
617
+ node.__micraItem = item;
618
+ node.__micraIndex = index;
619
+ const itemState = node._itemState;
620
+ itemState.item = item;
621
+ itemState.index = index;
622
+ itemState.$index = index;
616
623
  const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
617
624
  applyDirectives(rowScan, itemState, rawState, instance);
618
625
  nextNodes.push(node);
@@ -623,40 +630,113 @@ var Micra = (() => {
623
630
  keyMap.delete(key);
624
631
  }
625
632
  }
626
- let cursor = marker;
627
- for (const node of nextNodes) {
628
- if (cursor.nextSibling !== node) parent.insertBefore(node, cursor.nextSibling);
629
- cursor = node;
633
+ const prevList = tmpl.__micraList;
634
+ if (prevList.length === 0) {
635
+ if (nextNodes.length) {
636
+ const frag = document.createDocumentFragment();
637
+ for (const node of nextNodes) frag.append(node);
638
+ marker.after(frag);
639
+ }
640
+ } else {
641
+ let orderChanged = nextNodes.length !== prevList.length;
642
+ if (!orderChanged) {
643
+ for (let i = 0; i < nextNodes.length; i++) {
644
+ if (nextNodes[i] !== prevList[i]) {
645
+ orderChanged = true;
646
+ break;
647
+ }
648
+ }
649
+ }
650
+ if (orderChanged) reorderKeyed(nextNodes, prevList, marker);
630
651
  }
631
652
  tmpl.__micraList = nextNodes;
632
653
  }
633
- function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
634
- tmpl.__micraList.forEach((n) => n.remove());
635
- tmpl.__micraList = [];
636
- const frag = document.createDocumentFragment();
637
- for (const [index, item] of items.entries()) {
638
- const clone = tmpl.content.cloneNode(true);
639
- const itemState = Object.assign(
640
- Object.create(state),
641
- { item, index, $index: index }
642
- );
643
- const fragScan = scanFragment(clone);
644
- applyDirectives(fragScan, itemState, rawState, instance);
645
- bindDataOn(fragScan.on, instance);
646
- bindAtEvents(fragScan.atEvents, instance);
647
- bindModels(fragScan.model, instance);
648
- const nodes = Array.from(clone.childNodes);
649
- nodes.forEach((n) => {
650
- n.__micraEach = true;
651
- frag.append(n);
652
- });
653
- tmpl.__micraList.push(...nodes);
654
+ function reorderKeyed(nextNodes, prevList, marker) {
655
+ const prevPos = /* @__PURE__ */ new Map();
656
+ for (let i = 0; i < prevList.length; i++) prevPos.set(prevList[i], i);
657
+ const n = nextNodes.length;
658
+ const tails = [];
659
+ const tailIdx = [];
660
+ const prev = new Array(n).fill(-1);
661
+ for (let i = 0; i < n; i++) {
662
+ const p = prevPos.get(nextNodes[i]);
663
+ if (p === void 0) continue;
664
+ let lo = 0, hi = tails.length;
665
+ while (lo < hi) {
666
+ const m = lo + hi >> 1;
667
+ tails[m] < p ? lo = m + 1 : hi = m;
668
+ }
669
+ if (lo > 0) prev[i] = tailIdx[lo - 1];
670
+ tails[lo] = p;
671
+ tailIdx[lo] = i;
672
+ }
673
+ const stable = /* @__PURE__ */ new Set();
674
+ let idx = tailIdx[tails.length - 1];
675
+ while (idx >= 0) {
676
+ stable.add(idx);
677
+ idx = prev[idx];
678
+ }
679
+ let anchor = marker;
680
+ for (let i = 0; i < n; i++) {
681
+ const node = nextNodes[i];
682
+ if (stable.has(i)) {
683
+ anchor = node;
684
+ continue;
685
+ }
686
+ anchor.after(node);
687
+ anchor = node;
688
+ }
689
+ }
690
+ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged) {
691
+ const prevList = tmpl.__micraList;
692
+ const prevLen = prevList.length;
693
+ const nextLen = items.length;
694
+ const reuseLen = nextLen < prevLen ? nextLen : prevLen;
695
+ const nextList = new Array(nextLen);
696
+ for (let i = 0; i < reuseLen; i++) {
697
+ const node = prevList[i];
698
+ const item = items[i];
699
+ if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === i) {
700
+ nextList[i] = node;
701
+ continue;
702
+ }
703
+ node.__micraItem = item;
704
+ node.__micraIndex = i;
705
+ const itemState = node._itemState;
706
+ itemState.item = item;
707
+ itemState.index = i;
708
+ itemState.$index = i;
709
+ applyDirectives(node.__micraScan, itemState, rawState, instance);
710
+ nextList[i] = node;
711
+ }
712
+ for (let i = nextLen; i < prevLen; i++) {
713
+ prevList[i].remove();
714
+ }
715
+ if (nextLen > prevLen) {
716
+ const frag = document.createDocumentFragment();
717
+ for (let i = prevLen; i < nextLen; i++) {
718
+ const node = createRowNode(tmpl, state, instance);
719
+ const item = items[i];
720
+ const itemState = node._itemState;
721
+ itemState.item = item;
722
+ itemState.index = i;
723
+ itemState.$index = i;
724
+ node.__micraEach = true;
725
+ node.__micraItem = item;
726
+ node.__micraIndex = i;
727
+ applyDirectives(node.__micraScan, itemState, rawState, instance);
728
+ nextList[i] = node;
729
+ frag.append(node);
730
+ }
731
+ const anchor = prevLen > 0 ? nextList[prevLen - 1] : marker;
732
+ anchor.after(frag);
654
733
  }
655
- parent.insertBefore(frag, marker.nextSibling);
734
+ tmpl.__micraList = nextList;
656
735
  }
657
736
 
658
737
  // src/dom/refs.ts
659
738
  function collectRefs(els, instance) {
739
+ if (!els.length) return;
660
740
  instance.refs = {};
661
741
  for (const el of els) {
662
742
  const name = el.dataset["ref"];
@@ -699,8 +779,12 @@ var Micra = (() => {
699
779
  return unsub;
700
780
  };
701
781
  let isRendering = false;
782
+ let _triggerKey = null;
702
783
  const schedule = createScheduler(() => instance.render());
703
- instance.state = createReactiveState(rawState, schedule);
784
+ instance.state = createReactiveState(rawState, schedule, (key) => {
785
+ if (_triggerKey === null) _triggerKey = key;
786
+ else if (_triggerKey !== key) _triggerKey = "MULTIPLE";
787
+ });
704
788
  const boundMethods = /* @__PURE__ */ new Map();
705
789
  const exprState = new Proxy(rawState, {
706
790
  get(target, key) {
@@ -724,6 +808,8 @@ var Micra = (() => {
724
808
  instance.render = function() {
725
809
  var _a2;
726
810
  if (instance.__micraDestroyed) return;
811
+ const triggerKey = _triggerKey;
812
+ _triggerKey = null;
727
813
  if (isRendering) {
728
814
  if (!warnedReentry) {
729
815
  warn(
@@ -738,7 +824,7 @@ var Micra = (() => {
738
824
  const mRoot2 = root;
739
825
  const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
740
826
  applyDirectives(scan, exprState, rawState, instance);
741
- renderList(scan.each, exprState, rawState, instance);
827
+ renderList(scan.each, exprState, rawState, instance, triggerKey);
742
828
  bindDataOn(scan.on, instance);
743
829
  bindAtEvents(scan.atEvents, instance);
744
830
  bindModels(scan.model, instance);