@ulu/frontend-vue 0.1.0-beta.11 → 0.1.0-beta.13

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,46 +1,93 @@
1
- import { ref, computed } from 'vue';
1
+ import { ref, computed, watch } from 'vue';
2
2
  import Fuse from 'fuse.js';
3
3
 
4
- const sortAlpha = items => {
5
- const getTitle = i => (i.title || i.label || "");
6
- return items.sort((a, b) => getTitle(a).localeCompare(getTitle(b)));
4
+ /**
5
+ * Generates facet groups and their possible values from a list of items.
6
+ * @param {Array<Object>} allItems - The full list of items.
7
+ * @param {Array<Object>} facetFields - Configuration for which fields to create facets from.
8
+ * @returns {Array<Object>} The generated facet structure.
9
+ */
10
+ function generateInitialFacets(allItems, facetFields) {
11
+ if (!facetFields || !Array.isArray(facetFields)) return [];
12
+ return facetFields.map(group => {
13
+ const allValues = new Set();
14
+ const getValue = group.getValue || (item => item[group.uid]);
15
+
16
+ allItems.forEach(item => {
17
+ const value = getValue(item);
18
+ if (Array.isArray(value)) {
19
+ value.forEach(v => v && allValues.add(v));
20
+ } else if (value) {
21
+ allValues.add(value);
22
+ }
23
+ });
24
+
25
+ const getLabel = group.getLabel || (value => value);
26
+
27
+ return {
28
+ ...group,
29
+ children: [...allValues].sort().map(value => ({
30
+ uid: value,
31
+ label: getLabel(value),
32
+ selected: false
33
+ }))
34
+ };
35
+ });
7
36
  }
8
- const defaultSorts = {
9
- az: { text: "A-Z", sort: sortAlpha },
10
- za: { text: "Z-A", sort: items => sortAlpha(items).reverse() },
11
- };
37
+
12
38
 
13
39
  /**
14
40
  * A composable for handling client-side faceted search, filtering, and sorting.
15
41
  * @param {import('vue').Ref<Array<Object>>} allItems - A Vue ref containing the full list of items to be processed.
16
42
  * @param {Object} options - Configuration options for the composable.
17
- * @param {Array} [options.initialFacets=[]] - The initial configuration for the facets.
43
+ * @param {Array} [options.initialFacets] - The initial configuration for the facets. Can be generated automatically if `facetFields` is provided.
44
+ * @param {Array} [options.facetFields] - A simpler configuration to automatically generate facets from items. Each item can have `uid`, `name`, `open`, `getValue` and `getLabel`.
18
45
  * @param {String} [options.initialSearchValue=''] - The initial value for the search input.
19
46
  * @param {String} [options.initialSortType='az'] - The initial sort type.
20
47
  * @param {Boolean} [options.noDefaultSorts=false] - If true, the default 'A-Z' and 'Z-A' sorts will not be included.
21
48
  * @param {Object} [options.extraSortTypes={}] - Additional sort types to be merged with the default ones.
22
49
  * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
23
- * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item.
50
+ * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item. Should always return an array of values.
51
+ * @param {Function} [options.getSortValue] - A function to get the value to sort by from an item.
24
52
  */
25
53
  export function useFacets(allItems, options = {}) {
54
+ const defaultGetItemFacet = (item, uid) => {
55
+ const value = item[uid];
56
+ if (value === null || typeof value === 'undefined') return [];
57
+ return Array.isArray(value) ? value : [value];
58
+ };
59
+
26
60
  const {
27
- initialFacets = [],
61
+ initialFacets,
62
+ facetFields,
28
63
  initialSearchValue = '',
29
64
  initialSortType = 'az',
30
65
  noDefaultSorts = false,
31
66
  extraSortTypes = {},
32
67
  searchOptions: initialSearchOptions = {},
33
- getItemFacet = (item, uid) => item[uid]
68
+ getItemFacet = defaultGetItemFacet,
69
+ getSortValue = item => (item.title || item.label || "")
34
70
  } = options;
35
71
 
36
- // --- State ---
37
- const facets = ref(createFacets(initialFacets));
38
- const searchValue = ref(initialSearchValue);
39
- const selectedSort = ref(initialSortType);
72
+ const sortAlpha = items => {
73
+ return items.sort((a, b) => {
74
+ const va = getSortValue(a);
75
+ const vb = getSortValue(b);
76
+ if (va && vb) {
77
+ return String(valueA).localeCompare(String(valueB));
78
+ } else {
79
+ return va ? -1 : vb ? 1 : 0;
80
+ }
81
+ });
82
+ }
83
+ const defaultSorts = {
84
+ az: { text: "A-Z", sort: sortAlpha },
85
+ za: { text: "Z-A", sort: items => sortAlpha(items).reverse() },
86
+ };
40
87
 
41
88
  // --- Helpers ---
42
89
  function createFacets(initial) {
43
- return initial.map(group => ({
90
+ return (initial || []).map(group => ({
44
91
  ...group,
45
92
  open: group.open || false,
46
93
  children: group.children.map(facet => ({
@@ -51,6 +98,23 @@ export function useFacets(allItems, options = {}) {
51
98
  }));
52
99
  }
53
100
 
101
+ const generatedFacets = computed(() => {
102
+ if (!facetFields || !allItems.value?.length) return null;
103
+ return generateInitialFacets(allItems.value, facetFields);
104
+ });
105
+
106
+ // --- State ---
107
+ const facets = ref(createFacets(initialFacets || generatedFacets.value));
108
+ const searchValue = ref(initialSearchValue);
109
+ const selectedSort = ref(initialSortType);
110
+
111
+ // If using facetFields, watch for changes in items and regenerate facets
112
+ if (facetFields && !initialFacets) {
113
+ watch(generatedFacets, (newGeneratedFacets) => {
114
+ facets.value = createFacets(newGeneratedFacets);
115
+ });
116
+ }
117
+
54
118
  // --- Computed ---
55
119
  const sortTypes = computed(() => ({
56
120
  ...(noDefaultSorts ? {} : defaultSorts),
@@ -91,10 +155,12 @@ export function useFacets(allItems, options = {}) {
91
155
  return allItems.value;
92
156
  }
93
157
  return allItems.value.filter(item => {
94
- return selectedFacets.value.some(group => {
95
- const cats = getItemFacet(item, group.uid);
96
- if (cats && cats.length) {
97
- return group.children.some(facet => cats.includes(facet.uid));
158
+ // An item must match every active facet group
159
+ return selectedFacets.value.every(group => {
160
+ const itemFacetValues = getItemFacet(item, group.uid);
161
+ if (itemFacetValues && itemFacetValues.length) {
162
+ // An item can match any of the selected facets within a group
163
+ return group.children.some(facet => itemFacetValues.includes(facet.uid));
98
164
  }
99
165
  return false;
100
166
  });
@@ -120,7 +186,21 @@ export function useFacets(allItems, options = {}) {
120
186
 
121
187
  // --- Methods ---
122
188
  function clearFilters() {
123
- facets.value = createFacets(initialFacets);
189
+ facets.value.forEach(group => {
190
+ if (group.children) {
191
+ group.children.forEach(child => child.selected = false);
192
+ }
193
+ });
194
+ }
195
+
196
+ function handleFacetChange({ groupUid, facetUid, selected }) {
197
+ const group = facets.value.find(g => g.uid === groupUid);
198
+ if (group) {
199
+ const facet = group.children.find(f => f.uid === facetUid);
200
+ if (facet) {
201
+ facet.selected = selected;
202
+ }
203
+ }
124
204
  }
125
205
 
126
206
  return {
@@ -135,6 +215,7 @@ export function useFacets(allItems, options = {}) {
135
215
  selectedFacets,
136
216
 
137
217
  // Methods
138
- clearFilters
218
+ clearFilters,
219
+ handleFacetChange
139
220
  };
140
- }
221
+ }
@@ -1,3 +1,4 @@
1
+ export { useFacets } from './facets/useFacets.js';
1
2
  export { default as UluFacetsFilters } from './facets/UluFacetsFilters.vue';
2
3
  export { default as UluFacetsResults } from './facets/UluFacetsResults.vue';
3
4
  export { default as UluFacetsSearch } from './facets/UluFacetsSearch.vue';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.1.0-beta.11",
3
+ "version": "0.1.0-beta.13",
4
4
  "description": "A modular and tree-shakeable Vue 3 component library for the Ulu frontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,18 @@
1
+ export const initialMockFacets: {
2
+ name: string;
3
+ uid: string;
4
+ open: boolean;
5
+ children: {
6
+ uid: string;
7
+ label: string;
8
+ }[];
9
+ }[];
10
+ export const mockItems: {
11
+ id: number;
12
+ title: string;
13
+ description: string;
14
+ category: string[];
15
+ author: string[];
16
+ date: Date;
17
+ }[];
18
+ //# sourceMappingURL=_mock-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_mock-data.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/facets/_mock-data.js"],"names":[],"mappings":"AAAA;;;;;;;;IAwBE;AAEF;;;;;;;IAaE"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A composable for handling client-side faceted search, filtering, and sorting.
3
+ * @param {import('vue').Ref<Array<Object>>} allItems - A Vue ref containing the full list of items to be processed.
4
+ * @param {Object} options - Configuration options for the composable.
5
+ * @param {Array} [options.initialFacets] - The initial configuration for the facets. Can be generated automatically if `facetFields` is provided.
6
+ * @param {Array} [options.facetFields] - A simpler configuration to automatically generate facets from items. Each item can have `uid`, `name`, `open`, `getValue` and `getLabel`.
7
+ * @param {String} [options.initialSearchValue=''] - The initial value for the search input.
8
+ * @param {String} [options.initialSortType='az'] - The initial sort type.
9
+ * @param {Boolean} [options.noDefaultSorts=false] - If true, the default 'A-Z' and 'Z-A' sorts will not be included.
10
+ * @param {Object} [options.extraSortTypes={}] - Additional sort types to be merged with the default ones.
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
+ * @param {Function} [options.getSortValue] - A function to get the value to sort by from an item.
14
+ */
15
+ export function useFacets(allItems: import("vue").Ref<Array<any>>, options?: {
16
+ initialFacets?: any[];
17
+ facetFields?: any[];
18
+ initialSearchValue?: string;
19
+ initialSortType?: string;
20
+ noDefaultSorts?: boolean;
21
+ extraSortTypes?: any;
22
+ searchOptions?: any;
23
+ getItemFacet?: Function;
24
+ getSortValue?: Function;
25
+ }): {
26
+ facets: import("vue").Ref<any, any>;
27
+ searchValue: import("vue").Ref<string, string>;
28
+ selectedSort: import("vue").Ref<string, string>;
29
+ sortTypes: import("vue").ComputedRef<any>;
30
+ displayItems: import("vue").ComputedRef<any>;
31
+ selectedFacets: import("vue").ComputedRef<any[]>;
32
+ clearFilters: () => void;
33
+ handleFacetChange: ({ groupUid, facetUid, selected }: {
34
+ groupUid: any;
35
+ facetUid: any;
36
+ selected: any;
37
+ }) => void;
38
+ };
39
+ //# sourceMappingURL=useFacets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFacets.d.ts","sourceRoot":"","sources":["../../../../lib/components/systems/facets/useFacets.js"],"names":[],"mappings":"AAsCA;;;;;;;;;;;;;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;;;;;;;;;;;;;EAyKA"}
@@ -1,2 +1,2 @@
1
- export {};
1
+ export { useFacets } from "./facets/useFacets.js";
2
2
  //# sourceMappingURL=index.d.ts.map