@jarenjs/collection 0.86.0 → 0.89.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.
@@ -81,7 +81,9 @@ pending realization. `removedKeys` tells interaction to resolve a removal fallba
81
81
  only bounded observations and JSON intent need enter app state.
82
82
 
83
83
  Selection contains stable `keys`, or query/snapshot-scoped `mode:'all'` with
84
- `exclusions`, or one range with stable `fromKey`/`toKey` endpoints. Shift ranges
84
+ `exclusions`, or one range with stable `fromKey`/`toKey` endpoints and optional
85
+ exclusions. Toggling a range member excludes or restores it in both the UI and
86
+ complete output. Clearing selection resets its shift anchor. Shift ranges
85
87
  survive asynchronous endpoint realization. `maxSelectedKeys` (4096) bounds explicit
86
88
  keys/exclusions. `state`, `selected`, `toggle`, `selectAll`, `clear`, `restore`,
87
89
  `cancelPending` and `update` expose the model. Range membership for rendered rows
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/collection",
3
3
  "private": false,
4
- "version": "0.86.0",
4
+ "version": "0.89.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
@@ -52,8 +52,8 @@
52
52
  "prepack": "npm run build:types"
53
53
  },
54
54
  "dependencies": {
55
- "@jarenjs/core": "^0.86.0",
56
- "@jarenjs/view": "^0.86.0",
57
- "@jarenjs/app": "^0.86.0"
55
+ "@jarenjs/core": "^0.89.0",
56
+ "@jarenjs/view": "^0.89.0",
57
+ "@jarenjs/app": "^0.89.0"
58
58
  }
59
59
  }
@@ -19,27 +19,30 @@ export function createCollectionInteraction(options) {
19
19
  return { state: 'ready', ...focus };
20
20
  }
21
21
  function scoped() { return selection.query === config.query && selection.snapshot === config.snapshot; }
22
- function selected(key) {
23
- if (key == null) return false;
24
- if (selection.mode === 'all' && scoped()) return !selection.exclusions.includes(key);
25
- if (selection.keys.includes(key)) return true;
22
+ function covered(key) {
26
23
  if (!scoped()) return false;
24
+ if (selection.mode === 'all') return true;
27
25
  return selection.ranges.some((range) => {
28
26
  if (key === range.fromKey || key === range.toKey) return true;
29
27
  const index = config.indexOf?.(key), start = config.indexOf?.(range.fromKey), end = config.indexOf?.(range.toKey);
30
28
  return index >= 0 && start >= 0 && end >= 0 && index >= Math.min(start, end) && index <= Math.max(start, end);
31
29
  });
32
30
  }
31
+ function selected(key) {
32
+ return key != null && (selection.keys.includes(key) || (covered(key) && !selection.exclusions.includes(key)));
33
+ }
33
34
  function toggle(key) {
34
35
  if (typeof key !== 'string') return {state:'error',reason:'invalid-selection'};
35
- if (selection.mode === 'all' && scoped()) {
36
+ if (covered(key)) {
36
37
  const excluded = selection.exclusions.includes(key);
37
- if (!excluded && selection.exclusions.length >= config.maxSelectedKeys) return { state: 'budget-exhausted', reason: 'selection-credits' };
38
+ const keys = selection.keys.filter((item) => item !== key);
39
+ if (!excluded && keys.length + selection.exclusions.length >= config.maxSelectedKeys) return { state: 'budget-exhausted', reason: 'selection-credits' };
40
+ selection.keys = keys;
38
41
  selection.exclusions = excluded ? selection.exclusions.filter((item) => item !== key) : [...selection.exclusions, key];
39
42
  }
40
43
  else {
41
44
  const exists = selection.keys.includes(key);
42
- if (!exists && selection.keys.length >= config.maxSelectedKeys) return { state: 'budget-exhausted', reason: 'selection-credits' };
45
+ if (!exists && selection.keys.length + selection.exclusions.length >= config.maxSelectedKeys) return { state: 'budget-exhausted', reason: 'selection-credits' };
43
46
  selection.keys = exists ? selection.keys.filter((item) => item !== key) : [...selection.keys, key];
44
47
  }
45
48
  return { state: 'ready' };
@@ -67,7 +70,7 @@ export function createCollectionInteraction(options) {
67
70
  },
68
71
  toggle,
69
72
  selectAll() { selection = { mode: 'all', keys: [], ranges: [], exclusions: [], query: config.query, snapshot: config.snapshot }; },
70
- clear() { selection = { mode: 'keys', keys: [], ranges: [], exclusions: [], query: config.query, snapshot: config.snapshot }; },
73
+ clear() { rangeAnchor = null; selection = { mode: 'keys', keys: [], ranges: [], exclusions: [], query: config.query, snapshot: config.snapshot }; },
71
74
  restore(intent) {
72
75
  // Only JSON intent returns from navigation; loaded resources never travel here.
73
76
  if (!intent || !isJsonValue(intent) || typeof intent.query !== 'string' || typeof intent.snapshot !== 'string' || !['keys', 'all'].includes(intent.mode) || !Array.isArray(intent.keys) || !Array.isArray(intent.exclusions)