@abi-software/map-utilities 1.2.2-beta.3 → 1.2.2-beta.5

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,27 +1,67 @@
1
1
  <template>
2
2
  <div class="resource-container">
3
- <template v-for="resource in resources" :key="resource.id">
4
- <div class="resource">
5
- <el-button
6
- v-if="resource.id === 'pubmed'"
7
- class="button"
8
- id="open-pubmed-button"
9
- :icon="ElIconNotebook"
10
- @click="openUrl(resource.url)"
11
- >
12
- Open publications in PubMed
13
- </el-button>
14
- </div>
15
- </template>
3
+ <div class="attribute-title-container">
4
+ <div class="attribute-title">References</div>
5
+ </div>
6
+ <div class="citation-tabs" v-if="referencesWithDOI">
7
+ <el-button
8
+ link
9
+ v-for="citationOption of citationOptions"
10
+ :key="citationOption.value"
11
+ :type="citationType === citationOption.value ? 'primary' : ''"
12
+ @click="onCitationFormatChange(citationOption.value)"
13
+ >
14
+ {{ citationOption.label }}
15
+ </el-button>
16
+ </div>
17
+ <ul class="citation-list">
18
+ <li
19
+ v-for="reference of pubMedReferences"
20
+ :key="reference.id"
21
+ :class="{'loading': reference.citation && reference.citation[citationType] === ''}"
22
+ >
23
+ <template v-if="reference.citation && reference.citation[citationType]">
24
+ <span v-html="reference.citation[citationType]"></span>
25
+ <CopyToClipboard :content="reference.citation[citationType]" />
26
+ </template>
27
+ </li>
28
+
29
+ <li v-for="reference of openLibReferences">
30
+ <div v-html="formatCopyReference(reference)"></div>
31
+ <CopyToClipboard :content="formatCopyReference(reference)" />
32
+ </li>
33
+
34
+ <li v-for="reference of isbnDBReferences">
35
+ <a :href="reference.url" target="_blank">{{ reference.url }}</a>
36
+ <CopyToClipboard :content="reference.url" />
37
+ </li>
38
+ </ul>
16
39
  </div>
17
40
  </template>
18
41
 
19
42
  <script>
20
- /* eslint-disable no-alert, no-console */
21
- import { shallowRef } from "vue";
22
- import { Notebook as ElIconNotebook } from "@element-plus/icons-vue";
43
+ import CopyToClipboard from '../CopyToClipboard/CopyToClipboard.vue';
23
44
 
24
- import EventBus from "../EventBus.js";
45
+ const CROSSCITE_API_HOST = 'https://citation.crosscite.org';
46
+ const CITATION_OPTIONS = [
47
+ {
48
+ label: 'APA',
49
+ value: 'apa',
50
+ },
51
+ {
52
+ label: 'Chicago',
53
+ value: 'chicago-note-bibliography',
54
+ },
55
+ {
56
+ label: 'IEEE',
57
+ value: 'ieee',
58
+ },
59
+ {
60
+ label: 'Bibtex',
61
+ value: 'bibtex',
62
+ },
63
+ ];
64
+ const CITATION_DEFAULT = 'apa';
25
65
 
26
66
  export default {
27
67
  name: "ExternalResourceCard",
@@ -33,26 +73,373 @@ export default {
33
73
  },
34
74
  data: function () {
35
75
  return {
36
- pubmeds: [],
37
- pubmedIds: [],
38
- ElIconNotebook: shallowRef(ElIconNotebook),
39
- };
76
+ pubMedReferences: [],
77
+ openLibReferences: [],
78
+ isbnDBReferences: [],
79
+ citationOptions: CITATION_OPTIONS,
80
+ citationType: CITATION_DEFAULT,
81
+ }
82
+ },
83
+ watch: {
84
+ resources: function (_resources) {
85
+ this.formatReferences([..._resources]);
86
+ },
87
+ },
88
+ computed: {
89
+ referencesWithDOI: function () {
90
+ const withDOI = this.pubMedReferences.filter((reference) => reference.type === 'doi' || reference.doi);
91
+ return withDOI.length;
92
+ },
93
+ },
94
+ mounted: function () {
95
+ this.formatReferences([...this.resources]);
96
+ this.getCitationText(CITATION_DEFAULT);
40
97
  },
