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

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.
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div class="UluFacetsFilters">
3
+ <UluCollapsibleRegion
4
+ class="UluFacets__group"
5
+ :class="classes.group"
6
+ :classToggle="['UluFacets__group-toggle', classes.groupToggle]"
7
+ :classContent="['UluFacets__group-content', classes.groupContent]"
8
+ v-for="group in facets"
9
+ :key="group.uid"
10
+ :group="group"
11
+ :startOpen="group.open"
12
+ :clickOutsideCloses="false"
13
+ :closeOnEscape="false"
14
+ :transitionHeight="true"
15
+ >
16
+ <template #toggle="{ isOpen }">
17
+ <slot name="groupToggle" :group="group" :isOpen="isOpen">
18
+ {{ group.name }}
19
+ </slot>
20
+ </template>
21
+ <template #default>
22
+ <UluFacetsList
23
+ :children="group.children.slice(0, maxVisible)"
24
+ :groupUid="group.uid"
25
+ :classFacet="classes.facet"
26
+ @facet-change="emit('facet-change', $event)"
27
+ />
28
+ <UluCollapsibleRegion
29
+ v-if="group.children.length > maxVisible"
30
+ class="UluFacets__more-facets"
31
+ :class="classes.moreFacets"
32
+ :clickOutsideCloses="false"
33
+ :closeOnEscape="false"
34
+ :transitionHeight="true"
35
+ >
36
+ <template #toggle="{ isOpen }">
37
+ {{ isOpen ? "- Less" : "+ More" }}
38
+ </template>
39
+ <template #default>
40
+ <UluFacetsList
41
+ :children="group.children.slice(maxVisible)"
42
+ :groupUid="group.uid"
43
+ :classFacet="classes.facet"
44
+ @facet-change="emit('facet-change', $event)"
45
+ />
46
+ </template>
47
+ </UluCollapsibleRegion>
48
+ </template>
49
+ </UluCollapsibleRegion>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup>
54
+ import UluFacetsList from "./UluFacetsList.vue";
55
+ import UluCollapsibleRegion from "../../collapsible/UluCollapsibleRegion.vue";
56
+
57
+ defineProps({
58
+ classes: {
59
+ type: Object,
60
+ default: () => ({})
61
+ },
62
+ maxVisible: {
63
+ type: Number,
64
+ default: 5
65
+ },
66
+ facets: {
67
+ type: Array,
68
+ default: () => []
69
+ }
70
+ });
71
+
72
+ const emit = defineEmits(['facet-change']);
73
+ </script>
@@ -10,7 +10,8 @@
10
10
  class="UluFacets__facet-checkbox"
11
11
  :id="facetCheckboxId(facet)"
12
12
  type="checkbox"
13
- v-model="facet.selected"
13
+ :checked="facet.selected"
14
+ @change="emit('facet-change', { groupUid, facetUid: facet.uid, selected: $event.target.checked })"
14
15
  >
15
16
  <label
16
17
  class="UluFacets__facet-label"
@@ -22,18 +23,16 @@
22
23
  </ul>
23
24
  </template>
24
25
 
