diff --git a/karma.conf.js b/karma.conf.js
index 8875b78284..daf2a9d873 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -100,6 +100,6 @@ module.exports = (config) => {
},
concurrency: 1,
singleRun: true,
- browserNoActivityTimeout: 90000
+ browserNoActivityTimeout: 400000
});
};
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index ab85477ee5..de753dac9c 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -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);
}
/**
diff --git a/src/api/objects/RootObjectProvider.js b/src/api/objects/RootObjectProvider.js
index a15b4c41f7..00c43a215b 100644
--- a/src/api/objects/RootObjectProvider.js
+++ b/src/api/objects/RootObjectProvider.js
@@ -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;
diff --git a/src/api/objects/test/RootObjectProviderSpec.js b/src/api/objects/test/RootObjectProviderSpec.js
index 5cc83a4c2f..9536f845d5 100644
--- a/src/api/objects/test/RootObjectProviderSpec.js
+++ b/src/api/objects/test/RootObjectProviderSpec.js
@@ -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']
});
});
});
diff --git a/src/plugins/defaultRootName/plugin.js b/src/plugins/defaultRootName/plugin.js
new file mode 100644
index 0000000000..a8edf2eeb4
--- /dev/null
+++ b/src/plugins/defaultRootName/plugin.js
@@ -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);
+ };
+}
diff --git a/src/plugins/defaultRootName/pluginSpec.js b/src/plugins/defaultRootName/pluginSpec.js
new file mode 100644
index 0000000000..00c88a81b3
--- /dev/null
+++ b/src/plugins/defaultRootName/pluginSpec.js
@@ -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();
+ });
+ });
+});
diff --git a/src/plugins/folderView/components/list-view.scss b/src/plugins/folderView/components/list-view.scss
index bb4ba0ace8..21aced062d 100644
--- a/src/plugins/folderView/components/list-view.scss
+++ b/src/plugins/folderView/components/list-view.scss
@@ -13,7 +13,7 @@
cursor: pointer;
&:hover {
- background: $colorItemTreeHoverBg;
+ background: $colorListItemBgHov;
filter: $filterHov;
transition: $transIn;
}
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 8dc1e82777..d03612e2c4 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -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;
});
diff --git a/src/styles/_animations.scss b/src/styles/_animations.scss
index 1d06cf7f9e..f95e73560c 100644
--- a/src/styles/_animations.scss
+++ b/src/styles/_animations.scss
@@ -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 {
diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss
index 4098bd20a0..96b41a62f8 100644
--- a/src/styles/_constants-espresso.scss
+++ b/src/styles/_constants-espresso.scss
@@ -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;
diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss
index 9c6593acc3..e4d1992612 100644
--- a/src/styles/_constants-maelstrom.scss
+++ b/src/styles/_constants-maelstrom.scss
@@ -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;
diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss
index d3d1e0908e..aa6541064f 100644
--- a/src/styles/_constants-snow.scss
+++ b/src/styles/_constants-snow.scss
@@ -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;
diff --git a/src/styles/_constants.scss b/src/styles/_constants.scss
index ce5954741b..5a5c20a4a9 100755
--- a/src/styles/_constants.scss
+++ b/src/styles/_constants.scss
@@ -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;
diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss
index bb47dcb6fb..dccff7b0dc 100644
--- a/src/styles/_controls.scss
+++ b/src/styles/_controls.scss
@@ -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;
}
diff --git a/src/styles/_global.scss b/src/styles/_global.scss
index 72fbb53d53..b786bbfcac 100644
--- a/src/styles/_global.scss
+++ b/src/styles/_global.scss
@@ -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;
diff --git a/src/ui/components/viewControl.vue b/src/ui/components/viewControl.vue
index c32693a809..2a54a0bb5b 100644
--- a/src/ui/components/viewControl.vue
+++ b/src/ui/components/viewControl.vue
@@ -1,10 +1,10 @@
@@ -25,6 +25,10 @@ export default {
propagate: {
type: Boolean,
default: true
+ },
+ controlClass: {
+ type: String,
+ default: 'c-disclosure-triangle'
}
},
methods: {
diff --git a/src/ui/layout/BrowseBar.vue b/src/ui/layout/BrowseBar.vue
index 87912e1b0b..8ef379ad75 100644
--- a/src/ui/layout/BrowseBar.vue
+++ b/src/ui/layout/BrowseBar.vue
@@ -3,7 +3,8 @@
@@ -47,12 +49,23 @@
label="Browse"
collapsable
>
-
+
+
0;
+ },
+ handleSyncTreeNavigation() {
+ this.triggerSync = !this.triggerSync;
}
}
};
diff --git a/src/ui/layout/layout.scss b/src/ui/layout/layout.scss
index 4d7944838e..527de3d6c0 100644
--- a/src/ui/layout/layout.scss
+++ b/src/ui/layout/layout.scss
@@ -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 {
diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss
index 3defe59e60..b0fb8ec02c 100644
--- a/src/ui/layout/mct-tree.scss
+++ b/src/ui/layout/mct-tree.scss
@@ -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);}
+}
diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue
index c75352e93e..f1d969ecdf 100644
--- a/src/ui/layout/mct-tree.vue
+++ b/src/ui/layout/mct-tree.vue
@@ -1,5 +1,8 @@
-
+
+
-
-
-
-
No results found
@@ -26,30 +22,72 @@
-
+
+
+
+
+
-
+ Loading...
+
+
+
+
+
+ -
+
+
+
-
-
-
-
@@ -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));
}
}
};
diff --git a/src/ui/layout/pane.scss b/src/ui/layout/pane.scss
index 0d988e1124..fdcb0913e3 100644
--- a/src/ui/layout/pane.scss
+++ b/src/ui/layout/pane.scss
@@ -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;
}
}
}
diff --git a/src/ui/layout/pane.vue b/src/ui/layout/pane.vue
index 1f8a2267f7..8bd178dc50 100644
--- a/src/ui/layout/pane.vue
+++ b/src/ui/layout/pane.vue
@@ -20,12 +20,19 @@
{{ label }}
+
+
diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue
index 3cb3b7490d..800955b758 100644
--- a/src/ui/layout/tree-item.vue
+++ b/src/ui/layout/tree-item.vue
@@ -1,38 +1,39 @@
-
+
+
-
@@ -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);
}
}
};