41
98
  methods: {
42
- capitalise: function (string) {
43
- return string.charAt(0).toUpperCase() + string.slice(1);
99
+ formatReferences: function (references) {
100
+ const nonPubMedReferences = this.extractNonPubMedReferences(references);
101
+ const pubMedReferences = references.filter((reference) => !nonPubMedReferences.includes(reference));
102
+
103
+ this.pubMedReferences = pubMedReferences.map((reference) =>
104
+ (typeof reference === 'object') ?
105
+ this.extractPublicationIdFromURLString(reference[0]) :
106
+ this.extractPublicationIdFromURLString(reference)
107
+ );
108
+
109
+ // pmc to pmid
110
+ this.pubMedReferences.forEach((reference) => {
111
+ if (reference.type === 'pmc') {
112
+ const pmcId = reference.id;
113
+ this.searchPMID(pmcId).then((data) => {
114
+ if (data && data.esearchresult) {
115
+ const idList = data.esearchresult.idlist || [];
116
+ reference.id = idList[0];
117
+ reference.type = 'pmid';
118
+ }
119
+ });
120
+ }
121
+ });
122
+
123
+ this.formatNonPubMedReferences(nonPubMedReferences).then((responses) => {
124
+ this.openLibReferences = responses.filter((response) => response.type === 'openlib');
125
+ this.isbnDBReferences = responses.filter((response) => response.type === 'isbndb');
126
+
127
+ this.formatOpenLibReferences();
128
+ });
129
+ },
130
+ extractNonPubMedReferences: function (references) {
131
+ const extractedReferences = [];
132
+ const pubmedDomains = this.getPubMedDomains();
133
+
134
+ references.forEach((reference) => {
135
+ let count = 0;
136
+ pubmedDomains.forEach((name) => {
137
+ if (reference.includes(name)) {
138
+ count++;
139
+ }
140
+ });
141
+ if (!count) {
142
+ extractedReferences.push(reference);
143
+ }
144
+ });
145
+
146
+ return extractedReferences;
147
+ },
148
+ formatNonPubMedReferences: async function (references) {
149
+ const transformedReferences = [];
150
+ const filteredReferences = references.filter((referenceURL) => referenceURL.indexOf('isbn') !== -1);
151
+ const isbnIDs = filteredReferences.map((url) => {
152
+ const isbnId = url.split('/').pop();
153
+ return 'ISBN:' + isbnId;
154
+ });
155
+ const isbnIDsKey = isbnIDs.join(',');
156
+ const failedIDs = isbnIDs.slice();
157
+
158
+ const openlibAPI = `https://openlibrary.org/api/books?bibkeys=${isbnIDsKey}&format=json`;
159
+ const data = await this.fetchData(openlibAPI);
160
+
161
+ for (const key in data) {
162
+ const successKeyIndex = failedIDs.indexOf(key);
163
+ failedIDs.splice(successKeyIndex, 1);
164
+
165
+ const url = data[key].info_url;
166
+ const urlSegments = url.split('/');
167
+ const endpointIndex = urlSegments.indexOf('books');
168
+ const bookId = urlSegments[endpointIndex + 1];
169
+
170
+ transformedReferences.push({
171
+ id: key.split(':')[1], // Key => "ISBN:1234"
172
+ type: 'openlib',
173
+ url: url,
174
+ bookId: bookId,
175
+ });
176
+ }
177
+
178
+ failedIDs.forEach((failedID) => {
179
+ const id = failedID.split(':')[1];
180
+ // Data does not exist in OpenLibrary
181
+ // Provide ISBNDB link for reference
182
+ const url = `https://isbndb.com/book/${id}`;
183
+ transformedReferences.push({
184
+ id: id,
185
+ url: url,
186
+ type: 'isbndb'
187
+ });
188
+ });
189
+
190
+ return transformedReferences;
191
+ },
192
+ extractPublicationIdFromURLString: function (urlStr) {
193
+ if (!urlStr) return
194
+
195
+ const str = decodeURIComponent(urlStr)
196
+
197
+ let term = {id: '', type: '', citation: {}}
198
+
199
+ const names = this.getPubMedDomains()
200
+
201
+ names.forEach((name) => {
202
+ const lastIndex = str.lastIndexOf(name)
203
+ if (lastIndex !== -1) {
204
+ term.id = str.slice(lastIndex + name.length)
205
+ if (name === 'doi.org/') {
206
+ term.type = "doi"
207
+ } else if (name === 'pmc/articles/') {
208
+ term.type = "pmc"
209
+ } else {
210
+ term.type = "pmid"
211
+ }
212
+ }
213
+ })
214
+
215
+ //Backward compatability with doi: and PMID:
216
+ if (term.id === '') {
217
+ if (urlStr.includes("doi:")) {
218
+ term.id = this.stripPMIDPrefix(urlStr)
219
+ term.type = "doi"
220
+ } else if (urlStr.includes("PMID:")) {
221
+ term.id = this.stripPMIDPrefix(urlStr)
222
+ term.type = "pmid"
223
+ }
224
+ }
225
+
226
+ if (term.id.endsWith('/')) {
227
+ term.id = term.id.slice(0, -1)
228
+ }
229
+
230
+ return term
44
231
  },
45
- openUrl: function (url) {
46
- EventBus.emit("open-pubmed-url", url);
47
- window.open(url, "_blank");
232
+ getPubMedDomains: function () {
233
+ const names = [
234
+ 'doi.org/',
235
+ 'nih.gov/pubmed/',
236
+ 'pmc/articles/',
237
+ 'pubmed.ncbi.nlm.nih.gov/',
238
+ ]
239
+
240
+ return names;
241
+ },
242
+ stripPMIDPrefix: function (pubmedId) {
243
+ return pubmedId.split(':')[1]
244
+ },
245
+ onCitationFormatChange: function(citationType) {
246
+ this.citationType = citationType;
247
+ this.getCitationText(citationType);
248
+ },
249
+ getCitationText: function(citationType) {
250
+ this.pubMedReferences.forEach((reference) => {
251
+ const { id, type, doi } = reference;
252
+
253
+ if (
254
+ !(reference.citation && reference.citation[citationType])
255
+ && id
256
+ ) {
257
+ reference.citation[citationType] = ''; // loading
258
+
259
+ if (type === 'doi' || doi) {
260
+ const doiID = type === 'doi' ? id : doi;
261
+ this.getCitationTextByDOI(doiID).then((text) => {
262
+ const formattedText = this.replaceLinkInText(text);
263
+ reference.citation[citationType] = formattedText;
264
+ this.updateCopyContents();
265
+ });
266
+ } else if (type === 'pmid') {
267
+ this.getDOIFromPubMedID(id).then((data) => {
268
+ if (data?.result) {
269
+ const resultObj = data.result[id];
270
+ const articleIDs = resultObj?.articleids || [];
271
+ const doiObj = articleIDs.find((item) => item.idtype === 'doi');
272
+ const doiID = doiObj?.value;
273
+
274
+ if (doiID) {
275
+ reference['doi'] = doiID;
276
+ this.getCitationTextByDOI(doiID).then((text) => {
277
+ const formattedText = this.replaceLinkInText(text);
278
+ reference.citation[citationType] = formattedText;
279
+ this.updateCopyContents();
280
+ });
281
+ } else {
282
+ // If there has no doi in PubMed
283
+ const { title, pubdate, authors } = resultObj;
284
+ const authorNames = authors ? authors.map((author) => author.name) : [];
285
+ const formattedText = this.formatCopyReference({
286
+ title: title || '',
287
+ date: pubdate || '',
288
+ authors: authorNames,
289
+ url: `https://pubmed.ncbi.nlm.nih.gov/${id}`,
290
+ });
291
+ reference.citation[citationType] = formattedText;
292
+ this.updateCopyContents();
293
+ }
294
+ }
295
+ });
296
+ }
297
+ }
298
+ });
299
+ },
300
+ updateCopyContents: function () {
301
+ const citationTypeObj = this.citationOptions.find((item) => item.value === this.citationType);
302
+ let citationFormatStyle = '';
303
+ const values = [];
304
+
305
+ if (this.referencesWithDOI) {
306
+ citationFormatStyle = citationTypeObj?.label;
307
+ }
308
+
309
+ this.pubMedReferences.forEach((reference) => {
310
+ values.push(reference.citation[this.citationType]);
311
+ });
312
+
313
+ this.openLibReferences.forEach((reference) => {
314
+ values.push(this.formatCopyReference(reference));
315
+ });
316
+
317
+ this.isbnDBReferences.forEach((reference) => {
318
+ values.push(reference.url);
319
+ });
320
+
321
+ this.$emit('references-loaded', {
322
+ style: citationFormatStyle,
323
+ list: values
324
+ });
325
+ },
326
+ replaceLinkInText: function (text) {
327
+ const protocol = 'https://';
328
+ let linkBody = text.split(protocol)[1];
329
+
330
+ if (linkBody) {
331
+ linkBody = linkBody.split(' ')[0];
332
+ linkBody = linkBody.trim();
333
+
334
+ if (linkBody.endsWith('.')) {
335
+ linkBody = linkBody.substring(0, linkBody.length - 1);
336
+ }
337
+
338
+ const fullLink = protocol + linkBody;
339
+ const htmlLink = `<a href="${fullLink}" target="_blank">${fullLink}</a>`;
340
+
341
+ return text.replace(fullLink, htmlLink);
342
+ }
343
+
344
+ return text;
345
+ },
346
+ searchPMID: async function (term) {
347
+ const esearchAPI = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${term}&format=json`;
348
+ return await this.fetchData(esearchAPI);
349
+ },
350
+ getCitationTextByDOI: async function (id) {
351
+ const citationAPI = `${CROSSCITE_API_HOST}/format?doi=${id}&style=${this.citationType}&lang=en-US`;
352
+ return await this.fetchData(citationAPI, 'text');
353
+ },
354
+ getDOIFromPubMedID: async function (pubmedId) {
355
+ const summaryAPI = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${pubmedId}&format=json`;
356
+ return await this.fetchData(summaryAPI);
357
+ },
358
+ formatOpenLibReferences: function () {
359
+ this.openLibReferences.forEach((reference) => {
360
+ const { bookId } = reference;
361
+ this.getBookData(bookId).then((data) => {
362
+ const { title, authors, publish_date } = data;
363
+ if (title) {
364
+ reference['title'] = title;
365
+ }
366
+
367
+ if (publish_date) {
368
+ reference['date'] = publish_date;
369
+ }
370
+
371
+ if (authors) {
372
+ reference['authors'] = [];
373
+
374
+ authors.forEach((author) => {
375
+ this.getBookAuthor(author.key).then((data) => {
376
+ const { name } = data;
377
+ if (name) {
378
+ reference['authors'].push(name);
379
+ this.updateCopyContents();
380
+ }
381
+ });
382
+ });
383
+ }
384
+ this.updateCopyContents();
385
+ });
386
+ });
387
+ },
388
+ getBookData: async function (bookId) {
389
+ const apiURL = `https://openlibrary.org/books/${bookId}.json`;
390
+ return await this.fetchData(apiURL);
391
+ },
392
+ getBookAuthor: async function (key) {
393
+ const apiURL = `https://openlibrary.org${key}.json`;
394
+ return await this.fetchData(apiURL);
395
+ },
396
+ formatCopyReference: function (reference) {
397
+ const copyContents = [];
398
+ const { title, date, authors, url } = reference;
399
+
400
+ if (title) {
401
+ copyContents.push(title);
402
+ }
403
+
404
+ if (date) {
405
+ copyContents.push(`(${date})`);
406
+ }
407
+
408
+ if (authors) {
409
+ copyContents.push(`- ${authors.join(', ')}`);
410
+ }
411
+
412
+ copyContents.push(`<div><a href="${url}" target="_blank">${url}</a></div>`);
413
+
414
+ return copyContents.join(' ');
415
+ },
416
+ fetchData: async function (apiURL, format) {
417
+ try {
418
+ const response = await fetch(apiURL);
419
+ if (!response.ok) {
420
+ throw new Error(`Response status: ${response.status}`);
421
+ }
422
+
423
+ if (format === 'text') {
424
+ return await response.text();
425
+ } else {
426
+ return await response.json();
427
+ }
428
+ } catch (error) {
429
+ console.error(`Fetch data error: ${error}`);
430
+ }
48
431
  },
49
432
  },
50
- };
433
+ }
51
434
  </script>
