aboutsummaryrefslogtreecommitdiff
path: root/src/components/Tile.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Tile.vue')
-rw-r--r--src/components/Tile.vue579
1 files changed, 0 insertions, 579 deletions
diff --git a/src/components/Tile.vue b/src/components/Tile.vue
deleted file mode 100644
index 2edd531..0000000
--- a/src/components/Tile.vue
+++ /dev/null
@@ -1,579 +0,0 @@
-<template>
-<div :style="styles" @scroll="onScroll">
- <div v-if="isLeaf" :class="[hasOneImage ? 'flex' : 'grid', {'hover:cursor-pointer': isExpandableLeaf}]"
- class="w-full h-full flex-col grid-cols-1" :style="leafStyles"
- @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
- <template v-if="hasOneImage">
- <h1 :style="leafHeaderStyles">{{displayName}}</h1>
- <info-icon v-if="infoIconDisabled" :style="infoIconStyles" :class="infoIconClasses"
- @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
- </template>
- <template v-else>
- <div :style="leafFirstImgStyles" class="col-start-1 row-start-1"></div>
- <div :style="leafSecondImgStyles" class="col-start-1 row-start-1"></div>
- <h1 :style="leafHeaderStyles" class="col-start-1 row-start-1 z-10">{{displayName}}</h1>
- <info-icon v-if="infoIconDisabled" class="col-start-1 row-start-1 z-10"
- :style="infoIconStyles" :class="infoIconClasses"
- @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
- </template>
- </div>
- <div v-else :style="nonleafStyles">
- <div v-if="showNonleafHeader" :style="nonleafHeaderStyles" class="flex hover:cursor-pointer"
- @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
- <h1 :style="nonleafHeaderTextStyles" class="grow">{{displayName}}</h1>
- <info-icon v-if="infoIconDisabled" :style="infoIconStyles" :class="infoIconClasses"
- @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
- </div>
- <div :style="sepSweptAreaStyles" :class="sepSweptAreaHideEdgeClass">
- <div v-if="layoutNode.sepSweptArea?.sweptLeft === false"
- :style="nonleafHeaderStyles" class="flex hover:cursor-pointer"
- @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
- <h1 :style="nonleafHeaderTextStyles" class="grow">{{displayName}}</h1>
- <info-icon v-if="infoIconDisabled" :style="infoIconStyles" :class="infoIconClasses"
- @click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
- </div>
- <transition name="fadein">
- <div v-if="inFlash" class="absolute w-full h-full top-0 left-0 rounded-[inherit] bg-amber-500/70 z-20"/>
- </transition>
- </div>
- <tile v-for="child in visibleChildren" :key="child.name"
- :layoutNode="child" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" :overflownDim="overflownDim"
- @leaf-click="onInnerLeafClick" @nonleaf-click="onInnerNonleafClick"
- @leaf-click-held="onInnerLeafClickHeld" @nonleaf-click-held="onInnerNonleafClickHeld"
- @info-click="onInnerInfoIconClick"/>
- </div>
- <transition name="fadein">
- <div v-if="inFlash" class="absolute w-full h-full top-0 left-0 rounded-[inherit] bg-amber-500/70"/>
- </transition>
-</div>
-</template>
-
-<script lang="ts">
-import {defineComponent, PropType} from 'vue';
-import InfoIcon from './icon/InfoIcon.vue';
-import {TolNode, TolMap} from '../tol';
-import {LayoutNode, LayoutOptions} from '../layout';
-import {getImagePath, UiOptions} from '../lib';
-import {capitalizeWords} from '../util';
-
-const scrimGradient = 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.2) 100%)';
-
-export default defineComponent({
- props: {
- layoutNode: {type: Object as PropType<LayoutNode>, required: true},
- tolMap: {type: Object as PropType<TolMap>, required: true},
- // Options
- lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
- // Other
- skipTransition: {type: Boolean, default: false},
- nonAbsPos: {type: Boolean, default: false},
- // For a leaf node, prevents usage of absolute positioning (used by AncestryBar)
- overflownDim: {type: Number, default: 0},
- // For a non-leaf node, display with overflow within area of this height
- },
- data(){
- return {
- // Mouse-event related
- clickHoldTimer: 0, // Used to recognise click-and-hold events
- highlight: false, // Used to draw a colored outline on mouse hover
- // Scroll-during-overflow related
- scrollOffset: 0, // Used to track scroll offset when displaying with overflow
- pendingScrollHdlr: 0, // Used for throttling updating of scrollOffset
- // Transition related
- inTransition: false, // Used to avoid content overlap and overflow during 'user-perceivable' transitions
- wasClicked: false, // Used to increase z-index during transition after this tile (or a child) is clicked
- hasExpanded: false, // Set to true after an expansion transition ends, and false upon collapse
- // Used to hide overflow on tile expansion, but not hide a sepSweptArea on subsequent transitions
- justUnhidden: false, // Used to allow overflow temporarily after being unhidden
- // Other
- inFlash: false, // Used to 'flash' the tile when focused
- };
- },
- computed: {
- tolNode(): TolNode {
- return this.tolMap.get(this.layoutNode.name)!;
- },
- visibleChildren(): LayoutNode[] { // Used to reduce slowdown from rendering many nodes
- let children = this.layoutNode.children;
- // If not displaying with overflow, return 'visible' layoutNode children
- if (!this.isOverflownRoot){
- return children.filter(n => !n.hidden || n.hiddenWithVisibleTip);
- }
- // Otherwise, return children within/near non-overflowing region
- let firstIdx = children.length - 1;
- for (let i = 0; i < children.length; i++){
- if (children[i].pos[1] + children[i].dims[1] >= this.scrollOffset){
- firstIdx = i;
- break;
- }
- }
- let lastIdx = children.length;
- for (let i = firstIdx + 1; i < children.length; i++){
- if (children[i].pos[1] > this.scrollOffset + this.overflownDim){
- lastIdx = i;
- break;
- }
- }
- return children.slice(firstIdx, lastIdx);
- },
- // Convenience abbreviations
- isLeaf(): boolean {
- return this.layoutNode.children.length == 0;
- },
- isExpandableLeaf(): boolean {
- return this.isLeaf && this.tolNode.children.length > 0;
- },
- showNonleafHeader(): boolean {
- return (this.layoutNode.showHeader && this.layoutNode.sepSweptArea == null) ||
- (this.layoutNode.sepSweptArea != null && this.layoutNode.sepSweptArea.sweptLeft);
- },
- displayName(): string {
- let newName = capitalizeWords(this.tolNode.commonName || this.layoutNode.name);
- if (!this.tolNode.pSupport && this.tolNode.parent != null){
- newName += '*';
- }
- return newName;
- },
- hasOneImage(): boolean {
- return !Array.isArray(this.tolNode.imgName);
- },
- isOverflownRoot(): boolean {
- return this.overflownDim > 0 && !this.layoutNode.hidden && this.layoutNode.children.length > 0;
- },
- hasFocusedChild(): boolean {
- return this.layoutNode.children.some(n => n.hasFocus);
- },
- infoIconDisabled(): boolean {
- return !this.uiOpts.disabledActions.has('tileInfo');
- },
- // For styling
- nonleafBgColor(): string {
- let colorArray = this.uiOpts.nonleafBgColors;
- return colorArray[this.layoutNode.depth % colorArray.length];
- },
- boxShadow(): string {
- if (this.highlight){
- return this.uiOpts.shadowHovered;
- } else if (this.layoutNode.hasFocus && !this.inTransition){
- return this.uiOpts.shadowFocused;
- } else {
- return this.uiOpts.shadowNormal;
- }
- },
- fontSz(): number {
- // These values are a compromise between dynamic font size and code simplicity
- if (this.layoutNode.dims[0] >= 150){
- return this.lytOpts.headerSz * 0.8;
- } else if (this.layoutNode.dims[0] >= 80){
- return this.lytOpts.headerSz * 0.7;
- } else {
- return this.lytOpts.headerSz * 0.6;
- }
- },
- 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',
- borderRadius: this.uiOpts.borderRadius + 'px',
- boxShadow: this.boxShadow,
- visibility: 'visible',
- // Transition related
- transitionDuration: (this.skipTransition ? 0 : this.uiOpts.transitionDuration) + 'ms',
- transitionProperty: 'left, top, width, height, visibility',
- transitionTimingFunction: 'ease-out',
- zIndex: this.inTransition && this.wasClicked ? '1' : '0',
- overflow: (this.inTransition && !this.isLeaf && !this.hasExpanded && !this.justUnhidden) ?
- 'hidden' : 'visible',
- // CSS variables
- '--nonleafBgColor': this.nonleafBgColor,
- '--tileSpacing': this.lytOpts.tileSpacing + 'px',
- };
- if (!this.isLeaf){
- let borderR = this.uiOpts.borderRadius + 'px';
- if (this.layoutNode.sepSweptArea != null){
- borderR = this.layoutNode.sepSweptArea.sweptLeft ?
- `${borderR} ${borderR} ${borderR} 0` :
- `${borderR} 0 ${borderR} ${borderR}`;
- }
- layoutStyles.borderRadius = borderR;
- }
- if (this.isOverflownRoot){
- layoutStyles.width = (this.layoutNode.dims[0] + this.uiOpts.scrollGap) + 'px';
- layoutStyles.height = this.overflownDim + 'px';
- layoutStyles.overflow = 'hidden scroll';
- }
- if (this.layoutNode.hidden){
- layoutStyles.left = layoutStyles.top = layoutStyles.width = layoutStyles.height = '0';
- layoutStyles.visibility = 'hidden';
- }
- if (this.nonAbsPos){
- layoutStyles.position = 'static';
- }
- return layoutStyles;
- },
- leafStyles(): Record<string,string> {
- let styles: Record<string,string> = {
- borderRadius: 'inherit',
- };
- if (this.hasOneImage){
- styles = {
- ...styles,
- backgroundImage: this.tolNode.imgName != null ?
- `${scrimGradient},url('${getImagePath(this.tolNode.imgName as string)}')` :
- 'none',
- backgroundColor: this.uiOpts.bgColorDark,
- backgroundSize: 'cover',
- };
- }
- return styles;
- },
- leafHeaderStyles(): Record<string,string> {
- let numChildren = this.tolNode.children.length;
- let textColor = this.uiOpts.textColor;
- for (let [threshold, color] of this.uiOpts.childQtyColors){
- if (numChildren >= threshold){
- textColor = color;
- } else {
- break;
- }
- }
- return {
- lineHeight: (this.fontSz * 1.3) + 'px',
- fontSize: this.fontSz + 'px',
- paddingLeft: (this.fontSz * 0.2) + 'px',
- color: textColor,
- // For ellipsis
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- };
- },
- leafFirstImgStyles(): Record<string,string> {
- return this.leafSubImgStyles(0);
- },
- leafSecondImgStyles(): Record<string,string> {
- return this.leafSubImgStyles(1);
- },
- nonleafStyles(): Record<string,string> {
- let styles = {
- width: '100%',
- height: '100%',
- backgroundColor: this.nonleafBgColor,
- borderRadius: 'inherit',
- };
- if (this.isOverflownRoot){
- styles.width = this.layoutNode.dims[0] + 'px';
- styles.height = this.layoutNode.dims[1] + 'px';
- }
- return styles;
- },
- nonleafHeaderStyles(): Record<string,string> {
- let styles: Record<string,string> = {
- position: 'static',
- height: this.lytOpts.headerSz + 'px',
- borderTopLeftRadius: 'inherit',
- borderTopRightRadius: 'inherit',
- backgroundColor: this.uiOpts.nonleafHeaderColor,
- };
- if (this.isOverflownRoot){
- styles = {
- ...styles,
- position: 'sticky',
- top: '0',
- left: '0',
- borderTopRightRadius: '0',
- zIndex: '1',
- boxShadow: this.uiOpts.shadowNormal,
- };
- }
- return styles;
- },
- nonleafHeaderTextStyles(): Record<string,string> {
- return {
- lineHeight: (this.fontSz * 1.3) + 'px',
- fontSize: this.fontSz + 'px',
- paddingLeft: (this.fontSz * 0.2) + 'px',
- textAlign: 'center',
- color: this.uiOpts.textColor,
- // For ellipsis
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- };
- },
- sepSweptAreaStyles(): Record<string,string> {
- let borderR = this.uiOpts.borderRadius + 'px';
- let styles = {
- position: 'absolute',
- backgroundColor: this.nonleafBgColor,
- boxShadow: this.boxShadow,
- transitionDuration: this.uiOpts.transitionDuration + 'ms',
- transitionProperty: 'left, top, width, height, visibility',
- transitionTimingFunction: 'ease-out',
- };
- let area = this.layoutNode.sepSweptArea;
- 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 {
- ...styles,
- visibility: 'hidden',
- left: '0',
- top: this.lytOpts.headerSz + 'px',
- width: '0',
- height: '0',
- borderRadius: borderR,
- };
- }
- },
- sepSweptAreaHideEdgeClass(): string {
- if (this.layoutNode.sepSweptArea == null){
- return '';
- } else if (this.layoutNode.sepSweptArea.sweptLeft){
- return 'hide-right-edge';
- } else {
- return 'hide-top-edge';
- }
- },
- infoIconStyles(): Record<string,string> {
- let size = (this.lytOpts.headerSz * 0.85);
- let marginSz = (this.lytOpts.headerSz - size);
- return {
- width: size + 'px',
- height: size + 'px',
- minWidth: size + 'px',
- minHeight: size + 'px',
- margin: this.isLeaf ? `auto ${marginSz}px ${marginSz}px auto` : `auto ${marginSz}px auto 0`,
- };
- },
- infoIconClasses(): string {
- return 'text-white/30 hover:text-white hover:cursor-pointer';
- },
- // For watching layoutNode data
- pos(){
- return this.layoutNode.pos;
- },
- dims(){
- return this.layoutNode.dims;
- },
- hidden(){
- return this.layoutNode.hidden;
- },
- hasFocus(){
- return this.layoutNode.hasFocus;
- },
- failFlag(){
- return this.layoutNode.failFlag;
- },
- },
- methods: {
- // Click handling
- onMouseDown(): void {
- this.highlight = false;
- if (!this.uiOpts.touchDevice){
- // Wait for a mouseup or click-hold
- clearTimeout(this.clickHoldTimer);
- this.clickHoldTimer = setTimeout(() => {
- this.clickHoldTimer = 0;
- this.onClickHold();
- }, this.uiOpts.clickHoldDuration);
- } else {
- // Wait for or recognise a double-click
- if (this.clickHoldTimer == 0){
- this.clickHoldTimer = setTimeout(() => {
- this.clickHoldTimer = 0;
- this.onClick();
- }, this.uiOpts.clickHoldDuration);
- } else {
- clearTimeout(this.clickHoldTimer)
- this.clickHoldTimer = 0;
- this.onDblClick();
- }
- }
- },
- onMouseUp(): void {
- if (!this.uiOpts.touchDevice){
- if (this.clickHoldTimer > 0){
- clearTimeout(this.clickHoldTimer);
- this.clickHoldTimer = 0;
- this.onClick();
- }
- }
- },
- onClick(): void {
- if (this.isLeaf && !this.isExpandableLeaf){
- console.log('Ignored click on non-expandable node');
- return;
- }
- this.wasClicked = true;
- this.$emit(this.isLeaf ? 'leaf-click' : 'nonleaf-click', this.layoutNode);
- },
- onClickHold(): void {
- if (this.isLeaf && !this.isExpandableLeaf){
- console.log('Ignored click-hold on non-expandable node');
- return;
- }
- this.$emit(this.isLeaf ? 'leaf-click-held' : 'nonleaf-click-held', this.layoutNode);
- },
- onDblClick(): void {
- this.onClickHold();
- },
- onInfoIconClick(evt: Event): void {
- this.$emit('info-click', this.layoutNode.name);
- },
- // Mouse-hover handling
- onMouseEnter(evt: Event): void {
- if ((!this.isLeaf || this.isExpandableLeaf) && !this.inTransition){
- this.highlight = true;
- }
- },
- onMouseLeave(evt: Event): void {
- this.highlight = false;
- },
- // Child event propagation
- onInnerLeafClick(node: LayoutNode): void {
- this.wasClicked = true;
- this.$emit('leaf-click', node);
- },
- onInnerNonleafClick(node: LayoutNode): void {
- this.wasClicked = true;
- this.$emit('nonleaf-click', node);
- },
- onInnerLeafClickHeld(node: LayoutNode): void {
- this.$emit('leaf-click-held', node);
- },
- onInnerNonleafClickHeld(node: LayoutNode): void {
- this.$emit('nonleaf-click-held', node);
- },
- onInnerInfoIconClick(nodeName: string): void {
- this.$emit('info-click', nodeName);
- },
- onScroll(evt: Event): void {
- if (this.pendingScrollHdlr == 0){
- this.pendingScrollHdlr = setTimeout(() => {
- this.scrollOffset = this.$el.scrollTop;
- this.pendingScrollHdlr = 0;
- }, this.uiOpts.animationDelay);
- }
- },
- // Other
- leafSubImgStyles(idx: number): Record<string,string> {
- let [w, h] = this.layoutNode.dims;
- return {
- width: '100%',
- height: '100%',
- // Image (and scrims)
- backgroundImage: (this.tolNode.imgName![idx]! != null) ?
- `${scrimGradient},url('${getImagePath(this.tolNode.imgName![idx]! as string)}')` :
- 'none',
- backgroundColor: this.uiOpts.bgColorDark,
- backgroundSize: '125%',
- borderRadius: 'inherit',
- clipPath: idx == 0 ? 'polygon(0 0, 100% 0, 0 100%)' : 'polygon(100% 0, 0 100%, 100% 100%)',
- backgroundPosition: idx == 0 ? `${-w/4}px ${-h/4}px` : '0px 0px',
- };
- },
- onTransitionEnd(evt: Event){
- if (this.inTransition){
- this.inTransition = false;
- this.wasClicked = false;
- this.hasExpanded = this.layoutNode.children.length > 0;
- }
- },
- triggerAnimation(animation: string){
- this.$el.classList.remove(animation);
- this.$el.offsetWidth; // Triggers reflow
- this.$el.classList.add(animation);
- },
- },
- watch: {
- // For setting transition state (allows external triggering, like via search and auto-mode)
- pos: {
- handler(newVal: [number, number], oldVal: [number, number]){
- let valChanged = newVal[0] != oldVal[0] || newVal[1] != oldVal[1];
- if (valChanged && this.uiOpts.transitionDuration > 100 && !this.inTransition){
- this.inTransition = true;
- setTimeout(this.onTransitionEnd, this.uiOpts.transitionDuration);
- }
- },
- deep: true,
- },
- dims: {
- handler(newVal: [number, number], oldVal: [number, number]){
- let valChanged = newVal[0] != oldVal[0] || newVal[1] != oldVal[1];
- if (valChanged && this.uiOpts.transitionDuration > 100 && !this.inTransition){
- this.inTransition = true;
- setTimeout(this.onTransitionEnd, this.uiOpts.transitionDuration);
- }
- },
- deep: true,
- },
- // For externally triggering fail animations (used by search and auto-mode)
- failFlag(){
- this.triggerAnimation(this.isLeaf ? 'animate-expand-shrink' : 'animate-shrink-expand');
- },
- // Scroll to focused child if overflownRoot
- hasFocusedChild(newVal: boolean, oldVal: boolean){
- if (newVal && this.isOverflownRoot){
- let focusedChild = this.layoutNode.children.find(n => n.hasFocus)!
- let bottomY = focusedChild.pos[1] + focusedChild.dims[1] + this.lytOpts.tileSpacing;
- let scrollTop = Math.max(0, bottomY - (this.overflownDim / 2)); // No need to manually cap at max
- this.$el.scrollTop = scrollTop;
- }
- },
- // Allow overflow temporarily after being unhidden
- hidden(newVal: boolean, oldVal: boolean){
- if (oldVal && !newVal){
- this.justUnhidden = true;
- setTimeout(() => {this.justUnhidden = false;}, this.uiOpts.transitionDuration + 100);
- }
- },
- // Used to 'flash' the tile when focused
- hasFocus(newVal: boolean, oldVal: boolean){
- if (newVal != oldVal && newVal){
- this.inFlash = true;
- setTimeout(() => {this.inFlash = false;}, this.uiOpts.transitionDuration);
- }
- },
- },
- name: 'tile', // Note: Need this to use self in template
- components: {InfoIcon, },
- emits: ['leaf-click', 'nonleaf-click', 'leaf-click-held', 'nonleaf-click-held', 'info-click', ],
-});
-</script>
-
-<style>
-/* For making a parent-swept-area div look continuous with the tile div */
-.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>