aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/AncestryBar.vue64
-rw-r--r--src/components/HelpModal.vue11
-rw-r--r--src/components/SearchModal.vue14
-rw-r--r--src/components/SettingsPane.vue37
-rw-r--r--src/components/Tile.vue360
-rw-r--r--src/components/TileInfoModal.vue13
6 files changed, 275 insertions, 224 deletions
diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue
index f7ce232..6d6ae3c 100644
--- a/src/components/AncestryBar.vue
+++ b/src/components/AncestryBar.vue
@@ -1,83 +1,87 @@
<script lang="ts">
import {defineComponent, PropType} from 'vue';
+import Tile from './Tile.vue'
import {LayoutNode} from '../layout';
import type {LayoutOptions} from '../layout';
-import Tile from './Tile.vue'
+// Displays a sequence of nodes, representing ancestors from a tree-of-life root to a currently-active root
export default defineComponent({
props: {
+ // For absolute positioning
pos: {type: Array as unknown as PropType<[number,number]>, required: true},
dims: {type: Array as unknown as PropType<[number,number]>, required: true},
+ // The ancestors to display
nodes: {type: Array as PropType<LayoutNode[]>, required: true},
+ // Options
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object, required: true},
},
- data(){
- return {
- tileMargin: 5, //px (gap between detached-ancestor tiles)
- scrollBarOffset: 10, //px (gap for scrollbar, used to prevent overlap with tiles)
- };
- },
computed: {
wideArea(){
return this.dims[0] >= this.dims[1];
},
tileSz(){
- return (this.wideArea ? this.dims[1] : this.dims[0]) - (this.tileMargin * 2) - this.scrollBarOffset;
+ return (this.wideArea ? this.dims[1] : this.dims[0]) -
+ (this.uiOpts.ancestryTileMargin * 2) - this.uiOpts.ancestryBarScrollGap;
},
- usedNodes(){
+ usedNodes(){ // Childless versions of 'nodes' used to parameterise <tile>
return this.nodes.map(n => {
let newNode = new LayoutNode(n.tolNode, []);
newNode.dims = [this.tileSz, this.tileSz];
return newNode;
});
},
- hasOverflow(){
- let len = this.tileMargin + (this.tileSz + this.tileMargin) * this.nodes.length;
+ overflowing(){
+ let len = this.uiOpts.ancestryTileMargin +
+ (this.tileSz + this.uiOpts.ancestryTileMargin) * this.nodes.length;
return len > (this.wideArea ? this.dims[0] : this.dims[1]);
},
+ width(){
+ return this.dims[0] + (this.wideArea || this.overflowing ? 0 : -this.uiOpts.ancestryBarScrollGap);
+ },
+ height(){
+ return this.dims[1] + (!this.wideArea || this.overflowing ? 0 : -this.uiOpts.ancestryBarScrollGap);
+ },
styles(): Record<string,string> {
return {
position: 'absolute',
left: this.pos[0] + 'px',
top: this.pos[1] + 'px',
- width: (this.dims[0] + (this.wideArea || this.hasOverflow ? 0 : -this.scrollBarOffset)) + 'px',
- height: (this.dims[1] + (!this.wideArea || this.hasOverflow ? 0 : -this.scrollBarOffset)) + 'px',
+ width: this.width + 'px',
+ height: this.height + 'px',
overflowX: this.wideArea ? 'auto' : 'hidden',
overflowY: this.wideArea ? 'hidden' : 'auto',
// Extra padding for scrollbar inclusion
- paddingRight: (this.hasOverflow && !this.wideArea ? this.scrollBarOffset : 0) + 'px',
- paddingBottom: (this.hasOverflow && this.wideArea ? this.scrollBarOffset : 0) + 'px',
+ paddingRight: (this.overflowing && !this.wideArea ? this.uiOpts.ancestryBarScrollGap : 0) + 'px',
+ paddingBottom: (this.overflowing && this.wideArea ? this.uiOpts.ancestryBarScrollGap : 0) + 'px',
// For child layout
display: 'flex',
flexDirection: this.wideArea ? 'row' : 'column',
- gap: this.tileMargin + 'px',
- padding: this.tileMargin + 'px',
- //
- backgroundColor: '#44403c',
+ gap: this.uiOpts.ancestryTileMargin + 'px',
+ padding: this.uiOpts.ancestryTileMargin + 'px',
+ // Other
+ backgroundColor: this.uiOpts.ancestryBarBgColor,
boxShadow: this.uiOpts.shadowNormal,
};
},
},
methods: {
- onClick(node: LayoutNode){
- this.$emit('detached-ancestor-clicked', node);
+ onTileClick(node: LayoutNode){
+ this.$emit('detached-ancestor-click', node);
},
- onInnerInfoIconClicked(data: LayoutNode){
- this.$emit('info-icon-clicked', data);
+ onInfoIconClick(data: LayoutNode){
+ this.$emit('info-icon-click', data);
}
},
- components: {
- Tile,
- },
- emits: ['detached-ancestor-clicked', 'info-icon-clicked'],
+ components: {Tile, },
+ emits: ['detached-ancestor-click', 'info-icon-click', ],
});
</script>
<template>
<div :style="styles">
- <tile v-for="(node, idx) in usedNodes" :key="node.tolNode.name" :layoutNode="node"
- :nonAbsPos="true" :lytOpts="lytOpts" :uiOpts="uiOpts"
- @leaf-clicked="onClick(nodes[idx])" @info-icon-clicked="onInnerInfoIconClicked"/>
+ <tile v-for="(node, idx) in usedNodes" :key="node.tolNode.name" class="shrink-0"
+ :layoutNode="node" :nonAbsPos="true" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ @leaf-click="onTileClick(nodes[idx])" @info-icon-click="onInfoIconClick"/>
</div>
</template>
diff --git a/src/components/HelpModal.vue b/src/components/HelpModal.vue
index 30b1b21..539d3dc 100644
--- a/src/components/HelpModal.vue
+++ b/src/components/HelpModal.vue
@@ -2,27 +2,28 @@
import {defineComponent, PropType} from 'vue';
import CloseIcon from './icon/CloseIcon.vue';
+// Displays help information
export default defineComponent({
props: {
uiOpts: {type: Object, required: true},
},
methods: {
- closeClicked(evt: Event){
- if (evt.target == this.$el || (this.$refs.closeIcon.$el as HTMLElement).contains(evt.target as HTMLElement)){
+ onCloseClick(evt: Event){
+ if (evt.target == this.$el || (this.$refs.closeIcon as typeof CloseIcon).$el.contains(evt.target)){
this.$emit('help-modal-close');
}
},
},
components: {CloseIcon, },
- emits: ['help-modal-close'],
+ emits: ['help-modal-close', ],
});
</script>
<template>
-<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="closeClicked">
+<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onCloseClick">
<div class="absolute left-1/2 -translate-x-1/2 w-4/5 top-1/2 -translate-y-1/2 p-4
bg-stone-50 rounded-md shadow shadow-black">
- <close-icon @click.stop="closeClicked" ref="closeIcon"
+ <close-icon @click.stop="onCloseClick" ref="closeIcon"
class="block absolute top-2 right-2 w-6 h-6 hover:cursor-pointer"/>
<h1 class="text-center text-xl font-bold mb-2">Help Info</h1>
<hr class="mb-4 border-stone-400"/>
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index 369b632..91f06ae 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -4,15 +4,17 @@ import SearchIcon from './icon/SearchIcon.vue';
import {TolNode} from '../tol';
import {LayoutNode} from '../layout';
+// Displays a search box, and sends search requests
export default defineComponent({
props: {
- layoutTree: {type: Object as PropType<LayoutNode>, required: true},
+ // Map from tree-of-life node names to TolNode objects
tolMap: {type: Object as PropType<Map<string,TolNode>>, required: true},
+ // Options
uiOpts: {type: Object, required: true},
},
methods: {
- closeClicked(evt: Event){
- if (evt.target == this.$el || (this.$refs.closeIcon.$el as HTMLElement).contains(evt.target as HTMLElement)){
+ onCloseClick(evt: Event){
+ if (evt.target == this.$el || (this.$refs.searchInput as typeof SearchIcon).$el.contains(evt.target)){
this.$emit('search-close');
}
},
@@ -37,16 +39,16 @@ export default defineComponent({
(this.$refs.searchInput as HTMLInputElement).focus();
},
components: {SearchIcon, },
- emits: ['search-node', 'search-close']
+ emits: ['search-node', 'search-close', ],
});
</script>
<template>
-<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="closeClicked">
+<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onCloseClick">
<div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 p-3
bg-stone-50 rounded-md shadow shadow-black flex gap-1">
<input type="text" class="block border"
- @keyup.enter="onSearchEnter" @keyup.esc="closeClicked" ref="searchInput"/>
+ @keyup.enter="onSearchEnter" @keyup.esc="onCloseClick" ref="searchInput"/>
<search-icon @click.stop="onSearchEnter"
class="block w-6 h-6 ml-1 hover:cursor-pointer hover:bg-stone-200" />
</div>
diff --git a/src/components/SettingsPane.vue b/src/components/SettingsPane.vue
index c9f1833..990d1f7 100644
--- a/src/components/SettingsPane.vue
+++ b/src/components/SettingsPane.vue
@@ -3,18 +3,19 @@ import {defineComponent, PropType} from 'vue';
import CloseIcon from './icon/CloseIcon.vue';
import type {LayoutOptions} from '../layout';
+// Displays configurable options, and sends option-change requests
export default defineComponent({
props: {
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object, required: true},
},
methods: {
- closeClicked(evt: Event){
- if (evt.target == this.$el || (this.$refs.closeIcon.$el as HTMLElement).contains(evt.target as HTMLElement)){
+ onCloseClick(evt: Event){
+ if (evt.target == this.$el || (this.$refs.closeIcon as typeof CloseIcon).$el.contains(evt.target)){
this.$emit('settings-close');
}
},
- onLayoutOptChg(){
+ onLytOptChg(){
this.$emit('layout-option-change');
},
onMinTileSzChg(){
@@ -23,7 +24,7 @@ export default defineComponent({
if (Number(minInput.value) > Number(maxInput.value)){
this.lytOpts.maxTileSz = this.lytOpts.minTileSz;
}
- this.onLayoutOptChg();
+ this.onLytOptChg();
},
onMaxTileSzChg(){
let minInput = this.$refs.minTileSzInput as HTMLInputElement;
@@ -31,7 +32,7 @@ export default defineComponent({
if (Number(maxInput.value) < Number(minInput.value)){
this.lytOpts.minTileSz = this.lytOpts.maxTileSz;
}
- this.onLayoutOptChg();
+ this.onLytOptChg();
},
},
components: {CloseIcon, },
@@ -41,13 +42,13 @@ export default defineComponent({
<template>
<div class="absolute bottom-4 right-4 min-w-[5cm] p-3 bg-stone-50 visible rounded-md shadow shadow-black">
- <close-icon @click="closeClicked" ref="closeIcon"
+ <close-icon @click="onCloseClick" ref="closeIcon"
class="block absolute top-2 right-2 w-6 h-6 hover:cursor-pointer" />
<h1 class="text-xl font-bold mb-2">Settings</h1>
<hr class="border-stone-400"/>
<div>
<label>Tile Spacing <input type="range" min="0" max="20" class="mx-2 w-[3cm]"
- v-model.number="lytOpts.tileSpacing" @input="onLayoutOptChg"/></label>
+ v-model.number="lytOpts.tileSpacing" @input="onLytOptChg"/></label>
</div>
<hr class="border-stone-400"/>
<div>
@@ -70,22 +71,22 @@ export default defineComponent({
<ul>
<li>
<label> <input type="radio" v-model="lytOpts.layoutType" value="sqr"
- @change="onLayoutOptChg"/> Squares </label>
+ @change="onLytOptChg"/> Squares </label>
</li>
<li>
<label> <input type="radio" v-model="lytOpts.layoutType" value="rect"
- @change="onLayoutOptChg"/> Rectangles </label>
+ @change="onLytOptChg"/> Rectangles </label>
</li>
<li>
<label> <input type="radio" v-model="lytOpts.layoutType" value="sweep"
- @change="onLayoutOptChg"/> Sweep to side </label>
+ @change="onLytOptChg"/> Sweep to side </label>
</li>
</ul>
</div>
<hr class="border-stone-400"/>
<div>
- <label> <input type="checkbox" v-model="lytOpts.sweepingToParent"
- @change="onLayoutOptChg"/> Sweep to parent</label>
+ <label> <input type="checkbox" v-model="lytOpts.sweepToParent"
+ @change="onLytOptChg"/> Sweep to parent</label>
</div>
<hr class="border-stone-400"/>
<div>
@@ -93,26 +94,26 @@ export default defineComponent({
<ul>
<li>
<label> <input type="radio" v-model="lytOpts.sweepMode" value="left"
- @change="onLayoutOptChg"/> To left </label>
+ @change="onLytOptChg"/> To left </label>
</li>
<li>
<label> <input type="radio" v-model="lytOpts.sweepMode" value="top"
- @change="onLayoutOptChg"/> To top </label>
+ @change="onLytOptChg"/> To top </label>
</li>
<li>
<label> <input type="radio" v-model="lytOpts.sweepMode" value="shorter"
- @change="onLayoutOptChg"/> To shorter </label>
+ @change="onLytOptChg"/> To shorter </label>
</li>
<li>
<label> <input type="radio" v-model="lytOpts.sweepMode" value="auto"
- @change="onLayoutOptChg"/> Auto </label>
+ @change="onLytOptChg"/> Auto </label>
</li>
</ul>
</div>
<hr class="border-stone-400"/>
<div>
- <label>Animation Speed <input type="range" min="0" max="1000" class="mx-2 w-[3cm]"
- v-model.number="uiOpts.transitionDuration"/></label>
+ <label>Animation Duration <input type="range" min="0" max="1000" class="mx-2 w-[3cm]"
+ v-model.number="uiOpts.tileChgDuration"/></label>
</div>
</div>
</template>
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>
diff --git a/src/components/TileInfoModal.vue b/src/components/TileInfoModal.vue
index cfa1a10..7549375 100644
--- a/src/components/TileInfoModal.vue
+++ b/src/components/TileInfoModal.vue
@@ -3,6 +3,7 @@ import {defineComponent, PropType} from 'vue';
import CloseIcon from './icon/CloseIcon.vue';
import {TolNode} from '../tol';
+// Displays information about a tree-of-life node
export default defineComponent({
props: {
tolNode: {type: Object as PropType<TolNode>, required: true},
@@ -16,26 +17,26 @@ export default defineComponent({
height: this.uiOpts.infoModalImgSz + 'px',
backgroundSize: 'cover',
borderRadius: this.uiOpts.borderRadius + 'px',
- }
+ };
},
},
methods: {
- closeClicked(evt: Event){
- if (evt.target == this.$el || (this.$refs.closeIcon.$el as HTMLElement).contains(evt.target as HTMLElement)){
+ onCloseClick(evt: Event){
+ if (evt.target == this.$el || (this.$refs.closeIcon as typeof CloseIcon).$el.contains(evt.target)){
this.$emit('info-modal-close');
}
},
},
components: {CloseIcon, },
- emits: ['info-modal-close'],
+ emits: ['info-modal-close', ],
});
</script>
<template>
-<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="closeClicked">
+<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onCloseClick">
<div class="absolute left-1/2 -translate-x-1/2 w-4/5 top-1/2 -translate-y-1/2 p-4
bg-stone-50 rounded-md shadow shadow-black">
- <close-icon @click.stop="closeClicked" ref="closeIcon"
+ <close-icon @click.stop="onCloseClick" ref="closeIcon"
class="block absolute top-2 right-2 w-6 h-6 hover:cursor-pointer"/>
<h1 class="text-center text-xl font-bold mb-2">{{tolNode.name}}</h1>
<hr class="mb-4 border-stone-400"/>