Organised components into directories
This commit is contained in:
197
src/components/LinkItems/Collapsable.vue
Normal file
197
src/components/LinkItems/Collapsable.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div :class="`collapsable ${checkSpanNum(cols, 'col')} ${checkSpanNum(rows, 'row')}`"
|
||||
:style="`${color ? 'background: '+color : ''}; ${sanitizeCustomStyles(customStyles)};`"
|
||||
>
|
||||
<input
|
||||
:id="`collapsible-${uniqueKey}`"
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
:checked="getCollapseState()"
|
||||
@change="collapseChanged"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<label :for="`collapsible-${uniqueKey}`" class="lbl-toggle" tabindex="-1">
|
||||
<h3>{{ title }}</h3>
|
||||
</label>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CollapsableContainer',
|
||||
props: {
|
||||
uniqueKey: String,
|
||||
title: String,
|
||||
collapsed: Boolean,
|
||||
cols: Number,
|
||||
rows: Number,
|
||||
color: String,
|
||||
customStyles: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: !this.collapsed,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* Check that row & column span is valid, and not over the max */
|
||||
checkSpanNum(span, classPrefix) {
|
||||
const maxSpan = 4;
|
||||
let numSpan = /^\d*$/.test(span) ? parseInt(span, 10) : 1;
|
||||
numSpan = (numSpan > maxSpan) ? maxSpan : numSpan;
|
||||
return `${classPrefix}-${numSpan}`;
|
||||
},
|
||||
/* Removes all special characters, except those allowed in valid CSS */
|
||||
sanitizeCustomStyles(userCss) {
|
||||
return userCss ? userCss.replace(/[^a-zA-Z0-9- :;.]/g, '') : '';
|
||||
},
|
||||
/* If not already done, then add object structure to local storage */
|
||||
initialiseStorage() {
|
||||
const initStorage = () => localStorage.setItem('collapseState', JSON.stringify({}));
|
||||
if (!localStorage.collapseState) initStorage(); // If not yet set, then init localstorage
|
||||
try { // Check storage is valid JSON, and has not been corrupted
|
||||
JSON.parse(localStorage.collapseState);
|
||||
} catch {
|
||||
initStorage();
|
||||
}
|
||||
return JSON.parse(localStorage.collapseState);
|
||||
},
|
||||
getCollapseState() {
|
||||
const collapseStateObject = this.initialiseStorage();
|
||||
let collapseState = !this.collapsed;
|
||||
if (collapseStateObject[this.uniqueKey] !== undefined) {
|
||||
collapseState = collapseStateObject[this.uniqueKey];
|
||||
}
|
||||
return collapseState;
|
||||
},
|
||||
setCollapseState(id, newState) {
|
||||
// Get the current localstorage collapse state object
|
||||
const collapseState = JSON.parse(localStorage.collapseState);
|
||||
// Add the new state to it
|
||||
collapseState[id] = newState;
|
||||
// Stringify, and set the new object into local storage
|
||||
localStorage.setItem('collapseState', JSON.stringify(collapseState));
|
||||
},
|
||||
collapseChanged(whatChanged) {
|
||||
this.initialiseStorage();
|
||||
this.setCollapseState(this.uniqueKey.toString(), whatChanged.srcElement.checked);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@import '@/styles/constants.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
.collapsable {
|
||||
padding: 5px;
|
||||
margin: 10px;
|
||||
border-radius: $curve-factor;
|
||||
background: var(--primary);
|
||||
// background: -webkit-linear-gradient(to left top, #9F86FF, #1CA8DD, #007AE1);
|
||||
// background: linear-gradient(to left top, #9F86FF, #1CA8DD, #007AE1);
|
||||
box-shadow: 1px 1px 2px #130f23;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
width: stretch;
|
||||
|
||||
grid-row-start: span 1;
|
||||
&.row-2 { grid-row-start: span 2; }
|
||||
&.row-3 { grid-row-start: span 3; }
|
||||
&.row-4 { grid-row-start: span 4; }
|
||||
|
||||
grid-column-start: span 1;
|
||||
@include tablet-up {
|
||||
&.col-2 { grid-column-start: span 2; }
|
||||
&.col-3 { grid-column-start: span 2; }
|
||||
&.col-4 { grid-column-start: span 2; }
|
||||
}
|
||||
@include laptop-up {
|
||||
&.col-2 { grid-column-start: span 2; }
|
||||
&.col-3 { grid-column-start: span 3; }
|
||||
&.col-4 { grid-column-start: span 3; }
|
||||
}
|
||||
@include monitor-up {
|
||||
&.col-2 { grid-column-start: span 2; }
|
||||
&.col-3 { grid-column-start: span 3; }
|
||||
&.col-4 { grid-column-start: span 4; }
|
||||
}
|
||||
|
||||
.wrap-collabsible {
|
||||
margin-bottom: 1.2rem 0;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lbl-toggle {
|
||||
display: block;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
border-radius: $curve-factor;
|
||||
transition: all 0.25s ease-out;
|
||||
text-align: left;
|
||||
color: var(--background-transparent);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.lbl-toggle:hover {
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.lbl-toggle::before {
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 5px solid currentColor;
|
||||
vertical-align: middle;
|
||||
margin-right: .7rem;
|
||||
transform: translateY(-2px);
|
||||
|
||||
transition: transform .2s ease-out;
|
||||
}
|
||||
|
||||
.toggle:checked + .lbl-toggle::before {
|
||||
transform: rotate(90deg) translateX(-3px);
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .25s ease-in-out;
|
||||
background: var(--background-transparent);
|
||||
border-radius: 0 0 $inner-radius $inner-radius;
|
||||
}
|
||||
|
||||
.toggle:checked + .lbl-toggle + .collapsible-content {
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
.toggle:checked + .lbl-toggle {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.collapsible-content .content-inner {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/components/LinkItems/IframeModal.vue
Normal file
57
src/components/LinkItems/IframeModal.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<modal :name="name" :resizable="true" width="80%" height="80%" @closed="$emit('closed')">
|
||||
<div slot="top-right" @click="hide()">Close</div>
|
||||
<a @click="hide()" class="close-button" title="Close">x</a>
|
||||
<iframe :src="url" @keydown.esc="close" class="frame"/>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IframeModal',
|
||||
props: {
|
||||
name: String,
|
||||
},
|
||||
data: () => ({
|
||||
url: '#',
|
||||
}),
|
||||
methods: {
|
||||
show: function show(url) {
|
||||
this.url = url;
|
||||
this.$modal.show(this.name);
|
||||
},
|
||||
hide: function hide() {
|
||||
this.$modal.hide(this.name);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 0.5rem;
|
||||
border: 0;
|
||||
border-radius: 0 0 0 10px;
|
||||
background: var(--primary);
|
||||
color: var(--background);
|
||||
border-left: 1px solid var(--primary);
|
||||
border-bottom: 1px solid var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--background);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
246
src/components/LinkItems/Item.vue
Normal file
246
src/components/LinkItems/Item.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template ref="container">
|
||||
<a @click="itemOpened"
|
||||
:href="target !== 'iframe' ? url : '#'"
|
||||
:target="target === 'newtab' ? '_blank' : ''"
|
||||
:class="`item ${!icon? 'short': ''}`"
|
||||
:id="`link-${id}`"
|
||||
v-tooltip="getTooltipOptions()"
|
||||
rel="noopener noreferrer"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Item Text -->
|
||||
<div class="tile-title" :id="`tile-${id}`">
|
||||
<span class="text">{{ title }}</span>
|
||||
<div class="overflow-dots">...</div>
|
||||
</div>
|
||||
<!-- Item Icon -->
|
||||
<Icon :icon="icon" :url="url" />
|
||||
<!-- Small icon, showing opening method on hover -->
|
||||
<ItemOpenMethodIcon class="opening-method-icon" :openingMethod="target" :isSmall="!icon" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
import ItemOpenMethodIcon from '@/components/LinkItems/ItemOpenMethodIcon';
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
props: {
|
||||
id: String, // The unique ID of a tile (e.g. 001)
|
||||
title: String, // The main text of tile, required
|
||||
subtitle: String, // Optional sub-text
|
||||
description: String, // Optional tooltip hover text
|
||||
icon: String, // Optional path to icon, within public/img/tile-icons
|
||||
svg: String, // Optional vector graphic, that is then dynamically filled
|
||||
color: String, // Optional background color, specified in hex code
|
||||
url: String, // URL to the resource, optional but recommended
|
||||
target: { // Where resource will open, either 'newtab', 'sametab' or 'iframe'
|
||||
type: String,
|
||||
default: 'newtab',
|
||||
validator: (value) => ['newtab', 'sametab', 'iframe'].indexOf(value) !== -1,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
getId: this.id,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
ItemOpenMethodIcon,
|
||||
},
|
||||
methods: {
|
||||
/* Called when an item is clicked, manages the opening of iframe & resets the search field */
|
||||
itemOpened(e) {
|
||||
if (e.altKey || this.target === 'iframe') {
|
||||
e.preventDefault();
|
||||
this.$emit('triggerModal', this.url);
|
||||
} else {
|
||||
this.$emit('itemClicked');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Detects overflowing text, shows ellipse, and allows is to marguee on hover
|
||||
* The below code is horifically bad, it is embarassing that I wrote it...
|
||||
*/
|
||||
manageTitleEllipse() {
|
||||
const tileElem = document.getElementById(`tile-${this.getId}`);
|
||||
if (tileElem) {
|
||||
const isOverflowing = tileElem.scrollHeight > tileElem.clientHeight
|
||||
|| tileElem.scrollWidth > tileElem.clientWidth;
|
||||
if (isOverflowing) tileElem.className += ' is-overflowing';
|
||||
} // Note from present me to past me: WTF?!
|
||||
},
|
||||
/* Returns configuration object for the tooltip */
|
||||
getTooltipOptions() {
|
||||
return {
|
||||
disabled: !this.description,
|
||||
content: this.description,
|
||||
trigger: 'hover focus',
|
||||
hideOnTargetClick: true,
|
||||
html: false,
|
||||
delay: { show: 350, hide: 200 },
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.manageTitleEllipse();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../../src/styles/constants.scss';
|
||||
|
||||
/* Item wrapper */
|
||||
.item-wrapper {
|
||||
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-grow: 1;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
color: var(--primary);
|
||||
vertical-align: middle;
|
||||
margin: 0.5rem;
|
||||
background: #607d8b33;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: $curve-factor;
|
||||
box-shadow: 1px 1px 2px #373737;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
box-shadow: 1px 2px 4px #373737;
|
||||
background: #607d8b4d;
|
||||
}
|
||||
&:focus {
|
||||
border: 2px solid var(--primary);
|
||||
outline: none;
|
||||
}
|
||||
&.short {
|
||||
height: 18px;
|
||||
}
|
||||
.item {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* Text in tile */
|
||||
.tile-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 120px;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
span.text {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
transition: 1s;
|
||||
float: left;
|
||||
left: 0;
|
||||
}
|
||||
&:not(.is-overflowing) span.text{
|
||||
width: 100%;
|
||||
}
|
||||
.overflow-dots {
|
||||
opacity: 0;
|
||||
}
|
||||
&.is-overflowing {
|
||||
span.text {
|
||||
overflow: hidden;
|
||||
}
|
||||
.overflow-dots {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
// background: var(--background-transparent);
|
||||
background: #283e51;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
right: 0;
|
||||
transition: opacity 0.1s ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opening-method-icon {
|
||||
display: none; // Hidden by default, visible on hover
|
||||
}
|
||||
|
||||
/* Manage hover and focus actions */
|
||||
.item:hover, .item:focus {
|
||||
/* Show opening-method icon */
|
||||
.opening-method-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Trigger text-marquee for text that doesn't fit */
|
||||
.tile-title.is-overflowing{
|
||||
.overflow-dots {
|
||||
opacity: 0;
|
||||
}
|
||||
span.text {
|
||||
transform: translateX(calc(120px - 100%));
|
||||
}
|
||||
}
|
||||
|
||||
/* Colourize icons on hover */
|
||||
.tile-svg {
|
||||
filter: drop-shadow(4px 8px 3px var(--transparent-50));
|
||||
}
|
||||
.tile-icon {
|
||||
filter:
|
||||
drop-shadow(4px 8px 3px var(--transparent-50))
|
||||
saturate(2);
|
||||
}
|
||||
}
|
||||
|
||||
.tile-icon {
|
||||
width: 60px;
|
||||
filter: drop-shadow(2px 4px 6px var(--transparent-50)) saturate(0.65);
|
||||
}
|
||||
|
||||
.tile-svg {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- An un-scoped style tag, since tooltip is outside this DOM tree -->
|
||||
<style lang="scss">
|
||||
.tooltip {
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: #0b1021cc;
|
||||
border: 1px solid #0b1021;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
max-width: 250px;
|
||||
}
|
||||
.tooltip-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent!important;
|
||||
border-right-color: transparent!important;
|
||||
border-bottom-color: transparent!important;
|
||||
bottom: -11px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: #0b1021cc;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.disabled-link {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
90
src/components/LinkItems/ItemGroup.vue
Normal file
90
src/components/LinkItems/ItemGroup.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<Collapsable
|
||||
:title="title"
|
||||
:uniqueKey="groupId"
|
||||
:collapsed="displayData.collapsed"
|
||||
:cols="displayData.cols"
|
||||
:rows="displayData.rows"
|
||||
:color="displayData.color"
|
||||
:customStyles="displayData.customStyles"
|
||||
>
|
||||
<div v-if="!items || items.length < 1" class="no-items">
|
||||
No Items to Show Yet
|
||||
</div>
|
||||
<div v-else class="there-are-items">
|
||||
<Item
|
||||
v-for="(item, index) in items"
|
||||
:id="`${index}_${makeId(item.title)}`"
|
||||
:key="`${index}_${makeId(item.title)}`"
|
||||
:url="item.url"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:icon="item.icon"
|
||||
:target="item.target"
|
||||
:svg="item.svg"
|
||||
@itemClicked="$emit('itemClicked')"
|
||||
@triggerModal="triggerModal"
|
||||
/>
|
||||
<div ref="modalContainer"></div>
|
||||
</div>
|
||||
<IframeModal
|
||||
:ref="`iframeModal-${groupId}`"
|
||||
:name="`iframeModal-${groupId}`"
|
||||
@closed="$emit('itemClicked')"
|
||||
/>
|
||||
</Collapsable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Item from '@/components/LinkItems/Item.vue';
|
||||
import Collapsable from '@/components/LinkItems/Collapsable.vue';
|
||||
import IframeModal from '@/components/LinkItems/IframeModal.vue';
|
||||
|
||||
export default {
|
||||
name: 'ItemGroup',
|
||||
props: {
|
||||
groupId: String,
|
||||
title: String,
|
||||
displayData: Object,
|
||||
items: Array,
|
||||
},
|
||||
components: {
|
||||
Collapsable,
|
||||
Item,
|
||||
IframeModal,
|
||||
},
|
||||
methods: {
|
||||
/* Returns a unique lowercase string, based on name, for section ID */
|
||||
makeId(str) {
|
||||
return str.replace(/\s+/g, '-').replace(/[^a-zA-Z ]/g, '').toLowerCase();
|
||||
},
|
||||
/* Opens the iframe modal */
|
||||
triggerModal(url) {
|
||||
this.$refs[`iframeModal-${this.groupId}`].show(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../../src/styles/constants.scss';
|
||||
|
||||
.no-items {
|
||||
width: 100px;
|
||||
margin: 0 auto;
|
||||
padding: 0.8rem;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
border-radius: $curve-factor;
|
||||
background: #607d8b33;
|
||||
color: var(--primary);
|
||||
box-shadow: 1px 1px 2px #373737;
|
||||
}
|
||||
|
||||
.there-are-items {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
90
src/components/LinkItems/ItemIcon.vue
Normal file
90
src/components/LinkItems/ItemIcon.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<img
|
||||
v-if="icon"
|
||||
:src="getAppropriateImgPath(icon, url)"
|
||||
class="tile-icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
props: {
|
||||
icon: String,
|
||||
url: String,
|
||||
},
|
||||
methods: {
|
||||
/* Check if a string is in a URL format. Used to identify tile icon source */
|
||||
isUrl(str) {
|
||||
const pattern = new RegExp(/(http|https):\/\/(\w+:{0,1}\w*)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%!\-/]))?/);
|
||||
return pattern.test(str);
|
||||
},
|
||||
/* Returns true if the input is a path to an image file */
|
||||
isImage(img) {
|
||||
const fileExtRegex = /(?:\.([^.]+))?$/;
|
||||
const validImgExtensions = ['png', 'jpg'];
|
||||
const splitPath = fileExtRegex.exec(img);
|
||||
if (splitPath.length >= 1) return validImgExtensions.includes(splitPath[1]);
|
||||
return false;
|
||||
},
|
||||
/* Get favicon URL, for items which use the favicon as their icon */
|
||||
getFavicon(fullUrl) {
|
||||
const isLocalIP = /(127\.)|(192\.168\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(::1$)|([fF][cCdD])|(localhost)/;
|
||||
if (isLocalIP.test(fullUrl)) { // Check if using a local IP format or localhost
|
||||
const urlParts = fullUrl.split('/');
|
||||
// For locally running services, use the default path for favicon
|
||||
if (urlParts.length >= 2) return `${urlParts[0]}/${urlParts[1]}/${urlParts[2]}/favicon.ico`;
|
||||
} else if (fullUrl.includes('http')) {
|
||||
// For publicly accessible sites, a more reliable method is using Google's API
|
||||
return `https://s2.googleusercontent.com/s2/favicons?domain=${fullUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
/* Checks if the icon is from a local image, remote URL, SVG or font-awesome */
|
||||
getAppropriateImgPath(img, url) {
|
||||
const imageType = this.determineImageType(img);
|
||||
switch (imageType) {
|
||||
case 'url':
|
||||
return img;
|
||||
case 'img':
|
||||
return `/img/item-icons/tile-icons/${img}`;
|
||||
case 'favicon':
|
||||
return this.getFavicon(url);
|
||||
case 'svg':
|
||||
return img;
|
||||
case 'fas':
|
||||
return img;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
/* Checks if the icon is from a local image, remote URL, SVG or font-awesome */
|
||||
determineImageType(img) {
|
||||
let imgType = '';
|
||||
if (this.isUrl(img)) {
|
||||
imgType = 'url';
|
||||
} else if (this.isImage(img)) {
|
||||
imgType = 'img';
|
||||
// } else if (fileExtRegex.exec(img)[1] === 'svg') {
|
||||
// imgType = 'svg';
|
||||
} else if (img.includes('fas')) {
|
||||
imgType = 'fas';
|
||||
} else if (img === 'favicon') {
|
||||
imgType = 'favicon';
|
||||
} else {
|
||||
imgType = 'none';
|
||||
}
|
||||
return imgType;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.tile-icon {
|
||||
width: 60px;
|
||||
filter: drop-shadow(2px 4px 6px var(--transparent-50)) saturate(0.65);
|
||||
}
|
||||
</style>
|
||||
46
src/components/LinkItems/ItemOpenMethodIcon.vue
Normal file
46
src/components/LinkItems/ItemOpenMethodIcon.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div :class="`opening-method-icon ${isSmall? 'short': ''}`">
|
||||
<NewTabOpenIcon v-if="openingMethod === 'newtab'" />
|
||||
<SameTabOpenIcon v-else-if="openingMethod === 'sametab'" />
|
||||
<IframeOpenIcon v-else-if="openingMethod === 'iframe'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NewTabOpenIcon from '@/assets/icons/open-new-tab.svg';
|
||||
import SameTabOpenIcon from '@/assets/icons/open-current-tab.svg';
|
||||
import IframeOpenIcon from '@/assets/icons/open-iframe.svg';
|
||||
|
||||
export default {
|
||||
name: 'ItemOpenMethodIcon',
|
||||
props: {
|
||||
openingMethod: String,
|
||||
isSmall: Boolean,
|
||||
},
|
||||
components: {
|
||||
NewTabOpenIcon,
|
||||
SameTabOpenIcon,
|
||||
IframeOpenIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.opening-method-icon {
|
||||
svg {
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
margin: 2px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
path {
|
||||
fill: var(--primary-transparent);
|
||||
}
|
||||
}
|
||||
&.short svg {
|
||||
width: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user