@ulu/frontend-vue 0.1.1-beta.2 → 0.1.1-beta.3

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.
@@ -1,6 +1,74 @@
1
1
  import { ref, computed, watch } from 'vue';
2
2
  import Fuse from 'fuse.js';
3
3
 
4
+ /**
5
+ * Helper function to create a union of multiple Sets.
6
+ * @param {Array<Set>} sets - An array of sets to unify.
7
+ * @returns {Set} A new set containing all unique items from the input sets.
8
+ */
9
+ function unionSets(sets) {
10
+ const union = new Set();
11
+ for (const set of sets) {
12
+ for (const item of set) {
13
+ union.add(item);
14
+ }
15
+ }
16
+ return union;
17
+ }
18
+
19
+ /**
20
+ * Helper function to create an intersection of multiple Sets.
21
+ * @param {Array<Set>} sets - An array of sets to intersect.
22
+ * @returns {Set} A new set containing only the items present in all input sets.
23
+ */
24
+ function intersectSets(sets) {
25
+ if (!sets || sets.length === 0) return new Set();
26
+ // Start with a copy of the first set, which is an optimization if it's the smallest.
27
+ // For better performance, sort sets by size and start with the smallest.
28
+ const sortedSets = sets.sort((a, b) => a.size - b.size);
29
+ const intersection = new Set(sortedSets[0]);
30
+ for (let i = 1; i < sortedSets.length; i++) {
31
+ for (const item of intersection) {
32
+ if (!sortedSets[i].has(item)) {
33
+ intersection.delete(item);
34
+ }
35
+ }
36
+ // If intersection becomes empty, no need to continue.
37
+ if (intersection.size === 0) break;
38
+ }
39
+ return intersection;
40
+ }
41
+
42
+ /**
43
+ * Calculates the final set of item indices based on selected facets using the pre-built index.
44
+ * @param {Array<Object>} selected - The array of selected facet groups.
45
+ * @param {Map<String, Set>} index - The inverted index.
46
+ * @param {Set<number>} allItemsSet - A set of all possible item indices.
47
+ * @returns {Set} A set of indices for the items that match the filters.
48
+ */
49
+ function getFilteredSetFromIndex(selected, index, allItemsSet) {
50
+ if (!selected || selected.length === 0) {
51
+ return allItemsSet;
52
+ }
53
+
54
+ const groupSets = selected.map(group => {
55
+ const childSets = group.children.map(child => {
56
+ const key = `${group.uid}:${child.uid}`;
57
+ return index.get(key) || new Set();
58
+ });
59
+
60
+ // For 'all' (AND), intersect the sets within the group.
61
+ if (group.match === 'all') {
62
+ return intersectSets(childSets);
63
+ }
64
+ // For 'some' (OR), union the sets within the group.
65
+ return unionSets(childSets);
66
+ });
67
+
68
+ // Intersect the results from each group.
69
+ return intersectSets(groupSets);
70
+ }
71
+
4
72
  /**
5
73
  * Generates facet groups and their possible values from a list of items.
6
74
  * @param {Array<Object>} allItems - The full list of items.
@@ -54,6 +122,7 @@ function generateInitialFacets(allItems, facetFields) {
54
122
  * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
55
123
  * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item. Should always return an array of values.
56
124
  * @param {Function} [options.getSortValue] - A function to get the value to sort by from an item.
125
+ * @param {String} [options.countMode='none'] - The mode for calculating facet counts. Can be 'none', 'simple', or 'intuitive'.
57
126
  */
58
127
  export function useFacets(allItems, options = {}) {
59
128
  const defaultGetItemFacet = (item, uid) => {
@@ -120,6 +189,37 @@ export function useFacets(allItems, options = {}) {
120
189
  ...extraSortTypes
121
190
  }));
122
191
 
