Improved layout for items and sub-items

This commit is contained in:
Alicia Sykes 2022-04-06 00:04:47 +01:00
parent 151028c8cf
commit 57abd67cf9
5 changed files with 143 additions and 52 deletions

View File

@ -1,5 +1,5 @@
<template ref="container"> <template ref="container">
<div :class="`item-wrapper wrap-size-${size}`" > <div :class="`item-wrapper wrap-size-${size} span-${makeColumnCount}`" >
<a @click="itemClicked" <a @click="itemClicked"
@long-press="openContextMenu" @long-press="openContextMenu"
@contextmenu.prevent @contextmenu.prevent
@ -11,8 +11,7 @@
v-tooltip="getTooltipOptions()" v-tooltip="getTooltipOptions()"
rel="noopener noreferrer" tabindex="0" rel="noopener noreferrer" tabindex="0"
:id="`link-${item.id}`" :id="`link-${item.id}`"
:style= :style="customStyle"
"`--open-icon:${unicodeOpeningIcon};color:${item.color};background:${item.backgroundColor}`"
> >
<!-- Item Text --> <!-- Item Text -->
<div :class="`tile-title ${!item.icon? 'bounce no-icon': ''}`" :id="`tile-${item.id}`" > <div :class="`tile-title ${!item.icon? 'bounce no-icon': ''}`" :id="`tile-${item.id}`" >
@ -77,6 +76,8 @@ export default {
itemSize: String, itemSize: String,
parentSectionTitle: String, // Title of parent section (for add new) parentSectionTitle: String, // Title of parent section (for add new)
isAddNew: Boolean, // Only set if 'fake' item used as Add New button isAddNew: Boolean, // Only set if 'fake' item used as Add New button
sectionWidth: Number, // Width of parent section
sectionDisplayData: Object,
}, },
components: { components: {
Icon, Icon,
@ -88,6 +89,15 @@ export default {
EditModeIcon, EditModeIcon,
}, },
computed: { computed: {
makeColumnCount() {
if ((this.sectionDisplayData || {}).itemCountX) return this.sectionDisplayData.itemCountX;
if (this.sectionWidth < 380) return 1;
if (this.sectionWidth < 520) return 2;
if (this.sectionWidth < 730) return 3;
if (this.sectionWidth < 1000) return 4;
if (this.sectionWidth < 1300) return 5;
return 0;
},
/* Based on item props, adjust class names */ /* Based on item props, adjust class names */
makeClassList() { makeClassList() {
const { isAddNew, isEditMode, size } = this; const { isAddNew, isEditMode, size } = this;
@ -183,6 +193,17 @@ export default {
&.wrap-size-large { &.wrap-size-large {
flex-basis: 12rem; flex-basis: 12rem;
} }
&.wrap-size-small {
flex-grow: revert;
&.span-1 { min-width: 100%; }
&.span-2 { min-width: 50%; }
&.span-3 { min-width: 33%; }
&.span-4 { min-width: 25%; }
&.span-5 { min-width: 20%; }
&.span-6 { min-width: 16%; }
&.span-7 { min-width: 14%; }
&.span-8 { min-width: 12.5%; }
}
} }
.item { .item {
@ -292,7 +313,6 @@ p.description {
align-items: center; align-items: center;
height: 2rem; height: 2rem;
padding-top: 4px; padding-top: 4px;
max-width: 14rem;
div img { div img {
width: 2rem; width: 2rem;
} }

View File

@ -77,6 +77,7 @@ export default {
posX: Number, // The X coordinate for positioning posX: Number, // The X coordinate for positioning
posY: Number, // The Y coordinate for positioning posY: Number, // The Y coordinate for positioning
show: Boolean, // Should show or hide the menu show: Boolean, // Should show or hide the menu
disableEdit: Boolean, // Disable editing for certain items
}, },
computed: { computed: {
isMenuDisabled() { isMenuDisabled() {
@ -86,6 +87,7 @@ export default {
return this.$store.state.editMode; return this.$store.state.editMode;
}, },
isEditAllowed() { isEditAllowed() {
if (this.disableEdit) return false;
return this.$store.getters.permissions.allowViewConfig; return this.$store.getters.permissions.allowViewConfig;
}, },
}, },

View File

@ -11,7 +11,8 @@
:cutToHeight="displayData.cutToHeight" :cutToHeight="displayData.cutToHeight"
@openEditSection="openEditSection" @openEditSection="openEditSection"
@openContextMenu="openContextMenu" @openContextMenu="openContextMenu"
:id="`section-outer-${groupId}`" :id="sectionRef"
:ref="sectionRef"
> >
<!-- If no items, show message --> <!-- If no items, show message -->
<div v-if="isEmpty" class="no-items"> <div v-if="isEmpty" class="no-items">
@ -23,17 +24,13 @@
:style="gridStyle" :id="`section-${groupId}`" :style="gridStyle" :id="`section-${groupId}`"
> <!-- Show for each item --> > <!-- Show for each item -->
<template v-for="(item) in sortedItems"> <template v-for="(item) in sortedItems">
<div v-if="item.subItems" :key="item.id" class="sub-items-group"> <SubItemGroup
<template v-for="(subItem, subIndex) in item.subItems"> v-if="item.subItems"
<SubItem :key="item.id"
:key="subIndex" :itemId="item.id"
:id="`${item.id}-sub-${subIndex}`" :title="item.title"
:url="subItem.url" :subItems="item.subItems"
:icon="subItem.icon" />
:title="subItem.title"
/>
</template>
</div>
<Item <Item
v-else v-else
:item="item" :item="item"
@ -43,6 +40,8 @@
@itemClicked="$emit('itemClicked')" @itemClicked="$emit('itemClicked')"
@triggerModal="triggerModal" @triggerModal="triggerModal"
:isAddNew="false" :isAddNew="false"
:sectionWidth="sectionWidth"
:sectionDisplayData="displayData"
/> />
</template> </template>
<!-- When in edit mode, show additional item, for Add New item --> <!-- When in edit mode, show additional item, for Add New item -->
@ -101,7 +100,7 @@
<script> <script>
import router from '@/router'; import router from '@/router';
import Item from '@/components/LinkItems/Item.vue'; import Item from '@/components/LinkItems/Item.vue';
import SubItem from '@/components/LinkItems/SubItem.vue'; import SubItemGroup from '@/components/LinkItems/SubItemGroup.vue';
import WidgetBase from '@/components/Widgets/WidgetBase'; import WidgetBase from '@/components/Widgets/WidgetBase';
import Collapsable from '@/components/LinkItems/Collapsable.vue'; import Collapsable from '@/components/LinkItems/Collapsable.vue';
import IframeModal from '@/components/LinkItems/IframeModal.vue'; import IframeModal from '@/components/LinkItems/IframeModal.vue';
@ -131,7 +130,7 @@ export default {
Collapsable, Collapsable,
ContextMenu, ContextMenu,
Item, Item,
SubItem, SubItemGroup,
WidgetBase, WidgetBase,
IframeModal, IframeModal,
EditSection, EditSection,
@ -144,6 +143,8 @@ export default {
posX: undefined, posX: undefined,
posY: undefined, posY: undefined,
}, },
sectionWidth: 0,
resizeObserver: null,
}; };
}, },
computed: { computed: {
@ -169,6 +170,9 @@ export default {
isEmpty() { isEmpty() {
return !this.hasItems && !this.hasWidgets; return !this.hasItems && !this.hasWidgets;
}, },
sectionRef() {
return `section-outer-${this.groupId}`;
},
/* If the sortBy attribute is specified, then return sorted data */ /* If the sortBy attribute is specified, then return sorted data */
sortedItems() { sortedItems() {
let { items } = this; let { items } = this;
@ -294,6 +298,22 @@ export default {
closeContextMenu() { closeContextMenu() {
this.contextMenuOpen = false; this.contextMenuOpen = false;
}, },
/* Calculate width of section, used to dynamically set number of columns */
calculateSectionWidth() {
const secElem = this.$refs[this.sectionRef];
if (secElem) this.sectionWidth = secElem.$el.clientWidth;
},
},
mounted() {
// Set the section width, and recalculate when section resized
this.resizeObserver = new ResizeObserver(this.calculateSectionWidth)
.observe(this.$refs[this.sectionRef].$el);
},
beforeDestroy() {
// If resize observer set, and element still present, then de-register
if (this.resizeObserver && this.$refs[this.sectionRef]) {
this.resizeObserver.unobserve(this.$refs[this.sectionRef].$el);
}
}, },
}; };
</script> </script>
@ -375,18 +395,4 @@ export default {
} }
} }
.sub-items-group {
display: grid;
margin: 0.5rem;
padding: 0.1rem;
flex-grow: 1;
flex-basis: 6rem;
grid-template-columns: repeat(3, minmax(0, 1fr));
color: var(--item-text-color);
border: 1px solid var(--outline-color);
border-radius: var(--curve-factor);
text-decoration: none;
transition: all 0.2s ease-in-out 0s;
}
</style> </style>

View File

@ -1,21 +1,36 @@
<template ref="container"> <template ref="container">
<div class="sub-item-wrapper"> <div class="sub-item-wrapper">
<a @click="beforeLaunchItem" <a @click="itemClicked"
@contextmenu.prevent
@long-press="openContextMenu"
@mouseup.right="openContextMenu"
v-longPress="true"
:href="hyperLinkHref" :href="hyperLinkHref"
:target="anchorTarget" :target="anchorTarget"
class="sub-item-link item"
v-tooltip="subItemTooltip" v-tooltip="subItemTooltip"
rel="noopener noreferrer" tabindex="0" rel="noopener noreferrer" tabindex="0"
:id="`link-${id}`" :id="`link-${id}`"
class="sub-item-link item"
> >
<!-- Item Icon --> <!-- Item Icon -->
<Icon :icon="icon" :url="url" size="small" class="sub-icon-img bounce" /> <Icon :icon="icon" :url="url" size="small" class="sub-icon-img bounce" />
</a> </a>
<!-- Right-click context menu -->
<ContextMenu
:show="contextMenuOpen && !isAddNew"
v-click-outside="closeContextMenu"
:posX="contextPos.posX"
:posY="contextPos.posY"
:id="`context-menu-${id}`"
:disableEdit="true"
@launchItem="launchItem"
/>
</div> </div>
</template> </template>
<script> <script>
import Icon from '@/components/LinkItems/ItemIcon.vue'; import Icon from '@/components/LinkItems/ItemIcon.vue';
import ContextMenu from '@/components/LinkItems/ItemContextMenu';
import ItemMixin from '@/mixins/ItemMixin'; import ItemMixin from '@/mixins/ItemMixin';
import { targetValidator } from '@/utils/ConfigHelpers'; import { targetValidator } from '@/utils/ConfigHelpers';
@ -34,6 +49,7 @@ export default {
}, },
components: { components: {
Icon, Icon,
ContextMenu,
}, },
computed: { computed: {
subItemTooltip() { subItemTooltip() {
@ -43,23 +59,7 @@ export default {
data() { data() {
return {}; return {};
}, },
methods: { methods: {},
beforeLaunchItem(e) {
if (this.isEditMode) return;
if (e.altKey) {
e.preventDefault();
this.launchItem('modal');
} else if (this.accumulatedTarget === 'modal') {
this.launchItem('modal');
} else if (this.accumulatedTarget === 'workspace') {
this.launchItem('workspace');
} else if (this.accumulatedTarget === 'clipboard') {
this.launchItem('clipboard');
}
// Clear search bar
this.$emit('itemClicked');
},
},
}; };
</script> </script>
@ -68,7 +68,7 @@ export default {
flex-grow: 1; flex-grow: 1;
flex-basis: 6rem; flex-basis: 6rem;
display: flex; display: flex;
a { a.sub-item-link {
border: none; border: none;
margin: 0.2rem; margin: 0.2rem;
.sub-icon-img { .sub-icon-img {

View File

@ -0,0 +1,63 @@
<template>
<div class="sub-items-group" :style="`--sub-item-col-count: ${columnCount}`">
<p v-if="title" class="sub-item-group-title">{{ title }}</p>
<SubItem
v-for="(subItem, subIndex) in subItems"
:key="subIndex"
:id="`${itemId}-sub-${subIndex}`"
:url="subItem.url"
:icon="subItem.icon"
:title="subItem.title"
/>
</div>
</template>
<script>
import SubItem from '@/components/LinkItems/SubItem.vue';
export default {
props: {
itemId: String,
subItems: Array,
title: String,
subItemGridSize: Number,
},
components: {
SubItem,
},
computed: {
/* Determine number of columns to split items into, depending on number of items */
columnCount() {
if (this.subItemGridSize) return this.subItemGridSize;
const numItems = this.subItems.length;
if (numItems >= 10) return 4;
if (numItems >= 5) return 3;
if (numItems >= 2) return 2;
if (numItems === 1) return 1;
return 2;
},
},
};
</script>
<style scoped lang="scss">
.sub-items-group {
display: grid;
margin: 0.5rem;
padding: 0.1rem;
flex-grow: 1;
flex-basis: 6rem;
grid-template-columns: repeat(var(--sub-item-col-count, 3), minmax(0, 1fr));
color: var(--item-text-color);
border: 1px solid var(--outline-color);
border-radius: var(--curve-factor);
text-decoration: none;
transition: all 0.2s ease-in-out 0s;
color: var(--item-text-color);
p.sub-item-group-title {
margin: 0 auto;
cursor: default;
grid-column-start: span var(--sub-item-col-count, 3);
}
}
</style>