diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-03-28 20:59:19 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-03-28 20:59:19 +1100 |
| commit | 712d9c14911ce1e470100c6cc41b14026c033574 (patch) | |
| tree | 7ec7d7531ea5d4d3e438226822d3e18c152495e6 /src | |
| parent | 50e1e7ae48daf04323093a438aff2d55ff20193d (diff) | |
Separate parts of Tile into LeafTile and NonLeafTiletest-leaftile-nonleaftile
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/LeafTile.vue | 115 | ||||
| -rw-r--r-- | src/components/NonLeafTile.vue | 205 | ||||
| -rw-r--r-- | src/components/Tile.vue | 205 |
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> |
