New tree refactor (#3098)

* revised new tree refactor, moved most of the logic to mct-tree instead of tree-item

* scrollTo set for sync, bug fixes, window resize handling

* removing console logs

* checking domainobject composition for length to verify children instead of composition object itself

* added scrollTo on load if in viewed objects directory

* loading, sync bug, search issues, opitmization

* initial PR review updates

* modified so search now uses the same container and virtual scroll

* eslint fix

* Adding new glyphs

- Multiple new glyphs cherrypicked from branch `add-new-glyphs-062320`;

* Styling for new-tree-refactor WIP

- WIP!
- New glyphs, markup changes in BrowseBar.vue;
- Refinements to tree items, WIP;
- TODO: move hard-coded CSS values into _constants, make
theme-compatible;

* Styling for new-tree-refactor WIP

- WIP!
- Added new `c-click-link` CSS class;
- Move tree sync button into tree pane area;
- Added named "controls" slot to pane.vue;
- _up and _down arrows now use visibility instead of opacity to prevent
accidental clicks;

* Styling for new-tree-refactor WIP

- WIP!
- Significant mods and simplification in pane.vue and assoc CSS for
expand/collapse functionality;
- Wait spinner when in tree: cleanups, simplification;

* More new glyphs, updated art

- New glyphs: icon-unlocked and icon-target;
- Updated art for icon-lock glyph;

* remove arrows for search results, hightlight "my items" correctly, added empty folder notic

* Styling for new-tree-refactor WIP

- WIP!
- Refinements to "empty" object element;
- Changed sync-tree icon glyph;

* Styling for new-tree-refactor WIP

- Nav up arrows now left-align properly;

* Styling for new-tree-refactor

- Significant consolidation and cleanups in mct-tree.scss;
- Normalize base and hover styles across new tree, legacy tree,
list-items (used in Notebook) and Folder List View;
- Class naming normalization, change `c-list-item__name-value` to
`c-list-item__name`;
- Add styling to override and remove `<a> outline: dotted` coming from
normalize-min;
- Removed too-broad `<a>` coloring in tables;

* Styling for new-tree-refactor

- Fix styles for Snow theme;
- Sync Maelstrom and Espresso themes;
- Remove too-broad `<a>` hover styling from global.scss;
- Disallow pointer-events on `is-navigated` object's label (click on
c-nav__down element still allowed);

* Styling for new-tree-refactor

- Normalizing status area expand/collapse iconography to match new
approach in panes;

* Adding new glyphs

- Added `icon-items-collapse` and `icon-items-expand`;

* Styling for new-tree-refactor

- Using new glyphs for items expand/collapse in Status area;

* dynamic item height for desktop and mobile views

* lint fixes

* updated addChild and removeChild functions as they were not working at all

* some PR comment updates!;

* Remove unneeded hard-coded CSS color property

* fixed issues when multiple root children exist, added plugin to change the name of the default root object

* removing "my other items" testing references

* linting fixes

* updating karma timeouts for testing purposes

* eslint fixes

* WIP: fixing linting issues

* updating for testing

* set root object provider to update root registry if called more than once

* tweaking tests so that it passes both locally and on the serve tests

* removing old css code preventing context clicks on active menu items

* fixing testing errors

* backwards compatible storage fix

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
This commit is contained in:
Jamie V 2020-08-24 13:47:56 -07:00 committed by GitHub
parent 9e8f845fbe
commit 4801dc4f32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1141 additions and 362 deletions

View File

@ -100,6 +100,6 @@ module.exports = (config) => {
},
concurrency: 1,
singleRun: true,
browserNoActivityTimeout: 90000
browserNoActivityTimeout: 400000
});
};

View File

