@ulu/frontend-vue 0.1.0-beta.3 → 0.1.0-beta.30

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.
Files changed (151) hide show
  1. package/README.md +113 -2
  2. package/dist/{breakpoints-Cq2oSdYS.js → breakpoints-O2rDlMRj.js} +1 -1
  3. package/dist/frontend-vue.css +1 -1
  4. package/dist/frontend-vue.js +78 -72
  5. package/dist/index-BkFIm5Ds.js +6941 -0
  6. package/lib/components/collapsible/UluAccordion.vue +1 -1
  7. package/lib/components/collapsible/UluModal.vue +4 -5
  8. package/lib/components/collapsible/UluOverflowPopover.vue +1 -1
  9. package/lib/components/elements/UluAlert.vue +1 -2
  10. package/lib/components/elements/UluBadge.vue +27 -28
  11. package/lib/components/elements/UluBadgeStack.vue +8 -13
  12. package/lib/components/elements/UluButton.vue +2 -2
  13. package/lib/components/elements/UluButtonVerbose.vue +119 -0
  14. package/lib/components/elements/UluCard.vue +1 -1
  15. package/lib/components/elements/UluDefinitionList.vue +14 -17
  16. package/lib/components/elements/UluExternalLink.vue +22 -29
  17. package/lib/components/elements/UluIcon.vue +22 -17
  18. package/lib/components/elements/UluList.vue +53 -55
  19. package/lib/components/elements/UluSpokeSpinner.vue +12 -18
  20. package/lib/components/elements/UluTag.vue +35 -35
  21. package/lib/components/forms/UluFileDisplay.vue +40 -31
  22. package/lib/components/forms/UluFormFile.vue +22 -24
  23. package/lib/components/forms/UluFormMessage.vue +7 -10
  24. package/lib/components/forms/UluFormSelect.vue +16 -16
  25. package/lib/components/forms/UluFormText.vue +15 -15
  26. package/lib/components/forms/UluSearchForm.vue +8 -10
  27. package/lib/components/forms/UluSelectableMenu.vue +78 -0
  28. package/lib/components/index.js +2 -2
  29. package/lib/components/layout/UluAdaptiveLayout.vue +3 -5
  30. package/lib/components/layout/UluTitleRail.vue +9 -5
  31. package/lib/components/layout/UluWhenBreakpoint.vue +71 -77
  32. package/lib/components/navigation/UluBreadcrumb.vue +10 -4
  33. package/lib/components/navigation/UluMenu.vue +3 -3
  34. package/lib/components/navigation/UluPager.vue +102 -0
  35. package/lib/components/systems/facets/ExampleFacetsWithPagination.vue +119 -0
  36. package/lib/components/systems/facets/UluFacetsFilterLists.vue +84 -0
  37. package/lib/components/systems/facets/UluFacetsFilterPopovers.vue +112 -0
  38. package/lib/components/systems/facets/UluFacetsFilterSelects.vue +71 -0
  39. package/lib/components/systems/facets/UluFacetsHeaderLayout.vue +24 -0
  40. package/lib/components/systems/facets/UluFacetsList.vue +61 -33
  41. package/lib/components/systems/facets/UluFacetsResults.vue +63 -0
  42. package/lib/components/systems/facets/UluFacetsSearch.vue +26 -49
  43. package/lib/components/systems/facets/UluFacetsSidebarLayout.vue +31 -0
  44. package/lib/components/systems/facets/UluFacetsSort.vue +45 -0
  45. package/lib/components/systems/facets/_facets.scss +2 -3
  46. package/lib/components/systems/facets/_mock-data.js +40 -0
  47. package/lib/components/systems/facets/useFacets.js +229 -0
  48. package/lib/components/systems/index.js +13 -2
  49. package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +2 -1
  50. package/lib/components/systems/skeleton/UluShowSkeleton.vue +9 -8
  51. package/lib/components/systems/skeleton/UluSkeletonContent.vue +39 -43
  52. package/lib/components/systems/skeleton/UluSkeletonMedia.vue +4 -6
  53. package/lib/components/systems/skeleton/UluSkeletonText.vue +27 -0
  54. package/lib/components/systems/slider/UluImageSlideShow.vue +1 -1
  55. package/lib/components/systems/slider/UluSlideShow.vue +8 -3
  56. package/lib/components/systems/table-sticky/UluTableSticky.vue +8 -8
  57. package/lib/components/systems/table-sticky/UluTableStickyTable.vue +3 -3
  58. package/lib/composables/index.js +4 -1
  59. package/lib/composables/useDocumentTitle.js +61 -0
  60. package/lib/composables/usePagination.js +122 -0
  61. package/lib/composables/useRequiredInject.js +26 -0
  62. package/lib/index.js +1 -1
  63. package/lib/meta.js +14 -0
  64. package/lib/plugins/core/index.js +91 -0
  65. package/lib/plugins/index.js +1 -0
  66. package/lib/plugins/popovers/UluPopover.vue +3 -1
  67. package/lib/plugins/toast/UluToast.vue +2 -2
  68. package/lib/utils/index.js +2 -0
  69. package/lib/utils/{vue-router.js → router.js} +114 -30
  70. package/package.json +38 -13
  71. package/types/components/index.d.ts +2 -0
  72. package/types/components/index.d.ts.map +1 -0
  73. package/types/components/systems/facets/_mock-data.d.ts +18 -0
  74. package/types/components/systems/facets/_mock-data.d.ts.map +1 -0
  75. package/types/components/systems/facets/useFacets.d.ts +39 -0
  76. package/types/components/systems/facets/useFacets.d.ts.map +1 -0
  77. package/types/components/systems/index.d.ts +2 -0
  78. package/types/components/systems/index.d.ts.map +1 -0
  79. package/types/components/systems/scroll-anchors/symbols.d.ts +7 -0
  80. package/types/components/systems/scroll-anchors/symbols.d.ts.map +1 -0
  81. package/types/composables/index.d.ts +8 -0
  82. package/types/composables/index.d.ts.map +1 -0
  83. package/types/composables/useBreakpointManager.d.ts +8 -0
  84. package/types/composables/useBreakpointManager.d.ts.map +1 -0
  85. package/types/composables/useDocumentTitle.d.ts +22 -0
  86. package/types/composables/useDocumentTitle.d.ts.map +1 -0
  87. package/types/composables/useIcon.d.ts +6 -0
  88. package/types/composables/useIcon.d.ts.map +1 -0
  89. package/types/composables/useModifiers.d.ts +69 -0
  90. package/types/composables/useModifiers.d.ts.map +1 -0
  91. package/types/composables/usePageTitle.d.ts +19 -0
  92. package/types/composables/usePageTitle.d.ts.map +1 -0
  93. package/types/composables/usePagination.d.ts +25 -0
  94. package/types/composables/usePagination.d.ts.map +1 -0
  95. package/types/composables/useRequiredInject.d.ts +8 -0
  96. package/types/composables/useRequiredInject.d.ts.map +1 -0
  97. package/types/composables/useWindowResize.d.ts +6 -0
  98. package/types/composables/useWindowResize.d.ts.map +1 -0
  99. package/types/index.d.ts +5 -0
  100. package/types/index.d.ts.map +1 -0
  101. package/types/meta.d.ts +10 -0
  102. package/types/meta.d.ts.map +1 -0
  103. package/types/plugins/breakpoints/index.d.ts +2 -0
  104. package/types/plugins/breakpoints/index.d.ts.map +1 -0
  105. package/types/plugins/core/index.d.ts +3 -0
  106. package/types/plugins/core/index.d.ts.map +1 -0
  107. package/types/plugins/index.d.ts +6 -0
  108. package/types/plugins/index.d.ts.map +1 -0
  109. package/types/plugins/modals/api.d.ts +34 -0
  110. package/types/plugins/modals/api.d.ts.map +1 -0
  111. package/types/plugins/modals/index.d.ts +28 -0
  112. package/types/plugins/modals/index.d.ts.map +1 -0
  113. package/types/plugins/modals/useModals.d.ts +2 -0
  114. package/types/plugins/modals/useModals.d.ts.map +1 -0
  115. package/types/plugins/popovers/defaults.d.ts +14 -0
  116. package/types/plugins/popovers/defaults.d.ts.map +1 -0
  117. package/types/plugins/popovers/directive.d.ts +8 -0
  118. package/types/plugins/popovers/directive.d.ts.map +1 -0
  119. package/types/plugins/popovers/index.d.ts +7 -0
  120. package/types/plugins/popovers/index.d.ts.map +1 -0
  121. package/types/plugins/popovers/manager.d.ts +52 -0
  122. package/types/plugins/popovers/manager.d.ts.map +1 -0
  123. package/types/plugins/popovers/useFollow.d.ts +31 -0
  124. package/types/plugins/popovers/useFollow.d.ts.map +1 -0
  125. package/types/plugins/popovers/utils.d.ts +2 -0
  126. package/types/plugins/popovers/utils.d.ts.map +1 -0
  127. package/types/plugins/toast/defaults.d.ts +15 -0
  128. package/types/plugins/toast/defaults.d.ts.map +1 -0
  129. package/types/plugins/toast/index.d.ts +5 -0
  130. package/types/plugins/toast/index.d.ts.map +1 -0
  131. package/types/plugins/toast/store.d.ts +22 -0
  132. package/types/plugins/toast/store.d.ts.map +1 -0
  133. package/types/plugins/toast/useToast.d.ts +2 -0
  134. package/types/plugins/toast/useToast.d.ts.map +1 -0
  135. package/types/utils/dom.d.ts +8 -0
  136. package/types/utils/dom.d.ts.map +1 -0
  137. package/types/utils/index.d.ts +3 -0
  138. package/types/utils/index.d.ts.map +1 -0
  139. package/types/utils/placeholder.d.ts +8 -0
  140. package/types/utils/placeholder.d.ts.map +1 -0
  141. package/types/utils/router.d.ts +144 -0
  142. package/types/utils/router.d.ts.map +1 -0
  143. package/types/utils/vue-router.d.ts +122 -0
  144. package/types/utils/vue-router.d.ts.map +1 -0
  145. package/dist/index-CMGxe_M1.js +0 -6466
  146. package/lib/components/forms/UluCheckboxMenu.vue +0 -36
  147. package/lib/components/forms/UluFormDropzone.vue +0 -62
  148. package/lib/components/systems/facets/UluFacets.vue +0 -380
  149. package/lib/components/systems/skeleton/UluSkeletonTextInline.vue +0 -9
  150. package/lib/settings.js +0 -119
  151. package/lib/utils/placeholder.js +0 -6