192
+ const facetIndex = computed(() => {
193
+ const index = new Map();
194
+ // Depend on searchedItems to re-build when search changes.
195
+ const items = searchedItems.value;
196
+ if (!items || !facetFields) return index;
197
+
198
+ // Create a map of getters for efficiency inside the loop.
199
+ const getters = new Map(facetFields.map(group => {
200
+ const getValue = group.getValue || (item => item[group.uid]);
201
+ return [group.uid, getValue];
202
+ }));
203
+
204
+ for (let i = 0; i < items.length; i++) {
205
+ const item = items[i];
206
+ for (const group of facetFields) {
207
+ const getValue = getters.get(group.uid);
208
+ const value = getValue(item);
209
+ const values = Array.isArray(value) ? value : (value ? [value] : []);
210
+
211
+ for (const v of values) {
212
+ const key = `${group.uid}:${v}`;
213
+ if (!index.has(key)) {
214
+ index.set(key, new Set());
215
+ }
216
+ index.get(key).add(i);
217
+ }
218
+ }
219
+ }
220
+ return index;
221
+ });
222
+
123
223
  const searchOptions = computed(() => ({
124
224
  shouldSort: true,
125
225
  keys: ["title", "label", "description", "author"],
@@ -147,7 +247,23 @@ export function useFacets(allItems, options = {}) {
147
247
  });
148
248
 
149
249
  const filteredItems = computed(() => {
150
- return getFilteredItems(searchedItems.value, selectedFacets.value);
250
+ if (!selectedFacets.value.length) {
251
+ return searchedItems.value;
252
+ }
253
+ const index = facetIndex.value;
254
+ // This check is important because the index might not be ready on the first render.
255
+ if (index.size === 0 && searchedItems.value.length > 0 && facetFields?.length > 0) {
256
+ return [];
257
+ }
258
+ const allItemsSet = new Set(searchedItems.value.map((_, i) => i));
259
+ const filteredIndexSet = getFilteredSetFromIndex(selectedFacets.value, index, allItemsSet);
260
+
261
+ const result = [];
262
+ // Using a direct loop for performance.
263
+ for (const i of filteredIndexSet) {
264
+ result.push(searchedItems.value[i]);
265
+ }
266
+ return result;
151
267
  });
152
268
 
153
269
  const displayItems = computed(() => {
@@ -159,22 +275,6 @@ export function useFacets(allItems, options = {}) {
159
275
  });
160
276
 
161
277
  // --- Methods ---
162
- function getFilteredItems(items, selected) {
163
- if (!selected.length) return items;
164
- return items.filter(item => {
165
- return selected.every(group => {
166
- const itemFacetValues = getItemFacet(item, group.uid);
167
- if (itemFacetValues && itemFacetValues.length) {
168
- if (group.match === 'all') {
169
- return group.children.every(facet => itemFacetValues.includes(facet.uid));
170
- }
171
- return group.children.some(facet => itemFacetValues.includes(facet.uid));
172
- }
173
- return false;
174
- });
175
- });
176
- }
177
-
178
278
  function clearFilters() {
179
279
  facets.value.forEach(group => {
180
280
  if (group.children) {
@@ -223,33 +323,52 @@ export function useFacets(allItems, options = {}) {
223
323
  });
224
324
  });
225
325
  } else if (countMode === 'intuitive') {
326
+ const index = facetIndex.value;
327
+ if (index.size === 0 && searchedItems.value.length > 0 && facetFields?.length > 0) {
328
+ // Index might not be ready yet.
329
+ return;
330
+ }
331
+ const allItemsSet = new Set(searchedItems.value.map((_, i) => i));
332
+ const currentFilteredSet = getFilteredSetFromIndex(currentSelected, index, allItemsSet);
333
+
226
334
  facets.value.forEach(group => {
227
335
  group.children.forEach(child => {
228
- const tempFacets = JSON.parse(JSON.stringify(facets.value));
229
- const tempGroup = tempFacets.find(g => g.uid === group.uid);
230
- const tempChild = tempGroup.children.find(c => c.uid === child.uid);
231
-
232
- if (tempGroup.multiple) {
233
- if (tempChild.selected) {
234
- child.count = filteredItems.value.filter(item => {
235
- const itemValues = getItemFacet(item, group.uid);
236
- return itemValues.includes(child.uid);
237
- }).length;
238
- return;
336
+ const key = `${group.uid}:${child.uid}`;
337
+ const childSet = index.get(key) || new Set();
338
+
339
+ if (child.selected) {
340
+ // --- Logic for already-selected facets (now performant) ---
341
+ if (group.multiple) {
342
+ // This is the intersection of currently filtered items and items with this facet value.
343
+ const intersection = intersectSets([currentFilteredSet, childSet]);
344
+ child.count = intersection.size;
345
+ } else {
346
+ // For single-select, the count is just the total number of filtered items.
347
+ child.count = currentFilteredSet.size;
239
348
  }
240
- tempChild.selected = true;
241
349
  } else {
242
- if (tempChild.selected) {
243
- child.count = filteredItems.value.length;
244
- return;
350
+ // --- Logic for un-selected facets (now without deep clone) ---
351
+ const tempSelected = [];
352
+ for (const g of currentSelected) {
353
+ tempSelected.push({ ...g, children: [...g.children] });
354
+ }
355
+
356
+ let groupInTemp = tempSelected.find(g => g.uid === group.uid);
357
+
358
+ if (!groupInTemp) {
359
+ groupInTemp = { ...group, children: [] };
360
+ tempSelected.push(groupInTemp);
361
+ }
362
+
363
+ if (group.multiple) {
364
+ groupInTemp.children.push(child);
365
+ } else {
366
+ groupInTemp.children = [child];
245
367
  }
246
- tempGroup.children.forEach(f => { f.selected = false; });
247
- tempChild.selected = true;
368
+
369
+ const finalSet = getFilteredSetFromIndex(tempSelected, index, allItemsSet);
370
+ child.count = finalSet.size;
248
371
  }
249
-
250
- const tempSelected = tempFacets.map(g => ({...g, children: g.children.filter(c => c.selected)})).filter(g => g.children.length > 0);
251
- const resultItems = getFilteredItems(currentSearchedItems, tempSelected);
252
- child.count = resultItems.length;
253
372
  });
254
373
  });
255
374
  }
@@ -61,7 +61,7 @@ const props = defineProps({
61
61
  */
62
62
  formatValue: {
63
63
  type: Function,
64
- default: (value) => `${value}%`,
64
+ default: (value) => `${ value }%`,
65
65
  },
66
66
  /**
67
67
  * Hides the percentage value display.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.1.1-beta.2",
3
+ "version": "0.1.1-beta.3",
4
4
  "description": "A modular and tree-shakeable Vue 3 component library for the Ulu frontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,6 +11,7 @@
11
11
  * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
12
12
  * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item. Should always return an array of values.
13
13
  * @param {Function} [options.getSortValue] - A function to get the value to sort by from an item.
14
+ * @param {String} [options.countMode='none'] - The mode for calculating facet counts. Can be 'none', 'simple', or 'intuitive'.
14
15
  */
15
16
  export function useFacets(allItems: import("vue").Ref<Array<any>>, options?: {
16
17
  initialFacets?: any[];
@@ -22,6 +23,7 @@ export function useFacets(allItems: import("vue").Ref<Array<any>>, options?: {
22
23
  searchOptions?: any;
23
24
  getItemFacet?: Function;
24
25
  getSortValue?: Function;
26
+ countMode?: string;
25
27
  }): {
26
28
  facets: import("vue").Ref<any, any>;
27
29
  searchValue: import("vue").Ref<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"useFacets.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/facets/useFacets.js"],"names":[],"mappings":"AA2CA;;;;;;;;;;;;;GAaG;AACH,oCAZW,OAAO,KAAK,EAAE,GAAG,CAAC,KAAK,KAAQ,CAAC,YAExC;IAAwB,aAAa;IACb,WAAW;IACV,kBAAkB;IAClB,eAAe;IACd,cAAc;IACf,cAAc;IACd,aAAa;IACX,YAAY;IACZ,YAAY;CACzC;;;;;;;;;;;;;EAmNA"}
1
+ {"version":3,"file":"useFacets.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/facets/useFacets.js"],"names":[],"mappings":"AA+GA;;;;;;;;;;;;;;GAcG;AACH,oCAbW,OAAO,KAAK,EAAE,GAAG,CAAC,KAAK,KAAQ,CAAC,YAExC;IAAwB,aAAa;IACb,WAAW;IACV,kBAAkB;IAClB,eAAe;IACd,cAAc;IACf,cAAc;IACd,aAAa;IACX,YAAY;IACZ,YAAY;IACd,SAAS;CACpC;;;;;;;;;;;;;EAqQA"}