@explorer-1/vue 0.2.56 → 0.2.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer-1/vue",
3
- "version": "0.2.56",
3
+ "version": "0.2.57",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -58,3 +58,92 @@ export const DateFilter = {
58
58
  truncateFilters: true
59
59
  }
60
60
  }
61
+ export const SubFilters = {
62
+ decorators: [
63
+ () => ({
64
+ template: '<div id="storyRoot" class="md:w-1/2 lg:w-1/3"><story /></div>'
65
+ })
66
+ ],
67
+ args: {
68
+ filterBy: [],
69
+ buckets: [
70
+ { key: 'Solar System', doc_count: 3308 },
71
+ { key: 'Earth', doc_count: 1179 },
72
+ { key: 'Stars and Galaxies', doc_count: 979 },
73
+ { key: 'Technology', doc_count: 480 }
74
+ ],
75
+ groupKey: 'topics',
76
+ groupTitle: 'Topic',
77
+ truncateFilters: false,
78
+ subFilters: {
79
+ solar_system: [
80
+ {
81
+ key: 'Sun',
82
+ agg: 'solar_system_area',
83
+ doc_count: 20
84
+ },
85
+ {
86
+ key: 'Mercury',
87
+ agg: 'solar_system_area',
88
+ doc_count: 21
89
+ },
90
+ {
91
+ key: 'Venus',
92
+ agg: 'solar_system_area',
93
+ doc_count: 22
94
+ }
95
+ ],
96
+ earth: [
97
+ {
98
+ key: 'Sea Level',
99
+ agg: 'solar_system_area',
100
+ doc_count: 20
101
+ },
102
+ {
103
+ key: 'Pollution',
104
+ agg: 'solar_system_area',
105
+ doc_count: 21
106
+ },
107
+ {
108
+ key: 'Climate Change',
109
+ agg: 'solar_system_area',
110
+ doc_count: 22
111
+ }
112
+ ],
113
+ stars_and_galaxies: [
114
+ {
115
+ key: 'Sea Level',
116
+ agg: 'solar_system_area',
117
+ doc_count: 20
118
+ },
119
+ {
120
+ key: 'Pollution',
121
+ agg: 'solar_system_area',
122
+ doc_count: 21
123
+ },
124
+ {
125
+ key: 'Climate Change',
126
+ agg: 'solar_system_area',
127
+ doc_count: 22
128
+ }
129
+ ],
130
+ technology: [
131
+ {
132
+ key: 'Sea Level',
133
+ agg: 'solar_system_area',
134
+ doc_count: 20
135
+ },
136
+ {
137
+ key: 'Pollution',
138
+ agg: 'solar_system_area',
139
+ doc_count: 21
140
+ },
141
+ {
142
+ key: 'Climate Change',
143
+ agg: 'solar_system_area',
144
+ doc_count: 22
145
+ }
146
+ ]
147
+ }
148
+ }
149
+ }
@@ -18,27 +18,98 @@
18
18
  ref="buckets"
19
19
  class="form-group form-check"
20
20
  >
21
- <!-- correct for zero based index -->
22
- <div
23
- v-if="!truncateFilters || index <= checkbox.checkboxLimit - 1"
24
- class="flex my-2"
21
+ <SearchFilterGroupAccordionItem
22
+ v-if="hasSubFilters(bucket.key_as_string || bucket.key)"
23
+ class="w-auto"
24
+ :init-open="subFilterIsInQueryParams(bucket.key_as_string || bucket.key)"
25
25
  >
