<template> <div class="context-card-container" ref="container"> <div v-show="showContextCard"> <div v-if="showDetails && Object.keys(contextData).length !== 0" v-loading="loading" class="context-card" > <div class="card-left"> <img :src="banner" class="context-image"> </div> <div class="card-right scrollbar"> <div> <div class="title">{{contextData.heading}}</div> <div v-html="parseMarkdown(contextData.description)"/> <!-- <br/> --> </div> </div> <div class="card-bottom"> <div> <!-- Show sampeles and views seperately if they do not match --> <template v-if="!samplesUnderViews"> <div v-if="contextData.views && contextData.views.length > 0" class="subtitle">Scaffold Views</div> <template v-for="(view, i) in contextData.views" :key="i+'_1'"> <div @click="openViewFile(view)" class="context-card-view"> <img class="view-image" :src="getFileFromPath(view.thumbnail)"> <div class="view-description">{{view.description}}</div> </div> <div class="padding"/> </template> <div style="margin-bottom: 16px;"/> <div v-if="contextData.samples && contextData.samples.length > 0" class="subtitle">Samples on Scaffold</div> <template v-for="(sample, i) in contextData.samples" :key="i+'_3'"> <span class="context-card-item cursor-pointer" @click="toggleSampleDetails(i)"> <div v-bind:key="i+'_6'" style="display: flex"> <div v-if="sample.color" class="color-box" :style="'background-color:'+ sample.color"></div> <img class="key-image" v-else-if="sample.thumbnail" :src="getFileFromPath(sample.thumbnail)"> {{sample.heading}} <i class="el-icon-warning-outline info"></i> </div> </span> <div v-if="sampleDetails[i]" v-html="sample.description"/> <a v-if="sampleDetails[i] && sample.path" :href="generateFileLink(sample)" target="_blank">View Source</a> <div class="padding"/> </template> </template> <!-- Show samples under views if the ids match --> <template v-else> <div v-if="contextData.views && contextData.views.length > 0" class="subtitle">Scaffold Views</div> <template v-for="(view, i) in contextData.views" :key="i+'_1'"> <span @click="viewClicked(view, i)" class="context-card-view"> <img class="view-image" :src="getFileFromPath(view.thumbnail)"/> <div class="view-description">{{view.description}}<i class="el-icon-warning-outline info"></i> </div> </span> <div v-if="sampleDetails[i]" v-html="samplesMatching(view.id).description"/> <a v-bind:key="i+'_5'" v-if="sampleDetails[i] && samplesMatching(view.id).path" :href="generateFileLink(samplesMatching(view.id))" target="_blank">View Source</a> <div class="padding"/> <!-- Extra padding if sample details is open --> <div v-if="sampleDetails[i]" class="padding"/> </template> </template> </div> </div> </div> <!-- Copy to clipboard button container --> <div class="float-button-container"> <CopyToClipboard :content="updatedCopyContent" theme="light" /> </div> </div> </div> </template> <script> /* eslint-disable no-alert, no-console */ import { CopyToClipboard } from "@abi-software/map-utilities"; import '@abi-software/map-utilities/dist/style.css'; //provide the s3Bucket related methods and data. import S3Bucket from "../mixins/S3Bucket.vue"; import { marked } from 'marked' import xss from 'xss' const addFilesToPathIfMissing = function(path){ if (!path.includes('files')){ return 'files/' + path } else { return path } } const convertBackslashToForwardSlash = function(path){ path = path.replaceAll('\\','/') path = path.replaceAll('\\\\', '/') return path } // const switchPathToDirectory = function(path){ // let newPath = path.split('/') // newPath.pop() // return newPath.join('/') // } export default { name: "contextCard", components: { CopyToClipboard, }, mixins: [S3Bucket], props: { /** * Object containing information for * the required viewing. */ entry: Object, envVars: Object, }, data: function () { return { contextData: {}, showDetails: true, showContextCard: true, sampleDetails: {}, loading: false, }; }, watch: { 'entry.contextCardUrl': { handler(val){ if (val) { // used for hardcoding data if (val === true){ console.error('asked for hardcoding but none provided') } else { this.getContextFile(val) this.showContextCard = true } } else { this.showContextCard = false } }, immediate: true }, 'entry.s3uri': { handler(val){ this.updateS3Bucket(val); }, immediate: true } }, computed: { samplesUnderViews: function(){ if (this.contextData){ if (this.contextData.samplesUnderViews){ return true } else { let viewId = this.contextData.views?.map(v=>v.id) || []; let samplesView = this.contextData.samples?.map(s=>s.view) || []; // get matching values let matching = viewId.filter(v=>samplesView.includes(v)) // check all arrays have the same length (which means all values are in all three) if ( viewId.length === matching.length && matching.length === samplesView.length){ return true } } } return false; }, banner: function(){ if (this.contextData.banner){ return this.getFileFromPath(this.contextData.banner) } else if (this.contextData && this.contextData.views && this.contextData.views.length > 0) { if(this.contextData.views[0].thumbnail){ return this.getFileFromPath(this.contextData.views[0].thumbnail) } } return this.entry.banner }, updatedCopyContent: function () { const contentArray = []; // Use <div> instead of <h1>..<h6> or <p> // to avoid default formatting on font size and margin if (this.contextData.heading) { contentArray.push(`<div><strong>${this.contextData.heading}</strong></div>`); } if (this.contextData.description) { contentArray.push(`<div>${this.contextData.description}</div>`); } if (this.contextData.views?.length) { let scaffoldViews = '<div><strong>Scaffold Views</strong></div>'; const views = []; this.contextData.views.forEach((view, i) => { const viewContents = []; const viewPath = this.getFileFromPath(view.path); let viewContent = `<div>${view.description}</div>`; viewContent += `\n`; viewContent += `<div><a href="${viewPath}">${viewPath}</a></div>`; viewContents.push(viewContent); if (this.samplesUnderViews) { const description = this.samplesMatching(view.id).description; let sampleContent = `<div>${description}</div>`; if (this.samplesMatching(view.id).path) { sampleContent += `\n`; const url = this.generateFileLink(this.samplesMatching(view.id)); sampleContent += `<div><a href="${url}">${url}</a></div>`; } viewContents.push(sampleContent); } const viewContentStr = viewContents.join('\n'); views.push(`<li>${viewContentStr}</li>`); }); scaffoldViews += '\n\n'; scaffoldViews += `<ul>${views.join('\n')}</ul>`; contentArray.push(scaffoldViews); } if (!this.samplesUnderViews) { if (this.contextData.samples?.length) { let sampleViews = '<div><strong>Samples on Scaffold</strong></div>'; const samples = []; this.contextData.samples.forEach((sample, i) => { let sampleContents = ''; sampleContents += `<div>${sample.heading}</div>`; sampleContents += `\n`; sampleContents += `<div>${sample.description}</div>`; if (sample.path) { const url = this.generateFileLink(sample); sampleContents += `\n`; sampleContents += `<div><a href="${url}">${url}</a></div>`; } samples.push(`<li>${sampleContents}</li>`); }); sampleViews += '\n\n'; sampleViews += `<ul>${samples.join('\n')}</ul>`; contentArray.push(sampleViews); } } return contentArray.join('\n\n<br>'); }, }, methods: { samplesMatching: function(viewId){ if (this.contextData && this.contextData.samples){ return this.contextData.samples.filter(s=>s.view == viewId)[0] } else return [] }, viewClicked: function(view, i){ this.openViewFile(view) this.toggleSampleDetails(i) }, getContextFile: function (contextFileUrl) { this.loading = true fetch(contextFileUrl) .then((response) =>{ if (!response.ok){ throw Error(response.statusText) } else { return response.json() } }) .then((data) => { this.contextData = data this.loading = false this.addDiscoverIdsToContextData() }) .catch((err) => { //set defaults if we hit an error console.error('caught error!', err) this.discoverId = undefined this.loading = false }); }, removeDoubleFilesPath: function(path){ if (path) { if (path.includes('files/')){ return path.replace('files/', '') } else if (path.includes('files\\')) { return path.replace('files\\', '') } else { return path } } }, toggleSampleDetails: function(i){ if (this.sampleDetails[i] === undefined){ this.sampleDetails[i] = true; } else { this.sampleDetails[i] = !this.sampleDetails[i]; } }, getFileFromPath: function(path){ // for hardcoded data if(this.entry.contextCardUrl === true){ return path } path = this.removeDoubleFilesPath(path) return `${this.envVars.API_LOCATION}s3-resource/${this.getS3Prefix()}files/${path}${this.getS3Args()}` }, // This is used later when generateing links to the resource on sparc.science (see generateFileLink) addDiscoverIdsToContextData(){ this.contextData.samples.forEach((sample, i)=>{ if (sample && sample.doi && sample.doi !== ""){ fetch(`${this.envVars.PENNSIEVE_API_LOCATION}/discover/datasets/doi/${this.splitDoiFromUrl(sample.doi)}`) .then((response) => response.json()) .then((data) => { this.contextData.samples[i].discoverId = data.id this.contextData.samples[i].version = data.version }) } else { this.contextData.samples[i].discoverId = this.entry.discoverId this.contextData.samples[i].version = this.entry.version } }) }, processPathForUrl(path){ path = convertBackslashToForwardSlash(path) path = addFilesToPathIfMissing(path) return encodeURIComponent(path) }, splitDoiFromUrl(url){ return url.split('https://doi.org/').pop() }, generateFileLink(sample){ const path = this.processPathForUrl(sample.path); let link = `${this.envVars.ROOT_URL}/file/${sample.discoverId}/${sample.version}` + '?path='; link = link + path; return link; }, parseMarkdown(markdown){ const returned_data = xss(marked.parse(markdown)) this.$emit('context-ready') return returned_data }, openViewFile: function(view){ // note that we assume that the view file is in the same directory as the scaffold (viewUrls take relative paths) const viewUrl = this.getFileFromPath(view.path) this.$emit("scaffold-view-clicked", viewUrl); } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="scss"> .hide{ color: #e4e7ed; cursor: pointer; } .context-card-container { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; // text-align: initial; // default is justify word-break: initial; } .context-card{ background-color: white; font-size: 14px; position: relative; display: flex; flex-wrap: wrap; gap: 10px; width: 100%; max-height: 258px; overflow-y: auto; scrollbar-width: thin; box-sizing: border-box; &:not(.context-card-container) { padding: 10px; } } .context-card-view{ cursor: pointer; margin-bottom: 8px; display: flex; gap: 8px; } .view-image { width: 34px; height: auto; } .view-descriptions { flex: 8; } .context-card :deep(.el-card__body) { margin: 0px; display: flex; width: 516px; } .context-image{ width: 150px; height: auto; } .color-box { width: 16px; height: 16px; border: solid 1px $app-primary-color; border-radius: 2px; margin-right: 8px; } .card-left{ flex: 0.8 } .card-right { flex: 1.5; word-break: normal !important; :deep(p:last-child) { margin-bottom: 0; } } .card-bottom { flex: 0 0 100%; max-width: 100%; } .cursor-pointer { cursor: pointer; } .info{ transform: rotate(180deg); color: $app-primary-color; margin-left: 8px; } .padding { margin-bottom: 8px; } .title{ font-weight: bold; } .subtitle{ font-weight: bold; margin-bottom: 8px; } .scrollbar::-webkit-scrollbar-track { border-radius: 10px; background-color: #f5f5f5; } .scrollbar::-webkit-scrollbar { width: 12px; right: -12px; background-color: #f5f5f5; } .scrollbar::-webkit-scrollbar-thumb { border-radius: 4px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.06); background-color: #979797; } .float-button-container { position: absolute; bottom: 6px; right: 12px; opacity: 0; visibility: hidden; .context-card-container:hover & { opacity: 1; visibility: visible; } } </style>