@@ -0,0 +1,63 @@
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
+ :class="classes.list"
9
+ >
10
+ <li
11
+ class="UluFacetsResults__item"
12
+ :class="classes.item"
13
+ v-for="(item, index) in items"
14
+ :key="item.id || index"
15
+ >
16
+ <slot name="item" :item="item" :index="index"></slot>
17
+ </li>
18
+ </transition-group>
19
+ <div v-else class="UluFacetsResults__empty">
20
+ <slot name="empty">
21
+ <p>No matching items found.</p>
22
+ </slot>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ defineProps({
29
+ items: {
30
+ type: Array,
31
+ required: true
32
+ },
33
+ tag: {
34
+ type: String,
35
+ default: 'ul'
36
+ },
37
+ transitionName: {
38
+ type: String,
39
+ default: 'UluFacetsFade'
40
+ },
41
+ classes: {
42
+ type: Object,
43
+ default: () => ({})
44
+ }
45
+ });
46
+ </script>
47
+
48
+ <style lang="scss">
49
+ .UluFacetsResults__list {
50
+ list-style: none;
51
+ padding: 0;
52
+ }
53
+
54
+ .UluFacetsFade-enter-active,
55
+ .UluFacetsFade-leave-active {
56
+ transition: opacity 0.25s ease;
57
+ }
58
+
59
+ .UluFacetsFade-enter-from,
60
+ .UluFacetsFade-leave-to {
61
+ opacity: 0;
62
+ }
63
+ </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>
@@ -54,9 +54,8 @@ $config: (
54
54
  $prefix: selector.class("facets");
55
55
 
56
56
  #{ $prefix }__body {
57
- .FacetView__body {
58
- display: flex
59
- ;
57
+ .FacetView__body {
58
+ display: flex;
60
59
  }
61
60
  }
62
61
  }
