diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-03-29 11:39:33 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-03-29 13:30:50 +1100 |
| commit | a68a55205ed189250693368af7028031a70631d9 (patch) | |
| tree | 4d780d1b450a2a3a51866103e80c53a818a8cb23 /src/components/Tile.vue | |
| parent | 304274b1380f955b6e1913428f6dbbc9efcf0bcf (diff) | |
Update comments, adjust names, do minor refactors
Diffstat (limited to 'src/components/Tile.vue')
| -rw-r--r-- | src/components/Tile.vue | 360 |
1 files changed, 201 insertions, 159 deletions
diff --git a/src/components/Tile.vue b/src/components/Tile.vue index 9e10615..a17869b 100644 --- a/src/components/Tile.vue +++ b/src/components/Tile.vue @@ -4,166 +4,191 @@ import InfoIcon from './icon/InfoIcon.vue'; import {LayoutNode} from '../layout'; import type {LayoutOptions} from '../layout'; -// Component holds a tree-node structure representing a tile or tile-group to be rendered +// Displays one, or a hierarchy of, tree-of-life nodes, as a 'tile' export default defineComponent({ props: { + // A LayoutNode representing a laid-out tree-of-life node to display layoutNode: {type: Object as PropType<LayoutNode>, required: true}, + // Options lytOpts: {type: Object as PropType<LayoutOptions>, required: true}, uiOpts: {type: Object, required: true}, - nonAbsPos: {type: Boolean, default: false}, // Don't use absolute positioning (only applies for leaf nodes) + // For a leaf node, prevents usage of absolute positioning (used by AncestryBar) + nonAbsPos: {type: Boolean, default: false}, }, 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, // Used to draw a colored outline on mouse hover, etc + inTransition: false, // Used to prevent content overlap and overflow during transitions + clickHoldTimer: 0, // Used to recognise click-and-hold events + }; }, computed: { - isLeaf(){ + // Basic abbreviations + isLeaf(): boolean { return this.layoutNode.children.length == 0; }, - isExpandable(){ - return this.layoutNode.tolNode.children.length > this.layoutNode.children.length; + isExpandableLeaf(): boolean { + return this.isLeaf && this.layoutNode.tolNode.children.length > 0; }, - showHeader(){ - return (this.layoutNode.showHeader && !this.layoutNode.sepSweptArea) || - (this.layoutNode.sepSweptArea && this.layoutNode.sepSweptArea.sweptLeft); + showNonleafHeader(): boolean { + return (this.layoutNode.showHeader && this.layoutNode.sepSweptArea == null) || + (this.layoutNode.sepSweptArea != null && this.layoutNode.sepSweptArea.sweptLeft); }, - nonLeafBgColor(){ - let colorArray = this.uiOpts.nonLeafBgColors; + // Style related + nonleafBgColor(): string { + let colorArray = this.uiOpts.nonleafBgColors; return colorArray[this.layoutNode.depth % colorArray.length]; }, - tileStyles(): Record<string,string> { + boxShadow(): string { + if (this.highlight){ + return this.uiOpts.shadowHighlight; + } else if (this.layoutNode.hasFocus && !this.inTransition){ + return this.uiOpts.shadowFocused; + } else { + return this.uiOpts.shadowNormal; + } + }, + styles(): Record<string,string> { + let layoutStyles = { + 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', + }; + if (this.layoutNode.hidden){ + layoutStyles.left = layoutStyles.top = layoutStyles.width = layoutStyles.height = '0'; + layoutStyles.visibility = 'hidden'; + } + if (this.nonAbsPos){ + layoutStyles.position = 'static'; + } 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 + ...layoutStyles, + // Transition related + transitionDuration: this.uiOpts.tileChgDuration + 'ms', transitionProperty: 'left, top, width, height, visibility', transitionTimingFunction: 'ease-out', + zIndex: this.inTransition ? '1' : '0', + overflow: this.inTransition && !this.isLeaf ? 'hidden' : 'visible', // CSS variables - '--nonLeafBgColor': this.nonLeafBgColor, + '--nonleafBgColor': this.nonleafBgColor, '--tileSpacing': this.lytOpts.tileSpacing + 'px', }; }, leafStyles(): Record<string,string> { return { - width: '100%', - height: '100%', - // Image + // Image (and scrims) 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%),' + + 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 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), + boxShadow: this.boxShadow, }; }, 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, + height: (this.uiOpts.leafHeaderFontSz + this.uiOpts.leafTilePadding * 2) + 'px', + padding: this.uiOpts.leafTilePadding + 'px', + lineHeight: this.uiOpts.leafHeaderFontSz + 'px', + fontSize: this.uiOpts.leafHeaderFontSz + 'px', + color: !this.isExpandableLeaf ? this.uiOpts.leafHeaderColor : this.uiOpts.leafHeaderExColor, // 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)), - }; + nonleafStyles(): Record<string,string> { + let borderR = this.uiOpts.borderRadius + 'px'; 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}`}; + borderR = this.layoutNode.sepSweptArea.sweptLeft ? + `${borderR} ${borderR} ${borderR} 0` : + `${borderR} 0 ${borderR} ${borderR}`; } - return temp; + return { + backgroundColor: this.nonleafBgColor, + borderRadius: borderR, + boxShadow: this.boxShadow, + }; }, - nonLeafHeaderStyles(): Record<string,string> { - let r = this.uiOpts.borderRadius + 'px'; + nonleafHeaderStyles(): Record<string,string> { + let borderR = this.uiOpts.borderRadius + 'px'; + borderR = `${borderR} ${borderR} 0 0`; return { height: this.lytOpts.headerSz + 'px', + borderRadius: borderR, + backgroundColor: this.uiOpts.nonleafHeaderBgColor, + }; + }, + nonleafHeaderTextStyles(): Record<string,string> { + return { lineHeight: this.lytOpts.headerSz + 'px', - fontSize: this.uiOpts.nonLeafHeaderFontSz + 'px', + fontSize: this.uiOpts.nonleafHeaderFontSz + 'px', textAlign: 'center', - color: this.uiOpts.nonLeafHeaderColor, - backgroundColor: this.uiOpts.nonLeafHeaderBgColor, - borderRadius: `${r} ${r} 0 0`, + color: this.uiOpts.nonleafHeaderColor, // For ellipsis overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }; }, + infoIconStyles(): Record<string,string> { + let size = this.uiOpts.infoIconSz + 'px'; + return { + width: size, + height: size, + minWidth: size, + minHeight: size, + margin: this.uiOpts.infoIconMargin + 'px', + }; + }, sepSweptAreaStyles(): Record<string,string> { - let commonStyles = { + let borderR = this.uiOpts.borderRadius + 'px'; + let styles = { 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', + backgroundColor: this.nonleafBgColor, + boxShadow: this.boxShadow, + transitionDuration: this.uiOpts.tileChgDuration + 'ms', + transitionProperty: 'left, top, width, height, visibility', transitionTimingFunction: 'ease-out', }; let area = this.layoutNode.sepSweptArea; - if (this.layoutNode.hidden || area == null){ + if (!this.layoutNode.hidden && area != null){ + return { + ...styles, + visibility: 'visible', + 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`, + }; + } else { return { - ...commonStyles, + ...styles, 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`, + borderRadius: borderR, }; } }, - collapseFailFlag(){ - return this.layoutNode.collapseFailFlag; - }, - expandFailFlag(){ - return this.layoutNode.expandFailFlag; + // Other + failFlag(){ + return this.layoutNode.failFlag; }, }, watch: { - expandFailFlag(newVal){ - this.triggerAnimation('animate-expand-shrink'); - }, - collapseFailFlag(newVal){ - this.triggerAnimation('animate-shrink-expand'); + failFlag(newVal){ + this.triggerAnimation(this.isLeaf ? 'animate-expand-shrink' : 'animate-shrink-expand'); }, }, methods: { @@ -184,115 +209,98 @@ export default defineComponent({ } }, onClick(){ - if (this.isLeaf && !this.isExpandable){ + if (this.isLeaf && !this.isExpandableLeaf){ console.log('Ignored click on non-expandable node'); return; } this.prepForTransition(); - if (this.isLeaf){ - this.$emit('leaf-clicked', this.layoutNode); - } else { - this.$emit('header-clicked', this.layoutNode); - } + this.$emit(this.isLeaf ? 'leaf-click' : 'nonleaf-click', this.layoutNode); }, onClickHold(){ - if (this.isLeaf && !this.isExpandable){ + if (this.isLeaf && !this.isExpandableLeaf){ console.log('Ignored click-hold on non-expandable node'); return; } this.prepForTransition(); - if (this.isLeaf){ - this.$emit('leaf-click-held', this.layoutNode); - } else { - this.$emit('header-click-held', this.layoutNode); - } + this.$emit(this.isLeaf ? 'leaf-click-held' : 'nonleaf-click-held', this.layoutNode); }, prepForTransition(){ - this.animating = true; - setTimeout(() => {this.animating = false}, this.uiOpts.transitionDuration); + this.inTransition = true; + setTimeout(() => {this.inTransition = false}, this.uiOpts.tileChgDuration); }, - onInfoClick(evt: Event){ - this.$emit('info-icon-clicked', this.layoutNode); + onInfoIconClick(evt: Event){ + this.$emit('info-icon-click', this.layoutNode); }, - // For coloured outlines on hover + // Mouse hover handling onMouseEnter(evt: Event){ - if (!this.isLeaf || this.isExpandable){ + if ((!this.isLeaf || this.isExpandableLeaf) && !this.inTransition){ this.highlight = true; } }, onMouseLeave(evt: Event){ - if (!this.isLeaf || this.isExpandable){ - this.highlight = false; - } + this.highlight = false; }, // Child event propagation - onInnerLeafClicked(data: LayoutNode){ - this.$emit('leaf-clicked', data); + onInnerLeafClick(node: LayoutNode){ + this.$emit('leaf-click', node); }, - onInnerHeaderClicked(data: LayoutNode){ - this.$emit('header-clicked', data); + onInnerNonleafClick(node: LayoutNode){ + this.$emit('nonleaf-click', node); }, - onInnerLeafClickHeld(data: LayoutNode){ - this.$emit('leaf-click-held', data); + onInnerLeafClickHeld(node: LayoutNode){ + this.$emit('leaf-click-held', node); }, - onInnerHeaderClickHeld(data: LayoutNode){ - this.$emit('header-click-held', data); + onInnerNonleafClickHeld(node: LayoutNode){ + this.$emit('nonleaf-click-held', node); }, - onInnerInfoIconClicked(data: LayoutNode){ - this.$emit('info-icon-clicked', data); + onInnerInfoIconClick(node: LayoutNode){ + this.$emit('info-icon-click', node); }, - // - triggerAnimation(animationClass: string){ - this.$el.classList.remove(animationClass); + // Other + triggerAnimation(animation: string){ + this.$el.classList.remove(animation); this.$el.offsetWidth; // Triggers reflow - this.$el.classList.add(animationClass); + this.$el.classList.add(animation); }, }, - name: 'tile', // Need this to use self in template + name: 'tile', // Note: Need this to use self in template components: {InfoIcon, }, - emits: ['leaf-clicked', 'header-clicked', 'leaf-click-held', 'header-click-held', 'info-icon-clicked'], + emits: ['leaf-click', 'nonleaf-click', 'leaf-click-held', 'nonleaf-click-held', 'info-icon-click', ], }); </script> <template> -<div :style="tileStyles"> - <div v-if="isLeaf" :style="leafStyles" :class="isExpandable ? ['hover:cursor-pointer'] : []" - @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" - @mousedown="onMouseDown" @mouseup="onMouseUp"> +<div :style="styles"> <!-- Enclosing div needed for size transitions --> + <div v-if="isLeaf" :style="leafStyles" + class="w-full h-full flex flex-col overflow-hidden" :class="{'hover:cursor-pointer': isExpandableLeaf}" + @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" - @click.stop="onInfoClick" @mousedown.stop @mouseup.stop/> + <info-icon :style="[infoIconStyles, {marginTop: 'auto'}]" + class="self-end text-white/10 hover:text-white hover:cursor-pointer" + @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/> </div> - <div v-else :style="nonLeafStyles" ref="nonLeaf"> - <div v-if="showHeader" :style="nonLeafHeaderStyles" class="flex hover:cursor-pointer" - @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" - @mousedown="onMouseDown" @mouseup="onMouseUp"> - <h1 class="grow">{{layoutNode.tolNode.name}}</h1> - <info-icon - class="w-[18px] h-[18px] mr-[2px] - text-white/20 hover:text-white hover:cursor-pointer" - @click.stop="onInfoClick" @mousedown.stop @mouseup.stop/> + <div v-else :style="nonleafStyles" class="w-full h-full" ref="nonleaf"> + <div v-if="showNonleafHeader" :style="nonleafHeaderStyles" class="flex hover:cursor-pointer" + @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp"> + <h1 :style="nonleafHeaderTextStyles" class="grow">{{layoutNode.tolNode.name}}</h1> + <info-icon :style="infoIconStyles" class="text-white/10 hover:text-white hover:cursor-pointer" + @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/> </div> <div :style="sepSweptAreaStyles" ref="sepSweptArea" :class="layoutNode?.sepSweptArea?.sweptLeft ? 'hide-right-edge' : 'hide-top-edge'"> <div v-if="layoutNode?.sepSweptArea?.sweptLeft === false" - :style="nonLeafHeaderStyles" class="flex hover:cursor-pointer" - @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" - @mousedown="onMouseDown" @mouseup="onMouseUp"> - <h1 class="grow">{{layoutNode.tolNode.name}}</h1> - <info-icon - class="w-[18px] h-[18px] mr-[2px] - text-white/20 hover:text-white hover:cursor-pointer" - @click.stop="onInfoClick" @mousedown.stop @mouseup.stop/> + :style="nonleafHeaderStyles" class="flex hover:cursor-pointer" + @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp"> + <h1 :style="nonleafHeaderTextStyles" class="grow">{{layoutNode.tolNode.name}}</h1> + <info-icon :style="infoIconStyles" class="text-white/10 hover:text-white hover:cursor-pointer" + @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/> </div> </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"/> + <tile v-for="child in layoutNode.children" :key="child.tolNode.name" + :layoutNode="child" :lytOpts="lytOpts" :uiOpts="uiOpts" + @leaf-click="onInnerLeafClick" @nonleaf-click="onInnerNonleafClick" + @leaf-click-held="onInnerLeafClickHeld" @nonleaf-click-held="onInnerNonleafClickHeld" + @info-icon-click="onInnerInfoIconClick"/> </div> </div> </template> @@ -301,7 +309,7 @@ export default defineComponent({ .hide-right-edge::before { content: ''; position: absolute; - background-color: var(--nonLeafBgColor); + background-color: var(--nonleafBgColor); right: calc(0px - var(--tileSpacing)); bottom: 0; width: var(--tileSpacing); @@ -310,10 +318,44 @@ export default defineComponent({ .hide-top-edge::before { content: ''; position: absolute; - background-color: var(--nonLeafBgColor); + background-color: var(--nonleafBgColor); bottom: calc(0px - var(--tileSpacing)); right: 0; width: calc(100% + var(--tileSpacing)); height: var(--tileSpacing); } +.animate-expand-shrink { + animation-name: expand-shrink; + animation-duration: 300ms; + animation-iteration-count: 1; + animation-timing-function: ease-in-out; +} +@keyframes expand-shrink { + from { + transform: scale(1, 1); + } + 50% { + transform: scale(1.1, 1.1); + } + to { + transform: scale(1, 1); + } +} +.animate-shrink-expand { + animation-name: shrink-expand; + animation-duration: 300ms; + animation-iteration-count: 1; + animation-timing-function: ease-in-out; +} +@keyframes shrink-expand { + from { + transform: translate3d(0,0,0) scale(1, 1); + } + 50% { + transform: translate3d(0,0,0) scale(0.9, 0.9); + } + to { + transform: translate3d(0,0,0) scale(1, 1); + } +} </style> |
