@ulu/frontend-vue 0.1.0-beta.2 → 0.1.0-beta.21
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.
- package/README.md +113 -2
- package/dist/{breakpoints-ClT9bfZm.js → breakpoints-DOXmgVoG.js} +1 -1
- package/dist/frontend-vue.css +1 -1
- package/dist/frontend-vue.js +75 -73
- package/dist/index-BpmkfeZb.js +6671 -0
- package/lib/components/collapsible/UluAccordion.vue +1 -1
- package/lib/components/collapsible/UluModal.vue +4 -5
- package/lib/components/collapsible/UluOverflowPopover.vue +1 -1
- package/lib/components/elements/UluAlert.vue +1 -2
- package/lib/components/elements/UluBadge.vue +27 -28
- package/lib/components/elements/UluBadgeStack.vue +8 -13
- package/lib/components/elements/UluButton.vue +2 -2
- package/lib/components/elements/UluButtonVerbose.vue +119 -0
- package/lib/components/elements/UluCard.vue +1 -1
- package/lib/components/elements/UluDefinitionList.vue +14 -17
- package/lib/components/elements/UluExternalLink.vue +22 -29
- package/lib/components/elements/UluIcon.vue +22 -17
- package/lib/components/elements/UluList.vue +53 -55
- package/lib/components/elements/UluSpokeSpinner.vue +12 -18
- package/lib/components/elements/UluTag.vue +35 -35
- package/lib/components/forms/UluCheckboxMenu.vue +32 -31
- package/lib/components/forms/UluFileDisplay.vue +40 -31
- package/lib/components/forms/UluFormFile.vue +22 -24
- package/lib/components/forms/UluFormMessage.vue +7 -10
- package/lib/components/forms/UluFormSelect.vue +16 -16
- package/lib/components/forms/UluFormText.vue +15 -15
- package/lib/components/forms/UluSearchForm.vue +8 -10
- package/lib/components/index.js +1 -1
- package/lib/components/layout/UluAdaptiveLayout.vue +3 -5
- package/lib/components/layout/UluTitleRail.vue +9 -5
- package/lib/components/layout/UluWhenBreakpoint.vue +71 -77
- package/lib/components/navigation/UluBreadcrumb.vue +1 -2
- package/lib/components/navigation/UluMenu.vue +3 -3
- package/lib/components/navigation/UluPager.vue +102 -0
- package/lib/components/systems/facets/ExampleFacetsWithPagination.vue +119 -0
- package/lib/components/systems/facets/UluFacetsFilters.vue +73 -0
- package/lib/components/systems/facets/UluFacetsList.vue +13 -14
- package/lib/components/systems/facets/UluFacetsResults.vue +57 -0
- package/lib/components/systems/facets/UluFacetsSearch.vue +26 -49
- package/lib/components/systems/facets/UluFacetsSidebarLayout.vue +31 -0
- package/lib/components/systems/facets/UluFacetsSort.vue +45 -0
- package/lib/components/systems/facets/_facets.scss +2 -3
- package/lib/components/systems/facets/_mock-data.js +40 -0
- package/lib/components/systems/facets/useFacets.js +221 -0
- package/lib/components/systems/index.js +10 -2
- package/lib/components/systems/scroll-anchors/UluScrollAnchors.vue +2 -1
- package/lib/components/systems/skeleton/UluShowSkeleton.vue +9 -8
- package/lib/components/systems/skeleton/UluSkeletonContent.vue +39 -43
- package/lib/components/systems/skeleton/UluSkeletonMedia.vue +4 -6
- package/lib/components/systems/skeleton/UluSkeletonText.vue +27 -0
- package/lib/components/systems/slider/UluImageSlideShow.vue +1 -1
- package/lib/components/systems/slider/UluSlideShow.vue +8 -3
- package/lib/components/systems/table-sticky/UluTableSticky.vue +8 -8
- package/lib/components/systems/table-sticky/UluTableStickyTable.vue +3 -3
- package/lib/composables/index.js +3 -1
- package/lib/composables/useDocumentTitle.js +47 -0
- package/lib/composables/usePageTitle.js +37 -0
- package/lib/composables/usePagination.js +122 -0
- package/lib/composables/useRequiredInject.js +26 -0
- package/lib/index.js +1 -1
- package/lib/meta.js +14 -0
- package/lib/plugins/core/index.js +91 -0
- package/lib/plugins/index.js +1 -0
- package/lib/plugins/toast/UluToast.vue +2 -2
- package/lib/utils/index.js +2 -0
- package/lib/utils/{vue-router.js → router.js} +106 -11
- package/package.json +37 -14
- package/types/components/index.d.ts +2 -0
- package/types/components/index.d.ts.map +1 -0
- package/types/components/systems/facets/_mock-data.d.ts +18 -0
- package/types/components/systems/facets/_mock-data.d.ts.map +1 -0
- package/types/components/systems/facets/useFacets.d.ts +39 -0
- package/types/components/systems/facets/useFacets.d.ts.map +1 -0
- package/types/components/systems/index.d.ts +2 -0
- package/types/components/systems/index.d.ts.map +1 -0
- package/types/components/systems/scroll-anchors/symbols.d.ts +7 -0
- package/types/components/systems/scroll-anchors/symbols.d.ts.map +1 -0
- package/types/composables/index.d.ts +7 -0
- package/types/composables/index.d.ts.map +1 -0
- package/types/composables/useBreakpointManager.d.ts +8 -0
- package/types/composables/useBreakpointManager.d.ts.map +1 -0
- package/types/composables/useDocumentTitle.d.ts +18 -0
- package/types/composables/useDocumentTitle.d.ts.map +1 -0
- package/types/composables/useIcon.d.ts +6 -0
- package/types/composables/useIcon.d.ts.map +1 -0
- package/types/composables/useModifiers.d.ts +69 -0
- package/types/composables/useModifiers.d.ts.map +1 -0
- package/types/composables/usePageTitle.d.ts +19 -0
- package/types/composables/usePageTitle.d.ts.map +1 -0
- package/types/composables/usePagination.d.ts +25 -0
- package/types/composables/usePagination.d.ts.map +1 -0
- package/types/composables/useRequiredInject.d.ts +8 -0
- package/types/composables/useRequiredInject.d.ts.map +1 -0
- package/types/composables/useWindowResize.d.ts +6 -0
- package/types/composables/useWindowResize.d.ts.map +1 -0
- package/types/index.d.ts +5 -0
- package/types/index.d.ts.map +1 -0
- package/types/meta.d.ts +10 -0
- package/types/meta.d.ts.map +1 -0
- package/types/plugins/breakpoints/index.d.ts +2 -0
- package/types/plugins/breakpoints/index.d.ts.map +1 -0
- package/types/plugins/core/index.d.ts +3 -0
- package/types/plugins/core/index.d.ts.map +1 -0
- package/types/plugins/index.d.ts +6 -0
- package/types/plugins/index.d.ts.map +1 -0
- package/types/plugins/modals/api.d.ts +34 -0
- package/types/plugins/modals/api.d.ts.map +1 -0
- package/types/plugins/modals/index.d.ts +28 -0
- package/types/plugins/modals/index.d.ts.map +1 -0
- package/types/plugins/modals/useModals.d.ts +2 -0
- package/types/plugins/modals/useModals.d.ts.map +1 -0
- package/types/plugins/popovers/defaults.d.ts +14 -0
- package/types/plugins/popovers/defaults.d.ts.map +1 -0
- package/types/plugins/popovers/directive.d.ts +8 -0
- package/types/plugins/popovers/directive.d.ts.map +1 -0
- package/types/plugins/popovers/index.d.ts +7 -0
- package/types/plugins/popovers/index.d.ts.map +1 -0
- package/types/plugins/popovers/manager.d.ts +52 -0
- package/types/plugins/popovers/manager.d.ts.map +1 -0
- package/types/plugins/popovers/useFollow.d.ts +31 -0
- package/types/plugins/popovers/useFollow.d.ts.map +1 -0
- package/types/plugins/popovers/utils.d.ts +2 -0
- package/types/plugins/popovers/utils.d.ts.map +1 -0
- package/types/plugins/toast/defaults.d.ts +15 -0
- package/types/plugins/toast/defaults.d.ts.map +1 -0
- package/types/plugins/toast/index.d.ts +5 -0
- package/types/plugins/toast/index.d.ts.map +1 -0
- package/types/plugins/toast/store.d.ts +22 -0
- package/types/plugins/toast/store.d.ts.map +1 -0
- package/types/plugins/toast/useToast.d.ts +2 -0
- package/types/plugins/toast/useToast.d.ts.map +1 -0
- package/types/utils/dom.d.ts +8 -0
- package/types/utils/dom.d.ts.map +1 -0
- package/types/utils/index.d.ts +3 -0
- package/types/utils/index.d.ts.map +1 -0
- package/types/utils/placeholder.d.ts +8 -0
- package/types/utils/placeholder.d.ts.map +1 -0
- package/types/utils/router.d.ts +141 -0
- package/types/utils/router.d.ts.map +1 -0
- package/types/utils/vue-router.d.ts +122 -0
- package/types/utils/vue-router.d.ts.map +1 -0
- package/dist/frontend-vue.umd.cjs +0 -561
- package/dist/index-P5Rwl_Dl.js +0 -7263
- package/lib/components/forms/UluFormDropzone.vue +0 -62
- package/lib/components/systems/facets/UluFacets.vue +0 -380
- package/lib/components/systems/skeleton/UluSkeletonTextInline.vue +0 -9
- package/lib/settings.js +0 -119
- package/lib/utils/placeholder.js +0 -6
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
const facet = group.children.find(f => f.uid === facetUid);
|
|
200
|
+
if (facet) {
|
|
201
|
+
facet.selected = selected;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
// State
|
|
208
|
+
facets,
|
|
209
|
+
searchValue,
|
|
210
|
+
selectedSort,
|
|
211
|
+
sortTypes,
|
|
212
|
+
|
|
213
|
+
// Computed
|
|
214
|
+
displayItems,
|
|
215
|
+
selectedFacets,
|
|
216
|
+
|
|
217
|
+
// Methods
|
|
218
|
+
clearFilters,
|
|
219
|
+
handleFacetChange
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useFacets } from './facets/useFacets.js';
|
|
2
|
+
export { default as UluFacetsFilters } from './facets/UluFacetsFilters.vue';
|
|
3
|
+
export { default as UluFacetsResults } from './facets/UluFacetsResults.vue';
|
|
2
4
|
export { default as UluFacetsSearch } from './facets/UluFacetsSearch.vue';
|
|
5
|
+
export { default as UluFacetsSidebarLayout } from './facets/UluFacetsSidebarLayout.vue';
|
|
6
|
+
export { default as UluFacetsSort } from './facets/UluFacetsSort.vue';
|
|
3
7
|
export { default as UluFacetsList } from './facets/UluFacetsList.vue';
|
|
8
|
+
|
|
4
9
|
export { default as UluScrollAnchors } from './scroll-anchors/UluScrollAnchors.vue';
|
|
5
10
|
export { default as UluScrollAnchorsNav } from './scroll-anchors/UluScrollAnchorsNav.vue';
|
|
6
11
|
export { default as UluScrollAnchorsNavAnimated } from './scroll-anchors/UluScrollAnchorsNavAnimated.vue';
|
|
7
12
|
export { default as UluScrollAnchorsSection } from './scroll-anchors/UluScrollAnchorsSection.vue';
|
|
13
|
+
|
|
8
14
|
export { default as UluShowSkeleton } from './skeleton/UluShowSkeleton.vue';
|
|
9
15
|
export { default as UluSkeletonContent } from './skeleton/UluSkeletonContent.vue';
|
|
10
16
|
export { default as UluSkeletonMedia } from './skeleton/UluSkeletonMedia.vue';
|
|
11
|
-
export { default as
|
|
17
|
+
export { default as UluSkeletonText } from './skeleton/UluSkeletonText.vue';
|
|
18
|
+
|
|
12
19
|
export { default as UluImageSlideShow } from './slider/UluImageSlideShow.vue';
|
|
13
20
|
export { default as UluSlideShow } from './slider/UluSlideShow.vue';
|
|
14
21
|
export { default as UluSlideShowSlide } from './slider/UluSlideShowSlide.vue';
|
|
22
|
+
|
|
15
23
|
export { default as UluTableSticky } from './table-sticky/UluTableSticky.vue';
|
|
16
24
|
export { default as UluTableStickyRows } from './table-sticky/UluTableStickyRows.vue';
|
|
17
25
|
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("
|
|
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
|
-
<
|
|
3
|
+
<UluSkeletonText v-else inline/>
|
|
4
4
|
</template>
|
|
5
5
|
|
|
6
|
-
<script>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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="
|
|
8
|
-
:class="{ '
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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>
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
<
|
|
2
|
+
<div class="skeleton skeleton-block--media">
|
|
3
|
+
<UluIcon icon="type:image"/>
|
|
4
4
|
</div>
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
|
-
<script>
|
|
8
|
-
|
|
9
|
-
name: "SkeletonMedia",
|
|
10
|
-
};
|
|
7
|
+
<script setup>
|
|
8
|
+
import UluIcon from "../../elements/UluIcon.vue";
|
|
11
9
|
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class="skeleton skeleton--text"
|
|
4
|
+
:class="{
|
|
5
|
+
'skeleton--inline' : inline,
|
|
6
|
+
'skeleton--background-alt' : alt,
|
|
7
|
+
[`skeleton--width-${ width }`] : width
|
|
8
|
+
}"
|
|
9
|
+
></span>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
defineProps({
|
|
14
|
+
/**
|
|
15
|
+
* Inline modifier
|
|
16
|
+
*/
|
|
17
|
+
inline: Boolean,
|
|
18
|
+
/**
|
|
19
|
+
* Use alternate background color
|
|
20
|
+
*/
|
|
21
|
+
alt: Boolean,
|
|
22
|
+
/**
|
|
23
|
+
* Optional size (width) - should correspond with width setup in scss component
|
|
24
|
+
*/
|
|
25
|
+
width: String
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
@click="previous"
|
|
51
51
|
:disabled="!canScrollLeft"
|
|
52
52
|
>
|
|
53
|
-
<
|
|
53
|
+
<UluIcon class="slideshow__control-icon" icon="type:next"/>
|
|
54
54
|
</button>
|
|
55
55
|
</li>
|
|
56
56
|
<li class="slideshow__controls-item slideshow__controls-item--next">
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
@click="next"
|
|
61
61
|
:disabled="!canScrollRight"
|
|
62
62
|
>
|
|
63
|
-
<
|
|
63
|
+
<UluIcon class="slideshow__control-icon" icon="type:previous" />
|
|
64
64
|
</button>
|
|
65
65
|
</li>
|
|
66
66
|
</ul>
|
|
@@ -97,8 +97,13 @@
|
|
|
97
97
|
</template>
|
|
98
98
|
|
|
99
99
|
<script>
|
|
100
|
+
import UluIcon from "../../elements/UluIcon.vue";
|
|
100
101
|
export default {
|
|
101
102
|
name: 'SlideShow',
|
|
103
|
+
emits: ['slide-change'],
|
|
104
|
+
components: {
|
|
105
|
+
UluIcon
|
|
106
|
+
},
|
|
102
107
|
props: {
|
|
103
108
|
/**
|
|
104
109
|
* Should slides be focusable by tab key
|
|
@@ -278,7 +283,7 @@
|
|
|
278
283
|
this.$nextTick(() => {
|
|
279
284
|
const slide = this.getSlideByElement(entry.target);
|
|
280
285
|
slide.active = entry.isIntersecting;
|
|
281
|
-
this.$emit('
|
|
286
|
+
this.$emit('slide-change', { slide, track, nav });
|
|
282
287
|
});
|
|
283
288
|
});
|
|
284
289
|
};
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
pointerEvents: sizesCalculated ? 'auto' : 'none',
|
|
44
44
|
width: tableWidth
|
|
45
45
|
}"
|
|
46
|
-
@
|
|
46
|
+
@column-sorted="applySort"
|
|
47
47
|
>
|
|
48
48
|
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
|
|
49
49
|
<slot :name="name" v-bind="slotData" />
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
opacity: headerOpacityX,
|
|
67
67
|
pointerEvents: headerVisibleX ? 'auto' : 'none'
|
|
68
68
|
}"
|
|
69
|
-
@
|
|
69
|
+
@column-sorted="applySort"
|
|
70
70
|
>
|
|
71
71
|
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
|
|
72
72
|
<slot :name="name" v-bind="slotData" />
|
|
@@ -138,9 +138,9 @@
|
|
|
138
138
|
:getRowValue="getRowValue"
|
|
139
139
|
:getColumnTitle="getColumnTitle"
|
|
140
140
|
@vue:mounted="tableReady"
|
|
141
|
-
@
|
|
142
|
-
@
|
|
143
|
-
@
|
|
141
|
+
@actual-header-removed="headerRemoved"
|
|
142
|
+
@actual-header-added="headerAdded"
|
|
143
|
+
@column-sorted="applySort"
|
|
144
144
|
>
|
|
145
145
|
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
|
|
146
146
|
<slot :name="name" v-bind="slotData" />
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
opacity: headerOpacityX,
|
|
168
168
|
pointerEvents: headerVisibleX ? 'auto' : 'none'
|
|
169
169
|
}"
|
|
170
|
-
@
|
|
170
|
+
@column-sorted="applySort"
|
|
171
171
|
>
|
|
172
172
|
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
|
|
173
173
|
<slot :name="name" v-bind="slotData" />
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
import UluTableStickyTable from "./UluTableStickyTable.vue";
|
|
181
181
|
import { debounce } from "@ulu/utils/performance.js";
|
|
182
182
|
import { runAfterFramePaint } from "@ulu/utils/browser/performance.js";
|
|
183
|
-
import cloneDeep from "lodash/cloneDeep";
|
|
183
|
+
import cloneDeep from "lodash-es/cloneDeep.js";
|
|
184
184
|
|
|
185
185
|
const arrayOfObjects = a => a.every(o => typeof o === "object");
|
|
186
186
|
const required = true;
|
|
@@ -443,7 +443,7 @@
|
|
|
443
443
|
} else {
|
|
444
444
|
column.sortApplied = true;
|
|
445
445
|
}
|
|
446
|
-
this.$emit("
|
|
446
|
+
this.$emit("column-sort", column);
|
|
447
447
|
},
|
|
448
448
|
onColumnResize() {
|
|
449
449
|
if (this.sizesPainted) {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
:class="{
|
|
51
51
|
'table-sticky__sort-button--focused' : column.sortFocused,
|
|
52
52
|
}"
|
|
53
|
-
@click="$emit('
|
|
53
|
+
@click="$emit('column-sorted', column)"
|
|
54
54
|
@focus="handleSortFocus(column, true)"
|
|
55
55
|
@blur="handleSortFocus(column, false)"
|
|
56
56
|
:aria-pressed="column.sortApplied ? 'true' : 'false'"
|
|
@@ -199,9 +199,9 @@
|
|
|
199
199
|
const { id } = column;
|
|
200
200
|
const old = headerRefs[id];
|
|
201
201
|
if (old) {
|
|
202
|
-
this.$emit("
|
|
202
|
+
this.$emit("actual-header-removed", old);
|
|
203
203
|
}
|
|
204
|
-
this.$emit("
|
|
204
|
+
this.$emit("actual-header-added", el);
|
|
205
205
|
headerRefs[id] = el;
|
|
206
206
|
},
|
|
207
207
|
/**
|
package/lib/composables/index.js
CHANGED
|
@@ -7,4 +7,6 @@
|
|
|
7
7
|
export { useIcon } from './useIcon.js';
|
|
8
8
|
export { useModifiers } from './useModifiers.js';
|
|
9
9
|
export { useWindowResize } from './useWindowResize.js';
|
|
10
|
-
export {
|
|
10
|
+
export { useRequiredInject } from './useRequiredInject.js';
|
|
11
|
+
export { useBreakpointManager } from './useBreakpointManager.js';
|
|
12
|
+
export { usePagination } from './usePagination.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
import { useHead as defaultUseHead } from '@unhead/vue';
|
|
3
|
+
import { useRoute as defaultUseRoute } from 'vue-router';
|
|
4
|
+
import { pageTitles } from './usePageTitle.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages the document's <title> tag based on the current route's title.
|
|
8
|
+
* It pulls titles from the `usePageTitle` system, falling back to `meta.title`,
|
|
9
|
+
* and formats it with a template.
|
|
10
|
+
*
|
|
11
|
+
* This should be called once in the root App.vue component.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {string} [options.titleTemplate='%s | My Awesome Site'] - The template for the title.
|
|
15
|
+
* @param {Function} [options.useRoute=defaultUseRoute] - The `useRoute` function, injectable for testing.
|
|
16
|
+
* @param {Function} [options.useHead=defaultUseHead] - The `useHead` function, injectable for testing.
|
|
17
|
+
*/
|
|
18
|
+
export function useDocumentTitle(options = {}) {
|
|
19
|
+
const {
|
|
20
|
+
titleTemplate = '%s',
|
|
21
|
+
useRoute = defaultUseRoute,
|
|
22
|
+
useHead = defaultUseHead
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
const route = useRoute();
|
|
26
|
+
|
|
27
|
+
const documentTitle = computed(() => {
|
|
28
|
+
// Get the title from our reactive state, or fall back to the route's meta.
|
|
29
|
+
const titleFromState = pageTitles[route.path];
|
|
30
|
+
const titleFromMeta = route.meta.title;
|
|
31
|
+
|
|
32
|
+
let title = titleFromState || titleFromMeta;
|
|
33
|
+
|
|
34
|
+
// If the title from meta is a function, resolve it.
|
|
35
|
+
if (typeof title === 'function') {
|
|
36
|
+
title = title(route);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Format the title with the template, or provide a default.
|
|
40
|
+
return title ? titleTemplate.replace('%s', title) : 'App';
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// useHead is reactive, so it will automatically update when documentTitle changes.
|
|
44
|
+
useHead({
|
|
45
|
+
title: documentTitle,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { reactive, watchEffect, onUnmounted, unref } from "vue";
|
|
2
|
+
import { useRoute as defaultUseRoute } from "vue-router";
|
|
3
|
+
|
|
4
|
+
// A reactive map to store component-defined titles for the current route.
|
|
5
|
+
// Key: route.path, Value: title string
|
|
6
|
+
export const pageTitles = reactive({});
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A composable to set the title for the current page/route from within its component.
|
|
10
|
+
* This provides a single source of truth for a page's title, which can be
|
|
11
|
+
* consumed by various parts of the application (e.g., breadcrumbs, document title).
|
|
12
|
+
* @param {import('vue').Ref<string> | string} title The title to set for the current page. Can be a ref, computed, or a plain string.
|
|
13
|
+
* @param {{ useRoute: Function }} options For dependency injection in tests/stories.
|
|
14
|
+
*/
|
|
15
|
+
export function usePageTitle(title, { useRoute = defaultUseRoute } = {}) {
|
|
16
|
+
const route = useRoute();
|
|
17
|
+
const path = route.path;
|
|
18
|
+
|
|
19
|
+
watchEffect(() => {
|
|
20
|
+
pageTitles[path] = unref(title);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Clean up when the component is unmounted to prevent memory leaks
|
|
24
|
+
onUnmounted(() => {
|
|
25
|
+
delete pageTitles[path];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Gets the dynamically set page title for a given path.
|
|
31
|
+
* For internal use by consumers like breadcrumb or document title utilities.
|
|
32
|
+
* @param {string} path The route path to look up.
|
|
33
|
+
* @returns {string | undefined}
|
|
34
|
+
*/
|
|
35
|
+
export function getPageTitle(path) {
|
|
36
|
+
return pageTitles[path];
|
|
37
|
+
}
|