Fix scroll issues in tree overflow state (#3385)

* Fixes #3383 - Tree scrolling area should not display horizontal scroll.
* Includes various additional improvements to the object tree.
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
This commit is contained in:
Charles Hacskaylo
2020-10-07 11:29:42 -07:00
committed by GitHub
parent ab76451360
commit 87a45de05b
3 changed files with 109 additions and 125 deletions

View File

@@ -26,10 +26,8 @@
overflow: hidden; overflow: hidden;
transition: all; transition: all;
.scrollable-children { &__scrollable-children {
.c-tree__item-h { overflow: auto;
width: 100%;
}
} }
&__item--empty { &__item--empty {
@@ -57,7 +55,11 @@
li { li {
position: relative; position: relative;
&[class*="__item-h"] { display: block; } &[class*="__item-h"] {
display: block;
width: 100%;
}
+ li { + li {
margin-top: 1px; margin-top: 1px;
} }

View File

@@ -35,7 +35,7 @@
class="c-tree-and-search__tree c-tree" class="c-tree-and-search__tree c-tree"
> >
<!-- ancestors --> <!-- ancestors -->
<div v-if="!activeSearch"> <li v-if="!activeSearch">
<tree-item <tree-item
v-for="(ancestor, index) in ancestors" v-for="(ancestor, index) in ancestors"
:key="ancestor.id" :key="ancestor.id"
@@ -43,20 +43,20 @@
:show-up="index < ancestors.length - 1" :show-up="index < ancestors.length - 1"
:show-down="false" :show-down="false"
:left-offset="index * 10 + 'px'" :left-offset="index * 10 + 'px'"
:emit-height="getChildHeight" :should-emit-height="shouldEmitHeight"
@emittedHeight="setChildHeight" @emittedHeight="setItemHeight"
@resetTree="handleReset" @resetTree="handleReset"
/> />
<!-- loading --> <!-- loading -->
<li <div
v-if="isLoading" v-if="isLoading || !itemHeightCalculated"
:style="indicatorLeftOffset" :style="indicatorLeftOffset"
class="c-tree__item c-tree-and-search__loading loading" class="c-tree__item c-tree-and-search__loading loading"
> >
<span class="c-tree__item__label">Loading...</span> <span class="c-tree__item__label">Loading...</span>
</li> </div>
<!-- end loading --> <!-- end loading -->
</div> </li>
<!-- currently viewed children --> <!-- currently viewed children -->
<transition <transition
@@ -64,17 +64,17 @@
appear appear
> >
<li <li
v-if="!isLoading && !searchLoading" v-if="!isLoading && !searchLoading && itemHeightCalculated"
:style="childrenListStyles()" :style="childrenListStyles()"
:class="childrenSlideClass" :class="childrenSlideClass"
> >
<ul <ul
ref="scrollable" ref="scrollable"
class="scrollable-children" class="c-tree__scrollable-children"
:style="scrollableStyles()" :style="scrollableStyles()"
@scroll="scrollItems" @scroll="scrollItems"
> >
<div :style="{ height: childrenHeight + 'px'}"> <div :style="{ height: childrenHeight + 'px' }">
<tree-item <tree-item
v-for="(treeItem, index) in visibleItems" v-for="(treeItem, index) in visibleItems"
:key="treeItem.id" :key="treeItem.id"
@@ -83,7 +83,7 @@
:item-offset="itemOffset" :item-offset="itemOffset"
:item-index="index" :item-index="index"
:item-height="itemHeight" :item-height="itemHeight"
:virtual-scroll="!noScroll" :virtual-scroll="true"
:show-down="activeSearch ? false : true" :show-down="activeSearch ? false : true"
@expanded="handleExpanded" @expanded="handleExpanded"
/> />
@@ -143,20 +143,18 @@ export default {
ancestors: [], ancestors: [],
childrenSlideClass: 'down', childrenSlideClass: 'down',
availableContainerHeight: 0, availableContainerHeight: 0,
noScroll: true,
updatingView: false, updatingView: false,
itemHeightCalculated: false,
itemHeight: 28, itemHeight: 28,
itemOffset: 0, itemOffset: 0,
childrenHeight: 0,
scrollable: undefined, scrollable: undefined,
pageThreshold: 50,
activeSearch: false, activeSearch: false,
getChildHeight: false, shouldEmitHeight: false,
settingChildrenHeight: false,
isMobile: isMobile.mobileName, isMobile: isMobile.mobileName,
multipleRootChildren: false, multipleRootChildren: false,
noVisibleItems: false, noVisibleItems: false,
observedAncestors: {} observedAncestors: {},
mainTreeTopMargin: undefined
}; };
}, },
computed: { computed: {
@@ -189,6 +187,21 @@ export default {
return { return {
paddingLeft: offset + 'px' paddingLeft: offset + 'px'
}; };
},
ancestorsHeight() {
if (this.activeSearch) {
return 0;
}
return this.itemHeight * this.ancestors.length;
},
pageThreshold() {
return Math.ceil(this.availableContainerHeight / this.itemHeight) + ITEM_BUFFER;
},
childrenHeight() {
let childrenCount = this.focusedItems.length || 1;
return (this.itemHeight * childrenCount) - this.mainTreeTopMargin; // 5px margin
} }
}, },
watch: { watch: {
@@ -199,7 +212,7 @@ export default {
let jumpAndScroll = currentLocationPath let jumpAndScroll = currentLocationPath
&& hasParent && hasParent
&& !this.currentPathIsActivePath(); && !this.currentPathIsActivePath();
let justScroll = this.currentPathIsActivePath() && !this.noScroll; let justScroll = this.currentPathIsActivePath();
if (this.searchValue) { if (this.searchValue) {
this.searchValue = ''; this.searchValue = '';
@@ -233,20 +246,31 @@ export default {
this.searchDeactivated(); this.searchDeactivated();
} }
}, },
searchResultItems() {
this.setContainerHeight();
},
allTreeItems() { allTreeItems() {
// catches an edge case race condition and when new items are added (ex. folder) // catches an edge case when new items are added (ex. folder)
if (!this.isLoading) { if (!this.isLoading) {
this.setContainerHeight(); this.setContainerHeight();
} }
}, },
ancestors() { ancestors() {
this.observeAncestors(); this.observeAncestors();
},
availableContainerHeight() {
this.updateVisibleItems();
},
focusedItems() {
this.updateVisibleItems();
} }
}, },
async mounted() { async mounted() {
// only reliable way to get final tree top margin
document.onreadystatechange = () => {
if (document.readyState === "complete") {
this.mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
}
};
this.backwardsCompatibilityCheck(); this.backwardsCompatibilityCheck();
let savedPath = this.getSavedNavigatedPath(); let savedPath = this.getSavedNavigatedPath();
@@ -257,13 +281,20 @@ export default {
if (root.identifier !== undefined) { if (root.identifier !== undefined) {
let rootNode = this.buildTreeItem(root); let rootNode = this.buildTreeItem(root);
let rootCompositionCollection = this.openmct.composition.get(root);
let rootComposition = await rootCompositionCollection.load();
// if more than one root item, set multipleRootChildren to true and add root to ancestors // if more than one root item, set multipleRootChildren to true and add root to ancestors
if (root.composition && root.composition.length > 1) { if (rootComposition && rootComposition.length > 1) {
this.ancestors.push(rootNode); this.ancestors.push(rootNode);
if (!this.itemHeightCalculated) {
await this.calculateItemHeight();
}
this.multipleRootChildren = true; this.multipleRootChildren = true;
} else if (!savedPath && root.composition[0] !== undefined) { } else if (!savedPath && rootComposition[0] !== undefined) {
// needed if saved path is not set, need to set it to the only root child // needed if saved path is not set, need to set it to the only root child
savedPath = root.composition[0]; savedPath = rootComposition[0].identifier;
} }
if (savedPath) { if (savedPath) {
@@ -282,13 +313,19 @@ export default {
}, },
created() { created() {
this.getSearchResults = _.debounce(this.getSearchResults, 400); this.getSearchResults = _.debounce(this.getSearchResults, 400);
this.setContainerHeight = _.debounce(this.setContainerHeight, 200);
},
updated() {
this.$nextTick(() => {
this.setContainerHeight();
});
}, },
destroyed() { destroyed() {
window.removeEventListener('resize', this.handleWindowResize); window.removeEventListener('resize', this.handleWindowResize);
this.stopObservingAncestors(); this.stopObservingAncestors();
}, },
methods: { methods: {
updatevisibleItems() { updateVisibleItems() {
if (this.updatingView) { if (this.updatingView) {
return; return;
} }
@@ -326,105 +363,54 @@ export default {
this.updatingView = false; this.updatingView = false;
}); });
}, },
async setContainerHeight() { setContainerHeight() {
await this.$nextTick();
let mainTree = this.$refs.mainTree; let mainTree = this.$refs.mainTree;
let mainTreeHeight = mainTree && mainTree.clientHeight ? mainTree.clientHeight : 0; let mainTreeHeight = mainTree && mainTree.clientHeight ? mainTree.clientHeight : 0;
if (mainTreeHeight !== 0) { if (mainTreeHeight !== 0) {
this.calculateChildHeight(() => { this.availableContainerHeight = mainTreeHeight - this.ancestorsHeight;
let ancestorsHeight = this.calculateAncestorHeight();
let allChildrenHeight = this.calculateChildrenHeight();
if (this.activeSearch) {
ancestorsHeight = 0;
}
this.availableContainerHeight = mainTreeHeight - ancestorsHeight;
if (allChildrenHeight > this.availableContainerHeight) {
this.setPageThreshold();
this.noScroll = false;
} else {
this.noScroll = true;
}
this.updatevisibleItems();
});
} else { } else {
window.setTimeout(this.setContainerHeight, RECHECK_DELAY); window.setTimeout(this.setContainerHeight, RECHECK_DELAY);
} }
}, },
calculateFirstVisibleItem() { calculateFirstVisibleItem() {
if (!this.$refs.scrollable) {
return;
}
let scrollTop = this.$refs.scrollable.scrollTop; let scrollTop = this.$refs.scrollable.scrollTop;
return Math.floor(scrollTop / this.itemHeight); return Math.floor(scrollTop / this.itemHeight);
}, },
calculateLastVisibleItem() { calculateLastVisibleItem() {
if (!this.$refs.scrollable) {
return;
}
let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight; let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight;
return Math.ceil(scrollBottom / this.itemHeight); return Math.ceil(scrollBottom / this.itemHeight);
}, },
calculateChildrenHeight() { calculateItemHeight() {
let mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop'); this.shouldEmitHeight = true;
let childrenCount = this.focusedItems.length;
return (this.itemHeight * childrenCount) - mainTreeTopMargin; // 5px margin return new Promise((resolve, reject) => {
this.itemHeightResolve = resolve;
});
}, },
setChildrenHeight() { async setItemHeight(height) {
this.childrenHeight = this.calculateChildrenHeight();
},
calculateAncestorHeight() {
let ancestorCount = this.ancestors.length;
return this.itemHeight * ancestorCount; if (this.itemHeightCalculated) {
},
calculateChildHeight(callback) {
if (callback) {
this.afterChildHeight = callback;
}
if (!this.activeSearch) {
this.getChildHeight = true;
} else if (this.afterChildHeight) {
// keep the height from before
this.afterChildHeight();
delete this.afterChildHeight;
}
},
async setChildHeight(item) {
if (!this.getChildHeight || this.settingChildrenHeight) {
return; return;
} }
this.settingChildrenHeight = true;
if (this.isMobile) {
item = item.children[0];
}
await this.$nextTick(); await this.$nextTick();
let topMargin = this.getElementStyleValue(item, 'marginTop');
let bottomMargin = this.getElementStyleValue(item, 'marginBottom');
let totalVerticalMargin = topMargin + bottomMargin;
this.itemHeight = item.clientHeight + totalVerticalMargin; this.itemHeight = height;
this.setChildrenHeight(); this.itemHeightCalculated = true;
if (this.afterChildHeight) { this.shouldEmitHeight = false;
this.afterChildHeight();
delete this.afterChildHeight;
}
this.getChildHeight = false; this.itemHeightResolve();
this.settingChildrenHeight = false;
},
setPageThreshold() {
let threshold = Math.ceil(this.availableContainerHeight / this.itemHeight) + ITEM_BUFFER;
// all items haven't loaded yet (nextTick not working for this)
if (threshold === ITEM_BUFFER) {
window.setTimeout(this.setPageThreshold, RECHECK_DELAY);
} else {
this.pageThreshold = threshold;
}
}, },
handleWindowResize() { handleWindowResize() {
if (!windowResizing) { if (!windowResizing) {
@@ -544,6 +530,9 @@ export default {
let currentNode = await this.openmct.objects.get(nodes[i]); let currentNode = await this.openmct.objects.get(nodes[i]);
let newParent = this.buildTreeItem(currentNode); let newParent = this.buildTreeItem(currentNode);
this.ancestors.push(newParent); this.ancestors.push(newParent);
if (!this.itemHeightCalculated) {
await this.calculateItemHeight();
}
if (i === nodes.length - 1) { if (i === nodes.length - 1) {
this.jumpPath = ''; this.jumpPath = '';
@@ -570,14 +559,8 @@ export default {
let scrollTopAmount = indexOfScroll * this.itemHeight; let scrollTopAmount = indexOfScroll * this.itemHeight;
await this.$nextTick(); await this.$nextTick();
this.$refs.scrollable.scrollTop = scrollTopAmount; this.$refs.scrollable.scrollTop = scrollTopAmount;
// race condition check
if (scrollTopAmount > 0 && this.$refs.scrollable.scrollTop === 0) {
window.setTimeout(this.autoScroll, RECHECK_DELAY);
return;
}
this.scrollTo = undefined; this.scrollTo = undefined;
} else { } else {
window.setTimeout(this.autoScroll, RECHECK_DELAY); window.setTimeout(this.autoScroll, RECHECK_DELAY);
@@ -635,7 +618,6 @@ export default {
this.activeSearch = false; this.activeSearch = false;
await this.$nextTick(); await this.$nextTick();
this.$refs.scrollable.scrollTop = 0; this.$refs.scrollable.scrollTop = 0;
this.setContainerHeight();
}, },
handleReset(node) { handleReset(node) {
this.childrenSlideClass = 'up'; this.childrenSlideClass = 'up';
@@ -688,17 +670,16 @@ export default {
}, },
scrollItems(event) { scrollItems(event) {
if (!windowResizing) { if (!windowResizing) {
this.updatevisibleItems(); this.updateVisibleItems();
} }
}, },
childrenListStyles() { childrenListStyles() {
return { position: 'relative' }; return { position: 'relative' };
}, },
scrollableStyles() { scrollableStyles() {
return { let height = this.availableContainerHeight + 'px';
height: this.availableContainerHeight + 'px',
overflow: this.noScroll ? 'hidden' : 'scroll' return { height };
};
}, },
getElementStyleValue(el, style) { getElementStyleValue(el, style) {
let styleString = window.getComputedStyle(el)[style]; let styleString = window.getComputedStyle(el)[style];

View File

@@ -84,7 +84,7 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
emitHeight: { shouldEmitHeight: {
type: Boolean, type: Boolean,
default: false default: false
} }
@@ -116,16 +116,20 @@ export default {
watch: { watch: {
expanded() { expanded() {
this.$emit('expanded', this.domainObject); this.$emit('expanded', this.domainObject);
},
emitHeight() {
this.$nextTick(() => {
this.$emit('emittedHeight', this.$refs.me);
});
} }
}, },
mounted() { mounted() {
let objectComposition = this.openmct.composition.get(this.node.object); let objectComposition = this.openmct.composition.get(this.node.object);
// only reliable way to get final item height
document.onreadystatechange = () => {
if (document.readyState === "complete") {
if (this.shouldEmitHeight) {
this.$emit('emittedHeight', this.$el.offsetHeight);
}
}
};
this.domainObject = this.node.object; this.domainObject = this.node.object;
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => { let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
this.domainObject = newObject; this.domainObject = newObject;
@@ -137,9 +141,6 @@ export default {
} }
this.openmct.router.on('change:path', this.highlightIfNavigated); this.openmct.router.on('change:path', this.highlightIfNavigated);
if (this.emitHeight) {
this.$emit('emittedHeight', this.$refs.me);
}
}, },
destroyed() { destroyed() {
this.openmct.router.off('change:path', this.highlightIfNavigated); this.openmct.router.off('change:path', this.highlightIfNavigated);