diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-09-14 19:17:41 +1000 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-09-14 20:29:01 +1000 |
| commit | 8b5538e0a55a83b1ff190cd5ad689827777e73a7 (patch) | |
| tree | 33ccb2eadbe9a53dc2a870d57ba5efe758592390 | |
| parent | 556d6c953e74996e0ae6a8328e352ab43735f993 (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.js | 3 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/App.vue | 244 | ||||
| -rw-r--r-- | src/components/AncestryBar.vue | 34 | ||||
| -rw-r--r-- | src/components/HelpModal.vue | 29 | ||||
| -rw-r--r-- | src/components/LoadingModal.vue | 17 | ||||
| -rw-r--r-- | src/components/SButton.vue | 2 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 40 | ||||
| -rw-r--r-- | src/components/SettingsModal.vue | 192 | ||||
| -rw-r--r-- | src/components/TileInfoModal.vue | 43 | ||||
| -rw-r--r-- | src/components/TolTile.vue | 89 | ||||
| -rw-r--r-- | src/components/TutorialPane.vue | 21 | ||||
| -rw-r--r-- | src/layout.ts | 204 | ||||
| -rw-r--r-- | src/lib.ts | 125 | ||||
| -rw-r--r-- | src/main.ts | 5 | ||||
| -rw-r--r-- | src/store.ts | 226 | ||||
| -rw-r--r-- | src/util.ts | 24 |
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">• • •</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; } @@ -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; |