52
435
 
53
436
  <style lang="scss" scoped>
54
437
  .resource-container {
55
- margin-top: 0.5em;
438
+ margin-top: 1em;
439
+ }
440
+
441
+ .attribute-title-container {
442
+ margin-bottom: 0.5rem;
56
443
  }
57
444
 
58
445
  .attribute-title {
@@ -62,47 +449,89 @@ export default {
62
449
  text-transform: uppercase;
63
450
  }
64
451
 
65
- .attribute-content {
66
- font-size: 14px;
67
- font-weight: 400;
68
- }
452
+ .citation-list {
453
+ margin: 0;
454
+ margin-top: 0.5rem;
455
+ padding: 0;
456
+ list-style: none;
457
+ line-height: 1.3;
458
+
459
+ li {
460
+ margin: 0;
461
+ padding: 0.5rem 1.5rem 0.5rem 0.75rem;
462
+ border-radius: var(--el-border-radius-base);
463
+ background-color: var(--el-bg-color-page);
464
+ position: relative;
465
+
466
+ :deep(a) {
467
+ word-wrap: break-word;
468
+ }
469
+
470
+ + li {
471
+ margin-top: 0.5rem;
472
+ }
473
+
474
+ &.loading {
475
+ padding: 1rem;
69
476
 
70
- .el-link {
71
- color: $app-primary-color;
72
- text-decoration: none;
73
- word-wrap: break-word;
74
- &:hover,
75
- &:focus {
76
- color: $app-primary-color;
477
+ &::before {
478
+ content: "";
479
+ display: block;
480
+ width: 100%;
481
+ height: 100%;
482
+ position: absolute;
483
+ top: 0;
484
+ left: 0;
485
+ animation-duration: 3s;
486
+ animation-fill-mode: forwards;
487
+ animation-iteration-count: infinite;
488
+ animation-name: loadingAnimation;
489
+ animation-timing-function: linear;
490
+ background: linear-gradient(to right,
491
+ var(--el-bg-color-page) 5%,
492
+ var(--el-color-info-light-8) 15%,
493
+ var(--el-bg-color-page) 30%
494
+ );
495
+ }
496
+ }
497
+
498
+ :deep(.copy-clipboard-button) {
499
+ position: absolute;
500
+ bottom: 0.25rem;
501
+ right: 0.25rem;
502
+ opacity: 0;
503
+ visibility: hidden;
504
+ }
505
+
506
+ &:hover {
507
+ :deep(.copy-clipboard-button) {
508
+ opacity: 1;
509
+ visibility: visible;
510
+ }
511
+ }
77
512
  }
78
513
  }
79
514
 
80
- :deep(.el-carousel__button) {
81
- background-color: $app-primary-color;
82
- }
515
+ .citation-tabs {
516
+ .el-button {
517
+ &:hover,
518
+ &:focus,
519
+ &:active {
520
+ color: $app-primary-color;
521
+ }
522
+ }
83
523
 
84
- .attribute-title {
85
- font-size: 16px;
86
- font-weight: 600;
87
- /* font-weight: bold; */
88
- text-transform: uppercase;
524
+ .el-button + .el-button {
525
+ margin-left: 0.25rem;
526
+ }
89
527
  }
90
528
 
91
- .button {
92
- margin-left: 0px !important;
93
- margin-top: 0px !important;
94
- font-size: 14px !important;
95
- background-color: $app-primary-color;
96
- color: #fff;
97
- &:hover {
98
- color: #fff !important;
99
- background: #ac76c5 !important;
100
- border: 1px solid #ac76c5 !important;
529
+ @keyframes loadingAnimation {
530
+ 0% {
531
+ background-position: -30vw 0;
101
532
  }
102
- & + .button {
103
- margin-top: 10px !important;
104
- background-color: $app-primary-color;
105
- color: #fff;
533
+ 100% {
534
+ background-position: 70vw 0;
106
535
  }
107
536
  }
108
537
  </style>
@@ -164,7 +164,7 @@
164
164
  Search for data on components
165
165
  </el-button>
166
166
 
167
- <external-resource-card :resources="resources"></external-resource-card>
167
+ <external-resource-card :resources="resources" v-if="resources.length"></external-resource-card>
168
168
  </div>
169
169
  </transition>
170
170
  </div>
@@ -6,6 +6,7 @@ import DrawToolbar from "./DrawToolbar/DrawToolbar.vue";
6
6
  import HelpModeDialog from "./HelpModeDialog/HelpModeDialog.vue";
7
7
  import Tooltip from "./Tooltip/Tooltip.vue";
8
8
  import TreeControls from "./TreeControls/TreeControls.vue";
9
+ import ExternalResourceCard from "./Tooltip/ExternalResourceCard.vue";
9
10
 
10
11
  export {
11
12
  AnnotationPopup,
@@ -16,4 +17,5 @@ export {
16
17
  HelpModeDialog,
17
18
  Tooltip,
18
19
  TreeControls,
20
+ ExternalResourceCard,
19
21
  };
@@ -4,6 +4,47 @@ const capitalise = term => {
4
4
  return term;
5
5
  };
6
6
 
7
+ const convertNodeToObject = (node) => {
8
+ const obj = {};
9
+
10
+ if (node.attributes) {
11
+ for (let i = 0; i < node.attributes.length; i++) {
12
+ const attr = node.attributes[i];
13
+ obj[`@${attr.nodeName}`] = attr.nodeValue;
14
+ }
15
+ }
16
+
17
+ for (let i = 0; i < node.childNodes.length; i++) {
18
+ const child = node.childNodes[i];
19
+ if (child.nodeType === Node.ELEMENT_NODE) {
20
+ const childResult = convertNodeToObject(child);
21
+ if (obj[child.nodeName]) {
22
+ if (!Array.isArray(obj[child.nodeName])) {
23
+ obj[child.nodeName] = [obj[child.nodeName]];
24
+ }
25
+ obj[child.nodeName].push(childResult);
26
+ } else {
27
+ obj[child.nodeName] = childResult;
28
+ }
29
+ } else if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim() !== '') {
30
+ return child.nodeValue.trim();
31
+ }
32
+ }
33
+
34
+ return obj;
35
+ };
36
+
37
+ const xmlToJSON = (xmlText) => {
38
+ const parser = new DOMParser();
39
+ const xmlDoc = parser.parseFromString(xmlText, "text/xml");
40
+
41
+ const result = {};
42
+ result[xmlDoc.documentElement.nodeName] = convertNodeToObject(xmlDoc.documentElement);
43
+
44
+ return result;
45
+ };
46
+
7
47
  export {
8
48
  capitalise,
49
+ xmlToJSON,
9
50
  };
@@ -14,6 +14,7 @@ declare module 'vue' {
14
14
  CreateTooltipContent: typeof import('./components/Tooltip/CreateTooltipContent.vue')['default']
15
15
  DrawToolbar: typeof import('./components/DrawToolbar/DrawToolbar.vue')['default']
16
16
  ElButton: typeof import('element-plus/es')['ElButton']
17
+ ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
17
18
  ElCard: typeof import('element-plus/es')['ElCard']
18
19
  ElCol: typeof import('element-plus/es')['ElCol']
19
20
  ElColorPicker: typeof import('element-plus/es')['ElColorPicker']