From 8b5538e0a55a83b1ff190cd5ad689827777e73a7 Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Wed, 14 Sep 2022 19:17:41 +1000 Subject: Use Pinia to store user settings, palette colors, etc Move uiOpts and lytOpts to store.ts Add 'const's to *.ts --- .eslintrc.js | 3 +- package.json | 1 + src/App.vue | 244 +++++++++++++++------------------------ src/components/AncestryBar.vue | 34 +++--- src/components/HelpModal.vue | 29 ++--- src/components/LoadingModal.vue | 17 +-- src/components/SButton.vue | 2 +- src/components/SearchModal.vue | 40 ++++--- src/components/SettingsModal.vue | 192 +++++++++++++----------------- src/components/TileInfoModal.vue | 43 ++++--- src/components/TolTile.vue | 89 +++++++------- src/components/TutorialPane.vue | 21 ++-- src/layout.ts | 204 ++++++++++++++++---------------- src/lib.ts | 125 +------------------- src/main.ts | 5 +- src/store.ts | 226 ++++++++++++++++++++++++++++++++++++ src/util.ts | 24 ++-- 17 files changed, 670 insertions(+), 629 deletions(-) create mode 100644 src/store.ts 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 @@ - Start Tutorial @@ -427,23 +420,25 @@ 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 @@ 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 @@
@@ -25,7 +25,7 @@
• • •
@@ -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, required: true}, // Used to check if a searched-for node exists activeRoot: {type: Object as PropType, required: true}, // Sent to server to reduce response size tolMap: {type: Object as PropType, required: true}, // Upon a search response, gets new nodes added - lytOpts: {type: Object as PropType, required: true}, - uiOpts: {type: Object as PropType, 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 => { - 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 => { - 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, })); 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 @@

Timing

-
@@ -31,49 +31,49 @@
Sweep leaves left
Sweep into parent
-
@@ -81,29 +81,29 @@
Tree to use
    -
  • -
  • -
  • +
  • +
  • +
