diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-09-13 19:59:06 +1000 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-09-13 20:00:17 +1000 |
| commit | 23b5cc80ba02936659564dd03b173d3214ce5978 (patch) | |
| tree | cdf6a183d1a0bfcb45a924585b764c723dd67b55 /src/components/TolTile.vue | |
| parent | e382d4173c990a49a9ef3db1b3681763a3e2e908 (diff) | |
Use Vue Composition API and ESLint
Diffstat (limited to 'src/components/TolTile.vue')
| -rw-r--r-- | src/components/TolTile.vue | 954 |
1 files changed, 456 insertions, 498 deletions
diff --git a/src/components/TolTile.vue b/src/components/TolTile.vue index afb6616..d3b3539 100644 --- a/src/components/TolTile.vue +++ b/src/components/TolTile.vue @@ -1,5 +1,5 @@ <template> -<div :style="styles" @scroll="onScroll"> +<div :style="styles" @scroll="onScroll" ref="rootRef"> <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"> @@ -48,512 +48,470 @@ </div> </template> -<script lang="ts"> -import {defineComponent, PropType} from 'vue'; +<script setup lang="ts"> +import {ref, computed, watch, PropType} from 'vue'; import InfoIcon from './icon/InfoIcon.vue'; -import {TolNode, TolMap} from '../tol'; +import {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%)'; +const SCRIM_GRADIENT = '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(){ +// Refs +const rootRef = ref(null as HTMLDivElement | null); + +// Props + events +const props = defineProps({ + 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 +}); +const emit = defineEmits(['leaf-click', 'nonleaf-click', 'leaf-click-held', 'nonleaf-click-held', 'info-click']); + +// Data computed from props +const tolNode = computed(() => props.tolMap.get(props.layoutNode.name)!); +const visibleChildren = computed((): LayoutNode[] => { // Used to reduce slowdown from rendering many nodes + let children = props.layoutNode.children; + // If not displaying with overflow, return 'visible' layoutNode children + if (!isOverflownRoot.value){ + 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] >= scrollOffset.value){ + firstIdx = i; + break; + } + } + let lastIdx = children.length; + for (let i = firstIdx + 1; i < children.length; i++){ + if (children[i].pos[1] > scrollOffset.value + props.overflownDim){ + lastIdx = i; + break; + } + } + return children.slice(firstIdx, lastIdx); +}); +const isLeaf = computed(() => props.layoutNode.children.length == 0); +const isExpandableLeaf = computed(() => isLeaf.value && tolNode.value.children.length > 0); +const showNonleafHeader = computed(() => + (props.layoutNode.showHeader && props.layoutNode.sepSweptArea == null) || + (props.layoutNode.sepSweptArea != null && props.layoutNode.sepSweptArea.sweptLeft) +); +const displayName = computed((): string => { + let newName = capitalizeWords(tolNode.value.commonName || props.layoutNode.name); + if (!tolNode.value.pSupport && tolNode.value.parent != null){ + newName += '*'; + } + return newName; +}); +const hasOneImage = computed(() => !Array.isArray(tolNode.value.imgName)); +const isOverflownRoot = computed(() => + props.overflownDim > 0 && !props.layoutNode.hidden && props.layoutNode.children.length > 0 +); +const hasFocusedChild = computed(() => props.layoutNode.children.some(n => n.hasFocus)); +const infoIconDisabled = computed(() => !props.uiOpts.disabledActions.has('tileInfo')); + +// Click/hold handling +const clickHoldTimer = ref(0); // Used to recognise click-and-hold events +function onMouseDown(): void { + highlight.value = false; + if (!props.uiOpts.touchDevice){ + // Wait for a mouseup or click-hold + clearTimeout(clickHoldTimer.value); + clickHoldTimer.value = setTimeout(() => { + clickHoldTimer.value = 0; + onClickHold(); + }, props.uiOpts.clickHoldDuration); + } else { + // Wait for or recognise a double-click + if (clickHoldTimer.value == 0){ + clickHoldTimer.value = setTimeout(() => { + clickHoldTimer.value = 0; + onClick(); + }, props.uiOpts.clickHoldDuration); + } else { + clearTimeout(clickHoldTimer.value) + clickHoldTimer.value = 0; + onDblClick(); + } + } +} +function onMouseUp(): void { + if (!props.uiOpts.touchDevice){ + if (clickHoldTimer.value > 0){ + clearTimeout(clickHoldTimer.value); + clickHoldTimer.value = 0; + onClick(); + } + } +} + +// Click-action handling +const wasClicked = ref(false); // Used to increase z-index during transition after this tile (or a child) is clicked +function onClick(): void { + if (isLeaf.value && !isExpandableLeaf.value){ + console.log('Ignored click on non-expandable node'); + return; + } + wasClicked.value = true; + emit(isLeaf.value ? 'leaf-click' : 'nonleaf-click', props.layoutNode); +} +function onClickHold(): void { + if (isLeaf.value && !isExpandableLeaf.value){ + console.log('Ignored click-hold on non-expandable node'); + return; + } + emit(isLeaf.value ? 'leaf-click-held' : 'nonleaf-click-held', props.layoutNode); +} +function onDblClick(): void { + onClickHold(); +} +function onInfoIconClick(): void { + emit('info-click', props.layoutNode.name); +} +// Child click-action propagation +function onInnerLeafClick(node: LayoutNode): void { + wasClicked.value = true; + emit('leaf-click', node); +} +function onInnerNonleafClick(node: LayoutNode): void { + wasClicked.value = true; + emit('nonleaf-click', node); +} +function onInnerLeafClickHeld(node: LayoutNode): void { + emit('leaf-click-held', node); +} +function onInnerNonleafClickHeld(node: LayoutNode): void { + emit('nonleaf-click-held', node); +} +function onInnerInfoIconClick(nodeName: string): void { + emit('info-click', nodeName); +} + +// Mouse-hover handling +const highlight = ref(false); // Used to draw a colored outline on mouse hover +function onMouseEnter(): void { + if ((!isLeaf.value || isExpandableLeaf.value) && !inTransition.value){ + highlight.value = true; + } +} +function onMouseLeave(): void { + highlight.value = false; +} + +// Scrolling if overflownRoot +const scrollOffset = ref(0); // Used to track scroll offset when displaying with overflow +const pendingScrollHdlr = ref(0); // Used for throttling updating of scrollOffset +function onScroll(): void { + if (pendingScrollHdlr.value == 0){ + pendingScrollHdlr.value = setTimeout(() => { + scrollOffset.value = rootRef.value!.scrollTop; + pendingScrollHdlr.value = 0; + }, props.uiOpts.animationDelay); + } +} +// Scroll to focused child if overflownRoot +watch(hasFocusedChild, (newVal: boolean) => { + if (newVal && isOverflownRoot.value){ + let focusedChild = props.layoutNode.children.find(n => n.hasFocus)! + let bottomY = focusedChild.pos[1] + focusedChild.dims[1] + props.lytOpts.tileSpacing; + let scrollTop = Math.max(0, bottomY - (props.overflownDim / 2)); // No need to manually cap at max + rootRef.value!.scrollTop = scrollTop; + } +}); + +// Transition related +const inTransition = ref(false); // Used to avoid content overlap and overflow during 'user-perceivable' transitions +const hasExpanded = ref(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 +function onTransitionEnd(){ + if (inTransition.value){ + inTransition.value = false; + wasClicked.value = false; + hasExpanded.value = props.layoutNode.children.length > 0; + } +} +// For setting transition state (allows external triggering, like via search and auto-mode) +watch(() => props.layoutNode.pos, (newVal: [number, number], oldVal: [number, number]) => { + let valChanged = newVal[0] != oldVal[0] || newVal[1] != oldVal[1]; + if (valChanged && props.uiOpts.transitionDuration > 100 && !inTransition.value){ + inTransition.value = true; + setTimeout(onTransitionEnd, props.uiOpts.transitionDuration); + } +}); +watch(() => props.layoutNode.dims, (newVal: [number, number], oldVal: [number, number]) => { + let valChanged = newVal[0] != oldVal[0] || newVal[1] != oldVal[1]; + if (valChanged && props.uiOpts.transitionDuration > 100 && !inTransition.value){ + inTransition.value = true; + setTimeout(onTransitionEnd, props.uiOpts.transitionDuration); + } +}); + +// For externally triggering fail animations (used by search and auto-mode) +function triggerAnimation(animation: string){ + let el = rootRef.value!; + el.classList.remove(animation); + el.offsetWidth; // Triggers reflow + el.classList.add(animation); +} +watch(() => props.layoutNode.failFlag, () => + triggerAnimation(isLeaf.value ? 'animate-expand-shrink' : 'animate-shrink-expand') +); + +// For 'flashing' the tile when focused +const inFlash = ref(false); // Used to 'flash' the tile when focused +watch(() => props.layoutNode.hasFocus, (newVal: boolean, oldVal: boolean) => { + if (newVal != oldVal && newVal){ + inFlash.value = true; + setTimeout(() => {inFlash.value = false;}, props.uiOpts.transitionDuration); + } +}); + +// For temporarily enabling overflow after being unhidden +const justUnhidden = ref(false); // Used to allow overflow temporarily after being unhidden +watch(() => props.layoutNode.hidden, (newVal: boolean, oldVal: boolean) => { + if (oldVal && !newVal){ + justUnhidden.value = true; + setTimeout(() => {justUnhidden.value = false}, props.uiOpts.transitionDuration + 100); + } +}); + +// Styles + classes +const nonleafBgColor = computed(() => { + let colorArray = props.uiOpts.nonleafBgColors; + return colorArray[props.layoutNode.depth % colorArray.length]; +}); +const boxShadow = computed((): string => { + if (highlight.value){ + return props.uiOpts.shadowHovered; + } else if (props.layoutNode.hasFocus && !inTransition.value){ + return props.uiOpts.shadowFocused; + } else { + return props.uiOpts.shadowNormal; + } +}); +const fontSz = computed((): number => { + // These values are a compromise between dynamic font size and code simplicity + if (props.layoutNode.dims[0] >= 150){ + return props.lytOpts.headerSz * 0.8; + } else if (props.layoutNode.dims[0] >= 80){ + return props.lytOpts.headerSz * 0.7; + } else { + return props.lytOpts.headerSz * 0.6; + } +}); +// +const styles = computed((): Record<string,string> => { + let layoutStyles = { + position: 'absolute', + left: props.layoutNode.pos[0] + 'px', + top: props.layoutNode.pos[1] + 'px', + width: props.layoutNode.dims[0] + 'px', + height: props.layoutNode.dims[1] + 'px', + borderRadius: props.uiOpts.borderRadius + 'px', + boxShadow: boxShadow.value, + visibility: 'visible', + // Transition related + transitionDuration: (props.skipTransition ? 0 : props.uiOpts.transitionDuration) + 'ms', + transitionProperty: 'left, top, width, height, visibility', + transitionTimingFunction: 'ease-out', + zIndex: inTransition.value && wasClicked.value ? '1' : '0', + overflow: (inTransition.value && !isLeaf.value && !hasExpanded.value && !justUnhidden.value) ? + 'hidden' : 'visible', + // CSS variables + '--nonleafBgColor': nonleafBgColor.value, + '--tileSpacing': props.lytOpts.tileSpacing + 'px', + }; + if (!isLeaf.value){ + let borderR = props.uiOpts.borderRadius + 'px'; + if (props.layoutNode.sepSweptArea != null){ + borderR = props.layoutNode.sepSweptArea.sweptLeft ? + `${borderR} ${borderR} ${borderR} 0` : + `${borderR} 0 ${borderR} ${borderR}`; + } + layoutStyles.borderRadius = borderR; + } + if (isOverflownRoot.value){ + layoutStyles.width = (props.layoutNode.dims[0] + props.uiOpts.scrollGap) + 'px'; + layoutStyles.height = props.overflownDim + 'px'; + layoutStyles.overflow = 'hidden scroll'; + } + if (props.layoutNode.hidden){ + layoutStyles.left = layoutStyles.top = layoutStyles.width = layoutStyles.height = '0'; + layoutStyles.visibility = 'hidden'; + } + if (props.nonAbsPos){ + layoutStyles.position = 'static'; + } + return layoutStyles; +}); +const leafStyles = computed((): Record<string,string> => { + let styles: Record<string,string> = { + borderRadius: 'inherit', + }; + if (hasOneImage.value){ + styles = { + ...styles, + backgroundImage: tolNode.value.imgName != null ? + `${SCRIM_GRADIENT},url('${getImagePath(tolNode.value.imgName as string)}')` : + 'none', + backgroundColor: props.uiOpts.bgColorDark, + backgroundSize: 'cover', + }; + } + return styles; +}); +const leafHeaderStyles = computed((): Record<string,string> => { + let numChildren = tolNode.value.children.length; + let textColor = props.uiOpts.textColor; + for (let [threshold, color] of props.uiOpts.childQtyColors){ + if (numChildren >= threshold){ + textColor = color; + } else { + break; + } + } + return { + lineHeight: (fontSz.value * 1.3) + 'px', + fontSize: fontSz.value + 'px', + paddingLeft: (fontSz.value * 0.2) + 'px', + color: textColor, + // For ellipsis + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }; +}); +function leafSubImgStyles(idx: number): Record<string,string> { + let [w, h] = props.layoutNode.dims; + return { + width: '100%', + height: '100%', + // Image (and scrims) + backgroundImage: (tolNode.value.imgName![idx]! != null) ? + `${SCRIM_GRADIENT},url('${getImagePath(tolNode.value.imgName![idx]! as string)}')` : + 'none', + backgroundColor: props.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', + }; +} +const leafFirstImgStyles = computed(() => leafSubImgStyles(0)); +const leafSecondImgStyles = computed(() => leafSubImgStyles(1)); +const nonleafStyles = computed((): Record<string,string> => { + let styles = { + width: '100%', + height: '100%', + backgroundColor: nonleafBgColor.value, + borderRadius: 'inherit', + }; + if (isOverflownRoot.value){ + styles.width = props.layoutNode.dims[0] + 'px'; + styles.height = props.layoutNode.dims[1] + 'px'; + } + return styles; +}); +const nonleafHeaderStyles = computed((): Record<string,string> => { + let styles: Record<string,string> = { + position: 'static', + height: props.lytOpts.headerSz + 'px', + borderTopLeftRadius: 'inherit', + borderTopRightRadius: 'inherit', + backgroundColor: props.uiOpts.nonleafHeaderColor, + }; + if (isOverflownRoot.value){ + styles = { + ...styles, + position: 'sticky', + top: '0', + left: '0', + borderTopRightRadius: '0', + zIndex: '1', + boxShadow: props.uiOpts.shadowNormal, + }; + } + return styles; +}); +const nonleafHeaderTextStyles = computed(() => ({ + lineHeight: (fontSz.value * 1.3) + 'px', + fontSize: fontSz.value + 'px', + paddingLeft: (fontSz.value * 0.2) + 'px', + textAlign: 'center', + color: props.uiOpts.textColor, + // For ellipsis + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +})); +const sepSweptAreaStyles = computed((): Record<string,string> => { + let borderR = props.uiOpts.borderRadius + 'px'; + let styles = { + position: 'absolute', + backgroundColor: nonleafBgColor.value, + boxShadow: boxShadow.value, + transitionDuration: props.uiOpts.transitionDuration + 'ms', + transitionProperty: 'left, top, width, height, visibility', + transitionTimingFunction: 'ease-out', + }; + let area = props.layoutNode.sepSweptArea; + if (!props.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 { - // 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 + ...styles, + visibility: 'hidden', + left: '0', + top: props.lytOpts.headerSz + 'px', + width: '0', + height: '0', + borderRadius: borderR, }; - }, - 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: 'tol-tile', // Note: Need this to use self in template - components: {InfoIcon, }, - emits: ['leaf-click', 'nonleaf-click', 'leaf-click-held', 'nonleaf-click-held', 'info-click', ], + } +}); +const sepSweptAreaHideEdgeClass = computed((): string => { + if (props.layoutNode.sepSweptArea == null){ + return ''; + } else if (props.layoutNode.sepSweptArea.sweptLeft){ + return 'hide-right-edge'; + } else { + return 'hide-top-edge'; + } +}); +const infoIconStyles = computed((): Record<string,string> => { + let size = (props.lytOpts.headerSz * 0.85); + let marginSz = (props.lytOpts.headerSz - size); + return { + width: size + 'px', + height: size + 'px', + minWidth: size + 'px', + minHeight: size + 'px', + margin: isLeaf.value ? `auto ${marginSz}px ${marginSz}px auto` : `auto ${marginSz}px auto 0`, + }; }); +const infoIconClasses = 'text-white/30 hover:text-white hover:cursor-pointer'; </script> <style scoped> |