@@ -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,229 @@
1
+ import { ref, computed, watch } from 'vue';
2
+ import Fuse from 'fuse.js';
3
+
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
+ });
36
+ }
37
+
38
+
39
+ /**
40
+ * A composable for handling client-side faceted search, filtering, and sorting.
41
+ * @param {import('vue').Ref<Array<Object>>} allItems - A Vue ref containing the full list of items to be processed.
42
+ * @param {Object} options - Configuration options for the composable.
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`.
45
+ * @param {String} [options.initialSearchValue=''] - The initial value for the search input.
46
+ * @param {String} [options.initialSortType='az'] - The initial sort type.
47
+ * @param {Boolean} [options.noDefaultSorts=false] - If true, the default 'A-Z' and 'Z-A' sorts will not be included.
48
+ * @param {Object} [options.extraSortTypes={}] - Additional sort types to be merged with the default ones.
49
+ * @param {Object} [options.searchOptions={}] - Configuration options for Fuse.js.
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.
52
+ */
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
+
60
+ const {
61
+ initialFacets,
62
+ facetFields,
63
+ initialSearchValue = '',
64
+ initialSortType = 'az',
65
+ noDefaultSorts = false,
66
+ extraSortTypes = {},
67
+ searchOptions: initialSearchOptions = {},
68
+ getItemFacet = defaultGetItemFacet,
69
+ getSortValue = item => (item.title || item.label || "")
70
+ } = options;
71
+
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(va).localeCompare(String(vb));
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
+ };
87
+
88
+ // --- Helpers ---
89
+ function createFacets(initial) {
90
+ return (initial || []).map(group => ({
91
+ ...group,
92
+ open: group.open || false,
93
+ children: group.children.map(facet => ({
94
+ ...facet,
95
+ selected: facet.selected || false
96
+ })),
97
+ selectedCount: 0
98
+ }));
99
+ }
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
+
118
+ // --- Computed ---
119
+ const sortTypes = computed(() => ({
120
+ ...(noDefaultSorts ? {} : defaultSorts),
121
+ ...extraSortTypes
122
+ }));
123
+
124
+ const searchOptions = computed(() => ({
125
+ shouldSort: true,
126
+ keys: ["title", "label", "description", "author"],
127
+ ...initialSearchOptions
128
+ }));
129
+
130
+ const selectedFacets = computed(() => {
131
+ const selected = [];
132
+ facets.value.forEach((group) => {
133
+ const { name, uid, children } = group;
134
+ let count = 0;
135
+ let added = false;
136
+ if (children) {
137
+ children.forEach(child => {
138
+ if (child.selected) {
139
+ ++count;
140
+ if (!added) {
141
+ selected.push({ uid, name, children: [] });
142
+ added = true;
143
+ }
144
+ selected[selected.length - 1].children.push(child);
145
+ }
146
+ });
147
+ }
148
+ group.selectedCount = count;
149
+ });
150
+ return selected;
151
+ });
152
+
153
+ const filteredItems = computed(() => {
154
+ if (!selectedFacets.value.length) {
155
+ return allItems.value;
156
+ }
157
+ return allItems.value.filter(item => {
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));
164
+ }
165
+ return false;
166
+ });
167
+ });
168
+ });
169
+
170
+ const searchedItems = computed(() => {
171
+ if (!searchValue.value?.length) {
172
+ return filteredItems.value;
173
+ }
174
+ const fuse = new Fuse(filteredItems.value, searchOptions.value);
175
+ return fuse.search(searchValue.value).map(result => result.item);
176
+ });
177
+
178
+ const displayItems = computed(() => {
179
+ const sortFn = sortTypes.value[selectedSort.value]?.sort;
180
+ if (typeof sortFn !== 'function') {
181
+ return searchedItems.value;
182
+ }
183
+ // The sort function should not mutate the original array
184
+ return sortFn([...searchedItems.value]);
185
+ });
186
+
187
+ // --- Methods ---
188
+ function clearFilters() {
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
+ // For single-select groups, deselect other options when a new one is selected.
200
+ if (!group.multiple && selected) {
201
+ group.children.forEach(f => {
202
+ if (f.uid !== facetUid) {
203
+ f.selected = false;
204
+ }
205
+ });
206
+ }
207
+ const facet = group.children.find(f => f.uid === facetUid);
208
+ if (facet) {
209
+ facet.selected = selected;
210
+ }
211
+ }
212
+ }
213
+
214
+ return {
215
+ // State
216
+ facets,
217
+ searchValue,
218
+ selectedSort,
219
+ sortTypes,
220
+
221
+ // Computed
222
+ displayItems,
223
+ selectedFacets,
224
+
225
+ // Methods
226
+ clearFilters,
227
+ handleFacetChange
228
+ };
229
+ }
@@ -1,17 +1,28 @@
1
- export { default as UluFacets } from './facets/UluFacets.vue';
1
+ export { useFacets } from './facets/useFacets.js';
2
+ export { default as UluFacetsFilterLists } from './facets/UluFacetsFilterLists.vue';
3
+ export { default as UluFacetsFilterPopovers } from './facets/UluFacetsFilterPopovers.vue';
4
+ export { default as UluFacetsFilterSelects } from './facets/UluFacetsFilterSelects.vue';
5
+ export { default as UluFacetsHeaderLayout } from './facets/UluFacetsHeaderLayout.vue';
6
+ export { default as UluFacetsResults } from './facets/UluFacetsResults.vue';
2
7
  export { default as UluFacetsSearch } from './facets/UluFacetsSearch.vue';
