aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/LeafTile.vue115
-rw-r--r--src/components/NonLeafTile.vue205
-rw-r--r--src/components/Tile.vue205
3 files changed, 334 insertions, 191 deletions
diff --git a/src/components/LeafTile.vue b/src/components/LeafTile.vue
new file mode 100644
index 0000000..7448746
--- /dev/null
+++ b/src/components/LeafTile.vue
@@ -0,0 +1,115 @@
+<script lang="ts">
+import {defineComponent, PropType} from 'vue';
+import InfoIcon from './icon/InfoIcon.vue';
+import {LayoutNode} from '../layout';
+import type {LayoutOptions} from '../layout';
+
+export default defineComponent({
+ props: {
+ layoutNode: {type: Object as PropType<LayoutNode>, required: true},
+ lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
+ uiOpts: {type: Object, required: true},
+ nonAbsPos: {type: Boolean, default: false},
+ highlight: {type: Boolean, default: false},
+ inTransition: {type: Boolean, default: false},
+ },
+ computed: {
+ isExpandable(){
+ return this.layoutNode.tolNode.children.length > 0;
+ },
+ styles(): Record<string,string> {
+ let placementStyles;
+ if (!this.layoutNode.hidden){
+ placementStyles = {
+ position: 'absolute',
+ left: this.layoutNode.pos[0] + 'px',
+ top: this.layoutNode.pos[1] + 'px',
+ width: this.layoutNode.dims[0] + 'px',
+ height: this.layoutNode.dims[1] + 'px',
+ visibility: 'visible',
+ };
+ } else {
+ placementStyles = {
+ position: 'absolute',
+ left: '0',
+ top: '0',
+ width: '0',
+ height: '0',
+ visibility: 'hidden',
+ };
+ }
+ if (this.nonAbsPos){
+ placementStyles.position = 'static';
+ }
+ return {
+ ...placementStyles,
+ // Image
+ backgroundImage:
+ 'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 40%, rgba(0,0,0,0) 60%, rgba(0,0,0,0.4) 100%),' +
+ 'url(\'/img/' + this.layoutNode.tolNode.name.replaceAll('\'', '\\\'') + '.png\')',
+ backgroundSize: 'cover',
+ // Child layout
+ display: 'flex',
+ flexDirection: 'column',
+ // Transition related
+ transitionDuration: this.uiOpts.transitionDuration + 'ms',
+ transitionProperty: 'left, top, width, height, visibility',
+ transitionTimingFunction: 'ease-out',
+ zIndex: this.inTransition ? '1' : '0',
+ overflow: 'visible',
+ // Other
+ borderRadius: this.uiOpts.borderRadius + 'px',
+ boxShadow: this.highlight ? this.uiOpts.shadowHighlight :
+ (this.layoutNode.hasFocus ? this.uiOpts.shadowFocused : this.uiOpts.shadowNormal),
+ };
+ },
+ headerStyles(): Record<string,string> {
+ return {
+ height: (this.uiOpts.imgTileFontSz + this.uiOpts.imgTilePadding * 2) + 'px',
+ lineHeight: this.uiOpts.imgTileFontSz + 'px',
+ fontSize: this.uiOpts.imgTileFontSz + 'px',
+ padding: this.uiOpts.imgTilePadding + 'px',
+ color: this.isExpandable ? this.uiOpts.expandableImgTileColor : this.uiOpts.imgTileColor,
+ // For ellipsis
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ };
+ },
+ },
+ methods: {
+ onInfoClick(evt: Event){
+ this.$emit('info-icon-clicked', this.layoutNode);
+ },
+ onMouseEnter(){
+ this.$emit('tile-mouse-enter');
+ },
+ onMouseLeave(){
+ this.$emit('tile-mouse-leave');
+ },
+ onMouseDown(){
+ this.$emit('tile-mouse-down');
+ },
+ onMouseUp(){
+ this.$emit('tile-mouse-up');
+ },
+ },
+ components: {InfoIcon, },
+ emits: [
+ 'info-icon-clicked',
+ 'tile-mouse-enter', 'tile-mouse-leave', 'tile-mouse-down', 'tile-mouse-up',
+ ],
+});
+</script>
+
+<template>
+<div :style="styles" :class="isExpandable ? ['hover:cursor-pointer'] : []"
+ @mouseenter="onMouseEnter" @mouseleave="onMouseLeave"
+ @mousedown="onMouseDown" @mouseup="onMouseUp">
+ <h1 :style="headerStyles">{{layoutNode.tolNode.name}}</h1>
+ <info-icon
+ class="w-[18px] h-[18px] mt-auto mb-[2px] mr-[2px] self-end
+ text-white/30 hover:text-white hover:cursor-pointer"
+ @click.stop="onInfoClick" @mousedown.stop @mouseup.stop/>
+</div>
+</template>
diff --git a/src/components/NonLeafTile.vue b/src/components/NonLeafTile.vue
new file mode 100644
index 0000000..f9f2983
--- /dev/null
+++ b/src/components/NonLeafTile.vue
@@ -0,0 +1,205 @@
+<script lang="ts">
+import {defineComponent, PropType} from 'vue';
+import Tile from './Tile.vue';
+import {LayoutNode} from '../layout';
+import type {LayoutOptions} from '../layout';
+
+export default defineComponent({
+ props: {
+ layoutNode: {type: Object as PropType<LayoutNode>, required: true},
+ lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
+ uiOpts: {type: Object, required: true},
+ nonAbsPos: {type: Boolean, default: false},
+ highlight: {type: Boolean, default: false},
+ inTransition: {type: Boolean, default: false},
+ },
+ computed: {
+ showHeader(){
+ return (this.layoutNode.showHeader && !this.layoutNode.sepSweptArea) ||
+ (this.layoutNode.sepSweptArea && this.layoutNode.sepSweptArea.sweptLeft);
+ },
+ nonLeafBgColor(){
+ let colorArray = this.uiOpts.nonLeafBgColors;
+ return colorArray[this.layoutNode.depth % colorArray.length];
+ },
+ styles(): Record<string,string> {
+ let placementStyles;
+ if (!this.layoutNode.hidden){
+ placementStyles = {
+ position: 'absolute',
+ left: this.layoutNode.pos[0] + 'px',
+ top: this.layoutNode.pos[1] + 'px',
+ width: this.layoutNode.dims[0] + 'px',
+ height: this.layoutNode.dims[1] + 'px',
+ visibility: 'visible',
+ };
+ } else {
+ placementStyles = {
+ position: 'absolute',
+ left: '0',
+ top: '0',
+ width: '0',
+ height: '0',
+ visibility: 'hidden',
+ };
+ }
+ let borderR = this.uiOpts.borderRadius + 'px';
+ if (this.layoutNode.sepSweptArea != null){
+ borderR = this.layoutNode.sepSweptArea.sweptLeft ?
+ `${borderR} ${borderR} ${borderR} 0` :
+ `${borderR} 0 ${borderR} ${borderR}`;
+ }
+ return {
+ ...placementStyles,
+ // Transition related
+ transitionDuration: this.uiOpts.transitionDuration + 'ms',
+ transitionProperty: 'left, top, width, height, visibility',
+ transitionTimingFunction: 'ease-out',
+ zIndex: this.inTransition ? '1' : '0',
+ overflow: this.inTransition ? 'hidden' : 'visible',
+ // CSS variables
+ '--nonLeafBgColor': this.nonLeafBgColor,
+ '--tileSpacing': this.lytOpts.tileSpacing + 'px',
+ // Other
+ borderRadius: borderR,
+ backgroundColor: this.nonLeafBgColor,
+ boxShadow: this.inTransition ? 'none' :
+ (this.highlight ? this.uiOpts.shadowHighlight :
+ (this.layoutNode.hasFocus ? this.uiOpts.shadowFocused : this.uiOpts.shadowNormal)),
+ };
+ },
+ headerStyles(): Record<string,string> {
+ let borderR = this.uiOpts.borderRadius + 'px';
+ return {
+ height: this.lytOpts.headerSz + 'px',
+ lineHeight: this.lytOpts.headerSz + 'px',
+ fontSize: this.uiOpts.nonLeafHeaderFontSz + 'px',
+ textAlign: 'center',
+ color: this.uiOpts.nonLeafHeaderColor,
+ backgroundColor: this.uiOpts.nonLeafHeaderBgColor,
+ borderRadius: `${borderR} ${borderR} 0 0`,
+ // For ellipsis
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ };
+ },
+ sepSweptAreaStyles(): Record<string,string> {
+ let commonStyles = {
+ position: 'absolute',
+ backgroundColor: this.nonLeafBgColor,
+ boxShadow: this.inTransition ? 'none' :
+ (this.highlight ? this.uiOpts.shadowHighlight :
+ (this.layoutNode.hasFocus ? this.uiOpts.shadowFocused : this.uiOpts.shadowNormal)),
+ transitionDuration: this.uiOpts.transitionDuration + 'ms',
+ transitionProperty: 'left, top, width, height',
+ transitionTimingFunction: 'ease-out',
+ };
+ let area = this.layoutNode.sepSweptArea;
+ if (this.layoutNode.hidden || area == null){
+ return {
+ ...commonStyles,
+ visibility: 'hidden',
+ left: '0',
+ top: this.lytOpts.headerSz + 'px',
+ width: '0',
+ height: '0',
+ };
+ } else {
+ let borderR = this.uiOpts.borderRadius + 'px';
+ return {
+ ...commonStyles,
+ left: area.pos[0] + 'px',
+ top: area.pos[1] + 'px',
+ width: area.dims[0] + 'px',
+ height: area.dims[1] + 'px',
+ borderRadius: area.sweptLeft ?
+ `${borderR} 0 0 ${borderR}` :
+ `${borderR} ${borderR} 0 0`,
+ };
+ }
+ },
+ },
+ methods: {
+ onMouseEnter(){
+ this.$emit('tile-mouse-enter');
+ },
+ onMouseLeave(){
+ this.$emit('tile-mouse-leave');
+ },
+ onMouseDown(){
+ this.$emit('tile-mouse-down');
+ },
+ onMouseUp(){
+ this.$emit('tile-mouse-up');
+ },
+ // Child event propagation
+ onInnerLeafClicked(data: LayoutNode){
+ this.$emit('leaf-clicked', data);
+ },
+ onInnerHeaderClicked(data: LayoutNode){
+ this.$emit('header-clicked', data);
+ },
+ onInnerLeafClickHeld(data: LayoutNode){
+ this.$emit('leaf-click-held', data);
+ },
+ onInnerHeaderClickHeld(data: LayoutNode){
+ this.$emit('header-click-held', data);
+ },
+ onInnerInfoIconClicked(data: LayoutNode){
+ this.$emit('info-icon-clicked', data);
+ },
+ },
+ name: 'non-leaf-tile', // Need this to use self in template
+ emits: [
+ 'leaf-clicked', 'header-clicked', 'leaf-click-held', 'header-click-held', 'info-icon-clicked',
+ 'tile-mouse-enter', 'tile-mouse-leave', 'tile-mouse-down', 'tile-mouse-up',
+ ],
+ components: {Tile, },
+});
+</script>
+
+<template>
+<div :style="styles">
+ <h1 v-if="showHeader" :style="headerStyles" class="hover:cursor-pointer"
+ @mouseenter="onMouseEnter" @mouseleave="onMouseLeave"
+ @mousedown="onMouseDown" @mouseup="onMouseUp">
+ {{layoutNode.tolNode.name}}
+ </h1>
+ <div :style="sepSweptAreaStyles"
+ :class="layoutNode?.sepSweptArea?.sweptLeft ? 'hide-right-edge' : 'hide-top-edge'">
+ <h1 v-if="layoutNode?.sepSweptArea?.sweptLeft === false"
+ :style="headerStyles" class="hover:cursor-pointer"
+ @mouseenter="onMouseEnter" @mouseleave="onMouseLeave"
+ @mousedown="onMouseDown" @mouseup="onMouseUp">
+ {{layoutNode.tolNode.name}}
+ </h1>
+ </div>
+ <tile v-for="child in layoutNode.children" :key="child.tolNode.name"
+ :layoutNode="child" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ @leaf-clicked="onInnerLeafClicked" @header-clicked="onInnerHeaderClicked"
+ @leaf-click-held="onInnerLeafClickHeld" @header-click-held="onInnerHeaderClickHeld"
+ @info-icon-clicked="onInnerInfoIconClicked"/>
+</div>
+</template>
+
+<style>
+.hide-right-edge::before {
+ content: '';
+ position: absolute;
+ background-color: var(--nonLeafBgColor);
+ right: calc(0px - var(--tileSpacing));
+ bottom: 0;
+ width: var(--tileSpacing);
+ height: calc(100% + var(--tileSpacing));
+}
+.hide-top-edge::before {
+ content: '';
+ position: absolute;
+ background-color: var(--nonLeafBgColor);
+ bottom: calc(0px - var(--tileSpacing));
+ right: 0;
+ width: calc(100% + var(--tileSpacing));
+ height: var(--tileSpacing);
+}
+</style>
diff --git a/src/components/Tile.vue b/src/components/Tile.vue
index 26ece88..56709db 100644
--- a/src/components/Tile.vue
+++ b/src/components/Tile.vue
@@ -1,6 +1,7 @@
<script lang="ts">
import {defineComponent, PropType} from 'vue';
-import InfoIcon from './icon/InfoIcon.vue';
+import LeafTile from './LeafTile.vue';
+import NonLeafTile from './NonLeafTile.vue';
import {LayoutNode} from '../layout';
import type {LayoutOptions} from '../layout';
@@ -14,10 +15,10 @@ export default defineComponent({
},
data(){
return {
- highlight: false,
clickHoldTimer: 0, // Used to recognise a click-and-hold event
- animating: false, // Used to prevent content overlap and overflow during transitions
- }
+ highlight: false,
+ inTransition: false,
+ };
},
computed: {
isLeaf(){
@@ -26,131 +27,6 @@ export default defineComponent({
isExpandable(){
return this.layoutNode.tolNode.children.length > this.layoutNode.children.length;
},
- showHeader(){
- return (this.layoutNode.showHeader && !this.layoutNode.sepSweptArea) ||
- (this.layoutNode.sepSweptArea && this.layoutNode.sepSweptArea.sweptLeft);
- },
- nonLeafBgColor(){
- let colorArray = this.uiOpts.nonLeafBgColors;
- return colorArray[this.layoutNode.depth % colorArray.length];
- },
- tileStyles(): Record<string,string> {
- return {
- // Places div using layoutNode, with centering if root
- position: this.nonAbsPos && this.isLeaf ? 'static' : 'absolute',
- left: (this.layoutNode.hidden ? 0 : this.layoutNode.pos[0]) + 'px',
- top: (this.layoutNode.hidden ? 0 : this.layoutNode.pos[1]) + 'px',
- width: (this.layoutNode.hidden ? 0 : this.layoutNode.dims[0]) + 'px',
- height: (this.layoutNode.hidden ? 0 : this.layoutNode.dims[1]) + 'px',
- visibility: this.layoutNode.hidden ? 'hidden' : 'visible',
- // Other bindings
- transitionDuration: this.uiOpts.transitionDuration + 'ms',
- zIndex: this.animating ? '1' : '0',
- overflow: this.animating && !this.isLeaf ? 'hidden' : 'visible',
- // Static styles
- transitionProperty: 'left, top, width, height, visibility',
- transitionTimingFunction: 'ease-out',
- // CSS variables
- '--nonLeafBgColor': this.nonLeafBgColor,
- '--tileSpacing': this.lytOpts.tileSpacing + 'px',
- };
- },
- leafStyles(): Record<string,string> {
- return {
- width: '100%',
- height: '100%',
- // Image
- backgroundImage:
- 'linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(0,0,0,0) 40%, rgba(0,0,0,0) 60%, rgba(0,0,0,0.4) 100%),' +
- 'url(\'/img/' + this.layoutNode.tolNode.name.replaceAll('\'', '\\\'') + '.png\')',
- backgroundSize: 'cover',
- // Child layout
- display: 'flex',
- flexDirection: 'column',
- // Other
- borderRadius: this.uiOpts.borderRadius + 'px',
- boxShadow: this.highlight ? this.uiOpts.shadowHighlight :
- (this.layoutNode.hasFocus ? this.uiOpts.shadowFocused : this.uiOpts.shadowNormal),
- };
- },
- leafHeaderStyles(): Record<string,string> {
- return {
- height: (this.uiOpts.imgTileFontSz + this.uiOpts.imgTilePadding * 2) + 'px',
- lineHeight: this.uiOpts.imgTileFontSz + 'px',
- fontSize: this.uiOpts.imgTileFontSz + 'px',
- padding: this.uiOpts.imgTilePadding + 'px',
- color: this.isExpandable ? this.uiOpts.expandableImgTileColor : this.uiOpts.imgTileColor,
- // For ellipsis
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- };
- },
- nonLeafStyles(): Record<string,string> {
- let temp = {
- width: '100%',
- height: '100%',
- backgroundColor: this.nonLeafBgColor,
- borderRadius: this.uiOpts.borderRadius + 'px',
- boxShadow: this.animating ? 'none' : (this.highlight ? this.uiOpts.shadowHighlight :
- (this.layoutNode.hasFocus ? this.uiOpts.shadowFocused : this.uiOpts.shadowNormal)),
- };
- if (this.layoutNode.sepSweptArea != null){
- let r = this.uiOpts.borderRadius + 'px';
- temp = this.layoutNode.sepSweptArea.sweptLeft ?
- {...temp, borderRadius: `${r} ${r} ${r} 0`} :
- {...temp, borderRadius: `${r} 0 ${r} ${r}`};
- }
- return temp;
- },
- nonLeafHeaderStyles(): Record<string,string> {
- let r = this.uiOpts.borderRadius + 'px';
- return {
- height: this.lytOpts.headerSz + 'px',
- lineHeight: this.lytOpts.headerSz + 'px',
- fontSize: this.uiOpts.nonLeafHeaderFontSz + 'px',
- textAlign: 'center',
- color: this.uiOpts.nonLeafHeaderColor,
- backgroundColor: this.uiOpts.nonLeafHeaderBgColor,
- borderRadius: `${r} ${r} 0 0`,
- // For ellipsis
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- };
- },
- sepSweptAreaStyles(): Record<string,string> {
- let commonStyles = {
- position: 'absolute',
- backgroundColor: this.nonLeafBgColor,
- boxShadow: this.animating ? 'none' : (this.highlight ? this.uiOpts.shadowHighlight :
- (this.layoutNode.hasFocus ? this.uiOpts.shadowFocused : this.uiOpts.shadowNormal)),
- transitionDuration: this.uiOpts.transitionDuration + 'ms',
- transitionProperty: 'left, top, width, height',
- transitionTimingFunction: 'ease-out',
- };
- let area = this.layoutNode.sepSweptArea;
- if (this.layoutNode.hidden || area == null){
- return {
- ...commonStyles,
- visibility: 'hidden',
- left: '0',
- top: this.lytOpts.headerSz + 'px',
- width: '0',
- height: '0',
- };
- } else {
- let r = this.uiOpts.borderRadius + 'px';
- return {
- ...commonStyles,
- left: area.pos[0] + 'px',
- top: area.pos[1] + 'px',
- width: area.dims[0] + 'px',
- height: area.dims[1] + 'px',
- borderRadius: area.sweptLeft ? `${r} 0 0 ${r}` : `${r} ${r} 0 0`,
- };
- }
- },
collapseFailFlag(){
return this.layoutNode.collapseFailFlag;
},
@@ -208,11 +84,8 @@ export default defineComponent({
}
},
prepForTransition(){
- this.animating = true;
- setTimeout(() => {this.animating = false}, this.uiOpts.transitionDuration);
- },
- onInfoClick(evt: Event){
- this.$emit('info-icon-clicked', this.layoutNode);
+ this.inTransition = true;
+ setTimeout(() => {this.inTransition = false}, this.uiOpts.transitionDuration);
},
// For coloured outlines on hover
onMouseEnter(evt: Event){
@@ -241,72 +114,22 @@ export default defineComponent({
onInnerInfoIconClicked(data: LayoutNode){
this.$emit('info-icon-clicked', data);
},
- //
+ // Other
triggerAnimation(animationClass: string){
this.$el.classList.remove(animationClass);
this.$el.offsetWidth; // Triggers reflow
this.$el.classList.add(animationClass);
},
},
- name: 'tile', // Need this to use self in template
- components: {InfoIcon, },
emits: ['leaf-clicked', 'header-clicked', 'leaf-click-held', 'header-click-held', 'info-icon-clicked'],
+ components: {LeafTile, NonLeafTile, },
});
</script>
<template>
-<div :style="tileStyles">
- <div v-if="isLeaf" :style="leafStyles" :class="isExpandable ? ['hover:cursor-pointer'] : []"
- @mouseenter="onMouseEnter" @mouseleave="onMouseLeave"
- @mousedown="onMouseDown" @mouseup="onMouseUp">
- <h1 :style="leafHeaderStyles">{{layoutNode.tolNode.name}}</h1>
- <info-icon
- class="w-[18px] h-[18px] mt-auto mb-[2px] mr-[2px] self-end
- text-white/30 hover:text-white hover:cursor-pointer"
- @mouseenter="onInfoMouseEnter" @mouseleave="onInfoMouseLeave"
- @click.stop="onInfoClick" @mousedown.stop @mouseup.stop/>
- </div>
- <div v-else :style="nonLeafStyles" ref="nonLeaf">
- <h1 v-if="showHeader" :style="nonLeafHeaderStyles" class="hover:cursor-pointer"
- @mouseenter="onMouseEnter" @mouseleave="onMouseLeave"
- @mousedown="onMouseDown" @mouseup="onMouseUp">
- {{layoutNode.tolNode.name}}
- </h1>
- <div :style="sepSweptAreaStyles" ref="sepSweptArea"
- :class="layoutNode?.sepSweptArea?.sweptLeft ? 'hide-right-edge' : 'hide-top-edge'">
- <h1 v-if="layoutNode?.sepSweptArea?.sweptLeft === false"
- :style="nonLeafHeaderStyles" class="hover:cursor-pointer"
- @mouseenter="onMouseEnter" @mouseleave="onMouseLeave"
- @mousedown="onMouseDown" @mouseup="onMouseUp">
- {{layoutNode.tolNode.name}}
- </h1>
- </div>
- <tile v-for="child in layoutNode.children" :key="child.tolNode.name" :layoutNode="child"
- :lytOpts="lytOpts" :uiOpts="uiOpts"
- @leaf-clicked="onInnerLeafClicked" @header-clicked="onInnerHeaderClicked"
- @leaf-click-held="onInnerLeafClickHeld" @header-click-held="onInnerHeaderClickHeld"
- @info-icon-clicked="onInnerInfoIconClicked"/>
- </div>
-</div>
+<component :is="isLeaf ? LeafTile : NonLeafTile"
+ :layoutNode="layoutNode" :lytOpts="lytOpts" :uiOpts="uiOpts" :nonAbsPos="nonAbsPos"
+ :highlight="highlight" :inTransition="inTransition"
+ @tile-mouse-enter="onMouseEnter" @tile-mouse-leave="onMouseLeave"
+ @tile-mouse-down="onMouseDown" @tile-mouse-up="onMouseUp"/>
</template>
-
-<style>
-.hide-right-edge::before {
- content: '';
- position: absolute;
- background-color: var(--nonLeafBgColor);
- right: calc(0px - var(--tileSpacing));
- bottom: 0;
- width: var(--tileSpacing);
- height: calc(100% + var(--tileSpacing));
-}
-.hide-top-edge::before {
- content: '';
- position: absolute;
- background-color: var(--nonLeafBgColor);
- bottom: calc(0px - var(--tileSpacing));
- right: 0;
- width: calc(100% + var(--tileSpacing));
- height: var(--tileSpacing);
-}
-</style>