mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 02:29:24 +00:00
Compare commits
6 Commits
plotly-and
...
tree-refac
Author | SHA1 | Date | |
---|---|---|---|
15bef1c162 | |||
a2d21e3479 | |||
5693b153cb | |||
13e9541ad2 | |||
c0e3f6b577 | |||
f763d1dea1 |
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<span
|
||||
class="c-disclosure-triangle"
|
||||
:class="{
|
||||
'c-disclosure-triangle--expanded' : value,
|
||||
'is-enabled' : enabled
|
||||
}"
|
||||
:class="[
|
||||
controlClass,
|
||||
{ 'c-disclosure-triangle--expanded' : value },
|
||||
{'is-enabled' : enabled }
|
||||
]"
|
||||
@click="handleClick"
|
||||
></span>
|
||||
</template>
|
||||
@ -25,6 +25,10 @@ export default {
|
||||
propagate: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
controlClass: {
|
||||
type: String,
|
||||
default: 'c-disclosure-triangle'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -24,6 +24,10 @@
|
||||
class="l-browse-bar__context-actions c-disclosure-button"
|
||||
@click.prevent.stop="showContextMenu"
|
||||
></div>
|
||||
<div
|
||||
class="l-browse-sync-tree"
|
||||
@click="syncNavigationTree"
|
||||
>Sync</div>
|
||||
</div>
|
||||
|
||||
<div class="l-browse-bar__end">
|
||||
@ -271,6 +275,9 @@ export default {
|
||||
},
|
||||
goToParent() {
|
||||
window.location.hash = this.parentUrl;
|
||||
},
|
||||
syncNavigationTree() {
|
||||
this.$emit('syncTreeNavigation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,12 +47,16 @@
|
||||
label="Browse"
|
||||
collapsable
|
||||
>
|
||||
<mct-tree class="l-shell__tree" />
|
||||
<mct-tree
|
||||
:sync-tree-navigation="triggerSync"
|
||||
class="l-shell__tree"
|
||||
/>
|
||||
</pane>
|
||||
<pane class="l-shell__pane-main">
|
||||
<browse-bar
|
||||
ref="browseBar"
|
||||
class="l-shell__main-view-browse-bar"
|
||||
@syncTreeNavigation="handleSyncTreeNavigation"
|
||||
/>
|
||||
<toolbar
|
||||
v-if="toolbar"
|
||||
@ -154,7 +158,8 @@ export default {
|
||||
conductorComponent: undefined,
|
||||
isEditing: false,
|
||||
hasToolbar: false,
|
||||
headExpanded
|
||||
headExpanded,
|
||||
triggerSync: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -204,6 +209,9 @@ export default {
|
||||
}
|
||||
|
||||
this.hasToolbar = structure.length > 0;
|
||||
},
|
||||
handleSyncTreeNavigation() {
|
||||
this.triggerSync = !this.triggerSync;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
.c-tree-and-search {
|
||||
$hoverBg: rgba(#000, 0.1);
|
||||
$hoverFg: #ccc;
|
||||
$selected: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
//TODO: Do we need this???
|
||||
//padding-right: $interiorMarginSm;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> * + * { margin-top: $interiorMargin; }
|
||||
|
||||
@ -26,6 +30,105 @@
|
||||
height: 0; // Chrome 73 overflow bug fix
|
||||
padding-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
// new tree refactor
|
||||
.c-tree,
|
||||
.c-list {
|
||||
&__item {
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background: $hoverBg;
|
||||
|
||||
[class*="__name"] {
|
||||
color: darken($colorKeyFg, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-navigated-object,
|
||||
&.is-selected {
|
||||
background: none;
|
||||
|
||||
[class*="__name"] {
|
||||
color: $selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// new tree refactor
|
||||
.c-tree {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
transition: all;
|
||||
|
||||
.c-tree {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
border-bottom: 1px solid $colorInteriorBorder;
|
||||
|
||||
&.is-navigated-object,
|
||||
&.is-selected {
|
||||
.c-tree__item__type-icon:before {
|
||||
color: $selected;
|
||||
}
|
||||
}
|
||||
|
||||
.c-nav {
|
||||
$dimension: 20px;
|
||||
border-radius: 3px;
|
||||
|
||||
&__up, &__down {
|
||||
color: #fff;
|
||||
flex: 0 0 auto;
|
||||
height: $dimension;
|
||||
width: $dimension;
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&.is-enabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: darken($colorKeyFg, 10%);
|
||||
}
|
||||
|
||||
&:before {
|
||||
// Nav arrow
|
||||
$color: rgba(#999, 0.5);
|
||||
$dimension: 7px;
|
||||
$width: 3px;
|
||||
border: solid $color;
|
||||
border-width: $width;
|
||||
border-width: 0 $width $width 0;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
height: $dimension;
|
||||
width: $dimension;
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
border-color: $colorItemTreeHoverFg;
|
||||
}
|
||||
}
|
||||
|
||||
&__up:before {
|
||||
transform: translate(-30%, -50%) rotate(135deg);
|
||||
}
|
||||
|
||||
&__down:before {
|
||||
transform: translate(-70%, -50%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.c-tree,
|
||||
@ -72,8 +175,9 @@
|
||||
}
|
||||
|
||||
.c-tree {
|
||||
|
||||
.c-tree {
|
||||
margin-left: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
@ -157,3 +261,32 @@
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
// TRANSITIONS
|
||||
.slide-left,
|
||||
.slide-right {
|
||||
animation-duration: 500ms;
|
||||
animation-iteration-count: 1;
|
||||
transition: all;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.slide-left {
|
||||
animation-name: animSlideLeft;
|
||||
}
|
||||
|
||||
.slide-right {
|
||||
animation-name: animSlideRight;
|
||||
}
|
||||
|
||||
@keyframes animSlideLeft {
|
||||
0% {opactiy: 0; transform: translateX(100%);}
|
||||
10% {opacity: 1;}
|
||||
100% {transform: translateX(0);}
|
||||
}
|
||||
|
||||
@keyframes animSlideRight {
|
||||
0% {opactiy: 0; transform: translateX(-100%);}
|
||||
10% {opacity: 1;}
|
||||
100% {transform: translateX(0);}
|
||||
}
|
@ -31,9 +31,11 @@
|
||||
class="c-tree-and-search__tree c-tree"
|
||||
>
|
||||
<tree-item
|
||||
v-for="treeItem in allTreeItems"
|
||||
:key="treeItem.id"
|
||||
:node="treeItem"
|
||||
v-for="item in allTreeItems"
|
||||
:key="item.id"
|
||||
:class="childrenSlideClass"
|
||||
:node="item"
|
||||
:sync-check="checkForSync"
|
||||
/>
|
||||
</ul>
|
||||
<!-- end main tree -->
|
||||
@ -44,9 +46,9 @@
|
||||
class="c-tree-and-search__tree c-tree"
|
||||
>
|
||||
<tree-item
|
||||
v-for="treeItem in filteredTreeItems"
|
||||
:key="treeItem.id"
|
||||
:node="treeItem"
|
||||
v-for="item in filteredTreeItems"
|
||||
:key="item.id"
|
||||
:node="item"
|
||||
/>
|
||||
</ul>
|
||||
<!-- end search tree -->
|
||||
@ -64,26 +66,43 @@ export default {
|
||||
search,
|
||||
treeItem
|
||||
},
|
||||
props: {
|
||||
syncTreeNavigation: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchValue: '',
|
||||
allTreeItems: [],
|
||||
filteredTreeItems: [],
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
childrenSlideClass: 'slide-left',
|
||||
checkForSync: this.makeHash()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
syncTreeNavigation() {
|
||||
console.log('sync in mct-tree');
|
||||
this.checkForSync = this.makeHash();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.searchService = this.openmct.$injector.get('searchService');
|
||||
this.getAllChildren();
|
||||
console.log('tree: mounted');
|
||||
},
|
||||
methods: {
|
||||
getAllChildren() {
|
||||
this.isLoading = true;
|
||||
this.openmct.objects.get('ROOT')
|
||||
.then(root => {
|
||||
console.log('tree: root', root);
|
||||
return this.openmct.composition.get(root).load()
|
||||
})
|
||||
.then(children => {
|
||||
console.log('tree: children', children);
|
||||
this.isLoading = false;
|
||||
this.allTreeItems = children.map(c => {
|
||||
return {
|
||||
@ -128,6 +147,14 @@ export default {
|
||||
if (this.searchValue !== '') {
|
||||
this.getFilteredChildren();
|
||||
}
|
||||
},
|
||||
makeHash(length = 20) {
|
||||
let hash = '',
|
||||
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < length; i++) {
|
||||
hash += characters.charAt(Math.floor(Math.random() * characters.length)) + Date.now();
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,26 @@
|
||||
<div
|
||||
class="c-tree__item"
|
||||
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
|
||||
:style="{ paddingLeft: ancestors * 10 + 10 + 'px' }"
|
||||
>
|
||||
<view-control
|
||||
v-model="expanded"
|
||||
class="c-tree__item__view-control"
|
||||
:enabled="hasChildren"
|
||||
:enabled="hasChildren && activeChild !== undefined"
|
||||
:control-class="'c-nav__up'"
|
||||
@input="resetTreeHere"
|
||||
/>
|
||||
<object-label
|
||||
:domain-object="node.object"
|
||||
:object-path="node.objectPath"
|
||||
:navigate-to-path="navigateToPath"
|
||||
/>
|
||||
<view-control
|
||||
v-model="expanded"
|
||||
class="c-tree__item__view-control"
|
||||
:control-class="'c-nav__down'"
|
||||
:enabled="hasChildren && !activeChild && !expanded"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
v-if="expanded"
|
||||
@ -27,11 +36,21 @@
|
||||
<span class="c-tree__item__label">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
<tree-item
|
||||
<template
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
/>
|
||||
>
|
||||
<tree-item
|
||||
v-if="activeChild && child.id === activeChild || !activeChild"
|
||||
:key="child.id"
|
||||
:class="{[childrenSlideClass] : child.id !== activeChild}"
|
||||
:node="child"
|
||||
:collapse-children="collapseMyChildren"
|
||||
:ancestors="ancestors + 1"
|
||||
:sync-check="triggerChildSync"
|
||||
@expanded="handleExpanded"
|
||||
@childState="handleChildState"
|
||||
/>
|
||||
</template>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
@ -41,6 +60,12 @@ import viewControl from '../components/viewControl.vue';
|
||||
import ObjectLabel from '../components/ObjectLabel.vue';
|
||||
|
||||
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
|
||||
const SLIDE_RIGHT = 'slide-right';
|
||||
const SLIDE_LEFT = 'slide-left';
|
||||
|
||||
function copyItem(item) {
|
||||
return JSON.parse(JSON.stringify(item));
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'TreeItem',
|
||||
@ -53,6 +78,18 @@ export default {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
ancestors: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
collapseChildren: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
syncCheck: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -63,7 +100,14 @@ export default {
|
||||
loaded: false,
|
||||
navigated: this.navigateToPath === this.openmct.router.currentLocation.path,
|
||||
children: [],
|
||||
expanded: false
|
||||
expanded: false,
|
||||
activeChild: undefined,
|
||||
collapseMyChildren: '',
|
||||
childrenSlideClass: SLIDE_LEFT,
|
||||
triggerChildSync: '',
|
||||
onChildrenLoaded: [],
|
||||
mountedChildren: [],
|
||||
onChildMounted: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -81,23 +125,77 @@ export default {
|
||||
if (!this.hasChildren) {
|
||||
return;
|
||||
}
|
||||
if (!this.loaded && !this.isLoading) {
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addChild);
|
||||
this.composition.on('remove', this.removeChild);
|
||||
this.composition.load().then(this.finishLoading);
|
||||
this.isLoading = true;
|
||||
if(this.expanded) {
|
||||
this.$emit('expanded', this.domainObject);
|
||||
this.loadChildren();
|
||||
}
|
||||
this.setLocalStorageExpanded(this.navigateToPath);
|
||||
},
|
||||
collapseChildren() {
|
||||
if(this.collapseChildren) {
|
||||
this.expanded = false;
|
||||
this.activeChild = undefined;
|
||||
this.loaded = false;
|
||||
this.children = [];
|
||||
}
|
||||
},
|
||||
syncCheck() {
|
||||
let currentLocationPath = this.openmct.router.currentLocation.path;
|
||||
if(currentLocationPath) {
|
||||
let isAncestor = currentLocationPath.includes(this.navigateToPath);
|
||||
|
||||
// not the currently navigated object, but it is an ancestor
|
||||
if(isAncestor && !this.navigated) {
|
||||
let descendantPath = currentLocationPath.split(this.navigateToPath + '/')[1],
|
||||
descendants = descendantPath.split('/'),
|
||||
descendantCount = descendants.length,
|
||||
immediateDescendant = descendants[0];
|
||||
|
||||
this.activeChild = undefined;
|
||||
|
||||
if(descendantCount > 1) {
|
||||
this.activeChild = immediateDescendant;
|
||||
}
|
||||
|
||||
// if current path is not expanded, need to expand (load children) and trigger sync
|
||||
if(!this.expanded) {
|
||||
if(descendantCount > 1) {
|
||||
this.onChildrenLoaded.push(() => {
|
||||
this.triggerChildrenSyncCheck()
|
||||
});
|
||||
}
|
||||
this.expanded = true;
|
||||
|
||||
// if current path IS expanded, then we need to check that child is mounted
|
||||
// as it could have been unmounted previously if it was not the activeChild
|
||||
} else {
|
||||
let alreadyMounted = this.mountedChildren.includes(immediateDescendant);
|
||||
if(alreadyMounted) {
|
||||
this.triggerChildrenSyncCheck()
|
||||
} else {
|
||||
this.onChildMounted.push({
|
||||
child: immediateDescendant,
|
||||
callback: () => {
|
||||
this.triggerChildrenSyncCheck()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.expanded = false;
|
||||
this.activeChild = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// TODO: should update on mutation.
|
||||
// TODO: click navigation should not fubar hash quite so much.
|
||||
// TODO: should highlight if navigated to.
|
||||
// TODO: should have context menu.
|
||||
// TODO: should support drag/drop composition
|
||||
// TODO: set isAlias per tree-item
|
||||
let objectComposition = this.openmct.composition.get(this.node.object);
|
||||
|
||||
this.$emit('childState', {
|
||||
type: 'mounted',
|
||||
id: this.openmct.objects.makeKeyString(this.node.object.identifier),
|
||||
name: this.node.object.name
|
||||
});
|
||||
|
||||
this.domainObject = this.node.object;
|
||||
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
|
||||
@ -105,7 +203,7 @@ export default {
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', removeListener);
|
||||
if (this.openmct.composition.get(this.node.object)) {
|
||||
if (objectComposition && objectComposition.domainObject.composition.length > 0) {
|
||||
this.hasChildren = true;
|
||||
}
|
||||
|
||||
@ -121,8 +219,14 @@ export default {
|
||||
*****/
|
||||
this.expanded = false;
|
||||
this.setLocalStorageExpanded();
|
||||
this.activeChild = undefined;
|
||||
},
|
||||
destroyed() {
|
||||
this.$emit('childState', {
|
||||
type: 'destroyed',
|
||||
id: this.openmct.objects.makeKeyString(this.domainObject.identifier),
|
||||
name: this.domainObject.name
|
||||
});
|
||||
this.openmct.router.off('change:path', this.highlightIfNavigated);
|
||||
if (this.composition) {
|
||||
this.composition.off('add', this.addChild);
|
||||
@ -144,9 +248,26 @@ export default {
|
||||
this.children = this.children
|
||||
.filter(c => c.id !== removeId);
|
||||
},
|
||||
loadChildren() {
|
||||
if (!this.loaded && !this.isLoading) {
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addChild);
|
||||
this.composition.on('remove', this.removeChild);
|
||||
this.composition.load().then(this.finishLoading);
|
||||
this.isLoading = true;
|
||||
}
|
||||
},
|
||||
finishLoading() {
|
||||
this.isLoading = false;
|
||||
this.loaded = true;
|
||||
// specifically for sync child loading
|
||||
for(let callback of this.onChildrenLoaded) {
|
||||
callback();
|
||||
}
|
||||
this.onChildrenLoaded = [];
|
||||
},
|
||||
triggerChildrenSyncCheck() {
|
||||
this.triggerChildSync = this.makeHash();
|
||||
},
|
||||
buildPathString(parentPath) {
|
||||
return [parentPath, this.openmct.objects.makeKeyString(this.node.object.identifier)].join('/');
|
||||
@ -160,7 +281,6 @@ export default {
|
||||
},
|
||||
getLocalStorageExpanded() {
|
||||
let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
|
||||
|
||||
if (expandedPaths) {
|
||||
expandedPaths = JSON.parse(expandedPaths);
|
||||
this.expanded = expandedPaths.includes(this.navigateToPath);
|
||||
@ -170,7 +290,6 @@ export default {
|
||||
setLocalStorageExpanded() {
|
||||
let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
|
||||
expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : [];
|
||||
|
||||
if (this.expanded) {
|
||||
if (!expandedPaths.includes(this.navigateToPath)) {
|
||||
expandedPaths.push(this.navigateToPath);
|
||||
@ -187,6 +306,39 @@ export default {
|
||||
expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : [];
|
||||
expandedPaths = expandedPaths.filter(path => !path.startsWith(this.navigateToPath));
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(expandedPaths));
|
||||
},
|
||||
handleExpanded(expandedObject) {
|
||||
this.activeChild = this.openmct.objects.makeKeyString(expandedObject.identifier);
|
||||
this.childrenSlideClass = SLIDE_LEFT;
|
||||
},
|
||||
handleChildState(opts) {
|
||||
if(opts.type === 'mounted') {
|
||||
this.mountedChildren.push(opts.id);
|
||||
if(this.onChildMounted.length && this.onChildMounted[0].child === opts.id) {
|
||||
this.onChildMounted[0].callback();
|
||||
this.onChildMounted = [];
|
||||
}
|
||||
} else {
|
||||
if(this.mountedChildren.includes(opts.id)) {
|
||||
let removeIndex = this.mountedChildren.indexOf(opts.id);
|
||||
this.mountedChildren.splice(removeIndex, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetTreeHere() {
|
||||
this.childrenSlideClass = SLIDE_RIGHT;
|
||||
this.activeChild = undefined;
|
||||
this.collapseMyChildren = this.makeHash();
|
||||
this.expanded = true;
|
||||
},
|
||||
makeHash(length = 20) {
|
||||
let hash = String(Date.now()),
|
||||
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
length -= hash.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
hash += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user