8
+ export { default as UluFacetsSidebarLayout } from './facets/UluFacetsSidebarLayout.vue';
9
+ export { default as UluFacetsSort } from './facets/UluFacetsSort.vue';
3
10
  export { default as UluFacetsList } from './facets/UluFacetsList.vue';
11
+
4
12
  export { default as UluScrollAnchors } from './scroll-anchors/UluScrollAnchors.vue';
5
13
  export { default as UluScrollAnchorsNav } from './scroll-anchors/UluScrollAnchorsNav.vue';
6
14
  export { default as UluScrollAnchorsNavAnimated } from './scroll-anchors/UluScrollAnchorsNavAnimated.vue';
7
15
  export { default as UluScrollAnchorsSection } from './scroll-anchors/UluScrollAnchorsSection.vue';
16
+
8
17
  export { default as UluShowSkeleton } from './skeleton/UluShowSkeleton.vue';
9
18
  export { default as UluSkeletonContent } from './skeleton/UluSkeletonContent.vue';
10
19
  export { default as UluSkeletonMedia } from './skeleton/UluSkeletonMedia.vue';
11
- export { default as UluSkeletonTextInline } from './skeleton/UluSkeletonTextInline.vue';
20
+ export { default as UluSkeletonText } from './skeleton/UluSkeletonText.vue';
21
+
12
22
  export { default as UluImageSlideShow } from './slider/UluImageSlideShow.vue';