25
- <script>
26
- export default {
27
- name: 'UluFacetsList',
28
- props: {
29
- groupUid: String,
30
- children: Array,
31
- classFacet: String
32
- },
33
- methods: {
34
- facetCheckboxId(facet) {
35
- return `facet-${ this.groupUid }-${ facet.uid }`;
36
- }
37
- }
26
+ <script setup>
27
+ const props = defineProps({
28
+ groupUid: String,
29
+ children: Array,
30
+ classFacet: String
31
+ });
32
+
33
+ const emit = defineEmits(['facet-change']);
34
+
35
+ function facetCheckboxId(facet) {
36
+ return `facet-${props.groupUid}-${facet.uid}`;
38
37
  }
39
38
  </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div class="UluFacetsResults">
3
+ <transition-group
4
+ v-if="items.length"
5
+ :tag="tag"
6
+ :name="transitionName"
7
+ class="UluFacetsResults__list"
8
+ >
9
+ <li
10
+ class="UluFacetsResults__item"
11
+ v-for="(item, index) in items"
12
+ :key="item.id || index"
13
+ >
14
+ <slot name="item" :item="item" :index="index"></slot>
15
+ </li>
16
+ </transition-group>
17
+ <div v-else class="UluFacetsResults__empty">
18
+ <slot name="empty">
19
+ <p>No matching items found.</p>
20
+ </slot>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup>
26
+ defineProps({
27
+ items: {
28
+ type: Array,
29
+ required: true
30
+ },
31
+ tag: {
32
+ type: String,
33
+ default: 'ul'
34
+ },
35
+ transitionName: {
36
+ type: String,
37
+ default: 'UluFacetsFade'
38
+ }
39
+ });
40
+ </script>
41
+
42
+ <style lang="scss">
43
+ .UluFacetsResults__list {
44
+ list-style: none;
45
+ padding: 0;
46
+ }
47
+
48
+ .UluFacetsFade-enter-active,
49
+ .UluFacetsFade-leave-active {
50
+ transition: opacity 0.25s ease;
51
+ }
52
+
53
+ .UluFacetsFade-enter-from,
54
+ .UluFacetsFade-leave-to {
55
+ opacity: 0;
56
+ }
57
+ </style>
@@ -10,58 +10,35 @@
10
10
  type="text"
11
11
  :placeholder="placeholder"
12
12
  >
13
- <!-- <button
14
- v-if="value"
15
- :class="classes.searchClear"
16
- @click="clear"
17
- :aria-label="classes.searchClearIcon ? 'Clear Search' : false"
18
- type="button"
19
- >
20
- <span
21
- v-if="classes.searchClearIcon"
22
- :class="classes.searchClearIcon"
23
- aria-hidden="true"
24
- ></span>
25
- <span v-else>
26
- Clear
27
- </span>
28
- </button> -->
29
13
  </div>
30
14
  </template>
31
15
 
32
- <script>
33
- let uid = 0;
34
- export default {
35
- name: 'UluFacetsSearch',
36
- props: {
37
- classes: Object,
38
- modelValue: String,
39
- placeholder: {
40
- type: String,
41
- default: "Keywords…"
42
- }
43
- },
44
- data() {
45
- return {
46
- id: `facet-view-keyword-${ ++uid }`
47
- }
16
+ <script setup>
17
+ import { computed } from 'vue';
18
+
19
+ const props = defineProps({
20
+ classes: {
21
+ type: Object,
22
+ default: () => ({})
48
23
  },
49
- computed: {
50
- localValue: {
51
- get() {
52
- return this.modelValue;
53
- },
54
- set(val) {
55
- this.$emit('update:modelValue', val);
56
- }
57
- }
24
+ modelValue: String,
25
+ placeholder: {
26
+ type: String,
27
+ default: "Keywords…"
28
+ }
29
+ });
30
+
31
+ const emit = defineEmits(['update:modelValue']);
32
+
33
+ let uid = 0;
34
+ const id = `facet-view-keyword-${++uid}`;
35
+
36
+ const localValue = computed({
37
+ get() {
38
+ return props.modelValue;
58
39
  },
59
- methods: {
60
- clear() {
61
- // this.value = null;
62
- // this.applied = false;
63
- // this.$emit("search", null);
64
- }
40
+ set(val) {
41
+ emit('update:modelValue', val);
65
42
  }
66
- }
67
- </script>
43
+ });
44
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="UluFacetsSidebarLayout">
3
+ <div class="UluFacetsSidebarLayout__header">
4
+ <slot name="header"></slot>
5
+ </div>
6
+ <div class="UluFacetsSidebarLayout__body">
7
+ <div class="UluFacetsSidebarLayout__sidebar">
8
+ <slot name="sidebar"></slot>
9
+ </div>
10
+ <div class="UluFacetsSidebarLayout__main">
11
+ <slot name="main"></slot>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup>
18
+ // This component is purely for layout, no logic needed.
19
+ </script>
20
+
21
+ <style lang="scss">
22
+ .UluFacetsSidebarLayout__body {
23
+ display: grid;
24
+ grid-template-columns: 1fr;
25
+ gap: 2rem;
26
+
27
+ @media (min-width: 768px) {
28
+ grid-template-columns: 250px 1fr;
29
+ }
30
+ }
31
+ </style>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div class="UluFacetsSort" :class="classes.sortForm">
3
+ <label
4
+ :for="sortId"
5
+ :class="classes.sortFormLabel"
6
+ >
7
+ <slot>Sort:</slot>
8
+ </label>
9
+ <select
10
+ :value="modelValue"
11
+ @change="emit('update:modelValue', $event.target.value)"
12
+ :id="sortId"
13
+ :class="classes.sortFormSelect"
14
+ >
15
+ <option v-for="(item, key) in sortTypes" :value="key" :key="key">
16
+ {{ item.text }}
17
+ </option>
18
+ </select>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { ref } from 'vue';
24
+
25
+ let idCounter = 0;
26
+
27
+ defineProps({
28
+ classes: {
29
+ type: Object,
30
+ default: () => ({})
31
+ },
32
+ sortTypes: {
33
+ type: Object,
34
+ default: () => ({})
35
+ },
36
+ modelValue: {
37
+ type: String,
38
+ default: ''
39
+ }
40
+ });
41
+
42
+ const emit = defineEmits(['update:modelValue']);
43
+
44
+ const sortId = ref(`ulu-facet-sort-${++idCounter}`);
45
+ </script>
@@ -0,0 +1,40 @@
1
+ export const initialMockFacets = [
2
+ {
3
+ name: 'Category',
4
+ uid: 'category',
5
+ open: true,
6
+ children: [
7
+ { uid: 'cat1', label: 'Design' },
8
+ { uid: 'cat2', label: 'Development' },
9
+ { uid: 'cat3', label: 'Marketing' },
10
+ { uid: 'cat4', label: 'Business' },
11
+ { uid: 'cat5', label: 'Lifestyle' },
12
+ { uid: 'cat6', label: 'Technology' },
13
+ ]
14
+ },
15
+ {
16
+ name: 'Author',
17
+ uid: 'author',
18
+ open: true,
19
+ children: [
20
+ { uid: 'jane-doe', label: 'Jane Doe' },
21
+ { uid: 'john-smith', label: 'John Smith' },
22
+ { uid: 'peter-jones', label: 'Peter Jones' },
23
+ ]
24
+ }
25
+ ];
26
+
27
+ export const mockItems = [
28
+ { id: 1, title: 'The Art of UI Design', description: 'A deep dive into creating beautiful user interfaces.', category: ['cat1', 'cat2'], author: ['jane-doe'], date: new Date(2023, 5, 15) },
29
+ { id: 2, title: 'Vue.js for Beginners', description: 'Getting started with the popular JavaScript framework.', category: ['cat2', 'cat6'], author: ['john-smith'], date: new Date(2023, 8, 22) },
30
+ { id: 3, title: 'Content Marketing Strategies', description: 'How to attract and retain customers with great content.', category: ['cat3'], author: ['peter-jones'], date: new Date(2022, 11, 10) },
31
+ { id: 4, title: 'Startup Funding 101', description: 'A guide to raising capital for your new venture.', category: ['cat4'], author: ['jane-doe'], date: new Date(2024, 1, 5) },
32
+ { id: 5, title: 'Minimalist Living', description: 'Declutter your life and find more happiness.', category: ['cat5'], author: ['john-smith'], date: new Date(2023, 3, 30) },
33
+ { id: 6, title: 'The Future of AI', description: 'Exploring the impact of artificial intelligence on society.', category: ['cat6'], author: ['peter-jones'], date: new Date(2024, 0, 1) },
34
+ { id: 7, title: 'Advanced CSS Techniques', description: 'Take your styling skills to the next level.', category: ['cat1', 'cat2'], author: ['jane-doe'], date: new Date(2023, 10, 18) },
35
+ { id: 8, title: 'Building a Scalable API', description: 'Best practices for designing and implementing APIs.', category: ['cat2', 'cat6'], author: ['john-smith'], date: new Date(2023, 7, 3) },
36
+ { id: 9, title: 'Social Media for Business', description: 'Leveraging social platforms for growth.', category: ['cat3'], author: ['peter-jones'], date: new Date(2022, 9, 14) },
37
+ { id: 10, title: 'Negotiation and Deal Making', description: 'Master the art of getting what you want.', category: ['cat4'], author: ['jane-doe'], date: new Date(2023, 6, 25) },
38
+ { id: 11, title: 'Healthy Eating Habits', description: 'A guide to a balanced and nutritious diet.', category: ['cat5'], author: ['john-smith'], date: new Date(2024, 2, 12) },
39
+ { id: 12, title: 'Quantum Computing Explained', description: 'A simple introduction to a complex topic.', category: ['cat6'], author: ['peter-jones'], date: new Date(2023, 9, 9) },
40
+ ];
@@ -0,0 +1,140 @@
1
+ import { ref, computed } from 'vue';
2
+ import Fuse from 'fuse.js';
3
+
4
+ const sortAlpha = items => {
5
+ const getTitle = i => (i.title || i.label || "");
6
+ return items.sort((a, b) => getTitle(a).localeCompare(getTitle(b)));
7
+ }
8
+ const defaultSorts = {
9
+ az: { text: "A-Z", sort: sortAlpha },
10
+ za: { text: "Z-A", sort: items => sortAlpha(items).reverse() },
11
+ };
12
+
13
+ /**
14
+ * A composable for handling client-side faceted search, filtering, and sorting.
15
+ * @param {import('vue').Ref<Array<Object>>} allItems - A Vue ref containing the full list of items to be processed.
16
+ * @param {Object} options - Configuration options for the composable.
17
+ * @param {Array} [options.initialFacets=[]] - The initial configuration for the facets.
18
+ * @param {String} [options.initialSearchValue=''] - The initial value for the search input.
19
+ * @param {String} [options.initialSortType='az'] - The initial sort type.
20
+ * @param {Boolean} [options.noDefaultSorts=false] - If true, the default 'A-Z' and 'Z-A' sorts will not be included.
21
+ * @param {Object} [options.extraSortTypes={}] - Additional sort types to be merged with the default ones.
22
+ * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
23
+ * @param {Function} [options.getItemFacet] - A function to retrieve facet information from an item.
24
+ */
25
+ export function useFacets(allItems, options = {}) {
26
+ const {
27
+ initialFacets = [],
28
+ initialSearchValue = '',
29
+ initialSortType = 'az',
30
+ noDefaultSorts = false,
31
+ extraSortTypes = {},
32
+ searchOptions: initialSearchOptions = {},
33
+ getItemFacet = (item, uid) => item[uid]
34
+ } = options;
35
+
36
+ // --- State ---
37
+ const facets = ref(createFacets(initialFacets));
38
+ const searchValue = ref(initialSearchValue);
39
+ const selectedSort = ref(initialSortType);
40
+
41
+ // --- Helpers ---
42
+ function createFacets(initial) {
43
+ return initial.map(group => ({
44
+ ...group,
45
+ open: group.open || false,
46
+ children: group.children.map(facet => ({
47
+ ...facet,
48
+ selected: facet.selected || false
49
+ })),
50
+ selectedCount: 0
51
+ }));
52
+ }
53
+
54
+ // --- Computed ---
55
+ const sortTypes = computed(() => ({
56
+ ...(noDefaultSorts ? {} : defaultSorts),
57
+ ...extraSortTypes
58
+ }));
59
+
60
+ const searchOptions = computed(() => ({
61
+ shouldSort: true,
62
+ keys: ["title", "label", "description", "author"],
63
+ ...initialSearchOptions
64
+ }));
65
+
66
+ const selectedFacets = computed(() => {
67
+ const selected = [];
68
+ facets.value.forEach((group) => {
69
+ const { name, uid, children } = group;
70
+ let count = 0;
71
+ let added = false;
72
+ if (children) {
73
+ children.forEach(child => {
74
+ if (child.selected) {
75
+ ++count;
76
+ if (!added) {
77
+ selected.push({ uid, name, children: [] });
78
+ added = true;
79
+ }
80
+ selected[selected.length - 1].children.push(child);
81
+ }
82
+ });
83
+ }
84
+ group.selectedCount = count;
85
+ });
86
+ return selected;
87
+ });
88
+
89
+ const filteredItems = computed(() => {
90
+ if (!selectedFacets.value.length) {
91
+ return allItems.value;
92
+ }
93
+ 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));
98
+ }
99
+ return false;
100
+ });
101
+ });
102
+ });
103
+
104
+ const searchedItems = computed(() => {
105
+ if (!searchValue.value?.length) {
106
+ return filteredItems.value;
107
+ }
108
+ const fuse = new Fuse(filteredItems.value, searchOptions.value);
109
+ return fuse.search(searchValue.value).map(result => result.item);
110
+ });
111
+
112
+ const displayItems = computed(() => {
113
+ const sortFn = sortTypes.value[selectedSort.value]?.sort;
114
+ if (typeof sortFn !== 'function') {
115
+ return searchedItems.value;
116
+ }
117
+ // The sort function should not mutate the original array
118
+ return sortFn([...searchedItems.value]);
119
+ });
120
+
121
+ // --- Methods ---
122
+ function clearFilters() {
123
+ facets.value = createFacets(initialFacets);
124
+ }
125
+
126
+ return {
127
+ // State
128
+ facets,
129
+ searchValue,
130
+ selectedSort,
131
+ sortTypes,
132
+
133
+ // Computed
134
+ displayItems,
135
+ selectedFacets,
136
+
137
+ // Methods
138
+ clearFilters
139
+ };
140
+ }
@@ -1,5 +1,8 @@
1
- export { default as UluFacets } from './facets/UluFacets.vue';
1
+ export { default as UluFacetsFilters } from './facets/UluFacetsFilters.vue';
2
+ export { default as UluFacetsResults } from './facets/UluFacetsResults.vue';
2
3
  export { default as UluFacetsSearch } from './facets/UluFacetsSearch.vue';
