Compare commits

...

6 Commits

6 changed files with 368 additions and 37 deletions

View File

@ -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: {

View File

@ -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');
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}