@asd20/ui-next 2.0.23 → 2.0.25
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/components/atoms/Asd20MultiselectInput/index.vue +32 -6
- package/src/components/templates/Asd20ArticleDigestTemplate/index.vue +3 -2
- package/src/components/templates/Asd20ArticleListTemplate/index.vue +2 -51
- package/src/components/utils/Multiselect.vue +6 -3
- package/src/mixins/categoryFilterQuerySyncMixin.js +170 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.25](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.24...ui-next-v2.0.25) (2026-04-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* allow article list and digest pages to process more than one category selection ([5ebf88a](https://github.com/academydistrict20/asd20-ui-next/commit/5ebf88a9d51d44de3976df3951ccf925cec1f269))
|
|
9
|
+
|
|
10
|
+
## [2.0.24](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.23...ui-next-v2.0.24) (2026-04-03)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* prevent double activation of multi select on mouse click ([0e8dfbd](https://github.com/academydistrict20/asd20-ui-next/commit/0e8dfbdd6ac6c20181065c47324f7e624d9f49e6))
|
|
16
|
+
|
|
3
17
|
## [2.0.23](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.0.22...ui-next-v2.0.23) (2026-04-03)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -75,6 +75,22 @@ function normalizeItem(item, itemLabel, itemValue) {
|
|
|
75
75
|
return normalized
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function getItemKey(item, itemLabel, itemValue) {
|
|
79
|
+
const normalized = normalizeItem(item, itemLabel, itemValue)
|
|
80
|
+
return normalized[itemValue] ?? normalized[itemLabel]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sameItemKey(left, right) {
|
|
84
|
+
return (
|
|
85
|
+
left === right ||
|
|
86
|
+
(left !== undefined &&
|
|
87
|
+
left !== null &&
|
|
88
|
+
right !== undefined &&
|
|
89
|
+
right !== null &&
|
|
90
|
+
String(left) === String(right))
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
const MULTISELECT_OMITTED_INPUT_ATTRS = [
|
|
79
95
|
'autocomplete',
|
|
80
96
|
'disabled',
|
|
@@ -101,9 +117,7 @@ export default {
|
|
|
101
117
|
computed: {
|
|
102
118
|
multiselectValue() {
|
|
103
119
|
if (Array.isArray(this.resolvedValue)) {
|
|
104
|
-
return this.resolvedValue.map(item =>
|
|
105
|
-
normalizeItem(item, this.itemLabel, this.itemValue)
|
|
106
|
-
)
|
|
120
|
+
return this.resolvedValue.map(item => this.resolveSelectedItem(item))
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
if (
|
|
@@ -114,7 +128,7 @@ export default {
|
|
|
114
128
|
return []
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
return [
|
|
131
|
+
return [this.resolveSelectedItem(this.resolvedValue)]
|
|
118
132
|
},
|
|
119
133
|
multiselectItems() {
|
|
120
134
|
const seen = new Set()
|
|
@@ -156,9 +170,21 @@ export default {
|
|
|
156
170
|
emitValue(value) {
|
|
157
171
|
this.$emit('update:modelValue', value)
|
|
158
172
|
},
|
|
159
|
-
|
|
173
|
+
resolveSelectedItem(item) {
|
|
160
174
|
const normalized = normalizeItem(item, this.itemLabel, this.itemValue)
|
|
161
|
-
|
|
175
|
+
const itemKey = getItemKey(normalized, this.itemLabel, this.itemValue)
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
this.computedItems.find(candidate =>
|
|
179
|
+
sameItemKey(
|
|
180
|
+
getItemKey(candidate, this.itemLabel, this.itemValue),
|
|
181
|
+
itemKey
|
|
182
|
+
)
|
|
183
|
+
) || normalized
|
|
184
|
+
)
|
|
185
|
+
},
|
|
186
|
+
getItemKey(item) {
|
|
187
|
+
return getItemKey(item, this.itemLabel, this.itemValue)
|
|
162
188
|
},
|
|
163
189
|
addTag(newTag) {
|
|
164
190
|
const normalizedTag = normalizeItem(newTag, this.itemLabel, this.itemValue)
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
:items="categoryOptions"
|
|
91
91
|
:hide-label="true"
|
|
92
92
|
placeholder="Filter by Category"
|
|
93
|
-
@update:modelValue="
|
|
93
|
+
@update:modelValue="onCategorySelect"
|
|
94
94
|
/>
|
|
95
95
|
</div>
|
|
96
96
|
|
|
@@ -282,6 +282,7 @@ import Intersect from '../../../components/utils/Intersect'
|
|
|
282
282
|
|
|
283
283
|
// Mixins
|
|
284
284
|
import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
|
|
285
|
+
import categoryFilterQuerySyncMixin from '../../../mixins/categoryFilterQuerySyncMixin'
|
|
285
286
|
|
|
286
287
|
// Helpers
|
|
287
288
|
import mapMessageToCard from '../../../helpers/mapMessageToCard'
|
|
@@ -304,7 +305,7 @@ export default {
|
|
|
304
305
|
Intersect,
|
|
305
306
|
Asd20MediaSection,
|
|
306
307
|
},
|
|
307
|
-
mixins: [pageTemplateMixin],
|
|
308
|
+
mixins: [pageTemplateMixin, categoryFilterQuerySyncMixin],
|
|
308
309
|
|
|
309
310
|
props: {
|
|
310
311
|
keywords: { type: String, default: '' },
|
|
@@ -299,6 +299,7 @@ import Intersect from '../../../components/utils/Intersect'
|
|
|
299
299
|
|
|
300
300
|
// Mixins
|
|
301
301
|
import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
|
|
302
|
+
import categoryFilterQuerySyncMixin from '../../../mixins/categoryFilterQuerySyncMixin'
|
|
302
303
|
|
|
303
304
|
// Helpers
|
|
304
305
|
import mapMessageToCard from '../../../helpers/mapMessageToCard'
|
|
@@ -322,7 +323,7 @@ export default {
|
|
|
322
323
|
Intersect,
|
|
323
324
|
Asd20MediaSection,
|
|
324
325
|
},
|
|
325
|
-
mixins: [pageTemplateMixin],
|
|
326
|
+
mixins: [pageTemplateMixin, categoryFilterQuerySyncMixin],
|
|
326
327
|
props: {
|
|
327
328
|
keywords: { type: String, default: '' },
|
|
328
329
|
selectedCategories: { type: Array, default: () => [] },
|
|
@@ -332,7 +333,6 @@ export default {
|
|
|
332
333
|
numberToShow: 25,
|
|
333
334
|
counter: 1,
|
|
334
335
|
counter2: 25,
|
|
335
|
-
currentCategory: null,
|
|
336
336
|
}
|
|
337
337
|
},
|
|
338
338
|
computed: {
|
|
@@ -367,57 +367,8 @@ export default {
|
|
|
367
367
|
this.reset() // Reset page data when categories change
|
|
368
368
|
}
|
|
369
369
|
},
|
|
370
|
-
categoryOptions(newOptions) {
|
|
371
|
-
// Ensure $route and $route.query are defined before accessing query.category
|
|
372
|
-
if (this.$route && this.$route.query) {
|
|
373
|
-
const categoryFromQuery = this.$route.query.category
|
|
374
|
-
if (categoryFromQuery && newOptions.length) {
|
|
375
|
-
this.setCategoryFromUrl(categoryFromQuery) // Set category from URL when options are ready
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
370
|
},
|
|
380
371
|
methods: {
|
|
381
|
-
setCategoryFromUrl(category) {
|
|
382
|
-
// Avoid re-processing if the category is already set
|
|
383
|
-
if (this.currentCategory !== category) {
|
|
384
|
-
const matchingCategory = this.categoryOptions.find(
|
|
385
|
-
opt => opt === category
|
|
386
|
-
)
|
|
387
|
-
if (matchingCategory) {
|
|
388
|
-
this.currentCategory = category // Update the current category
|
|
389
|
-
this.$emit('update:selected-categories', [
|
|
390
|
-
{ name: matchingCategory, id: matchingCategory },
|
|
391
|
-
])
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
},
|
|
395
|
-
onCategorySelect(selected) {
|
|
396
|
-
// Ensure $router and $route are defined before using them
|
|
397
|
-
if (this.$router && this.$route) {
|
|
398
|
-
if (selected.length > 0) {
|
|
399
|
-
const selectedCategory = selected[0].id
|
|
400
|
-
|
|
401
|
-
// Only update if the selection has changed
|
|
402
|
-
if (this.currentCategory !== selectedCategory) {
|
|
403
|
-
this.currentCategory = selectedCategory // Track the selected category
|
|
404
|
-
this.$emit('update:selected-categories', selected) // Update selected categories
|
|
405
|
-
this.$router.replace({
|
|
406
|
-
query: { ...this.$route.query, category: selectedCategory },
|
|
407
|
-
})
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
// Handle clearing the selection
|
|
411
|
-
this.currentCategory = null // Reset current category
|
|
412
|
-
this.$emit('update:selected-categories', []) // Clear selected categories
|
|
413
|
-
|
|
414
|
-
// Remove the 'category' parameter entirely from the URL
|
|
415
|
-
const { ...remainingQuery } = this.$route.query || {} // Ensure $route.query is defined
|
|
416
|
-
delete remainingQuery.category
|
|
417
|
-
this.$router.replace({ query: remainingQuery }) // Update the URL without the category
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
},
|
|
421
372
|
nextSet() {
|
|
422
373
|
if (
|
|
423
374
|
Math.ceil(this.counter2 / this.numberToShow) <
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
class="multiselect__tag-icon"
|
|
28
28
|
:aria-label="`Remove ${getOptionLabel(option)}`"
|
|
29
29
|
@mousedown.prevent.stop="removeOption(option)"
|
|
30
|
-
@click.prevent.stop="
|
|
30
|
+
@click.prevent.stop="handleRemoveClick($event, option)"
|
|
31
31
|
/>
|
|
32
32
|
</span>
|
|
33
33
|
</div>
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
role="option"
|
|
83
83
|
:aria-selected="isSelected(option) ? 'true' : 'false'"
|
|
84
84
|
@mousedown.prevent="toggleOption(option)"
|
|
85
|
-
@click.prevent="toggleOption(option)"
|
|
86
85
|
>
|
|
87
86
|
<div class="option__desc">
|
|
88
87
|
<span
|
|
@@ -102,7 +101,6 @@
|
|
|
102
101
|
role="option"
|
|
103
102
|
:aria-selected="'false'"
|
|
104
103
|
@mousedown.prevent="createTag"
|
|
105
|
-
@click.prevent="createTag"
|
|
106
104
|
>
|
|
107
105
|
<div class="option__desc">
|
|
108
106
|
<span class="option__title">
|
|
@@ -384,6 +382,11 @@ export default {
|
|
|
384
382
|
)
|
|
385
383
|
this.open()
|
|
386
384
|
},
|
|
385
|
+
handleRemoveClick(event, option) {
|
|
386
|
+
// Buttons need click handling for keyboard and assistive-tech activation.
|
|
387
|
+
if (event.detail !== 0) return
|
|
388
|
+
this.removeOption(option)
|
|
389
|
+
},
|
|
387
390
|
moveActive(step) {
|
|
388
391
|
if (!this.isOpen) {
|
|
389
392
|
this.open()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
function asArray(value) {
|
|
2
|
+
if (Array.isArray(value)) return value
|
|
3
|
+
if (value === null || value === undefined || value === '') return []
|
|
4
|
+
return [value]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function uniqueValues(values) {
|
|
8
|
+
return [...new Set(values)]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
mounted() {
|
|
13
|
+
this.syncCategoriesFromRoute()
|
|
14
|
+
},
|
|
15
|
+
watch: {
|
|
16
|
+
categoryOptions(newOptions) {
|
|
17
|
+
if (Array.isArray(newOptions) && newOptions.length) {
|
|
18
|
+
this.syncCategoriesFromRoute()
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
methods: {
|
|
23
|
+
normalizeCategoryQueryValues(value) {
|
|
24
|
+
return uniqueValues(
|
|
25
|
+
asArray(value)
|
|
26
|
+
.flatMap(entry => String(entry).split(','))
|
|
27
|
+
.map(entry => entry.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
)
|
|
30
|
+
},
|
|
31
|
+
normalizeCategorySelection(selection) {
|
|
32
|
+
return asArray(selection)
|
|
33
|
+
.map(item => {
|
|
34
|
+
const id =
|
|
35
|
+
item && typeof item === 'object'
|
|
36
|
+
? item.id ?? item.name ?? item.title
|
|
37
|
+
: item
|
|
38
|
+
|
|
39
|
+
if (id === null || id === undefined || id === '') {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const label =
|
|
44
|
+
item && typeof item === 'object'
|
|
45
|
+
? item.name ?? item.title ?? id
|
|
46
|
+
: id
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
name: label,
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
},
|
|
55
|
+
getCategoryQueryValues(query = this.$route && this.$route.query) {
|
|
56
|
+
if (!query) return []
|
|
57
|
+
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(query, 'categories')) {
|
|
59
|
+
return this.normalizeCategoryQueryValues(query.categories)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(query, 'category')) {
|
|
63
|
+
return this.normalizeCategoryQueryValues(query.category)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return []
|
|
67
|
+
},
|
|
68
|
+
buildSelectionFromCategoryQuery() {
|
|
69
|
+
const queryValues = this.getCategoryQueryValues()
|
|
70
|
+
|
|
71
|
+
if (!queryValues.length || !Array.isArray(this.categoryOptions)) {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const optionLookup = new Map(
|
|
76
|
+
this.categoryOptions.map(option => [String(option), String(option)])
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return queryValues
|
|
80
|
+
.filter(value => optionLookup.has(String(value)))
|
|
81
|
+
.map(value => {
|
|
82
|
+
const option = optionLookup.get(String(value))
|
|
83
|
+
return {
|
|
84
|
+
id: option,
|
|
85
|
+
name: option,
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
hasSameCategorySelection(left, right) {
|
|
90
|
+
const leftIds = this.normalizeCategorySelection(left).map(item => String(item.id))
|
|
91
|
+
const rightIds = this.normalizeCategorySelection(right).map(item => String(item.id))
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
leftIds.length === rightIds.length &&
|
|
95
|
+
leftIds.every((id, index) => id === rightIds[index])
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
syncCategoriesFromRoute() {
|
|
99
|
+
const routeSelection = this.buildSelectionFromCategoryQuery()
|
|
100
|
+
|
|
101
|
+
if (!routeSelection.length) return
|
|
102
|
+
if (this.hasSameCategorySelection(routeSelection, this.selectedCategories)) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.$emit('update:selected-categories', routeSelection)
|
|
107
|
+
},
|
|
108
|
+
categoryQueryMatchesSelection(selection) {
|
|
109
|
+
const normalizedSelection = this.normalizeCategorySelection(selection)
|
|
110
|
+
const selectionIds = normalizedSelection.map(item => String(item.id))
|
|
111
|
+
const query = (this.$route && this.$route.query) || {}
|
|
112
|
+
const queryIds = this.getCategoryQueryValues(query)
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
selectionIds.length !== queryIds.length ||
|
|
116
|
+
selectionIds.some((id, index) => id !== queryIds[index])
|
|
117
|
+
) {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const hasCategory = Object.prototype.hasOwnProperty.call(query, 'category')
|
|
122
|
+
const hasCategories = Object.prototype.hasOwnProperty.call(query, 'categories')
|
|
123
|
+
|
|
124
|
+
if (!selectionIds.length) {
|
|
125
|
+
return !hasCategory && !hasCategories
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (String(query.category) !== selectionIds[0]) {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (selectionIds.length === 1) {
|
|
133
|
+
return !hasCategories
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const categoryQueryValues = this.normalizeCategoryQueryValues(query.categories)
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
categoryQueryValues.length === selectionIds.length &&
|
|
140
|
+
categoryQueryValues.every((id, index) => id === selectionIds[index])
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
updateCategoryQuery(selection) {
|
|
144
|
+
if (!this.$router || !this.$route) return
|
|
145
|
+
if (this.categoryQueryMatchesSelection(selection)) return
|
|
146
|
+
|
|
147
|
+
const normalizedSelection = this.normalizeCategorySelection(selection)
|
|
148
|
+
const nextQuery = { ...(this.$route.query || {}) }
|
|
149
|
+
const selectionIds = normalizedSelection.map(item => item.id)
|
|
150
|
+
|
|
151
|
+
delete nextQuery.category
|
|
152
|
+
delete nextQuery.categories
|
|
153
|
+
|
|
154
|
+
if (selectionIds.length === 1) {
|
|
155
|
+
nextQuery.category = selectionIds[0]
|
|
156
|
+
} else if (selectionIds.length > 1) {
|
|
157
|
+
nextQuery.category = selectionIds[0]
|
|
158
|
+
nextQuery.categories = selectionIds
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.$router.replace({ query: nextQuery })
|
|
162
|
+
},
|
|
163
|
+
onCategorySelect(selection) {
|
|
164
|
+
const normalizedSelection = this.normalizeCategorySelection(selection)
|
|
165
|
+
|
|
166
|
+
this.$emit('update:selected-categories', normalizedSelection)
|
|
167
|
+
this.updateCategoryQuery(normalizedSelection)
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
}
|