Compare commits

...

6 Commits

6 changed files with 368 additions and 37 deletions

View File

@ -1,10 +1,10 @@
<template> <template>
<span <span
class="c-disclosure-triangle" :class="[
:class="{ controlClass,
'c-disclosure-triangle--expanded' : value, { 'c-disclosure-triangle--expanded' : value },
'is-enabled' : enabled {'is-enabled' : enabled }
}" ]"
@click="handleClick" @click="handleClick"
></span> ></span>
</template> </template>
@ -25,6 +25,10 @@ export default {
propagate: { propagate: {
type: Boolean, type: Boolean,
default: true default: true
},
controlClass: {
type: String,
default: 'c-disclosure-triangle'
} }
}, },
methods: { methods: {

View File

@ -24,6 +24,10 @@
class="l-browse-bar__context-actions c-disclosure-button" class="l-browse-bar__context-actions c-disclosure-button"
@click.prevent.stop="showContextMenu" @click.prevent.stop="showContextMenu"
></div> ></div>
<div
class="l-browse-sync-tree"
@click="syncNavigationTree"
>Sync</div>
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
@ -271,6 +275,9 @@ export default {
}, },
goToParent() { goToParent() {
window.location.hash = this.parentUrl; window.location.hash = this.parentUrl;
},
syncNavigationTree() {
this.$emit('syncTreeNavigation');
} }
} }
} }

View File

@ -47,12 +47,16 @@
label="Browse" label="Browse"
collapsable collapsable
> >
<mct-tree class="l-shell__tree" /> <mct-tree
:sync-tree-navigation="triggerSync"
class="l-shell__tree"
/>
</pane> </pane>
<pane class="l-shell__pane-main"> <pane class="l-shell__pane-main">
<browse-bar <browse-bar
ref="browseBar" ref="browseBar"
class="l-shell__main-view-browse-bar" class="l-shell__main-view-browse-bar"
@syncTreeNavigation="handleSyncTreeNavigation"
/> />
<toolbar <toolbar
v-if="toolbar" v-if="toolbar"
@ -154,7 +158,8 @@ export default {
conductorComponent: undefined, conductorComponent: undefined,
isEditing: false, isEditing: false,
hasToolbar: false, hasToolbar: false,
headExpanded headExpanded,
triggerSync: false
} }
}, },
computed: { computed: {
@ -204,6 +209,9 @@ export default {
} }
this.hasToolbar = structure.length > 0; this.hasToolbar = structure.length > 0;
},
handleSyncTreeNavigation() {
this.triggerSync = !this.triggerSync;
} }
} }
} }

View File