13
23
  export { default as UluSlideShow } from './slider/UluSlideShow.vue';
14
24
  export { default as UluSlideShowSlide } from './slider/UluSlideShowSlide.vue';
25
+
15
26
  export { default as UluTableSticky } from './table-sticky/UluTableSticky.vue';
16
27
  export { default as UluTableStickyRows } from './table-sticky/UluTableStickyRows.vue';
17
28
  export { default as UluTableStickyTable } from './table-sticky/UluTableStickyTable.vue';
@@ -10,6 +10,7 @@
10
10
  import { SECTIONS, REGISTER, UNREGISTER } from "./symbols.js";
11
11
  export default {
12
12
  name: "ScrollAnchors",
13
+ emits: ["section-change"],
13
14
  props: {
14
15
  firstItemActive: Boolean,
15
16
  /**
@@ -97,7 +98,7 @@
97
98
  } else if (lastExiting && section.active) {
98
99
  removeActive();
99
100
  }
100
- this.$emit("sectionChange", {
101
+ this.$emit("section-change", {
101
102
  section,
102
103
  sections,
103
104
  active: isIntersecting
@@ -1,13 +1,14 @@
1
1
  <template>
2
2
  <slot v-if="!when"/>
3
- <SkeletonTextInline class="skeleton" v-else/>
3
+ <UluSkeletonText v-else inline/>
4
4
  </template>
5
5
 
6
- <script>
7
- export default {
8
- name: "ShowSkeleton",
9
- props: {
10
- when: Boolean,
11
- },
12
- };
6
+ <script setup>
7
+ import UluSkeletonText from "./UluSkeletonText.vue";
8
+ defineProps({
9
+ /**
10
+ * If true will show whatever is passed to slot, else skeleton text
11
+ */
12
+ when: Boolean,
13
+ });
13
14
  </script>
