@getforma/core 1.2.0 → 1.4.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.
Files changed (41) hide show
  1. package/dist/{chunk-YRNYOZF3.js → chunk-AJP4OVCN.js} +27 -7
  2. package/dist/chunk-AJP4OVCN.js.map +1 -0
  3. package/dist/{chunk-E3FV2VSU.cjs → chunk-FBM7V4NE.cjs} +27 -6
  4. package/dist/chunk-FBM7V4NE.cjs.map +1 -0
  5. package/dist/forma-runtime-csp.js +178 -79
  6. package/dist/forma-runtime.js +178 -79
  7. package/dist/formajs-runtime-hardened.global.js +178 -79
  8. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  9. package/dist/formajs-runtime.global.js +178 -79
  10. package/dist/formajs-runtime.global.js.map +1 -1
  11. package/dist/formajs.global.js +69 -9
  12. package/dist/formajs.global.js.map +1 -1
  13. package/dist/index.cjs +59 -16
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +14 -1
  16. package/dist/index.d.ts +14 -1
  17. package/dist/index.js +46 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/runtime-hardened.cjs +178 -79
  20. package/dist/runtime-hardened.cjs.map +1 -1
  21. package/dist/runtime-hardened.js +178 -79
  22. package/dist/runtime-hardened.js.map +1 -1
  23. package/dist/runtime.cjs +172 -77
  24. package/dist/runtime.cjs.map +1 -1
  25. package/dist/runtime.js +172 -77
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/server.cjs +62 -9
  28. package/dist/server.cjs.map +1 -1
  29. package/dist/server.d.cts +29 -3
  30. package/dist/server.d.ts +29 -3
  31. package/dist/server.js +62 -10
  32. package/dist/server.js.map +1 -1
  33. package/dist/ssr/index.cjs +22 -4
  34. package/dist/ssr/index.cjs.map +1 -1
  35. package/dist/ssr/index.d.cts +7 -0
  36. package/dist/ssr/index.d.ts +7 -0
  37. package/dist/ssr/index.js +22 -4
  38. package/dist/ssr/index.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-E3FV2VSU.cjs.map +0 -1
  41. package/dist/chunk-YRNYOZF3.js.map +0 -1