- +
- +
-
- +
+
- + Reset @@ -114,11 +114,10 @@ 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 @@
@@ -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, required: true}, tolMap: {type: Object as PropType, required: true}, - // Options - lytOpts: {type: Object as PropType, required: true}, - uiOpts: {type: Object as PropType, 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 => { 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 => { '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 => { 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 => { 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 => { }); const leafHeaderStyles = computed((): Record => { 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 { 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 => { const nonleafHeaderStyles = computed((): Record => { let styles: Record = { 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 => { 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 => { - 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 => { ...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 => { - 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, 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, })); diff --git a/src/layout.ts b/src/layout.ts index 140af77..2739037 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -5,7 +5,7 @@ * LayoutNode tree, on which tryLayout() can run a layout algorithm. */ -import {TolNode, TolMap} from './tol'; +import {TolMap} from './tol'; import {range, arraySum, linspace, limitVals, updateAscSeq} from './util'; // Represents a node/tree that holds layout data for a TolNode node/tree @@ -51,20 +51,22 @@ export class LayoutNode { let newNode: LayoutNode; if (chg != null && this == chg.node){ switch (chg.type){ - case 'expand': - let children = chg.tolMap.get(this.name)!.children.map((n: string) => new LayoutNode(n, [])); + case 'expand': { + const children = chg.tolMap.get(this.name)!.children.map((n: string) => new LayoutNode(n, [])); newNode = new LayoutNode(this.name, children); newNode.children.forEach(n => { n.parent = newNode; n.depth = this.depth + 1; }); break; - case 'collapse': + } + case 'collapse': { newNode = new LayoutNode(this.name, []); break; + } } } else { - let children = this.children.map(n => n.cloneNodeTree(chg)); + const children = this.children.map(n => n.cloneNodeTree(chg)); newNode = new LayoutNode(this.name, children); children.forEach(n => {n.parent = newNode}); } @@ -109,9 +111,9 @@ export class LayoutNode { // Given a sequence of child/grandchild/etc names, adds this/the_child's/the_grandchild's/etc children addDescendantChain(nameChain: string[], tolMap: TolMap, map?: LayoutMap): void { let layoutNode = this as LayoutNode; - for (let childName of nameChain){ + for (const childName of nameChain){ // Add children - let tolNode = tolMap.get(layoutNode.name)!; + const tolNode = tolMap.get(layoutNode.name)!; layoutNode.children = tolNode.children.map((name: string) => new LayoutNode(name, [])); layoutNode.children.forEach(node => { node.parent = layoutNode; @@ -122,7 +124,7 @@ export class LayoutNode { }); LayoutNode.updateTips(layoutNode, layoutNode.children.length - 1); // Get matching child node - let childNode = layoutNode.children.find(n => n.name == childName); + const childNode = layoutNode.children.find(n => n.name == childName); if (childNode == null){ throw new Error('Child name not found'); } @@ -204,7 +206,7 @@ export function initLayoutMap(layoutTree: LayoutNode): LayoutMap { map.set(node.name, node); node.children.forEach(n => helper(n, map)); } - let map = new Map(); + const map = new Map(); helper(layoutTree, map); return map; } @@ -221,19 +223,19 @@ function removeFromLayoutMap(node: LayoutNode, map: LayoutMap): void { // Creates a LayoutNode representing a TolNode tree, up to a given depth (0 means just the root, -1 means no limit) export function initLayoutTree(tolMap: TolMap, rootName: string, depth: number): LayoutNode { - function initHelper(tolMap: TolMap, nodeName: string, depthLeft: number, atDepth: number = 0): LayoutNode { + function initHelper(tolMap: TolMap, nodeName: string, depthLeft: number, atDepth = 0): LayoutNode { if (depthLeft == 0){ - let node = new LayoutNode(nodeName, []); + const node = new LayoutNode(nodeName, []); node.depth = atDepth; return node; } else { - let childNames = tolMap.get(nodeName)!.children; + const childNames = tolMap.get(nodeName)!.children; if (childNames.length == 0 || !tolMap.has(childNames[0])){ return new LayoutNode(nodeName, []); } else { - let children = childNames.map((name: string) => + const children = childNames.map((name: string) => initHelper(tolMap, name, depthLeft != -1 ? depthLeft-1 : -1, atDepth+1)); - let node = new LayoutNode(nodeName, children); + const node = new LayoutNode(nodeName, children); children.forEach(n => n.parent = node); return node; } @@ -251,7 +253,7 @@ export function tryLayout( {allowCollapse = false, chg = null as LayoutTreeChg | null, layoutMap = null as LayoutMap | null} = {} ): boolean { // Create a new LayoutNode tree, in case of layout failure - let tempTree = layoutTree.cloneNodeTree(chg); + const tempTree = layoutTree.cloneNodeTree(chg); let success: boolean; switch (options.layoutType){ case 'sqr': success = sqrLayout(tempTree, [0,0], dims, true, allowCollapse, options); break; @@ -283,8 +285,8 @@ type LayoutFn = ( ownOpts?: any, ) => boolean; // Lays out node as one square, ignoring child nodes // Used for base cases -let oneSqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){ - let tileSz = Math.min(dims[0], dims[1], opts.maxTileSz); +const oneSqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){ + const tileSz = Math.min(dims[0], dims[1], opts.maxTileSz); if (tileSz < opts.minTileSz){ return false; } @@ -292,28 +294,28 @@ let oneSqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollaps return true; } // Lays out nodes as squares within a grid, with intervening+surrounding spacing -let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){ +const sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts){ if (node.children.length == 0){ return oneSqrLayout(node, pos, dims, false, false, opts); } // Consider area excluding header and top/left spacing - let headerSz = showHeader ? opts.headerSz : 0; - let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz]; - let newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz]; + const headerSz = showHeader ? opts.headerSz : 0; + const newPos = [opts.tileSpacing, opts.tileSpacing + headerSz]; + const newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz]; if (newDims[0] * newDims[1] <= 0){ return false; } // Find number of rows/columns with least empty space - let numChildren = node.children.length; - let areaAR = newDims[0] / newDims[1]; // Aspect ratio + const numChildren = node.children.length; + const areaAR = newDims[0] / newDims[1]; // Aspect ratio let lowestEmpSpc = Number.POSITIVE_INFINITY, usedNumCols = 0, usedNumRows = 0, usedTileSz = 0; const MAX_TRIES = 50; // If there are many possibilities, skip some - let ptlNumCols = numChildren == 1 ? [1] : + const ptlNumCols = numChildren == 1 ? [1] : linspace(1, numChildren, Math.min(numChildren, MAX_TRIES)).map(n => Math.floor(n)); - for (let numCols of ptlNumCols){ - let numRows = Math.ceil(numChildren / numCols); - let gridAR = numCols / numRows; - let usedFrac = // Fraction of area occupied by maximally-fitting grid + for (const numCols of ptlNumCols){ + const numRows = Math.ceil(numChildren / numCols); + const gridAR = numCols / numRows; + const usedFrac = // Fraction of area occupied by maximally-fitting grid areaAR > gridAR ? gridAR / areaAR : areaAR / gridAR; // Get tile edge length let tileSz = (areaAR > gridAR ? newDims[1] / numRows : newDims[0] / numCols) - opts.tileSpacing; @@ -323,7 +325,7 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, tileSz = opts.maxTileSz; } // Get empty space - let empSpc = (1 - usedFrac) * (newDims[0] * newDims[1]) + // Area outside grid plus ... + const empSpc = (1 - usedFrac) * (newDims[0] * newDims[1]) + // Area outside grid plus ... (numCols * numRows - numChildren) * (tileSz - opts.tileSpacing)**2; // empty cells within grid // Compare with best-so-far if (empSpc < lowestEmpSpc){ @@ -344,9 +346,9 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } // Layout children for (let i = 0; i < numChildren; i++){ - let child = node.children[i]; - let childX = newPos[0] + (i % usedNumCols) * (usedTileSz + opts.tileSpacing); - let childY = newPos[1] + Math.floor(i / usedNumCols) * (usedTileSz + opts.tileSpacing); + const child = node.children[i]; + const childX = newPos[0] + (i % usedNumCols) * (usedTileSz + opts.tileSpacing); + const childY = newPos[1] + Math.floor(i / usedNumCols) * (usedTileSz + opts.tileSpacing); let success: boolean; if (child.children.length == 0){ success = oneSqrLayout(child, [childX,childY], [usedTileSz,usedTileSz], false, false, opts); @@ -363,11 +365,11 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } } // Create layout - let usedDims: [number, number] = [ + const usedDims: [number, number] = [ usedNumCols * (usedTileSz + opts.tileSpacing) + opts.tileSpacing, usedNumRows * (usedTileSz + opts.tileSpacing) + opts.tileSpacing + headerSz, ]; - let empSpc = // Empty space within usedDims area + const empSpc = // Empty space within usedDims area (usedNumCols * usedNumRows - numChildren) * (usedTileSz - opts.tileSpacing)**2 + arraySum(node.children.map(child => child.empSpc)); node.assignLayoutData(pos, usedDims, {showHeader, empSpc}); @@ -375,7 +377,7 @@ let sqrLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } // Lays out nodes as rows of rectangles, deferring to sqrLayout() or oneSqrLayout() for simpler cases //'subLayoutFn' allows other LayoutFns to use this layout, but transfer control back to themselves on recursion -let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts, +const rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts, ownOpts?: {subLayoutFn?: LayoutFn}){ // Check for simpler cases if (node.children.length == 0){ @@ -384,9 +386,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, return sqrLayout(node, pos, dims, showHeader, allowCollapse, opts); } // Consider area excluding header and top/left spacing - let headerSz = showHeader ? opts.headerSz : 0; - let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz]; - let newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz]; + const headerSz = showHeader ? opts.headerSz : 0; + const newPos = [opts.tileSpacing, opts.tileSpacing + headerSz]; + const newDims = [dims[0] - opts.tileSpacing, dims[1] - opts.tileSpacing - headerSz]; if (newDims[0] * newDims[1] < node.tips * (opts.minTileSz + opts.tileSpacing)**2){ if (allowCollapse){ node.children = []; @@ -397,7 +399,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } // Try finding arrangement with low empty space // Done by searching possible row-groupings, allocating within rows using 'tips' vals, and trimming empty space - let numChildren = node.children.length; + const numChildren = node.children.length; let rowBrks: number[] = []; // Will hold indices for nodes at which each row starts let lowestEmpSpc = Number.POSITIVE_INFINITY; let usedTree: LayoutNode | null = null; // Best-so-far layout @@ -439,7 +441,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, if (rowBrks.length == 0){ rowBrks = [0]; } else { - let updated = updateAscSeq(rowBrks, numChildren); + const updated = updateAscSeq(rowBrks, numChildren); if (!updated){ break RowBrksLoop; } @@ -450,18 +452,18 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, rowBrks = [0]; } else { // Get next possible first row - let idxFirstRowLastEl = (rowBrks.length == 1 ? numChildren : rowBrks[1]) - 1; + const idxFirstRowLastEl = (rowBrks.length == 1 ? numChildren : rowBrks[1]) - 1; if (idxFirstRowLastEl == 0){ break RowBrksLoop; } rowBrks = [0]; rowBrks.push(idxFirstRowLastEl); // Allocate remaining rows - let firstRowTips = arraySum(range(rowBrks[1]).map(idx => node.children[idx].tips)); + const firstRowTips = arraySum(range(rowBrks[1]).map(idx => node.children[idx].tips)); let tipsTotal = node.children[idxFirstRowLastEl].tips; let nextRowIdx = idxFirstRowLastEl + 1; while (nextRowIdx < numChildren){ // Over potential next row breaks - let nextTipsTotal = tipsTotal + node.children[nextRowIdx].tips; + const nextTipsTotal = tipsTotal + node.children[nextRowIdx].tips; if (nextTipsTotal <= firstRowTips){ // If acceptable within current row tipsTotal = nextTipsTotal; } else { @@ -474,26 +476,26 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, break; } // Create array-of-arrays representing each rows' cells' 'tips' values - let rowsOfCnts: number[][] = new Array(rowBrks.length); + const rowsOfCnts: number[][] = new Array(rowBrks.length); for (let rowIdx = 0; rowIdx < rowBrks.length; rowIdx++){ - let numNodes = (rowIdx < rowBrks.length - 1) ? + const numNodes = (rowIdx < rowBrks.length - 1) ? rowBrks[rowIdx + 1] - rowBrks[rowIdx] : numChildren - rowBrks[rowIdx]; - let rowNodeIdxs = range(numNodes).map(i => i + rowBrks![rowIdx]); + const rowNodeIdxs = range(numNodes).map(i => i + rowBrks![rowIdx]); rowsOfCnts[rowIdx] = rowNodeIdxs.map(idx => node.children[idx].tips); } // Get initial cell dims - let cellWs: number[][] = new Array(rowsOfCnts.length); + const cellWs: number[][] = new Array(rowsOfCnts.length); for (let rowIdx = 0; rowIdx < rowsOfCnts.length; rowIdx++){ - let rowCount = arraySum(rowsOfCnts[rowIdx]); + const rowCount = arraySum(rowsOfCnts[rowIdx]); cellWs[rowIdx] = range(rowsOfCnts[rowIdx].length).map( colIdx => rowsOfCnts[rowIdx][colIdx] / rowCount * newDims[0]); } - let totalTips = arraySum(node.children.map(n => n.tips)); + const totalTips = arraySum(node.children.map(n => n.tips)); let cellHs = rowsOfCnts.map(rowOfCnts => arraySum(rowOfCnts) / totalTips * newDims[1]); // Check min-tile-size, attempting to reallocate space if needed for (let rowIdx = 0; rowIdx < rowsOfCnts.length; rowIdx++){ - let newWs = limitVals(cellWs[rowIdx], minCellDims[0], Number.POSITIVE_INFINITY); + const newWs = limitVals(cellWs[rowIdx], minCellDims[0], Number.POSITIVE_INFINITY); if (newWs == null){ continue RowBrksLoop; } @@ -504,26 +506,26 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, continue RowBrksLoop; } // Get cell xy-coordinates - let cellXs: number[][] = new Array(rowsOfCnts.length); + const cellXs: number[][] = new Array(rowsOfCnts.length); for (let rowIdx = 0; rowIdx < rowBrks.length; rowIdx++){ cellXs[rowIdx] = [0]; for (let colIdx = 1; colIdx < rowsOfCnts[rowIdx].length; colIdx++){ cellXs[rowIdx].push(cellXs[rowIdx][colIdx - 1] + cellWs[rowIdx][colIdx - 1]); } } - let cellYs: number[] = new Array(rowsOfCnts.length).fill(0); + const cellYs: number[] = new Array(rowsOfCnts.length).fill(0); for (let rowIdx = 1; rowIdx < rowBrks.length; rowIdx++){ cellYs[rowIdx] = cellYs[rowIdx - 1] + cellHs[rowIdx - 1]; } // Determine child layouts, resizing cells to reduce empty space - let tempTree: LayoutNode = node.cloneNodeTree(); + const tempTree: LayoutNode = node.cloneNodeTree(); let empRight = Number.POSITIVE_INFINITY, empBottom = 0; for (let rowIdx = 0; rowIdx < rowBrks.length; rowIdx++){ for (let colIdx = 0; colIdx < rowsOfCnts[rowIdx].length; colIdx++){ - let nodeIdx = rowBrks[rowIdx] + colIdx; - let child: LayoutNode = tempTree.children[nodeIdx]; - let childPos: [number, number] = [newPos[0] + cellXs[rowIdx][colIdx], newPos[1] + cellYs[rowIdx]]; - let childDims: [number, number] = [ + const nodeIdx = rowBrks[rowIdx] + colIdx; + const child: LayoutNode = tempTree.children[nodeIdx]; + const childPos: [number, number] = [newPos[0] + cellXs[rowIdx][colIdx], newPos[1] + cellYs[rowIdx]]; + const childDims: [number, number] = [ cellWs[rowIdx][colIdx] - opts.tileSpacing, cellHs[rowIdx] - opts.tileSpacing ]; @@ -533,14 +535,14 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } else if (child.children.every(n => n.children.length == 0)){ success = sqrLayout(child, childPos, childDims, true, allowCollapse, opts); } else { - let layoutFn = (ownOpts && ownOpts.subLayoutFn) || rectLayout; + const layoutFn = (ownOpts && ownOpts.subLayoutFn) || rectLayout; success = layoutFn(child, childPos, childDims, true, allowCollapse, opts); } if (!success){ continue RowBrksLoop; } // Remove horizontal empty space by trimming cell and moving/expanding any next cell - let horzEmp = childDims[0] - child.dims[0]; + const horzEmp = childDims[0] - child.dims[0]; cellWs[rowIdx][colIdx] -= horzEmp; if (colIdx < rowsOfCnts[rowIdx].length - 1){ cellXs[rowIdx][colIdx + 1] -= horzEmp; @@ -550,9 +552,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } } // Remove vertical empty space by trimming row and moving/expanding any next row - let childUsedHs = range(rowsOfCnts[rowIdx].length).map( + const childUsedHs = range(rowsOfCnts[rowIdx].length).map( colIdx => tempTree.children[rowBrks[rowIdx] + colIdx].dims[1]); - let vertEmp = cellHs[rowIdx] - opts.tileSpacing - Math.max(...childUsedHs); + const vertEmp = cellHs[rowIdx] - opts.tileSpacing - Math.max(...childUsedHs); cellHs[rowIdx] -= vertEmp; if (rowIdx < rowBrks.length - 1){ cellYs[rowIdx + 1] -= vertEmp; @@ -562,9 +564,9 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } } // Get empty space - let usedSpc = arraySum(tempTree.children.map( + const usedSpc = arraySum(tempTree.children.map( child => (child.dims[0] + opts.tileSpacing) * (child.dims[1] + opts.tileSpacing) - child.empSpc)); - let empSpc = newDims[0] * newDims[1] - usedSpc; + const empSpc = newDims[0] * newDims[1] - usedSpc; // Check with best-so-far if (empSpc < lowestEmpSpc * opts.rectSensitivity){ lowestEmpSpc = empSpc; @@ -584,7 +586,7 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, } // Create layout usedTree.copyTreeForRender(node); - let usedDims: [number, number] = [dims[0] - usedEmpRight, dims[1] - usedEmpBottom]; + const usedDims: [number, number] = [dims[0] - usedEmpRight, dims[1] - usedEmpBottom]; node.assignLayoutData(pos, usedDims, {showHeader, empSpc: lowestEmpSpc}); return true; } @@ -592,10 +594,10 @@ let rectLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, // With layout option 'sweepToParent', leaves from child nodes may occupy a parent's leaf-section // 'sepArea' represents a usable leaf-section area from a direct parent, // and is changed to represent the area used, with those changes visible to the parent for reducing empty space -let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts, +const sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse, opts, ownOpts?: {sepArea?: SepSweptArea}){ // Separate leaf and non-leaf nodes - let leaves: LayoutNode[] = [], nonLeaves: LayoutNode[] = []; + const leaves: LayoutNode[] = [], nonLeaves: LayoutNode[] = []; node.children.forEach(child => (child.children.length == 0 ? leaves : nonLeaves).push(child)); // Check for simpler cases if (node.children.length == 0){ @@ -606,17 +608,17 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse return rectLayout(node, pos, dims, showHeader, allowCollapse, opts, {subLayoutFn: sweepLayout}); } // Some variables - let headerSz = showHeader ? opts.headerSz : 0; + const headerSz = showHeader ? opts.headerSz : 0; let leavesLyt: LayoutNode | null = null, nonLeavesLyt: LayoutNode | null = null, sweptLeft = false; let sepArea: SepSweptArea | null = null; // Represents leaf-section area provided for a child - let haveParentArea = ownOpts != null && ownOpts.sepArea != null; + const haveParentArea = ownOpts != null && ownOpts.sepArea != null; let trySweepToParent = haveParentArea && opts.sweepToParent == 'prefer'; // Using a loop for conditionally retrying layout while (true){ if (!trySweepToParent){ // Try laying-out normally // Choose proportion of area to use for leaves let ratio: number; // area-for-leaves / area-for-non-leaves - let nonLeavesTiles = arraySum(nonLeaves.map(n => n.tips)); + const nonLeavesTiles = arraySum(nonLeaves.map(n => n.tips)); switch (opts.sweptNodesPrio){ case 'linear': ratio = leaves.length / (leaves.length + nonLeavesTiles); @@ -630,25 +632,27 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse break; } // Attempt leaves layout - let newPos = [0, headerSz]; - let newDims: [number,number] = [dims[0], dims[1] - headerSz]; + const newPos = [0, headerSz]; + const newDims: [number,number] = [dims[0], dims[1] - headerSz]; leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves); // Note: Intentionally neglecting to update child nodes' 'parent' or 'depth' fields here - let minSz = opts.minTileSz + opts.tileSpacing*4; - let sweptW = Math.min(Math.max(minSz, newDims[0] * ratio), newDims[0] - minSz); - let sweptH = Math.min(Math.max(minSz, newDims[1] * ratio), newDims[0] - minSz); + const minSz = opts.minTileSz + opts.tileSpacing*4; + const sweptW = Math.min(Math.max(minSz, newDims[0] * ratio), newDims[0] - minSz); + const sweptH = Math.min(Math.max(minSz, newDims[1] * ratio), newDims[0] - minSz); let leavesSuccess: boolean; switch (opts.sweepMode){ - case 'left': + case 'left': { leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); sweptLeft = true; break; - case 'top': + } + case 'top': { leavesSuccess = sqrLayout(leavesLyt, [0,0], [newDims[0], sweptH], false, false, opts); sweptLeft = false; break; - case 'shorter': - let documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight; + } + case 'shorter': { + const documentAR = document.documentElement.clientWidth / document.documentElement.clientHeight; if (documentAR >= 1){ leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); sweptLeft = true; @@ -657,18 +661,20 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse sweptLeft = false; } break; - case 'auto': + } + case 'auto': { // Attempt left-sweep, then top-sweep on a copy, and copy over if better leavesSuccess = sqrLayout(leavesLyt, [0,0], [sweptW, newDims[1]], false, false, opts); sweptLeft = true; - let tempTree = leavesLyt.cloneNodeTree(); - let sweptTopSuccess = sqrLayout(tempTree, [0,0], [newDims[0], sweptH], false, false, opts);; + const tempTree = leavesLyt.cloneNodeTree(); + const sweptTopSuccess = sqrLayout(tempTree, [0,0], [newDims[0], sweptH], false, false, opts); if (sweptTopSuccess && (!leavesSuccess || tempTree.empSpc < leavesLyt.empSpc)){ tempTree.copyTreeForRender(leavesLyt); sweptLeft = false; leavesSuccess = true; } break; + } } if (leavesSuccess){ leavesLyt.children.forEach(lyt => {lyt.pos[1] += headerSz}); @@ -727,7 +733,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse leavesLyt.dims[1] + nonLeavesLyt.dims[1] - opts.tileSpacing + headerSz ]; } - let empSpc = leavesLyt.empSpc + nonLeavesLyt.empSpc; + const empSpc = leavesLyt.empSpc + nonLeavesLyt.empSpc; node.assignLayoutData(pos, usedDims, {showHeader, empSpc, sepSweptArea: null}); return true; } @@ -738,15 +744,15 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse } break; } else { // Try using parent-provided area - let parentArea = ownOpts!.sepArea!; + const parentArea = ownOpts!.sepArea!; // Attempt leaves layout sweptLeft = parentArea.sweptLeft; leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves); - let leavesSuccess = sqrLayout(leavesLyt, [0,0], parentArea.dims, !sweptLeft, false, opts); + const leavesSuccess = sqrLayout(leavesLyt, [0,0], parentArea.dims, !sweptLeft, false, opts); let nonLeavesSuccess = true; if (leavesSuccess){ // Attempt non-leaves layout - let newDims: [number,number] = [dims[0], dims[1] - (sweptLeft ? headerSz : 0)]; + const newDims: [number,number] = [dims[0], dims[1] - (sweptLeft ? headerSz : 0)]; nonLeavesLyt = new LayoutNode('SWEEP_REM_' + node.name, nonLeaves); if (nonLeaves.length > 1){ nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn: @@ -824,7 +830,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse lyt.pos[1] += parentArea!.pos[1]; }); // - let usedDims: [number,number] = [nonLeavesLyt.dims[0], nonLeavesLyt.dims[1] + (sweptLeft ? headerSz : 0)]; + const usedDims: [number,number] = [nonLeavesLyt.dims[0], nonLeavesLyt.dims[1] + (sweptLeft ? headerSz : 0)]; node.assignLayoutData(pos, usedDims, {showHeader, empSpc: nonLeavesLyt.empSpc, sepSweptArea: parentArea}); return true; } @@ -846,20 +852,20 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse } // Lays out nodes like sqrLayout(), but may extend past the height limit to fit nodes, // and does not recurse on child nodes with children -let sqrOverflowLayout: LayoutFn = function(node, pos, dims, showHeader, allowCollapse, opts){ +const sqrOverflowLayout: LayoutFn = function(node, pos, dims, showHeader, allowCollapse, opts){ if (node.children.length == 0){ return oneSqrLayout(node, pos, dims, false, false, opts); } // Consider area excluding header and top/left spacing - let headerSz = showHeader ? opts.headerSz : 0; - let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz]; - let newWidth = dims[0] - opts.tileSpacing; + const headerSz = showHeader ? opts.headerSz : 0; + const newPos = [opts.tileSpacing, opts.tileSpacing + headerSz]; + const newWidth = dims[0] - opts.tileSpacing; if (newWidth <= 0){ return false; } // Find number of rows and columns - let numChildren = node.children.length; - let maxNumCols = Math.floor(newWidth / (opts.minTileSz + opts.tileSpacing)); + const numChildren = node.children.length; + const maxNumCols = Math.floor(newWidth / (opts.minTileSz + opts.tileSpacing)); if (maxNumCols == 0){ if (allowCollapse){ node.children = []; @@ -868,21 +874,21 @@ let sqrOverflowLayout: LayoutFn = function(node, pos, dims, showHeader, allowCol } return false; } - let numCols = Math.min(numChildren, maxNumCols); - let numRows = Math.ceil(numChildren / numCols); - let tileSz = Math.min(opts.maxTileSz, Math.floor(newWidth / numCols) - opts.tileSpacing); + const numCols = Math.min(numChildren, maxNumCols); + const numRows = Math.ceil(numChildren / numCols); + const tileSz = Math.min(opts.maxTileSz, Math.floor(newWidth / numCols) - opts.tileSpacing); // Layout children for (let i = 0; i < numChildren; i++){ - let childX = newPos[0] + (i % numCols) * (tileSz + opts.tileSpacing); - let childY = newPos[1] + Math.floor(i / numCols) * (tileSz + opts.tileSpacing); + const childX = newPos[0] + (i % numCols) * (tileSz + opts.tileSpacing); + const childY = newPos[1] + Math.floor(i / numCols) * (tileSz + opts.tileSpacing); oneSqrLayout(node.children[i], [childX,childY], [tileSz,tileSz], false, false, opts); } // - let usedDims: [number, number] = [ + const usedDims: [number, number] = [ numCols * (tileSz + opts.tileSpacing) + opts.tileSpacing, numRows * (tileSz + opts.tileSpacing) + opts.tileSpacing + headerSz ]; - let empSpc = 0; // Intentionally not used + const empSpc = 0; // Intentionally not used node.assignLayoutData(pos, usedDims, {showHeader, empSpc}); return true; } diff --git a/src/lib.ts b/src/lib.ts index d82965f..e262e05 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -3,20 +3,18 @@ */ import {TolNode} from './tol'; -import {LayoutOptions} from './layout'; -import {getBreakpoint, Breakpoint, getScrollBarWidth, onTouchDevice} from './util'; // For server requests const SERVER_DATA_URL = (new URL(window.location.href)).origin + '/data/' const SERVER_IMG_PATH = '/tol_data/img/' export async function queryServer(params: URLSearchParams){ // Construct URL - let url = new URL(SERVER_DATA_URL); + const url = new URL(SERVER_DATA_URL); url.search = params.toString(); // Query server let responseObj; try { - let response = await fetch(url.toString()); + const response = await fetch(url.toString()); responseObj = await response.json(); } catch (error){ console.log(`Error with querying ${url.toString()}: ${error}`); @@ -62,124 +60,7 @@ export type InfoResponse = { subNodesInfo: [] | [NodeInfo | null, NodeInfo | null], }; -// Used by auto-mode and tutorial +// Used by auto-mode and tutorial-pane export type Action = 'expand' | 'collapse' | 'expandToView' | 'unhideAncestor' | 'tileInfo' | 'search' | 'autoMode' | 'settings' | 'help'; - -// Project-wide configurable options (supersets the user-configurable settings) -export type UiOptions = { - // Shared coloring/sizing - textColor: string, // CSS color - textColorAlt: string, - bgColor: string, - bgColorLight: string, - bgColorDark: string, - bgColorLight2: string, - bgColorDark2: string, - bgColorAlt: string, - bgColorAltDark: string, - altColor: string, - altColorDark: string, - borderRadius: number, // CSS border-radius value, in px - shadowNormal: string, // CSS box-shadow value - shadowHovered: string, - shadowFocused: string, - // Component coloring - childQtyColors: [number, string][], - // Specifies, for an increasing sequence of minimum-child-quantity values, CSS colors to use - //eg: [[1, 'green'], [10, 'orange'], [100, 'red']] - nonleafBgColors: string[], - // Specifies CSS colors to use at various tree depths - // With N strings, tiles at depth M use the color at index M % N - nonleafHeaderColor: string, // CSS color - ancestryBarBgColor: string, - // Component sizing - ancestryBarBreadth: number, // px (fixed value needed for transitions) - tutPaneSz: number, // px (fixed value needed for transitions) - scrollGap: number, // Size of scroll bar, in px - // Timing related - clickHoldDuration: number, // Time after mousedown when a click-and-hold is recognised, in ms - transitionDuration: number, // ms - animationDelay: number, // Time between updates during transitions/resizes/etc, in ms - autoActionDelay: number, // Time between auto-mode actions (incl transitions), in ms - // Device-info-like - touchDevice: boolean, - breakpoint: Breakpoint, - // Other - tree: 'trimmed' | 'images' | 'picked', - searchSuggLimit: number, // Max number of search suggestions - searchJumpMode: boolean, - tutorialSkip: boolean, - disabledActions: Set, - 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, - 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, + // 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, + // 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; -- cgit v1.2.3