4
+ export { default as UluFacetsSidebarLayout } from './facets/UluFacetsSidebarLayout.vue';
5
+ export { default as UluFacetsSort } from './facets/UluFacetsSort.vue';
3
6
  export { default as UluFacetsList } from './facets/UluFacetsList.vue';
4
7
  export { default as UluScrollAnchors } from './scroll-anchors/UluScrollAnchors.vue';
5
8
  export { default as UluScrollAnchorsNav } from './scroll-anchors/UluScrollAnchorsNav.vue';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulu/frontend-vue",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.11",
4
4
  "description": "A modular and tree-shakeable Vue 3 component library for the Ulu frontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,380 +0,0 @@
1
- <template>
2
- <div class="UluFacets">
3
- <div class="UluFacets__header" :class="classes.header">
4
- <slot name="header" :count="filteredItems.length"></slot>
5
- <div class="UluFacets__header-actions" :class="classes.headerActions">
6
- <button
7
- @click="toggleFilterVisibility"
8
- :class="classes.buttonFilterToggle"
9
- :aria-controls="filterId"
10
- :aria-expanded="filtersHidden ? 'false' : 'true'"
11
- type="button"
12
- >
13
- <slot name="buttonFilterToggle" :hidden="filtersHidden">
14
- {{ filtersHidden ? 'Show' : 'Hide' }} Filters
15
- </slot>
16
- </button>
17
- <button
18
- v-if="selectedFacets.length"
19
- @click="clearFilters"
20
- :class="classes.buttonClearFilters"
21
- type="button"
22
- >
23
- <slot name="buttonClearFilters">
24
- Clear Filters
25
- </slot>
26
- </button>
27
- <div :class="classes.sortForm">
28
- <label
29
- :for="sortId"
30
- :class="classes.sortFormLabel"
31
- >Sort:</label>
32
- <select
33
- v-model="selectedSort"
34
- :id="sortId"
35
- :class="classes.sortFormSelect"
36
- >
37
- <option v-for="(item, key) in sortTypes" :value="key" :key="key">
38
- {{ item.text }}
39
- </option>
40
- </select>
41
- </div>
42
- </div>
43
- </div>
44
- <div class="UluFacets__body">
45
- <transition name="UluFacetsFade" mode="out-in">
46
- <div
47
- v-show="!filtersHidden"
48
- class="UluFacets__filters"
49
- :id="filterId"
50
- :class="{ 'UluFacets__filters--hidden' : filtersHidden }"
51
- >
52
- <UluFacetsSearch
53
- :classes="classes"
54
- :initialValue="initialSearchValue"
55
- :placeholder="searchPlaceholder"
56
- v-model="searchValue"
57
- />
58
- <UluCollapsibleRegion
59
- class="UluFacets__group"
60
- :class="classes.group"
61
- :classToggle="['UluFacets__group-toggle', classes.groupToggle]"
62
- :classContent="['UluFacets__group-content', classes.groupContent]"
63
- v-for="group in facets"
64
- :key="group.uid"
65
- :group="group"
66
- :startOpen="group.open"
67
- :clickOutsideCloses="false"
68
- :closeOnEscape="false"
69
- :transitionHeight="true"
70
- >
71
- <template #toggle="{ isOpen }">
72
- <slot name="groupToggle" :group="group" :isOpen="isOpen">
73
- {{ group.name }}
74
- </slot>
75
- </template>
76
- <template #default>
77
- <UluFacetsList
78
- :children="group.children.slice(0, maxVisible)"
79
- :groupUid="group.uid"
80
- :classFacet="classes.facet"
81
- />
82
- <UluCollapsibleRegion
83
- v-if="group.children.length > maxVisible"
84
- class="UluFacets__more-facets"
85
- :class="classes.moreFacets"
86
- :clickOutsideCloses="false"
87
- :closeOnEscape="false"
88
- :transitionHeight="true"
89
- >
90
- <template #toggle="{ isOpen }">
91
- {{ isOpen ? "- Less" : "+ More" }}
92
- </template>
93
- <template #default>
94
- <UluFacetsList
95
- :children="group.children.slice(maxVisible)"
96
- :groupUid="group.uid"
97
- :classFacet="classes.facet"
98
- />
99
- </template>
100
- </UluCollapsibleRegion>
101
- </template>
102
- </UluCollapsibleRegion>
103
- </div>
104
- </transition>
105
- <transition name="UluFacetsFade" mode="out-in">
106
- <ul
107
- class="UluFacets__results"
108
- :class="classes.results"
109
- :key="filterIteration"
110
- v-if="resultsVisible && filteredItems.length"
111
- >
112
-
113
- <li
114
- class="UluFacets__results-item"
115
- :class="classes.resultsItem"
116
- v-for="(item, index) in filteredItems"
117
- :key="index"
118
- >
119
- <slot name="item" :item="item" :index="index"></slot>
120
- </li>
121
- </ul>
122
- <div v-else class="UluFacets__empty">
123
- <slot name="empty">
124
- No Results Found
125
- </slot>
126
- </div>
127
- </transition>
128
- <!-- <div class="UluFacets__pagination"></div> -->
129
- </div>
130
- </div>
131
- </template>
132
-
133
- <script>
134
- import Fuse from 'fuse.js';
135
- import UluFacetsList from "./UluFacetsList.vue";
136
- import UluFacetsSearch from "./UluFacetsSearch.vue";
137
- import UluCollapsibleRegion from "../../collapsible/UluCollapsibleRegion.vue";
138
-
139
- let idCounter = 0;
140
- const sortAlpha = items => {
141
- const getTitle = i => (i.title || i.label || "");
142
- return items.sort((a, b) => getTitle(a).localeCompare(getTitle(b)));
143
- }
144
- const defaultSorts = {
145
- az: { text: "A-Z", sort: sortAlpha },
146
- za: { text: "Z-A", sort: items => sortAlpha(items).reverse() },
147
- };
148
- export default {
149
- name: 'UluFacets',
150
- components: {
151
- UluCollapsibleRegion,
152
- UluFacetsList,
153
- UluFacetsSearch
154
- },
155
- props: {
156
- /**
157
- * Options passed to fuse js for search feature
158
- */
159
- searchOptions: {
160
- type: Object,
161
- default: () => ({
162
- // isCaseSensitive: false,
163
- // includeScore: false,
164
- shouldSort: true,
165
- // includeMatches: false,
166
- // findAllMatches: false,
167
- // minMatchCharLength: 1,
168
- // location: 0,
169
- // threshold: 0.6,
170
- // distance: 100,
171
- // useExtendedSearch: false,
172
- // ignoreLocation: false,
173
- // ignoreFieldNorm: false,
174
- // fieldNormWeight: 1,
175
- keys: [
176
- "title",
177
- "label",
178
- "description",
179
- "author"
180
- ]
181
- })
182
- },
183
- initialFiltersHidden: Boolean,
184
- searchPlaceholder: String,
185
- /**
186
- * Array of facet configurations
187
- */
188
- initialFacets: {
189
- required: true,
190
- type: Array
191
- },
192
- initialSearchValue: String,
193
- classes: {
194
- type: Object,
195
- required: false,
196
- default: () => ({})
197
- },
198
- /**
199
- * Maximum facets shown per group before truncating
200
- */
201
- maxVisible: {
202
- type: Number,
203
- default: 5
204
- },
205
- /**
206
- * Array of objects of the items to display
207
- */
208
- items: {
209
- required: true,
210
- type: Array
211
- },
212
- /**
213
- * Provides a way to find categories for each facet
214
- * @param {Object} item An item to lookup the facet/category info for
215
- * @param {String} uid The facet's uid (the categories uid) to return a value, value should be an array of facet (child) keys
216
- */
217
- getItemFacet: {
218
- type: Function,
219
- default: (item, uid) => item[uid]
220
- },
221
- /**
222
- * Return the value for an item to use for sorting alphabetically
223
- */
224
- getItemSortAlpha: {
225
- type: Function,
226
- default: item => (item.title || item.label || "")
227
- },
228
- initialSortType: {
229
- type: String,
230
- default: "az"
231
- },
232
- noDefaultSorts: Boolean,
233
- extraSortTypes: {
234
- type: Object,
235
- default: () => ({})
236
- }
237
- },
238
- data() {
239
- const {
240
- initialFiltersHidden,
241
- initialSearchValue,
242
- noDefaultSorts,
243
- initialSortType,
244
- extraSortTypes
245
- } = this;
246
- return {
247
- filterId: `ulu-facet-filters-${ ++idCounter }`,
248
- sortId: `ulu-facet-sort-${ ++idCounter }`,
249
- selectedSort: initialSortType,
250
- sortTypes: {
251
- ...(noDefaultSorts ? {} : defaultSorts),
252
- ...extraSortTypes
253
- },
254
- facets: this.createFacets(), // Copy of users facet configs
255
- filtersHidden: initialFiltersHidden || false,
256
- searchValue: initialSearchValue || null,
257
- resultsVisible: true,
258
- filterIteration: 0,
259
- }
260
- },
261
- computed: {
262
- /**
263
- * Returns an array of groups with children that are active
264
- */
265
- selectedFacets() {
266
- const selected = [];
267
- this.facets.forEach((group) => {
268
- const { name, uid, children } = group;
269
- let count = 0;
270
- let added = false;
271
- if (children) {
272
- children.forEach(child => {
273
- if (child.selected) {
274
- ++count;
275
- if (!added) {
276
- selected.push({ uid, name, children: [] });
277
- added = true;
278
- }
279
- selected[selected.length - 1].children.push(child);
280
- }
281
- });
282
- }
283
- group.selectedCount = count;
284
- });
285
- return selected;
286
- },
287
- filteredItems() {
288
- this.resultsVisible = false;
289
- const { getItemFacet, selectedFacets, sortTypes, selectedSort } = this;
290
- const sort = sortTypes[selectedSort].sort;
291
-
292
- const filteredItems = this.items.filter(item => {
293
- if (selectedFacets.length) {
294
- return selectedFacets.some(group => {
295
- let matched;
296
- const cats = getItemFacet(item, group.uid);
297
- if (cats && cats.length) {
298
- matched = group.children.some(facet => cats.includes(facet.uid));
299
- }
300
- return matched;
301
- });
302
- // No filters are applied
303
- } else {
304
- return true;
305
- }
306
- });
307
- // Increment counter (used for transitions)
308
- // this.filterIteration = filterIteration + 1;
309
- const newItems = sort(this.search(filteredItems));
310
- // this.resultsVisible = false;
311
- this.$nextTick(() => {
312
- this.resultsVisible = true;
313
- this.filterIteration = this.filterIteration + 1;
314
- // this.$nextTick(() => this.resultsVisible = true);
315
- });
316
- return newItems;
317
- }
318
- },
319
- methods: {
320
- /**
321
- * Resets all active filters to user's initial
322
- */
323
- clearFilters() {
324
- this.facets = this.createFacets();
325
- },
326
- /**
327
- * Maps users initial facets to the local facet array used in this component
328
- */
329
- createFacets() {
330
- return this.initialFacets.map(group => {
331
- const children = group.children.map(facet => ({
332
- ...facet,
333
- selected: facet.selected || false
334
- }));
335
- return {
336
- ...group,
337
- open: group.open || false,
338
- children,
339
- selectedCount: 0
340
- };
341
- })
342
- },
343
- /**
344
- * Search applied to an already filtered batch of items
345
- */
346
- search(items) {
347
- const { searchValue, searchOptions } = this;
348
- if (!searchValue?.length) return items;
349
- const fuse = new Fuse(items, searchOptions);
350
- const results = fuse.search(searchValue);
351
- return results.map(result => result.item);
352
- },
353
- toggleFilterVisibility() {
354
- this.filtersHidden = !this.filtersHidden;
355
- }
356
- }
357
- }
358
- </script>
359
-
360
- <style lang="scss">
361
- .UluFacets__more-facets {
362
- display: flex;
363
- flex-direction: column;
364
- &.UluCollapsibleRegion--open,
365
- &.UluCollapsibleRegion--transitioning {
366
- .UluCollapsibleRegion__content {
367
- order: -1;
368
- }
369
- }
370
- }
371
- .UluFacetsFade-enter-active,
372
- .UluFacetsFade-leave-active {
373
- transition: opacity 0.25s ease;
374
- }
375
-
376
- .UluFacetsFade-enter-from,
377
- .UluFacetsFade-leave-to {
378
- opacity: 0;
379
- }
380
- </style>