@ -46,7 +46,7 @@ define([
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
}
/**

View File

@ -20,28 +20,42 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
class RootObjectProvider {
constructor(rootRegistry) {
if (!RootObjectProvider.instance) {
this.rootRegistry = rootRegistry;
this.rootObject = {
identifier: {
key: "ROOT",
namespace: ""
},
name: 'The root object',
type: 'root',
composition: []
};
RootObjectProvider.instance = this;
} else {
// if called twice, update instance rootRegistry
RootObjectProvider.instance.rootRegistry = rootRegistry;
}
function RootObjectProvider(rootRegistry) {
this.rootRegistry = rootRegistry;
return RootObjectProvider.instance; // eslint-disable-line no-constructor-return
}
RootObjectProvider.prototype.get = function () {
return this.rootRegistry.getRoots()
.then(function (roots) {
return {
identifier: {
key: "ROOT",
namespace: ""
},
name: 'The root object',
type: 'root',
composition: roots
};
});
};
updateName(name) {
this.rootObject.name = name;
}
return RootObjectProvider;
});
async get() {
let roots = await this.rootRegistry.getRoots();
this.rootObject.composition = roots;
return this.rootObject;
}
}
function instance(rootRegistry) {
return new RootObjectProvider(rootRegistry);
}
export default instance;

View File

@ -19,34 +19,33 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../RootObjectProvider'
], function (
RootObjectProvider
) {
describe('RootObjectProvider', function () {
let rootRegistry;
let rootObjectProvider;
import RootObjectProvider from '../RootObjectProvider';
beforeEach(function () {
rootRegistry = jasmine.createSpyObj('rootRegistry', ['getRoots']);
rootRegistry.getRoots.and.returnValue(Promise.resolve(['some root']));
rootObjectProvider = new RootObjectProvider(rootRegistry);
});
describe('RootObjectProvider', function () {
// let rootRegistry;
let rootObjectProvider;
let roots = ['some root'];
let rootRegistry = {
getRoots: () => {
return Promise.resolve(roots);
}
};
it('supports fetching root', function () {
return rootObjectProvider.get()
.then(function (root) {
expect(root).toEqual({
identifier: {
key: "ROOT",
namespace: ""
},
name: 'The root object',
type: 'root',
composition: ['some root']
});
});
beforeEach(function () {
rootObjectProvider = new RootObjectProvider(rootRegistry);
});
it('supports fetching root', async () => {
let root = await rootObjectProvider.get();
expect(root).toEqual({
identifier: {
key: "ROOT",
namespace: ""
},
name: 'The root object',
type: 'root',
composition: ['some root']
});
});
});

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import RootObjectProvider from '../../api/objects/RootObjectProvider.js';
export default function (name) {
return function (openmct) {
let rootObjectProvider = new RootObjectProvider();
rootObjectProvider.updateName(name);
};
}

View File

@ -0,0 +1,99 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
xdescribe("the plugin", () => {
let openmct;
let compositionAPI;
let newFolderAction;
let mockObjectPath;
let mockDialogService;
let mockComposition;
let mockPromise;
let newFolderName = 'New Folder';
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => {
return action.key === 'newFolder';
})[0];
});
afterEach(() => {
resetApplicationState(openmct);
});
it('installs the new folder action', () => {
expect(newFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach((done) => {
compositionAPI = openmct.composition;
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
mockPromise = {
then: (callback) => {
callback({name: newFolderName});
done();
}
};
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
mockComposition = jasmine.createSpyObj('composition', ['add']);
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'mutate');
newFolderAction.invoke(mockObjectPath);
});
it('gets user input for folder name', () => {
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
it('creates a new folder object', () => {
expect(openmct.objects.mutate).toHaveBeenCalled();
});
it('adds new folder object to parent composition', () => {
expect(mockComposition.add).toHaveBeenCalled();
});
});
});

View File

@ -13,7 +13,7 @@
cursor: pointer;
&:hover {
background: $colorItemTreeHoverBg;
background: $colorListItemBgHov;
filter: $filterHov;
transition: $transIn;
}

View File

@ -54,7 +54,8 @@ define([
'./themes/snow',
'./URLTimeSettingsSynchronizer/plugin',
'./notificationIndicator/plugin',
'./newFolderAction/plugin'
'./newFolderAction/plugin',
'./defaultRootName/plugin'
], function (
_,
UTCTimeSystem,
@ -89,7 +90,8 @@ define([
Snow,
URLTimeSettingsSynchronizer,
NotificationIndicator,
NewFolderAction
NewFolderAction,
DefaultRootName
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@ -201,6 +203,7 @@ define([
plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default;
plugins.NotificationIndicator = NotificationIndicator.default;
plugins.NewFolderAction = NewFolderAction.default;
plugins.DefaultRootName = DefaultRootName.default;
return plugins;
});

View File

@ -3,8 +3,8 @@
}
@keyframes rotation-centered {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes clock-hands {

View File

@ -80,8 +80,8 @@ $uiColor: #0093ff; // Resize bars, splitter bars, etc.
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
$filterHov: brightness(1.3); // Tree, location items
$colorSelectedBg: pushBack($colorKey, 10%);
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%);
// Object labels
@ -361,13 +361,14 @@ $legendTableHeadBg: $colorTabHeaderBg;
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: rgba(white, 0.07);
$colorItemTreeHoverFg: pullForward($colorBodyFg, 20%);
$colorItemTreeHoverBg: rgba(#fff, 0.03);
$colorItemTreeHoverFg: #fff;
$colorItemTreeIcon: $colorKey; // Used
$colorItemTreeIconHover: $colorItemTreeIcon; // Used
$colorItemTreeFg: $colorBodyFg;
$colorItemTreeSelectedBg: $colorSelectedBg;
$colorItemTreeSelectedFg: $colorItemTreeHoverFg;
$filterItemTreeSelected: $filterHov;
$colorItemTreeSelectedIcon: $colorItemTreeSelectedFg;
$colorItemTreeEditingBg: pushBack($editUIColor, 20%);
$colorItemTreeEditingFg: $editUIColor;
@ -402,7 +403,7 @@ $splitterBtnColorBg: $colorBtnBg;
$splitterBtnColorFg: #999;
$splitterBtnLabelColorFg: #666;
$splitterCollapsedBtnColorBg: #222;
$splitterCollapsedBtnColorFg: #666;
$splitterCollapsedBtnColorFg: #555;
$splitterCollapsedBtnColorBgHov: $colorKey;
$splitterCollapsedBtnColorFgHov: $colorKeyFg;

View File

@ -80,12 +80,12 @@ $colorKeyHov: #26d8ff;
$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) contrast(101%);
$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) contrast(100%);
$colorKeySelectedBg: $colorKey;
$uiColor: #00b2ff; // Resize bars, splitter bars, etc.
$uiColor: #0093ff; // Resize bars, splitter bars, etc.
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
$filterHov: brightness(1.3); // Tree, location items
$colorSelectedBg: pushBack($colorKey, 10%);
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%);
// Object labels
@ -365,13 +365,14 @@ $legendTableHeadBg: rgba($colorBodyFg, 0.15);
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: rgba(white, 0.07);
$colorItemTreeHoverFg: pullForward($colorBodyFg, 20%);
$colorItemTreeHoverBg: rgba(#fff, 0.03);
$colorItemTreeHoverFg: #fff;
$colorItemTreeIcon: $colorKey; // Used
$colorItemTreeIconHover: $colorItemTreeIcon; // Used
$colorItemTreeFg: $colorBodyFg;
$colorItemTreeSelectedBg: $colorSelectedBg;
$colorItemTreeSelectedFg: $colorItemTreeHoverFg;
$filterItemTreeSelected: $filterHov;
$colorItemTreeSelectedIcon: $colorItemTreeSelectedFg;
$colorItemTreeEditingBg: pushBack($editUIColor, 20%);
$colorItemTreeEditingFg: $editUIColor;
@ -406,7 +407,7 @@ $splitterBtnColorBg: $colorBtnBg;
$splitterBtnColorFg: #999;
$splitterBtnLabelColorFg: #666;
$splitterCollapsedBtnColorBg: #222;
$splitterCollapsedBtnColorFg: #666;
$splitterCollapsedBtnColorFg: #555;
$splitterCollapsedBtnColorBgHov: $colorKey;
$splitterCollapsedBtnColorFgHov: $colorKeyFg;

View File

@ -78,10 +78,10 @@ $colorKeyFilterHov: invert(69%) sepia(87%) saturate(3243%) hue-rotate(151deg) br
$colorKeySelectedBg: $colorKey;
$uiColor: #289fec; // Resize bars, splitter bars, etc.
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #999;
$colorA: $colorBodyFg;
$colorAHov: $colorKey;
$filterHov: brightness(0.8) contrast(2); // Tree, location items
$colorSelectedBg: pushBack($colorKey, 40%);
$colorSelectedBg: rgba($colorKey, 0.2);
$colorSelectedFg: pullForward($colorBodyFg, 10%);
// Object labels
@ -94,7 +94,7 @@ $shellPanePad: $interiorMargin, 7px;
$drawerBg: darken($colorBodyBg, 5%);
$drawerFg: darken($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba(black, 0.25);
$sideBarHeaderBg: rgba(black, 0.1);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols
@ -368,7 +368,8 @@ $colorItemTreeIconHover: $colorItemTreeIcon; // Used
$colorItemTreeFg: $colorBodyFg;
$colorItemTreeSelectedBg: $colorSelectedBg;
$colorItemTreeSelectedFg: $colorItemTreeHoverFg;
$colorItemTreeSelectedIcon: $colorItemTreeSelectedFg;
$filterItemTreeSelected: contrast(1.4);
$colorItemTreeSelectedIcon: $colorItemTreeIcon;
$colorItemTreeEditingBg: pushBack($editUIColor, 20%);
$colorItemTreeEditingFg: $editUIColor;
$colorItemTreeEditingIcon: $editUIColor;

View File

@ -44,6 +44,7 @@ $overlayOuterMarginFullscreen: 0%;
$overlayOuterMarginDialog: 20%;
$overlayInnerMargin: 25px;
$mainViewPad: 0px;
$treeNavArrowD: 20px;
/*************** Items */
$itemPadLR: 5px;
$gridItemDesk: 175px;
@ -81,8 +82,8 @@ $formLabelMinW: 120px;
$formLabelW: 30%;
/*************** Wait Spinner */
$waitSpinnerD: 32px;
$waitSpinnerTreeD: 20px;
$waitSpinnerBorderW: 5px;
$waitSpinnerTreeD: 20px;
$waitSpinnerTreeBorderW: 3px;
/*************** Messages */
$messageIconD: 80px;

View File

@ -97,7 +97,8 @@ button {
}
}
.c-click-link {
.c-click-link,
.c-icon-link {
// A clickable element, typically inline, with an icon and label
@include cControl();
cursor: pointer;
@ -112,8 +113,15 @@ button {
}
}
.c-icon-link {
&:before {
// Icon
//color: $colorBtnMajorBg;
}
}
.c-icon-button {
.c-icon-button__label {
&__label {
margin-left: $interiorMargin;
}

View File

@ -101,8 +101,9 @@ a {
color: $colorA;
cursor: pointer;
text-decoration: none;
&:hover {
color: $colorAHov;
&:focus {
outline: none !important;
}
}
@ -280,19 +281,23 @@ body.desktop .has-local-controls {
display: flex;
align-items: center;
padding-left: $spinnerL + $d/2 + $interiorMargin;
background: $colorLoadingBg;
margin-left: $treeNavArrowD + $interiorMargin;
min-height: 5px + $d;
.c-tree__item__label {
font-style: italic;
margin-left: $interiorMargin;
opacity: 0.6;
}
&:before {
left: auto;
top: auto;
transform: translate(0);
height: $d;
width: $d;
border-width: 4px;
left: $spinnerL;
border-width: 3px;
//left: $spinnerL;
position: relative;
}
&:after {
display: none;

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

@ -3,7 +3,8 @@
<div class="l-browse-bar__start">
<button
v-if="hasParent"
class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-pointer-left"
class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-arrow-nav-to-parent"
title="Navigate up to parent"
@click="goToParent"
></button>
<div

View File

@ -15,7 +15,9 @@
<CreateButton class="l-shell__create-button" />
<indicators class="l-shell__head-section l-shell__indicators" />
<button
class="l-shell__head__collapse-button c-button"
class="l-shell__head__collapse-button c-icon-button"
:class="headExpanded ? 'l-shell__head__collapse-button--collapse' : 'l-shell__head__collapse-button--expand'"
:title="`Click to ${headExpanded ? 'collapse' : 'expand'} items`"
@click="toggleShellHead"
></button>
<notification-banner />
@ -47,12 +49,23 @@
label="Browse"
collapsable
>
<mct-tree class="l-shell__tree" />
<button
slot="controls"
class="c-icon-button l-shell__sync-tree-button icon-target"
title="Show selected item in tree"
@click="handleSyncTreeNavigation"
>
</button>
<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"
@sync-tree-navigation="handleSyncTreeNavigation"
/>
<toolbar
v-if="toolbar"
@ -126,7 +139,8 @@ export default {
conductorComponent: undefined,
isEditing: false,
hasToolbar: false,
headExpanded
headExpanded,
triggerSync: false
};
},
computed: {
@ -200,6 +214,9 @@ export default {
}
this.hasToolbar = structure.length > 0;
},
handleSyncTreeNavigation() {
this.triggerSync = !this.triggerSync;
}
}
};

View File

@ -52,7 +52,7 @@
color: $colorKey !important;
position: absolute;
right: -18px;
top: 0;
top: $interiorMarginSm;
transform: translateX(100%);
width: $mobileMenuIconD;
z-index: 2;
@ -100,6 +100,11 @@
&__pane-tree {
background: linear-gradient(90deg, transparent 70%, rgba(black, 0.2) 99%, rgba(black, 0.3));
[class*="expand-button"],
[class*="sync-tree-button"] {
display: none;
}
&[class*="--collapsed"] {
[class*="collapse-button"] {
right: -8px;
@ -153,7 +158,7 @@
}
&__head {
align-items: stretch;
align-items: center;
background: $colorHeadBg;
justify-content: space-between;
padding: $interiorMargin $interiorMargin + 2;
@ -162,14 +167,21 @@
margin-left: $interiorMargin;
}
[class*='__head__collapse-button'] {
align-self: start;
.l-shell__head__collapse-button {
color: $colorBtnMajorBg;
flex: 0 0 auto;
margin-top: 6px;
font-size: 0.9em;
&:before {
content: $glyph-icon-arrow-down;
font-size: 1.1em;
&--collapse {
&:before {
content: $glyph-icon-items-collapse;
}
}
&--expand {
&:before {
content: $glyph-icon-items-expand;
}
}
}
@ -184,12 +196,6 @@
.c-indicator__label {
transition: none !important;
}
[class*='__head__collapse-button'] {
&:before {
transform: rotate(180deg);
}
}
}
}
@ -304,6 +310,10 @@
display: inline-flex;
}
> * + * {
margin-left: $interiorMarginSm;
}
&__start,
&__end,
&__actions {
@ -327,8 +337,12 @@
&__start {
flex: 1 1 auto;
margin-right: $interiorMargin;
//margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
[class*='button'] {
flex: 0 0 auto;
}
}
&__end {
@ -337,15 +351,15 @@
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
//flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
//$p: $interiorMargin;
//margin-right: $interiorMargin;
//padding-left: $p;
//padding-right: $p;
.is-editing & {
display: none;
@ -362,7 +376,8 @@
}
&__object-name--w {
@include headerFont(1.4em);
@include headerFont(1.5em);
margin-left: $interiorMarginLg;
min-width: 0;
.is-missing__indicator {

View File

@ -12,10 +12,6 @@
flex: 0 0 auto;
}
&__loading {
flex: 1 1 auto;
}
&__no-results {
font-style: italic;
opacity: 0.6;
@ -26,6 +22,33 @@
height: 0; // Chrome 73 overflow bug fix
padding-right: $interiorMarginSm;
}
.c-tree {
flex: 1 1 auto;
overflow: hidden;
transition: all;
.scrollable-children {
.c-tree__item-h {
width: 100%;
}
}
&__item--empty {
// Styling for empty tree items
// Indent should allow for c-nav view-control width and icon spacing
font-style: italic;
padding: $interiorMarginSm * 2 1px;
opacity: 0.7;
pointer-events: none;
&:before {
content: '';
display: inline-block;
width: $treeNavArrowD + $interiorMarginLg;
}
}
}
}
.c-tree,
@ -43,7 +66,6 @@
}
&__item {
$aPad: $interiorMarginSm;
border-radius: $controlCr;
display: flex;
align-items: center;
@ -82,22 +104,9 @@
margin-left: $interiorMarginSm;
}
&.is-navigated-object,
&.is-selected {
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
}
}
&.is-being-edited {
background: $colorItemTreeEditingBg;
.c-tree__item__type-icon:before {
color: $colorItemTreeEditingIcon;
}
.c-tree__item__name {
color: $colorItemTreeEditingFg;
font-style: italic;
@include desktop {
&:hover {
background: $colorItemTreeHoverBg;
}
}
@ -106,10 +115,6 @@
flex: 1 1 auto;
}
&__name {
color: $colorItemTreeFg;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
@ -125,6 +130,55 @@
width: ceil($mobileTreeItemH * 0.5);
}
}
&.is-navigated-object,
&.is-selected {
background: $colorItemTreeSelectedBg;
[class*="__label"],
[class*="__name"] {
color: $colorItemTreeSelectedFg;
}
[class*="__type-icon"]:before {
color: $colorItemTreeSelectedIcon;
}
}
}
&__item__label {
@include desktop {
&:hover {
filter: $filterHov;
}
}
}
}
.is-editing .is-navigated-object {
a[class*="__item__label"] {
opacity: 0.4;
[class*="__name"] {
font-style: italic;
}
}
}
.c-tree {
&__item {
body.mobile & {
@include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg);
height: $mobileTreeItemH;
margin-bottom: $interiorMarginSm;
[class*="view-control"] {
width: ceil($mobileTreeItemH * 0.5);
}
}
}
.c-tree {
margin-left: $treeItemIndent;
}
}
@ -141,6 +195,51 @@
}
}
.c-nav {
$dimension: $treeNavArrowD;
&__up, &__down {
flex: 0 0 auto;
height: $dimension;
width: $dimension;
visibility: hidden;
position: relative;
text-align: center;
&.is-enabled {
visibility: visible;
}
&:before {
// Nav arrow
$dimension: 9px;
$width: 3px;
border: solid $colorItemTreeVC;
border-width: 0 $width $width 0;
content: '';
display: block;
position: absolute;
left: 50%; top: 50%;
height: $dimension;
width: $dimension;
}
@include desktop {
&:hover:before {
border-color: $colorItemTreeHoverFg;
}
}
}
&__up:before {
transform: translate(-30%, -50%) rotate(135deg);
}
&__down:before {
transform: translate(-70%, -50%) rotate(-45deg);
}
}
.c-selector {
.c-tree-and-search__tree.c-tree {
border: 1px solid $colorInteriorBorder;
@ -148,3 +247,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

@ -1,5 +1,8 @@
<template>
<div class="c-tree-and-search">
<div
class="c-tree-and-search"
>
<div class="c-tree-and-search__search">
<search
ref="shell-search"
@ -10,15 +13,8 @@
/>
</div>
<!-- loading -->
<div
v-if="isLoading"
class="c-tree-and-search__loading loading"
></div>
<!-- end loading -->
<div
v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
v-if="(searchValue && allTreeItems.length === 0 && !isLoading) || (searchValue && searchResultItems.length === 0)"
class="c-tree-and-search__no-results"
>
No results found
@ -26,30 +22,72 @@
<!-- main tree -->
<ul
v-if="!isLoading"
v-show="!searchValue"
ref="mainTree"
class="c-tree-and-search__tree c-tree"
>
<tree-item
v-for="treeItem in allTreeItems"
:key="treeItem.id"
:node="treeItem"
/>
<!-- ancestors -->
<div v-if="!activeSearch">
<tree-item
v-for="(ancestor, index) in ancestors"
:key="ancestor.id"
:node="ancestor"
:show-up="index < ancestors.length - 1"
:show-down="false"
:left-offset="index * 10 + 'px'"
:emit-height="getChildHeight"
@emittedHeight="setChildHeight"
@resetTree="handleReset"
/>
<!-- loading -->
<li
v-if="isLoading"
class="c-tree__item c-tree-and-search__loading loading"
>
<span class="c-tree__item__label">Loading...</span>
</li>
<!-- end loading -->
</div>
<!-- currently viewed children -->
<transition
@enter="childrenIn"
>
<li
v-if="!isLoading"
:class="childrenSlideClass"
:style="childrenListStyles()"
>
<ul
ref="scrollable"
class="scrollable-children"
:style="scrollableStyles()"
@scroll="scrollItems"
>
<div :style="{ height: childrenHeight + 'px'}">
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="treeItem.id"
:node="treeItem"
:left-offset="itemLeftOffset"
:item-offset="itemOffset"
:item-index="index"
:item-height="itemHeight"
:virtual-scroll="!noScroll"
:show-down="activeSearch ? false : true"
@expanded="handleExpanded"
/>
<li
v-if="visibleItems.length === 0"
:style="emptyStyles()"
class="c-tree__item c-tree__item--empty"
>
No items
</li>
</div>
</ul>
</li>
</transition>
</ul>
<!-- end main tree -->
<!-- search tree -->
<ul
v-if="searchValue"
class="c-tree-and-search__tree c-tree"
>
<tree-item
v-for="treeItem in filteredTreeItems"
:key="treeItem.id"
:node="treeItem"
/>
</ul>
<!-- end search tree -->
</div>
</template>
@ -57,6 +95,14 @@
import treeItem from './tree-item.vue';
import search from '../components/search.vue';
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
const ROOT_PATH = '/browse/';
const ITEM_BUFFER = 5;
const RECHECK_DELAY = 100;
const RESIZE_FIRE_DELAY_MS = 500;
let windowResizeId = undefined;
let windowResizing = false;
export default {
inject: ['openmct'],
name: 'MctTree',
@ -64,75 +110,518 @@ export default {
search,
treeItem
},
props: {
syncTreeNavigation: {
type: Boolean,
required: true
}
},
data() {
let isMobile = this.openmct.$injector.get('agentService');
return {
isLoading: false,
searchValue: '',
allTreeItems: [],
filteredTreeItems: [],
isLoading: false
searchResultItems: [],
visibleItems: [],
ancestors: [],
childrenSlideClass: 'slide-left',
availableContainerHeight: 0,
noScroll: true,
updatingView: false,
itemHeight: 28,
itemOffset: 0,
childrenHeight: 0,
scrollable: undefined,
pageThreshold: 50,
activeSearch: false,
getChildHeight: false,
settingChildrenHeight: false,
isMobile: isMobile.mobileName,
multipleRootChildren: false
};
},
mounted() {
computed: {
currentNavigatedPath() {
let ancestorsCopy = [...this.ancestors];
if (this.multipleRootChildren) {
ancestorsCopy.shift(); // remove root
}
return ancestorsCopy
.map((ancestor) => ancestor.id)
.join('/');
},
currentObjectPath() {
let ancestorsCopy = [...this.ancestors];
return ancestorsCopy
.reverse()
.map((ancestor) => ancestor.object);
},
focusedItems() {
return this.activeSearch ? this.searchResultItems : this.allTreeItems;
},
itemLeftOffset() {
return this.activeSearch ? '0px' : this.ancestors.length * 10 + 'px';
}
},
watch: {
syncTreeNavigation() {
const AND_SAVE_PATH = true;
let currentLocationPath = this.openmct.router.currentLocation.path;
let hasParent = this.currentlyViewedObjectParentPath() || (this.multipleRootChildren && !this.currentlyViewedObjectParentPath());
let jumpAndScroll = currentLocationPath
&& hasParent
&& !this.currentPathIsActivePath();
let justScroll = this.currentPathIsActivePath() && !this.noScroll;
if (this.searchValue) {
this.searchValue = '';
}
if (jumpAndScroll) {
this.scrollTo = this.currentlyViewedObjectId();
this.allTreeItems = [];
this.jumpPath = this.currentlyViewedObjectParentPath();
if (this.multipleRootChildren) {
if (!this.jumpPath) {
this.jumpPath = 'ROOT';
this.ancestors = [];
} else {
this.ancestors = [this.ancestors[0]];
}
} else {
this.ancestors = [];
}
this.jumpToPath(AND_SAVE_PATH);
} else if (justScroll) {
this.scrollTo = this.currentlyViewedObjectId();
this.autoScroll();
}
},
searchValue() {
if (this.searchValue !== '' && !this.activeSearch) {
this.searchActivated();
} else if (this.searchValue === '') {
this.searchDeactivated();
}
},
searchResultItems() {
this.setContainerHeight();
}
},
async mounted() {
let savedPath = this.getSavedNavigatedPath();
this.searchService = this.openmct.$injector.get('searchService');
this.getAllChildren();
window.addEventListener('resize', this.handleWindowResize);
let root = await this.openmct.objects.get('ROOT');
if (root.identifier !== undefined) {
let rootNode = this.buildTreeItem(root);
// if more than one root item, set multipleRootChildren to true and add root to ancestors
if (root.composition && root.composition.length > 1) {
this.ancestors.push(rootNode);
this.multipleRootChildren = true;
} else if (!savedPath && root.composition[0] !== undefined) {
// needed if saved path is not set, need to set it to the only root child
savedPath = root.composition[0];
}
if (savedPath) {
let scrollIfApplicable = () => {
if (this.currentPathIsActivePath()) {
this.scrollTo = this.currentlyViewedObjectId();
}
};
this.jumpPath = savedPath;
this.afterJump = scrollIfApplicable;
}
this.getAllChildren(rootNode);
}
},
destroyed() {
window.removeEventListener('resize', this.handleWindowResize);
},
methods: {
getAllChildren() {
this.isLoading = true;
this.openmct.objects.get('ROOT')
.then(root => {
let composition = this.openmct.composition.get(root);
if (composition !== undefined) {
return composition.load();
} else {
return [];
updatevisibleItems() {
if (this.updatingView) {
return;
}
this.updatingView = true;
requestAnimationFrame(() => {
let start = 0;
let end = this.pageThreshold;
let allItemsCount = this.focusedItems.length;
if (allItemsCount < this.pageThreshold) {
end = allItemsCount;
} else {
let firstVisible = this.calculateFirstVisibleItem();
let lastVisible = this.calculateLastVisibleItem();
let totalVisible = lastVisible - firstVisible;
let numberOffscreen = this.pageThreshold - totalVisible;
start = firstVisible - Math.floor(numberOffscreen / 2);
end = lastVisible + Math.ceil(numberOffscreen / 2);
if (start < 0) {
start = 0;
end = Math.min(this.pageThreshold, allItemsCount);
} else if (end >= allItemsCount) {
end = allItemsCount;
start = end - this.pageThreshold + 1;
}
})
.then(children => {
this.isLoading = false;
this.allTreeItems = children.map(c => {
return {
id: this.openmct.objects.makeKeyString(c.identifier),
object: c,
objectPath: [c],
navigateToParent: '/browse'
};
});
});
}
this.itemOffset = start;
this.visibleItems = this.focusedItems.slice(start, end);
this.updatingView = false;
});
},
getFilteredChildren() {
this.searchService.query(this.searchValue).then(children => {
this.filteredTreeItems = children.hits.map(child => {
async setContainerHeight() {
await this.$nextTick();
let mainTree = this.$refs.mainTree;
let mainTreeHeight = mainTree.clientHeight;
let context = child.object.getCapability('context');
let object = child.object.useCapability('adapter');
let objectPath = [];
let navigateToParent;
if (mainTreeHeight !== 0) {
this.calculateChildHeight(() => {
let ancestorsHeight = this.calculateAncestorHeight();
let allChildrenHeight = this.calculateChildrenHeight();
if (context) {
objectPath = context.getPath().slice(1)
.map(oldObject => oldObject.useCapability('adapter'))
.reverse();
navigateToParent = '/browse/' + objectPath.slice(1)
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
.join('/');
if (this.activeSearch) {
ancestorsHeight = 0;
}
return {
id: this.openmct.objects.makeKeyString(object.identifier),
object,
objectPath,
navigateToParent
};
this.availableContainerHeight = mainTreeHeight - ancestorsHeight;
if (allChildrenHeight > this.availableContainerHeight) {
this.setPageThreshold();
this.noScroll = false;
} else {
this.noScroll = true;
}
this.updatevisibleItems();
});
} else {
window.setTimeout(this.setContainerHeight, RECHECK_DELAY);
}
},
calculateFirstVisibleItem() {
let scrollTop = this.$refs.scrollable.scrollTop;
return Math.floor(scrollTop / this.itemHeight);
},
calculateLastVisibleItem() {
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;
return (this.itemHeight * childrenCount) - mainTreeTopMargin; // 5px margin
},
setChildrenHeight() {
this.childrenHeight = this.calculateChildrenHeight();
},
calculateAncestorHeight() {
let ancestorCount = this.ancestors.length;
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) {
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.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;
}
},
handleWindowResize() {
if (!windowResizing) {
windowResizing = true;
window.clearTimeout(windowResizeId);
windowResizeId = window.setTimeout(() => {
this.setContainerHeight();
windowResizing = false;
}, RESIZE_FIRE_DELAY_MS);
}
},
async getAllChildren(node) {
this.isLoading = true;
if (this.composition) {
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
delete this.composition;
}
this.allTreeItems = [];
this.composition = this.openmct.composition.get(node.object);
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
await this.composition.load();
this.finishLoading();
},
buildTreeItem(domainObject) {
let navToParent = ROOT_PATH + this.currentNavigatedPath;
if (navToParent === ROOT_PATH) {
navToParent = navToParent.slice(0, -1);
}
return {
id: this.openmct.objects.makeKeyString(domainObject.identifier),
object: domainObject,
objectPath: [domainObject].concat(this.currentObjectPath),
navigateToParent: navToParent
};
},
addChild(child) {
let item = this.buildTreeItem(child);
this.allTreeItems.push(item);
if (!this.isLoading) {
this.setContainerHeight();
}
},
removeChild(identifier) {
let removeId = this.openmct.objects.makeKeyString(identifier);
this.allTreeItems = this.allTreeItems
.filter(c => c.id !== removeId);
this.setContainerHeight();
},
finishLoading() {
if (this.jumpPath) {
this.jumpToPath();
}
this.autoScroll();
this.isLoading = false;
},
async jumpToPath(saveExpandedPath = false) {
// check for older implementations of tree storage and reformat if necessary
if (Array.isArray(this.jumpPath)) {
this.jumpPath = this.jumpPath[0];
}
let nodes = this.jumpPath.split('/');
for (let i = 0; i < nodes.length; i++) {
let currentNode = await this.openmct.objects.get(nodes[i]);
let newParent = this.buildTreeItem(currentNode);
this.ancestors.push(newParent);
if (i === nodes.length - 1) {
this.jumpPath = '';
this.getAllChildren(newParent);
if (this.afterJump) {
await this.$nextTick();
this.afterJump();
delete this.afterJump;
}
if (saveExpandedPath) {
this.setCurrentNavigatedPath();
}
}
}
},
async autoScroll() {
if (!this.scrollTo) {
return;
}
if (this.$refs.scrollable) {
let indexOfScroll = this.indexOfItemById(this.scrollTo);
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);
}
},
indexOfItemById(id) {
for (let i = 0; i < this.allTreeItems.length; i++) {
if (this.allTreeItems[i].id === id) {
return i;
}
}
},
async getSearchResults() {
let results = await this.searchService.query(this.searchValue);
this.searchResultItems = results.hits.map(result => {
let context = result.object.getCapability('context');
let object = result.object.useCapability('adapter');
let objectPath = [];
let navigateToParent;
if (context) {
objectPath = context.getPath().slice(1)
.map(oldObject => oldObject.useCapability('adapter'))
.reverse();
navigateToParent = objectPath.slice(1)
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier));
navigateToParent = ROOT_PATH + navigateToParent.reverse().join('/');
}
return {
id: this.openmct.objects.makeKeyString(object.identifier),
object,
objectPath,
navigateToParent
};
});
},
searchTree(value) {
this.searchValue = value;
if (this.searchValue !== '') {
this.getFilteredChildren();
this.getSearchResults();
}
},
searchActivated() {
this.activeSearch = true;
this.$refs.scrollable.scrollTop = 0;
},
searchDeactivated() {
this.activeSearch = false;
this.$refs.scrollable.scrollTop = 0;
this.setContainerHeight();
},
handleReset(node) {
this.childrenSlideClass = 'slide-right';
this.ancestors.splice(this.ancestors.indexOf(node) + 1);
this.getAllChildren(node);
this.setCurrentNavigatedPath();
},
handleExpanded(node) {
if (this.activeSearch) {
return;
}
this.childrenSlideClass = 'slide-left';
let newParent = this.buildTreeItem(node);
this.ancestors.push(newParent);
this.getAllChildren(newParent);
this.setCurrentNavigatedPath();
},
getSavedNavigatedPath() {
return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED));
},
setCurrentNavigatedPath() {
if (!this.searchValue) {
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(this.currentNavigatedPath));
}
},
currentPathIsActivePath() {
return this.getSavedNavigatedPath() === this.currentlyViewedObjectParentPath();
},
currentlyViewedObjectId() {
let currentPath = this.openmct.router.currentLocation.path;
if (currentPath) {
currentPath = currentPath.split(ROOT_PATH)[1];
return currentPath.split('/').pop();
}
},
currentlyViewedObjectParentPath() {
let currentPath = this.openmct.router.currentLocation.path;
if (currentPath) {
currentPath = currentPath.split(ROOT_PATH)[1];
currentPath = currentPath.split('/');
currentPath.pop();
return currentPath.join('/');
}
},
scrollItems(event) {
if (!windowResizing) {
this.updatevisibleItems();
}
},
childrenListStyles() {
return { position: 'relative' };
},
scrollableStyles() {
return {
height: this.availableContainerHeight + 'px',
overflow: this.noScroll ? 'hidden' : 'scroll'
};
},
emptyStyles() {
let offset = ((this.ancestors.length + 1) * 10);
return {
paddingLeft: offset + 'px'
};
},
childrenIn(el, done) {
// still needing this timeout for some reason
window.setTimeout(this.setContainerHeight, RECHECK_DELAY);
done();
},
getElementStyleValue(el, style) {
let styleString = window.getComputedStyle(el)[style];
let index = styleString.indexOf('px');
return Number(styleString.slice(0, index));
}
}
};

View File

@ -40,6 +40,10 @@
display: flex;
align-items: center;
@include desktop() { margin-bottom: $interiorMargin; }
[class*="button"] {
color: $colorBtnMajorBg;
}
}
&--reacts {
@ -128,12 +132,23 @@
@include userSelectNone();
color: $splitterBtnLabelColorFg;
display: block;
pointer-events: none;
text-transform: uppercase;
transform-origin: top left;
flex: 1 1 auto;
}
[class*="expand-button"] {
display: none; // Hidden by default
background: $splitterCollapsedBtnColorBg;
color: $splitterCollapsedBtnColorFg;
font-size: 0.9em;
&:hover {
background: $splitterCollapsedBtnColorBgHov;
color: inherit;
transition: $transIn;
}
}
&--resizing {
// User is dragging the handle and resizing a pane
@include userSelectNone();
@ -160,23 +175,12 @@
display: none;
}
.l-pane__header {
&:hover {
color: $splitterCollapsedBtnColorFgHov;
.l-pane__label {
color: inherit;
}
.l-pane__collapse-button {
background: $splitterCollapsedBtnColorBgHov;
color: inherit;
transition: $transIn;
}
}
[class*="collapse-button"] {
display: none;
}
.l-pane__collapse-button {
background: $splitterCollapsedBtnColorBg;
color: $splitterCollapsedBtnColorFg;
[class*="expand-button"] {
display: block;
}
}
@ -198,36 +202,26 @@
.l-pane__collapse-button {
&:before {
content: $glyph-icon-arrow-right-equilateral;
content: $glyph-icon-line-horz;
}
}
&[class*="--collapsed"] {
/************************ COLLAPSED HORIZONTAL SPLITTER, EITHER DIRECTION */
[class*="__header"] {
@include abs();
margin: 0;
display: none;
}
[class*="label"] {
position: absolute;
transform: translate($interiorMarginLg + 1, 18px) rotate(90deg);
left: 3px;
top: 0;
z-index: 1;
}
.l-pane__collapse-button {
border-top-left-radius: 0;
border-bottom-left-radius: 0; // Only have to do this once, because of scaleX(-1) below.
[class*="expand-button"] {
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
height: auto; width: 100%;
padding: 0;
padding: $interiorMargin 2px;
&:before {
position: absolute;
top: 5px;
[class*="label"] {
text-orientation: mixed;
text-transform: uppercase;
writing-mode: vertical-lr;
}
}
}
@ -243,10 +237,9 @@
transform: translateX(floor($splitterHandleD / -2)); // Center over the pane edge
}
&[class*="--collapsed"] {
.l-pane__collapse-button {
transform: scaleX(-1);
}
[class*="expand-button"] {
border-top-left-radius: $controlCr;
border-bottom-left-radius: $controlCr;
}
}
@ -261,10 +254,9 @@
transform: translateX(floor($splitterHandleD / 2));
}
&:not([class*="--collapsed"]) {
.l-pane__collapse-button {
transform: scaleX(-1);
}
[class*="expand-button"] {
border-top-right-radius: $controlCr;
border-bottom-right-radius: $controlCr;
}
}
}

View File

@ -20,12 +20,19 @@
<span v-if="label"
class="l-pane__label"
>{{ label }}</span>
<slot name="controls"></slot>
<button
v-if="collapsable"
class="l-pane__collapse-button c-button"
class="l-pane__collapse-button c-icon-button"
@click="toggleCollapse"
></button>
</div>
<button
class="l-pane__expand-button"
@click="toggleCollapse"
>
<span class="l-pane__expand-button__label">{{ label }}</span>
</button>
<div class="l-pane__contents">
<slot></slot>
</div>

View File

@ -1,38 +1,39 @@
<template>
<li class="c-tree__item-h">
<li
ref="me"
:style="{
'top': virtualScroll ? itemTop : 'auto',
'position': virtualScroll ? 'absolute' : 'relative'
}"
class="c-tree__item-h"
>
<div
class="c-tree__item"
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
:class="{
'is-alias': isAlias,
'is-navigated-object': navigated
}"
>
<view-control
v-model="expanded"
class="c-tree__item__view-control"
:enabled="hasChildren"
:control-class="'c-nav__up'"
:enabled="showUp"
@input="resetTreeHere"
/>
<object-label
:domain-object="node.object"
:object-path="node.objectPath"
:navigate-to-path="navigateToPath"
:style="{ paddingLeft: leftOffset }"
/>
<view-control
v-model="expanded"
class="c-tree__item__view-control"
:control-class="'c-nav__down'"
:enabled="hasComposition && showDown"
/>
</div>
<ul
v-if="expanded"
class="c-tree"
>
<li
v-if="isLoading && !loaded"
class="c-tree__item-h"
>
<div class="c-tree__item loading">
<span class="c-tree__item__label">Loading...</span>
</div>
</li>
<tree-item
v-for="child in children"
:key="child.id"
:node="child"
/>
</ul>
</li>
</template>
@ -40,8 +41,6 @@
import viewControl from '../components/viewControl.vue';
import ObjectLabel from '../components/ObjectLabel.vue';
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
export default {
name: 'TreeItem',
inject: ['openmct'],
@ -53,17 +52,49 @@ export default {
node: {
type: Object,
required: true
},
leftOffset: {
type: String,
default: '0px'
},
showUp: {
type: Boolean,
default: false
},
showDown: {
type: Boolean,
default: true
},
itemIndex: {
type: Number,
required: false,
default: undefined
},
itemOffset: {
type: Number,
required: false,
default: undefined
},
itemHeight: {
type: Number,
required: false,
default: 0
},
virtualScroll: {
type: Boolean,
default: false
},
emitHeight: {
type: Boolean,
default: false
}
},
data() {
this.navigateToPath = this.buildPathString(this.node.navigateToParent);
return {
hasChildren: false,
isLoading: false,
loaded: false,
hasComposition: false,
navigated: this.navigateToPath === this.openmct.router.currentLocation.path,
children: [],
expanded: false
};
},
@ -77,32 +108,23 @@ export default {
let parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
return parentKeyString !== this.node.object.location;
},
itemTop() {
return (this.itemOffset + this.itemIndex) * this.itemHeight + 'px';
}
},
watch: {
expanded() {
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;
}
this.setLocalStorageExpanded(this.navigateToPath);
this.$emit('expanded', this.domainObject);
},
emitHeight() {
this.$nextTick(() => {
this.$emit('emittedHeight', this.$refs.me);
});
}
},
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.domainObject = this.node.object;
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
@ -110,49 +132,19 @@ export default {
});
this.$once('hook:destroyed', removeListener);
if (this.openmct.composition.get(this.node.object)) {
this.hasChildren = true;
if (objectComposition) {
this.hasComposition = true;
}
this.openmct.router.on('change:path', this.highlightIfNavigated);
this.getLocalStorageExpanded();
},
beforeDestroy() {
/****
* calling this.setLocalStorageExpanded explicitly here because for whatever reason,
* the watcher on this.expanded is not triggering this.setLocalStorageExpanded(),
* even though Vue documentation states, "At this stage the instance is still fully functional."
*****/
this.expanded = false;
this.setLocalStorageExpanded();
if (this.emitHeight) {
this.$emit('emittedHeight', this.$refs.me);
}
},
destroyed() {
this.openmct.router.off('change:path', this.highlightIfNavigated);
if (this.composition) {
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
delete this.composition;
}
},
methods: {
addChild(child) {
this.children.push({
id: this.openmct.objects.makeKeyString(child.identifier),
object: child,
objectPath: [child].concat(this.node.objectPath),
navigateToParent: this.navigateToPath
});
},
removeChild(identifier) {
let removeId = this.openmct.objects.makeKeyString(identifier);
this.children = this.children
.filter(c => c.id !== removeId);
},
finishLoading() {
this.isLoading = false;
this.loaded = true;
},
buildPathString(parentPath) {
return [parentPath, this.openmct.objects.makeKeyString(this.node.object.identifier)].join('/');
},
@ -163,35 +155,8 @@ export default {
this.navigated = false;
}
},
getLocalStorageExpanded() {
let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
if (expandedPaths) {
expandedPaths = JSON.parse(expandedPaths);
this.expanded = expandedPaths.includes(this.navigateToPath);
}
},
// expanded nodes/paths are stored in local storage as an array
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);
}
} else {
// remove this node path and all children paths from stored expanded paths
expandedPaths = expandedPaths.filter(path => !path.startsWith(this.navigateToPath));
}
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(expandedPaths));
},
removeLocalStorageExpanded() {
let expandedPaths = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
expandedPaths = expandedPaths ? JSON.parse(expandedPaths) : [];
expandedPaths = expandedPaths.filter(path => !path.startsWith(this.navigateToPath));
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(expandedPaths));
resetTreeHere() {
this.$emit('resetTree', this.node);
}
}
};