@@ -1,11 +1,11 @@
1
1
  <template>
2
- <div class="skeleton">
2
+ <div>
3
3
  <div v-for="(line, index) in linesWithSegments" :key="index">
4
4
  <span
5
5
  v-for="segment in line"
6
6
  :key="segment"
7
- class="skeleton__text skeleton__text--inline"
8
- :class="{ 'skeleton__alt' : segment.alt }"
7
+ class="skeleton skeleton--text skeleton--inline"
8
+ :class="{ 'skeleton--background-alt' : segment.alt }"
9
9
  :style="{ width: `${ segment.width }%` }"
10
10
  >
11
11
  </span>
@@ -13,48 +13,44 @@
13
13
  </div>
14
14
  </template>
15
15
 
16
- <script>
16
+ <script setup>
17
+ import { computed } from "vue";
17
18
  import { randomInt } from "@ulu/utils/random.js";
18
19
  import { arrayCreate } from "@ulu/utils/array.js";
19
- export default {
20
- name: "SkeletonContent",
21
- props: {
22
- lines: {
23
- type: Number,
24
- default: 6
25
- },
26
- },
27
- methods: {
28
- randomInt
20
+
21
+ const props = defineProps({
22
+ /**
23
+ * Amount of lines to generate
24
+ */
25
+ lines: {
26
+ type: Number,
27
+ default: 6
29
28
  },
30
- computed: {
31
- /**
32
- * Creates the segments (like words) for the given line count
33
- * - Uses random number of segments and makes sure to fill the line between 70% - 100%
34
- */
35
- linesWithSegments() {
36
- return arrayCreate(this.lines, () => {
37
- const minWidth = 15;
38
- const total = randomInt(70, 100);
39
- let widthCurrent = 0;
40
- const newWidth = () => {
41
- const remaining = total - widthCurrent;
42
- const width = randomInt(minWidth, remaining);
43
- widthCurrent += width;
44
- return width;
45
- };
46
- const segments = [];
47
- while (widthCurrent < total - minWidth) {
48
- segments.push(newWidth());
49
- }
50
- const getActualTotal = () => segments.reduce((acc, a) => acc + a, 0);
51
- while (getActualTotal() >= total) {
52
- let removed = segments.pop();
53
- if (!removed) break;
54
- }
55
- return segments.map(width => ({ width, alt: Math.random() < 0.5 }));
56
- });
57
- }
29
+ });
30
+
31
+ /**
32
+ * Creates the segments (like words) for the given line count
33
+ * - Uses random number of segments and makes sure to fill the line between 70% - 100%
34
+ */
35
+ const linesWithSegments = computed(() => arrayCreate(props.lines, () => {
36
+ const minWidth = 15;
37
+ const total = randomInt(70, 100);
38
+ let widthCurrent = 0;
39
+ const newWidth = () => {
40
+ const remaining = total - widthCurrent;
41
+ const width = randomInt(minWidth, remaining);
42
+ widthCurrent += width;
43
+ return width;
44
+ };
45
+ const segments = [];
46
+ while (widthCurrent < total - minWidth) {
47
+ segments.push(newWidth());
48
+ }
49
+ const getActualTotal = () => segments.reduce((acc, a) => acc + a, 0);
50
+ while (getActualTotal() >= total) {
51
+ let removed = segments.pop();
52
+ if (!removed) break;
58
53
  }
59
- };
54
+ return segments.map(width => ({ width, alt: Math.random() < 0.5 }));
55
+ }));
60
56
  </script>