@abi-software/map-side-bar 2.4.1 → 2.4.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abi-software/map-side-bar",
3
- "version": "2.4.1",
3
+ "version": "2.4.2-beta.0",
4
4
  "files": [
5
5
  "dist/*",
6
6
  "src/*",
package/src/App.vue CHANGED
@@ -14,6 +14,7 @@
14
14
  <el-button @click="neuronSearch">open neuron search</el-button>
15
15
  <el-button @click="keywordSearch">keyword search</el-button>
16
16
  <el-button @click="getFacets">Get facets</el-button>
17
+ <el-button @click="showImages">Show Images</el-button>
17
18
  </div>
18
19
  <SideBar
19
20
  :envVars="envVars"
@@ -23,10 +24,13 @@
23
24
  :tabs="tabs"
24
25
  :activeTabId="activeId"
25
26
  :connectivityInfo="connectivityInput"
27
+ :imageThumbnails="imageThumbnails"
26
28
  @tabClicked="tabClicked"
27
29
  @search-changed="searchChanged($event)"
28
30
  @hover-changed="hoverChanged($event)"
29
31
  @actionClick="action"
32
+ @connectivity-info-close="onConnectivityInfoClose"
33
+ @image-thumbnail-close="onImageThumbnailClose"
30
34
  />
31
35
  </div>
32
36
  </template>
@@ -37,6 +41,7 @@
37
41
  import SideBar from './components/SideBar.vue'
38
42
  import EventBus from './components/EventBus.js'
39
43
  import exampleConnectivityInput from './exampleConnectivityInput.js'
44
+ import imageThumbnails from './data/images.json';
40
45
 
41
46
  const capitalise = (str) => str.charAt(0).toUpperCase() + str.slice(1);
42
47
 
@@ -101,7 +106,11 @@ export default {
101
106
  data: function () {
102
107
  return {
103
108
  contextArray: [null, null],
104
- tabArray: [{ title: 'Flatmap', id: 1, type: 'search'}, { title: 'Connectivity', id: 2, type: 'connectivity' }],
109
+ tabArray: [
110
+ { title: 'Flatmap', id: 1, type: 'search'},
111
+ { title: 'Connectivity', id: 2, type: 'connectivity' },
112
+ { title: 'Images', id: 3, type: 'images' },
113
+ ],
105
114
  sideBarVisibility: true,
106
115
  envVars: {
107
116
  API_LOCATION: import.meta.env.VITE_APP_API_LOCATION,
@@ -114,6 +123,7 @@ export default {
114
123
  ROOT_URL: import.meta.env.VITE_APP_ROOT_URL,
115
124
  },
116
125
  connectivityInput: exampleConnectivityInput,
126
+ imageThumbnails: imageThumbnails,
117
127
  activeId: 1,
118
128
  }
119
129
  },
@@ -224,6 +234,18 @@ export default {
224
234
  let facets = await this.$refs.sideBar.getAlgoliaFacets()
225
235
  console.log('Algolia facets:', facets)
226
236
  },
237
+ showImages: function () {
238
+ if (this.$refs.sideBar) {
239
+ this.tabClicked({id: 3, type: 'images'});
240
+ this.$refs.sideBar.setDrawerOpen(true);
241
+ }
242
+ },
243
+ onConnectivityInfoClose: function () {
244
+ console.log('connectivity-info-close');
245
+ },
246
+ onImageThumbnailClose: function () {
247
+ console.log('image-thumbnail-close');
248
+ },
227
249
  },
228
250
  mounted: function () {
229
251
  console.log('mounted app')
@@ -0,0 +1,497 @@
1
+ <template>
2
+ <div class="image-thumbnails-container">
3
+ <div class="toolbar">
4
+ <div class="filters">
5
+ <el-tag
6
+ v-for="(species, index) in speciesFilterTags"
7
+ :key="index"
8
+ type="info"
9
+ class="tag"
10
+ :class="{ 'active-tag': species.name === activeSpecies.name }"
11
+ :closable="species.name === activeSpecies.name"
12
+ @close="removeSpeciesFilterTag"
13
+ @click="filterBySpecies(species)"
14
+ >
15
+ {{ species.name }} ({{ species.count }})
16
+ </el-tag>
17
+ </div>
18
+ <div class="view-selector">
19
+ <el-select
20
+ v-model="viewOption"
21
+ placeholder="Select view"
22
+ popper-class="view-selector-select-popper"
23
+ >
24
+ <el-option
25
+ v-for="item in viewOptions"
26
+ :key="item.value"
27
+ :label="item.label"
28
+ :value="item.value"
29
+ />
30
+ </el-select>
31
+ </div>
32
+ </div>
33
+ <div class="gallery-container" v-if="viewOption === 'gallery'">
34
+ <Gallery :items="imageItems" :imageStyle="imageStyle" />
35
+ </div>
36
+ <div class="dataset-card-container" v-if="viewOption === 'list'">
37
+ <div class="dataset-card" v-for="imageThumbnail in imageItems">
38
+ <div class="card" :key="imageThumbnail.link">
39
+ <div class="card-left">
40
+ <a :href="imageThumbnail.link" class="card-image card-button-link" target="_blank">
41
+ <el-image :src="imageThumbnail.imgSrc" loading="lazy">
42
+ <template #error>
43
+ <div class="image-slot">Loading...</div>
44
+ </template>
45
+ </el-image>
46
+ </a>
47
+ </div>
48
+ <div class="card-right">
49
+ <div class="details">
50
+ <div class="title">
51
+ <strong>{{ imageThumbnail.type }}</strong>
52
+ </div>
53
+ </div>
54
+ <div class="details">
55
+ <div>
56
+ {{ formattedTitle(imageThumbnail) }}
57
+ </div>
58
+ </div>
59
+ <div class="details">
60
+ <a class="button el-button el-button--large card-button-link" :href="imageThumbnail.link" target="_blank">
61
+ View {{ imageThumbnail.type }}
62
+ </a>
63
+ </div>
64
+ <!-- Copy to clipboard button container -->
65
+ <div class="float-button-container">
66
+ <CopyToClipboard :content="getCopyContent(imageThumbnail)" />
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <script>
76
+ import {
77
+ ElImage,
78
+ ElTag as Tag,
79
+ ElOption as Option,
80
+ ElSelect as Select,
81
+ } from 'element-plus';
82
+ import { CopyToClipboard } from '@abi-software/map-utilities';
83
+ import '@abi-software/map-utilities/dist/style.css';
84
+ import Gallery from "@abi-software/gallery";
85
+ import "@abi-software/gallery/dist/style.css";
86
+
87
+ const BASE64PREFIX = 'data:image/png;base64,';
88
+ const VIEW_OPTIONS = [
89
+ {
90
+ value: 'list',
91
+ label: 'List'
92
+ },
93
+ {
94
+ value: 'gallery',
95
+ label: 'Gallery'
96
+ }
97
+ ];
98
+
99
+ export default {
100
+ name: 'ImageThumbnails',
101
+ components: {
102
+ Tag,
103
+ Option,
104
+ Select,
105
+ ElImage,
106
+ Gallery,
107
+ CopyToClipboard,
108
+ },
109
+ props: {
110
+ /**
111
+ * The image thumbnails data to show in sidebar.
112
+ */
113
+ imageThumbnails: {
114
+ type: Array,
115
+ default: [],
116
+ },
117
+ },
118
+ data: function () {
119
+ return {
120
+ activeSpecies: { name: "" },
121
+ speciesFilterTags: [],
122
+ imageItems: [],
123
+ showImageGallery: false,
124
+ viewOption: 'list',
125
+ viewOptions: VIEW_OPTIONS,
126
+ };
127
+ },
128
+ computed: {
129
+ imageStyle() {
130
+ return {
131
+ width: "180px",
132
+ height: "135px",
133
+ };
134
+ },
135
+ },
136
+ mounted: function () {
137
+ this.injectImgSrc();
138
+ this.populateFilterTags();
139
+ this.imageItems = this.imageThumbnails;
140
+ },
141
+ watch: {
142
+ imageThumbnails: {
143
+ handler: function (value) {
144
+ if (value) {
145
+ this.injectImgSrc();
146
+ this.populateFilterTags();
147
+ this.imageItems = this.imageThumbnails;
148
+ }
149
+ },
150
+ deep: true,
151
+ },
152
+ },
153
+ methods: {
154
+ injectImgSrc: function() {
155
+ this.imageThumbnails.forEach((imageThumbnail) => {
156
+ return this.mapImage(imageThumbnail);
157
+ });
158
+ },
159
+ formattedTitle: function(imageThumbnail) {
160
+ const type = imageThumbnail.mimetype?.split('/')[1];
161
+ const title = imageThumbnail.title;
162
+ let formattedTitle = '';
163
+ if (type !== 'jpg') {
164
+ formattedTitle = title.replace('.' + type, '');
165
+ }
166
+ formattedTitle = formattedTitle.replaceAll('_', ' ');
167
+ return formattedTitle;
168
+ },
169
+ datasetURL: function(id) {
170
+ return `https://sparc.science/datasets/${id}?type=dataset`;
171
+ },
172
+ mapImage: async function (image) {
173
+ const imgSrc = await this.transformedImage(image.thumbnail);
174
+ if (imgSrc) {
175
+ image.imgSrc = imgSrc;
176
+ }
177
+ },
178
+ transformedImage: async function(imgUrl) {
179
+ try {
180
+ const response = await fetch(imgUrl);
181
+ if (!response.ok) {
182
+ return null;
183
+ }
184
+ const data = await response.text();
185
+ return BASE64PREFIX + data;
186
+ } catch (error) {
187
+ return null;
188
+ }
189
+ },
190
+ removeSpeciesFilterTag: function () {
191
+ this.activeSpecies = { name: "" };
192
+ this.imageItems = this.imageThumbnails;
193
+ },
194
+ filterBySpecies: function (tagInfo) {
195
+ this.activeSpecies = tagInfo;
196
+ let filteredImageItems = [];
197
+ this.imageThumbnails.forEach((image) => {
198
+ if (image.species && image.species.length) {
199
+ image.species.forEach((species) => {
200
+ if (species === tagInfo.name) {
201
+ filteredImageItems.push(image);
202
+ }
203
+ });
204
+ }
205
+ });
206
+ this.imageItems = filteredImageItems;
207
+ },
208
+ populateFilterTags: function () {
209
+ let imageObjects = {};
210
+ this.imageThumbnails.forEach((image) => {
211
+ if (image.species && image.species.length) {
212
+ image.species.forEach((species) => {
213
+ if (!(species in imageObjects)) {
214
+ imageObjects[species] = {
215
+ name: species,
216
+ count: 0,
217
+ };
218
+ }
219
+ imageObjects[species].count++;
220
+ });
221
+ }
222
+ });
223
+ this.speciesFilterTags = Object.values(imageObjects);
224
+ },
225
+ getCopyContent: function (imageThumbnail) {
226
+ const contentArray = [];
227
+
228
+ // Use <div> instead of <h1>..<h6> or <p>
229
+ // to avoid default formatting on font size and margin
230
+
231
+ // Title
232
+ if (imageThumbnail.title) {
233
+ contentArray.push(`<div><strong>${this.formattedTitle(imageThumbnail)}</strong></div>`);
234
+ }
235
+
236
+ // Type
237
+ if (imageThumbnail.type) {
238
+ let imageType = `<div><strong>Type:</strong></div>`;
239
+ imageType += `\n`;
240
+ imageType += `${imageThumbnail.type}`;
241
+ contentArray.push(`<div>${imageType}</div>`);
242
+ }
243
+
244
+ // Species
245
+ if (imageThumbnail.species?.length) {
246
+ let species = `<div><strong>Species:</strong></div>`;
247
+ species += `\n`;
248
+ species += imageThumbnail.species.join(', ');
249
+ contentArray.push(`<div>${species}</div>`);
250
+ }
251
+
252
+ // Image URL
253
+ if (imageThumbnail.link) {
254
+ let thumbnailLink = `<div><strong>Link:</strong></div>`;
255
+ thumbnailLink += `\n`;
256
+ thumbnailLink += `<a href="${imageThumbnail.link}">${imageThumbnail.link}</a>`;
257
+ contentArray.push(`<div>${thumbnailLink}</div>`);
258
+ }
259
+
260
+ // Dataset ID
261
+ if (imageThumbnail.id) {
262
+ let datasetIdContent = `<div><strong>Dataset ID:</strong></div>`;
263
+ datasetIdContent += `\n`;
264
+ datasetIdContent += `${imageThumbnail.id}`;
265
+ contentArray.push(`<div>${datasetIdContent}</div>`);
266
+ }
267
+
268
+ // Dataset URL
269
+ if (imageThumbnail.id) {
270
+ const datasetURL = this.datasetURL(imageThumbnail.id);
271
+ let dataLocationContent = `<div><strong>Dataset URL:</strong></div>`;
272
+ dataLocationContent += `\n`;
273
+ dataLocationContent += `<a href="${datasetURL}">${datasetURL}</a>`;
274
+ contentArray.push(`<div>${dataLocationContent}</div>`);
275
+ }
276
+
277
+ // Dataset version
278
+ if (imageThumbnail.version) {
279
+ let versionContent = `<div><strong>Dataset version:</strong></div>`;
280
+ versionContent += `\n`;
281
+ versionContent += `${imageThumbnail.version}`;
282
+ contentArray.push(`<div>${versionContent}</div>`);
283
+ }
284
+
285
+ const copyContent = contentArray.join('\n\n<br>');
286
+ return copyContent;
287
+ },
288
+ },
289
+ }
290
+ </script>
291
+
292
+ <style lang="scss" scoped>
293
+ .image-thumbnails-container {
294
+ font-size: 14px;
295
+ text-align: left;
296
+ line-height: 1.5em;
297
+ font-family: $font-family;
298
+ font-weight: 400;
299
+ background-color: #f7faff;
300
+ border-left: 1px solid var(--el-border-color);
301
+ border-top: 1px solid var(--el-border-color);
302
+ display: flex;
303
+ flex-direction: column;
304
+ height: calc(100% - 30px); // minus tabs height
305
+ z-index: 1;
306
+ }
307
+
308
+ .toolbar {
309
+ display: flex;
310
+ flex-direction: row;
311
+ align-items: center;
312
+ justify-content: space-between;
313
+ padding: 1rem;
314
+ gap: 1rem;
315
+ background-color: #f7faff;
316
+ position: sticky;
317
+ top: 0;
318
+ z-index: 1;
319
+ }
320
+
321
+ .filters {
322
+ display: flex;
323
+ flex-direction: row;
324
+ flex-wrap: wrap;
325
+ gap: 5px;
326
+
327
+ .tag {
328
+ cursor: pointer;
329
+ }
330
+ }
331
+
332
+ .view-selector .el-select {
333
+ width: 100px;
334
+ font-family: $font-family;
335
+ }
336
+
337
+ .gallery-container,
338
+ .dataset-card-container {
339
+ margin: 1rem;
340
+ margin-top: 0;
341
+ padding: 1rem;
342
+ flex-grow: 1;
343
+ overflow-y: auto;
344
+ scrollbar-width: thin;
345
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
346
+ border: solid 1px #e4e7ed;
347
+ background-color: #ffffff;
348
+ border-radius: var(--el-border-radius-base);
349
+ }
350
+
351
+ .gallery-container {
352
+ :deep(.gallery) {
353
+ .gallery-strip {
354
+ padding: 1rem 0;
355
+ }
356
+
357
+ > div {
358
+ min-height: max-content !important;
359
+ }
360
+ }
361
+ }
362
+
363
+ .dataset-card-container {
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: 1.5rem;
367
+ }
368
+
369
+ .dataset-card {
370
+ position: relative;
371
+
372
+ + .dataset-card {
373
+ padding-top: 1.5rem;
374
+ border-top: 1px solid var(--el-border-color);
375
+ }
376
+ }
377
+
378
+ .card {
379
+ position: relative;
380
+ display: flex;
381
+ gap: 1.5rem;
382
+ }
383
+
384
+ .card-left {
385
+ flex: 1;
386
+ }
387
+
388
+ .card-right {
389
+ flex: 1.3;
390
+ padding-left: 6px;
391
+ display: flex;
392
+ flex-direction: column;
393
+ gap: 1rem;
394
+ }
395
+
396
+ .card-image {
397
+ display: block;
398
+ text-decoration: none;
399
+ outline: none;
400
+
401
+ .el-image {
402
+ display: block;
403
+ width: 200px;
404
+ height: 150px;
405
+
406
+ :deep(img) {
407
+ aspect-ratio: 4/3;
408
+ object-fit: cover;
409
+ }
410
+ }
411
+
412
+ .image-slot {
413
+ display: flex;
414
+ justify-content: center;
415
+ align-items: center;
416
+ width: 100%;
417
+ height: 100%;
418
+ background: var(--el-fill-color-light);
419
+ color: var(--el-text-color-secondary);
420
+ font-size: 14px;
421
+ }
422
+ }
423
+
424
+ .title {
425
+ font-family: $font-family;
426
+ font-size: 14px;
427
+ font-weight: bold;
428
+ font-stretch: normal;
429
+ font-style: normal;
430
+ line-height: 1.5;
431
+ letter-spacing: 1.05px;
432
+ color: #484848;
433
+ }
434
+
435
+ .details {
436
+ font-family: $font-family;
437
+ font-size: 14px;
438
+ font-weight: normal;
439
+ font-stretch: normal;
440
+ font-style: normal;
441
+ line-height: 1.5;
442
+ letter-spacing: 1.05px;
443
+ color: #484848;
444
+ }
445
+
446
+ .active-tag {
447
+ background-color: $app-primary-color;
448
+ color: #fff;
449
+ }
450
+
451
+ .button {
452
+ margin-left: 0px !important;
453
+ margin-top: 0px !important;
454
+ font-size: 14px !important;
455
+ letter-spacing: initial;
456
+ background-color: $app-primary-color;
457
+ color: #fff;
458
+
459
+ & + .button {
460
+ margin-top: 10px !important;
461
+ }
462
+ &:hover {
463
+ color: #fff !important;
464
+ background: #ac76c5 !important;
465
+ border: 1px solid #ac76c5 !important;
466
+ }
467
+ }
468
+
469
+ .card-button-link {
470
+ text-decoration: none;
471
+ }
472
+
473
+ .float-button-container {
474
+ position: absolute;
475
+ bottom: 0;
476
+ right: 0;
477
+ opacity: 0;
478
+ visibility: hidden;
479
+
480
+ .dataset-card:hover & {
481
+ opacity: 1;
482
+ visibility: visible;
483
+ }
484
+ }
485
+ </style>
486
+
487
+ <style lang="scss">
488
+ .view-selector-select-popper {
489
+ font-family: $font-family;
490
+ font-size: 14px;
491
+ color: #292b66;
492
+
493
+ .el-select-dropdown__item.is-selected {
494
+ color: $app-primary-color;
495
+ }
496
+ }
497
+ </style>
@@ -22,9 +22,11 @@
22
22
  </div>
23
23
  <div class="sidebar-container">
24
24
  <Tabs
25
- v-if="tabs.length > 1 && connectivityInfo"
25
+ v-if="tabs.length > 1 && (connectivityInfo || imageThumbnails.length)"
26
26
  :tabTitles="tabs"
27
27
  :activeId="activeTabId"
28
+ :hasConnectivityInfo="!!connectivityInfo"
29
+ :hasImageThumbnails="!!imageThumbnails.length"
28
30
  @titleClicked="tabClicked"
29
31
  @tab-close="tabClose"
30
32
  />
@@ -39,6 +41,13 @@
39
41
  @show-connectivity="showConnectivity"
40
42
  />
41
43
  </template>
44
+ <template v-else-if="tab.type === 'images'">
45
+ <ImageThumbnails
46
+ v-if="imageThumbnails.length"
47
+ v-show="tab.id === activeTabId"
48
+ :imageThumbnails="imageThumbnails"
49
+ />
50
+ </template>
42
51
  <template v-else>
43
52
  <SidebarContent
44
53
  class="sidebar-content-container"
@@ -68,6 +77,7 @@ import SidebarContent from './SidebarContent.vue'
68
77
  import EventBus from './EventBus.js'
69
78
  import Tabs from './Tabs.vue'
70
79
  import ConnectivityInfo from './ConnectivityInfo.vue'
80
+ import ImageThumbnails from './ImageThumbnails.vue'
71
81
 
72
82
  /**
73
83
  * Aims to provide a sidebar for searching capability for SPARC portal.
@@ -81,6 +91,7 @@ export default {
81
91
  Drawer,
82
92
  Icon,
83
93
  ConnectivityInfo,
94
+ ImageThumbnails,
84
95
  },
85
96
  name: 'SideBar',
86
97
  props: {
@@ -108,7 +119,8 @@ export default {
108
119
  type: Array,
109
120
  default: () => [
110
121
  { id: 1, title: 'Search', type: 'search' },
111
- { id: 2, title: 'Connectivity', type: 'connectivity' }
122
+ { id: 2, title: 'Connectivity', type: 'connectivity' },
123
+ { id: 3, title: 'Images', type: 'images' }, // Temporary
112
124
  ],
113
125
  },
114
126
  /**
@@ -132,6 +144,13 @@ export default {
132
144
  type: Object,
133
145
  default: null,
134
146
  },
147
+ /**
148
+ * The image thumbnails data to show in sidebar.
149
+ */
150
+ imageThumbnails: {
151
+ type: Array,
152
+ default: [],
153
+ },
135
154
  },
136
155
  data: function () {
137
156
  return {
@@ -260,7 +279,12 @@ export default {
260
279
  this.$emit('tabClicked', {id, type});
261
280
  },
262
281
  tabClose: function (id) {
263
- this.$emit('connectivity-info-close');
282
+ const closedTab = this.tabs.find((tab) => tab.id === id);
283
+ if (closedTab.type === 'connectivity') {
284
+ this.$emit('connectivity-info-close');
285
+ } else if (closedTab.type === 'images') {
286
+ this.$emit('image-thumbnail-close');
287
+ }
264
288
  },
265
289
  },
266
290
  created: function () {
@@ -2,7 +2,7 @@
2
2
  <div class="tab-container">
3
3
  <div
4
4
  class="title"
5
- v-for="title in tabTitles"
5
+ v-for="title in titles"
6
6
  :key="title.id"
7
7
  :class="{ 'active-tab': title.id == activeId }"
8
8
  >
@@ -16,7 +16,7 @@
16
16
  </div>
17
17
  </div>
18
18
  <el-button
19
- v-if="title.id === 2"
19
+ v-if="title.type === 'connectivity' || title.type === 'images'"
20
20
  @click="tabClose(title.id)"
21
21
  class="button-tab-close"
22
22
  aria-label="Close"
@@ -42,6 +42,25 @@ export default {
42
42
  type: Number,
43
43
  default: 1,
44
44
  },
45
+ hasConnectivityInfo: {
46
+ type: Boolean,
47
+ default: true,
48
+ },
49
+ hasImageThumbnails: {
50
+ type: Boolean,
51
+ default: true,
52
+ },
53
+ },
54
+ computed: {
55
+ titles: function() {
56
+ if (!this.hasConnectivityInfo) {
57
+ return this.tabTitles.filter((tab) => tab.type !== 'connectivity');
58
+ }
59
+ else if (!this.hasImageThumbnails) {
60
+ return this.tabTitles.filter((tab) => tab.type !== 'images');
61
+ }
62
+ return this.tabTitles;
63
+ },
45
64
  },
46
65
  methods: {
47
66
  titleClicked: function (id, type) {
@@ -73,6 +92,7 @@ $tab-height: 30px;
73
92
  align-items: center;
74
93
  position: relative;
75
94
  cursor: pointer;
95
+ z-index: 2;
76
96
  }
77
97
 
78
98
  .title-text {