aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/App.vue92
-rw-r--r--src/components/AncestryBar.vue7
-rw-r--r--src/components/HelpModal.vue51
-rw-r--r--src/components/IconButton.vue2
-rw-r--r--src/components/SButton.vue2
-rw-r--r--src/components/SearchModal.vue27
-rw-r--r--src/components/SettingsModal.vue85
-rw-r--r--src/components/Tile.vue14
-rw-r--r--src/components/TileInfoModal.vue142
-rw-r--r--src/components/TutorialPane.vue24
-rw-r--r--src/components/icon/HelpIcon.vue9
-rw-r--r--src/lib.ts4
12 files changed, 257 insertions, 202 deletions
diff --git a/src/App.vue b/src/App.vue
index 68888ee..47bbd1e 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -21,7 +21,7 @@
</icon-button>
</div>
<!-- Content area -->
- <div :style="tutPaneContainerStyles"> <!-- Used to slide-in/out the tutorial pane -->
+ <div :style="tutPaneContainerStyles" class="z-10"> <!-- Used to slide-in/out the tutorial pane -->
<transition name="fade" @after-enter="tutPaneInTransition = false" @after-leave="tutPaneInTransition = false">
<tutorial-pane v-if="tutPaneOpen" :style="tutPaneStyles"
:uiOpts="uiOpts" :triggerFlag="tutTriggerFlag" :skipWelcome="!tutWelcome"
@@ -58,12 +58,12 @@
:uiOpts="uiOpts" class="z-10" @close="infoModalNodeName = null"/>
</transition>
<transition name="fade">
- <help-modal v-if="helpOpen" :uiOpts="uiOpts" class="z-10"
+ <help-modal v-if="helpOpen" :tutOpen="tutPaneOpen" :uiOpts="uiOpts" class="z-10"
@close="helpOpen = false" @start-tutorial="onStartTutorial"/>
</transition>
<settings-modal v-if="settingsOpen" :lytOpts="lytOpts" :uiOpts="uiOpts" class="z-10"
@close="settingsOpen = false" @reset="onResetSettings" @setting-chg="onSettingChg"/>
- <!-- Overlay used to prevent interaction and capture clicks -->
+ <!-- Overlay used to capture clicks during auto mode, etc -->
<div :style="{visibility: modeRunning != null ? 'visible' : 'hidden'}"
class="absolute left-0 top-0 w-full h-full" @click="modeRunning = null"></div>
</div>
@@ -87,7 +87,7 @@ import PauseIcon from './components/icon/PauseIcon.vue';
import SettingsIcon from './components/icon/SettingsIcon.vue';
import HelpIcon from './components/icon/HelpIcon.vue';
// Other
- // Note: Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain
+ // Note: Import paths lack a .ts or .js because .ts makes vue-tsc complain, and .js makes vite complain
import {TolNode, TolMap} from './tol';
import {LayoutNode, LayoutOptions, LayoutTreeChg,
initLayoutTree, initLayoutMap, tryLayout} from './layout';
@@ -97,7 +97,7 @@ import {arraySum, randWeightedChoice, timeout} from './util';
// Type representing auto-mode actions
type AutoAction = 'move across' | 'move down' | 'move up' | Action;
-// Function used in auto-mode to help avoid action cycles
+// Function used in auto-mode to reduce action cycles
function getReverseAction(action: AutoAction): AutoAction | null {
const reversePairs: AutoAction[][] = [
['move down', 'move up'],
@@ -111,11 +111,10 @@ function getReverseAction(action: AutoAction): AutoAction | null {
return null;
}
}
-// For options
export default defineComponent({
data(){
- // Initial tree-of-life data
+ // Create initial tree-of-life data
let initialTolMap: TolMap = new Map();
initialTolMap.set("", new TolNode());
let layoutTree = initLayoutTree(initialTolMap, "", 0);
@@ -160,7 +159,7 @@ export default defineComponent({
ancestryBarInTransition: false,
tutPaneInTransition: false,
// Other
- justInitialised: false, // Used to skip transition for tile initially loaded from server
+ justInitialised: false, // Used to skip transition for the tile initially loaded from server
changedSweepToParent: false, // Set during search animation for efficiency
excessTolNodeThreshold: 1000, // Threshold where excess tolMap entries get removed
};
@@ -187,11 +186,10 @@ export default defineComponent({
return {
color: this.uiOpts.textColor,
backgroundColor: this.uiOpts.altColorDark,
- aspectRatio: '1/1',
};
},
tutPaneContainerStyles(): Record<string,string> {
- if (this.uiOpts.breakpoint != 'lg'){
+ if (this.uiOpts.breakpoint == 'sm'){
return {
minHeight: (this.tutPaneOpen ? this.uiOpts.tutPaneSz : 0) + 'px',
maxHeight: (this.tutPaneOpen ? this.uiOpts.tutPaneSz : 0) + 'px',
@@ -204,7 +202,6 @@ export default defineComponent({
position: 'absolute',
bottom: '0.5cm',
right: '0.5cm',
- zIndex: '1',
visibility: this.tutPaneOpen ? 'visible' : 'hidden',
transitionProperty: 'visibility',
transitionDuration: this.uiOpts.transitionDuration + 'ms',
@@ -212,7 +209,7 @@ export default defineComponent({
}
},
tutPaneStyles(): Record<string,string> {
- if (this.uiOpts.breakpoint != 'lg'){
+ if (this.uiOpts.breakpoint == 'sm'){
return {
height: this.uiOpts.tutPaneSz + 'px',
}
@@ -344,7 +341,7 @@ export default defineComponent({
}
this.handleActionForTutorial('collapse');
this.setLastFocused(null);
- //
+ // Relayout
let success = tryLayout(this.activeRoot, this.tileAreaDims, this.lytOpts, {
allowCollapse: false,
chg: {type: 'collapse', node: layoutNode, tolMap: this.tolMap},
@@ -730,16 +727,16 @@ export default defineComponent({
this.settingsOpen = true;
},
async onSettingChg(optionType: OptionType, option: string,
- {save = true, relayout = false, reinit = false} = {}){
- if (save){
- if (optionType == 'LYT'){
- localStorage.setItem(`${optionType} ${option}`,
- String(this.lytOpts[option as keyof LayoutOptions]));
- } else if (optionType == 'UI') {
- localStorage.setItem(`${optionType} ${option}`,
- String(this.uiOpts[option as keyof UiOptions]));
- }
- }
+ {relayout = false, reinit = false} = {}){
+ // Save setting
+ if (optionType == 'LYT'){
+ localStorage.setItem(`${optionType} ${option}`,
+ String(this.lytOpts[option as keyof LayoutOptions]));
+ } else if (optionType == 'UI') {
+ localStorage.setItem(`${optionType} ${option}`,
+ String(this.uiOpts[option as keyof UiOptions]));
+ }
+ // Possibly relayout/reinitialise
if (reinit){
this.reInit();
} else if (relayout){
@@ -776,7 +773,6 @@ export default defineComponent({
this.tutTriggerAction = triggerAction;
},
onTutorialSkip(): void {
- // Remember to skip in future sessions
localStorage.setItem('UI tutorialSkip', String(true));
},
onStartTutorial(): void {
@@ -947,6 +943,30 @@ export default defineComponent({
}
return opts;
},
+ // For relayout
+ relayoutWithCollapse(secondPass = true): boolean {
+ let success;
+ if (this.overflownRoot){
+ success = tryLayout(this.activeRoot, this.tileAreaDims,
+ {...this.lytOpts, layoutType: 'sqr-overflow'}, {allowCollapse: false, layoutMap: this.layoutMap});
+ } else {
+ success = tryLayout(this.activeRoot, this.tileAreaDims, this.lytOpts,
+ {allowCollapse: true, layoutMap: this.layoutMap});
+ if (secondPass){
+ // Relayout again, which can help allocate remaining tiles 'evenly'
+ success = tryLayout(this.activeRoot, this.tileAreaDims, this.lytOpts,
+ {allowCollapse: false, layoutMap: this.layoutMap});
+ }
+ }
+ return success;
+ },
+ async updateAreaDims(){
+ let mainAreaEl = this.$refs.mainArea as HTMLElement;
+ this.mainAreaDims = [mainAreaEl.offsetWidth, mainAreaEl.offsetHeight];
+ await this.$nextTick(); // Wait until ancestor-bar is laid-out
+ let tileAreaEl = this.$refs.tileArea as HTMLElement;
+ this.tileAreaDims = [tileAreaEl.offsetWidth, tileAreaEl.offsetHeight];
+ },
// For transitions
relayoutDuringAncestryBarTransition(): void {
let timerId = setInterval(async () => {
@@ -980,30 +1000,6 @@ export default defineComponent({
}
}, this.uiOpts.transitionDuration + 300);
},
- // For relayout
- relayoutWithCollapse(secondPass = true): boolean {
- let success;
- if (this.overflownRoot){
- success = tryLayout(this.activeRoot, this.tileAreaDims,
- {...this.lytOpts, layoutType: 'sqr-overflow'}, {allowCollapse: false, layoutMap: this.layoutMap});
- } else {
- success = tryLayout(this.activeRoot, this.tileAreaDims, this.lytOpts,
- {allowCollapse: true, layoutMap: this.layoutMap});
- if (secondPass){
- // Relayout again, which can help allocate remaining tiles 'evenly'
- success = tryLayout(this.activeRoot, this.tileAreaDims, this.lytOpts,
- {allowCollapse: false, layoutMap: this.layoutMap});
- }
- }
- return success;
- },
- async updateAreaDims(){
- let mainAreaEl = this.$refs.mainArea as HTMLElement;
- this.mainAreaDims = [mainAreaEl.offsetWidth, mainAreaEl.offsetHeight];
- await this.$nextTick(); // Wait until ancestor-bar is laid-out
- let tileAreaEl = this.$refs.tileArea as HTMLElement;
- this.tileAreaDims = [tileAreaEl.offsetWidth, tileAreaEl.offsetHeight];
- },
// Other
resetMode(): void {
this.infoModalNodeName = null;
diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue
index cf9513f..e7ba8a5 100644
--- a/src/components/AncestryBar.vue
+++ b/src/components/AncestryBar.vue
@@ -26,6 +26,7 @@ export default defineComponent({
computed: {
imgSz(){
return this.breadth - this.lytOpts.tileSpacing - this.uiOpts.scrollGap;
+ // Intentionally omitting extra tileSpacing, to allow for scrollGap with less image shrinkage
},
dummyNodes(){ // Childless versions of 'nodes' used to parameterise <tile>s
return this.nodes.map(n => {
@@ -53,10 +54,10 @@ export default defineComponent({
watch: {
// Used to scroll to end of bar upon node/screen changes
nodes(){
- this.$nextTick(() => this.scrollToEnd()); // Without timeout, seems to run before new tiles are added
+ this.$nextTick(() => this.scrollToEnd());
},
vert(){
- setTimeout(() => this.scrollToEnd(), 0);
+ this.$nextTick(() => this.scrollToEnd());
},
},
methods: {
@@ -67,7 +68,7 @@ export default defineComponent({
onInfoIconClick(data: string){
this.$emit('info-click', data);
},
- // For converting vertical scroll to horizontal
+ // For converting vertical scrolling to horizontal
onWheelEvt(evt: WheelEvent){
if (!this.vert && Math.abs(evt.deltaX) < Math.abs(evt.deltaY)){
this.$el.scrollLeft -= (evt.deltaY > 0 ? -30 : 30);
diff --git a/src/components/HelpModal.vue b/src/components/HelpModal.vue
index 7a18c05..f2ae8a4 100644
--- a/src/components/HelpModal.vue
+++ b/src/components/HelpModal.vue
@@ -1,30 +1,34 @@
<template>
<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose">
- <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 w-4/5 p-4" :style="styles">
+ <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2
+ w-4/5 max-w-[20cm] max-h-[80%] overflow-auto" :style="styles">
<close-icon @click.stop="onClose" 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"/>
- <div class="mb-4">
- Lorem ipsum dolor sit amet, consectetur adipiscing
- elit, sed do eiusmod tempor incididunt ut labore
- et dolore magna aliqua. Ut enim ad minim veniam,
- quis nostrud exercitation ullamco laboris nisi ut
- aliquip ex ea commodo consequat.
+ class="block absolute top-1 right-1 md:top-2 md:right-2 w-8 h-8 hover:cursor-pointer"/>
+ <h1 class="text-center text-xl font-bold py-2 border-b border-stone-400">Help</h1>
+ <div class="p-2 border-b border-stone-400">
+ <div>
+ Lorem ipsum dolor sit amet, consectetur adipiscing
+ elit, sed do eiusmod tempor incididunt ut labore
+ et dolore magna aliqua.
+ </div>
+ <ul class="list-disc ml-3">
+ <li>What is the Tree of Life? ('tips', )</li>
+ <li>Representing the Tree
+ (OneZoom, iTol)
+ (tile layout, overflown-root, )
+ (leaf header colors, asterisk, compound nodes/names/images, )
+ </li>
+ <li>Data sources (OTOL, EOL, Wikipedia) (imprecision: tree, names, images, descs, )</li>
+ <li>Using tilo
+ (tutorial)
+ (settings: layout methods, reduced trees, click-label-to-reset)
+ (keyboard shortcuts)
+ </li>
+ <li>Project page, error contact, other credits (feathericons, ionicons), </li>
+ </ul>
</div>
- <div>
- Lorem ipsum dolor sit amet, consectetur adipiscing
- elit, sed do eiusmod tempor incididunt ut labore
- et dolore magna aliqua. Ut enim ad minim veniam,
- quis nostrud exercitation ullamco laboris nisi ut
- aliquip ex ea commodo consequat. Duis aute irure
- dolor in reprehenderit in voluptate velit esse
- cillum dolore eu fugiat nulla pariatur. Excepteur
- sint occaecat cupidatat non proident, sunt
- in culpa qui officia deserunt mollit anim id
- est laborum.
- </div>
- <s-button :style="{color: uiOpts.textColor, backgroundColor: uiOpts.bgColor}" @click.stop="onStartTutorial">
+ <s-button :disabled="tutOpen" class="mx-auto my-2"
+ :style="{color: uiOpts.textColor, backgroundColor: uiOpts.bgColor}" @click.stop="onStartTutorial">
Start Tutorial
</s-button>
</div>
@@ -39,6 +43,7 @@ import {UiOptions} from '../lib';
export default defineComponent({
props: {
+ tutOpen: {type: Boolean, default: false},
uiOpts: {type: Object as PropType<UiOptions>, required: true},
},
computed: {
diff --git a/src/components/IconButton.vue b/src/components/IconButton.vue
index 0a32095..0294c5b 100644
--- a/src/components/IconButton.vue
+++ b/src/components/IconButton.vue
@@ -1,6 +1,6 @@
<template>
<div class="p-2 rounded-full hover:cursor-pointer"
- :class="{'hover:brightness-125': !disabled, 'brightness-75': disabled}"
+ :class="{'hover:brightness-125': !disabled, 'brightness-50': disabled}"
:style="{width: size + 'px', height: size + 'px', padding: (size / 5) + 'px'}">
<slot class="w-full h-full">?</slot>
</div>
diff --git a/src/components/SButton.vue b/src/components/SButton.vue
index a480c37..54c7843 100644
--- a/src/components/SButton.vue
+++ b/src/components/SButton.vue
@@ -1,7 +1,7 @@
<template>
<button :disabled="disabled"
class="block rounded px-4 py-2"
- :class="{'hover:brightness-125': !disabled, 'brightness-75': disabled}">
+ :class="{'hover:brightness-125': !disabled, 'brightness-50': disabled}">
<slot>?</slot>
</button>
</template>
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index 4d39772..55e50a1 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -1,17 +1,17 @@
<template>
<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose">
- <div class="absolute left-1/2 -translate-x-1/2 top-1/4 w-3/4 max-w-[12cm] -translate-y-1/2 flex" :style="styles">
- <input type="text" class="block border p-1 rounded-l-[inherit] grow" ref="searchInput"
+ <div class="absolute left-1/2 -translate-x-1/2 top-1/4 -translate-y-1/2 min-w-3/4 md:min-w-[12cm] flex"
+ :style="styles">
+ <input type="text" class="block border p-1 px-2 rounded-l-[inherit] grow" ref="searchInput"
@keyup.enter="onSearch" @keyup.esc="onClose"
@input="onInput" @keydown.down.prevent="onDownKey" @keydown.up.prevent="onUpKey"/>
<div class="p-1 hover:cursor-pointer">
<search-icon @click.stop="onSearch" class="block w-8 h-8"/>
</div>
- <div class="absolute top-[100%] w-full"
- :style="{backgroundColor: uiOpts.bgColorAlt, color: uiOpts.textColorAlt}">
+ <div class="absolute top-[100%] w-full overflow-hidden" :style="suggContainerStyles">
<div v-for="(sugg, idx) of searchSuggs"
:style="{backgroundColor: idx == focusedSuggIdx ? uiOpts.bgColorAltDark : uiOpts.bgColorAlt}"
- class="border p-1 hover:underline hover:cursor-pointer flex"
+ class="border-b p-1 px-2 hover:underline hover:cursor-pointer flex"
@click="resolveSearch(sugg.canonicalName || sugg.name)">
<div class="grow overflow-hidden whitespace-nowrap text-ellipsis">
<span>{{suggDisplayStrings[idx][0]}}</span>
@@ -21,11 +21,10 @@
<info-icon class="hover:cursor-pointer my-auto w-5 h-5"
@click.stop="onInfoIconClick(sugg.canonicalName || sugg.name)"/>
</div>
- <div v-if="searchHadMoreSuggs" class="text-center border">...</div>
+ <div v-if="searchHadMoreSuggs" class="text-center">&#x2022; &#x2022; &#x2022;</div>
</div>
<label :style="animateLabelStyles" class="flex gap-1">
- <input type="checkbox" v-model="uiOpts.searchJumpMode"
- @change="$emit('setting-chg', 'searchJumpMode')"/>
+ <input type="checkbox" v-model="uiOpts.searchJumpMode" @change="$emit('setting-chg', 'searchJumpMode')"/>
<div class="text-sm">Jump to result</div>
</label>
</div>
@@ -43,7 +42,7 @@ import {getServerResponse, SearchSugg, SearchSuggResponse, UiOptions} from '../l
export default defineComponent({
props: {
- tolMap: {type: Object as PropType<TolMap>, required: true}, // Added to from a search response
+ tolMap: {type: Object as PropType<TolMap>, required: true}, // Upon a search response, gets new nodes added
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object as PropType<UiOptions>, required: true},
},
@@ -71,6 +70,14 @@ export default defineComponent({
boxShadow: this.uiOpts.shadowNormal,
};
},
+ suggContainerStyles(): Record<string,string> {
+ let br = this.uiOpts.borderRadius;
+ return {
+ backgroundColor: this.uiOpts.bgColorAlt,
+ color: this.uiOpts.textColorAlt,
+ borderRadius: `0 0 ${br}px ${br}px`,
+ };
+ },
animateLabelStyles(): Record<string,string> {
return {
position: 'absolute',
@@ -167,7 +174,7 @@ export default defineComponent({
onUpKey(){
if (this.focusedSuggIdx != null){
this.focusedSuggIdx = (this.focusedSuggIdx - 1 + this.searchSuggs.length) % this.searchSuggs.length;
- // The addition after -1 is to avoid becoming negative
+ // The addition after '-1' is to avoid becoming negative
}
},
// Search events
diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue
index 315ddb4..73b204d 100644
--- a/src/components/SettingsModal.vue
+++ b/src/components/SettingsModal.vue
@@ -1,15 +1,34 @@
<template>
<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose">
<div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2
- min-w-[8cm] max-h-[80%] overflow-auto p-3" :style="styles">
+ min-w-[8cm] max-w-[80%] max-h-[80%] overflow-auto" :style="styles">
<close-icon @click.stop="onClose" 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>
- <div class="border rounded p-1">
- <h2 class="text-center">Layout</h2>
- <div class="flex gap-2">
- <div class="grow">
- <div>Sweep leaves to side</div>
+ class="block absolute top-1 right-1 md:top-2 md:right-2 w-8 h-8 hover:cursor-pointer" />
+ <h1 class="text-xl md:text-2xl font-bold text-center py-2 border-b border-stone-400">Settings</h1>
+ <div class="border-b border-stone-400 pb-2">
+ <h2 class="font-bold md:text-xl text-center pt-1 md:pt-2 md:pb-1">Timing</h2>
+ <div class="grid grid-cols-[130px_minmax(0,1fr)_65px] gap-1 px-2 md:px-3">
+ <!-- Row 1 -->
+ <label for="animTimeInput" @click="onReset('UI', 'transitionDuration')" :class="rLabelClasses">
+ Animation Time
+ </label>
+ <input type="range" min="0" max="1000" v-model.number="uiOpts.transitionDuration"
+ @change="onSettingChg('UI', 'transitionDuration')" class="my-auto" name="animTimeInput"/>
+ <div class="my-auto text-right">{{uiOpts.transitionDuration}} ms</div>
+ <!-- Row 2 -->
+ <label for="autoDelayInput" @click="onReset('UI', 'autoActionDelay')" :class="rLabelClasses">
+ Auto-mode Delay
+ </label>
+ <input type="range" min="0" max="1000" v-model.number="uiOpts.autoActionDelay"
+ @change="onSettingChg('UI', 'autoActionDelay')" class="my-auto" name="autoDelayInput"/>
+ <div class="my-auto text-right">{{uiOpts.autoActionDelay}} ms</div>
+ </div>
+ </div>
+ <div class="border-b border-stone-400 pb-2">
+ <h2 class="font-bold md:text-xl text-center pt-1 md:pt-2 md:pb-1">Layout</h2>
+ <div class="flex gap-2 justify-around px-2 pb-1">
+ <div>
+ <div>Sweep leaves left</div>
<ul>
<li> <label> <input type="radio" v-model="sweepLeaves" :value="true"
@change="onSettingChg('LYT', 'layoutType')"/> Yes </label> </li>
@@ -17,7 +36,7 @@
@change="onSettingChg('LYT', 'layoutType')"/> No </label> </li>
</ul>
</div>
- <div class="grow">
+ <div>
<div>Sweep into parent</div>
<ul>
<li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="lytOpts.sweepToParent"
@@ -29,12 +48,13 @@
</ul>
</div>
</div>
- <div class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)_minmax(0,2fr)] gap-1">
+ <div class="grid grid-cols-[100px_minmax(0,1fr)_65px] gap-1 w-fit mx-auto px-2 md:px-3">
<!-- Row 1 -->
<label for="minTileSizeInput" @click="onReset('LYT', 'minTileSz')" :class="rLabelClasses">
Min Tile Size
</label>
- <input type="range" min="0" max="400" v-model.number="lytOpts.minTileSz"
+ <input type="range"
+ min="15" :max="uiOpts.breakpoint == 'sm' ? 150 : 200" v-model.number="lytOpts.minTileSz"
@input="onSettingChgThrottled('LYT', 'minTileSz')" @change="onSettingChg('LYT', 'minTileSz')"
name="minTileSizeInput" ref="minTileSzInput"/>
<div class="my-auto text-right">{{pxToDisplayStr(lytOpts.minTileSz)}}</div>
@@ -42,7 +62,7 @@
<label for="maxTileSizeInput" @click="onReset('LYT', 'maxTileSz')" :class="rLabelClasses">
Max Tile Size
</label>
- <input type="range" min="0" max="400" v-model.number="lytOpts.maxTileSz"
+ <input type="range" min="15" max="400" v-model.number="lytOpts.maxTileSz"
@input="onSettingChgThrottled('LYT', 'maxTileSz')" @change="onSettingChg('LYT', 'maxTileSz')"
name="maxTileSizeInput" ref="maxTileSzInput"/>
<div class="my-auto text-right">{{pxToDisplayStr(lytOpts.maxTileSz)}}</div>
@@ -56,32 +76,13 @@
<div class="my-auto text-right">{{pxToDisplayStr(lytOpts.tileSpacing)}}</div>
</div>
</div>
- <div class="border rounded p-1">
- <h2 class="text-center">Timing</h2>
- <div class="grid grid-cols-[minmax(0,3fr)_minmax(0,4fr)_minmax(0,2fr)] gap-1">
- <!-- Row 1 -->
- <label for="animTimeInput" @click="onReset('UI', 'transitionDuration')" :class="rLabelClasses">
- Animation Time
- </label>
- <input type="range" min="0" max="3000" v-model.number="uiOpts.transitionDuration"
- @change="onSettingChg('UI', 'transitionDuration')" class="my-auto" name="animTimeInput"/>
- <div class="my-auto text-right">{{uiOpts.transitionDuration}} ms</div>
- <!-- Row 2 -->
- <label for="autoDelayInput" @click="onReset('UI', 'autoActionDelay')" :class="rLabelClasses">
- Auto-mode Delay
- </label>
- <input type="range" min="0" max="3000" v-model.number="uiOpts.autoActionDelay"
- @change="onSettingChg('UI', 'autoActionDelay')" class="my-auto" name="autoDelayInput"/>
- <div class="my-auto text-right">{{uiOpts.autoActionDelay}} ms</div>
- </div>
- </div>
- <div class="border rounded p-1">
- <h2 class="text-center">Other</h2>
+ <div class="border-b border-stone-400 pb-2 px-2 md:px-3">
+ <h2 class="font-bold md:text-xl text-center pt-1 md:pt-2 -mb-2 ">Other</h2>
<div>
- <div>Tree to use</div>
- <ul>
+ Tree to use
+ <ul class="flex justify-evenly">
<li> <label> <input type="radio" v-model="uiOpts.tree" value="trimmed"
- @change="onSettingChg('UI', 'tree')"/> Comprehensive </label> </li>
+ @change="onSettingChg('UI', 'tree')"/> Complex </label> </li>
<li> <label> <input type="radio" v-model="uiOpts.tree" value="images"
@change="onSettingChg('UI', 'tree')"/> Visual </label> </li>
<li> <label> <input type="radio" v-model="uiOpts.tree" value="picked"
@@ -97,7 +98,7 @@
@change="onSettingChg('UI', 'disableShortcuts')"/> Disable keyboard shortcuts </label>
</div>
</div>
- <s-button class="mx-auto mt-2" :style="{color: uiOpts.textColor, backgroundColor: uiOpts.bgColor}"
+ <s-button class="mx-auto my-2" :style="{color: uiOpts.textColor, backgroundColor: uiOpts.bgColor}"
@click="onResetAll">
Reset
</s-button>
@@ -160,13 +161,13 @@ export default defineComponent({
let maxInput = this.$refs.maxTileSzInput as HTMLInputElement;
if (option == 'minTileSz' && Number(minInput.value) > Number(maxInput.value)){
this.lytOpts.maxTileSz = this.lytOpts.minTileSz;
- this.$emit('setting-chg', 'LYT', 'maxTileSz', {save: false});
+ this.$emit('setting-chg', 'LYT', 'maxTileSz');
} else if (option == 'maxTileSz' && Number(maxInput.value) < Number(minInput.value)){
this.lytOpts.minTileSz = this.lytOpts.maxTileSz;
- this.$emit('setting-chg', 'LYT', 'minTileSz', {save: false});
+ this.$emit('setting-chg', 'LYT', 'minTileSz');
}
}
- // Notify App
+ // Notify parent component
this.$emit('setting-chg', optionType, option,
{relayout: optionType == 'LYT', reinit: optionType == 'UI' && option == 'tree'});
// Possibly make saved-indicator appear/animate
@@ -188,6 +189,7 @@ export default defineComponent({
}
},
onReset(optionType: OptionType, option: string){
+ // Restore the setting's default
let defaultLytOpts = getDefaultLytOpts();
let defaultUiOpts = getDefaultUiOpts(defaultLytOpts);
if (optionType == 'LYT'){
@@ -206,6 +208,7 @@ export default defineComponent({
}
(this.uiOpts[uiOpt] as any) = defaultUiOpts[uiOpt];
}
+ // Notify parent component
this.onSettingChg(optionType, option);
},
onResetAll(){
@@ -215,7 +218,7 @@ export default defineComponent({
let needReinit = this.uiOpts.tree != defaultUiOpts.tree;
Object.assign(this.lytOpts, defaultLytOpts);
Object.assign(this.uiOpts, defaultUiOpts);
- // Notify App
+ // Notify parent component
this.$emit('reset', needReinit);
// Clear saved-indicator
this.saved = false;
diff --git a/src/components/Tile.vue b/src/components/Tile.vue
index 9b43467..ab1538f 100644
--- a/src/components/Tile.vue
+++ b/src/components/Tile.vue
@@ -17,14 +17,14 @@
@click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
</template>
</div>
- <div v-else :style="nonleafStyles" ref="nonleaf">
+ <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" ref="sepSweptArea">
+ <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">
@@ -162,7 +162,7 @@ export default defineComponent({
}
},
fontSz(): number {
- return 0.75 * this.lytOpts.headerSz;
+ return 0.8 * this.lytOpts.headerSz;
},
styles(): Record<string,string> {
let layoutStyles = {
@@ -234,10 +234,11 @@ export default defineComponent({
break;
}
}
+ let screenSz = this.uiOpts.breakpoint;
return {
height: this.lytOpts.headerSz + 'px',
- padding: ((this.lytOpts.headerSz - this.fontSz) / 2) + 'px',
- lineHeight: this.fontSz + 'px',
+ padding: `0 ${(this.lytOpts.headerSz - this.fontSz)}px`,
+ lineHeight: this.lytOpts.headerSz + 'px',
fontSize: this.fontSz + 'px',
color: textColor,
// For ellipsis
@@ -288,8 +289,7 @@ export default defineComponent({
},
nonleafHeaderTextStyles(): Record<string,string> {
return {
- margin: 'auto 0',
- lineHeight: this.fontSz + 'px',
+ lineHeight: this.lytOpts.headerSz + 'px',
fontSize: this.fontSz + 'px',
textAlign: 'center',
color: this.uiOpts.textColor,
diff --git a/src/components/TileInfoModal.vue b/src/components/TileInfoModal.vue
index f507f8d..6b7bd26 100644
--- a/src/components/TileInfoModal.vue
+++ b/src/components/TileInfoModal.vue
@@ -1,64 +1,96 @@
<template>
<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose">
- <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 w-4/5 max-h-[80%] p-4" :style="styles">
- <close-icon @click.stop="onClose" 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">
- {{getDisplayName(nodeName, tolNode)}}
- <div v-if="tolNode != null">
- ({{tolNode.children.length}} children, {{tolNode.tips}} tips,
+ <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2
+ max-w-[80%] min-w-[8cm] md:min-w-[15cm] max-h-[80%]"
+ :style="styles">
+ <div class="pb-1 md:pb-2">
+ <div class="flex">
+ <h1 class="text-center text-xl font-bold grow pt-2 pb-1 md:text-2xl md:pt-3 md:pb-1">
+ {{getDisplayName(nodeName, tolNode)}}
+ </h1>
+ <close-icon @click.stop="onClose" ref="closeIcon"
+ class="block m-1 md:m-2 w-8 h-8 hover:cursor-pointer"/>
+ </div>
+ <div class="flex justify-evenly text-sm md:text-base">
+ <div> Children: {{tolNode.children.length}} </div>
+ <div> Tips: {{tolNode.tips}} </div>
+ <div>
<a :href="'https://tree.opentreeoflife.org/opentree/argus/opentree13.4@' + tolNode.otolId"
- target="_blank">{{tolNode.otolId}}</a>)
+ target="_blank">OTOL <external-link-icon class="inline-block w-3 h-3"/></a>
+ </div>
+ </div>
+ <div v-if="nodes.length > 1" class="text-center text-sm px-2">
+ <div> (This is a compound node. The details below describe two descendants) </div>
</div>
- </h1>
- <hr class="mb-4 border-stone-400"/>
- <div v-if="nodes.length > 1">This is a compound node</div>
- <div v-for="idx in (nodes.length == 1 ? [0] : [0, 1])">
- <h1 v-if="nodes.length > 1" class="text-center font-bold">
+ </div>
+ <div v-for="idx in (nodes.length == 1 ? [0] : [0, 1])" class="border-t border-stone-400 p-2 md:p-3 clear-both">
+ <h1 v-if="nodes.length > 1" class="text-center font-bold mb-1">
{{getDisplayName(subNames![idx], nodes[idx])}}
</h1>
<div v-if="nodes[idx] == null" class="text-center">
- (This node has been trimmed away)
+ (This node was trimmed away duing tree simplification)
</div>
- <div v-else class="flex gap-1">
- <div class="w-1/2">
- <div v-if="imgInfos[idx] == null" :style="getImgStyles(nodes[idx])"/>
- <a v-else :href="imgInfos[idx]!.url" target="_blank">
+ <div v-else>
+ <div v-if="imgInfos[idx] != null" class="mt-1 mr-2 md:mb-2 md:mr-4 md:float-left">
+ <a :href="imgInfos[idx]!.url" target="_blank" class="block w-fit mx-auto">
<div :style="getImgStyles(nodes[idx])"/>
</a>
- <ul v-if="imgInfos[idx]! != null">
- <li>Obtained via: {{imgInfos[idx]!.src}}</li>
- <li>License:
- <a :href="licenseToUrl(imgInfos[idx]!.license)" target="_blank">
- <span class="underline">{{imgInfos[idx]!.license}}</span>
- <external-link-icon class="inline-block w-3 h-3 ml-1"/>
- </a>
- </li>
- <li v-if="imgInfos[idx]!.url != ''">
- <a :href="imgInfos[idx]!.url" target="_blank">
- <span class="underline">Source URL</span>
- <external-link-icon class="inline-block w-3 h-3 ml-1"/>
- </a>
- </li>
- <li v-if="imgInfos[idx]!.artist != ''">Artist: {{imgInfos[idx]!.artist}}</li>
- <li v-if="imgInfos[idx]!.credit != ''" class="overflow-auto">
- Credit: {{imgInfos[idx]!.credit}}
- </li>
- </ul>
+ <div class="relative">
+ <details class="w-fit mx-auto text-sm hover:cursor-pointer"
+ @click="onDetailsClick($event, idx)" ref="srcInfoDetails">
+ <summary>Source information</summary>
+ <transition name="fade" @after-leave="onSrcInfoHideTransition(idx)">
+ <div v-if="srcInfoShow[idx]" class="absolute left-1/2 -translate-x-1/2 w-max max-w-full
+ md:left-0 md:translate-x-0 md:max-w-[14cm] pb-2"> <!-- Provides bottom-padding -->
+ <ul class="rounded shadow p-1 overflow-auto"
+ :style="{backgroundColor: uiOpts.bgColor, color: uiOpts.textColor}">
+ <li v-if="imgInfos[idx]!.url != ''">
+ <span :style="{color: uiOpts.altColor}">Source: </span>
+ <a :href="imgInfos[idx]!.url" target="_blank">Link</a>
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </li>
+ <li v-if="imgInfos[idx]!.artist != ''">
+ <span :style="{color: uiOpts.altColor}">Artist: </span>
+ {{imgInfos[idx]!.artist}}
+ </li>
+ <li v-if="imgInfos[idx]!.credit != ''">
+ <span :style="{color: uiOpts.altColor}">Credits: </span>
+ {{imgInfos[idx]!.credit}}
+ </li>
+ <li>
+ <span :style="{color: uiOpts.altColor}">License: </span>
+ <a :href="licenseToUrl(imgInfos[idx]!.license)" target="_blank">
+ {{imgInfos[idx]!.license}}
+ </a>
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </li>
+ <li v-if="imgInfos[idx]!.src != 'picked'">
+ <span :style="{color: uiOpts.altColor}">Obtained via: </span>
+ <a v-if="imgInfos[idx]!.src == 'eol'" href="https://eol.org/">EoL</a>
+ <a v-else href="https://www.wikipedia.org/">Wikipedia</a>
+ <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ </li>
+ <li>
+ <span :style="{color: uiOpts.altColor}">Changes: </span>
+ Cropped and resized
+ </li>
+ </ul>
+ </div>
+ </transition>
+ </details>
+ </div>
</div>
<div v-if="descInfos[idx]! != null">
- <div>
- Redirected: {{descInfos[idx]!.fromRedirect}} <br/>
- Short-description from {{descInfos[idx]!.fromDbp ? 'DBpedia' : 'Wikipedia'}} <br/>
+ <div>{{descInfos[idx]!.text}}</div>
+ <div class="text-sm text-right">
+ From
<a :href="'https://en.wikipedia.org/?curid=' + descInfos[idx]!.wikiId" target="_blank">
- <span class="underline">Wikipedia Link</span>
- <external-link-icon class="inline-block w-3 h-3 ml-1"/>
+ Wikipedia
</a>
+ <external-link-icon class="inline-block w-3 h-3"/>
</div>
- <hr/>
- <div>{{descInfos[idx]!.text}}</div>
</div>
- <div v-else>
+ <div v-else class="text-center">
(No description found)
</div>
</div>
@@ -85,6 +117,11 @@ export default defineComponent({
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object as PropType<UiOptions>, required: true},
},
+ data(){
+ return {
+ srcInfoShow: [false, false], // For transitioning source-info panes when enclosing <details> are clicked
+ };
+ },
computed: {
tolNode(): TolNode {
return this.infoResponse.nodeInfo.tolNode;
@@ -138,8 +175,8 @@ export default defineComponent({
imgName = tolNode.imgName;
}
return {
- width: this.lytOpts.maxTileSz + 'px',
- height: this.lytOpts.maxTileSz + 'px',
+ width: '200px',
+ height: '200px',
backgroundImage: imgName != null ?
`url('${getImagePath(imgName as string)}')` :
'none',
@@ -184,6 +221,17 @@ export default defineComponent({
this.$emit('close');
}
},
+ onDetailsClick(evt: Event, idx: number){
+ this.srcInfoShow[idx] = !this.srcInfoShow[idx];
+ if (this.srcInfoShow[idx] == false){
+ evt.preventDefault(); // Delay <details> closure, to allow for transition
+ }
+ },
+ onSrcInfoHideTransition(idx: number){
+ // Close <details>, now that it's content has transitioned away
+ let details = this.$refs.srcInfoDetails[idx];
+ details.removeAttribute('open');
+ },
},
components: {CloseIcon, ExternalLinkIcon, },
emits: ['close', ],
diff --git a/src/components/TutorialPane.vue b/src/components/TutorialPane.vue
index 289f386..430db8a 100644
--- a/src/components/TutorialPane.vue
+++ b/src/components/TutorialPane.vue
@@ -1,12 +1,10 @@
<template>
-<div :style="styles" class="p-2 flex flex-col justify-between">
- <div class="flex">
- <h2 class="text-center mb-2 mx-auto">
- {{stage == 0 ? 'Welcome' : `Tutorial (Step ${stage} of ${lastStage})`}}
- </h2>
- <close-icon @click.stop="onClose"
- class="block w-6 h-6 hover:cursor-pointer"/>
- </div>
+<div :style="styles" class="relative flex flex-col justify-between">
+ <close-icon @click.stop="onClose"
+ class="block absolute top-2 right-2 w-8 h-8 hover:cursor-pointer"/>
+ <h1 class="text-center text-lg font-bold pt-3 pb-2">
+ {{stage == 0 ? 'Welcome' : `Tutorial (Step ${stage} of ${lastStage})`}}
+ </h1>
<transition name="fade" mode="out-in">
<div v-if="stage == 0" :style="contentStyles">
This is a visualiser for the biological Tree of Life. For more information,
@@ -31,7 +29,7 @@
{{touchDevice ? 'Tap' : 'Click'}} the icon on a tile's bottom-right to
bring up more information
<span class="block text-sm brightness-50">
- For an expanded tile, it's at the header's right
+ For an expanded tile, it's on the header's right
</span>
</div>
<div v-else-if="stage == 6" :style="contentStyles">
@@ -47,11 +45,11 @@
{{touchDevice ? 'Tap' : 'Click'}} the settings icon
</div>
<div v-else-if="stage == 9" :style="contentStyles">
- And finally, tap the help icon for more information
+ And finally, {{touchDevice ? 'tap' : 'click'}} the help icon for more information
</div>
</transition>
<!-- Buttons -->
- <div class="w-full flex justify-evenly mt-2">
+ <div class="w-full my-2 flex justify-evenly">
<template v-if="stage == 0">
<s-button :style="buttonStyles" @click="onStartTutorial">Start Tutorial</s-button>
<s-button :style="buttonStyles" @click="onSkipTutorial">Skip</s-button>
@@ -88,12 +86,12 @@ export default defineComponent({
stage: 0, // Indicates the current step of the tutorial (stage 0 is the welcome message)
lastStage: 9,
disabledOnce: false, // Set to true after disabling features at stage 1
- hidNextPrevOnce: false, // Used to hide prev/next buttons when initially at stage 1
stageActions: [
// Specifies, for stages 1+, what action to enable (can repeat an action to enable nothing new)
'expand', 'collapse', 'expandToView', 'unhideAncestor',
'tileInfo', 'search', 'autoMode', 'settings', 'help',
] as Action[],
+ hidNextPrevOnce: false, // Used to hide prev/next buttons when initially at stage 1
};
},
computed: {
@@ -106,8 +104,6 @@ export default defineComponent({
contentStyles(): Record<string,string> {
return {
padding: '0 0.5cm',
- maxWidth: '15cm',
- margin: '0 auto',
overflow: 'auto',
textAlign: 'center',
};
diff --git a/src/components/icon/HelpIcon.vue b/src/components/icon/HelpIcon.vue
index c8e4ca2..8486686 100644
--- a/src/components/icon/HelpIcon.vue
+++ b/src/components/icon/HelpIcon.vue
@@ -1,9 +1,8 @@
<template>
-<svg viewBox="0 0 24 24" fill="none"
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <circle cx="12" cy="12" r="10"/>
- <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
- <line x1="12" y1="17" x2="12.01" y2="17"/>
+<svg viewBox="0 0 512 512">
+ <path d="M160 164s1.44-33 33.54-59.46C212.6 88.83 235.49 84.28 256 84c18.73-.23 35.47 2.94 45.48 7.82C318.59 100.2 352 120.6 352 164c0 45.67-29.18 66.37-62.35 89.18S248 298.36 248 324"
+ fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="45"/>
+ <circle cx="248" cy="430" r="32" fill="currentColor"/>
</svg>
</template>
diff --git a/src/lib.ts b/src/lib.ts
index 73c4c03..8404725 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -118,7 +118,7 @@ export function getDefaultLytOpts(): LayoutOptions {
let screenSz = getBreakpoint();
return {
tileSpacing: screenSz == 'sm' ? 6 : 10, //px
- headerSz: 22, // px
+ headerSz: screenSz == 'sm' ? 18 : 22, // px
minTileSz: 50, // px
maxTileSz: 200, // px
// Layout-algorithm related
@@ -157,7 +157,7 @@ export function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions {
nonleafHeaderColor: bgColorDark,
ancestryBarBgColor: bgColorLight,
// Component sizing
- ancestryBarBreadth: lytOpts.maxTileSz / 2 + lytOpts.tileSpacing*2, // px
+ ancestryBarBreadth: (screenSz == 'sm' ? 80 : 100) + lytOpts.tileSpacing*2, // px
tutPaneSz: 180, // px
scrollGap,
// Timing related