@abi-software/map-side-bar 2.3.0 → 2.4.0-alpha-1

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 (43) hide show
  1. package/.eslintrc.js +12 -12
  2. package/.postcssrc.json +5 -5
  3. package/LICENSE +201 -201
  4. package/README.md +168 -168
  5. package/cypress.config.js +23 -23
  6. package/dist/data/pmr-sample.json +3181 -0
  7. package/dist/map-side-bar.js +15142 -9024
  8. package/dist/map-side-bar.umd.cjs +50 -103
  9. package/dist/style.css +1 -1
  10. package/package.json +77 -77
  11. package/public/data/pmr-sample.json +3181 -0
  12. package/reporter-config.json +9 -9
  13. package/src/App.vue +266 -265
  14. package/src/algolia/algolia.js +255 -242
  15. package/src/algolia/utils.js +100 -100
  16. package/src/assets/_variables.scss +43 -43
  17. package/src/assets/styles.scss +6 -6
  18. package/src/components/BadgesGroup.vue +124 -124
  19. package/src/components/ConnectivityInfo.vue +619 -619
  20. package/src/components/DatasetCard.vue +367 -357
  21. package/src/components/EventBus.js +3 -3
  22. package/src/components/ExternalResourceCard.vue +113 -113
  23. package/src/components/FlatmapDatasetCard.vue +171 -0
  24. package/src/components/ImageGallery.vue +542 -542
  25. package/src/components/PMRDatasetCard.vue +237 -0
  26. package/src/components/SearchFilters.vue +1023 -1006
  27. package/src/components/SearchHistory.vue +175 -175
  28. package/src/components/SideBar.vue +436 -436
  29. package/src/components/SidebarContent.vue +730 -603
  30. package/src/components/Tabs.vue +145 -145
  31. package/src/components/allPaths.js +5928 -0
  32. package/src/components/index.js +8 -8
  33. package/src/components/pmrTest.js +4 -0
  34. package/src/components/species-map.js +8 -8
  35. package/src/components.d.ts +2 -0
  36. package/src/exampleConnectivityInput.js +291 -291
  37. package/src/flatmapQueries/flatmapQueries.js +169 -0
  38. package/src/main.js +9 -9
  39. package/src/mixins/S3Bucket.vue +37 -37
  40. package/src/mixins/mixedPageCalculation.vue +78 -0
  41. package/static.json +6 -6
  42. package/vite.config.js +55 -55
  43. package/vuese-generator.js +65 -65