@@ -679,14 +679,18 @@ var FormaRuntime = (function (exports) {
679
679
  }
680
680
  const oldKeyMap = /* @__PURE__ */ new Map();
681
681
  for (let i = 0; i < oldLen; i++) {
682
- oldKeyMap.set(keyFn(oldItems[i]), i);
682
+ const k = keyFn(oldItems[i]);
683
+ const bucket = oldKeyMap.get(k);
684
+ if (bucket) bucket.push(i);
685
+ else oldKeyMap.set(k, [i]);
683
686
  }
684
687
  const oldIndices = new Array(newLen);
685
688
  const oldUsed = new Uint8Array(oldLen);
686
689
  for (let i = 0; i < newLen; i++) {
687
690
  const key = keyFn(newItems[i]);
688
- const oldIdx = oldKeyMap.get(key);
689
- if (oldIdx !== void 0) {
691
+ const bucket = oldKeyMap.get(key);
692
+ if (bucket && bucket.length > 0) {
693
+ const oldIdx = bucket.shift();
690
694
  oldIndices[i] = oldIdx;
691
695
  oldUsed[oldIdx] = 1;
692
696
  } else {
@@ -1035,12 +1039,12 @@ var FormaRuntime = (function (exports) {
1035
1039
  return null;
1036
1040
  }
1037
1041
  function createReconciler(config) {
1038
- let _lastHtml = "";
1042
+ const lastHtmlByContainer = /* @__PURE__ */ new WeakMap();
1039
1043
  return function reconcile2(container, html) {
1040
1044
  const trimmed = html.trim();
1041
1045
  if (!trimmed) return;
1042
- if (trimmed === _lastHtml && container.hasChildNodes()) return;
1043
- _lastHtml = trimmed;
1046
+ if (lastHtmlByContainer.get(container) === trimmed && container.hasChildNodes()) return;
1047
+ lastHtmlByContainer.set(container, trimmed);
1044
1048
  config.disconnectObserver();
1045
1049
  try {
1046
1050
  if (!container.hasChildNodes() || container.children.length === 0) {
@@ -1354,9 +1358,97 @@ var FormaRuntime = (function (exports) {
1354
1358
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
1355
1359
  var RE_AND = /^(.+?)\s*&&\s*(.+)$/;
1356
1360
  var RE_OR = /^(.+?)\s*\|\|\s*(.+)$/;
1357
- var RE_COMPARISON = /^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/;
1358
- var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
1359
- var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
1361
+ var UNARY_PRECEDERS = "+-*/%<>=!&|^~(,[{?:";
1362
+ function matchBinaryOp(expr, ops) {
1363
+ const SQ = String.fromCharCode(39);
1364
+ const DQ = String.fromCharCode(34);
1365
+ const BT = String.fromCharCode(96);
1366
+ const BS = String.fromCharCode(92);
1367
+ let depth = 0;
1368
+ let inSingle = false, inDouble = false, inTemplate = false, escaped = false;
1369
+ let prevSig = "";
1370
+ let bestIdx = -1;
1371
+ let bestOp = "";
1372
+ for (let i = 0; i < expr.length; i++) {
1373
+ const ch = expr[i];
1374
+ if (escaped) {
1375
+ escaped = false;
1376
+ continue;
1377
+ }
1378
+ if (ch === BS && (inSingle || inDouble || inTemplate)) {
1379
+ escaped = true;
1380
+ continue;
1381
+ }
1382
+ if (inSingle) {
1383
+ if (ch === SQ) {
1384
+ inSingle = false;
1385
+ prevSig = DQ;
1386
+ }
1387
+ continue;
1388
+ }
1389
+ if (inDouble) {
1390
+ if (ch === DQ) {
1391
+ inDouble = false;
1392
+ prevSig = DQ;
1393
+ }
1394
+ continue;
1395
+ }
1396
+ if (inTemplate) {
1397
+ if (ch === BT) {
1398
+ inTemplate = false;
1399
+ prevSig = DQ;
1400
+ }
1401
+ continue;
1402
+ }
1403
+ if (ch === SQ) {
1404
+ inSingle = true;
1405
+ continue;
1406
+ }
1407
+ if (ch === DQ) {
1408
+ inDouble = true;
1409
+ continue;
1410
+ }
1411
+ if (ch === BT) {
1412
+ inTemplate = true;
1413
+ continue;
1414
+ }
1415
+ if (ch === "(" || ch === "[" || ch === "{") {
1416
+ depth++;
1417
+ prevSig = ch;
1418
+ continue;
1419
+ }
1420
+ if (ch === ")" || ch === "]" || ch === "}") {
1421
+ if (depth > 0) depth--;
1422
+ prevSig = ch;
1423
+ continue;
1424
+ }
1425
+ if (depth !== 0) continue;
1426
+ if (ch === " " || ch === " " || ch === "\n") continue;
1427
+ if (i > 0) {
1428
+ let matchedOp = "";
1429
+ for (const op of ops) {
1430
+ if (expr.startsWith(op, i)) {
1431
+ matchedOp = op;
1432
+ break;
1433
+ }
1434
+ }
1435
+ if (matchedOp) {
1436
+ const isPlusMinus = matchedOp === "+" || matchedOp === "-";
1437
+ const unary = isPlusMinus && (prevSig === "" || UNARY_PRECEDERS.includes(prevSig));
1438
+ if (!unary) {
1439
+ bestIdx = i;
1440
+ bestOp = matchedOp;
1441
+ }
1442
+ i += matchedOp.length - 1;
1443
+ prevSig = matchedOp[matchedOp.length - 1];
1444
+ continue;
1445
+ }
1446
+ }
1447
+ prevSig = ch;
1448
+ }
1449
+ if (bestIdx === -1) return null;
1450
+ return { left: expr.slice(0, bestIdx).trim(), op: bestOp, right: expr.slice(bestIdx + bestOp.length).trim() };
1451
+ }
1360
1452
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
1361
1453
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
1362
1454
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
@@ -2313,12 +2405,12 @@ var FormaRuntime = (function (exports) {
2313
2405
  return () => left() && right();
2314
2406
  }
2315
2407
  }
2316
- const compMatch = expr.match(RE_COMPARISON);
2408
+ const compMatch = matchBinaryOp(expr, ["===", "!==", "==", "!=", ">=", "<=", ">", "<"]);
2317
2409
  if (compMatch) {
2318
- const left = parseExpression(compMatch[1].trim(), scope);
2319
- const right = parseExpression(compMatch[3].trim(), scope);
2410
+ const left = parseExpression(compMatch.left, scope);
2411
+ const right = parseExpression(compMatch.right, scope);
2320
2412
  if (left && right) {
2321
- const op = compMatch[2];
2413
+ const op = compMatch.op;
2322
2414
  return () => {
2323
2415
  const l = left(), r = right();
2324
2416
  switch (op) {
@@ -2342,12 +2434,12 @@ var FormaRuntime = (function (exports) {
2342
2434
  };
2343
2435
  }
2344
2436
  }
2345
- const addMatch = expr.match(RE_ADD);
2437
+ const addMatch = matchBinaryOp(expr, ["+", "-"]);
2346
2438
  if (addMatch) {
2347
- const left = parseExpression(addMatch[1].trim(), scope);
2348
- const right = parseExpression(addMatch[3].trim(), scope);
2439
+ const left = parseExpression(addMatch.left, scope);
2440
+ const right = parseExpression(addMatch.right, scope);
2349
2441
  if (left && right) {
2350
- const op = addMatch[2];
2442
+ const op = addMatch.op;
2351
2443
  return () => {
2352
2444
  const l = left(), r = right();
2353
2445
  if (op === "+") return l + r;
@@ -2355,12 +2447,12 @@ var FormaRuntime = (function (exports) {
2355
2447
  };
2356
2448
  }
2357
2449
  }
2358
- const mulMatch = expr.match(RE_MUL);
2450
+ const mulMatch = matchBinaryOp(expr, ["*", "/", "%"]);
2359
2451
  if (mulMatch) {
2360
- const left = parseExpression(mulMatch[1].trim(), scope);
2361
- const right = parseExpression(mulMatch[3].trim(), scope);
2452
+ const left = parseExpression(mulMatch.left, scope);
2453
+ const right = parseExpression(mulMatch.right, scope);
2362
2454
  if (left && right) {
2363
- const op = mulMatch[2];
2455
+ const op = mulMatch.op;
2364
2456
  return () => {
2365
2457
  const l = left(), r = right();
2366
2458
  switch (op) {
@@ -2785,25 +2877,61 @@ var FormaRuntime = (function (exports) {
2785
2877
  const modelExpr = !known || known.has("data-model") ? el.getAttribute("data-model") : null;
2786
2878
  if (modelExpr) {
2787
2879
  const prop = modelExpr.replace(RE_STRIP_BRACES, "").trim();
2788
- const getter = scope.getters[prop];
2789
- const setter = scope.setters[prop];
2880
+ let getter = scope.getters[prop];
2881
+ let setter = scope.setters[prop];
2882
+ if ((!getter || !setter) && prop.includes(".")) {
2883
+ getter = buildEvaluator(prop, scope);
2884
+ const lastDot = prop.lastIndexOf(".");
2885
+ const basePath = prop.slice(0, lastDot);
2886
+ const key = prop.slice(lastDot + 1);
2887
+ const baseGet = buildEvaluator(basePath, scope);
2888
+ setter = (v) => {
2889
+ const base = baseGet();
2890
+ if (base != null && typeof base === "object") base[key] = v;
2891
+ };
2892
+ }
2790
2893
  if (getter && setter) {
2791
2894
  const input = el;
2895
+ const tag = input.tagName;
2792
2896
  const dispose = internalEffect(() => {
2793
2897
  const val = getter();
2794
- if (input.type === "checkbox") {
2898
+ const type = input.type;
2899
+ if (type === "checkbox") {
2795
2900
  input.checked = !!val;
2901
+ const indetExpr = el.getAttribute("data-model-indeterminate");
2902
+ if (indetExpr) {
2903
+ const indetGetter = scope.getters[indetExpr.replace(RE_STRIP_BRACES, "").trim()];
2904
+ if (indetGetter) input.indeterminate = !!indetGetter();
2905
+ }
2906
+ } else if (type === "radio") {
2907
+ input.checked = String(val) === input.value;
2908
+ } else if (tag === "SELECT" && input.multiple) {
2909
+ const sel = input;
2910
+ const arr = Array.isArray(val) ? val.map(String) : [];
2911
+ for (const opt of Array.from(sel.options)) opt.selected = arr.includes(opt.value);
2796
2912
  } else {
2797
2913
  input.value = String(val ?? "");
2798
2914
  }
2799
2915
  });
2800
2916
  disposers.push(dispose);
2801
- const event = input.type === "checkbox" ? "change" : "input";
2917
+ const event = input.type === "checkbox" || input.type === "radio" || tag === "SELECT" ? "change" : "input";
2802
2918
  const onModelInput = () => {
2803
- if (input.type === "checkbox") {
2919
+ const type = input.type;
2920
+ if (type === "checkbox") {
2804
2921
  setter(input.checked);
2805
- } else if (input.type === "number" || input.type === "range") {
2806
- setter(Number(input.value));
2922
+ } else if (type === "radio") {
2923
+ if (input.checked) setter(input.value);
2924
+ } else if (tag === "SELECT" && input.multiple) {
2925
+ const sel = input;
2926
+ setter(Array.from(sel.selectedOptions).map((o) => o.value));
2927
+ } else if (type === "number" || type === "range") {
2928
+ const raw = input.value;
2929
+ if (raw === "") {
2930
+ setter(null);
2931
+ } else {
2932
+ const n = Number(raw);
2933
+ if (!Number.isNaN(n)) setter(n);
2934
+ }
2807
2935
  } else {
2808
2936
  setter(input.value);
2809
2937
  }
@@ -2995,59 +3123,30 @@ var FormaRuntime = (function (exports) {
2995
3123
  }
2996
3124
  }
2997
3125
  const prevNodes = new Set(oldNodes);
2998
- if (keyProp) {
2999
- const result = reconcileList(
3000
- el,
3001
- oldItems,
3002
- rawItems,
3003
- oldNodes,
3004
- (item) => String(item?.[keyProp] ?? ""),
3005
- (item) => {
3006
- const idx = rawItems.indexOf(item);
3007
- return createBoundClone2(item, idx);
3008
- },
3009
- (node, item) => {
3010
- const idx = rawItems.indexOf(item);
3011
- updateBoundClone2(node, item, idx);
3012
- },
3013
- void 0,
3014
- // beforeNode
3015
- listHooks
3016
- );
3017
- const nextNodes = new Set(result.nodes);
3018
- for (const n of prevNodes) {
3019
- if (!nextNodes.has(n)) {
3020
- if (n.hasAttribute?.("data-forma-leaving")) continue;
3021
- disposeCloneBindings2(n);
3022
- }
3023
- }
3024
- oldItems = result.items;
3025
- oldNodes = result.nodes;
3026
- } else {
3027
- const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
3028
- const oldWrapped = oldItems;
3029
- const result = reconcileList(
3030
- el,
3031
- oldWrapped,
3032
- wrapped,
3033
- oldNodes,
3034
- (w) => w.__idx,
3035
- (w) => createBoundClone2(w.__item, w.__idx),
3036
- (node, w) => updateBoundClone2(node, w.__item, w.__idx),
3037
- void 0,
3038
- // beforeNode
3039
- listHooks
3040
- );
3041
- const nextNodes = new Set(result.nodes);
3042
- for (const n of prevNodes) {
3043
- if (!nextNodes.has(n)) {
3044
- if (n.hasAttribute?.("data-forma-leaving")) continue;
3045
- disposeCloneBindings2(n);
3046
- }
3126
+ const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
3127
+ const oldWrapped = oldItems;
3128
+ const keyFn = keyProp ? (w) => String(w.__item?.[keyProp] ?? "") : (w) => w.__idx;
3129
+ const result = reconcileList(
3130
+ el,
3131
+ oldWrapped,
3132
+ wrapped,
3133
+ oldNodes,
3134
+ keyFn,
3135
+ (w) => createBoundClone2(w.__item, w.__idx),
3136
+ (node, w) => updateBoundClone2(node, w.__item, w.__idx),
3137
+ void 0,
3138
+ // beforeNode
3139
+ listHooks
3140
+ );
3141
+ const nextNodes = new Set(result.nodes);
3142
+ for (const n of prevNodes) {
3143
+ if (!nextNodes.has(n)) {
3144
+ if (n.hasAttribute?.("data-forma-leaving")) continue;
3145
+ disposeCloneBindings2(n);
3047
3146
  }
3048
- oldItems = result.items;
3049
- oldNodes = result.nodes;
3050
3147
  }
3148
+ oldItems = result.items;
3149
+ oldNodes = result.nodes;
3051
3150
  });
3052
3151
  disposers.push(dispose);
3053
3152
  }
@@ -788,14 +788,18 @@ var FormaRuntime = (() => {
788
788
  }
789
789
  const oldKeyMap = /* @__PURE__ */ new Map();
790
790
  for (let i = 0; i < oldLen; i++) {
791
- oldKeyMap.set(keyFn(oldItems[i]), i);
791
+ const k = keyFn(oldItems[i]);
792
+ const bucket = oldKeyMap.get(k);
793
+ if (bucket) bucket.push(i);
794
+ else oldKeyMap.set(k, [i]);
792
795
  }
793
796
  const oldIndices = new Array(newLen);
794
797
  const oldUsed = new Uint8Array(oldLen);
795
798
  for (let i = 0; i < newLen; i++) {
796
799
  const key = keyFn(newItems[i]);
797
- const oldIdx = oldKeyMap.get(key);
798
- if (oldIdx !== void 0) {
800
+ const bucket = oldKeyMap.get(key);
801
+ if (bucket && bucket.length > 0) {
802
+ const oldIdx = bucket.shift();
799
803
  oldIndices[i] = oldIdx;
800
804
  oldUsed[oldIdx] = 1;
801
805
  } else {
@@ -1144,12 +1148,12 @@ var FormaRuntime = (() => {
1144
1148
  return null;
1145
1149
  }
1146
1150
  function createReconciler(config) {
1147
- let _lastHtml = "";
1151
+ const lastHtmlByContainer = /* @__PURE__ */ new WeakMap();
1148
1152
  return function reconcile2(container, html) {
1149
1153
  const trimmed = html.trim();
1150
1154
  if (!trimmed) return;
1151
- if (trimmed === _lastHtml && container.hasChildNodes()) return;
1152
- _lastHtml = trimmed;
1155
+ if (lastHtmlByContainer.get(container) === trimmed && container.hasChildNodes()) return;
1156
+ lastHtmlByContainer.set(container, trimmed);
1153
1157
  config.disconnectObserver();
1154
1158
  try {
1155
1159
  if (!container.hasChildNodes() || container.children.length === 0) {
@@ -1464,9 +1468,97 @@ var FormaRuntime = (() => {
1464
1468
  var RE_NULLISH = /^(.+?)\s*\?\?\s*(.+)$/;
1465
1469
  var RE_AND = /^(.+?)\s*&&\s*(.+)$/;
1466
1470
  var RE_OR = /^(.+?)\s*\|\|\s*(.+)$/;
1467
- var RE_COMPARISON = /^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/;
1468
- var RE_MUL = /^(.+?)\s*([*/%])\s*(.+)$/;
1469
- var RE_ADD = /^(.+?)\s*([+-])\s*(.+)$/;
1471
+ var UNARY_PRECEDERS = "+-*/%<>=!&|^~(,[{?:";
1472
+ function matchBinaryOp(expr, ops) {
1473
+ const SQ = String.fromCharCode(39);
1474
+ const DQ = String.fromCharCode(34);
1475
+ const BT = String.fromCharCode(96);
1476
+ const BS = String.fromCharCode(92);
1477
+ let depth = 0;
1478
+ let inSingle = false, inDouble = false, inTemplate = false, escaped = false;
1479
+ let prevSig = "";
1480
+ let bestIdx = -1;
1481
+ let bestOp = "";
1482
+ for (let i = 0; i < expr.length; i++) {
1483
+ const ch = expr[i];
1484
+ if (escaped) {
1485
+ escaped = false;
1486
+ continue;
1487
+ }
1488
+ if (ch === BS && (inSingle || inDouble || inTemplate)) {
1489
+ escaped = true;
1490
+ continue;
1491
+ }
1492
+ if (inSingle) {
1493
+ if (ch === SQ) {
1494
+ inSingle = false;
1495
+ prevSig = DQ;
1496
+ }
1497
+ continue;
1498
+ }
1499
+ if (inDouble) {
1500
+ if (ch === DQ) {
1501
+ inDouble = false;
1502
+ prevSig = DQ;
1503
+ }
1504
+ continue;
1505
+ }
1506
+ if (inTemplate) {
1507
+ if (ch === BT) {
1508
+ inTemplate = false;
1509
+ prevSig = DQ;
1510
+ }
1511
+ continue;
1512
+ }
1513
+ if (ch === SQ) {
1514
+ inSingle = true;
1515
+ continue;
1516
+ }
1517
+ if (ch === DQ) {
1518
+ inDouble = true;
1519
+ continue;
1520
+ }
1521
+ if (ch === BT) {
1522
+ inTemplate = true;
1523
+ continue;
1524
+ }
1525
+ if (ch === "(" || ch === "[" || ch === "{") {
1526
+ depth++;
1527
+ prevSig = ch;
1528
+ continue;
1529
+ }
1530
+ if (ch === ")" || ch === "]" || ch === "}") {
1531
+ if (depth > 0) depth--;
1532
+ prevSig = ch;
1533
+ continue;
1534
+ }
1535
+ if (depth !== 0) continue;
1536
+ if (ch === " " || ch === " " || ch === "\n") continue;
1537
+ if (i > 0) {
1538
+ let matchedOp = "";
1539
+ for (const op of ops) {
1540
+ if (expr.startsWith(op, i)) {
1541
+ matchedOp = op;
1542
+ break;
1543
+ }
1544
+ }
1545
+ if (matchedOp) {
1546
+ const isPlusMinus = matchedOp === "+" || matchedOp === "-";
1547
+ const unary = isPlusMinus && (prevSig === "" || UNARY_PRECEDERS.includes(prevSig));
1548
+ if (!unary) {
1549
+ bestIdx = i;
1550
+ bestOp = matchedOp;
1551
+ }
1552
+ i += matchedOp.length - 1;
1553
+ prevSig = matchedOp[matchedOp.length - 1];
1554
+ continue;
1555
+ }
1556
+ }
1557
+ prevSig = ch;
1558
+ }
1559
+ if (bestIdx === -1) return null;
1560
+ return { left: expr.slice(0, bestIdx).trim(), op: bestOp, right: expr.slice(bestIdx + bestOp.length).trim() };
1561
+ }
1470
1562
  var RE_TEMPLATE_LIT = /^`([^`]*)`$/;
1471
1563
  var RE_TEMPLATE_INTERP = /\$\{([^}]+)\}/g;
1472
1564
  var RE_GROUP_METHOD_CALL = /^\((.+)\)\.(\w+)\((.*)\)$/;
@@ -2461,12 +2553,12 @@ var FormaRuntime = (() => {
2461
2553
  return () => left() && right();
2462
2554
  }
2463
2555
  }
2464
- const compMatch = expr.match(RE_COMPARISON);
2556
+ const compMatch = matchBinaryOp(expr, ["===", "!==", "==", "!=", ">=", "<=", ">", "<"]);
2465
2557
  if (compMatch) {
2466
- const left = parseExpression(compMatch[1].trim(), scope);
2467
- const right = parseExpression(compMatch[3].trim(), scope);
2558
+ const left = parseExpression(compMatch.left, scope);
2559
+ const right = parseExpression(compMatch.right, scope);
2468
2560
  if (left && right) {
2469
- const op = compMatch[2];
2561
+ const op = compMatch.op;
2470
2562
  return () => {
2471
2563
  const l = left(), r = right();
2472
2564
  switch (op) {
@@ -2490,12 +2582,12 @@ var FormaRuntime = (() => {
2490
2582
  };
2491
2583
  }
2492
2584
  }
2493
- const addMatch = expr.match(RE_ADD);
2585
+ const addMatch = matchBinaryOp(expr, ["+", "-"]);
2494
2586
  if (addMatch) {
2495
- const left = parseExpression(addMatch[1].trim(), scope);
2496
- const right = parseExpression(addMatch[3].trim(), scope);
2587
+ const left = parseExpression(addMatch.left, scope);
2588
+ const right = parseExpression(addMatch.right, scope);
2497
2589
  if (left && right) {
2498
- const op = addMatch[2];
2590
+ const op = addMatch.op;
2499
2591
  return () => {
2500
2592
  const l = left(), r = right();
2501
2593
  if (op === "+") return l + r;
@@ -2503,12 +2595,12 @@ var FormaRuntime = (() => {
2503
2595
  };
2504
2596
  }
2505
2597
  }
2506
- const mulMatch = expr.match(RE_MUL);
2598
+ const mulMatch = matchBinaryOp(expr, ["*", "/", "%"]);
2507
2599
  if (mulMatch) {
2508
- const left = parseExpression(mulMatch[1].trim(), scope);
2509
- const right = parseExpression(mulMatch[3].trim(), scope);
2600
+ const left = parseExpression(mulMatch.left, scope);
2601
+ const right = parseExpression(mulMatch.right, scope);
2510
2602
  if (left && right) {
2511
- const op = mulMatch[2];
2603
+ const op = mulMatch.op;
2512
2604
  return () => {
2513
2605
  const l = left(), r = right();
2514
2606
  switch (op) {
@@ -3003,25 +3095,61 @@ var FormaRuntime = (() => {
3003
3095
  const modelExpr = !known || known.has("data-model") ? el.getAttribute("data-model") : null;
3004
3096
  if (modelExpr) {
3005
3097
  const prop = modelExpr.replace(RE_STRIP_BRACES, "").trim();
3006
- const getter = scope.getters[prop];
3007
- const setter = scope.setters[prop];
3098
+ let getter = scope.getters[prop];
3099
+ let setter = scope.setters[prop];
3100
+ if ((!getter || !setter) && prop.includes(".")) {
3101
+ getter = buildEvaluator(prop, scope);
3102
+ const lastDot = prop.lastIndexOf(".");
3103
+ const basePath = prop.slice(0, lastDot);
3104
+ const key = prop.slice(lastDot + 1);
3105
+ const baseGet = buildEvaluator(basePath, scope);
3106
+ setter = (v) => {
3107
+ const base = baseGet();
3108
+ if (base != null && typeof base === "object") base[key] = v;
3109
+ };
3110
+ }
3008
3111
  if (getter && setter) {
3009
3112
  const input = el;
3113
+ const tag = input.tagName;
3010
3114
  const dispose = internalEffect(() => {
3011
3115
  const val = getter();
3012
- if (input.type === "checkbox") {
3116
+ const type = input.type;
3117
+ if (type === "checkbox") {
3013
3118
  input.checked = !!val;
3119
+ const indetExpr = el.getAttribute("data-model-indeterminate");
3120
+ if (indetExpr) {
3121
+ const indetGetter = scope.getters[indetExpr.replace(RE_STRIP_BRACES, "").trim()];
3122
+ if (indetGetter) input.indeterminate = !!indetGetter();
3123
+ }
3124
+ } else if (type === "radio") {
3125
+ input.checked = String(val) === input.value;
3126
+ } else if (tag === "SELECT" && input.multiple) {
3127
+ const sel = input;
3128
+ const arr = Array.isArray(val) ? val.map(String) : [];
3129
+ for (const opt of Array.from(sel.options)) opt.selected = arr.includes(opt.value);
3014
3130
  } else {
3015
3131
  input.value = String(val ?? "");
3016
3132
  }
3017
3133
  });
3018
3134
  disposers.push(dispose);
3019
- const event = input.type === "checkbox" ? "change" : "input";
3135
+ const event = input.type === "checkbox" || input.type === "radio" || tag === "SELECT" ? "change" : "input";
3020
3136
  const onModelInput = () => {
3021
- if (input.type === "checkbox") {
3137
+ const type = input.type;
3138
+ if (type === "checkbox") {
3022
3139
  setter(input.checked);
3023
- } else if (input.type === "number" || input.type === "range") {
3024
- setter(Number(input.value));
3140
+ } else if (type === "radio") {
3141
+ if (input.checked) setter(input.value);
3142
+ } else if (tag === "SELECT" && input.multiple) {
3143
+ const sel = input;
3144
+ setter(Array.from(sel.selectedOptions).map((o) => o.value));
3145
+ } else if (type === "number" || type === "range") {
3146
+ const raw = input.value;
3147
+ if (raw === "") {
3148
+ setter(null);
3149
+ } else {
3150
+ const n = Number(raw);
3151
+ if (!Number.isNaN(n)) setter(n);
3152
+ }
3025
3153
  } else {
3026
3154
  setter(input.value);
3027
3155
  }
@@ -3214,59 +3342,30 @@ var FormaRuntime = (() => {
3214
3342
  }
3215
3343
  }
3216
3344
  const prevNodes = new Set(oldNodes);
3217
- if (keyProp) {
3218
- const result = reconcileList(
3219
- el,
3220
- oldItems,
3221
- rawItems,
3222
- oldNodes,
3223
- (item) => String(item?.[keyProp] ?? ""),
3224
- (item) => {
3225
- const idx = rawItems.indexOf(item);
3226
- return createBoundClone2(item, idx);
3227
- },
3228
- (node, item) => {
3229
- const idx = rawItems.indexOf(item);
3230
- updateBoundClone2(node, item, idx);
3231
- },
3232
- void 0,
3233
- // beforeNode
3234
- listHooks
3235
- );
3236
- const nextNodes = new Set(result.nodes);
3237
- for (const n of prevNodes) {
3238
- if (!nextNodes.has(n)) {
3239
- if (n.hasAttribute?.("data-forma-leaving")) continue;
3240
- disposeCloneBindings2(n);
3241
- }
3242
- }
3243
- oldItems = result.items;
3244
- oldNodes = result.nodes;
3245
- } else {
3246
- const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
3247
- const oldWrapped = oldItems;
3248
- const result = reconcileList(
3249
- el,
3250
- oldWrapped,
3251
- wrapped,
3252
- oldNodes,
3253
- (w) => w.__idx,
3254
- (w) => createBoundClone2(w.__item, w.__idx),
3255
- (node, w) => updateBoundClone2(node, w.__item, w.__idx),
3256
- void 0,
3257
- // beforeNode
3258
- listHooks
3259
- );
3260
- const nextNodes = new Set(result.nodes);
3261
- for (const n of prevNodes) {
3262
- if (!nextNodes.has(n)) {
3263
- if (n.hasAttribute?.("data-forma-leaving")) continue;
3264
- disposeCloneBindings2(n);
3265
- }
3345
+ const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
3346
+ const oldWrapped = oldItems;
3347
+ const keyFn = keyProp ? (w) => String(w.__item?.[keyProp] ?? "") : (w) => w.__idx;
3348
+ const result = reconcileList(
3349
+ el,
3350
+ oldWrapped,
3351
+ wrapped,
3352
+ oldNodes,
3353
+ keyFn,
3354
+ (w) => createBoundClone2(w.__item, w.__idx),
3355
+ (node, w) => updateBoundClone2(node, w.__item, w.__idx),
3356
+ void 0,
3357
+ // beforeNode
3358
+ listHooks
3359
+ );
3360
+ const nextNodes = new Set(result.nodes);
3361
+ for (const n of prevNodes) {
3362
+ if (!nextNodes.has(n)) {
3363
+ if (n.hasAttribute?.("data-forma-leaving")) continue;
3364
+ disposeCloneBindings2(n);
3266
3365
  }
3267
- oldItems = result.items;
3268
- oldNodes = result.nodes;
3269
3366
  }
3367
+ oldItems = result.items;
3368
+ oldNodes = result.nodes;
3270
3369
  });
3271
3370
  disposers.push(dispose);
3272
3371
  }