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.esm.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
 
3
3
  // src/utils/fetch.ts
4
4
  function getCSRF() {
@@ -141,27 +141,32 @@ function safeStateHas(state, key) {
141
141
  return false;
142
142
  }
143
143
  function evalExpr(expr, state) {
144
- if (SIMPLE_PATH.test(expr)) {
145
- const parts = expr.split(".");
146
- if (!safeStateHas(state, parts[0])) return void 0;
147
- return parts.reduce(
144
+ let cached = exprCache.get(expr);
145
+ if (!cached) {
146
+ if (SIMPLE_PATH.test(expr)) {
147
+ cached = { kind: "path", parts: expr.split(".") };
148
+ } else {
149
+ try {
150
+ cached = {
151
+ kind: "fn",
152
+ fn: new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
153
+ };
154
+ } catch {
155
+ warn(`invalid expression "${expr}"`);
156
+ cached = { kind: "fn", fn: () => void 0 };
157
+ }
158
+ }
159
+ exprCache.set(expr, cached);
160
+ }
161
+ if (cached.kind === "path") {
162
+ if (!safeStateHas(state, cached.parts[0])) return void 0;
163
+ return cached.parts.reduce(
148
164
  (obj, key) => obj != null ? obj[key] : void 0,
149
165
  state
150
166
  );
151
167
  }
152
- if (!exprCache.has(expr)) {
153
- try {
154
- exprCache.set(
155
- expr,
156
- new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
157
- );
158
- } catch {
159
- warn(`invalid expression "${expr}"`);
160
- exprCache.set(expr, () => void 0);
161
- }
162
- }
163
168
  try {
164
- return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
169
+ return cached.fn(safeStateWrap(state), SAFE_OUTER);
165
170
  } catch (e) {
166
171
  if (!warnedRuntime.has(expr)) {
167
172
  warnedRuntime.add(expr);
@@ -187,8 +192,9 @@ function off(event, handler) {
187
192
  set.delete(handler);
188
193
  if (set.size === 0) _bus.delete(event);
189
194
  }
190
- function emit(event, payload) {
195
+ function emit(event, ...args) {
191
196
  var _a;
197
+ const payload = args[0];
192
198
  (_a = _bus.get(event)) == null ? void 0 : _a.forEach((h) => {
193
199
  try {
194
200
  h(payload);
@@ -199,11 +205,12 @@ function emit(event, payload) {
199
205
  }
200
206
 
201
207
  // src/core/reactive.ts
202
- function createReactiveState(obj, schedule) {
208
+ function createReactiveState(obj, schedule, onKey) {
203
209
  return new Proxy(obj, {
204
210
  set(target, key, value) {
205
211
  ;
206
212
  target[key] = value;
213
+ onKey == null ? void 0 : onKey(key);
207
214
  schedule();
208
215
  return true;
209
216
  }
@@ -229,7 +236,8 @@ function applyText(el, expr, state) {
229
236
  }
230
237
  function applyHtml(el, expr, state) {
231
238
  var _a;
232
- el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
239
+ const html = String((_a = evalExpr(expr, state)) != null ? _a : "");
240
+ if (el.innerHTML !== html) el.innerHTML = html;
233
241
  }
234
242
  function applyIf(binding, state) {
235
243
  const el = binding.el;
@@ -246,7 +254,9 @@ function applyIf(binding, state) {
246
254
  }
247
255
  }
248
256
  function applyShow(el, expr, state) {
249
- el.style.display = evalExpr(expr, state) ? "" : "none";
257
+ const desired = evalExpr(expr, state) ? "" : "none";
258
+ const htmlEl = el;
259
+ if (htmlEl.style.display !== desired) htmlEl.style.display = desired;
250
260
  }
251
261
  function applyBind(el, pairs, state) {
252
262
  for (const [attr, valExpr] of pairs) {
@@ -491,23 +501,9 @@ function scanComponent(root) {
491
501
  }
492
502
  return scan;
493
503
  }
494
- function scanFragment(frag) {
495
- const scan = emptyScan();
496
- const walker = document.createTreeWalker(
497
- frag,
498
- NodeFilter.SHOW_ELEMENT,
499
- NESTED_COMPONENT_FILTER
500
- );
501
- let node = walker.nextNode();
502
- while (node) {
503
- classify(node, scan);
504
- node = walker.nextNode();
505
- }
506
- return scan;
507
- }
508
504
 
509
505
  // src/dom/each.ts
510
- function renderList(templates, state, rawState, instance) {
506
+ function renderList(templates, state, rawState, instance, triggerKey) {
511
507
  var _a;
512
508
  for (const tmplEl of templates) {
513
509
  if (tmplEl.tagName !== "TEMPLATE") continue;
@@ -524,22 +520,40 @@ function renderList(templates, state, rawState, instance) {
524
520
  }
525
521
  const marker = tmpl.__micraMarker;
526
522
  const keyMap = tmpl.__micraNodes;
527
- const parent = marker.parentNode;
528
- if (!parent) continue;
523
+ if (!marker.parentNode) continue;
529
524
  if (!Array.isArray(items)) {
530
525
  tmpl.__micraList.forEach((n) => n.remove());
531
526
  tmpl.__micraList = [];
532
527
  keyMap.clear();
533
528
  continue;
534
529
  }
530
+ const canSkipUnchanged = triggerKey !== null && triggerKey !== "MULTIPLE" && triggerKey === itemsExpr;
535
531
  if (keyAttr) {
536
- renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
532
+ renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
537
533
  } else {
538
- renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
534
+ renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged);
539
535
  }
540
536
  }
541
537
  }
542
- function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
538
+ function createRowNode(tmpl, state, instance) {
539
+ const frag = tmpl.content.cloneNode(true);
540
+ let node;
541
+ if (frag.childNodes.length === 1) {
542
+ node = frag.firstElementChild;
543
+ } else {
544
+ node = document.createElement("micra-each-item");
545
+ node.style.display = "contents";
546
+ node.append(frag);
547
+ }
548
+ const rowScan = scanComponent(node);
549
+ node.__micraScan = rowScan;
550
+ node._itemState = Object.create(state);
551
+ bindDataOn(rowScan.on, instance);
552
+ bindAtEvents(rowScan.atEvents, instance);
553
+ bindModels(rowScan.model, instance);
554
+ return node;
555
+ }
556
+ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
543
557
  var _a;
544
558
  const nextKeys = /* @__PURE__ */ new Set();
545
559
  const nextNodes = [];
@@ -558,26 +572,19 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
558
572
  nextKeys.add(key);
559
573
  let node = keyMap.get(key);
560
574
  if (!node) {
561
- const frag = tmpl.content.cloneNode(true);
562
- if (frag.childNodes.length === 1) {
563
- node = frag.firstElementChild;
564
- } else {
565
- node = document.createElement("micra-each-item");
566
- node.style.display = "contents";
567
- node.append(frag);
568
- }
575
+ node = createRowNode(tmpl, state, instance);
569
576
  node.__micraKey = key;
570
577
  keyMap.set(key, node);
571
- const rowScan2 = scanComponent(node);
572
- node.__micraScan = rowScan2;
573
- bindDataOn(rowScan2.on, instance);
574
- bindAtEvents(rowScan2.atEvents, instance);
575
- bindModels(rowScan2.model, instance);
578
+ } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
579
+ nextNodes.push(node);
580
+ continue;
576
581
  }
577
- const itemState = Object.assign(
578
- Object.create(state),
579
- { item, index, $index: index }
580
- );
582
+ node.__micraItem = item;
583
+ node.__micraIndex = index;
584
+ const itemState = node._itemState;
585
+ itemState.item = item;
586
+ itemState.index = index;
587
+ itemState.$index = index;
581
588
  const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
582
589
  applyDirectives(rowScan, itemState, rawState, instance);
583
590
  nextNodes.push(node);
@@ -588,40 +595,113 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
588
595
  keyMap.delete(key);
589
596
  }
590
597
  }
591
- let cursor = marker;
592
- for (const node of nextNodes) {
593
- if (cursor.nextSibling !== node) parent.insertBefore(node, cursor.nextSibling);
594
- cursor = node;
598
+ const prevList = tmpl.__micraList;
599
+ if (prevList.length === 0) {
600
+ if (nextNodes.length) {
601
+ const frag = document.createDocumentFragment();
602
+ for (const node of nextNodes) frag.append(node);
603
+ marker.after(frag);
604
+ }
605
+ } else {
606
+ let orderChanged = nextNodes.length !== prevList.length;
607
+ if (!orderChanged) {
608
+ for (let i = 0; i < nextNodes.length; i++) {
609
+ if (nextNodes[i] !== prevList[i]) {
610
+ orderChanged = true;
611
+ break;
612
+ }
613
+ }
614
+ }
615
+ if (orderChanged) reorderKeyed(nextNodes, prevList, marker);
595
616
  }
596
617
  tmpl.__micraList = nextNodes;
597
618
  }
598
- function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
599
- tmpl.__micraList.forEach((n) => n.remove());
600
- tmpl.__micraList = [];
601
- const frag = document.createDocumentFragment();
602
- for (const [index, item] of items.entries()) {
603
- const clone = tmpl.content.cloneNode(true);
604
- const itemState = Object.assign(
605
- Object.create(state),
606
- { item, index, $index: index }
607
- );
608
- const fragScan = scanFragment(clone);
609
- applyDirectives(fragScan, itemState, rawState, instance);
610
- bindDataOn(fragScan.on, instance);
611
- bindAtEvents(fragScan.atEvents, instance);
612
- bindModels(fragScan.model, instance);
613
- const nodes = Array.from(clone.childNodes);
614
- nodes.forEach((n) => {
615
- n.__micraEach = true;
616
- frag.append(n);
617
- });
618
- tmpl.__micraList.push(...nodes);
619
+ function reorderKeyed(nextNodes, prevList, marker) {
620
+ const prevPos = /* @__PURE__ */ new Map();
621
+ for (let i = 0; i < prevList.length; i++) prevPos.set(prevList[i], i);
622
+ const n = nextNodes.length;
623
+ const tails = [];
624
+ const tailIdx = [];
625
+ const prev = new Array(n).fill(-1);
626
+ for (let i = 0; i < n; i++) {
627
+ const p = prevPos.get(nextNodes[i]);
628
+ if (p === void 0) continue;
629
+ let lo = 0, hi = tails.length;
630
+ while (lo < hi) {
631
+ const m = lo + hi >> 1;
632
+ tails[m] < p ? lo = m + 1 : hi = m;
633
+ }
634
+ if (lo > 0) prev[i] = tailIdx[lo - 1];
635
+ tails[lo] = p;
636
+ tailIdx[lo] = i;
619
637
  }
620
- parent.insertBefore(frag, marker.nextSibling);
638
+ const stable = /* @__PURE__ */ new Set();
639
+ let idx = tailIdx[tails.length - 1];
640
+ while (idx >= 0) {
641
+ stable.add(idx);
642
+ idx = prev[idx];
643
+ }
644
+ let anchor = marker;
645
+ for (let i = 0; i < n; i++) {
646
+ const node = nextNodes[i];
647
+ if (stable.has(i)) {
648
+ anchor = node;
649
+ continue;
650
+ }
651
+ anchor.after(node);
652
+ anchor = node;
653
+ }
654
+ }
655
+ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged) {
656
+ const prevList = tmpl.__micraList;
657
+ const prevLen = prevList.length;
658
+ const nextLen = items.length;
659
+ const reuseLen = nextLen < prevLen ? nextLen : prevLen;
660
+ const nextList = new Array(nextLen);
661
+ for (let i = 0; i < reuseLen; i++) {
662
+ const node = prevList[i];
663
+ const item = items[i];
664
+ if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === i) {
665
+ nextList[i] = node;
666
+ continue;
667
+ }
668
+ node.__micraItem = item;
669
+ node.__micraIndex = i;
670
+ const itemState = node._itemState;
671
+ itemState.item = item;
672
+ itemState.index = i;
673
+ itemState.$index = i;
674
+ applyDirectives(node.__micraScan, itemState, rawState, instance);
675
+ nextList[i] = node;
676
+ }
677
+ for (let i = nextLen; i < prevLen; i++) {
678
+ prevList[i].remove();
679
+ }
680
+ if (nextLen > prevLen) {
681
+ const frag = document.createDocumentFragment();
682
+ for (let i = prevLen; i < nextLen; i++) {
683
+ const node = createRowNode(tmpl, state, instance);
684
+ const item = items[i];
685
+ const itemState = node._itemState;
686
+ itemState.item = item;
687
+ itemState.index = i;
688
+ itemState.$index = i;
689
+ node.__micraEach = true;
690
+ node.__micraItem = item;
691
+ node.__micraIndex = i;
692
+ applyDirectives(node.__micraScan, itemState, rawState, instance);
693
+ nextList[i] = node;
694
+ frag.append(node);
695
+ }
696
+ const anchor = prevLen > 0 ? nextList[prevLen - 1] : marker;
697
+ anchor.after(frag);
698
+ }
699
+ tmpl.__micraList = nextList;
621
700
  }
622
701
 
623
702
  // src/dom/refs.ts
624
703
  function collectRefs(els, instance) {
704
+ if (!els.length) return;
625
705
  instance.refs = {};
626
706
  for (const el of els) {
627
707
  const name = el.dataset["ref"];
@@ -664,8 +744,12 @@ function mount(selector, definition) {
664
744
  return unsub;
665
745
  };
666
746
  let isRendering = false;
747
+ let _triggerKey = null;
667
748
  const schedule = createScheduler(() => instance.render());
668
- instance.state = createReactiveState(rawState, schedule);
749
+ instance.state = createReactiveState(rawState, schedule, (key) => {
750
+ if (_triggerKey === null) _triggerKey = key;
751
+ else if (_triggerKey !== key) _triggerKey = "MULTIPLE";
752
+ });
669
753
  const boundMethods = /* @__PURE__ */ new Map();
670
754
  const exprState = new Proxy(rawState, {
671
755
  get(target, key) {
@@ -689,6 +773,8 @@ function mount(selector, definition) {
689
773
  instance.render = function() {
690
774
  var _a2;
691
775
  if (instance.__micraDestroyed) return;
776
+ const triggerKey = _triggerKey;
777
+ _triggerKey = null;
692
778
  if (isRendering) {
693
779
  if (!warnedReentry) {
694
780
  warn(
@@ -703,7 +789,7 @@ function mount(selector, definition) {
703
789
  const mRoot2 = root;
704
790
  const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
705
791
  applyDirectives(scan, exprState, rawState, instance);
706
- renderList(scan.each, exprState, rawState, instance);
792
+ renderList(scan.each, exprState, rawState, instance, triggerKey);
707
793
  bindDataOn(scan.on, instance);
708
794
  bindAtEvents(scan.atEvents, instance);
709
795
  bindModels(scan.model, instance);