@explorer-1/vue 0.2.56 → 0.2.58

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 (31) hide show
  1. package/components.d.ts +2 -0
  2. package/dist/explorer-1-vue.js +5340 -5200
  3. package/dist/explorer-1-vue.umd.cjs +13 -13
  4. package/dist/src/components/BaseImage/BaseImage.stories.d.ts +1 -4
  5. package/dist/src/components/BaseImage/BaseImage.vue.d.ts +1 -4
  6. package/dist/src/components/BlockAudio/BlockAudio.vue.d.ts +1 -4
  7. package/dist/src/components/BlockCardGridItem/BlockCardGridItemElement.vue.d.ts +1 -4
  8. package/dist/src/components/BlockCircleImageCard/BlockCircleImageCard.vue.d.ts +1 -4
  9. package/dist/src/components/BlockTeaser/BlockTeaser.vue.d.ts +1 -4
  10. package/dist/src/components/BlockText/BlockText.stories.d.ts +13 -0
  11. package/dist/src/components/EventDetailHero/EventDetailHero.vue.d.ts +1 -4
  12. package/dist/src/components/HomepageFeaturedRobot/HomepageFeaturedRobot.vue.d.ts +1 -4
  13. package/dist/src/components/HomepageTeaserBlock/HomepageTeaserBlockCardImage.vue.d.ts +1 -4
  14. package/dist/src/components/ImageDetailContextImage/ImageDetailContextImage.vue.d.ts +1 -4
  15. package/dist/src/components/SearchFilterGroup/SearchFilterGroup.stories.d.ts +43 -0
  16. package/dist/src/components/SearchFilterGroup/SearchFilterGroup.vue.d.ts +121 -11
  17. package/dist/src/components/SearchFilterGroupAccordionItem/SearchFilterGroupAccordionItem.vue.d.ts +44 -0
  18. package/dist/src/components/SearchInput/SearchInput.vue.d.ts +11 -0
  19. package/dist/src/interfaces.d.ts +18 -0
  20. package/dist/src/templates/edu/PageEduHome/PageEduHome.stories.d.ts +70 -0
  21. package/dist/style.css +1 -1
  22. package/package.json +1 -1
  23. package/src/components/PodcastSeriesCarousel/PodcastSeriesCarousel.vue +42 -22
  24. package/src/components/SearchFilterGroup/SearchFilterGroup.stories.js +89 -0
  25. package/src/components/SearchFilterGroup/SearchFilterGroup.vue +165 -25
  26. package/src/components/SearchFilterGroupAccordionItem/SearchFilterGroupAccordionItem.vue +88 -0
  27. package/src/interfaces.ts +21 -0
  28. package/src/templates/PageAudioDetail/PageAudioDetail.vue +1 -0
  29. package/src/templates/PageImageDetail/PageImageDetail.vue +14 -37
  30. package/src/templates/www/PagePodcast/PagePodcast.vue +2 -2
  31. package/tsconfig.json +1 -1
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.58",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -46,7 +46,7 @@
46
46
  :initial-slide="
47
47
  activeSeasonId === initialSeasonId && initialEpisodeIndex ? initialEpisodeIndex : 0
48
48
  "
49
- :slides="activeTabData.episodes"
49
+ :slides="activeTabData.episodes as Partial<Slide>[]"
50
50
  />
51
51
  </keep-alive>
52
52
  </div>
@@ -61,9 +61,10 @@ import ThumbnailCarousel from './../ThumbnailCarousel/ThumbnailCarousel.vue'
61
61
 
62
62
  const route = useRoute()
63
63
 
64
- interface ActiveTab {
64
+ interface Slide {
65
+ url: string
65
66
  title: string
66
- episodes: Episode[]
67
+ thumbnailImage: Partial<ImageObject>
67
68
  }
68
69
 
69
70
  interface Episode {
@@ -72,19 +73,25 @@ interface Episode {
72
73
  publicationDate: any
73
74
  thumbnailImage: Partial<ImageObject>
74
75
  }
76
+
77
+ interface ActiveTab {
78
+ title?: string
79
+ episodes?: Episode[] | Season
80
+ }
81
+
75
82
  interface Season {
76
83
  id: string
77
- url: string
78
- title: string
79
- seasonNumber: number
80
- episodes: Episode[]
84
+ url?: string
85
+ title?: string
86
+ seasonNumber?: number
87
+ episodes?: Episode[]
81
88
  }
82
89
 
83
90
  interface Series {
84
91
  id: string
85
- title: string
86
- url: string
87
- seasons: Season[]
92
+ title?: string
93
+ url?: string
94
+ seasons?: Season[]
88
95
  }
89
96
  export default defineComponent({
90
97
  name: 'PodcastSeriesCarousel',
@@ -111,11 +118,17 @@ export default defineComponent({
111
118
  }
112
119
  },
113
120
  computed: {
114
- sortedSeasons(): Season[] | null {
115
- let seasons = null
121
+ sortedSeasons(): Season[] | undefined {
122
+ let seasons = undefined
116
123
  if (this.series && this.series.seasons) {
117
124
  seasons = this.series.seasons
118
- return seasons.sort((a: Season, b: Season) => a.seasonNumber - b.seasonNumber)
125
+ // @ts-expect-error seasons is an array
126
+ seasons = seasons.toSorted((a: Season, b: Season) => {
127
+ if (a.seasonNumber && b.seasonNumber) {
128
+ return a.seasonNumber - b.seasonNumber
129
+ }
130
+ })
131
+ return seasons
119
132
  }
120
133
  return seasons
121
134
  },
@@ -142,9 +155,9 @@ export default defineComponent({
142
155
  }
143
156
  return null
144
157
  },
145
- activeTabData(): ActiveTab | undefined {
146
- let season: Season | undefined = undefined
147
- if (this.series?.seasons) {
158
+ activeTabData(): ActiveTab | Season | undefined {
159
+ let season
160
+ if (this.series?.seasons?.length) {
148
161
  if (this.activeSeasonId) {
149
162
  season = this.series.seasons.find((o: Season) => {
150
163
  return o.id === this.activeSeasonId
@@ -152,14 +165,21 @@ export default defineComponent({
152
165
  } else {
153
166
  season = this.series.seasons[0]
154
167
  }
155
- if (season?.episodes) {
156
- season.episodes.sort(
157
- (a: Episode, b: Episode) =>
158
- new Date(a.publicationDate).getTime() - new Date(b.publicationDate).getTime()
159
- )
168
+ if (season?.episodes?.length) {
169
+ let episodes: Episode[] = season.episodes
170
+ // @ts-expect-error episodes is an array
171
+ episodes = episodes.toSorted((a: Episode, b: Episode) => {
172
+ if (a.publicationDate && b.publicationDate) {
173
+ return new Date(a.publicationDate).getTime() - new Date(b.publicationDate).getTime()
174
+ }
175
+ })
176
+ season = {
177
+ ...season,
178
+ episodes: episodes
179
+ }
160
180
  }
161
181
  }
162
- return season ? season : undefined
182
+ return season
163
183
  }
164
184
  },
165
185
  methods: {
@@ -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 {
@@ -204,6 +204,7 @@
204
204
  </LayoutHelper>
205
205
 
206
206
  <PodcastSeriesCarousel
207
+ v-if="data?.series"
207
208
  :series="data.series"
208
209
  :initial-season-id="data.parent ? data.parent.id : null"
209
210
  class="mb-12 lg:mb-24"