diff options
| -rw-r--r-- | src/App.vue | 217 | ||||
| -rw-r--r-- | src/components/AncestryBar.vue | 50 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 2 | ||||
| -rw-r--r-- | src/components/TutorialPane.vue | 25 | ||||
| -rw-r--r-- | src/lib.ts | 18 |
5 files changed, 105 insertions, 207 deletions
diff --git a/src/App.vue b/src/App.vue index 4cc7aa0..e9db4c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,7 +18,7 @@ import type {TolMap} from './tol'; import {TolNode} from './tol'; import {LayoutNode, initLayoutTree, initLayoutMap, tryLayout} from './layout'; import type {LayoutOptions, LayoutTreeChg} from './layout'; -import {arraySum, randWeightedChoice, getScrollBarWidth} from './lib'; +import {arraySum, randWeightedChoice} from './lib'; import type {Action} from './lib'; // Note: Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain @@ -78,8 +78,7 @@ const defaultUiOpts = { // For other components appBgColor: '#292524', tileAreaOffset: 5, //px (space between root tile and display boundary) - scrollGap: getScrollBarWidth(), //px (gap for overflown-root and ancestry-bar scrollbars, used to prevent overlap) - ancestryBarSz: defaultLytOpts.minTileSz * 2, //px (breadth of ancestry-bar area) + ancestryBarImgSz: defaultLytOpts.minTileSz * 2, //px ancestryBarBgColor: '#44403c', ancestryTileMargin: 5, //px (gap between detached-ancestor tiles) infoModalImgSz: 200, @@ -123,18 +122,15 @@ export default defineComponent({ // Options lytOpts: this.getLytOpts(), uiOpts: this.getUiOpts(), - // For window-resize handling - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, + // For layout and resize-handling + mainAreaDims: [0, 0] as [number, number], + tileAreaDims: [0, 0] as [number, number], pendingResizeHdlr: 0, // Set to a setTimeout() value // Other excessTolNodeThreshold: 1000, // Threshold where excess tolMap entries are removed (done on tile collapse) }; }, computed: { - wideArea(): boolean { - return this.width >= this.height; - }, // Nodes to show in ancestry-bar, with tol root first detachedAncestors(): LayoutNode[] | null { if (this.activeRoot == this.layoutTree){ @@ -148,59 +144,6 @@ export default defineComponent({ } return ancestors.reverse(); }, - // Placement info for Tile and AncestryBar - tileAreaPos(){ - let pos = [this.uiOpts.tileAreaOffset, this.uiOpts.tileAreaOffset] as [number, number]; - if (this.detachedAncestors != null){ - if (this.wideArea){ - pos[0] += this.uiOpts.ancestryBarSz; - } else { - pos[1] += this.uiOpts.ancestryBarSz; - } - } - if (this.tutorialOpen){ - pos[1] += this.uiOpts.tutorialPaneSz; - } - return pos; - }, - tileAreaDims(){ - let dims = [ - this.width - this.uiOpts.tileAreaOffset*2, - this.height - this.uiOpts.tileAreaOffset*2 - ] as [number, number]; - if (this.detachedAncestors != null){ - if (this.wideArea){ - dims[0] -= this.uiOpts.ancestryBarSz; - } else { - dims[1] -= this.uiOpts.ancestryBarSz; - } - } - if (this.tutorialOpen){ - dims[1] -= this.uiOpts.tutorialPaneSz; - } - return dims; - }, - ancestryBarPos(): [number, number] { - let pos = [0, 0] as [number, number]; - if (this.tutorialOpen){ - pos[1] += this.uiOpts.tutorialPaneSz; - } - return pos; - }, - ancestryBarDims(): [number, number] { - if (this.wideArea){ - let dims = [this.uiOpts.ancestryBarSz, this.height] as [number, number]; - if (this.tutorialOpen){ - dims[1] -= this.uiOpts.tutorialPaneSz; - } - return dims; - } else { - return [this.width, this.uiOpts.ancestryBarSz]; - } - }, - tutorialPaneDims(): [number, number] { - return [this.width, this.uiOpts.tutorialPaneSz]; - }, }, methods: { // For tile expand/collapse events @@ -221,12 +164,10 @@ export default defineComponent({ chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap} as LayoutTreeChg, layoutMap: this.layoutMap }; - let success = tryLayout( - this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, lytFnOpts); + let success = tryLayout(this.activeRoot, [0,0], this.tileAreaDims, this.lytOpts, lytFnOpts); // If expanding active-root with too many children to fit, allow overflow if (!success && layoutNode == this.activeRoot){ - success = tryLayout(this.activeRoot, this.tileAreaPos, - [this.tileAreaDims[0] - this.uiOpts.scrollGap, this.tileAreaDims[1]], + success = tryLayout(this.activeRoot, [0,0], this.tileAreaDims, {...this.lytOpts, layoutType: 'flex-sqr'}, lytFnOpts); if (success){ this.overflownRoot = true; @@ -261,7 +202,7 @@ export default defineComponent({ return false; } this.setLastFocused(null); - let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, { + let success = tryLayout(this.activeRoot, [0,0], this.tileAreaDims, this.lytOpts, { allowCollapse: false, chg: {type: 'collapse', node: layoutNode, tolMap: this.tolMap}, layoutMap: this.layoutMap @@ -301,27 +242,27 @@ export default defineComponent({ let doExpansion = () => { LayoutNode.hideUpward(layoutNode, this.layoutMap); this.activeRoot = layoutNode; - this.overflownRoot = false; - let lytFnOpts = { - allowCollapse: false, - chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap} as LayoutTreeChg, - layoutMap: this.layoutMap - }; - let success = tryLayout( - this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, lytFnOpts); - if (!success){ - success = tryLayout(this.activeRoot, this.tileAreaPos, - [this.tileAreaDims[0] - this.uiOpts.scrollGap, this.tileAreaDims[1]], - {...this.lytOpts, layoutType: 'flex-sqr'}, lytFnOpts); - if (success){ - this.overflownRoot = true; + return this.updateAreaDims().then(() => { + this.overflownRoot = false; + let lytFnOpts = { + allowCollapse: false, + chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap} as LayoutTreeChg, + layoutMap: this.layoutMap + }; + let success = tryLayout(this.activeRoot, [0,0], this.tileAreaDims, this.lytOpts, lytFnOpts); + if (!success){ + success = tryLayout(this.activeRoot, [0,0], this.tileAreaDims, + {...this.lytOpts, layoutType: 'flex-sqr'}, lytFnOpts); + if (success){ + this.overflownRoot = true; + } } - } - // Check for failure - if (!success){ - layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation - } - return success; + // Check for failure + if (!success){ + layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation + } + return success; + }); }; // Check if data for node-to-expand exists, getting from server if needed let tolNode = this.tolMap.get(layoutNode.name)!; @@ -330,15 +271,13 @@ export default defineComponent({ urlPath += this.uiOpts.useReducedTree ? '&tree=reduced' : ''; return fetch(urlPath) .then(response => response.json()) - .then(obj => { - Object.getOwnPropertyNames(obj).forEach(key => {this.tolMap.set(key, obj[key])}); - doExpansion(); - }) + .then(obj => {Object.getOwnPropertyNames(obj).forEach(key => {this.tolMap.set(key, obj[key])})}) + .then(doExpansion) .catch(error => { console.log('ERROR loading tolnode data', error); }); } else { - return new Promise((resolve, reject) => resolve(doExpansion())); + return doExpansion(); } }, onNonleafClickHeld(layoutNode: LayoutNode){ @@ -352,7 +291,7 @@ export default defineComponent({ } LayoutNode.hideUpward(layoutNode, this.layoutMap); this.activeRoot = layoutNode; - this.relayoutWithCollapse(); + this.updateAreaDims().then(() => this.relayoutWithCollapse()); }, onDetachedAncestorClick(layoutNode: LayoutNode){ if (!this.handleActionForTutorial('unhideAncestor')){ @@ -361,8 +300,7 @@ export default defineComponent({ this.setLastFocused(null); LayoutNode.showDownward(layoutNode); this.activeRoot = layoutNode; - this.relayoutWithCollapse(); - this.overflownRoot = false; + this.updateAreaDims().then(() => this.relayoutWithCollapse()); }, // For tile-info events onInfoIconClick(nodeName: string){ @@ -618,14 +556,14 @@ export default defineComponent({ onStartTutorial(){ if (this.tutorialOpen == false){ this.tutorialOpen = true; - this.relayoutWithCollapse(); + this.updateAreaDims().then(() => this.relayoutWithCollapse()); } }, onTutorialClose(){ this.tutorialOpen = false; this.welcomeOpen = false; this.disabledActions.clear(); - this.relayoutWithCollapse(); + this.updateAreaDims().then(() => this.relayoutWithCollapse()); }, onTutStageChg(disabledActions: Set<Action>, triggerAction: Action | null){ this.welcomeOpen = false; @@ -651,12 +589,10 @@ export default defineComponent({ onResize(){ if (this.pendingResizeHdlr == 0){ this.pendingResizeHdlr = setTimeout(() => { - this.width = document.documentElement.clientWidth; - this.height = document.documentElement.clientHeight; - this.uiOpts.scrollGap = getScrollBarWidth(); - this.relayoutWithCollapse(); - this.overflownRoot = false; - this.pendingResizeHdlr = 0; + this.updateAreaDims().then(() => { + this.relayoutWithCollapse(); + this.pendingResizeHdlr = 0; + }); }, 100); } }, @@ -683,7 +619,7 @@ export default defineComponent({ this.layoutTree = initLayoutTree(this.tolMap, this.layoutTree.name, 0); this.activeRoot = this.layoutTree; this.layoutMap = initLayoutMap(this.layoutTree); - this.relayoutWithCollapse(); + this.updateAreaDims().then(() => this.relayoutWithCollapse()); }) .catch(error => { console.log('ERROR loading initial tolnode data', error); @@ -737,17 +673,27 @@ export default defineComponent({ } }, relayoutWithCollapse(){ - tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, + tryLayout(this.activeRoot, [0,0], this.tileAreaDims, this.lytOpts, {allowCollapse: true, layoutMap: this.layoutMap}); // Relayout again to allocate remaining tiles 'evenly' - tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, + tryLayout(this.activeRoot, [0,0], this.tileAreaDims, this.lytOpts, {allowCollapse: false, layoutMap: this.layoutMap}); + this.overflownRoot = false; + }, + updateAreaDims(){ + console.log('updating dims') + let mainAreaEl = this.$refs.mainArea as HTMLElement; + this.mainAreaDims = [mainAreaEl.offsetWidth, mainAreaEl.offsetHeight]; + // Need to wait until ancestor-bar is laid-out before computing tileAreaDims + return this.$nextTick(() => { + let tileAreaEl = this.$refs.tileArea as HTMLElement; + this.tileAreaDims = [tileAreaEl.offsetWidth, tileAreaEl.offsetHeight]; + }); }, }, created(){ window.addEventListener('resize', this.onResize); window.addEventListener('keyup', this.onKeyUp); - this.relayoutWithCollapse(); this.initTreeFromServer(); }, unmounted(){ @@ -763,33 +709,36 @@ export default defineComponent({ </script> <template> -<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden" :style="{backgroundColor: uiOpts.appBgColor}"> - <!-- Note: Making the above enclosing div's width/height dynamic seems to cause white flashes when resizing --> - <tile :layoutNode="layoutTree" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" - :overflownDim="overflownRoot ? tileAreaDims[1] : 0" - @leaf-click="onLeafClick" @nonleaf-click="onNonleafClick" - @leaf-click-held="onLeafClickHeld" @nonleaf-click-held="onNonleafClickHeld" - @info-icon-click="onInfoIconClick"/> - <ancestry-bar v-if="detachedAncestors != null" - :pos="ancestryBarPos" :dims="ancestryBarDims" :nodes="detachedAncestors" - :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" - @detached-ancestor-click="onDetachedAncestorClick" @info-icon-click="onInfoIconClick"/> - <tutorial-pane v-if="tutorialOpen" - :pos="[0,0]" :dims="tutorialPaneDims" :uiOpts="uiOpts" :triggerFlag="tutTriggerFlag" - :skipWelcome="!welcomeOpen" @tutorial-close="onTutorialClose" @tutorial-stage-chg="onTutStageChg"/> - <!-- Icons --> - <search-icon @click="onSearchIconClick" - class="absolute bottom-[6px] right-[78px] w-[18px] h-[18px] - text-white/40 hover:text-white hover:cursor-pointer"/> - <play-icon @click="onPlayIconClick" - class="absolute bottom-[6px] right-[54px] w-[18px] h-[18px] - text-white/40 hover:text-white hover:cursor-pointer"/> - <settings-icon @click="onSettingsIconClick" - class="absolute bottom-[6px] right-[30px] w-[18px] h-[18px] - text-white/40 hover:text-white hover:cursor-pointer"/> - <help-icon @click="onHelpIconClick" - class="absolute bottom-[6px] right-[6px] w-[18px] h-[18px] - text-white/40 hover:text-white hover:cursor-pointer"/> +<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden flex flex-col" + :style="{backgroundColor: uiOpts.appBgColor}"> + <div class="flex bg-black"> + <h1 class="text-white text-bold px-2">Title</h1> + <!-- Icons --> + <search-icon @click="onSearchIconClick" + class="ml-auto mr-[6px] my-[6px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/> + <play-icon @click="onPlayIconClick" + class="mr-[6px] my-[6px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/> + <settings-icon @click="onSettingsIconClick" + class="mr-[6px] my-[6px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/> + <help-icon @click="onHelpIconClick" + class="mr-[6px] my-[6px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/> + </div> + <tutorial-pane v-if="tutorialOpen" :height="uiOpts.tutorialPaneSz + 'px'" class="grow-0 shrink-0" + :uiOpts="uiOpts" :triggerFlag="tutTriggerFlag" :skipWelcome="!welcomeOpen" + @tutorial-close="onTutorialClose" @tutorial-stage-chg="onTutStageChg"/> + <div :class="['flex', mainAreaDims[0] > mainAreaDims[1] ? 'flex-row' : 'flex-col', 'grow', 'min-h-0']" ref="mainArea"> + <ancestry-bar v-if="detachedAncestors != null" + :nodes="detachedAncestors" :vert="mainAreaDims[0] > mainAreaDims[1]" + :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" + @detached-ancestor-click="onDetachedAncestorClick" @info-icon-click="onInfoIconClick"/> + <div class="relative m-[5px] grow" ref="tileArea"> + <tile :layoutNode="layoutTree" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" + :overflownDim="overflownRoot ? mainAreaDims[1] : 0" + @leaf-click="onLeafClick" @nonleaf-click="onNonleafClick" + @leaf-click-held="onLeafClickHeld" @nonleaf-click-held="onNonleafClickHeld" + @info-icon-click="onInfoIconClick"/> + </div> + </div> <!-- Modals --> <transition name="fade"> <tile-info-modal v-if="infoModalNodeName != null" diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue index 66c1b13..8f31900 100644 --- a/src/components/AncestryBar.vue +++ b/src/components/AncestryBar.vue @@ -8,57 +8,29 @@ import type {TolMap} from '../tol'; // Displays a sequence of nodes, representing ancestors from a tree-of-life root to a currently-active root export default defineComponent({ props: { - // For absolute positioning - pos: {type: Array as unknown as PropType<[number,number]>, required: true}, - dims: {type: Array as unknown as PropType<[number,number]>, required: true}, - // The ancestors to display nodes: {type: Array as PropType<LayoutNode[]>, required: true}, + vert: {type: Boolean, default: false}, // Other tolMap: {type: Object as PropType<TolMap>, required: true}, lytOpts: {type: Object as PropType<LayoutOptions>, required: true}, uiOpts: {type: Object, required: true}, }, computed: { - wideArea(){ - return this.dims[0] >= this.dims[1]; - }, - tileSz(){ - return (this.wideArea ? this.dims[1] : this.dims[0]) - - (this.uiOpts.ancestryTileMargin * 2) - this.uiOpts.scrollGap; - }, usedNodes(){ // Childless versions of 'nodes' used to parameterise <tile> return this.nodes.map(n => { let newNode = new LayoutNode(n.name, []); - newNode.dims = [this.tileSz, this.tileSz]; + newNode.dims = [this.uiOpts.ancestryBarImgSz, this.uiOpts.ancestryBarImgSz]; return newNode; }); }, - overflowing(){ - let len = this.uiOpts.ancestryTileMargin + - (this.tileSz + this.uiOpts.ancestryTileMargin) * this.nodes.length; - return len > (this.wideArea ? this.dims[0] : this.dims[1]); - }, - width(){ - return this.dims[0] + (this.wideArea || this.overflowing ? 0 : -this.uiOpts.scrollGap); - }, - height(){ - return this.dims[1] + (!this.wideArea || this.overflowing ? 0 : -this.uiOpts.scrollGap); - }, styles(): Record<string,string> { return { - position: 'absolute', - left: this.pos[0] + 'px', - top: this.pos[1] + 'px', - width: this.width + 'px', - height: this.height + 'px', - overflowX: this.wideArea ? 'auto' : 'hidden', - overflowY: this.wideArea ? 'hidden' : 'auto', - // Extra padding for scrollbar inclusion - paddingRight: (this.overflowing && !this.wideArea ? this.uiOpts.scrollGap : 0) + 'px', - paddingBottom: (this.overflowing && this.wideArea ? this.uiOpts.scrollGap : 0) + 'px', + overflowX: this.vert ? 'hidden' : 'auto', + overflowY: this.vert ? 'auto' : 'hidden', + maxHeight: '100vh', // For child layout display: 'flex', - flexDirection: this.wideArea ? 'row' : 'column', + flexDirection: this.vert ? 'column' : 'row', gap: this.uiOpts.ancestryTileMargin + 'px', padding: this.uiOpts.ancestryTileMargin + 'px', // Other @@ -71,7 +43,7 @@ export default defineComponent({ nodes(){ setTimeout(() => this.scrollToEnd(), 0); // Without timeout, seems to run before new tiles are added }, - wideArea(){ + vert(){ setTimeout(() => this.scrollToEnd(), 0); }, }, @@ -84,15 +56,15 @@ export default defineComponent({ }, onWheelEvt(evt: WheelEvent){ // Possibly convert vertical scroll to horizontal - if (this.wideArea && Math.abs(evt.deltaX) < Math.abs(evt.deltaY)){ + if (!this.vert && Math.abs(evt.deltaX) < Math.abs(evt.deltaY)){ this.$el.scrollLeft -= (evt.deltaY > 0 ? 30 : -30); } }, scrollToEnd(){ - if (this.wideArea){ - this.$el.scrollLeft = this.$el.scrollWidth; - } else { + if (this.vert){ this.$el.scrollTop = this.$el.scrollHeight; + } else { + this.$el.scrollLeft = this.$el.scrollWidth; } }, }, diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue index 35efb55..e5e8596 100644 --- a/src/components/SearchModal.vue +++ b/src/components/SearchModal.vue @@ -16,7 +16,7 @@ export default defineComponent({ searchHasMoreSuggs: false, focusedSuggIdx: null as null | number, // Denotes a search-suggestion selected using the arrow keys pendingSuggReq: 0, // Set via setTimeout() upon a search-suggestion request - pendingSuggReqUrl: null, // Used by a pendingSuggReq callback to use the latest user input + pendingSuggReqUrl: '', // Used by a pendingSuggReq callback to use the latest user input }; }, props: { diff --git a/src/components/TutorialPane.vue b/src/components/TutorialPane.vue index c04aeee..d5f681c 100644 --- a/src/components/TutorialPane.vue +++ b/src/components/TutorialPane.vue @@ -5,11 +5,10 @@ import {Action} from '../lib'; export default defineComponent({ props: { - pos: {type: Array as unknown as PropType<[number,number]>, required: true}, - dims: {type: Array as unknown as PropType<[number,number]>, required: true}, uiOpts: {type: Object, required: true}, triggerFlag: {type: Boolean, required: true}, skipWelcome: {type: Boolean, default: false}, + height: {type: String, default: 'auto'}, }, data(){ return { @@ -20,13 +19,9 @@ export default defineComponent({ computed: { styles(): Record<string,string> { return { - position: 'absolute', - left: this.pos[0] + 'px', - top: this.pos[1] + 'px', - width: this.dims[0] + 'px', - height: this.dims[1] + 'px', backgroundColor: this.uiOpts.tutorialPaneBgColor, color: this.uiOpts.tutorialPaneTextColor, + height: this.height, }; }, contentStyles(): Record<string,string> { @@ -98,18 +93,20 @@ export default defineComponent({ </script> <template> -<div :style="styles" class="flex flex-col justify-evenly"> - <close-icon @click.stop="onClose" - class="block absolute top-2 right-2 w-6 h-6 hover:cursor-pointer"/> +<div :style="styles" class="p-2 flex flex-col justify-between"> + <div class="flex"> + <h2 class="text-center mb-2">{{stage == 0 ? 'Welcome' : 'Tutorial'}}</h2> + <close-icon @click.stop="onClose" + class="block ml-auto w-6 h-6 hover:cursor-pointer"/> + </div> <template v-if="stage == 0"> - <h2 class="text-center">Welcome</h2> <div :style="contentStyles"> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco. </div> - <div class="w-full flex justify-evenly"> + <div class="w-full flex justify-evenly mt-2"> <button :style="buttonStyles" class="hover:brightness-125" @click="onStartTutorial"> Start Tutorial </button> @@ -119,8 +116,6 @@ export default defineComponent({ </div> </template> <template v-else> - <h2 class="text-center">Tutorial</h2> - <!-- Text content --> <div v-if="stage == 1" :style="contentStyles"> Click/touch on the tile to expand it and see it's children. <br/> A green title means the tile has children. Orange and red mean 100+ or 1000+ children. @@ -170,7 +165,7 @@ export default defineComponent({ And finally, the help icon provides summarised usage information. </div> <!-- Buttons --> - <div class="w-full flex justify-evenly"> + <div class="w-full flex justify-evenly mt-2"> <button :style="buttonStyles" :disabled="stage == 1" :class="stage == 1 ? ['brightness-75'] : ['hover:brightness-125']" @click="onPrevClick"> @@ -111,21 +111,3 @@ export function capitalizeWords(str: string){ return str.replace(/\b\w/g, x => x.toUpperCase()); // '\b' matches word boundary, '\w' is like [a-zA-Z0-9_], } -// Dynamically obtains 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'); - outer.style.visibility = 'hidden'; - outer.style.overflow = 'scroll'; - document.body.appendChild(outer); - // Create inner div - let inner = document.createElement('div'); - outer.appendChild(inner); - // Get width difference - let scrollBarWidth = outer.offsetWidth - inner.offsetWidth; - // Remove temporary divs - outer.parentNode!.removeChild(outer); - // - return scrollBarWidth; -} |