@@ -1,1006 +1,1023 @@
1
- <template>
2
- <div class="filters">
3
- <MapSvgSpriteColor />
4
- <div class="cascader-tag" v-if="presentTags.length > 0">
5
- <el-tag
6
- class="ml-2"
7
- type="info"
8
- closable
9
- @close="cascadeTagClose(presentTags[0])"
10
- >
11
- {{ presentTags[0] }}
12
- </el-tag>
13
- <el-popover
14
- v-if="presentTags.length > 1"
15
- placement="bottom-start"
16
- :width="200"
17
- trigger="hover"
18
- >
19
- <template #default>
20
- <div class="el-tags-container">
21
- <el-tag
22
- v-for="(tag, i) in presentTags.slice(1)"
23
- :key="i"
24
- class="ml-2"
25
- type="info"
26
- closable
27
- @close="cascadeTagClose(tag)"
28
- >
29
- {{ tag }}
30
- </el-tag>
31
- </div>
32
- </template>
33
- <template #reference>
34
- <div class="el-tags-container">
35
- <el-tag
36
- v-if="presentTags.length > 1"
37
- class="ml-2"
38
- type="info"
39
- >
40
- +{{ presentTags.length - 1 }}
41
- </el-tag>
42
- </div>
43
- </template>
44
- </el-popover>
45
- </div>
46
- <transition name="el-zoom-in-top">
47
- <span v-show="showFilters" v-loading="!cascaderIsReady" class="search-filters transition-box">
48
- <el-cascader
49
- class="cascader"
50
- ref="cascader"
51
- v-model="cascadeSelected"
52
- size="large"
53
- placeholder=" "
54
- :collapse-tags="true"
55
- collapse-tags-tooltip
56
- :options="options"
57
- :props="props"
58
- @change="cascadeEvent($event)"
59
- @expand-change="cascadeExpandChange"
60
- :show-all-levels="true"
61
- popper-class="sidebar-cascader-popper"
62
- />
63
- <div v-if="showFiltersText" class="filter-default-value">Filters</div>
64
- <el-popover
65
- title="How do filters work?"
66
- width="250"
67
- trigger="hover"
68
- :append-to-body="false"
69
- popper-class="popover"
70
- >
71
- <template #reference>
72
- <MapSvgIcon icon="help" class="help" />
73
- </template>
74
- <div>
75
- <strong>Within categories:</strong> OR
76
- <br />
77
- example: 'heart' OR 'colon'
78
- <br />
79
- <br />
80
- <strong>Between categories:</strong> AND
81
- <br />
82
- example: 'rat' AND 'lung'
83
- </div>
84
- </el-popover>
85
- </span>
86
- </transition>
87
- <div class="dataset-shown">
88
- <span class="dataset-results-feedback">{{ numberOfResultsText }}</span>
89
- <el-select
90
- class="number-shown-select"
91
- v-model="numberShown"
92
- placeholder="10"
93
- @change="numberShownChanged($event)"
94
- >
95
- <el-option
96
- v-for="item in numberDatasetsShown"
97
- :key="item"
98
- :label="item"
99
- :value="item"
100
- ></el-option>
101
- </el-select>
102
- </div>
103
- </div>
104
- </template>
105
-
106
- <script>
107
- /* eslint-disable no-alert, no-console */
108
- import { markRaw } from 'vue'
109
- import {
110
- ElOption as Option,
111
- ElSelect as Select,
112
- ElPopover as Popover,
113
- ElCascader as Cascader,
114
- } from 'element-plus'
115
- import speciesMap from './species-map.js'
116
- import { MapSvgIcon, MapSvgSpriteColor } from "@abi-software/svg-sprite";
117
- import '@abi-software/svg-sprite/dist/style.css'
118
-
119
- import { AlgoliaClient } from '../algolia/algolia.js'
120
- import { facetPropPathMapping } from '../algolia/utils.js'
121
- import EventBus from './EventBus.js'
122
-
123
- const capitalise = function (txt) {
124
- return txt.charAt(0).toUpperCase() + txt.slice(1)
125
- }
126
-
127
- const convertReadableLabel = function (original) {
128
- const name = original.toLowerCase()
129
- if (speciesMap[name]) {
130
- return capitalise(speciesMap[name])
131
- } else {
132
- return capitalise(name)
133
- }
134
- }
135
-
136
- export default {
137
- name: 'SearchFilters',
138
- components: {
139
- MapSvgIcon,
140
- MapSvgSpriteColor,
141
- Option,
142
- Select,
143
- Popover,
144
- Cascader
145
- },
146
- props: {
147
- /**
148
- * Object containing information for
149
- * the required viewing.
150
- */
151
- entry: Object,
152
- envVars: {
153
- type: Object,
154
- default: () => {},
155
- },
156
- },
157
- data: function () {
158
- return {
159
- cascaderIsReady: false,
160
- previousShowAllChecked: {
161
- species: false,
162
- gender: false,
163
- organ: false,
164
- datasets: false,
165
- },
166
- showFilters: true,
167
- showFiltersText: true,
168
- cascadeSelected: [],
169
- cascadeSelectedWithBoolean: [],
170
- numberShown: 10,
171
- filters: [],
172
- facets: ['Species', 'Gender', 'Organ', 'Datasets'],
173
- numberDatasetsShown: ['10', '20', '50'],
174
- props: { multiple: true },
175
- options: [
176
- {
177
- value: 'Species',
178
- label: 'Species',
179
- children: [{}],
180
- },
181
- ],
182
- presentTags:[],
183
- }
184
- },
185
- setup() {
186
- const cascaderTags = markRaw({});
187
- const correctnessCheck = markRaw({
188
- term: new Set(),
189
- facet: new Set(),
190
- facet2: new Set()
191
- });
192
- return { cascaderTags, correctnessCheck }
193
- },
194
- computed: {
195
- numberOfResultsText: function () {
196
- return `${this.entry.numberOfHits} results | Showing`
197
- },
198
- },
199
- methods: {
200
- createCascaderItemValue: function (
201
- term,
202
- facet1 = undefined,
203
- facet2 = undefined
204
- ) {
205
- let value = term
206
- if (facet1) value = `${term}>${facet1}`
207
- if (facet1 && facet2) value = `${term}>${facet1}>${facet2}`
208
- if (!facet1 && facet2)
209
- console.warn(
210
- `Warning: ${facet2} provided without its parent, this will not be shown in the cascader`
211
- )
212
- return value
213
- },
214
- populateCascader: function () {
215
- return new Promise((resolve) => {
216
- // Algolia facet serach
217
- this.algoliaClient
218
- .getAlgoliaFacets(facetPropPathMapping)
219
- .then((data) => {
220
- this.facets = data
221
- EventBus.emit('available-facets', data)
222
- this.options = data
223
-
224
- // create top level of options in cascader
225
- this.options.forEach((facet, i) => {
226
- this.options[i].total = this.countTotalFacet(facet)
227
-
228
- this.options[i].label = convertReadableLabel(facet.label)
229
- this.options[i].value = this.createCascaderItemValue(
230
- facet.key,
231
- undefined
232
- )
233
-
234
- // put "Show all" as first option
235
- this.options[i].children.unshift({
236
- value: this.createCascaderItemValue('Show all'),
237
- label: 'Show all',
238
- })
239
-
240
- // populate second level of options
241
- this.options[i].children.forEach((facetItem, j) => {
242
- this.options[i].children[j].label = convertReadableLabel(
243
- facetItem.label
244
- )
245
- this.options[i].children[j].value =
246
- this.createCascaderItemValue(facet.label, facetItem.label)
247
- if (
248
- this.options[i].children[j].children &&
249
- this.options[i].children[j].children.length > 0
250
- ) {
251
- this.options[i].children[j].children.forEach((term, k) => {
252
- this.options[i].children[j].children[k].label =
253
- convertReadableLabel(term.label)
254
- this.options[i].children[j].children[k].value =
255
- this.createCascaderItemValue(
256
- facet.label,
257
- facetItem.label,
258
- term.label
259
- )
260
- })
261
- }
262
- })
263
- })
264
- })
265
- .finally(() => {
266
- resolve()
267
- })
268
- })
269
- },
270
- /**
271
- * Create manual events when cascader tag is closed
272
- */
273
- cascadeTagClose: function (tag) {
274
- let manualEvent = []
275
-
276
- Object.entries(this.cascaderTags).map((entry) => {
277
- const term = entry[0], facet = entry[1] // Either "Array" or "Object", depends on the cascader item level
278
- const option = this.options.filter((option) => option.label == term)[0]
279
- const key = option.key
280
-
281
- for (let index = 0; index < option.children.length; index++) {
282
- const child = option.children[index]
283
- const label = child.label, value = child.value
284
-
285
- if (Array.isArray(facet)) {
286
- // push "Show all" if there is no item checked
287
- if (facet.length === 0 && label.toLowerCase() === "show all") {
288
- manualEvent.push([key, value])
289
- break
290
- // push all checked items
291
- } else if (label !== tag && facet.includes(label))
292
- manualEvent.push([key, value])
293
- } else {
294
- // loop nested cascader items
295
- Object.entries(facet).map((entry2) => {
296
- const term2 = entry2[0], facet2 = entry2[1] // object key, object value
297
-
298
- if (term2 === label) {
299
- child.children.map((child2) => {
300
- const label2 = child2.label, value2 = child2.value
301
- // push all checked items
302
- if (label2 !== tag && facet2.includes(label2))
303
- manualEvent.push([key, value2])
304
- })
305
- }
306
- })
307
- }
308
- }
309
- })
310
- this.cascadeEvent(manualEvent)
311
- },
312
- /**
313
- * Re-generate 'cascaderTags' and 'presentTags'
314
- * Not able to avoid wrong facet at the moment
315
- */
316
- tagsChangedCallback: function (event) {
317
- if (this.correctnessCheck.term && this.correctnessCheck.facet && this.correctnessCheck.facet2) {
318
- this.options.map((option) => {
319
- this.correctnessCheck.term.add(option.label)
320
- option.children.map((child) => {
321
- this.correctnessCheck.facet.add(child.label)
322
- if (option.label === 'Anatomical structure' && child.label !== 'Show all') {
323
- child.children.map((child2) => {
324
- this.correctnessCheck.facet2.add(child2.label)
325
- })
326
- }
327
- })
328
- })
329
- }
330
-
331
- this.cascaderTags = {}
332
- this.presentTags = []
333
- event.map((item) => {
334
- const { facet, facet2, term } = item
335
- if (this.correctnessCheck.term.has(term) && this.correctnessCheck.facet.has(facet)) {
336
- if (facet2) {
337
- if (this.correctnessCheck.facet2.has(facet2)) {
338
- if (term in this.cascaderTags) {
339
- if (facet in this.cascaderTags[term]) this.cascaderTags[term][facet].push(facet2)
340
- else this.cascaderTags[term][facet] = [facet2]
341
- } else {
342
- this.cascaderTags[term] = {}
343
- this.cascaderTags[term][facet] = [facet2]
344
- }
345
- }
346
- } else {
347
- // If 'cascaderTags' has key 'Anatomical structure',
348
- // it's value type will be Object (because it has nested facets),
349
- // in this case 'push' action will not available.
350
- if (term in this.cascaderTags && term !== 'Anatomical structure')
351
- this.cascaderTags[term].push(facet)
352
- else {
353
- if (facet.toLowerCase() !== "show all") this.cascaderTags[term] = [facet]
354
- else this.cascaderTags[term] = []
355
- }
356
- }
357
- }
358
- })
359
-
360
- Object.values(this.cascaderTags).map((value) => {
361
- const extend = Array.isArray(value) ? value : Object.values(value).flat(1)
362
- this.presentTags = [...this.presentTags, ...extend]
363
- })
364
- this.presentTags = [...new Set(this.presentTags)]
365
- if (this.presentTags.length > 0) this.showFiltersText = false
366
- else this.showFiltersText = true
367
- },
368
- /**
369
- * Support for function 'showAllEventModifierForAutoCheckAll'
370
- * Called in function 'populateCascader'
371
- */
372
- countTotalFacet: function (facet) {
373
- if (['anatomy.organ.category.name'].includes(facet.key)) {
374
- const count = facet.children.reduce((total, num) => {
375
- // The first 'total' will be an object
376
- total = typeof total == 'number' ? total : total.children.length
377
- return total + num.children.length
378
- })
379
- return count
380
- }
381
- return facet.children.length
382
- },
383
- /**
384
- * When check/uncheck all child items, automatically check "Show all"
385
- */
386
- showAllEventModifierForAutoCheckAll: function (event) {
387
- const currentKeys = {}
388
- event.map((e) => {
389
- const eventKey = e[0]
390
- if (eventKey in currentKeys) currentKeys[eventKey] += 1
391
- else currentKeys[eventKey] = 1
392
- })
393
- this.options.map((option) => {
394
- const key = option.key
395
- const value = option.children.filter((child) => child.label === "Show all")[0].value
396
- const total = option.total
397
- // Remove events if all child items is checked
398
- if (currentKeys[key] === total) {
399
- event = event.filter((e) => e[0] !== option.key)
400
- delete currentKeys[key]
401
- }
402
- // Add 'Show all' if facet type not exist in event
403
- if (!(key in currentKeys)) event.unshift([key, value])
404
- })
405
- return event
406
- },
407
- // cascadeEvent: initiate searches based off cascader changes
408
- cascadeEvent: function (eventIn) {
409
- let event = [...eventIn]
410
- if (event) {
411
- // Check for show all in selected cascade options
412
-
413
- event = this.showAllEventModifier(event)
414
-
415
- event = this.showAllEventModifierForAutoCheckAll(event)
416
- /**
417
- * Move the new added event to the beginning
418
- * Otherwise, cascader will show different expand item
419
- */
420
- if (this.__expandItem__) {
421
- let position = 0
422
- if (this.__expandItem__.length > 1) {
423
- position = 1
424
- }
425
- const current = event.filter((e) => e[position] == this.__expandItem__[position]);
426
- const rest = event.filter((e) => e[position] !== this.__expandItem__[position]);
427
- event = [...current, ...rest]
428
- }
429
- // Create results for the filter update
430
- let filterKeys = event
431
- .filter((selection) => selection !== undefined)
432
- .map((fs) => {
433
- let { hString, bString } =
434
- this.findHierarachyStringAndBooleanString(fs)
435
- let { facet, facet2, term } =
436
- this.getFacetsFromHierarchyString(hString)
437
- return {
438
- facetPropPath: fs[0],
439
- facet: facet,
440
- facet2: facet2,
441
- term: term,
442
- AND: bString, // for setting the boolean
443
- }
444
- })
445
-
446
- // Move results from arrays to object for use on scicrunch (note that we remove 'duplicate' as that is only needed for filter keys)
447
- let filters = event
448
- .filter((selection) => selection !== undefined)
449
- .map((fs) => {
450
- let facetSubPropPath = undefined
451
- let propPath = fs[0].includes('duplicate')
452
- ? fs[0].split('duplicate')[0]
453
- : fs[0]
454
- let { hString, bString } =
455
- this.findHierarachyStringAndBooleanString(fs)
456
- let { facet, facet2, term } =
457
- this.getFacetsFromHierarchyString(hString)
458
- if (facet2) {
459
- // We need to change the propPath if we are at the third level of the cascader
460
- facet = facet2
461
- facetSubPropPath = 'anatomy.organ.name'
462
- }
463
- return {
464
- facetPropPath: propPath,
465
- facet: facet,
466
- term: term,
467
- AND: bString, // for setting the boolean
468
- facetSubPropPath: facetSubPropPath, // will be used for filters if we are at the third level of the cascader
469
- }
470
- })
471
-
472
- this.$emit('loading', true) // let sidebarcontent wait for the requests
473
- this.$emit('filterResults', filters) // emit filters for apps above sidebar
474
- this.setCascader(filterKeys) //update our cascader v-model if we modified the event
475
- this.cssMods() // update css for the cascader
476
- }
477
- },
478
- //this fucntion is needed as we previously stored booleans in the array of event that
479
- // are stored in the cascader
480
- findHierarachyStringAndBooleanString(cascadeEventItem) {
481
- let hString, bString
482
- if (cascadeEventItem.length >= 3) {
483
- if (cascadeEventItem[2] &&
484
- (typeof cascadeEventItem[2] === 'string' ||
485
- cascadeEventItem[2] instanceof String) &&
486
- cascadeEventItem[2].split('>').length > 2) {
487
- hString = cascadeEventItem[2]
488
- bString = cascadeEventItem.length == 4 ? cascadeEventItem[3] : undefined
489
- } else {
490
- hString = cascadeEventItem[1]
491
- bString = cascadeEventItem[2]
492
- }
493
- } else {
494
- hString = cascadeEventItem[1]
495
- bString = undefined
496
- }
497
- return { hString, bString }
498
- },
499
- // Splits the terms and facets from the string stored in the cascader
500
- getFacetsFromHierarchyString(hierarchyString) {
501
- let facet,
502
- term,
503
- facet2 = undefined
504
- let fsSplit = hierarchyString.split('>')
505
- if (fsSplit.length == 3) {
506
- // if we are at the third level of the cascader
507
- facet2 = fsSplit[2]
508
- facet = fsSplit[1]
509
- term = fsSplit[0]
510
- } else {
511
- facet = fsSplit[1]
512
- term = fsSplit[0]
513
- }
514
- return { facet, facet2, term }
515
- },
516
- // showAllEventModifier: Modifies a cascade event to unclick all selections in category if "show all" is clicked. Also unchecks "Show all" if any secection is clicked
517
- // *NOTE* Does NOT remove 'Show all' selections from showing in 'cascadeSelected'
518
- showAllEventModifier: function (event) {
519
- // check if show all is in the cascader checked option list
520
- let hasShowAll = event
521
- .map((ev) => (ev ? ev[1].toLowerCase().includes('show all') : false))
522
- .includes(true)
523
- // remove all selected options below the show all if checked
524
- if (hasShowAll) {
525
- let modifiedEvent = []
526
- let facetMaps = {}
527
- //catagorised different facet items
528
- for (const i in event) {
529
- if (facetMaps[event[i][0]] === undefined) facetMaps[event[i][0]] = []
530
- facetMaps[event[i][0]].push(event[i])
531
- }
532
- // go through each facets
533
- for (const facet in facetMaps) {
534
- let showAll = undefined
535
- // Find the show all item if any
536
- for (let i = facetMaps[facet].length - 1; i >= 0; i--) {
537
- if (facetMaps[facet][i][1].toLowerCase().includes('show all')) {
538
- //seperate the showAll item and the rest
539
- showAll = facetMaps[facet].splice(i, 1)[0]
540
- break
541
- }
542
- }
543
- if (showAll) {
544
- if (this.previousShowAllChecked[facet]) {
545
- //Unset the show all if it was present previously
546
- //and there are other items
547
- if (facetMaps[facet].length > 0)
548
- modifiedEvent.push(...facetMaps[facet])
549
- else modifiedEvent.push(showAll)
550
- } else {
551
- //showAll is turned on
552
- modifiedEvent.push(showAll)
553
- }
554
- } else {
555
- modifiedEvent.push(...facetMaps[facet])
556
- }
557
- }
558
- //Make sure the expanded item are sorted first.
559
- return modifiedEvent.sort((a, b) => {
560
- if (this.__expandItem__) {
561
- if (a[0] == this.__expandItem__) {
562
- if (b[0] == this.__expandItem__) {
563
- return 0
564
- } else {
565
- return -1
566
- }
567
- } else if (b[0] == this.__expandItem__) {
568
- if (a[0] == this.__expandItem__) {
569
- return 0
570
- } else {
571
- return 1
572
- }
573
- } else {
574
- return 0
575
- }
576
- } else return 0
577
- })
578
- }
579
- return event
580
- },
581
- cascadeExpandChange: function (event) {
582
- //work around as the expand item may change on modifying the cascade props
583
- this.__expandItem__ = event
584
- this.cssMods()
585
- },
586
- numberShownChanged: function (event) {
587
- this.$emit('numberPerPage', parseInt(event))
588
- },
589
- updatePreviousShowAllChecked: function (options) {
590
- //Reset the states
591
- for (const facet in this.previousShowAllChecked) {
592
- this.previousShowAllChecked[facet] = false
593
- }
594
- options.forEach((element) => {
595
- if (element[1].toLowerCase().includes('show all'))
596
- this.previousShowAllChecked[element[0]] = true
597
- })
598
- },
599
- // setCascader: Clears previous selections and takes in an array of facets to select: filterFacets
600
- // facets are in the form:
601
- // {
602
- // facetPropPath: 'anatomy.organ.name',
603
- // term: 'Sex',
604
- // facet: 'Male'
605
- // AND: true // Optional value for setting the boolean within a facet
606
- // }
607
- setCascader: function (filterFacets) {
608
- //Do not set the value unless it is ready
609
- if (this.cascaderIsReady && filterFacets && filterFacets.length != 0) {
610
- //An inner function only used by this function
611
- const createFilter = (e) => {
612
- let filters = [
613
- e.facetPropPath,
614
- this.createCascaderItemValue(capitalise(e.term), e.facet),
615
- ]
616
- // Add the third level of the cascader if it exists
617
- if (e.facet2) {
618
- filters.push(
619
- this.createCascaderItemValue(
620
- capitalise(e.term),
621
- e.facet,
622
- e.facet2
623
- )
624
- )
625
- }
626
- return filters;
627
- }
628
-
629
- this.cascadeSelected = filterFacets.map((e) => {
630
- let filters = createFilter(e)
631
- return filters
632
- })
633
-
634
- // Unforttunately the cascader is very particular about it's v-model
635
- // to get around this we create a clone of it and use this clone for adding our boolean information
636
- this.cascadeSelectedWithBoolean = filterFacets.map((e) => {
637
- let filters = createFilter(e)
638
- filters.push(e.AND)
639
- return filters
640
- })
641
- this.updatePreviousShowAllChecked(this.cascadeSelected)
642
- }
643
- this.tagsChangedCallback(filterFacets);
644
- },
645
- addFilter: function (filterToAdd) {
646
- //Do not set the value unless it is ready
647
- if (this.cascaderIsReady && filterToAdd) {
648
- let filter = this.validateAndConvertFilterToHierarchical(filterToAdd)
649
- if (filter) {
650
- this.cascadeSelected.filter((f) => f.term != filter.term)
651
- this.cascadeSelected.push([
652
- filter.facetPropPath,
653
- this.createCascaderItemValue(filter.term, filter.facet),
654
- this.createCascaderItemValue(
655
- filter.term,
656
- filter.facet,
657
- filter.facet2
658
- ),
659
- ])
660
- this.cascadeSelectedWithBoolean.push([
661
- filter.facetPropPath,
662
- this.createCascaderItemValue(filter.term, filter.facet),
663
- this.createCascaderItemValue(
664
- filter.term,
665
- filter.facet,
666
- filter.facet2
667
- ),
668
- filter.AND,
669
- ])
670
- // The 'AND' her is to set the boolean value when we search on the filters. It can be undefined without breaking anything
671
- return true
672
- }
673
- }
674
- },
675
- initiateSearch: function () {
676
- this.cascadeEvent(this.cascadeSelectedWithBoolean)
677
- },
678
- // checkShowAllBoxes: Checks each 'Show all' cascade option by using the setCascader function
679
- checkShowAllBoxes: function () {
680
- this.setCascader(
681
- this.options.map((option) => {
682
- return {
683
- facetPropPath: option.value,
684
- term: option.label,
685
- facet: 'Show all',
686
- }
687
- })
688
- )
689
- },
690
- makeCascadeLabelsClickable: function () {
691
- // Next tick allows the cascader menu to change
692
- this.$nextTick(() => {
693
- document
694
- .querySelectorAll('.sidebar-cascader-popper .el-cascader-node__label')
695
- .forEach((el) => {
696
- // step through each cascade label
697
- el.onclick = function () {
698
- const checkbox = this.previousElementSibling
699
- if (checkbox) {
700
- if (!checkbox.parentElement.attributes['aria-owns']) {
701
- // check if we are at the lowest level of cascader
702
- this.previousElementSibling.click() // Click the checkbox
703
- }
704
- }
705
- }
706
- })
707
- })
708
- },
709
-
710
- cssMods: function () {
711
- this.makeCascadeLabelsClickable()
712
- this.removeTopLevelCascaderCheckboxes()
713
- },
714
-
715
- removeTopLevelCascaderCheckboxes: function () {
716
- // Next tick allows the cascader menu to change
717
- this.$nextTick(() => {
718
- let cascadePanels = document.querySelectorAll(
719
- '.sidebar-cascader-popper .el-cascader-menu__list'
720
- )
721
- // Hide the checkboxes on the first level of the cascader
722
- cascadePanels[0]
723
- .querySelectorAll('.el-checkbox__input')
724
- .forEach((el) => (el.style.display = 'none'))
725
- })
726
- },
727
- /*
728
- * Given a filter, the function below returns the filter in the format of the cascader, returns false if facet is not found
729
- */
730
- validateAndConvertFilterToHierarchical: function (filter) {
731
- if (filter && filter.facet && filter.term) {
732
- if (filter.facet2) {
733
- return filter // if it has a second term we will assume it is hierarchical and return it as is
734
- } else {
735
- for (const firstLayer of this.options) {
736
- if (firstLayer.value === filter.facetPropPath) {
737
- for (const secondLayer of firstLayer.children) {
738
- if (secondLayer.label === filter.facet) {
739
- // if we find a match on the second level, the filter will already be correct
740
- return filter
741
- } else {
742
- if (secondLayer.children && secondLayer.children.length > 0) {
743
- for (const thirdLayer of secondLayer.children) {
744
- if (thirdLayer.label === filter.facet) {
745
- // If we find a match on the third level, we need to switch facet1 to facet2
746
- // and populate facet1 with its parents label.
747
- filter.facet2 = thirdLayer.label
748
- filter.facet = secondLayer.label
749
- return filter
750
- }
751
- }
752
- }
753
- }
754
- }
755
- }
756
- }
757
- }
758
- }
759
- return false
760
- },
761
- getHierarchicalValidatedFilters: function (filters) {
762
- if (filters) {
763
- if (this.cascaderIsReady) {
764
- const result = []
765
- filters.forEach((filter) => {
766
- const validatedFilter =
767
- this.validateAndConvertFilterToHierarchical(filter)
768
- if (validatedFilter) {
769
- result.push(validatedFilter)
770
- }
771
- })
772
- return result
773
- } else return filters
774
- }
775
- return []
776
- },
777
- },
778
- mounted: function () {
779
- this.algoliaClient = new AlgoliaClient(
780
- this.envVars.ALGOLIA_ID,
781
- this.envVars.ALGOLIA_KEY,
782
- this.envVars.PENNSIEVE_API_LOCATION
783
- )
784
- this.algoliaClient.initIndex(this.envVars.ALGOLIA_INDEX)
785
- this.populateCascader().then(() => {
786
- this.cascaderIsReady = true
787
- this.checkShowAllBoxes()
788
- this.setCascader(this.entry.filterFacets)
789
- this.cssMods()
790
- this.$emit('cascaderReady')
791
- })
792
- },
793
- }
794
- </script>
795
-
796
- <style lang="scss" scoped>
797
-
798
- .filters {
799
- position: relative;
800
- }
801
-
802
- .cascader-tag {
803
- position: absolute;
804
- top: 8px;
805
- left: 8px;
806
- z-index: 1;
807
- display: flex;
808
- gap: 4px;
809
- }
810
-
811
- .el-tags-container {
812
- display: flex;
813
- flex-wrap: wrap;
814
- gap: 4px;
815
- }
816
-
817
- .el-tag {
818
- .cascader-tag &,
819
- .el-tags-container & {
820
- font-family: 'Asap', sans-serif;
821
- font-size: 12px;
822
- color: #303133 !important;
823
- background-color: #fff;
824
- border-color: #dcdfe6 !important;
825
- }
826
- }
827
-
828
- :deep(.el-cascader__tags) {
829
- display: none;
830
- }
831
-
832
- .filter-default-value {
833
- pointer-events: none;
834
- position: absolute;
835
- top: 0;
836
- left: 0;
837
- padding-top: 10px;
838
- padding-left: 16px;
839
- }
840
-
841
- .help {
842
- width: 24px !important;
843
- height: 24px;
844
- transform: scale(1.1);
845
- cursor: pointer;
846
- }
847
-
848
- .popover {
849
- color: rgb(48, 49, 51);
850
- font-family: Asap;
851
- margin: 12px;
852
- }
853
-
854
- .filter-icon-inside {
855
- width: 12px !important;
856
- height: 12px !important;
857
- color: #292b66;
858
- transform: scale(2) !important;
859
- margin-bottom: 0px !important;
860
- }
861
-
862
- .cascader {
863
- font-family: Asap;
864
- font-size: 14px;
865
- font-weight: 500;
866
- font-stretch: normal;
867
- font-style: normal;
868
- line-height: normal;
869
- letter-spacing: normal;
870
- color: #292b66;
871
- text-align: center;
872
- padding-bottom: 6px;
873
- }
874
-
875
- .dataset-shown {
876
- display: flex;
877
- flex-direction: row;
878
- float: right;
879
- padding-bottom: 6px;
880
- gap: 8px;
881
- }
882
-
883
- .dataset-results-feedback {
884
- white-space:nowrap;
885
- text-align: right;
886
- color: rgb(48, 49, 51);
887
- font-family: Asap;
888
- font-size: 18px;
889
- font-weight: 500;
890
- padding-top: 8px;
891
- }
892
-
893
- .search-filters {
894
- position: relative;
895
- float: left;
896
- padding-right: 15px;
897
- }
898
-
899
- .number-shown-select :deep(.el-select__wrapper) {
900
- width: 68px;
901
- height: 40px;
902
- color: rgb(48, 49, 51);
903
- }
904
-
905
- .el-select-dropdown__item.is-selected {
906
- color: #8300BF;
907
- }
908
-
909
- .filters :deep(.el-popover) {
910
- background: #f3ecf6 !important;
911
- border: 1px solid $app-primary-color;
912
- border-radius: 4px;
913
- color: #303133 !important;
914
- font-size: 12px;
915
- line-height: 18px;
916
- }
917
-
918
- .filters :deep(.el-popover[x-placement^='top'] .popper__arrow) {
919
- border-top-color: $app-primary-color;
920
- border-bottom-width: 0;
921
- }
922
- .filters :deep(.el-popover[x-placement^='top'] .popper__arrow::after) {
923
- border-top-color: #f3ecf6;
924
- border-bottom-width: 0;
925
- }
926
-
927
- .filters :deep(.el-popover[x-placement^='bottom'] .popper__arrow) {
928
- border-top-width: 0;
929
- border-bottom-color: $app-primary-color;
930
- }
931
- .filters :deep(.el-popover[x-placement^='bottom'] .popper__arrow::after) {
932
- border-top-width: 0;
933
- border-bottom-color: #f3ecf6;
934
- }
935
-
936
- .filters :deep(.el-popover[x-placement^='right'] .popper__arrow) {
937
- border-right-color: $app-primary-color;
938
- border-left-width: 0;
939
- }
940
- .filters :deep(.el-popover[x-placement^='right'] .popper__arrow::after) {
941
- border-right-color: #f3ecf6;
942
- border-left-width: 0;
943
- }
944
-
945
- .filters :deep(.el-popover[x-placement^='left'] .popper__arrow) {
946
- border-right-width: 0;
947
- border-left-color: $app-primary-color;
948
- }
949
- .filters :deep(.el-popover[x-placement^='left'] .popper__arrow::after) {
950
- border-right-width: 0;
951
- border-left-color: #f3ecf6;
952
- }
953
- </style>
954
-
955
- <style lang="scss">
956
- .sidebar-cascader-popper {
957
- font-family: Asap;
958
- font-size: 14px;
959
- font-weight: 500;
960
- font-stretch: normal;
961
- font-style: normal;
962
- line-height: normal;
963
- letter-spacing: normal;
964
- color: #292b66;
965
- text-align: center;
966
- padding-bottom: 6px;
967
- }
968
-
969
- .sidebar-cascader-popper .el-cascader-node.is-active {
970
- color: $app-primary-color;
971
- }
972
-
973
- .sidebar-cascader-popper .el-cascader-node.in-active-path {
974
- color: $app-primary-color;
975
- }
976
-
977
- .sidebar-cascader-popper .el-checkbox__input.is-checked > .el-checkbox__inner {
978
- background-color: $app-primary-color;
979
- border-color: $app-primary-color;
980
- }
981
-
982
- .sidebar-cascader-popper
983
- .el-cascader-menu:nth-child(2)
984
- .el-cascader-node:first-child {
985
- border-bottom: 1px solid #e4e7ed;
986
- }
987
-
988
- .sidebar-cascader-popper .el-cascader-node__label {
989
- text-align: left;
990
- }
991
-
992
- .sidebar-cascader-popper .el-cascder-panel {
993
- max-height: 500px;
994
- }
995
-
996
- .sidebar-cascader-popper .el-scrollbar__wrap {
997
- overflow-x: hidden;
998
- margin-bottom: 2px !important;
999
- }
1000
-
1001
- .sidebar-cascader-popper .el-checkbox__input.is-checked .el-checkbox__inner,
1002
- .el-checkbox__input.is-indeterminate .el-checkbox__inner {
1003
- background-color: $app-primary-color;
1004
- border-color: $app-primary-color;
1005
- }
1006
- </style>
1
+ <template>
2
+ <div class="filters">
3
+ <MapSvgSpriteColor />
4
+ <div class="cascader-tag" v-if="presentTags.length > 0">
5
+ <el-tag
6
+ class="ml-2"
7
+ type="info"
8
+ closable
9
+ @close="cascadeTagClose(presentTags[0])"
10
+ >
11
+ {{ presentTags[0] }}
12
+ </el-tag>
13
+ <el-popover
14
+ v-if="presentTags.length > 1"
15
+ placement="bottom-start"
16
+ :width="200"
17
+ trigger="hover"
18
+ >
19
+ <template #default>
20
+ <div class="el-tags-container">
21
+ <el-tag
22
+ v-for="(tag, i) in presentTags.slice(1)"
23
+ :key="i"
24
+ class="ml-2"
25
+ type="info"
26
+ closable
27
+ @close="cascadeTagClose(tag)"
28
+ >
29
+ {{ tag }}
30
+ </el-tag>
31
+ </div>
32
+ </template>
33
+ <template #reference>
34
+ <div class="el-tags-container">
35
+ <el-tag
36
+ v-if="presentTags.length > 1"
37
+ class="ml-2"
38
+ type="info"
39
+ >
40
+ +{{ presentTags.length - 1 }}
41
+ </el-tag>
42
+ </div>
43
+ </template>
44
+ </el-popover>
45
+ </div>
46
+ <transition name="el-zoom-in-top">
47
+ <span v-show="showFilters" v-loading="!cascaderIsReady" class="search-filters transition-box">
48
+ <el-cascader
49
+ class="cascader"
50
+ ref="cascader"
51
+ v-model="cascadeSelected"
52
+ size="large"
53
+ placeholder=" "
54
+ :collapse-tags="true"
55
+ collapse-tags-tooltip
56
+ :options="options"
57
+ :props="props"
58
+ @change="cascadeEvent($event)"
59
+ @expand-change="cascadeExpandChange"
60
+ :show-all-levels="true"
61
+ popper-class="sidebar-cascader-popper"
62
+ />
63
+ <div v-if="showFiltersText" class="filter-default-value">Filters</div>
64
+ <el-popover
65
+ title="How do filters work?"
66
+ width="250"
67
+ trigger="hover"
68
+ :append-to-body="false"
69
+ popper-class="popover"
70
+ >
71
+ <template #reference>
72
+ <MapSvgIcon icon="help" class="help" />
73
+ </template>
74
+ <div>
75
+ <strong>Within categories:</strong> OR
76
+ <br />
77
+ example: 'heart' OR 'colon'
78
+ <br />
79
+ <br />
80
+ <strong>Between categories:</strong> AND
81
+ <br />
82
+ example: 'rat' AND 'lung'
83
+ </div>
84
+ </el-popover>
85
+ </span>
86
+ </transition>
87
+ <div class="dataset-shown">
88
+ <span class="dataset-results-feedback">{{ numberOfResultsText }}</span>
89
+ <el-select
90
+ class="number-shown-select"
91
+ v-model="numberShown"
92
+ placeholder="10"
93
+ @change="numberShownChanged($event)"
94
+ >
95
+ <el-option
96
+ v-for="item in numberDatasetsShown"
97
+ :key="item"
98
+ :label="item"
99
+ :value="item"
100
+ ></el-option>
101
+ </el-select>
102
+ </div>
103
+ </div>
104
+ </template>
105
+
106
+ <script>
107
+ /* eslint-disable no-alert, no-console */
108
+ import { markRaw } from 'vue'
109
+ import {
110
+ ElOption as Option,
111
+ ElSelect as Select,
112
+ ElPopover as Popover,
113
+ ElCascader as Cascader,
114
+ } from 'element-plus'
115
+ import speciesMap from './species-map.js'
116
+ import { MapSvgIcon, MapSvgSpriteColor } from "@abi-software/svg-sprite";
117
+ import '@abi-software/svg-sprite/dist/style.css'
118
+
119
+ import { AlgoliaClient } from '../algolia/algolia.js'
120
+ import { facetPropPathMapping } from '../algolia/utils.js'
121
+ import EventBus from './EventBus.js'
122
+
123
+ const capitalise = function (txt) {
124
+ return txt.charAt(0).toUpperCase() + txt.slice(1)
125
+ }
126
+
127
+ const convertReadableLabel = function (original) {
128
+ const name = original.toLowerCase()
129
+ if (speciesMap[name]) {
130
+ return capitalise(speciesMap[name])
131
+ } else {
132
+ return capitalise(name)
133
+ }
134
+ }
135
+
136
+ export default {
137
+ name: 'SearchFilters',
138
+ components: {
139
+ MapSvgIcon,
140
+ MapSvgSpriteColor,
141
+ Option,
142
+ Select,
143
+ Popover,
144
+ Cascader
145
+ },
146
+ props: {
147
+ /**
148
+ * Object containing information for
149
+ * the required viewing.
150
+ */
151
+ entry: Object,
152
+ envVars: {
153
+ type: Object,
154
+ default: () => {},
155
+ },
156
+ },
157
+ data: function () {
158
+ return {
159
+ cascaderIsReady: false,
160
+ previousShowAllChecked: {
161
+ species: false,
162
+ gender: false,
163
+ organ: false,
164
+ datasets: false,
165
+ },
166
+ showFilters: true,
167
+ showFiltersText: true,
168
+ cascadeSelected: [],
169
+ cascadeSelectedWithBoolean: [],
170
+ numberShown: 10,
171
+ filters: [],
172
+ facets: ['Species', 'Gender', 'Organ', 'Datasets'],
173
+ numberDatasetsShown: ['10', '20', '50'],
174
+ props: { multiple: true },
175
+ options: [
176
+ {
177
+ value: 'Species',
178
+ label: 'Species',
179
+ children: [{}],
180
+ },
181
+ ],
182
+ presentTags:[],
183
+ }
184
+ },
185
+ setup() {
186
+ const cascaderTags = markRaw({});
187
+ const correctnessCheck = markRaw({
188
+ term: new Set(),
189
+ facet: new Set(),
190
+ facet2: new Set()
191
+ });
192
+ return { cascaderTags, correctnessCheck }
193
+ },
194
+ computed: {
195
+ numberOfResultsText: function () {
196
+ return `${this.entry.numberOfHits} results | Showing`
197
+ },
198
+ },
199
+ methods: {
200
+ createCascaderItemValue: function (
201
+ term,
202
+ facet1 = undefined,
203
+ facet2 = undefined
204
+ ) {
205
+ let value = term
206
+ if (facet1) value = `${term}>${facet1}`
207
+ if (facet1 && facet2) value = `${term}>${facet1}>${facet2}`
208
+ if (!facet1 && facet2)
209
+ console.warn(
210
+ `Warning: ${facet2} provided without its parent, this will not be shown in the cascader`
211
+ )
212
+ return value
213
+ },
214
+ populateCascader: function () {
215
+ return new Promise((resolve) => {
216
+ // Algolia facet serach
217
+ this.algoliaClient
218
+ .getAlgoliaFacets(facetPropPathMapping)
219
+ .then((data) => {
220
+ this.facets = data
221
+ EventBus.emit('available-facets', data)
222
+ this.options = data
223
+
224
+ // create top level of options in cascader
225
+ this.options.forEach((facet, i) => {
226
+ this.options[i].total = this.countTotalFacet(facet)
227
+
228
+ this.options[i].label = convertReadableLabel(facet.label)
229
+ this.options[i].value = this.createCascaderItemValue(
230
+ facet.key,
231
+ undefined
232
+ )
233
+
234
+ // put "Show all" as first option
235
+ this.options[i].children.unshift({
236
+ value: this.createCascaderItemValue('Show all'),
237
+ label: 'Show all',
238
+ })
239
+
240
+ // populate second level of options
241
+ this.options[i].children.forEach((facetItem, j) => {
242
+ this.options[i].children[j].label = convertReadableLabel(
243
+ facetItem.label
244
+ )
245
+ this.options[i].children[j].value =
246
+ this.createCascaderItemValue(facet.label, facetItem.label)
247
+ if (
248
+ this.options[i].children[j].children &&
249
+ this.options[i].children[j].children.length > 0
250
+ ) {
251
+ this.options[i].children[j].children.forEach((term, k) => {
252
+ this.options[i].children[j].children[k].label =
253
+ convertReadableLabel(term.label)
254
+ this.options[i].children[j].children[k].value =
255
+ this.createCascaderItemValue(
256
+ facet.label,
257
+ facetItem.label,
258
+ term.label
259
+ )
260
+ })
261
+ }
262
+ })
263
+ })
264
+
265
+ this.populatePMRinCascader();
266
+ })
267
+ .finally(() => {
268
+ resolve()
269
+ })
270
+ })
271
+ },
272
+ /**
273
+ * Add PMR checkbox in filters (cascader)
274
+ */
275
+ populatePMRinCascader: function () {
276
+ for (let i = 0; i < this.options.length; i += 1) {
277
+ const option = this.options[i];
278
+ // match with "Data type"'s' key
279
+ if (option.key === 'item.types.name') {
280
+ option.children.push({
281
+ label: 'PMR',
282
+ value: this.createCascaderItemValue("Data type", "PMR"),
283
+ });
284
+ }
285
+ }
286
+ },
287
+ /**
288
+ * Create manual events when cascader tag is closed
289
+ */
290
+ cascadeTagClose: function (tag) {
291
+ let manualEvent = []
292
+
293
+ Object.entries(this.cascaderTags).map((entry) => {
294
+ const term = entry[0], facet = entry[1] // Either "Array" or "Object", depends on the cascader item level
295
+ const option = this.options.filter((option) => option.label == term)[0]
296
+ const key = option.key
297
+
298
+ for (let index = 0; index < option.children.length; index++) {
299
+ const child = option.children[index]
300
+ const label = child.label, value = child.value
301
+
302
+ if (Array.isArray(facet)) {
303
+ // push "Show all" if there is no item checked
304
+ if (facet.length === 0 && label.toLowerCase() === "show all") {
305
+ manualEvent.push([key, value])
306
+ break
307
+ // push all checked items
308
+ } else if (label !== tag && facet.includes(label))
309
+ manualEvent.push([key, value])
310
+ } else {
311
+ // loop nested cascader items
312
+ Object.entries(facet).map((entry2) => {
313
+ const term2 = entry2[0], facet2 = entry2[1] // object key, object value
314
+
315
+ if (term2 === label) {
316
+ child.children.map((child2) => {
317
+ const label2 = child2.label, value2 = child2.value
318
+ // push all checked items
319
+ if (label2 !== tag && facet2.includes(label2))
320
+ manualEvent.push([key, value2])
321
+ })
322
+ }
323
+ })
324
+ }
325
+ }
326
+ })
327
+ this.cascadeEvent(manualEvent)
328
+ },
329
+ /**
330
+ * Re-generate 'cascaderTags' and 'presentTags'
331
+ * Not able to avoid wrong facet at the moment
332
+ */
333
+ tagsChangedCallback: function (event) {
334
+ if (this.correctnessCheck.term && this.correctnessCheck.facet && this.correctnessCheck.facet2) {
335
+ this.options.map((option) => {
336
+ this.correctnessCheck.term.add(option.label)
337
+ option.children.map((child) => {
338
+ this.correctnessCheck.facet.add(child.label)
339
+ if (option.label === 'Anatomical structure' && child.label !== 'Show all') {
340
+ child.children.map((child2) => {
341
+ this.correctnessCheck.facet2.add(child2.label)
342
+ })
343
+ }
344
+ })
345
+ })
346
+ }
347
+
348
+ this.cascaderTags = {}
349
+ this.presentTags = []
350
+ event.map((item) => {
351
+ const { facet, facet2, term } = item
352
+ if (this.correctnessCheck.term.has(term) && this.correctnessCheck.facet.has(facet)) {
353
+ if (facet2) {
354
+ if (this.correctnessCheck.facet2.has(facet2)) {
355
+ if (term in this.cascaderTags) {
356
+ if (facet in this.cascaderTags[term]) this.cascaderTags[term][facet].push(facet2)
357
+ else this.cascaderTags[term][facet] = [facet2]
358
+ } else {
359
+ this.cascaderTags[term] = {}
360
+ this.cascaderTags[term][facet] = [facet2]
361
+ }
362
+ }
363
+ } else {
364
+ // If 'cascaderTags' has key 'Anatomical structure',
365
+ // it's value type will be Object (because it has nested facets),
366
+ // in this case 'push' action will not available.
367
+ if (term in this.cascaderTags && term !== 'Anatomical structure')
368
+ this.cascaderTags[term].push(facet)
369
+ else {
370
+ if (facet.toLowerCase() !== "show all") this.cascaderTags[term] = [facet]
371
+ else this.cascaderTags[term] = []
372
+ }
373
+ }
374
+ }
375
+ })
376
+
377
+ Object.values(this.cascaderTags).map((value) => {
378
+ const extend = Array.isArray(value) ? value : Object.values(value).flat(1)
379
+ this.presentTags = [...this.presentTags, ...extend]
380
+ })
381
+ this.presentTags = [...new Set(this.presentTags)]
382
+ if (this.presentTags.length > 0) this.showFiltersText = false
383
+ else this.showFiltersText = true
384
+ },
385
+ /**
386
+ * Support for function 'showAllEventModifierForAutoCheckAll'
387
+ * Called in function 'populateCascader'
388
+ */
389
+ countTotalFacet: function (facet) {
390
+ if (['anatomy.organ.category.name'].includes(facet.key)) {
391
+ const count = facet.children.reduce((total, num) => {
392
+ // The first 'total' will be an object
393
+ total = typeof total == 'number' ? total : total.children.length
394
+ return total + num.children.length
395
+ })
396
+ return count
397
+ }
398
+ return facet.children.length
399
+ },
400
+ /**
401
+ * When check/uncheck all child items, automatically check "Show all"
402
+ */
403
+ showAllEventModifierForAutoCheckAll: function (event) {
404
+ const currentKeys = {}
405
+ event.map((e) => {
406
+ const eventKey = e[0]
407
+ if (eventKey in currentKeys) currentKeys[eventKey] += 1
408
+ else currentKeys[eventKey] = 1
409
+ })
410
+ this.options.map((option) => {
411
+ const key = option.key
412
+ const value = option.children.filter((child) => child.label === "Show all")[0].value
413
+ const total = option.total
414
+ // Remove events if all child items is checked
415
+ if (currentKeys[key] === total) {
416
+ event = event.filter((e) => e[0] !== option.key)
417
+ delete currentKeys[key]
418
+ }
419
+ // Add 'Show all' if facet type not exist in event
420
+ if (!(key in currentKeys)) event.unshift([key, value])
421
+ })
422
+ return event
423
+ },
424
+ // cascadeEvent: initiate searches based off cascader changes
425
+ cascadeEvent: function (eventIn) {
426
+ let event = [...eventIn]
427
+ if (event) {
428
+ // Check for show all in selected cascade options
429
+
430
+ event = this.showAllEventModifier(event)
431
+
432
+ event = this.showAllEventModifierForAutoCheckAll(event)
433
+ /**
434
+ * Move the new added event to the beginning
435
+ * Otherwise, cascader will show different expand item
436
+ */
437
+ if (this.__expandItem__) {
438
+ let position = 0
439
+ if (this.__expandItem__.length > 1) {
440
+ position = 1
441
+ }
442
+ const current = event.filter((e) => e[position] == this.__expandItem__[position]);
443
+ const rest = event.filter((e) => e[position] !== this.__expandItem__[position]);
444
+ event = [...current, ...rest]
445
+ }
446
+ // Create results for the filter update
447
+ let filterKeys = event
448
+ .filter((selection) => selection !== undefined)
449
+ .map((fs) => {
450
+ let { hString, bString } =
451
+ this.findHierarachyStringAndBooleanString(fs)
452
+ let { facet, facet2, term } =
453
+ this.getFacetsFromHierarchyString(hString)
454
+ return {
455
+ facetPropPath: fs[0],
456
+ facet: facet,
457
+ facet2: facet2,
458
+ term: term,
459
+ AND: bString, // for setting the boolean
460
+ }
461
+ })
462
+
463
+ // Move results from arrays to object for use on scicrunch (note that we remove 'duplicate' as that is only needed for filter keys)
464
+ let filters = event
465
+ .filter((selection) => selection !== undefined)
466
+ .map((fs) => {
467
+ let facetSubPropPath = undefined
468
+ let propPath = fs[0].includes('duplicate')
469
+ ? fs[0].split('duplicate')[0]
470
+ : fs[0]
471
+ let { hString, bString } =
472
+ this.findHierarachyStringAndBooleanString(fs)
473
+ let { facet, facet2, term } =
474
+ this.getFacetsFromHierarchyString(hString)
475
+ if (facet2) {
476
+ // We need to change the propPath if we are at the third level of the cascader
477
+ facet = facet2
478
+ facetSubPropPath = 'anatomy.organ.name'
479
+ }
480
+ return {
481
+ facetPropPath: propPath,
482
+ facet: facet,
483
+ term: term,
484
+ AND: bString, // for setting the boolean
485
+ facetSubPropPath: facetSubPropPath, // will be used for filters if we are at the third level of the cascader
486
+ }
487
+ })
488
+
489
+ this.$emit('loading', true) // let sidebarcontent wait for the requests
490
+ this.$emit('filterResults', filters) // emit filters for apps above sidebar
491
+ this.setCascader(filterKeys) //update our cascader v-model if we modified the event
492
+ this.cssMods() // update css for the cascader
493
+ }
494
+ },
495
+ //this fucntion is needed as we previously stored booleans in the array of event that
496
+ // are stored in the cascader
497
+ findHierarachyStringAndBooleanString(cascadeEventItem) {
498
+ let hString, bString
499
+ if (cascadeEventItem.length >= 3) {
500
+ if (cascadeEventItem[2] &&
501
+ (typeof cascadeEventItem[2] === 'string' ||
502
+ cascadeEventItem[2] instanceof String) &&
503
+ cascadeEventItem[2].split('>').length > 2) {
504
+ hString = cascadeEventItem[2]
505
+ bString = cascadeEventItem.length == 4 ? cascadeEventItem[3] : undefined
506
+ } else {
507
+ hString = cascadeEventItem[1]
508
+ bString = cascadeEventItem[2]
509
+ }
510
+ } else {
511
+ hString = cascadeEventItem[1]
512
+ bString = undefined
513
+ }
514
+ return { hString, bString }
515
+ },
516
+ // Splits the terms and facets from the string stored in the cascader
517
+ getFacetsFromHierarchyString(hierarchyString) {
518
+ let facet,
519
+ term,
520
+ facet2 = undefined
521
+ let fsSplit = hierarchyString.split('>')
522
+ if (fsSplit.length == 3) {
523
+ // if we are at the third level of the cascader
524
+ facet2 = fsSplit[2]
525
+ facet = fsSplit[1]
526
+ term = fsSplit[0]
527
+ } else {
528
+ facet = fsSplit[1]
529
+ term = fsSplit[0]
530
+ }
531
+ return { facet, facet2, term }
532
+ },
533
+ // showAllEventModifier: Modifies a cascade event to unclick all selections in category if "show all" is clicked. Also unchecks "Show all" if any secection is clicked
534
+ // *NOTE* Does NOT remove 'Show all' selections from showing in 'cascadeSelected'
535
+ showAllEventModifier: function (event) {
536
+ // check if show all is in the cascader checked option list
537
+ let hasShowAll = event
538
+ .map((ev) => (ev ? ev[1].toLowerCase().includes('show all') : false))
539
+ .includes(true)
540
+ // remove all selected options below the show all if checked
541
+ if (hasShowAll) {
542
+ let modifiedEvent = []
543
+ let facetMaps = {}
544
+ //catagorised different facet items
545
+ for (const i in event) {
546
+ if (facetMaps[event[i][0]] === undefined) facetMaps[event[i][0]] = []
547
+ facetMaps[event[i][0]].push(event[i])
548
+ }
549
+ // go through each facets
550
+ for (const facet in facetMaps) {
551
+ let showAll = undefined
552
+ // Find the show all item if any
553
+ for (let i = facetMaps[facet].length - 1; i >= 0; i--) {
554
+ if (facetMaps[facet][i][1].toLowerCase().includes('show all')) {
555
+ //seperate the showAll item and the rest
556
+ showAll = facetMaps[facet].splice(i, 1)[0]
557
+ break
558
+ }
559
+ }
560
+ if (showAll) {
561
+ if (this.previousShowAllChecked[facet]) {
562
+ //Unset the show all if it was present previously
563
+ //and there are other items
564
+ if (facetMaps[facet].length > 0)
565
+ modifiedEvent.push(...facetMaps[facet])
566
+ else modifiedEvent.push(showAll)
567
+ } else {
568
+ //showAll is turned on
569
+ modifiedEvent.push(showAll)
570
+ }
571
+ } else {
572
+ modifiedEvent.push(...facetMaps[facet])
573
+ }
574
+ }
575
+ //Make sure the expanded item are sorted first.
576
+ return modifiedEvent.sort((a, b) => {
577
+ if (this.__expandItem__) {
578
+ if (a[0] == this.__expandItem__) {
579
+ if (b[0] == this.__expandItem__) {
580
+ return 0
581
+ } else {
582
+ return -1
583
+ }
584
+ } else if (b[0] == this.__expandItem__) {
585
+ if (a[0] == this.__expandItem__) {
586
+ return 0
587
+ } else {
588
+ return 1
589
+ }
590
+ } else {
591
+ return 0
592
+ }
593
+ } else return 0
594
+ })
595
+ }
596
+ return event
597
+ },
598
+ cascadeExpandChange: function (event) {
599
+ //work around as the expand item may change on modifying the cascade props
600
+ this.__expandItem__ = event
601
+ this.cssMods()
602
+ },
603
+ numberShownChanged: function (event) {
604
+ this.$emit('numberPerPage', parseInt(event))
605
+ },
606
+ updatePreviousShowAllChecked: function (options) {
607
+ //Reset the states
608
+ for (const facet in this.previousShowAllChecked) {
609
+ this.previousShowAllChecked[facet] = false
610
+ }
611
+ options.forEach((element) => {
612
+ if (element[1].toLowerCase().includes('show all'))
613
+ this.previousShowAllChecked[element[0]] = true
614
+ })
615
+ },
616
+ // setCascader: Clears previous selections and takes in an array of facets to select: filterFacets
617
+ // facets are in the form:
618
+ // {
619
+ // facetPropPath: 'anatomy.organ.name',
620
+ // term: 'Sex',
621
+ // facet: 'Male'
622
+ // AND: true // Optional value for setting the boolean within a facet
623
+ // }
624
+ setCascader: function (filterFacets) {
625
+ //Do not set the value unless it is ready
626
+ if (this.cascaderIsReady && filterFacets && filterFacets.length != 0) {
627
+ //An inner function only used by this function
628
+ const createFilter = (e) => {
629
+ let filters = [
630
+ e.facetPropPath,
631
+ this.createCascaderItemValue(capitalise(e.term), e.facet),
632
+ ]
633
+ // Add the third level of the cascader if it exists
634
+ if (e.facet2) {
635
+ filters.push(
636
+ this.createCascaderItemValue(
637
+ capitalise(e.term),
638
+ e.facet,
639
+ e.facet2
640
+ )
641
+ )
642
+ }
643
+ return filters;
644
+ }
645
+
646
+ this.cascadeSelected = filterFacets.map((e) => {
647
+ let filters = createFilter(e)
648
+ return filters
649
+ })
650
+
651
+ // Unforttunately the cascader is very particular about it's v-model
652
+ // to get around this we create a clone of it and use this clone for adding our boolean information
653
+ this.cascadeSelectedWithBoolean = filterFacets.map((e) => {
654
+ let filters = createFilter(e)
655
+ filters.push(e.AND)
656
+ return filters
657
+ })
658
+ this.updatePreviousShowAllChecked(this.cascadeSelected)
659
+ }
660
+ this.tagsChangedCallback(filterFacets);
661
+ },
662
+ addFilter: function (filterToAdd) {
663
+ //Do not set the value unless it is ready
664
+ if (this.cascaderIsReady && filterToAdd) {
665
+ let filter = this.validateAndConvertFilterToHierarchical(filterToAdd)
666
+ if (filter) {
667
+ this.cascadeSelected.filter((f) => f.term != filter.term)
668
+ this.cascadeSelected.push([
669
+ filter.facetPropPath,
670
+ this.createCascaderItemValue(filter.term, filter.facet),
671
+ this.createCascaderItemValue(
672
+ filter.term,
673
+ filter.facet,
674
+ filter.facet2
675
+ ),
676
+ ])
677
+ this.cascadeSelectedWithBoolean.push([
678
+ filter.facetPropPath,
679
+ this.createCascaderItemValue(filter.term, filter.facet),
680
+ this.createCascaderItemValue(
681
+ filter.term,
682
+ filter.facet,
683
+ filter.facet2
684
+ ),
685
+ filter.AND,
686
+ ])
687
+ // The 'AND' her is to set the boolean value when we search on the filters. It can be undefined without breaking anything
688
+ return true
689
+ }
690
+ }
691
+ },
692
+ initiateSearch: function () {
693
+ this.cascadeEvent(this.cascadeSelectedWithBoolean)
694
+ },
695
+ // checkShowAllBoxes: Checks each 'Show all' cascade option by using the setCascader function
696
+ checkShowAllBoxes: function () {
697
+ this.setCascader(
698
+ this.options.map((option) => {
699
+ return {
700
+ facetPropPath: option.value,
701
+ term: option.label,
702
+ facet: 'Show all',
703
+ }
704
+ })
705
+ )
706
+ },
707
+ makeCascadeLabelsClickable: function () {
708
+ // Next tick allows the cascader menu to change
709
+ this.$nextTick(() => {
710
+ document
711
+ .querySelectorAll('.sidebar-cascader-popper .el-cascader-node__label')
712
+ .forEach((el) => {
713
+ // step through each cascade label
714
+ el.onclick = function () {
715
+ const checkbox = this.previousElementSibling
716
+ if (checkbox) {
717
+ if (!checkbox.parentElement.attributes['aria-owns']) {
718
+ // check if we are at the lowest level of cascader
719
+ this.previousElementSibling.click() // Click the checkbox
720
+ }
721
+ }
722
+ }
723
+ })
724
+ })
725
+ },
726
+
727
+ cssMods: function () {
728
+ this.makeCascadeLabelsClickable()
729
+ this.removeTopLevelCascaderCheckboxes()
730
+ },
731
+
732
+ removeTopLevelCascaderCheckboxes: function () {
733
+ // Next tick allows the cascader menu to change
734
+ this.$nextTick(() => {
735
+ let cascadePanels = document.querySelectorAll(
736
+ '.sidebar-cascader-popper .el-cascader-menu__list'
737
+ )
738
+ // Hide the checkboxes on the first level of the cascader
739
+ cascadePanels[0]
740
+ .querySelectorAll('.el-checkbox__input')
741
+ .forEach((el) => (el.style.display = 'none'))
742
+ })
743
+ },
744
+ /*
745
+ * Given a filter, the function below returns the filter in the format of the cascader, returns false if facet is not found
746
+ */
747
+ validateAndConvertFilterToHierarchical: function (filter) {
748
+ if (filter && filter.facet && filter.term) {
749
+ if (filter.facet2) {
750
+ return filter // if it has a second term we will assume it is hierarchical and return it as is
751
+ } else {
752
+ for (const firstLayer of this.options) {
753
+ if (firstLayer.value === filter.facetPropPath) {
754
+ for (const secondLayer of firstLayer.children) {
755
+ if (secondLayer.label === filter.facet) {
756
+ // if we find a match on the second level, the filter will already be correct
757
+ return filter
758
+ } else {
759
+ if (secondLayer.children && secondLayer.children.length > 0) {
760
+ for (const thirdLayer of secondLayer.children) {
761
+ if (thirdLayer.label === filter.facet) {
762
+ // If we find a match on the third level, we need to switch facet1 to facet2
763
+ // and populate facet1 with its parents label.
764
+ filter.facet2 = thirdLayer.label
765
+ filter.facet = secondLayer.label
766
+ return filter
767
+ }
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+ }
774
+ }
775
+ }
776
+ return false
777
+ },
778
+ getHierarchicalValidatedFilters: function (filters) {
779
+ if (filters) {
780
+ if (this.cascaderIsReady) {
781
+ const result = []
782
+ filters.forEach((filter) => {
783
+ const validatedFilter =
784
+ this.validateAndConvertFilterToHierarchical(filter)
785
+ if (validatedFilter) {
786
+ result.push(validatedFilter)
787
+ }
788
+ })
789
+ return result
790
+ } else return filters
791
+ }
792
+ return []
793
+ },
794
+ },
795
+ mounted: function () {
796
+ this.algoliaClient = new AlgoliaClient(
797
+ this.envVars.ALGOLIA_ID,
798
+ this.envVars.ALGOLIA_KEY,
799
+ this.envVars.PENNSIEVE_API_LOCATION
800
+ )
801
+ this.algoliaClient.initIndex(this.envVars.ALGOLIA_INDEX)
802
+ this.populateCascader().then(() => {
803
+ this.cascaderIsReady = true
804
+ this.checkShowAllBoxes()
805
+ this.setCascader(this.entry.filterFacets)
806
+ this.cssMods()
807
+ this.$emit('cascaderReady')
808
+ })
809
+ },
810
+ }
811
+ </script>
812
+
813
+ <style lang="scss" scoped>
814
+
815
+ .filters {
816
+ position: relative;
817
+ }
818
+
819
+ .cascader-tag {
820
+ position: absolute;
821
+ top: 8px;
822
+ left: 8px;
823
+ z-index: 1;
824
+ display: flex;
825
+ gap: 4px;
826
+ }
827
+
828
+ .el-tags-container {
829
+ display: flex;
830
+ flex-wrap: wrap;
831
+ gap: 4px;
832
+ }
833
+
834
+ .el-tag {
835
+ .cascader-tag &,
836
+ .el-tags-container & {
837
+ font-family: 'Asap', sans-serif;
838
+ font-size: 12px;
839
+ color: #303133 !important;
840
+ background-color: #fff;
841
+ border-color: #dcdfe6 !important;
842
+ }
843
+ }
844
+
845
+ :deep(.el-cascader__tags) {
846
+ display: none;
847
+ }
848
+
849
+ .filter-default-value {
850
+ pointer-events: none;
851
+ position: absolute;
852
+ top: 0;
853
+ left: 0;
854
+ padding-top: 10px;
855
+ padding-left: 16px;
856
+ }
857
+
858
+ .help {
859
+ width: 24px !important;
860
+ height: 24px;
861
+ transform: scale(1.1);
862
+ cursor: pointer;
863
+ }
864
+
865
+ .popover {
866
+ color: rgb(48, 49, 51);
867
+ font-family: Asap;
868
+ margin: 12px;
869
+ }
870
+
871
+ .filter-icon-inside {
872
+ width: 12px !important;
873
+ height: 12px !important;
874
+ color: #292b66;
875
+ transform: scale(2) !important;
876
+ margin-bottom: 0px !important;
877
+ }
878
+
879
+ .cascader {
880
+ font-family: Asap;
881
+ font-size: 14px;
882
+ font-weight: 500;
883
+ font-stretch: normal;
884
+ font-style: normal;
885
+ line-height: normal;
886
+ letter-spacing: normal;
887
+ color: #292b66;
888
+ text-align: center;
889
+ padding-bottom: 6px;
890
+ }
891
+
892
+ .dataset-shown {
893
+ display: flex;
894
+ flex-direction: row;
895
+ float: right;
896
+ padding-bottom: 6px;
897
+ gap: 8px;
898
+ }
899
+
900
+ .dataset-results-feedback {
901
+ white-space:nowrap;
902
+ text-align: right;
903
+ color: rgb(48, 49, 51);
904
+ font-family: Asap;
905
+ font-size: 18px;
906
+ font-weight: 500;
907
+ padding-top: 8px;
908
+ }
909
+
910
+ .search-filters {
911
+ position: relative;
912
+ float: left;
913
+ padding-right: 15px;
914
+ }
915
+
916
+ .number-shown-select :deep(.el-select__wrapper) {
917
+ width: 68px;
918
+ height: 40px;
919
+ color: rgb(48, 49, 51);
920
+ }
921
+
922
+ .el-select-dropdown__item.is-selected {
923
+ color: #8300BF;
924
+ }
925
+
926
+ .filters :deep(.el-popover) {
927
+ background: #f3ecf6 !important;
928
+ border: 1px solid $app-primary-color;
929
+ border-radius: 4px;
930
+ color: #303133 !important;
931
+ font-size: 12px;
932
+ line-height: 18px;
933
+ }
934
+
935
+ .filters :deep(.el-popover[x-placement^='top'] .popper__arrow) {
936
+ border-top-color: $app-primary-color;
937
+ border-bottom-width: 0;
938
+ }
939
+ .filters :deep(.el-popover[x-placement^='top'] .popper__arrow::after) {
940
+ border-top-color: #f3ecf6;
941
+ border-bottom-width: 0;
942
+ }
943
+
944
+ .filters :deep(.el-popover[x-placement^='bottom'] .popper__arrow) {
945
+ border-top-width: 0;
946
+ border-bottom-color: $app-primary-color;
947
+ }
948
+ .filters :deep(.el-popover[x-placement^='bottom'] .popper__arrow::after) {
949
+ border-top-width: 0;
950
+ border-bottom-color: #f3ecf6;
951
+ }
952
+
953
+ .filters :deep(.el-popover[x-placement^='right'] .popper__arrow) {
954
+ border-right-color: $app-primary-color;
955
+ border-left-width: 0;
956
+ }
957
+ .filters :deep(.el-popover[x-placement^='right'] .popper__arrow::after) {
958
+ border-right-color: #f3ecf6;
959
+ border-left-width: 0;
960
+ }
961
+
962
+ .filters :deep(.el-popover[x-placement^='left'] .popper__arrow) {
963
+ border-right-width: 0;
964
+ border-left-color: $app-primary-color;
965
+ }
966
+ .filters :deep(.el-popover[x-placement^='left'] .popper__arrow::after) {
967
+ border-right-width: 0;
968
+ border-left-color: #f3ecf6;
969
+ }
970
+ </style>
971
+
972
+ <style lang="scss">
973
+ .sidebar-cascader-popper {
974
+ font-family: Asap;
975
+ font-size: 14px;
976
+ font-weight: 500;
977
+ font-stretch: normal;
978
+ font-style: normal;
979
+ line-height: normal;
980
+ letter-spacing: normal;
981
+ color: #292b66;
982
+ text-align: center;
983
+ padding-bottom: 6px;
984
+ }
985
+
986
+ .sidebar-cascader-popper .el-cascader-node.is-active {
987
+ color: $app-primary-color;
988
+ }
989
+
990
+ .sidebar-cascader-popper .el-cascader-node.in-active-path {
991
+ color: $app-primary-color;
992
+ }
993
+
994
+ .sidebar-cascader-popper .el-checkbox__input.is-checked > .el-checkbox__inner {
995
+ background-color: $app-primary-color;
996
+ border-color: $app-primary-color;
997
+ }
998
+
999
+ .sidebar-cascader-popper
1000
+ .el-cascader-menu:nth-child(2)
1001
+ .el-cascader-node:first-child {
1002
+ border-bottom: 1px solid #e4e7ed;
1003
+ }
1004
+
1005
+ .sidebar-cascader-popper .el-cascader-node__label {
1006
+ text-align: left;
1007
+ }
1008
+
1009
+ .sidebar-cascader-popper .el-cascder-panel {
1010
+ max-height: 500px;
1011
+ }
1012
+
1013
+ .sidebar-cascader-popper .el-scrollbar__wrap {
1014
+ overflow-x: hidden;
1015
+ margin-bottom: 2px !important;
1016
+ }
1017
+
1018
+ .sidebar-cascader-popper .el-checkbox__input.is-checked .el-checkbox__inner,
1019
+ .el-checkbox__input.is-indeterminate .el-checkbox__inner {
1020
+ background-color: $app-primary-color;
1021
+ border-color: $app-primary-color;
1022
+ }
1023
+ </style>