26
- <input
27
- :id="bucket.key_as_string ? generateId(bucket.key_as_string) : generateId(bucket.key)"
28
- v-model="filterByHandler"
29
- type="checkbox"
30
- :value="bucket.key_as_string ? bucket.key_as_string : bucket.key"
31
- class="text-primary focus:ring-2 focus:ring-primary flex-shrink-0 w-5 h-5 mt-px mr-1 align-middle border rounded-none"
32
- />
33
- <!-- 'key_as_string' exists for dates to have a human readable version -->
34
- <label
35
- :for="bucket.key_as_string ? generateId(bucket.key_as_string) : generateId(bucket.key)"
36
- class="form-check-label pl-2 tracking-normal align-middle"
26
+ <template #header>
27
+ <!-- correct for zero based index -->
28
+ <div
29
+ v-if="!truncateFilters || index <= checkbox.checkboxLimit - 1"
30
+ class="flex flex-grow"
31
+ >
32
+ <input
33
+ :id="
34
+ bucket.key_as_string
35
+ ? generateId(bucket.key_as_string, groupKey)
36
+ : generateId(bucket.key, groupKey)
37
+ "
38
+ v-model="filterByHandler"
39
+ type="checkbox"
40
+ :value="bucket.key_as_string ? bucket.key_as_string : bucket.key"
41
+ class="text-primary focus:ring-2 focus:ring-primary flex-shrink-0 w-5 h-5 mt-px mr-1 align-middle border rounded-none"
42
+ />
43
+ <!-- 'key_as_string' exists for dates to have a human readable version -->
44
+ <label
45
+ :for="
46
+ bucket.key_as_string
47
+ ? generateId(bucket.key_as_string, groupKey)
48
+ : generateId(bucket.key, groupKey)
49
+ "
50
+ class="form-check-label pl-2 tracking-normal align-middle"
51
+ >
52
+ <span class="font-extrabold">{{
53
+ prettyFilterNames(bucket.key_as_string ? bucket.key_as_string : bucket.key)
54
+ }}</span>
55
+ <span class="text-gray-mid-dark font-normal text-sm">
56
+ ({{ bucket.doc_count.toLocaleString() }})
57
+ </span>
58
+ </label>
59
+ </div>
60
+ </template>
61
+ <template #default>
62
+ <!-- dynamic slots for subFilters -->
63
+ <div
64
+ v-if="
65
+ (bucket.key_as_string || bucket.key) &&
66
+ getSubFilters(bucket.key_as_string || bucket.key) &&
67
+ subFilterParentKeys?.length
68
+ "
69
+ class="block"
70
+ >
71
+ <div
72
+ v-if="hasSubFilters(bucket.key_as_string || bucket.key)"
73
+ class="SubFilters"
74
+ >
75
+ <slot :name="`slot_${getSubFilterParentKey(bucket.key_as_string || bucket.key)}`" />
76
+ </div>
77
+ </div>
78
+ </template>
79
+ </SearchFilterGroupAccordionItem>
80
+ <template v-else>
81
+ <!-- correct for zero based index -->
82
+ <div
83
+ v-if="!truncateFilters || index <= checkbox.checkboxLimit - 1"
84
+ class="flex my-2"
37
85
  >
38
- {{ prettyFilterNames(bucket.key_as_string ? bucket.key_as_string : bucket.key) }}
39
- <span class="text-gray-mid-dark"> ({{ bucket.doc_count.toLocaleString() }}) </span>
40
- </label>
41
- </div>
86
+ <input
87
+ :id="
88
+ bucket.key_as_string
89
+ ? generateId(bucket.key_as_string, groupKey)
90
+ : generateId(bucket.key, groupKey)
91
+ "
92
+ v-model="filterByHandler"
93
+ type="checkbox"
94
+ :value="bucket.key_as_string ? bucket.key_as_string : bucket.key"
95
+ class="text-primary focus:ring-2 focus:ring-primary flex-shrink-0 w-5 h-5 mt-px mr-1 align-middle border rounded-none"
96
+ />
97
+ <!-- 'key_as_string' exists for dates to have a human readable version -->
98
+ <label
99
+ :for="
100
+ bucket.key_as_string
101
+ ? generateId(bucket.key_as_string, groupKey)
102
+ : generateId(bucket.key, groupKey)
103
+ "
104
+ class="form-check-label pl-2 tracking-normal align-middle"
105
+ >
106
+ {{ prettyFilterNames(bucket.key_as_string ? bucket.key_as_string : bucket.key) }}
107
+ <span class="text-gray-mid-dark text-sm">
108
+ ({{ bucket.doc_count.toLocaleString() }})
109
+ </span>
110
+ </label>
111
+ </div>
112
+ </template>
42
113
  </div>
43
114
  </div>
44
115
  <div v-else><span class="text-sm text-gray-mid-dark">No matching filters</span></div>
@@ -63,16 +134,28 @@
63
134
  </template>
64
135
  <script lang="ts">
65
136
  // @ts-nocheck
137
+ import { PropType } from 'vue'
66
138
  import isEqual from 'lodash/isEqual.js'
67
139
  import { mapStores } from 'pinia'
68
140
  import { useThemeStore } from '../../store/theme'
69
141
  import { lookupContentType } from './../../utils/lookupContentType'
142
+ import { SubFiltersObject } from './../../interfaces'
143
+ import SearchFilterGroupAccordionItem from './../SearchFilterGroupAccordionItem/SearchFilterGroupAccordionItem.vue'
70
144
 
