aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2022-09-14 19:17:41 +1000
committerTerry Truong <terry06890@gmail.com>2022-09-14 20:29:01 +1000
commit8b5538e0a55a83b1ff190cd5ad689827777e73a7 (patch)
tree33ccb2eadbe9a53dc2a870d57ba5efe758592390
parent556d6c953e74996e0ae6a8328e352ab43735f993 (diff)
Use Pinia to store user settings, palette colors, etc
Move uiOpts and lytOpts to store.ts Add 'const's to *.ts
-rw-r--r--.eslintrc.js3
-rw-r--r--package.json1
-rw-r--r--src/App.vue244
-rw-r--r--src/components/AncestryBar.vue34
-rw-r--r--src/components/HelpModal.vue29
-rw-r--r--src/components/LoadingModal.vue17
-rw-r--r--src/components/SButton.vue2
-rw-r--r--src/components/SearchModal.vue40
-rw-r--r--src/components/SettingsModal.vue192
-rw-r--r--src/components/TileInfoModal.vue43
-rw-r--r--src/components/TolTile.vue89
-rw-r--r--src/components/TutorialPane.vue21
-rw-r--r--src/layout.ts204
-rw-r--r--src/lib.ts125
-rw-r--r--src/main.ts5
-rw-r--r--src/store.ts226
-rw-r--r--src/util.ts24
17 files changed, 670 insertions, 629 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index eb86580..11903bd 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -21,6 +21,7 @@ module.exports = {
"@typescript-eslint"
],
"rules": {
- "@typescript-eslint/no-non-null-assertion": "off"
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "no-constant-condition": "off"
}
}
diff --git a/package.json b/package.json
index 773d30a..525bc93 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"author": "Terry Truong",
"license": "MIT",
"dependencies": {
+ "pinia": "^2.0.22",
"vue": "^3.2.25"
},
"devDependencies": {
diff --git a/src/App.vue b/src/App.vue
index 01f41a2..9e8bd8f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,8 +1,8 @@
<template>
<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden flex flex-col"
- :style="{backgroundColor: uiOpts.bgColor, scrollbarColor: uiOpts.altColorDark + ' ' + uiOpts.bgColorDark}">
+ :style="{backgroundColor: store.color.bg, scrollbarColor: store.color.altDark + ' ' + store.color.bgDark}">
<!-- Title bar -->
- <div class="flex shadow gap-2 p-2" :style="{backgroundColor: uiOpts.bgColorDark2, color: uiOpts.altColor}">
+ <div class="flex shadow gap-2 p-2" :style="{backgroundColor: store.color.bgDark2, color: store.color.alt}">
<h1 class="my-auto ml-2 text-4xl hover:cursor-pointer" @click="collapseTree" title="Reset tree">Tilo</h1>
<div class="mx-auto"/> <!-- Spacer -->
<!-- Icons -->
@@ -30,20 +30,19 @@
<transition name="fade">
<tutorial-pane v-if="tutPaneOpen" :style="tutPaneStyles"
:actionsDone="actionsDone" :triggerFlag="tutTriggerFlag" :skipWelcome="!tutWelcome"
- :uiOpts="uiOpts" @close="onTutPaneClose" @skip="onTutorialSkip" @stage-chg="onTutStageChg"/>
+ @close="onTutPaneClose" @skip="onTutorialSkip" @stage-chg="onTutStageChg"/>
</transition>
</div>
<div :class="['flex', wideMainArea ? 'flex-row' : 'flex-col', 'grow', 'min-h-0']"> <!-- 'Main area' -->
<div :style="ancestryBarContainerStyles"> <!-- Used to slide-in/out the ancestry-bar -->
<transition name="fade">
<ancestry-bar v-if="detachedAncestors != null" class="w-full h-full"
- :nodes="detachedAncestors" :vert="wideMainArea" :breadth="uiOpts.ancestryBarBreadth"
- :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
- @ancestor-click="onDetachedAncestorClick" @info-click="onInfoClick"/>
+ :nodes="detachedAncestors" :vert="wideMainArea" :breadth="store.ancestryBarBreadth"
+ :tolMap="tolMap" @ancestor-click="onDetachedAncestorClick" @info-click="onInfoClick"/>
</transition>
</div>
- <div class="relative grow" :style="{margin: lytOpts.tileSpacing + 'px'}"> <!-- 'Tile area' -->
- <tol-tile :layoutNode="layoutTree" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ <div class="relative grow" :style="{margin: store.lytOpts.tileSpacing + 'px'}"> <!-- 'Tile area' -->
+ <tol-tile :layoutNode="layoutTree" :tolMap="tolMap"
:overflownDim="overflownRoot ? tileAreaDims[1] : 0" :skipTransition="justInitialised"
@leaf-click="onLeafClick" @nonleaf-click="onNonleafClick"
@leaf-click-held="onLeafClickHeld" @nonleaf-click-held="onNonleafClickHeld"
@@ -51,7 +50,7 @@
</div>
</div>
<transition name="fade">
- <icon-button v-if="!tutPaneOpen && !uiOpts.tutorialSkip" @click="onStartTutorial" title="Start Tutorial"
+ <icon-button v-if="!tutPaneOpen && !store.tutorialSkip" @click="onStartTutorial" title="Start Tutorial"
:size="45" :style="buttonStyles" class="absolute bottom-2 right-2 z-10 shadow-[0_0_2px_black]">
<edu-icon/>
</icon-button>
@@ -60,25 +59,25 @@
<!-- Modals -->
<transition name="fade">
<search-modal v-if="searchOpen"
- :tolMap="tolMap" :lytMap="layoutMap" :activeRoot="activeRoot" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ :tolMap="tolMap" :lytMap="layoutMap" :activeRoot="activeRoot"
@close="onSearchClose" @search="onSearch" @info-click="onInfoClick" @setting-chg="onSettingChg"
@net-wait="onSearchNetWait" @net-get="endLoadInd" class="z-10"/>
</transition>
<transition name="fade">
<tile-info-modal v-if="infoModalNodeName != null && infoModalData != null"
- :nodeName="infoModalNodeName" :infoResponse="infoModalData" :tolMap="tolMap" :lytOpts="lytOpts"
- :uiOpts="uiOpts" class="z-10" @close="onInfoClose"/>
+ :nodeName="infoModalNodeName" :infoResponse="infoModalData" :tolMap="tolMap"
+ class="z-10" @close="onInfoClose"/>
</transition>
<transition name="fade">
- <help-modal v-if="helpOpen" :tutOpen="tutPaneOpen" :uiOpts="uiOpts" class="z-10"
+ <help-modal v-if="helpOpen" :tutOpen="tutPaneOpen" class="z-10"
@close="onHelpClose" @start-tutorial="onStartTutorial"/>
</transition>
<transition name="fade">
- <settings-modal v-if="settingsOpen" :lytOpts="lytOpts" :uiOpts="uiOpts" class="z-10"
+ <settings-modal v-if="settingsOpen" class="z-10"
@close="onSettingsClose" @reset="onResetSettings" @setting-chg="onSettingChg"/>
</transition>
<transition name="fade">
- <loading-modal v-if="loadingMsg != null" :msg="loadingMsg" :uiOpts="uiOpts" class="z-10"/>
+ <loading-modal v-if="loadingMsg != null" :msg="loadingMsg" class="z-10"/>
</transition>
<!-- Overlay used to capture clicks during auto mode, etc -->
<div :style="{visibility: modeRunning != null ? 'visible' : 'hidden'}"
@@ -108,11 +107,10 @@ import EduIcon from './components/icon/EduIcon.vue';
// Other
// 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';
-import {queryServer, InfoResponse, Action,
- UiOptions, getDefaultLytOpts, getDefaultUiOpts, OptionType} from './lib';
+import {LayoutNode, LayoutTreeChg, initLayoutTree, initLayoutMap, tryLayout} from './layout';
+import {queryServer, InfoResponse, Action} from './lib';
import {arraySum, randWeightedChoice} from './util';
+import {useStore, StoreState} from './store';
// Constants
const SERVER_WAIT_MSG = 'Loading data';
@@ -122,39 +120,8 @@ const EXCESS_TOLNODE_THRESHOLD = 1000; // Threshold where excess tolMap entries
// Refs
const contentAreaRef = ref(null as HTMLElement | null);
-// Get/load option values
-function getLytOpts(): LayoutOptions {
- let opts = getDefaultLytOpts();
- for (let prop of Object.getOwnPropertyNames(opts) as (keyof LayoutOptions)[]){
- let item = localStorage.getItem('LYT ' + prop);
- if (item != null){
- switch (typeof(opts[prop])){
- case 'boolean': (opts[prop] as unknown as boolean) = Boolean(item); break;
- case 'number': (opts[prop] as unknown as number) = Number(item); break;
- case 'string': (opts[prop] as unknown as string) = item; break;
- default: console.log(`WARNING: Found saved layout setting "${prop}" with unexpected type`);
- }
- }
- }
- return opts;
-}
-function getUiOpts(): UiOptions {
- let opts = getDefaultUiOpts(getDefaultLytOpts());
- for (let prop of Object.getOwnPropertyNames(opts) as (keyof UiOptions)[]){
- let item = localStorage.getItem('UI ' + prop);
- if (item != null){
- switch (typeof(opts[prop])){
- case 'boolean': (opts[prop] as unknown as boolean) = (item == 'true'); break;
- case 'number': (opts[prop] as unknown as number) = Number(item); break;
- case 'string': (opts[prop] as unknown as string) = item; break;
- default: console.log(`WARNING: Found saved UI setting "${prop}" with unexpected type`);
- }
- }
- }
- return opts;
-}
-const lytOpts = ref(getLytOpts());
-const uiOpts = ref(getUiOpts());
+// Global store
+const store = useStore();
// Tree/layout data
const tolMap = ref(new Map() as TolMap);
@@ -183,7 +150,7 @@ async function initTreeFromServer(firstInit = true){
// Get possible target node from URL
let nodeName = (new URL(window.location.href)).searchParams.get('node');
// Query server
- let urlParams = new URLSearchParams({type: 'node', tree: uiOpts.value.tree});
+ let urlParams = new URLSearchParams({type: 'node', tree: store.tree});
if (nodeName != null && firstInit){
urlParams.append('name', nodeName);
urlParams.append('toroot', '1');
@@ -220,12 +187,12 @@ async function initTreeFromServer(firstInit = true){
let newRoot = targetNode.parent == null ? targetNode : targetNode.parent;
LayoutNode.hideUpward(newRoot, layoutMap.value);
activeRoot.value = newRoot;
- setTimeout(() => setLastFocused(targetNode!), uiOpts.value.transitionDuration);
+ setTimeout(() => setLastFocused(targetNode!), store.transitionDuration);
}
// Skip initial transition
if (firstInit){
justInitialised.value = true;
- setTimeout(() => {justInitialised.value = false}, uiOpts.value.transitionDuration);
+ setTimeout(() => {justInitialised.value = false}, store.transitionDuration);
}
// Relayout
updateAreaDims();
@@ -251,16 +218,16 @@ function relayoutWithCollapse(secondPass = true, keepOverflow = false): boolean
if (overflownRoot.value){
if (keepOverflow){
success = tryLayout(activeRoot.value, tileAreaDims.value,
- {...lytOpts.value, layoutType: 'sqr-overflow'}, {layoutMap: layoutMap.value});
+ {...store.lytOpts, layoutType: 'sqr-overflow'}, {layoutMap: layoutMap.value});
return success;
}
overflownRoot.value = false;
}
- success = tryLayout(activeRoot.value, tileAreaDims.value, lytOpts.value,
+ success = tryLayout(activeRoot.value, tileAreaDims.value, store.lytOpts,
{allowCollapse: true, layoutMap: layoutMap.value});
if (secondPass){
// Relayout again, which can help allocate remaining tiles 'evenly'
- success = tryLayout(activeRoot.value, tileAreaDims.value, lytOpts.value,
+ success = tryLayout(activeRoot.value, tileAreaDims.value, store.lytOpts,
{allowCollapse: false, layoutMap: layoutMap.value});
}
return success;
@@ -271,19 +238,19 @@ function updateAreaDims(){
// throughout their transitions, relayouting each time, but this makes the tile movements jerky
let contentAreaEl = contentAreaRef.value!;
let w = contentAreaEl.offsetWidth, h = contentAreaEl.offsetHeight;
- if (tutPaneOpen.value && uiOpts.value.breakpoint == 'sm'){
- h -= uiOpts.value.tutPaneSz;
+ if (tutPaneOpen.value && store.breakpoint == 'sm'){
+ h -= store.tutPaneSz;
}
mainAreaDims.value = [w, h];
if (detachedAncestors.value != null){
if (w > h){
- w -= uiOpts.value.ancestryBarBreadth;
+ w -= store.ancestryBarBreadth;
} else {
- h -= uiOpts.value.ancestryBarBreadth;
+ h -= store.ancestryBarBreadth;
}
}
- w -= lytOpts.value.tileSpacing * 2;
- h -= lytOpts.value.tileSpacing * 2;
+ w -= store.lytOpts.tileSpacing * 2;
+ h -= store.lytOpts.tileSpacing * 2;
tileAreaDims.value = [w, h];
}
@@ -293,28 +260,11 @@ let afterResizeHdlr = 0; // Set via setTimeout() to execute after a run of resiz
async function onResize(){
// Handle event if not recently done
let handleResize = async () => {
- // Update layout/ui options with defaults, excluding user-modified ones
- let lytOpts2 = getDefaultLytOpts();
- let uiOpts2 = getDefaultUiOpts(lytOpts2);
- let changedTree = false;
- for (let prop of Object.getOwnPropertyNames(lytOpts2) as (keyof LayoutOptions)[]){
- let item = localStorage.getItem('LYT ' + prop);
- if (item == null && lytOpts.value[prop] != lytOpts2[prop]){
- (lytOpts.value[prop] as any) = lytOpts2[prop];
- }
- }
- for (let prop of Object.getOwnPropertyNames(uiOpts2) as (keyof UiOptions)[]){
- let item = localStorage.getItem('UI ' + prop);
- //Note: Using JSON.stringify here to roughly deep-compare values
- if (item == null && JSON.stringify(uiOpts.value[prop]) != JSON.stringify(uiOpts2[prop])){
- (uiOpts.value[prop] as any) = uiOpts2[prop];
- if (prop == 'tree'){
- changedTree = true;
- }
- }
- }
+ // Update settings with defaults, keeping user modifications
+ let oldTree = store.tree;
+ store.softReset();
// Relayout
- if (!changedTree){
+ if (store.tree == oldTree){
updateAreaDims();
relayoutWithCollapse();
} else {
@@ -322,7 +272,7 @@ async function onResize(){
}
};
let currentTime = new Date().getTime();
- if (currentTime - lastResizeHdlrTime > uiOpts.value.transitionDuration){
+ if (currentTime - lastResizeHdlrTime > store.transitionDuration){
lastResizeHdlrTime = currentTime;
await handleResize();
lastResizeHdlrTime = new Date().getTime();
@@ -352,9 +302,9 @@ async function onLeafClick(
chg: {type: 'expand', node: layoutNode, tolMap: tolMap.value} as LayoutTreeChg,
layoutMap: layoutMap.value,
};
- let success = tryLayout(activeRoot.value, tileAreaDims.value, lytOpts.value, lytFnOpts);
+ let success = tryLayout(activeRoot.value, tileAreaDims.value, store.lytOpts, lytFnOpts);
// Handle auto-hide
- if (!success && uiOpts.value.autoHide){
+ if (!success && store.autoHide){
while (!success && layoutNode != activeRoot.value){
let node = layoutNode;
while (node.parent != activeRoot.value){
@@ -366,13 +316,13 @@ async function onLeafClick(
activeRoot.value = node;
// Try relayout
updateAreaDims();
- success = tryLayout(activeRoot.value, tileAreaDims.value, lytOpts.value, lytFnOpts);
+ success = tryLayout(activeRoot.value, tileAreaDims.value, store.lytOpts, lytFnOpts);
}
}
// If expanding active-root with too many children to fit, allow overflow
if (!success && layoutNode == activeRoot.value){
success = tryLayout(activeRoot.value, tileAreaDims.value,
- {...lytOpts.value, layoutType: 'sqr-overflow'}, lytFnOpts);
+ {...store.lytOpts, layoutType: 'sqr-overflow'}, lytFnOpts);
if (success){
overflownRoot.value = true;
}
@@ -387,7 +337,7 @@ async function onLeafClick(
//
let success: boolean;
if (overflownRoot.value){ // If clicking child of overflowing active-root
- if (!uiOpts.value.autoHide){
+ if (!store.autoHide){
if (!subAction && onFail != null){
onFail(); // Triggers failure animation
}
@@ -399,7 +349,7 @@ async function onLeafClick(
// Check if data for node-to-expand exists, getting from server if needed
let tolNode = tolMap.value.get(layoutNode.name)!;
if (!tolMap.value.has(tolNode.children[0])){
- let urlParams = new URLSearchParams({type: 'node', name: layoutNode.name, tree: uiOpts.value.tree});
+ let urlParams = new URLSearchParams({type: 'node', name: layoutNode.name, tree: store.tree});
let responseObj: {[x: string]: TolNode} = await loadFromServer(urlParams);
if (responseObj == null){
success = false;
@@ -423,7 +373,7 @@ async function onNonleafClick(
}
// Relayout
primeLoadInd(PROCESSING_WAIT_MSG);
- let success = tryLayout(activeRoot.value, tileAreaDims.value, lytOpts.value, {
+ let success = tryLayout(activeRoot.value, tileAreaDims.value, store.lytOpts, {
allowCollapse: false,
chg: {type: 'collapse', node: layoutNode, tolMap: tolMap.value},
layoutMap: layoutMap.value,
@@ -483,11 +433,11 @@ async function onLeafClickHeld(
chg: {type: 'expand', node: layoutNode, tolMap: tolMap.value} as LayoutTreeChg,
layoutMap: layoutMap.value,
};
- let success = tryLayout(activeRoot.value, tileAreaDims.value, lytOpts.value, lytFnOpts);
+ let success = tryLayout(activeRoot.value, tileAreaDims.value, store.lytOpts, lytFnOpts);
// If expanding active-root with too many children to fit, allow overflow
if (!success){
success = tryLayout(activeRoot.value, tileAreaDims.value,
- {...lytOpts.value, layoutType: 'sqr-overflow'}, lytFnOpts);
+ {...store.lytOpts, layoutType: 'sqr-overflow'}, lytFnOpts);
if (success){
overflownRoot.value = true;
}
@@ -503,7 +453,7 @@ async function onLeafClickHeld(
let success: boolean;
let tolNode = tolMap.value.get(layoutNode.name)!;
if (!tolMap.value.has(tolNode.children[0])){
- let urlParams = new URLSearchParams({type: 'node', name: layoutNode.name, tree: uiOpts.value.tree});
+ let urlParams = new URLSearchParams({type: 'node', name: layoutNode.name, tree: store.tree});
let responseObj: {[x: string]: TolNode} = await loadFromServer(urlParams);
if (responseObj == null){
success = false;
@@ -586,7 +536,7 @@ async function onInfoClick(nodeName: string){
resetMode();
}
// Query server for tol-node info
- let urlParams = new URLSearchParams({type: 'info', name: nodeName, tree: uiOpts.value.tree});
+ let urlParams = new URLSearchParams({type: 'info', name: nodeName, tree: store.tree});
let responseObj: InfoResponse = await loadFromServer(urlParams);
if (responseObj != null){
// Set fields from response
@@ -645,9 +595,9 @@ async function expandToNode(name: string){
while (!detachedAncestors.value!.includes(nodeInAncestryBar)){
nodeInAncestryBar = nodeInAncestryBar.parent!;
}
- if (!uiOpts.value.searchJumpMode){
+ if (!store.searchJumpMode){
await onDetachedAncestorClick(nodeInAncestryBar!, true);
- setTimeout(() => expandToNode(name), uiOpts.value.transitionDuration);
+ setTimeout(() => expandToNode(name), store.transitionDuration);
} else{
await onDetachedAncestorClick(nodeInAncestryBar!, true, true);
expandToNode(name);
@@ -655,7 +605,7 @@ async function expandToNode(name: string){
return;
}
// Attempt tile-expand
- if (uiOpts.value.searchJumpMode){
+ if (store.searchJumpMode){
// Extend layout tree
let tolNode = tolMap.value.get(name)!;
let nodesToAdd = [name] as string[];
@@ -677,17 +627,17 @@ async function expandToNode(name: string){
} else {
await onLeafClick(activeRoot.value, null, true);
}
- setTimeout(() => expandToNode(name), uiOpts.value.transitionDuration);
+ setTimeout(() => expandToNode(name), store.transitionDuration);
return;
}
if (overflownRoot.value){
await onLeafClickHeld(layoutNode, null, true);
- setTimeout(() => expandToNode(name), uiOpts.value.transitionDuration);
+ setTimeout(() => expandToNode(name), store.transitionDuration);
return;
}
let success = await onLeafClick(layoutNode, null, true);
if (success){
- setTimeout(() => expandToNode(name), uiOpts.value.transitionDuration);
+ setTimeout(() => expandToNode(name), store.transitionDuration);
return;
}
// Attempt expand-to-view on an ancestor halfway to the active root
@@ -703,7 +653,7 @@ async function expandToNode(name: string){
}
layoutNode = ancestorChain[Math.floor((ancestorChain.length - 1) / 2)]
await onNonleafClickHeld(layoutNode, null, true);
- setTimeout(() => expandToNode(name), uiOpts.value.transitionDuration);
+ setTimeout(() => expandToNode(name), store.transitionDuration);
}
function onSearchClose(){
modeRunning.value = null;
@@ -755,7 +705,7 @@ async function autoAction(){
layoutNode = layoutNode.children[idx!];
}
setLastFocused(layoutNode);
- setTimeout(autoAction, uiOpts.value.autoActionDelay);
+ setTimeout(autoAction, store.autoActionDelay);
} else {
// Determine available actions
let action: AutoAction | null;
@@ -844,7 +794,7 @@ async function autoAction(){
return;
}
autoPrevActionFail.value = !success;
- setTimeout(autoAction, uiOpts.value.transitionDuration + uiOpts.value.autoActionDelay);
+ setTimeout(autoAction, store.transitionDuration + store.autoActionDelay);
}
}
function onAutoClose(){
@@ -865,27 +815,23 @@ function onSettingsClose(){
settingsOpen.value = false;
onActionEnd('settings');
}
-async function onSettingChg(optionType: OptionType, option: string, {relayout = false, reinit = false} = {}){
- // Save setting
- if (optionType == 'LYT'){
- localStorage.setItem(`${optionType} ${option}`,
- String(lytOpts.value[option as keyof LayoutOptions]));
- } else if (optionType == 'UI') {
- localStorage.setItem(`${optionType} ${option}`,
- String(uiOpts.value[option as keyof UiOptions]));
- }
- // Possibly relayout/reinitialise
- if (reinit){
+async function onSettingChg(option: keyof StoreState){
+ store.save(option);
+ if (option == 'tree'){
reInit();
- } else if (relayout){
+ } else if (option.startsWith('lytOpts.')){
+ updateAreaDims();
relayoutWithCollapse();
}
}
-function onResetSettings(reinit: boolean){
- localStorage.clear();
- if (reinit){
+function onResetSettings(){
+ let oldTree = store.tree;
+ store.reset();
+ store.clear();
+ if (store.tree != oldTree){
reInit();
} else {
+ updateAreaDims();
relayoutWithCollapse();
}
}
@@ -905,8 +851,8 @@ function onHelpClose(){
}
// For tutorial pane
-const tutPaneOpen = ref(!uiOpts.value.tutorialSkip);
-const tutWelcome = ref(!uiOpts.value.tutorialSkip);
+const tutPaneOpen = ref(!store.tutorialSkip);
+const tutWelcome = ref(!store.tutorialSkip);
const tutTriggerAction = ref(null as Action | null); // Used to advance tutorial upon user-actions
const tutTriggerFlag = ref(false);
const actionsDone = ref(new Set() as Set<Action>); // Used to avoid disabling actions the user has already seen
@@ -918,8 +864,8 @@ function onStartTutorial(){
}
}
function onTutorialSkip(){
- uiOpts.value.tutorialSkip = true;
- onSettingChg('UI', 'tutorialSkip');
+ store.tutorialSkip = true;
+ onSettingChg('tutorialSkip')
}
function onTutStageChg(triggerAction: Action | null){
tutWelcome.value = false;
@@ -929,11 +875,11 @@ function onTutPaneClose(){
tutPaneOpen.value = false;
if (tutWelcome.value){
tutWelcome.value = false;
- } else if (uiOpts.value.tutorialSkip == false){
- uiOpts.value.tutorialSkip = true;
- onSettingChg('UI', 'tutorialSkip');
+ } else if (store.tutorialSkip == false){
+ store.tutorialSkip = true;
+ onSettingChg('tutorialSkip');
}
- uiOpts.value.disabledActions.clear();
+ store.disabledActions.clear();
updateAreaDims();
relayoutWithCollapse(true, true);
}
@@ -991,7 +937,7 @@ function onActionEnd(action: Action){
}
}
function isDisabled(...actions: Action[]): boolean {
- let disabledActions = uiOpts.value.disabledActions;
+ let disabledActions = store.disabledActions;
return actions.some(a => disabledActions.has(a));
}
@@ -1032,13 +978,13 @@ async function collapseTree(){
const changedSweepToParent = ref(false);
watch(modeRunning, (newVal) => {
if (newVal != null){
- if (lytOpts.value.sweepToParent == 'fallback'){
- lytOpts.value.sweepToParent = 'prefer';
+ if (store.lytOpts.sweepToParent == 'fallback'){
+ store.lytOpts.sweepToParent = 'prefer';
changedSweepToParent.value = true;
}
} else {
if (changedSweepToParent.value){
- lytOpts.value.sweepToParent = 'fallback';
+ store.lytOpts.sweepToParent = 'fallback';
changedSweepToParent.value = false;
}
}
@@ -1046,7 +992,7 @@ watch(modeRunning, (newVal) => {
// For keyboard shortcuts
function onKeyDown(evt: KeyboardEvent){
- if (uiOpts.value.disableShortcuts){
+ if (store.disableShortcuts){
return;
}
if (evt.key == 'Escape'){
@@ -1060,8 +1006,8 @@ function onKeyDown(evt: KeyboardEvent){
} else if (evt.key == 'F' && evt.ctrlKey){
// If search bar is open, switch search mode
if (searchOpen.value){
- uiOpts.value.searchJumpMode = !uiOpts.value.searchJumpMode;
- onSettingChg('UI', 'searchJumpMode');
+ store.searchJumpMode = !store.searchJumpMode;
+ onSettingChg('searchJumpMode');
}
}
}
@@ -1074,16 +1020,16 @@ onUnmounted(() => {
// Styles
const buttonStyles = computed(() => ({
- color: uiOpts.value.textColor,
- backgroundColor: uiOpts.value.altColorDark,
+ color: store.color.text,
+ backgroundColor: store.color.altDark,
}));
const tutPaneContainerStyles = computed((): Record<string,string> => {
- if (uiOpts.value.breakpoint == 'sm'){
+ if (store.breakpoint == 'sm'){
return {
- minHeight: (tutPaneOpen.value ? uiOpts.value.tutPaneSz : 0) + 'px',
- maxHeight: (tutPaneOpen.value ? uiOpts.value.tutPaneSz : 0) + 'px',
+ minHeight: (tutPaneOpen.value ? store.tutPaneSz : 0) + 'px',
+ maxHeight: (tutPaneOpen.value ? store.tutPaneSz : 0) + 'px',
transitionProperty: 'max-height, min-height',
- transitionDuration: uiOpts.value.transitionDuration + 'ms',
+ transitionDuration: store.transitionDuration + 'ms',
overflow: 'hidden',
};
} else {
@@ -1093,33 +1039,33 @@ const tutPaneContainerStyles = computed((): Record<string,string> => {
right: '0.5cm',
visibility: tutPaneOpen.value ? 'visible' : 'hidden',
transitionProperty: 'visibility',
- transitionDuration: uiOpts.value.transitionDuration + 'ms',
+ transitionDuration: store.transitionDuration + 'ms',
};
}
});
const tutPaneStyles = computed((): Record<string,string> => {
- if (uiOpts.value.breakpoint == 'sm'){
+ if (store.breakpoint == 'sm'){
return {
- height: uiOpts.value.tutPaneSz + 'px',
+ height: store.tutPaneSz + 'px',
}
} else {
return {
- height: uiOpts.value.tutPaneSz + 'px',
+ height: store.tutPaneSz + 'px',
minWidth: '10cm',
maxWidth: '10cm',
- borderRadius: uiOpts.value.borderRadius + 'px',
+ borderRadius: store.borderRadius + 'px',
boxShadow: '0 0 3px black',
};
}
});
const ancestryBarContainerStyles = computed((): Record<string,string> => {
- let ancestryBarBreadth = detachedAncestors.value == null ? 0 : uiOpts.value.ancestryBarBreadth;
+ let ancestryBarBreadth = detachedAncestors.value == null ? 0 : store.ancestryBarBreadth;
let styles = {
minWidth: 'auto',
maxWidth: 'none',
minHeight: 'auto',
maxHeight: 'none',
- transitionDuration: uiOpts.value.transitionDuration + 'ms',
+ transitionDuration: store.transitionDuration + 'ms',
transitionProperty: '',
overflow: 'hidden',
};
diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue
index 1b4ee81..8eabf22 100644
--- a/src/components/AncestryBar.vue
+++ b/src/components/AncestryBar.vue
@@ -1,7 +1,7 @@
<template>
<div :style="styles" @wheel.stop="onWheelEvt" ref="rootRef">
<tol-tile v-for="(node, idx) in dummyNodes" :key="node.name" class="shrink-0"
- :layoutNode="node" :tolMap="tolMap" :nonAbsPos="true" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ :layoutNode="node" :tolMap="tolMap" :nonAbsPos="true"
@leaf-click="onTileClick(nodes[idx])" @info-click="onInfoIconClick"/>
</div>
</template>
@@ -10,27 +10,27 @@
import {ref, computed, watch, onMounted, nextTick, PropType} from 'vue';
import TolTile from './TolTile.vue';
import {TolMap} from '../tol';
-import {LayoutNode, LayoutOptions} from '../layout';
-import {UiOptions} from '../lib';
+import {LayoutNode} from '../layout';
+import {useStore} from '../store';
// Refs
const rootRef = ref(null as HTMLDivElement | null);
+// Global store
+const store = useStore();
+
// Props + events
const props = defineProps({
nodes: {type: Array as PropType<LayoutNode[]>, required: true},
vert: {type: Boolean, default: false},
breadth: {type: Number, required: true},
- //
- lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
tolMap: {type: Object as PropType<TolMap>, required: true},
});
const emit = defineEmits(['ancestor-click', 'info-click']);
// Computed prop data for display
const imgSz = computed(() =>
- props.breadth - props.lytOpts.tileSpacing - props.uiOpts.scrollGap
+ props.breadth - store.lytOpts.tileSpacing - store.scrollGap
// Intentionally omitting extra tileSpacing, to allow for scrollGap with less image shrinkage
);
const dummyNodes = computed(() => props.nodes.map(n => {
@@ -54,11 +54,13 @@ function onWheelEvt(evt: WheelEvent){ // For converting vertical scrolling to ho
}
}
function scrollToEnd(){
- let el = rootRef.value!;
- if (props.vert){
- el.scrollTop = el.scrollHeight;
- } else {
- el.scrollLeft = el.scrollWidth;
+ let el = rootRef.value;
+ if (el != null){
+ if (props.vert){
+ el.scrollTop = el.scrollHeight;
+ } else {
+ el.scrollLeft = el.scrollWidth;
+ }
}
}
watch(props.nodes, () => {
@@ -75,12 +77,12 @@ const styles = computed(() => ({
display: 'flex',
flexDirection: props.vert ? 'column' : 'row',
alignItems: 'center',
- gap: props.lytOpts.tileSpacing + 'px',
- padding: props.lytOpts.tileSpacing + 'px',
+ gap: store.lytOpts.tileSpacing + 'px',
+ padding: store.lytOpts.tileSpacing + 'px',
overflowX: props.vert ? 'hidden' : 'auto',
overflowY: props.vert ? 'auto' : 'hidden',
// Other
- backgroundColor: props.uiOpts.ancestryBarBgColor,
- boxShadow: props.uiOpts.shadowNormal,
+ backgroundColor: store.ancestryBarBgColor,
+ boxShadow: store.shadowNormal,
}));
</script>
diff --git a/src/components/HelpModal.vue b/src/components/HelpModal.vue
index c403e53..d2576d5 100644
--- a/src/components/HelpModal.vue
+++ b/src/components/HelpModal.vue
@@ -385,13 +385,6 @@
two species of grass being described like a third closely-related species.
</p>
<br/>
- <h1 class="text-lg font-bold">Why did searching for 'goat' send me to the moths?</h1>
- <p>
- When you search for a name, then press enter, the first result is used.
- Currently, search suggestions are not ordered by well-known the taxons are,
- so the first result might mean 'Goat Moth' instead of 'Domestic Goat'.
- </p>
- <br/>
<h1 class="text-lg font-bold">Why do a lot of fish have their heads clipped out?</h1>
<p>
Cropping images into squares was done semi-automatically, and sometimes this
@@ -418,7 +411,7 @@
</template>
</s-collapsible>
</div>
- <s-button class="mx-auto mb-2" :style="{color: uiOpts.textColor, backgroundColor: uiOpts.bgColor}"
+ <s-button class="mx-auto mb-2" :style="{color: store.color.text, backgroundColor: store.color.bg}"
:disabled="tutOpen" @click.stop="onStartTutorial">
Start Tutorial
</s-button>
@@ -427,23 +420,25 @@
</template>
<script setup lang="ts">
-import {ref, computed, PropType} from 'vue';
+import {ref, computed} from 'vue';
import SButton from './SButton.vue';
import SCollapsible from './SCollapsible.vue';
import CloseIcon from './icon/CloseIcon.vue';
import DownIcon from './icon/DownIcon.vue';
-import {UiOptions} from '../lib';
+import {useStore} from '../store';
// Refs
const rootRef = ref(null as HTMLDivElement | null)
const closeRef = ref(null as typeof CloseIcon | null);
+// Global store
+const store = useStore();
+
// Props + events
-const props = defineProps({
+defineProps({
tutOpen: {type: Boolean, default: false},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
});
-const touchDevice = computed(() => props.uiOpts.touchDevice)
+const touchDevice = computed(() => store.touchDevice)
const emit = defineEmits(['close', 'start-tutorial']);
// Event handlers
@@ -459,12 +454,12 @@ function onStartTutorial(){
// Styles
const styles = computed(() => ({
- backgroundColor: props.uiOpts.bgColorAlt,
- borderRadius: props.uiOpts.borderRadius + 'px',
- boxShadow: props.uiOpts.shadowNormal,
+ backgroundColor: store.color.bgAlt,
+ borderRadius: store.borderRadius + 'px',
+ boxShadow: store.shadowNormal,
}));
const aStyles = computed(() => ({
- color: props.uiOpts.altColorDark,
+ color: store.color.altDark,
}));
// Classes
diff --git a/src/components/LoadingModal.vue b/src/components/LoadingModal.vue
index abd405c..3f941f2 100644
--- a/src/components/LoadingModal.vue
+++ b/src/components/LoadingModal.vue
@@ -9,19 +9,20 @@
</template>
<script setup lang="ts">
-import {computed, PropType} from 'vue';
+import {computed} from 'vue';
import LoaderIcon from './icon/LoaderIcon.vue';
-import {UiOptions} from '../lib';
+import {useStore} from '../store';
-const props = defineProps({
+const store = useStore();
+
+defineProps({
msg: {type: String, required: true},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
});
const styles = computed(() => ({
- color: props.uiOpts.textColor,
- backgroundColor: props.uiOpts.bgColorDark2,
- borderRadius: props.uiOpts.borderRadius + 'px',
- boxShadow: props.uiOpts.shadowNormal,
+ color: store.color.text,
+ backgroundColor: store.color.bgDark2,
+ borderRadius: store.borderRadius + 'px',
+ boxShadow: store.shadowNormal,
}));
</script>
diff --git a/src/components/SButton.vue b/src/components/SButton.vue
index 884fa30..487d6bd 100644
--- a/src/components/SButton.vue
+++ b/src/components/SButton.vue
@@ -6,7 +6,7 @@
</template>
<script setup lang="ts">
-const props = defineProps({
+defineProps({
disabled: {type: Boolean, default: false},
});
</script>
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index a035cac..1818529 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -10,7 +10,7 @@
</div>
<div class="absolute top-[100%] w-full overflow-hidden" :style="suggContainerStyles">
<div v-for="(sugg, idx) of searchSuggs" :key="sugg.name + '|' + sugg.canonicalName"
- :style="{backgroundColor: idx == focusedSuggIdx ? uiOpts.bgColorAltDark : uiOpts.bgColorAlt}"
+ :style="{backgroundColor: idx == focusedSuggIdx ? store.color.bgAltDark : store.color.bgAlt}"
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">
@@ -25,7 +25,7 @@
<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="store.searchJumpMode" @change="emit('setting-chg', 'searchJumpMode')"/>
<div class="text-sm">Jump to result</div>
</label>
</div>
@@ -37,20 +37,22 @@ import {ref, computed, onMounted, onUnmounted, PropType} from 'vue';
import SearchIcon from './icon/SearchIcon.vue';
import InfoIcon from './icon/InfoIcon.vue';
import {TolNode, TolMap} from '../tol';
-import {LayoutNode, LayoutMap, LayoutOptions} from '../layout';
-import {queryServer, SearchSugg, SearchSuggResponse, UiOptions} from '../lib';
+import {LayoutNode, LayoutMap} from '../layout';
+import {queryServer, SearchSugg, SearchSuggResponse} from '../lib';
+import {useStore} from '../store';
// Refs
const rootRef = ref(null as HTMLDivElement | null);
const inputRef = ref(null as HTMLInputElement | null);
+// Global store
+const store = useStore();
+
// Props + events
const props = defineProps({
lytMap: {type: Object as PropType<LayoutMap>, required: true}, // Used to check if a searched-for node exists
activeRoot: {type: Object as PropType<LayoutNode>, required: true}, // Sent to server to reduce response size
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},
});
const emit = defineEmits(['search', 'close', 'info-click', 'setting-chg', 'net-wait', 'net-get']);
@@ -101,8 +103,8 @@ async function onInput(){
let urlParams = new URLSearchParams({
type: 'sugg',
name: input.value,
- limit: String(props.uiOpts.searchSuggLimit),
- tree: props.uiOpts.tree,
+ limit: String(store.searchSuggLimit),
+ tree: store.tree,
});
// Query server, delaying/skipping if a request was recently sent
pendingSuggReqParams.value = urlParams;
@@ -168,7 +170,7 @@ async function resolveSearch(tolNodeName: string){
name: tolNodeName,
toroot: '1',
excl: props.activeRoot.name,
- tree: props.uiOpts.tree,
+ tree: store.tree,
});
emit('net-wait'); // Allows the parent component to show a loading-indicator
let responseObj: {[x: string]: TolNode} = await queryServer(urlParams);
@@ -216,7 +218,7 @@ function onInfoIconClick(nodeName: string){
// For keyboard shortcuts
function onKeyDown(evt: KeyboardEvent){
- if (props.uiOpts.disableShortcuts){
+ if (store.disableShortcuts){
return;
}
if (evt.key == 'f' && evt.ctrlKey){
@@ -232,26 +234,26 @@ onMounted(() => inputRef.value!.focus())
// Styles
const styles = computed((): Record<string,string> => {
- let br = props.uiOpts.borderRadius;
+ let br = store.borderRadius;
return {
- backgroundColor: props.uiOpts.bgColorAlt,
+ backgroundColor: store.color.bgAlt,
borderRadius: (searchSuggs.value.length == 0) ? `${br}px` : `${br}px ${br}px 0 0`,
- boxShadow: props.uiOpts.shadowNormal,
+ boxShadow: store.shadowNormal,
};
});
const suggContainerStyles = computed((): Record<string,string> => {
- let br = props.uiOpts.borderRadius;
+ let br = store.borderRadius;
return {
- backgroundColor: props.uiOpts.bgColorAlt,
- color: props.uiOpts.textColorAlt,
+ backgroundColor: store.color.bgAlt,
+ color: store.color.textAlt,
borderRadius: `0 0 ${br}px ${br}px`,
};
});
const animateLabelStyles = computed(() => ({
position: 'absolute',
- top: -props.lytOpts.headerSz - 2 + 'px',
+ top: -store.lytOpts.headerSz - 2 + 'px',
right: '0',
- height: props.lytOpts.headerSz + 'px',
- color: props.uiOpts.textColor,
+ height: store.lytOpts.headerSz + 'px',
+ color: store.color.text,
}));
</script>
diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue
index df8444f..a55dc41 100644
--- a/src/components/SettingsModal.vue
+++ b/src/components/SettingsModal.vue
@@ -9,19 +9,19 @@
<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">
+ <label for="animTimeInput" @click="onResetOne('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>
+ <input type="range" min="0" max="1000" v-model.number="store.transitionDuration"
+ @change="onSettingChg('transitionDuration')" class="my-auto" name="animTimeInput"/>
+ <div class="my-auto text-right">{{store.transitionDuration}} ms</div>
<!-- Row 2 -->
- <label for="autoDelayInput" @click="onReset('UI', 'autoActionDelay')" :class="rLabelClasses">
+ <label for="autoDelayInput" @click="onResetOne('autoActionDelay')" :class="rLabelClasses">
Auto-mode Delay
</label>
- <input type="range" min="100" 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>
+ <input type="range" min="300" max="2000" v-model.number="store.autoActionDelay"
+ @change="onSettingChg('autoActionDelay')" class="my-auto" name="autoDelayInput"/>
+ <div class="my-auto text-right">{{store.autoActionDelay}} ms</div>
</div>
</div>
<div class="pb-2" :class="borderBClasses">
@@ -31,49 +31,49 @@
<div>Sweep leaves left</div>
<ul>
<li> <label> <input type="radio" v-model="sweepLeaves" :value="true"
- @change="onSettingChg('LYT', 'layoutType')"/> Yes </label> </li>
+ @change="onSettingChg('lytOpts.layoutType')"/> Yes </label> </li>
<li> <label> <input type="radio" v-model="sweepLeaves" :value="false"
- @change="onSettingChg('LYT', 'layoutType')"/> No </label> </li>
+ @change="onSettingChg('lytOpts.layoutType')"/> No </label> </li>
</ul>
</div>
<div>
<div>Sweep into parent</div>
<ul>
- <li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="lytOpts.sweepToParent"
- value="none" @change="onSettingChg('LYT', 'sweepToParent')"/> Never </label> </li>
- <li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="lytOpts.sweepToParent"
- value="prefer" @change="onSettingChg('LYT', 'sweepToParent')"/> Always </label> </li>
- <li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="lytOpts.sweepToParent"
- value="fallback" @change="onSettingChg('LYT', 'sweepToParent')"/> If needed </label> </li>
+ <li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="store.lytOpts.sweepToParent"
+ value="none" @change="onSettingChg('lytOpts.sweepToParent')"/> Never </label> </li>
+ <li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="store.lytOpts.sweepToParent"
+ value="prefer" @change="onSettingChg('lytOpts.sweepToParent')"/> Always </label> </li>
+ <li> <label> <input type="radio" :disabled="!sweepLeaves" v-model="store.lytOpts.sweepToParent"
+ value="fallback" @change="onSettingChg('lytOpts.sweepToParent')"/> If needed </label> </li>
</ul>
</div>
</div>
<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">
+ <label for="minTileSizeInput" @click="onResetOne('lytOpts.minTileSz')" :class="rLabelClasses">
Min Tile Size
</label>
<input type="range"
- min="15" :max="uiOpts.breakpoint == 'sm' ? 150 : 200" v-model.number="lytOpts.minTileSz"
- @input="onSettingChgThrottled('LYT', 'minTileSz')" @change="onSettingChg('LYT', 'minTileSz')"
+ min="15" :max="store.breakpoint == 'sm' ? 150 : 200" v-model.number="store.lytOpts.minTileSz"
+ @input="onSettingChgThrottled('lytOpts.minTileSz')" @change="onSettingChg('lytOpts.minTileSz')"
name="minTileSizeInput" ref="minTileSzRef"/>
- <div class="my-auto text-right">{{lytOpts.minTileSz}} px</div>
+ <div class="my-auto text-right">{{store.lytOpts.minTileSz}} px</div>
<!-- Row 2 -->
- <label for="maxTileSizeInput" @click="onReset('LYT', 'maxTileSz')" :class="rLabelClasses">
+ <label for="maxTileSizeInput" @click="onResetOne('lytOpts.maxTileSz')" :class="rLabelClasses">
Max Tile Size
</label>
- <input type="range" min="15" max="400" v-model.number="lytOpts.maxTileSz"
- @input="onSettingChgThrottled('LYT', 'maxTileSz')" @change="onSettingChg('LYT', 'maxTileSz')"
+ <input type="range" min="15" max="400" v-model.number="store.lytOpts.maxTileSz"
+ @input="onSettingChgThrottled('lytOpts.maxTileSz')" @change="onSettingChg('lytOpts.maxTileSz')"
name="maxTileSizeInput" ref="maxTileSzRef"/>
- <div class="my-auto text-right">{{lytOpts.maxTileSz}} px</div>
+ <div class="my-auto text-right">{{store.lytOpts.maxTileSz}} px</div>
<!-- Row 3 -->
- <label for="tileSpacingInput" @click="onReset('LYT', 'tileSpacing')" :class="rLabelClasses">
+ <label for="tileSpacingInput" @click="onResetOne('lytOpts.tileSpacing')" :class="rLabelClasses">
Tile Spacing
</label>
- <input type="range" min="0" max="20" v-model.number="lytOpts.tileSpacing"
- @input="onSettingChgThrottled('LYT', 'tileSpacing')" @change="onSettingChg('LYT', 'tileSpacing')"
+ <input type="range" min="0" max="20" v-model.number="store.lytOpts.tileSpacing"
+ @input="onSettingChgThrottled('lytOpts.tileSpacing')" @change="onSettingChg('lytOpts.tileSpacing')"
name="tileSpacingInput"/>
- <div class="my-auto text-right">{{lytOpts.tileSpacing}} px</div>
+ <div class="my-auto text-right">{{store.lytOpts.tileSpacing}} px</div>
</div>
</div>
<div class="pb-2 px-2 md:px-3" :class="borderBClasses">
@@ -81,29 +81,29 @@
<div>
Tree to use
<ul class="flex justify-evenly">
- <li> <label> <input type="radio" v-model="uiOpts.tree" value="trimmed"
- @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"
- @change="onSettingChg('UI', 'tree')"/> Minimal </label> </li>
+ <li> <label> <input type="radio" v-model="store.tree" value="trimmed"
+ @change="onSettingChg('tree')"/> Complex </label> </li>
+ <li> <label> <input type="radio" v-model="store.tree" value="images"
+ @change="onSettingChg('tree')"/> Visual </label> </li>
+ <li> <label> <input type="radio" v-model="store.tree" value="picked"
+ @change="onSettingChg('tree')"/> Minimal </label> </li>
</ul>
</div>
<div>
- <label> <input type="checkbox" v-model="uiOpts.searchJumpMode"
- @change="onSettingChg('UI', 'searchJumpMode')"/> Skip search animation </label>
+ <label> <input type="checkbox" v-model="store.searchJumpMode"
+ @change="onSettingChg('searchJumpMode')"/> Skip search animation </label>
</div>
<div>
- <label> <input type="checkbox" v-model="uiOpts.autoHide"
- @change="onSettingChg('UI', 'autoHide')"/> Auto-hide ancestors </label>
+ <label> <input type="checkbox" v-model="store.autoHide"
+ @change="onSettingChg('autoHide')"/> Auto-hide ancestors </label>
</div>
- <div v-if="uiOpts.touchDevice == false">
- <label> <input type="checkbox" v-model="uiOpts.disableShortcuts"
- @change="onSettingChg('UI', 'disableShortcuts')"/> Disable keyboard shortcuts </label>
+ <div v-if="store.touchDevice == false">
+ <label> <input type="checkbox" v-model="store.disableShortcuts"
+ @change="onSettingChg('disableShortcuts')"/> Disable keyboard shortcuts </label>
</div>
</div>
- <s-button class="mx-auto my-2" :style="{color: uiOpts.textColor, backgroundColor: uiOpts.bgColor}"
- @click="onResetAll">
+ <s-button class="mx-auto my-2" :style="{color: store.color.text, backgroundColor: store.color.bg}"
+ @click="onReset">
Reset
</s-button>
<transition name="fade">
@@ -114,11 +114,10 @@
</template>
<script setup lang="ts">
-import {ref, computed, watch, PropType} from 'vue';
+import {ref, computed, watch} from 'vue';
import SButton from './SButton.vue';
import CloseIcon from './icon/CloseIcon.vue';
-import {UiOptions, OptionType, getDefaultLytOpts, getDefaultUiOpts} from '../lib';
-import {LayoutOptions} from '../layout';
+import {useStore, StoreState} from '../store';
// Refs
const rootRef = ref(null as HTMLDivElement | null);
@@ -127,37 +126,36 @@ const minTileSzRef = ref(null as HTMLInputElement | null);
const maxTileSzRef = ref(null as HTMLInputElement | null);
const saveIndRef = ref(null as HTMLDivElement | null);
-// Props + events
-const props = defineProps({
- lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
-});
-const emit = defineEmits(['close', 'setting-chg', 'reset', ]);
+// Global store
+const store = useStore();
-// For settings
-const sweepLeaves = ref(props.lytOpts.layoutType == 'sweep');
- // For making only two of 'layoutType's values available for user selection)
-watch(sweepLeaves, (newVal) => {props.lytOpts.layoutType = newVal ? 'sweep' : 'rect'})
+// Events
+const emit = defineEmits(['close', 'setting-chg', 'reset']);
+
+// For making only two of 'layoutType's values available for user selection)
+const sweepLeaves = ref(store.lytOpts.layoutType == 'sweep');
+watch(sweepLeaves, (newVal) => {store.lytOpts.layoutType = newVal ? 'sweep' : 'rect'})
// Settings change handling
const saved = ref(false); // Set to true after a setting is saved
-const settingChgTimeout = ref(0); // Used to throttle some setting-change handling
-function onSettingChg(optionType: OptionType, option: string){
+let settingChgTimeout = 0; // Used to throttle some setting-change handling
+function onSettingChg(option: string){
// Maintain min/max-tile-size consistency
- if (optionType == 'LYT' && (option == 'minTileSz' || option == 'maxTileSz')){
+ if (option == 'lytOpts.minTileSz' || option == 'lytOpts.maxTileSz'){
let minInput = minTileSzRef.value!;
let maxInput = maxTileSzRef.value!;
- if (option == 'minTileSz' && Number(minInput.value) > Number(maxInput.value)){
- props.lytOpts.maxTileSz = props.lytOpts.minTileSz;
- emit('setting-chg', 'LYT', 'maxTileSz');
- } else if (option == 'maxTileSz' && Number(maxInput.value) < Number(minInput.value)){
- props.lytOpts.minTileSz = props.lytOpts.maxTileSz;
- emit('setting-chg', 'LYT', 'minTileSz');
+ if (Number(minInput.value) > Number(maxInput.value)){
+ if (option == 'lytOpts.minTileSz'){
+ store.lytOpts.maxTileSz = store.lytOpts.minTileSz;
+ emit('setting-chg', 'lytOpts.maxTileSz');
+ } else {
+ store.lytOpts.minTileSz = store.lytOpts.maxTileSz;
+ emit('setting-chg', 'lytOpts.minTileSz');
+ }
}
}
- // Notify parent component
- emit('setting-chg', optionType, option,
- {relayout: optionType == 'LYT', reinit: optionType == 'UI' && option == 'tree'});
+ // Notify parent (might need to relayout)
+ emit('setting-chg', option);
// Possibly make saved-indicator appear/animate
if (!saved.value){
saved.value = true;
@@ -168,48 +166,24 @@ function onSettingChg(optionType: OptionType, option: string){
el.classList.add('animate-flash-green');
}
}
-function onSettingChgThrottled(optionType: OptionType, option: string){
- if (settingChgTimeout.value == 0){
- settingChgTimeout.value = setTimeout(() => {
- settingChgTimeout.value = 0;
- onSettingChg(optionType, option);
- }, props.uiOpts.animationDelay);
+function onSettingChgThrottled(option: string){
+ if (settingChgTimeout == 0){
+ settingChgTimeout = setTimeout(() => {
+ settingChgTimeout = 0;
+ onSettingChg(option);
+ }, store.animationDelay);
}
}
-function onReset(optionType: OptionType, option: string){
- // Restore the setting's default
- let defaultLytOpts = getDefaultLytOpts();
- let defaultUiOpts = getDefaultUiOpts(defaultLytOpts);
- if (optionType == 'LYT'){
- let lytOpt = option as keyof LayoutOptions;
- if (props.lytOpts[lytOpt] == defaultLytOpts[lytOpt]){
- return;
- }
- (props.lytOpts[lytOpt] as any) = defaultLytOpts[lytOpt];
- if (option == 'layoutType'){
- sweepLeaves.value = props.lytOpts.layoutType == 'sweep';
- }
- } else {
- let uiOpt = option as keyof UiOptions;
- if (props.uiOpts[uiOpt] == defaultUiOpts[uiOpt]){
- return;
- }
- (props.uiOpts[uiOpt] as any) = defaultUiOpts[uiOpt];
+function onResetOne(option: string){
+ store.resetOne(option);
+ if (option == 'lytOpts.layoutType'){
+ sweepLeaves.value = (store.lytOpts.layoutType == 'sweep');
}
- // Notify parent component
- onSettingChg(optionType, option);
+ onSettingChg(option);
}
-function onResetAll(){
- // Restore default options
- let defaultLytOpts = getDefaultLytOpts();
- let defaultUiOpts = getDefaultUiOpts(defaultLytOpts);
- let needReinit = props.uiOpts.tree != defaultUiOpts.tree;
- Object.assign(props.lytOpts, defaultLytOpts);
- Object.assign(props.uiOpts, defaultUiOpts);
- // Notify parent component
- emit('reset', needReinit);
- // Clear saved-indicator
- saved.value = false;
+function onReset(){
+ emit('reset'); // Notify parent (might need to relayout)
+ saved.value = false; // Clear saved-indicator
}
// Close handling
@@ -221,9 +195,9 @@ function onClose(evt: Event){
// Styles and classes
const styles = computed(() => ({
- backgroundColor: props.uiOpts.bgColorAlt,
- borderRadius: props.uiOpts.borderRadius + 'px',
- boxShadow: props.uiOpts.shadowNormal,
+ backgroundColor: store.color.bgAlt,
+ borderRadius: store.borderRadius + 'px',
+ boxShadow: store.shadowNormal,
}));
const borderBClasses = 'border-b border-stone-400';
const rLabelClasses = "w-fit hover:cursor-pointer hover:text-lime-600"; // For reset-upon-click labels
diff --git a/src/components/TileInfoModal.vue b/src/components/TileInfoModal.vue
index fc09d86..2d0e354 100644
--- a/src/components/TileInfoModal.vue
+++ b/src/components/TileInfoModal.vue
@@ -58,35 +58,35 @@
</template>
<template v-slot:content>
<ul class="rounded shadow overflow-x-auto p-1"
- :style="{backgroundColor: uiOpts.bgColor, color: uiOpts.textColor}">
+ :style="{backgroundColor: store.color.bg, color: store.color.text}">
<li v-if="imgInfos[idx]!.url != ''">
- <span :style="{color: uiOpts.altColor}">Source: </span>
+ <span :style="{color: store.color.alt}">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 != ''" class="whitespace-nowrap">
- <span :style="{color: uiOpts.altColor}">Artist: </span>
+ <span :style="{color: store.color.alt}">Artist: </span>
{{imgInfos[idx]!.artist}}
</li>
<li v-if="imgInfos[idx]!.credit != ''" class="whitespace-nowrap">
- <span :style="{color: uiOpts.altColor}">Credits: </span>
+ <span :style="{color: store.color.alt}">Credits: </span>
{{imgInfos[idx]!.credit}}
</li>
<li>
- <span :style="{color: uiOpts.altColor}">License: </span>
+ <span :style="{color: store.color.alt}">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>
+ <span :style="{color: store.color.alt}">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>
+ <span :style="{color: store.color.alt}">Changes: </span>
Cropped and resized
</li>
</ul>
@@ -119,22 +119,21 @@ import ExternalLinkIcon from './icon/ExternalLinkIcon.vue';
import DownIcon from './icon/DownIcon.vue';
import LinkIcon from './icon/LinkIcon.vue';
import {TolNode} from '../tol';
-import {LayoutOptions} from '../layout';
-import {getImagePath, DescInfo, ImgInfo, InfoResponse, UiOptions} from '../lib';
+import {getImagePath, DescInfo, ImgInfo, InfoResponse} from '../lib';
import {capitalizeWords} from '../util';
+import {useStore} from '../store';
// Refs
const rootRef = ref(null as HTMLDivElement | null);
const closeRef = ref(null as typeof CloseIcon | null);
+// Global store
+const store = useStore();
+
// Props + events
const props = defineProps({
- // Node data to display
nodeName: {type: String, required: true},
infoResponse: {type: Object as PropType<InfoResponse>, required: true},
- // Options
- lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
});
const emit = defineEmits(['close']);
@@ -239,9 +238,9 @@ function onLinkIconClick(){
// Styles
const styles = computed(() => ({
- backgroundColor: props.uiOpts.bgColorAlt,
- borderRadius: props.uiOpts.borderRadius + 'px',
- boxShadow: props.uiOpts.shadowNormal,
+ backgroundColor: store.color.bgAlt,
+ borderRadius: store.borderRadius + 'px',
+ boxShadow: store.shadowNormal,
overflow: 'visible auto',
}));
function getImgStyles(tolNode: TolNode | null): Record<string,string> {
@@ -255,10 +254,10 @@ function getImgStyles(tolNode: TolNode | null): Record<string,string> {
backgroundImage: imgName != null ?
`url('${getImagePath(imgName as string)}')` :
'none',
- backgroundColor: props.uiOpts.bgColorDark,
+ backgroundColor: store.color.bgDark,
backgroundSize: 'cover',
- borderRadius: props.uiOpts.borderRadius + 'px',
- boxShadow: props.uiOpts.shadowNormal,
+ borderRadius: store.borderRadius + 'px',
+ boxShadow: store.shadowNormal,
};
}
function iucnStyles(iucn: string): Record<string,string>{
@@ -277,8 +276,8 @@ function iucnStyles(iucn: string): Record<string,string>{
};
}
const linkCopyLabelStyles = computed(() => ({
- color: props.uiOpts.textColor,
- backgroundColor: props.uiOpts.bgColor,
- borderRadius: props.uiOpts.borderRadius + 'px',
+ color: store.color.text,
+ backgroundColor: store.color.bg,
+ borderRadius: store.borderRadius + 'px',
}));
</script>
diff --git a/src/components/TolTile.vue b/src/components/TolTile.vue
index 810c5b7..a30c6d9 100644
--- a/src/components/TolTile.vue
+++ b/src/components/TolTile.vue
@@ -37,7 +37,7 @@
</transition>
</div>
<tol-tile v-for="child in visibleChildren" :key="child.name"
- :layoutNode="child" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" :overflownDim="overflownDim"
+ :layoutNode="child" :tolMap="tolMap" :overflownDim="overflownDim"
@leaf-click="onInnerLeafClick" @nonleaf-click="onInnerNonleafClick"
@leaf-click-held="onInnerLeafClickHeld" @nonleaf-click-held="onInnerNonleafClickHeld"
@info-click="onInnerInfoIconClick"/>
@@ -52,22 +52,23 @@
import {ref, computed, watch, PropType} from 'vue';
import InfoIcon from './icon/InfoIcon.vue';
import {TolMap} from '../tol';
-import {LayoutNode, LayoutOptions} from '../layout';
-import {getImagePath, UiOptions} from '../lib';
+import {LayoutNode} from '../layout';
+import {getImagePath} from '../lib';
import {capitalizeWords} from '../util';
+import {useStore} from '../store';
const SCRIM_GRADIENT = 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.2) 100%)';
// Refs
const rootRef = ref(null as HTMLDivElement | null);
+// Global store
+const store = useStore();
+
// 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},
@@ -120,26 +121,26 @@ 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'));
+const infoIconDisabled = computed(() => !store.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){
+ if (!store.touchDevice){
// Wait for a mouseup or click-hold
clearTimeout(clickHoldTimer.value);
clickHoldTimer.value = setTimeout(() => {
clickHoldTimer.value = 0;
onClickHold();
- }, props.uiOpts.clickHoldDuration);
+ }, store.clickHoldDuration);
} else {
// Wait for or recognise a double-click
if (clickHoldTimer.value == 0){
clickHoldTimer.value = setTimeout(() => {
clickHoldTimer.value = 0;
onClick();
- }, props.uiOpts.clickHoldDuration);
+ }, store.clickHoldDuration);
} else {
clearTimeout(clickHoldTimer.value)
clickHoldTimer.value = 0;
@@ -148,7 +149,7 @@ function onMouseDown(): void {
}
}
function onMouseUp(): void {
- if (!props.uiOpts.touchDevice){
+ if (!store.touchDevice){
if (clickHoldTimer.value > 0){
clearTimeout(clickHoldTimer.value);
clickHoldTimer.value = 0;
@@ -226,14 +227,14 @@ function onScroll(): void {
pendingScrollHdlr.value = setTimeout(() => {
scrollOffset.value = rootRef.value!.scrollTop;
pendingScrollHdlr.value = 0;
- }, props.uiOpts.animationDelay);
+ }, store.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 bottomY = focusedChild.pos[1] + focusedChild.dims[1] + store.lytOpts.tileSpacing;
let scrollTop = Math.max(0, bottomY - (props.overflownDim / 2)); // No need to manually cap at max
rootRef.value!.scrollTop = scrollTop;
}
@@ -253,16 +254,16 @@ function onTransitionEnd(){
// 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){
+ if (valChanged && store.transitionDuration > 100 && !inTransition.value){
inTransition.value = true;
- setTimeout(onTransitionEnd, props.uiOpts.transitionDuration);
+ setTimeout(onTransitionEnd, store.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){
+ if (valChanged && store.transitionDuration > 100 && !inTransition.value){
inTransition.value = true;
- setTimeout(onTransitionEnd, props.uiOpts.transitionDuration);
+ setTimeout(onTransitionEnd, store.transitionDuration);
}
});
@@ -285,7 +286,7 @@ 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);
+ setTimeout(() => {inFlash.value = false;}, 300);
}
});
@@ -294,32 +295,32 @@ const justUnhidden = ref(false); // Used to allow overflow temporarily after bei
watch(() => props.layoutNode.hidden, (newVal: boolean, oldVal: boolean) => {
if (oldVal && !newVal){
justUnhidden.value = true;
- setTimeout(() => {justUnhidden.value = false}, props.uiOpts.transitionDuration + 100);
+ setTimeout(() => {justUnhidden.value = false}, store.transitionDuration + 100);
}
});
// Styles + classes
const nonleafBgColor = computed(() => {
- let colorArray = props.uiOpts.nonleafBgColors;
+ let colorArray = store.nonleafBgColors;
return colorArray[props.layoutNode.depth % colorArray.length];
});
const boxShadow = computed((): string => {
if (highlight.value){
- return props.uiOpts.shadowHovered;
+ return store.shadowHovered;
} else if (props.layoutNode.hasFocus && !inTransition.value){
- return props.uiOpts.shadowFocused;
+ return store.shadowFocused;
} else {
- return props.uiOpts.shadowNormal;
+ return store.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;
+ return store.lytOpts.headerSz * 0.8;
} else if (props.layoutNode.dims[0] >= 80){
- return props.lytOpts.headerSz * 0.7;
+ return store.lytOpts.headerSz * 0.7;
} else {
- return props.lytOpts.headerSz * 0.6;
+ return store.lytOpts.headerSz * 0.6;
}
});
//
@@ -330,11 +331,11 @@ const styles = computed((): Record<string,string> => {
top: props.layoutNode.pos[1] + 'px',
width: props.layoutNode.dims[0] + 'px',
height: props.layoutNode.dims[1] + 'px',
- borderRadius: props.uiOpts.borderRadius + 'px',
+ borderRadius: store.borderRadius + 'px',
boxShadow: boxShadow.value,
visibility: 'visible',
// Transition related
- transitionDuration: (props.skipTransition ? 0 : props.uiOpts.transitionDuration) + 'ms',
+ transitionDuration: (props.skipTransition ? 0 : store.transitionDuration) + 'ms',
transitionProperty: 'left, top, width, height, visibility',
transitionTimingFunction: 'ease-out',
zIndex: inTransition.value && wasClicked.value ? '1' : '0',
@@ -342,10 +343,10 @@ const styles = computed((): Record<string,string> => {
'hidden' : 'visible',
// CSS variables
'--nonleafBgColor': nonleafBgColor.value,
- '--tileSpacing': props.lytOpts.tileSpacing + 'px',
+ '--tileSpacing': store.lytOpts.tileSpacing + 'px',
};
if (!isLeaf.value){
- let borderR = props.uiOpts.borderRadius + 'px';
+ let borderR = store.borderRadius + 'px';
if (props.layoutNode.sepSweptArea != null){
borderR = props.layoutNode.sepSweptArea.sweptLeft ?
`${borderR} ${borderR} ${borderR} 0` :
@@ -354,7 +355,7 @@ const styles = computed((): Record<string,string> => {
layoutStyles.borderRadius = borderR;
}
if (isOverflownRoot.value){
- layoutStyles.width = (props.layoutNode.dims[0] + props.uiOpts.scrollGap) + 'px';
+ layoutStyles.width = (props.layoutNode.dims[0] + store.scrollGap) + 'px';
layoutStyles.height = props.overflownDim + 'px';
layoutStyles.overflow = 'hidden scroll';
}
@@ -377,7 +378,7 @@ const leafStyles = computed((): Record<string,string> => {
backgroundImage: tolNode.value.imgName != null ?
`${SCRIM_GRADIENT},url('${getImagePath(tolNode.value.imgName as string)}')` :
'none',
- backgroundColor: props.uiOpts.bgColorDark,
+ backgroundColor: store.color.bgDark,
backgroundSize: 'cover',
};
}
@@ -385,8 +386,8 @@ const leafStyles = computed((): Record<string,string> => {
});
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){
+ let textColor = store.color.text;
+ for (let [threshold, color] of store.childQtyColors){
if (numChildren >= threshold){
textColor = color;
} else {
@@ -413,7 +414,7 @@ function leafSubImgStyles(idx: number): Record<string,string> {
backgroundImage: (tolNode.value.imgName![idx]! != null) ?
`${SCRIM_GRADIENT},url('${getImagePath(tolNode.value.imgName![idx]! as string)}')` :
'none',
- backgroundColor: props.uiOpts.bgColorDark,
+ backgroundColor: store.color.bgDark,
backgroundSize: '125%',
borderRadius: 'inherit',
clipPath: (idx == 0) ? 'polygon(0 0, 100% 0, 0 100%)' : 'polygon(100% 0, 0 100%, 100% 100%)',
@@ -438,10 +439,10 @@ const nonleafStyles = computed((): Record<string,string> => {
const nonleafHeaderStyles = computed((): Record<string,string> => {
let styles: Record<string,string> = {
position: 'static',
- height: props.lytOpts.headerSz + 'px',
+ height: store.lytOpts.headerSz + 'px',
borderTopLeftRadius: 'inherit',
borderTopRightRadius: 'inherit',
- backgroundColor: props.uiOpts.nonleafHeaderColor,
+ backgroundColor: store.nonleafHeaderColor,
};
if (isOverflownRoot.value){
styles = {
@@ -451,7 +452,7 @@ const nonleafHeaderStyles = computed((): Record<string,string> => {
left: '0',
borderTopRightRadius: '0',
zIndex: '1',
- boxShadow: props.uiOpts.shadowNormal,
+ boxShadow: store.shadowNormal,
};
}
return styles;
@@ -461,19 +462,19 @@ const nonleafHeaderTextStyles = computed(() => ({
fontSize: fontSz.value + 'px',
paddingLeft: (fontSz.value * 0.2) + 'px',
textAlign: 'center',
- color: props.uiOpts.textColor,
+ color: store.color.text,
// For ellipsis
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}));
const sepSweptAreaStyles = computed((): Record<string,string> => {
- let borderR = props.uiOpts.borderRadius + 'px';
+ let borderR = store.borderRadius + 'px';
let styles = {
position: 'absolute',
backgroundColor: nonleafBgColor.value,
boxShadow: boxShadow.value,
- transitionDuration: props.uiOpts.transitionDuration + 'ms',
+ transitionDuration: store.transitionDuration + 'ms',
transitionProperty: 'left, top, width, height, visibility',
transitionTimingFunction: 'ease-out',
};
@@ -495,7 +496,7 @@ const sepSweptAreaStyles = computed((): Record<string,string> => {
...styles,
visibility: 'hidden',
left: '0',
- top: props.lytOpts.headerSz + 'px',
+ top: store.lytOpts.headerSz + 'px',
width: '0',
height: '0',
borderRadius: borderR,
@@ -512,8 +513,8 @@ const sepSweptAreaHideEdgeClass = computed((): string => {
}
});
const infoIconStyles = computed((): Record<string,string> => {
- let size = (props.lytOpts.headerSz * 0.85);
- let marginSz = (props.lytOpts.headerSz - size);
+ let size = (store.lytOpts.headerSz * 0.85);
+ let marginSz = (store.lytOpts.headerSz - size);
return {
width: size + 'px',
height: size + 'px',
diff --git a/src/components/TutorialPane.vue b/src/components/TutorialPane.vue
index 4c24bae..3ccbc46 100644
--- a/src/components/TutorialPane.vue
+++ b/src/components/TutorialPane.vue
@@ -70,7 +70,11 @@
import {ref, computed, watch, onMounted, PropType} from 'vue';
import SButton from './SButton.vue';
import CloseIcon from './icon/CloseIcon.vue';
-import {Action, UiOptions} from '../lib';
+import {Action} from '../lib';
+import {useStore} from '../store';
+
+// Global store
+const store = useStore();
// Props + events
const props = defineProps({
@@ -79,9 +83,8 @@ const props = defineProps({
triggerFlag: {type: Boolean, required: true},
// Used to indicate that a tutorial-requested 'trigger' action has been done
skipWelcome: {type: Boolean, default: false},
- uiOpts: {type: Object as PropType<UiOptions>, required: true},
});
-const touchDevice = computed(() => props.uiOpts.touchDevice);
+const touchDevice = computed(() => store.touchDevice);
const emit = defineEmits(['close', 'stage-chg', 'skip']);
// For tutorial stage
@@ -118,13 +121,13 @@ function onStageChange(){
if (stage.value == 1 && !disabledOnce){
for (let action of STAGE_ACTIONS){
if (action != null && !props.actionsDone.has(action)){
- props.uiOpts.disabledActions.add(action);
+ store.disabledActions.add(action);
}
}
disabledOnce = true;
}
// Enable action for this stage
- props.uiOpts.disabledActions.delete(STAGE_ACTIONS[stage.value - 1]);
+ store.disabledActions.delete(STAGE_ACTIONS[stage.value - 1]);
// Notify of new trigger-action
emit('stage-chg', STAGE_ACTIONS[stage.value - 1]);
// After stage 1, show prev/next buttons
@@ -148,8 +151,8 @@ watch(() => props.triggerFlag, () => {
// Styles
const styles = computed(() => ({
- backgroundColor: props.uiOpts.bgColorDark,
- color: props.uiOpts.textColor,
+ backgroundColor: store.color.bgDark,
+ color: store.color.text,
}));
const contentStyles = {
padding: '0 0.5cm',
@@ -157,7 +160,7 @@ const contentStyles = {
textAlign: 'center',
};
const buttonStyles = computed(() => ({
- color: props.uiOpts.textColor,
- backgroundColor: props.uiOpts.bgColor,
+ color: store.color.text,
+ backgroundColor: store.color.bg,
}));
</script>
diff --git a/src/layout.ts b/src/layout.ts
index 140af77..2739037 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -5,7 +5,7 @@
* LayoutNode tree, on which tryLayout() can run a layout algorithm.
*/
-import {TolNode, TolMap} from './tol';
+import {TolMap} from './tol';
import {range, arraySum, linspace, limitVals, updateAscSeq} from './util';
// Represents a node/tree that holds layout data for a TolNode node/tree
@@ -51,20 +51,22 @@ export class LayoutNode {
let newNode: LayoutNode;
if (chg != null && this == chg.node){
switch (chg.type){
- case 'expand':
- let children = chg.tolMap.get(this.name)!.children.map((n: string) => new LayoutNode(n, []));
+ case 'expand': {
+ const children = chg.tolMap.get(this.name)!.children.map((n: string) => new LayoutNode(n, []));
newNode = new LayoutNode(this.name, children);
newNode.children.forEach(n => {
n.parent = newNode;
n.depth = this.depth + 1;
});
break;
- case 'collapse':
+ }
+ case 'collapse': {
newNode = new LayoutNode(this.name, []);
break;
+ }
}
} else {
- let children = this.children.map(n => n.cloneNodeTree(chg));
+ const children = this.children.map(n => n.cloneNodeTree(chg));
newNode = new LayoutNode(this.name, children);
children.forEach(n => {n.parent = newNode});
}
@@ -109,9 +111,9 @@ export class LayoutNode {
// Given a sequence of child/grandchild/etc names, adds this/the_child's/the_grandchild's/etc children
addDescendantChain(nameChain: string[], tolMap: TolMap, map?: LayoutMap): void {
let layoutNode = this as LayoutNode;
- for (let childName of nameChain){
+ for (const childName of nameChain){
// Add children
- let tolNode = tolMap.get(layoutNode.name)!;
+ const tolNode = tolMap.get(layoutNode.name)!;
layoutNode.children = tolNode.children.map((name: string) => new LayoutNode(name, []));
layoutNode.children.forEach(node => {
node.parent = layoutNode;
@@ -122,7 +124,7 @@ export class LayoutNode {
});
LayoutNode.updateTips(layoutNode, layoutNode.children.length - 1);
// Get matching child node
- let childNode = layoutNode.children.find(n => n.name == childName);
+ const childNode = layoutNode.children.find(n => n.name == childName);
if (childNode == null){
throw new Error('Child name not found');
}
@@ -204,7 +206,7 @@ export function initLayoutMap(layoutTree: LayoutNode): LayoutMap {
map.set(node.name, node);
node.children.forEach(n => helper(n, map));
}
- let map = new Map();
+ const map = new Map();
helper(layoutTree, map);
return map;
}
@@ -221,19 +223,19 @@ function removeFromLayoutMap(node: LayoutNode, map: LayoutMap): void {
// Creates a LayoutNode representing a TolNode tree, up to a given depth (0 means just the root, -1 means no limit)
export function initLayoutTree(tolMap: TolMap, rootName: string, depth: number): LayoutNode {
- function initHelper(tolMap: TolMap, nodeName: string, depthLeft: number, atDepth: number = 0): LayoutNode {
+ function initHelper(tolMap: TolMap, nodeName: string, depthLeft: number, atDepth = 0): LayoutNode {
if (depthLeft == 0){
- let node = new LayoutNode(nodeName, []);
+ const node = new LayoutNode(nodeName, []);
node.depth = atDepth;
return node;
} else {
- let childNames = tolMap.get(nodeName)!.children;
+ const childNames = tolMap.get(nodeName)!.children;
if (childNames.length == 0 || !tolMap.has(childNames[0])){
return new LayoutNode(nodeName, []);
} else {
- let children = childNames.map((name: string) =>
+ const children = childNames.map((name: string) =>
initHelper(tolMap, name, depthLeft != -1 ? depthLeft-1 : -1, atDepth+1));
- let node = new LayoutNode(nodeName, children);
+ const node = new LayoutNode(nodeName, children);
children.forEach(n => n.parent = node);
return node;
}
@@ -251,7 +253,7 @@ export function tryLayout(
{allowCollapse = false, chg = null as LayoutTreeChg | null, layoutMap = null as LayoutMap | null} = {}
): boolean {
// Create a new LayoutNode tree, in case of layout failure
- let tempTree = layoutTree.cloneNodeTree(chg);
+ const tempTree = layoutTree.cloneNodeTree(chg);
let success: boolean;
switch (options.layoutType){
case 'sqr': success = sqrLayout(tempTree, [0,0], dims, true, allowCollapse, options); break;
@@ -283,8 +285,8 @@ type LayoutFn = (
ownOpts?: any,
) => boolean;
// Lays out node as one square, ignoring child nodes // Used for base cases
-let oneSqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){
- let tileSz = Math.min(dims[0], dims[1], opts.maxTileSz);
+const oneSqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){
+ const tileSz = Math.min(dims[0], dims[1], opts.maxTileSz);
if (tileSz < opts.minTileSz){
return false;
}
@@ -292,28 +294,28 @@ let oneSqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollaps
return true;
}
// Lays out nodes as squares within a grid, with intervening+surrounding spacing
-let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){
+const sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){
if (node.children.length == 0){
return oneSqrLayout(node, pos, dims, false, false, opts);
}
// Consider area excluding header and top/left spacing
- let headerSz = showHeader ? opts.headerSz : 0;
- let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
- let newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz];
+ const headerSz = showHeader ? opts.headerSz : 0;
+ const newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
+ const newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz];
if (newDims[0] * newDims[1] <= 0){
return false;
}
// Find number of rows/columns with least empty space
- let numChildren = node.children.length;
- let areaAR = newDims[0] / newDims[1]; // Aspect ratio
+ const numChildren = node.children.length;
+ const areaAR = newDims[0] / newDims[1]; // Aspect ratio
let lowestEmpSpc = Number.POSITIVE_INFINITY, usedNumCols = 0, usedNumRows = 0, usedTileSz = 0;
const MAX_TRIES = 50; // If there are many possibilities, skip some
- let ptlNumCols = numChildren == 1 ? [1] :
+ const ptlNumCols = numChildren == 1 ? [1] :
linspace(1, numChildren, Math.min(numChildren, MAX_TRIES)).map(n => Math.floor(n));
- for (let numCols of ptlNumCols){
- let numRows = Math.ceil(numChildren / numCols);
- let gridAR = numCols / numRows;
- let usedFrac = // Fraction of area occupied by maximally-fitting grid
+ for (const numCols of ptlNumCols){
+ const numRows = Math.ceil(numChildren / numCols);
+ const gridAR = numCols / numRows;
+ const usedFrac = // Fraction of area occupied by maximally-fitting grid
areaAR > gridAR ? gridAR / areaAR : areaAR / gridAR;
// Get tile edge length
let tileSz = (areaAR > gridAR ? newDims[1] / numRows : newDims[0] / numCols) - opts.tileSpacing;
@@ -323,7 +325,7 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
tileSz = opts.maxTileSz;
}
// Get empty space
- let empSpc = (1 - usedFrac) * (newDims[0] * newDims[1]) + // Area outside grid plus ...
+ const empSpc = (1 - usedFrac) * (newDims[0] * newDims[1]) + // Area outside grid plus ...
(numCols * numRows - numChildren) * (tileSz - opts.tileSpacing)**2; // empty cells within grid
// Compare with best-so-far
if (empSpc < lowestEmpSpc){
@@ -344,9 +346,9 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
// Layout children
for (let i = 0; i < numChildren; i++){
- let child = node.children[i];
- let childX = newPos[0] + (i % usedNumCols) * (usedTileSz + opts.tileSpacing);
- let childY = newPos[1] + Math.floor(i / usedNumCols) * (usedTileSz + opts.tileSpacing);
+ const child = node.children[i];
+ const childX = newPos[0] + (i % usedNumCols) * (usedTileSz + opts.tileSpacing);
+ const childY = newPos[1] + Math.floor(i / usedNumCols) * (usedTileSz + opts.tileSpacing);
let success: boolean;
if (child.children.length == 0){
success = oneSqrLayout(child, [childX,childY], [usedTileSz,usedTileSz], false, false, opts);
@@ -363,11 +365,11 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
}
// Create layout
- let usedDims: [number, number] = [
+ const usedDims: [number, number] = [
usedNumCols * (usedTileSz + opts.tileSpacing) + opts.tileSpacing,
usedNumRows * (usedTileSz + opts.tileSpacing) + opts.tileSpacing + headerSz,
];
- let empSpc = // Empty space within usedDims area
+ const empSpc = // Empty space within usedDims area
(usedNumCols * usedNumRows - numChildren) * (usedTileSz - opts.tileSpacing)**2 +
arraySum(node.children.map(child => child.empSpc));
node.assignLayoutData(pos, usedDims, {showHeader, empSpc});
@@ -375,7 +377,7 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
// Lays out nodes as rows of rectangles, deferring to sqrLayout() or oneSqrLayout() for simpler cases
//'subLayoutFn' allows other LayoutFns to use this layout, but transfer control back to themselves on recursion
-let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts,
+const rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts,
ownOpts?: {subLayoutFn?: LayoutFn}){
// Check for simpler cases
if (node.children.length == 0){
@@ -384,9 +386,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
return sqrLayout(node, pos, dims, showHeader, allowCollapse, opts);
}
// Consider area excluding header and top/left spacing
- let headerSz = showHeader ? opts.headerSz : 0;
- let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
- let newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz];
+ const headerSz = showHeader ? opts.headerSz : 0;
+ const newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
+ const newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz];
if (newDims[0] * newDims[1] < node.tips * (opts.minTileSz + opts.tileSpacing)**2){
if (allowCollapse){
node.children = [];
@@ -397,7 +399,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
// Try finding arrangement with low empty space
// Done by searching possible row-groupings, allocating within rows using 'tips' vals, and trimming empty space
- let numChildren = node.children.length;
+ const numChildren = node.children.length;
let rowBrks: number[] = []; // Will hold indices for nodes at which each row starts
let lowestEmpSpc = Number.POSITIVE_INFINITY;
let usedTree: LayoutNode | null = null; // Best-so-far layout
@@ -439,7 +441,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
if (rowBrks.length == 0){
rowBrks = [0];
} else {
- let updated = updateAscSeq(rowBrks, numChildren);
+ const updated = updateAscSeq(rowBrks, numChildren);
if (!updated){
break RowBrksLoop;
}
@@ -450,18 +452,18 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
rowBrks = [0];
} else {
// Get next possible first row
- let idxFirstRowLastEl = (rowBrks.length == 1 ? numChildren : rowBrks[1]) - 1;
+ const idxFirstRowLastEl = (rowBrks.length == 1 ? numChildren : rowBrks[1]) - 1;
if (idxFirstRowLastEl == 0){
break RowBrksLoop;
}
rowBrks = [0];
rowBrks.push(idxFirstRowLastEl);
// Allocate remaining rows
- let firstRowTips = arraySum(range(rowBrks[1]).map(idx => node.children[idx].tips));
+ const firstRowTips = arraySum(range(rowBrks[1]).map(idx => node.children[idx].tips));
let tipsTotal = node.children[idxFirstRowLastEl].tips;
let nextRowIdx = idxFirstRowLastEl + 1;
while (nextRowIdx < numChildren){ // Over potential next row breaks
- let nextTipsTotal = tipsTotal + node.children[nextRowIdx].tips;
+ const nextTipsTotal = tipsTotal + node.children[nextRowIdx].tips;
if (nextTipsTotal <= firstRowTips){ // If acceptable within current row
tipsTotal = nextTipsTotal;
} else {
@@ -474,26 +476,26 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
break;
}
// Create array-of-arrays representing each rows' cells' 'tips' values
- let rowsOfCnts: number[][] = new Array(rowBrks.length);
+ const rowsOfCnts: number[][] = new Array(rowBrks.length);
for (let rowIdx = 0; rowIdx < rowBrks.length; rowIdx++){
- let numNodes = (rowIdx < rowBrks.length - 1) ?
+ const numNodes = (rowIdx < rowBrks.length - 1) ?
rowBrks[rowIdx + 1] - rowBrks[rowIdx] :
numChildren - rowBrks[rowIdx];
- let rowNodeIdxs = range(numNodes).map(i => i + rowBrks![rowIdx]);
+ const rowNodeIdxs = range(numNodes).map(i => i + rowBrks![rowIdx]);
rowsOfCnts[rowIdx] = rowNodeIdxs.map(idx => node.children[idx].tips);
}
// Get initial cell dims
- let cellWs: number[][] = new Array(rowsOfCnts.length);
+ const cellWs: number[][] = new Array(rowsOfCnts.length);
for (let rowIdx = 0; rowIdx < rowsOfCnts.length; rowIdx++){
- let rowCount = arraySum(rowsOfCnts[rowIdx]);
+ const rowCount = arraySum(rowsOfCnts[rowIdx]);
cellWs[rowIdx] = range(rowsOfCnts[rowIdx].length).map(
colIdx => rowsOfCnts[rowIdx][colIdx] / rowCount * newDims[0]);
}
- let totalTips = arraySum(node.children.map(n => n.tips));
+ const totalTips = arraySum(node.children.map(n => n.tips));
let cellHs = rowsOfCnts.map(rowOfCnts => arraySum(rowOfCnts) / totalTips * newDims[1]);
// Check min-tile-size, attempting to reallocate space if needed
for (let rowIdx = 0; rowIdx < rowsOfCnts.length; rowIdx++){
- let newWs = limitVals(cellWs[rowIdx], minCellDims[0], Number.POSITIVE_INFINITY);
+ const newWs = limitVals(cellWs[rowIdx], minCellDims[0], Number.POSITIVE_INFINITY);
if (newWs == null){
continue RowBrksLoop;
}
@@ -504,26 +506,26 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
continue RowBrksLoop;
}
// Get cell xy-coordinates
- let cellXs: number[][] = new Array(rowsOfCnts.length);
+ const cellXs: number[][] = new Array(rowsOfCnts.length);
for (let rowIdx = 0; rowIdx < rowBrks.length; rowIdx++){
cellXs[rowIdx] = [0];
for (let colIdx = 1; colIdx < rowsOfCnts[rowIdx].length; colIdx++){
cellXs[rowIdx].push(cellXs[rowIdx][colIdx - 1] + cellWs[rowIdx][colIdx - 1]);
}
}
- let cellYs: number[] = new Array(rowsOfCnts.length).fill(0);
+ const cellYs: number[] = new Array(rowsOfCnts.length).fill(0);
for (let rowIdx = 1; rowIdx < rowBrks.length; rowIdx++){
cellYs[rowIdx] = cellYs[rowIdx - 1] + cellHs[rowIdx - 1];
}
// Determine child layouts, resizing cells to reduce empty space
- let tempTree: LayoutNode = node.cloneNodeTree();
+ const tempTree: LayoutNode = node.cloneNodeTree();
let empRight = Number.POSITIVE_INFINITY, empBottom = 0;
for (let rowIdx = 0; rowIdx < rowBrks.length; rowIdx++){
for (let colIdx = 0; colIdx < rowsOfCnts[rowIdx].length; colIdx++){
- let nodeIdx = rowBrks[rowIdx] + colIdx;
- let child: LayoutNode = tempTree.children[nodeIdx];
- let childPos: [number, number] = [newPos[0] + cellXs[rowIdx][colIdx], newPos[1] + cellYs[rowIdx]];
- let childDims: [number, number] = [
+ const nodeIdx = rowBrks[rowIdx] + colIdx;
+ const child: LayoutNode = tempTree.children[nodeIdx];
+ const childPos: [number, number] = [newPos[0] + cellXs[rowIdx][colIdx], newPos[1] + cellYs[rowIdx]];
+ const childDims: [number, number] = [
cellWs[rowIdx][colIdx] - opts.tileSpacing,
cellHs[rowIdx] - opts.tileSpacing
];
@@ -533,14 +535,14 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
} else if (child.children.every(n => n.children.length == 0)){
success = sqrLayout(child, childPos, childDims, true, allowCollapse, opts);
} else {
- let layoutFn = (ownOpts && ownOpts.subLayoutFn) || rectLayout;
+ const layoutFn = (ownOpts && ownOpts.subLayoutFn) || rectLayout;
success = layoutFn(child, childPos, childDims, true, allowCollapse, opts);
}
if (!success){
continue RowBrksLoop;
}
// Remove horizontal empty space by trimming cell and moving/expanding any next cell
- let horzEmp = childDims[0] - child.dims[0];
+ const horzEmp = childDims[0] - child.dims[0];
cellWs[rowIdx][colIdx] -= horzEmp;
if (colIdx < rowsOfCnts[rowIdx].length - 1){
cellXs[rowIdx][colIdx + 1] -= horzEmp;
@@ -550,9 +552,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
}
// Remove vertical empty space by trimming row and moving/expanding any next row
- let childUsedHs = range(rowsOfCnts[rowIdx].length).map(
+ const childUsedHs = range(rowsOfCnts[rowIdx].length).map(
colIdx => tempTree.children[rowBrks[rowIdx] + colIdx].dims[1]);
- let vertEmp = cellHs[rowIdx] - opts.tileSpacing - Math.max(...childUsedHs);
+ const vertEmp = cellHs[rowIdx] - opts.tileSpacing - Math.max(...childUsedHs);
cellHs[rowIdx] -= vertEmp;
if (rowIdx < rowBrks.length - 1){
cellYs[rowIdx + 1] -= vertEmp;
@@ -562,9 +564,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
}
// Get empty space
- let usedSpc = arraySum(tempTree.children.map(
+ const usedSpc = arraySum(tempTree.children.map(
child => (child.dims[0] + opts.tileSpacing) * (child.dims[1] + opts.tileSpacing) - child.empSpc));
- let empSpc = newDims[0] * newDims[1] - usedSpc;
+ const empSpc = newDims[0] * newDims[1] - usedSpc;
// Check with best-so-far
if (empSpc < lowestEmpSpc * opts.rectSensitivity){
lowestEmpSpc = empSpc;
@@ -584,7 +586,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
}
// Create layout
usedTree.copyTreeForRender(node);
- let usedDims: [number, number] = [dims[0] - usedEmpRight, dims[1] - usedEmpBottom];
+ const usedDims: [number, number] = [dims[0] - usedEmpRight, dims[1] - usedEmpBottom];
node.assignLayoutData(pos, usedDims, {showHeader, empSpc: lowestEmpSpc});
return true;
}
@@ -592,10 +594,10 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse,
// With layout option 'sweepToParent', leaves from child nodes may occupy a parent's leaf-section
// 'sepArea' represents a usable leaf-section area from a direct parent,
// and is changed to represent the area used, with those changes visible to the parent for reducing empty space
-let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts,
+const sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts,
ownOpts?: {sepArea?: SepSweptArea}){
// Separate leaf and non-leaf nodes
- let leaves: LayoutNode[] = [], nonLeaves: LayoutNode[] = [];
+ const leaves: LayoutNode[] = [], nonLeaves: LayoutNode[] = [];
node.children.forEach(child => (child.children.length == 0 ? leaves : nonLeaves).push(child));
// Check for simpler cases
if (node.children.length == 0){
@@ -606,17 +608,17 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
return rectLayout(node, pos, dims, showHeader, allowCollapse, opts, {subLayoutFn: sweepLayout});
}
// Some variables
- let headerSz = showHeader ? opts.headerSz : 0;
+ const headerSz = showHeader ? opts.headerSz : 0;
let leavesLyt: LayoutNode | null = null, nonLeavesLyt: LayoutNode | null = null, sweptLeft = false;
let sepArea: SepSweptArea | null = null; // Represents leaf-section area provided for a child
- let haveParentArea = ownOpts != null && ownOpts.sepArea != null;
+ const haveParentArea = ownOpts != null && ownOpts.sepArea != null;
let trySweepToParent = haveParentArea && opts.sweepToParent == 'prefer';
// Using a loop for conditionally retrying layout
while (true){
if (!trySweepToParent){ // Try laying-out normally
// Choose proportion of area to use for leaves
let ratio: number; // area-for-leaves / area-for-non-leaves
- let nonLeavesTiles = arraySum(nonLeaves.map(n => n.tips));
+ const nonLeavesTiles = arraySum(nonLeaves.map(n => n.tips));
switch (opts.sweptNodesPrio){
case 'linear':
ratio = leaves.length / (leaves.length + nonLeavesTiles);
@@ -630,25 +632,27 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
break;
}
// Attempt leaves layout
- let newPos = [0, headerSz];
- let newDims: [number,number] = [dims[0], dims[1] - headerSz];
+ const newPos = [0, headerSz];
+ const newDims: [number,number] = [dims[0], dims[1] - headerSz];
leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves);
// Note: Intentionally neglecting to update child nodes' 'parent' or 'depth' fields here
- let minSz = opts.minTileSz + opts.tileSpacing*4;
- let sweptW = Math.min(Math.max(minSz, newDims[0] * ratio), newDims[0] - minSz);
- let sweptH = Math.min(Math.max(minSz, newDims[1] * ratio), newDims[0] - minSz);
+ const minSz = opts.minTileSz + opts.tileSpacing*4;
+ const sweptW = Math.min(Math.max(minSz, newDims[0] * ratio), newDims[0] - minSz);
+ const sweptH = Math.min(Math.max(minSz, newDims[1] * ratio), newDims[0] - minSz);
let leavesSuccess: boolean;
switch (opts.sweepMode){
- case 'left':
+ case 'left': {
leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts);
sweptLeft = true;
break;
- case 'top':
+ }
+ case 'top': {
leavesSuccess = sqrLayout(leavesLyt, [0,0], [newDims[0], sweptH], false, false, opts);
sweptLeft = false;
break;
- case 'shorter':
- let documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight;
+ }
+ case 'shorter': {
+ const documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight;
if (documentAR >= 1){
leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts);
sweptLeft = true;
@@ -657,18 +661,20 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
sweptLeft = false;
}
break;
- case 'auto':
+ }
+ case 'auto': {
// Attempt left-sweep, then top-sweep on a copy, and copy over if better
leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts);
sweptLeft = true;
- let tempTree = leavesLyt.cloneNodeTree();
- let sweptTopSuccess = sqrLayout(tempTree, [0,0], [newDims[0], sweptH], false, false, opts);;
+ const tempTree = leavesLyt.cloneNodeTree();
+ const sweptTopSuccess = sqrLayout(tempTree, [0,0], [newDims[0], sweptH], false, false, opts);
if (sweptTopSuccess && (!leavesSuccess || tempTree.empSpc < leavesLyt.empSpc)){
tempTree.copyTreeForRender(leavesLyt);
sweptLeft = false;
leavesSuccess = true;
}
break;
+ }
}
if (leavesSuccess){
leavesLyt.children.forEach(lyt => {lyt.pos[1] += headerSz});
@@ -727,7 +733,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
leavesLyt.dims[1] + nonLeavesLyt.dims[1] - opts.tileSpacing + headerSz
];
}
- let empSpc = leavesLyt.empSpc + nonLeavesLyt.empSpc;
+ const empSpc = leavesLyt.empSpc + nonLeavesLyt.empSpc;
node.assignLayoutData(pos, usedDims, {showHeader, empSpc, sepSweptArea: null});
return true;
}
@@ -738,15 +744,15 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
}
break;
} else { // Try using parent-provided area
- let parentArea = ownOpts!.sepArea!;
+ const parentArea = ownOpts!.sepArea!;
// Attempt leaves layout
sweptLeft = parentArea.sweptLeft;
leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves);
- let leavesSuccess = sqrLayout(leavesLyt, [0,0], parentArea.dims, !sweptLeft, false, opts);
+ const leavesSuccess = sqrLayout(leavesLyt, [0,0], parentArea.dims, !sweptLeft, false, opts);
let nonLeavesSuccess = true;
if (leavesSuccess){
// Attempt non-leaves layout
- let newDims: [number,number] = [dims[0], dims[1] - (sweptLeft ? headerSz : 0)];
+ const newDims: [number,number] = [dims[0], dims[1] - (sweptLeft ? headerSz : 0)];
nonLeavesLyt = new LayoutNode('SWEEP_REM_' + node.name, nonLeaves);
if (nonLeaves.length > 1){
nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn:
@@ -824,7 +830,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
lyt.pos[1] += parentArea!.pos[1];
});
//
- let usedDims: [number,number] = [nonLeavesLyt.dims[0], nonLeavesLyt.dims[1] + (sweptLeft ? headerSz : 0)];
+ const usedDims: [number,number] = [nonLeavesLyt.dims[0], nonLeavesLyt.dims[1] + (sweptLeft ? headerSz : 0)];
node.assignLayoutData(pos, usedDims, {showHeader, empSpc: nonLeavesLyt.empSpc, sepSweptArea: parentArea});
return true;
}
@@ -846,20 +852,20 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
}
// Lays out nodes like sqrLayout(), but may extend past the height limit to fit nodes,
// and does not recurse on child nodes with children
-let sqrOverflowLayout: LayoutFn = function(node, pos, dims, showHeader, allowCollapse, opts){
+const sqrOverflowLayout: LayoutFn = function(node, pos, dims, showHeader, allowCollapse, opts){
if (node.children.length == 0){
return oneSqrLayout(node, pos, dims, false, false, opts);
}
// Consider area excluding header and top/left spacing
- let headerSz = showHeader ? opts.headerSz : 0;
- let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
- let newWidth = dims[0] - opts.tileSpacing;
+ const headerSz = showHeader ? opts.headerSz : 0;
+ const newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
+ const newWidth = dims[0] - opts.tileSpacing;
if (newWidth <= 0){
return false;
}
// Find number of rows and columns
- let numChildren = node.children.length;
- let maxNumCols = Math.floor(newWidth / (opts.minTileSz + opts.tileSpacing));
+ const numChildren = node.children.length;
+ const maxNumCols = Math.floor(newWidth / (opts.minTileSz + opts.tileSpacing));
if (maxNumCols == 0){
if (allowCollapse){
node.children = [];
@@ -868,21 +874,21 @@ let sqrOverflowLayout: LayoutFn = function(node, pos, dims, showHeader, allowCol
}
return false;
}
- let numCols = Math.min(numChildren, maxNumCols);
- let numRows = Math.ceil(numChildren / numCols);
- let tileSz = Math.min(opts.maxTileSz, Math.floor(newWidth / numCols) - opts.tileSpacing);
+ const numCols = Math.min(numChildren, maxNumCols);
+ const numRows = Math.ceil(numChildren / numCols);
+ const tileSz = Math.min(opts.maxTileSz, Math.floor(newWidth / numCols) - opts.tileSpacing);
// Layout children
for (let i = 0; i < numChildren; i++){
- let childX = newPos[0] + (i % numCols) * (tileSz + opts.tileSpacing);
- let childY = newPos[1] + Math.floor(i / numCols) * (tileSz + opts.tileSpacing);
+ const childX = newPos[0] + (i % numCols) * (tileSz + opts.tileSpacing);
+ const childY = newPos[1] + Math.floor(i / numCols) * (tileSz + opts.tileSpacing);
oneSqrLayout(node.children[i], [childX,childY], [tileSz,tileSz], false, false, opts);
}
//
- let usedDims: [number, number] = [
+ const usedDims: [number, number] = [
numCols * (tileSz + opts.tileSpacing) + opts.tileSpacing,
numRows * (tileSz + opts.tileSpacing) + opts.tileSpacing + headerSz
];
- let empSpc = 0; // Intentionally not used
+ const empSpc = 0; // Intentionally not used
node.assignLayoutData(pos, usedDims, {showHeader, empSpc});
return true;
}
diff --git a/src/lib.ts b/src/lib.ts
index d82965f..e262e05 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -3,20 +3,18 @@
*/
import {TolNode} from './tol';
-import {LayoutOptions} from './layout';
-import {getBreakpoint, Breakpoint, getScrollBarWidth, onTouchDevice} from './util';
// For server requests
const SERVER_DATA_URL = (new URL(window.location.href)).origin + '/data/'
const SERVER_IMG_PATH = '/tol_data/img/'
export async function queryServer(params: URLSearchParams){
// Construct URL
- let url = new URL(SERVER_DATA_URL);
+ const url = new URL(SERVER_DATA_URL);
url.search = params.toString();
// Query server
let responseObj;
try {
- let response = await fetch(url.toString());
+ const response = await fetch(url.toString());
responseObj = await response.json();
} catch (error){
console.log(`Error with querying ${url.toString()}: ${error}`);
@@ -62,124 +60,7 @@ export type InfoResponse = {
subNodesInfo: [] | [NodeInfo | null, NodeInfo | null],
};
-// Used by auto-mode and tutorial
+// Used by auto-mode and tutorial-pane
export type Action =
'expand' | 'collapse' | 'expandToView' | 'unhideAncestor' |
'tileInfo' | 'search' | 'autoMode' | 'settings' | 'help';
-
-// Project-wide configurable options (supersets the user-configurable settings)
-export type UiOptions = {
- // Shared coloring/sizing
- textColor: string, // CSS color
- textColorAlt: string,
- bgColor: string,
- bgColorLight: string,
- bgColorDark: string,
- bgColorLight2: string,
- bgColorDark2: string,
- bgColorAlt: string,
- bgColorAltDark: string,
- altColor: string,
- altColorDark: string,
- borderRadius: number, // CSS border-radius value, in px
- shadowNormal: string, // CSS box-shadow value
- shadowHovered: string,
- shadowFocused: string,
- // Component coloring
- childQtyColors: [number, string][],
- // Specifies, for an increasing sequence of minimum-child-quantity values, CSS colors to use
- //eg: [[1, 'green'], [10, 'orange'], [100, 'red']]
- nonleafBgColors: string[],
- // Specifies CSS colors to use at various tree depths
- // With N strings, tiles at depth M use the color at index M % N
- nonleafHeaderColor: string, // CSS color
- ancestryBarBgColor: string,
- // Component sizing
- ancestryBarBreadth: number, // px (fixed value needed for transitions)
- tutPaneSz: number, // px (fixed value needed for transitions)
- scrollGap: number, // Size of scroll bar, in px
- // Timing related
- clickHoldDuration: number, // Time after mousedown when a click-and-hold is recognised, in ms
- transitionDuration: number, // ms
- animationDelay: number, // Time between updates during transitions/resizes/etc, in ms
- autoActionDelay: number, // Time between auto-mode actions (incl transitions), in ms
- // Device-info-like
- touchDevice: boolean,
- breakpoint: Breakpoint,
- // Other
- tree: 'trimmed' | 'images' | 'picked',
- searchSuggLimit: number, // Max number of search suggestions
- searchJumpMode: boolean,
- tutorialSkip: boolean,
- disabledActions: Set<Action>,
- autoHide: boolean, // Upon a leaf-click fail, hide an ancestor and try again
- disableShortcuts: boolean,
-};
-// Option defaults
-export function getDefaultLytOpts(): LayoutOptions {
- let screenSz = getBreakpoint();
- return {
- tileSpacing: screenSz == 'sm' ? 6 : 9, //px
- headerSz: 22, // px
- minTileSz: screenSz == 'sm' ? 50 : 80, // px
- maxTileSz: 200, // px
- // Layout-algorithm related
- layoutType: 'sweep', // 'sqr' | 'rect' | 'sweep'
- rectMode: 'auto first-row', // 'horz' | 'vert' | 'linear' | 'auto' | 'auto first-row'
- rectSensitivity: 0.9, // Between 0 and 1
- sweepMode: 'left', // 'left' | 'top' | 'shorter' | 'auto'
- sweptNodesPrio: 'sqrt', // 'linear' | 'sqrt' | 'pow-2/3'
- sweepToParent: screenSz == 'sm' ? 'prefer' : 'fallback', // 'none' | 'prefer' | 'fallback'
- };
-}
-export function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions {
- let screenSz = getBreakpoint();
- // Reused option values
- // Note: For scrollbar colors on chrome, edit ./index.css
- let textColor = '#fafaf9', textColorAlt = '#1c1917';
- let bgColor = '#292524',
- bgColorLight = '#44403c', bgColorDark = '#1c1917',
- bgColorLight2 = '#57534e', bgColorDark2 = '#0e0c0b',
- bgColorAlt = '#f5f5f4', bgColorAltDark = '#d6d3d1';
- let altColor = '#a3e623', altColorDark = '#65a30d';
- let accentColor = '#f59e0b';
- let scrollGap = getScrollBarWidth();
- //
- return {
- // Shared coloring/sizing
- textColor, textColorAlt,
- bgColor, bgColorLight, bgColorDark, bgColorLight2, bgColorDark2, bgColorAlt, bgColorAltDark,
- altColor, altColorDark,
- borderRadius: 5, // px
- shadowNormal: '0 0 2px black',
- shadowHovered: '0 0 1px 2px ' + altColor,
- shadowFocused: '0 0 1px 2px ' + accentColor,
- // Component coloring
- childQtyColors: [[1, 'greenyellow'], [10, 'orange'], [100, 'red']],
- nonleafBgColors: [bgColorLight, bgColorLight2],
- nonleafHeaderColor: bgColorDark,
- ancestryBarBgColor: bgColorLight,
- // Component sizing
- ancestryBarBreadth: (screenSz == 'sm' ? 80 : 100) + lytOpts.tileSpacing*2, // px
- tutPaneSz: 180, // px
- scrollGap,
- // Timing related
- clickHoldDuration: 400, // ms
- transitionDuration: 300, // ms
- animationDelay: 100, // ms
- autoActionDelay: 500, // ms
- // Device-info-like
- touchDevice: onTouchDevice(),
- breakpoint: getBreakpoint(),
- // Other
- tree: 'images',
- searchSuggLimit: 10,
- searchJumpMode: false,
- tutorialSkip: false,
- disabledActions: new Set() as Set<Action>,
- autoHide: true,
- disableShortcuts: false,
- };
-}
-// Used in Settings.vue, and when saving to localStorage
-export type OptionType = 'LYT' | 'UI';
diff --git a/src/main.ts b/src/main.ts
index f289386..4a5d8a9 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,8 @@
import {createApp} from 'vue';
+import {createPinia} from 'pinia';
import App from './App.vue';
import './index.css';
-createApp(App).mount('#app');
+const app = createApp(App);
+app.use(createPinia());
+app.mount('#app');
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..d4f87d3
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,226 @@
+/*
+ * Defines a global store for UI settings, palette colors, etc
+ */
+
+
+import {defineStore} from 'pinia';
+import {Action} from './lib';
+import {LayoutOptions} from './layout';
+import {getBreakpoint, Breakpoint, getScrollBarWidth, onTouchDevice} from './util';
+
+export type StoreState = {
+ // Device info
+ touchDevice: boolean,
+ breakpoint: Breakpoint,
+ scrollGap: number, // Size of scroll bar, in px
+ // Tree display
+ tree: 'trimmed' | 'images' | 'picked',
+ lytOpts: LayoutOptions,
+ ancestryBarBreadth: number, // px (fixed value needed for transitions)
+ tutPaneSz: number, // px (fixed value needed for transitions)
+ // Search related
+ searchSuggLimit: number, // Max number of search suggestions
+ searchJumpMode: boolean,
+ // Tutorial related
+ tutorialSkip: boolean,
+ disabledActions: Set<Action>,
+ // Coloring
+ color: {
+ text: string, // CSS color
+ textAlt: string,
+ bg: string,
+ bgLight: string,
+ bgDark: string,
+ bgLight2: string,
+ bgDark2: string,
+ bgAlt: string,
+ bgAltDark: string,
+ alt: string,
+ altDark: string,
+ accent: string,
+ },
+ childQtyColors: [number, string][],
+ // Specifies, for an increasing sequence of minimum-child-quantity values, CSS colors to use
+ //eg: [[1, 'green'], [10, 'orange'], [100, 'red']]
+ nonleafBgColors: string[],
+ // Specifies CSS colors to use at various tree depths
+ // With N strings, tiles at depth M use the color at index M % N
+ nonleafHeaderColor: string, // CSS color
+ ancestryBarBgColor: string,
+ // More styling
+ borderRadius: number, // CSS border-radius value, in px
+ shadowNormal: string, // CSS box-shadow value
+ shadowHovered: string,
+ shadowFocused: string,
+ // Timing
+ clickHoldDuration: number, // Time after mousedown when a click-and-hold is recognised, in ms
+ transitionDuration: number, // ms
+ animationDelay: number, // Time between updates during transitions/resizes/etc, in ms
+ autoActionDelay: number, // Time between auto-mode actions (incl transitions), in ms
+ // Other
+ disableShortcuts: boolean,
+ autoHide: boolean, // If true, leaf-click failure results in hiding an ancestor and trying again
+};
+function getDefaultState(): StoreState {
+ const breakpoint = getBreakpoint();
+ const scrollGap = getScrollBarWidth();
+ const tileSpacing = breakpoint == 'sm' ? 6 : 9;
+ const color = { // Note: For scrollbar colors on chrome, edit ./index.css
+ text: '#fafaf9',
+ textAlt: '#1c1917',
+ bg: '#292524',
+ bgLight: '#44403c',
+ bgDark: '#1c1917',
+ bgLight2: '#57534e',
+ bgDark2: '#0e0c0b',
+ bgAlt: '#f5f5f4',
+ bgAltDark: '#d6d3d1',
+ alt: '#a3e623',
+ altDark: '#65a30d',
+ accent: '#f59e0b',
+ };
+ return {
+ // Device related
+ touchDevice: onTouchDevice(),
+ breakpoint: breakpoint,
+ scrollGap,
+ // Tree display
+ tree: 'images',
+ lytOpts: {
+ tileSpacing, //px
+ headerSz: 22, // px
+ minTileSz: breakpoint == 'sm' ? 50 : 80, // px
+ maxTileSz: 200, // px
+ // Layout-algorithm related
+ layoutType: 'sweep', // 'sqr' | 'rect' | 'sweep'
+ rectMode: 'auto first-row', // 'horz' | 'vert' | 'linear' | 'auto' | 'auto first-row'
+ rectSensitivity: 0.9, // Between 0 and 1
+ sweepMode: 'left', // 'left' | 'top' | 'shorter' | 'auto'
+ sweptNodesPrio: 'sqrt', // 'linear' | 'sqrt' | 'pow-2/3'
+ sweepToParent: breakpoint == 'sm' ? 'prefer' : 'fallback', // 'none' | 'prefer' | 'fallback'
+ },
+ ancestryBarBreadth: (breakpoint == 'sm' ? 80 : 100) + tileSpacing*2, // px
+ tutPaneSz: 180, // px
+ // Search related
+ searchSuggLimit: 10,
+ searchJumpMode: false,
+ // Tutorial related
+ tutorialSkip: false,
+ disabledActions: new Set() as Set<Action>,
+ // Coloring
+ color,
+ childQtyColors: [[1, 'greenyellow'], [10, 'orange'], [100, 'red']],
+ nonleafBgColors: [color.bgLight, color.bgLight2],
+ nonleafHeaderColor: color.bgDark,
+ ancestryBarBgColor: color.bgLight,
+ // More styling
+ borderRadius: 5, // px
+ shadowNormal: '0 0 2px black',
+ shadowHovered: '0 0 1px 2px ' + color.alt,
+ shadowFocused: '0 0 1px 2px ' + color.accent,
+ // Timing
+ clickHoldDuration: 400, // ms
+ transitionDuration: 300, // ms
+ animationDelay: 100, // ms
+ autoActionDelay: 1000, // ms
+ // Other
+ disableShortcuts: false,
+ autoHide: true,
+ };
+}
+// Gets 'composite keys' which have the form 'key1' or 'key1.key2' (usable to specify properties of store objects)
+function getCompositeKeys(state: StoreState){
+ const compKeys = [];
+ for (const key of Object.getOwnPropertyNames(state) as (keyof StoreState)[]){
+ if (typeof state[key] != 'object'){
+ compKeys.push(key);
+ } else {
+ for (const subkey of Object.getOwnPropertyNames(state[key])){
+ compKeys.push(`${key}.${subkey}`);
+ }
+ }
+ }
+ return compKeys;
+}
+const STORE_COMP_KEYS = getCompositeKeys(getDefaultState());
+// For getting/setting values in store
+function getStoreVal(state: StoreState, compKey: string): any {
+ if (compKey in state){
+ return state[compKey as keyof StoreState];
+ }
+ const [s1, s2] = compKey.split('.', 2);
+ if (s1 in state){
+ const key1 = s1 as keyof StoreState;
+ if (typeof state[key1] == 'object' && s2 in (state[key1] as any)){
+ return (state[key1] as any)[s2];
+ }
+ }
+ return null;
+}
+function setStoreVal(state: StoreState, compKey: string, val: any): void {
+ if (compKey in state){
+ (state[compKey as keyof StoreState] as any) = val;
+ return;
+ }
+ const [s1, s2] = compKey.split('.', 2);
+ if (s1 in state){
+ const key1 = s1 as keyof StoreState;
+ if (typeof state[key1] == 'object' && s2 in (state[key1] as any)){
+ (state[key1] as any)[s2] = val;
+ return;
+ }
+ }
+}
+// For loading settings into [initial] store state
+function loadFromLocalStorage(state: StoreState){
+ for (const key of STORE_COMP_KEYS){
+ const item = localStorage.getItem(key)
+ if (item != null){
+ setStoreVal(state, key, JSON.parse(item));
+ }
+ }
+}
+
+export const useStore = defineStore('store', {
+ state: () => {
+ const state = getDefaultState();
+ loadFromLocalStorage(state);
+ return state;
+ },
+ actions: {
+ reset(): void {
+ Object.assign(this, getDefaultState());
+ },
+ resetOne(key: string){
+ const val = getStoreVal(this, key);
+ if (val != null){
+ const val2 = getStoreVal(getDefaultState(), key);
+ if (val != val2){
+ setStoreVal(this, key, val2);
+ }
+ }
+ },
+ save(key: string){
+ if (STORE_COMP_KEYS.includes(key)){
+ localStorage.setItem(key, JSON.stringify(getStoreVal(this, key)));
+ }
+ },
+ load(): void {
+ loadFromLocalStorage(this);
+ },
+ clear(): void {
+ for (const key of STORE_COMP_KEYS){
+ localStorage.removeItem(key);
+ }
+ },
+ softReset(): void { // Like reset(), but keeps saved values
+ const defaultState = getDefaultState();
+ for (const key of STORE_COMP_KEYS){
+ const defaultVal = getStoreVal(defaultState, key);
+ if (getStoreVal(this, key) != defaultState && localStorage.getItem(key) == null){
+ setStoreVal(this, key, defaultVal)
+ }
+ }
+ },
+ },
+});
diff --git a/src/util.ts b/src/util.ts
index 142e8eb..a686b70 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -5,7 +5,7 @@
// For detecting screen size
export type Breakpoint = 'sm' | 'md' | 'lg';
export function getBreakpoint(): Breakpoint {
- let w = window.innerWidth;
+ const w = window.innerWidth;
if (w < 768){
return 'sm';
} else if (w < 1024){
@@ -17,15 +17,15 @@ export function getBreakpoint(): Breakpoint {
// For getting scroll-bar width // From stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript
export function getScrollBarWidth(){
// Create hidden outer div
- let outer = document.createElement('div');
+ const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll';
document.body.appendChild(outer);
// Create inner div
- let inner = document.createElement('div');
+ const inner = document.createElement('div');
outer.appendChild(inner);
// Get width difference
- let scrollBarWidth = outer.offsetWidth - inner.offsetWidth;
+ const scrollBarWidth = outer.offsetWidth - inner.offsetWidth;
// Remove temporary divs
outer.parentNode!.removeChild(outer);
//
@@ -46,8 +46,8 @@ export function arraySum(array: number[]): number {
}
// Returns an array of increasing evenly-spaced numbers from 'start' to 'end', with size 'size'
export function linspace(start: number, end: number, size: number): number[] {
- let step = (end - start) / (size - 1);
- let ar = [];
+ const step = (end - start) / (size - 1);
+ const ar = [];
for (let i = 0; i < size; i++){
ar.push(start + step * i);
}
@@ -56,8 +56,8 @@ export function linspace(start: number, end: number, size: number): number[] {
// Returns array copy with vals clipped to within [min,max], redistributing to compensate
// Returns null on failure
export function limitVals(arr: number[], min: number, max: number): number[] | null {
- let vals = [...arr];
- let clipped = new Array(vals.length).fill(false);
+ const vals = [...arr];
+ const clipped = new Array(vals.length).fill(false);
let owedChg = 0; // Stores total change made after clipping values
while (true){
// Clip values
@@ -79,13 +79,13 @@ export function limitVals(arr: number[], min: number, max: number): number[] | n
return vals;
}
// Compensate for changes made
- let indicesToUpdate = (owedChg > 0) ?
+ const indicesToUpdate = (owedChg > 0) ?
range(vals.length).filter(idx => vals[idx] < max) :
range(vals.length).filter(idx => vals[idx] > min);
if (indicesToUpdate.length == 0){
return null;
}
- for (let i of indicesToUpdate){
+ for (const i of indicesToUpdate){
vals[i] += owedChg / indicesToUpdate.length;
}
owedChg = 0;
@@ -117,13 +117,13 @@ export function updateAscSeq(seq: number[], maxLen: number): boolean {
// Given a non-empty array of non-negative weights, returns an array index chosen with weighted pseudorandomness
// Returns null if array contains all zeros
export function randWeightedChoice(weights: number[]): number | null {
- let thresholds = Array(weights.length);
+ const thresholds = Array(weights.length);
let sum = 0;
for (let i = 0; i < weights.length; i++){
sum += weights[i];
thresholds[i] = sum;
}
- let rand = Math.random();
+ const rand = Math.random();
for (let i = 0; i < weights.length; i++){
if (rand <= thresholds[i] / sum){
return i;