@ -1,10 +1,14 @@
.c-tree-and-search { .c-tree-and-search {
$hoverBg: rgba(#000, 0.1);
$hoverFg: #ccc;
$selected: #fff;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
//TODO: Do we need this??? //TODO: Do we need this???
//padding-right: $interiorMarginSm; //padding-right: $interiorMarginSm;
overflow: auto; width: 100%;
overflow: hidden;
> * + * { margin-top: $interiorMargin; } > * + * { margin-top: $interiorMargin; }
@ -26,6 +30,105 @@
height: 0; // Chrome 73 overflow bug fix height: 0; // Chrome 73 overflow bug fix
padding-right: $interiorMarginSm; 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, .c-tree,
@ -72,8 +175,9 @@
} }
.c-tree { .c-tree {
.c-tree { .c-tree {
margin-left: 15px; padding-left: 15px;
} }
&__item { &__item {
@ -157,3 +261,32 @@
padding: $interiorMargin; 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);}
}

View File

@ -31,9 +31,11 @@
class="c-tree-and-search__tree c-tree" class="c-tree-and-search__tree c-tree"
> >
<tree-item <tree-item
v-for="treeItem in allTreeItems" v-for="item in allTreeItems"
:key="treeItem.id" :key="item.id"
:node="treeItem" :class="childrenSlideClass"
:node="item"
:sync-check="checkForSync"
/> />
</ul> </ul>
<!-- end main tree --> <!-- end main tree -->
@ -44,9 +46,9 @@
class="c-tree-and-search__tree c-tree" class="c-tree-and-search__tree c-tree"
> >
<tree-item <tree-item
v-for="treeItem in filteredTreeItems" v-for="item in filteredTreeItems"
:key="treeItem.id" :key="item.id"
:node="treeItem" :node="item"
/> />
</ul> </ul>
<!-- end search tree --> <!-- end search tree -->
@ -64,26 +66,43 @@ export default {
search, search,
treeItem treeItem
}, },
props: {
syncTreeNavigation: {
type: Boolean,
required: true
}
},
data() { data() {
return { return {
searchValue: '', searchValue: '',
allTreeItems: [], allTreeItems: [],
filteredTreeItems: [], filteredTreeItems: [],
isLoading: false isLoading: false,
childrenSlideClass: 'slide-left',
checkForSync: this.makeHash()
}
},
watch: {
syncTreeNavigation() {
console.log('sync in mct-tree');
this.checkForSync = this.makeHash();
} }
}, },
mounted() { mounted() {
this.searchService = this.openmct.$injector.get('searchService'); this.searchService = this.openmct.$injector.get('searchService');
this.getAllChildren(); this.getAllChildren();
console.log('tree: mounted');
}, },
methods: { methods: {
getAllChildren() { getAllChildren() {
this.isLoading = true; this.isLoading = true;
this.openmct.objects.get('ROOT') this.openmct.objects.get('ROOT')
.then(root => { .then(root => {
console.log('tree: root', root);
return this.openmct.composition.get(root).load() return this.openmct.composition.get(root).load()
}) })
.then(children => { .then(children => {
console.log('tree: children', children);
this.isLoading = false; this.isLoading = false;
this.allTreeItems = children.map(c => { this.allTreeItems = children.map(c => {
return { return {
@ -128,6 +147,14 @@ export default {
if (this.searchValue !== '') { if (this.searchValue !== '') {
this.getFilteredChildren(); 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;
} }
} }
} }

View File

@ -3,17 +3,26 @@
<div <div
class="c-tree__item" class="c-tree__item"
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }" :class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
:style="{ paddingLeft: ancestors * 10 + 10 + 'px' }"
> >
<view-control <view-control
v-model="expanded" v-model="expanded"
class="c-tree__item__view-control" class="c-tree__item__view-control"
:enabled="hasChildren" :enabled="hasChildren && activeChild !== undefined"
:control-class="'c-nav__up'"
@input="resetTreeHere"
/> />
<object-label <object-label
:domain-object="node.object" :domain-object="node.object"
:object-path="node.objectPath" :object-path="node.objectPath"
:navigate-to-path="navigateToPath" :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> </div>
<ul <ul
v-if="expanded" v-if="expanded"
@ -27,11 +36,21 @@
<span class="c-tree__item__label">Loading...</span> <span class="c-tree__item__label">Loading...</span>
</div> </div>
</li> </li>
<tree-item <template
v-for="child in children" 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> </ul>
</li> </li>
</template> </template>
@ -41,6 +60,12 @@ import viewControl from '../components/viewControl.vue';
import ObjectLabel from '../components/ObjectLabel.vue'; import ObjectLabel from '../components/ObjectLabel.vue';
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded'; 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 { export default {
name: 'TreeItem', name: 'TreeItem',
@ -53,6 +78,18 @@ export default {
node: { node: {
type: Object, type: Object,
required: true required: true
},
ancestors: {
type: Number,
default: 0
},
collapseChildren: {
type: String,
default: ''
},
syncCheck: {
type: String,
default: ''
} }
}, },
data() { data() {
@ -63,7 +100,14 @@ export default {
loaded: false, loaded: false,
navigated: this.navigateToPath === this.openmct.router.currentLocation.path, navigated: this.navigateToPath === this.openmct.router.currentLocation.path,
children: [], children: [],
expanded: false expanded: false,
activeChild: undefined,
collapseMyChildren: '',
childrenSlideClass: SLIDE_LEFT,
triggerChildSync: '',
onChildrenLoaded: [],
mountedChildren: [],
onChildMounted: []
} }
}, },
computed: { computed: {
@ -81,23 +125,77 @@ export default {
if (!this.hasChildren) { if (!this.hasChildren) {
return; return;
} }
if (!this.loaded && !this.isLoading) { if(this.expanded) {
this.composition = this.openmct.composition.get(this.domainObject); this.$emit('expanded', this.domainObject);
this.composition.on('add', this.addChild); this.loadChildren();
this.composition.on('remove', this.removeChild);
this.composition.load().then(this.finishLoading);
this.isLoading = true;
} }
this.setLocalStorageExpanded(this.navigateToPath); 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() { mounted() {
// TODO: should update on mutation. let objectComposition = this.openmct.composition.get(this.node.object);
// TODO: click navigation should not fubar hash quite so much.
// TODO: should highlight if navigated to. this.$emit('childState', {
// TODO: should have context menu. type: 'mounted',
// TODO: should support drag/drop composition id: this.openmct.objects.makeKeyString(this.node.object.identifier),
// TODO: set isAlias per tree-item name: this.node.object.name
});
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) => {
@ -105,7 +203,7 @@ export default {
}); });
this.$once('hook:destroyed', removeListener); this.$once('hook:destroyed', removeListener);
if (this.openmct.composition.get(this.node.object)) { if (objectComposition && objectComposition.domainObject.composition.length > 0) {
this.hasChildren = true; this.hasChildren = true;
} }
@ -121,8 +219,14 @@ export default {
*****/ *****/
this.expanded = false; this.expanded = false;
this.setLocalStorageExpanded(); this.setLocalStorageExpanded();
this.activeChild = undefined;
}, },
destroyed() { 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); this.openmct.router.off('change:path', this.highlightIfNavigated);
if (this.composition) { if (this.composition) {
this.composition.off('add', this.addChild); this.composition.off('add', this.addChild);
@ -144,9 +248,26 @@ export default {
this.children = this.children this.children = this.children
.filter(c => c.id !== removeId); .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() { finishLoading() {
this.isLoading = false; this.isLoading = false;
this.loaded = true; this.loaded = true;
// specifically for sync child loading
for(let callback of this.onChildrenLoaded) {
callback();
}
this.onChildrenLoaded = [];
},
triggerChildrenSyncCheck() {
this.triggerChildSync = this.makeHash();
}, },
buildPathString(parentPath) { buildPathString(parentPath) {
return [parentPath, this.openmct.objects.makeKeyString(this.node.object.identifier)].join('/'); return [parentPath, this.openmct.objects.makeKeyString(this.node.object.identifier)].join('/');
@ -160,7 +281,6 @@ export default {
}, },
getLocalStorageExpanded() { getLocalStorageExpanded() {
let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED); let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
if (expandedPaths) { if (expandedPaths) {
expandedPaths = JSON.parse(expandedPaths); expandedPaths = JSON.parse(expandedPaths);
this.expanded = expandedPaths.includes(this.navigateToPath); this.expanded = expandedPaths.includes(this.navigateToPath);
@ -170,7 +290,6 @@ export default {
setLocalStorageExpanded() { setLocalStorageExpanded() {
let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED); let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : []; expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : [];
if (this.expanded) { if (this.expanded) {
if (!expandedPaths.includes(this.navigateToPath)) { if (!expandedPaths.includes(this.navigateToPath)) {
expandedPaths.push(this.navigateToPath); expandedPaths.push(this.navigateToPath);
@ -187,6 +306,39 @@ export default {
expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : []; expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : [];
expandedPaths = expandedPaths.filter(path => !path.startsWith(this.navigateToPath)); expandedPaths = expandedPaths.filter(path => !path.startsWith(this.navigateToPath));
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(expandedPaths)); 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;
} }
} }
} }