71
145
  export default {
72
146
  name: 'SearchFilterGroup',
147
+ components: {
148
+ SearchFilterGroupAccordionItem
149
+ },
73
150
  props: {
74
- filterBy: Array,
75
- buckets: null,
151
+ filterBy: {
152
+ type: Array,
153
+ default: undefined
154
+ },
155
+ buckets: {
156
+ type: Object,
157
+ default: undefined
158
+ },
76
159
  hideFilterGroups: {
77
160
  type: Array,
78
161
  default: () => []
@@ -83,12 +166,20 @@ export default {
83
166
  },
84
167
  groupTitle: {
85
168
  type: String,
86
- required: false
169
+ default: ''
87
170
  },
88
171
  truncateFilters: {
89
172
  type: Boolean,
90
173
  required: false,
91
174
  default: false
175
+ },
176
+ subFilters: {
177
+ type: Object as PropType<SubFiltersObject>,
178
+ default: undefined
179
+ },
180
+ subFilterAggKey: {
181
+ type: String,
182
+ default: undefined
92
183
  }
93
184
  },
94
185
  emits: ['update:filterBy'],
@@ -118,7 +209,10 @@ export default {
118
209
  },
119
210
  showFilterGroup() {
120
211
  if (this.themeStore.isEdu) {
121
- return this.groupTitle && !this.hideFilterGroups.includes(this.groupKey)
212
+ return (
213
+ (this.groupTitle || this.buckets?.length) &&
214
+ !this.hideFilterGroups.includes(this.groupKey)
215
+ )
122
216
  } else {
123
217
  return (
124
218
  typeof this.groupKey !== 'undefined' &&
@@ -127,6 +221,9 @@ export default {
127
221
  !this.hideFilterGroups.includes(this.groupKey)
128
222
  )
129
223
  }
224
+ },
225
+ subFilterParentKeys() {
226
+ return this.subFilters ? Object.keys(this.subFilters) : undefined
130
227
  }
131
228
  },
132
229
  watch: {
@@ -152,11 +249,51 @@ export default {
152
249
  }
153
250
  }
154
251
  },
252
+
155
253
  methods: {
156
- generateId(value) {
254
+ generateId(value, group) {
157
255
  let valueString = String(value)
158
256
  valueString = valueString.split(' ').join('')
159
- return `filter_${this.groupKey}_${valueString}`
257
+ return `filter_${group}_${valueString}`
258
+ },
259
+ // used to match sub-filters to their parent
260
+ getSubFilterParentKey(value) {
261
+ let key = value
262
+ if (key) {
263
+ key = key.toLowerCase()
264
+ key = key.replaceAll(' ', '_')
265
+ }
266
+ return key
267
+ },
268
+ hasSubFilters(filterKey) {
269
+ const lookupKey = this.getSubFilterParentKey(filterKey)
270
+ // check if any of the keys are populated in subFilters
271
+ if (this.subFilters && lookupKey && this.subFilters[lookupKey]) {
272
+ return true
273
+ }
274
+ return undefined
275
+ },
276
+ subFilterIsInQueryParams(bucketKey) {
277
+ const subFilters = this.getSubFilters(bucketKey)
278
+ let state = false
279
+ if (subFilters?.length) {
280
+ subFilters.forEach((subFilter) => {
281
+ const agg = subFilter.agg
282
+ const key = subFilter.key
283
+ if (agg && key && this.$route.query[agg]?.includes(key)) {
284
+ state = true
285
+ }
286
+ })
287
+ }
288
+ return state
289
+ },
290
+ getSubFilters(filterKey) {
291
+ const lookupKey = this.getSubFilterParentKey(filterKey)
292
+ // check if any of the keys are populated in subFilters
293
+ if (this.subFilters && lookupKey && this.subFilters[lookupKey]) {
294
+ return this.subFilters[lookupKey]
295
+ }
296
+ return undefined
160
297
  },
161
298
  toggleShowMoreFilters() {
162
299
  if (this.checkbox.checkboxLimit === this.checkbox.initialLimit) {
@@ -178,6 +315,9 @@ export default {
178
315
  name = name.replace('EDU ', '')
179
316
  }
180
317
  return name ? name : key
318
+ },
319
+ getSlotName(key) {
320
+ return `slot_${key}`
181
321
  }
182
322
  }
183
323
  }
@@ -0,0 +1,88 @@
1
+ <script setup lang="ts">
2
+ import { computed, reactive, ref } from 'vue'
3
+ import { uniqueId } from 'lodash'
4
+ import IconPlus from './../Icons/IconPlus.vue'
5
+
6
+ export interface SearchFilterGroupAccordionItemProps {
7
+ initOpen?: boolean
8
+ }
9
+
10
+ // define props
11
+ const props = withDefaults(defineProps<SearchFilterGroupAccordionItemProps>(), {
12
+ initOpen: false
13
+ })
14
+
15
+ const { initOpen } = reactive(props)
16
+
17
+ const ariaExpanded = ref(false)
18
+ const isHidden = ref(!initOpen)
19
+ const itemId = ref(uniqueId())
20
+
21
+ const panelId = computed(() => {
22
+ return `filterGroup_accordion_panel_${itemId.value}`
23
+ })
24
+
25
+ const headingId = computed(() => {
26
+ return `filterGroup_accordion_heading_${itemId.value}`
27
+ })
28
+
29
+ const handleClick = () => {
30
+ ariaExpanded.value = !ariaExpanded.value
31
+ isHidden.value = !isHidden.value
32
+ if (isHidden.value) {
33
+ emit('filterGroupAccordionItemClosed')
34
+ } else {
35
+ emit('filterGroupAccordionItemOpened')
36
+ }
37
+ }
38
+
39
+ const emit = defineEmits(['filterGroupAccordionItemOpened', 'filterGroupAccordionItemClosed'])
40
+ </script>
41
+ <template>
42
+ <div
43
+ class="SearchFilterGroupAccordionItem border-t pt-2.5"
44
+ :class="{
45
+ '-open border-gray-light-mid -mb-px': !isHidden,
46
+ 'border-transparent mb-3': isHidden
47
+ }"
48
+ >
49
+ <div class="SearchFilterGroupAccordionItemHeader flex flex-row w-full content-between">
50
+ <template v-if="$slots.header">
51
+ <slot name="header"></slot>
52
+ <button
53
+ v-bind-once="{ id: headingId, 'aria-controls': panelId }"
54
+ :aria-expanded="ariaExpanded"
55
+ aria-label="Expand"
56
+ class="SearchFilterGroupAccordionItem-trigger group block w-auto text-body-lg pl-4 pr-1 -my-2"
57
+ @click="handleClick()"
58
+ >
59
+ <span
60
+ class="SearchFilterGroupAccordionItem-icon inline-block text-xs text-action flex-shrink-0 transform transition-transform"
61
+ :class="{ '!rotate-45': !isHidden }"
62
+ >
63
+ <IconPlus />
64
+ </span>
65
+ </button>
66
+ </template>
67
+ </div>
68
+ <div
69
+ v-show="!isHidden"
70
+ class="SearchFilterGroupAccordionItemContent text-sm"
71
+ >
72
+ <div
73
+ v-bind-once="{ id: panelId, 'aria-labelledby': headingId }"
74
+ role="region"
75
+ class="SearchFilterGroupAccordionItem-panel border-b border-gray-light-mid"
76
+ >
77
+ <slot name="default" />
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </template>
82
+ <style lang="scss">
83
+ .SearchFilterGroupAccordionItemContent {
84
+ input.mt-px {
85
+ @apply mt-0 #{!important};
86
+ }
87
+ }
88
+ </style>
package/src/interfaces.ts CHANGED
@@ -185,6 +185,27 @@ export interface FooterResponse {
185
185
  footer: any
186
186
  }
187
187
 
188
+ export interface EDUSubjectArea {
189
+ id: number
190
+ primarySubject: EduResourcesSubject
191
+ subjectArea: string
192
+ }
193
+
194
+ export interface SubFiltersObject {
195
+ [key: string]: {
196
+ parentBucketKey: string
197
+ aggParentKey: string
198
+ aggMapKey: string
199
+ subFilterKey: string
200
+ }
201
+ }
202
+
203
+ export interface EduSubjectAreasResponse {
204
+ eduSubjectAreas: {
205
+ eduSubjectAreas: EDUSubjectArea[]
206
+ }
207
+ }
208
+
188
209
  export type Explorer1Theme = 'defaultTheme' | 'ThemeInternal' | 'ThemeEdu'
189
210
 
190
211
  export interface Attributes {
@@ -159,7 +159,7 @@
159
159
  <BaseLink
160
160
  variant="none"
161
161
  link-class="font-primary text-jpl-red hover:text-jpl-red-light w-full py-3 text-lg"
162
- to="/missions/?mission_target={{ item.target }}"
162
+ :to="`/missions/?mission_target=${item.target}`"
163
163
  >
164
164
  {{ item.target }}
165
165
  </BaseLink>