From 87a45de05b12b12c352e1447c7dfdd96b3868b6f Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Wed, 7 Oct 2020 11:29:42 -0700 Subject: [PATCH] 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 --- src/ui/layout/mct-tree.scss | 12 ++- src/ui/layout/mct-tree.vue | 203 ++++++++++++++++-------------------- src/ui/layout/tree-item.vue | 19 ++-- 3 files changed, 109 insertions(+), 125 deletions(-) diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss index 60146cd590..b632d79705 100644 --- a/src/ui/layout/mct-tree.scss +++ b/src/ui/layout/mct-tree.scss @@ -26,10 +26,8 @@ overflow: hidden; transition: all; - .scrollable-children { - .c-tree__item-h { - width: 100%; - } + &__scrollable-children { + overflow: auto; } &__item--empty { @@ -57,7 +55,11 @@ li { position: relative; - &[class*="__item-h"] { display: block; } + &[class*="__item-h"] { + display: block; + width: 100%; + } + + li { margin-top: 1px; } diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index a28830832e..ad54a315ee 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -35,7 +35,7 @@ class="c-tree-and-search__tree c-tree" > -
+
  • -
  • Loading... -
  • +
    - +
    • -
      +
      @@ -143,20 +143,18 @@ export default { ancestors: [], childrenSlideClass: 'down', availableContainerHeight: 0, - noScroll: true, updatingView: false, + itemHeightCalculated: false, itemHeight: 28, itemOffset: 0, - childrenHeight: 0, scrollable: undefined, - pageThreshold: 50, activeSearch: false, - getChildHeight: false, - settingChildrenHeight: false, + shouldEmitHeight: false, isMobile: isMobile.mobileName, multipleRootChildren: false, noVisibleItems: false, - observedAncestors: {} + observedAncestors: {}, + mainTreeTopMargin: undefined }; }, computed: { @@ -189,6 +187,21 @@ export default { return { 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: { @@ -199,7 +212,7 @@ export default { let jumpAndScroll = currentLocationPath && hasParent && !this.currentPathIsActivePath(); - let justScroll = this.currentPathIsActivePath() && !this.noScroll; + let justScroll = this.currentPathIsActivePath(); if (this.searchValue) { this.searchValue = ''; @@ -233,20 +246,31 @@ export default { this.searchDeactivated(); } }, - searchResultItems() { - this.setContainerHeight(); - }, 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) { this.setContainerHeight(); } }, ancestors() { this.observeAncestors(); + }, + availableContainerHeight() { + this.updateVisibleItems(); + }, + focusedItems() { + this.updateVisibleItems(); } }, 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(); let savedPath = this.getSavedNavigatedPath(); @@ -257,13 +281,20 @@ export default { if (root.identifier !== undefined) { 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 (root.composition && root.composition.length > 1) { + if (rootComposition && rootComposition.length > 1) { this.ancestors.push(rootNode); + if (!this.itemHeightCalculated) { + await this.calculateItemHeight(); + } + 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 - savedPath = root.composition[0]; + savedPath = rootComposition[0].identifier; } if (savedPath) { @@ -282,13 +313,19 @@ export default { }, created() { this.getSearchResults = _.debounce(this.getSearchResults, 400); + this.setContainerHeight = _.debounce(this.setContainerHeight, 200); + }, + updated() { + this.$nextTick(() => { + this.setContainerHeight(); + }); }, destroyed() { window.removeEventListener('resize', this.handleWindowResize); this.stopObservingAncestors(); }, methods: { - updatevisibleItems() { + updateVisibleItems() { if (this.updatingView) { return; } @@ -326,105 +363,54 @@ export default { this.updatingView = false; }); }, - async setContainerHeight() { - await this.$nextTick(); + setContainerHeight() { let mainTree = this.$refs.mainTree; let mainTreeHeight = mainTree && mainTree.clientHeight ? mainTree.clientHeight : 0; if (mainTreeHeight !== 0) { - this.calculateChildHeight(() => { - 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(); - }); + this.availableContainerHeight = mainTreeHeight - this.ancestorsHeight; } else { window.setTimeout(this.setContainerHeight, RECHECK_DELAY); } }, calculateFirstVisibleItem() { + if (!this.$refs.scrollable) { + return; + } + let scrollTop = this.$refs.scrollable.scrollTop; return Math.floor(scrollTop / this.itemHeight); }, calculateLastVisibleItem() { + if (!this.$refs.scrollable) { + return; + } + let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight; return Math.ceil(scrollBottom / this.itemHeight); }, - calculateChildrenHeight() { - let mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop'); - let childrenCount = this.focusedItems.length; + calculateItemHeight() { + this.shouldEmitHeight = true; - return (this.itemHeight * childrenCount) - mainTreeTopMargin; // 5px margin + return new Promise((resolve, reject) => { + this.itemHeightResolve = resolve; + }); }, - setChildrenHeight() { - this.childrenHeight = this.calculateChildrenHeight(); - }, - calculateAncestorHeight() { - let ancestorCount = this.ancestors.length; + async setItemHeight(height) { - return this.itemHeight * ancestorCount; - }, - 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) { + if (this.itemHeightCalculated) { return; } - this.settingChildrenHeight = true; - if (this.isMobile) { - item = item.children[0]; - } - 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.setChildrenHeight(); - if (this.afterChildHeight) { - this.afterChildHeight(); - delete this.afterChildHeight; - } + this.itemHeight = height; + this.itemHeightCalculated = true; + this.shouldEmitHeight = false; - this.getChildHeight = false; - 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; - } + this.itemHeightResolve(); }, handleWindowResize() { if (!windowResizing) { @@ -544,6 +530,9 @@ export default { let currentNode = await this.openmct.objects.get(nodes[i]); let newParent = this.buildTreeItem(currentNode); this.ancestors.push(newParent); + if (!this.itemHeightCalculated) { + await this.calculateItemHeight(); + } if (i === nodes.length - 1) { this.jumpPath = ''; @@ -570,14 +559,8 @@ export default { let scrollTopAmount = indexOfScroll * this.itemHeight; await this.$nextTick(); + 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; } else { window.setTimeout(this.autoScroll, RECHECK_DELAY); @@ -635,7 +618,6 @@ export default { this.activeSearch = false; await this.$nextTick(); this.$refs.scrollable.scrollTop = 0; - this.setContainerHeight(); }, handleReset(node) { this.childrenSlideClass = 'up'; @@ -688,17 +670,16 @@ export default { }, scrollItems(event) { if (!windowResizing) { - this.updatevisibleItems(); + this.updateVisibleItems(); } }, childrenListStyles() { return { position: 'relative' }; }, scrollableStyles() { - return { - height: this.availableContainerHeight + 'px', - overflow: this.noScroll ? 'hidden' : 'scroll' - }; + let height = this.availableContainerHeight + 'px'; + + return { height }; }, getElementStyleValue(el, style) { let styleString = window.getComputedStyle(el)[style]; diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue index 800955b758..be10350f71 100644 --- a/src/ui/layout/tree-item.vue +++ b/src/ui/layout/tree-item.vue @@ -84,7 +84,7 @@ export default { type: Boolean, default: false }, - emitHeight: { + shouldEmitHeight: { type: Boolean, default: false } @@ -116,16 +116,20 @@ export default { watch: { expanded() { this.$emit('expanded', this.domainObject); - }, - emitHeight() { - this.$nextTick(() => { - this.$emit('emittedHeight', this.$refs.me); - }); } }, mounted() { 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; let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => { this.domainObject = newObject; @@ -137,9 +141,6 @@ export default { } this.openmct.router.on('change:path', this.highlightIfNavigated); - if (this.emitHeight) { - this.$emit('emittedHeight', this.$refs.me); - } }, destroyed() { this.openmct.router.off('change:path', this.highlightIfNavigated);