@abi-software/map-side-bar 2.2.0 → 2.2.1-alpha-2

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