@ulu/frontend-vue 0.1.2-beta.7 → 0.1.2-beta.9

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.
@@ -9,9 +9,11 @@
9
9
  * @param {Boolean} [options.noDefaultSorts=false] - If true, the default 'A-Z' and 'Z-A' sorts will not be included.
10
10
  * @param {Object} [options.extraSortTypes={}] - Additional sort types to be merged with the default ones.
11
11
  * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
12
- * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item. Should always return an array of values.
13
12
  * @param {Function} [options.getSortValue] - A function to get the value to sort by from an item.
14
13
  * @param {String} [options.countMode='none'] - The mode for calculating facet counts. Can be 'none', 'simple', or 'intuitive'.
14
+ * @param {Object} [options.urlSync] - Optional configuration to sync state with URL.
15
+ * @param {import('vue-router').Router} [options.urlSync.router] - The Vue Router instance.
16
+ * @param {import('vue-router').RouteLocationNormalizedLoaded} [options.urlSync.route] - The current route instance.
15
17
  */
16
18
  export function useFacets(allItems: import("vue").Ref<Array<Object>>, options?: {
17
19
  initialFacets?: any[] | undefined;
@@ -21,9 +23,12 @@ export function useFacets(allItems: import("vue").Ref<Array<Object>>, options?:
21
23
  noDefaultSorts?: boolean | undefined;
22
24
  extraSortTypes?: Object | undefined;
23
25
  searchOptions?: Object | undefined;
24
- getItemFacet?: Function | undefined;
25
26
  getSortValue?: Function | undefined;
26
27
  countMode?: string | undefined;
28
+ urlSync?: {
29
+ router?: import("vue-router").Router | undefined;
30
+ route?: import("vue-router").RouteLocationNormalizedLoadedGeneric | undefined;
31
+ } | undefined;
27
32
  }): {
28
33
  facets: import("vue").Ref<never[], never[]>;
29
34
  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":"AA+GA;;;;;;;;;;;;;;GAcG;AACH,oCAbW,OAAO,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAExC;IAAwB,aAAa;IACb,WAAW;IACV,kBAAkB;IAClB,eAAe;IACd,cAAc;IACf,cAAc;IACd,aAAa;IACX,YAAY;IACZ,YAAY;IACd,SAAS;CACpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4QA"}
1
+ {"version":3,"file":"useFacets.d.ts","sourceRoot":"","sources":["../../../../../lib/components/systems/facets/useFacets.js"],"names":[],"mappings":"AA+GA;;;;;;;;;;;;;;;;GAgBG;AACH,oCAfW,OAAO,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAExC;IAAwB,aAAa;IACb,WAAW;IACV,kBAAkB;IAClB,eAAe;IACd,cAAc;IACf,cAAc;IACd,aAAa;IACX,YAAY;IACd,SAAS;IACT,OAAO;;;;CAGlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkWA"}
@@ -1,4 +1,4 @@
1
- import { ref, computed, watch } from 'vue';
1
+ import { ref, computed, watch, nextTick, watchPostEffect } from 'vue';
2
2
  import Fuse from 'fuse.js';
3
3
 
4
4
  /**
@@ -120,17 +120,13 @@ function generateInitialFacets(allItems, facetFields) {
120
120
  * @param {Boolean} [options.noDefaultSorts=false] - If true, the default 'A-Z' and 'Z-A' sorts will not be included.
121
121
  * @param {Object} [options.extraSortTypes={}] - Additional sort types to be merged with the default ones.
122
122
  * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
123
- * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item. Should always return an array of values.
124
123
  * @param {Function} [options.getSortValue] - A function to get the value to sort by from an item.
125
124
  * @param {String} [options.countMode='none'] - The mode for calculating facet counts. Can be 'none', 'simple', or 'intuitive'.
125
+ * @param {Object} [options.urlSync] - Optional configuration to sync state with URL.
126
+ * @param {import('vue-router').Router} [options.urlSync.router] - The Vue Router instance.
127
+ * @param {import('vue-router').RouteLocationNormalizedLoaded} [options.urlSync.route] - The current route instance.
126
128
  */
127
129
  export function useFacets(allItems, options = {}) {
128
- const defaultGetItemFacet = (item, uid) => {
129
- const value = item[uid];
130
- if (value === null || typeof value === 'undefined') return [];
131
- return Array.isArray(value) ? value : [value];
132
- };
133
-
134
130
  const {
135
131
  initialFacets,
136
132
  facetFields,
@@ -139,9 +135,9 @@ export function useFacets(allItems, options = {}) {
139
135
  noDefaultSorts = false,
140
136
  extraSortTypes = {},
141
137
  searchOptions: initialSearchOptions = {},
142
- getItemFacet = defaultGetItemFacet,
143
138
  getSortValue = item => (item.title || item.label || ""),
144
- countMode = 'none' // 'none', 'simple', 'intuitive'
139
+ countMode = 'none', // 'none', 'simple', 'intuitive'
140
+ urlSync: urlSyncOptions
145
141
  } = options;
146
142
 
147
143
  const sortAlpha = items => {
@@ -319,14 +315,26 @@ export function useFacets(allItems, options = {}) {
319
315
  if (currentSelected === prevSelected && currentSearchedItems === prevSearchedItems) return;
320
316
 
321
317
  if (countMode === 'simple') {
318
+ const index = facetIndex.value;
319
+ if (index.size === 0 && searchedItems.value.length > 0 && facetFields?.length > 0) {
320
+ return; // Index not ready
321
+ }
322
+ const allItemsSet = new Set(searchedItems.value.map((_, i) => i));
323
+
322
324
  facets.value.forEach(group => {
323
325
  const otherSelected = currentSelected.filter(g => g.uid !== group.uid);
324
- const itemsToCount = getFilteredItems(currentSearchedItems, otherSelected);
326
+
327
+ // Get the set of indices for items filtered by OTHER groups
328
+ const filteredByOthersSet = getFilteredSetFromIndex(otherSelected, index, allItemsSet);
329
+
325
330
  group.children.forEach(child => {
326
- child.count = itemsToCount.filter(item => {
327
- const itemValues = getItemFacet(item, group.uid);
328
- return itemValues.includes(child.uid);
329
- }).length;
331
+ const key = `${group.uid}:${child.uid}`;
332
+ const childSet = index.get(key) || new Set();
333
+
334
+ // The count is the size of the intersection between items that have this child facet
335
+ // and items that match all the *other* selected groups.
336
+ const intersection = intersectSets([filteredByOthersSet, childSet]);
337
+ child.count = intersection.size;
330
338
  });
331
339
  });
332
340
  } else if (countMode === 'intuitive') {
@@ -344,7 +352,6 @@ export function useFacets(allItems, options = {}) {
344
352
  const childSet = index.get(key) || new Set();
345
353
 
346
354
  if (child.selected) {
347
- // --- Logic for already-selected facets (now performant) ---
348
355
  if (group.multiple) {
349
356
  // This is the intersection of currently filtered items and items with this facet value.
350
357
  const intersection = intersectSets([currentFilteredSet, childSet]);
@@ -381,6 +388,87 @@ export function useFacets(allItems, options = {}) {
381
388
  }
382
389
  }, { deep: true, immediate: true });
383
390
 
391
+ // --- URL Sync Logic ---
392
+ if (urlSyncOptions?.router && urlSyncOptions?.route) {
393
+ const { router, route } = urlSyncOptions;
394
+
395
+ const areFacetsReady = () => facets.value && facets.value.length > 0;
396
+
397
+ const updateUrlFromState = () => {
398
+ if (!areFacetsReady()) return;
399
+
400
+ const query = { ...route.query };
401
+
402
+ // Clean up params that we manage, preserving others
403
+ delete query.sort;
404
+ delete query.search;
405
+ facets.value.forEach(group => delete query[group.uid]);
406
+
407
+ // Add sort if it's not the default
408
+ if (selectedSort.value && selectedSort.value !== initialSortType) {
409
+ query.sort = selectedSort.value;
410
+ }
411
+
412
+ // Add search if present
413
+ if (searchValue.value) {
414
+ query.search = searchValue.value;
415
+ }
416
+
417
+ // Add selected facets
418
+ selectedFacets.value.forEach(group => {
419
+ if (group.children.length > 0) {
420
+ query[group.uid] = group.children.map(c => c.uid).join(',');
421
+ }
422
+ });
423
+
424
+ if (JSON.stringify(query) !== JSON.stringify(route.query)) {
425
+ router.push({ query });
426
+ }
427
+ };
428
+
429
+ const updateStateFromUrl = () => {
430
+ const query = route.query;
431
+
432
+ if (query.sort) {
433
+ selectedSort.value = query.sort;
434
+ }
435
+ if (query.search) {
436
+ searchValue.value = query.search;
437
+ }
438
+
439
+ const selectionsFromUrl = new Map();
440
+ facets.value.forEach(group => {
441
+ const selectedUids = query[group.uid] ? query[group.uid].split(',') : [];
442
+ selectionsFromUrl.set(group.uid, new Set(selectedUids));
443
+ });
444
+
445
+ facets.value.forEach(group => {
446
+ const shouldBeSelected = selectionsFromUrl.get(group.uid) || new Set();
447
+ group.children.forEach(child => {
448
+ const isSelected = child.selected;
449
+ const shouldBe = shouldBeSelected.has(child.uid);
450
+ if (isSelected !== shouldBe) {
451
+ handleFacetChange({ groupUid: group.uid, facetUid: child.uid, selected: shouldBe });
452
+ }
453
+ });
454
+ });
455
+ };
456
+
457
+ // Initial sync from URL
458
+ const stopInitWatcher = watchPostEffect(() => {
459
+ if (facets.value && facets.value.length > 0) {
460
+ updateStateFromUrl();
461
+ stopInitWatcher();
462
+ }
463
+ });
464
+
465
+ // Sync state changes TO the URL
466
+ watch([selectedSort, searchValue, selectedFacets], updateUrlFromState, { deep: true });
467
+
468
+ // Sync URL changes TO the state
469
+ watch(() => route.query, updateStateFromUrl);
470
+ }
471
+
384
472
  return {
385
473
  facets,
386
474
  searchValue,
@@ -391,4 +479,4 @@ export function useFacets(allItems, options = {}) {
391
479
  clearFilters,
392
480
  handleFacetChange
393
481
  };
394
- }
482
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.1.2-beta.7",
3
+ "version": "0.1.2-beta.9",
4
4
  "description": "A modular and tree-shakeable Vue 3 component library for the Ulu frontend",
5
5
  "type": "module",
6
6
  "files": [