[Layout] Support sub-object selection in layout (#1811)

Updates to sub object selection, first cut of selection APIs.

* [API] Add inspector view registry to register inspector view providers and show a view in the inspector.

[API] Modify the selection API to register the click event and handle the event. The API will add a class to the selected object and the immediate parent of the selected object.

[Directive] Implemenet mct-selectable directive for making an element selectable.

[Layout] Update the layout controller to use the Selection API. Also, add double click gesture to allow drilling into a selected object.

Populate the Elements pool with contained elements of the selected object. Update toolbar and inspector to listen for the changes in selection.

* [Frontend] Mods to markup and CSS for sub-object selection

* MCTSelectable allows selection in initialization, use to select on navigation

[Frontend] Show grid in first nested layout, hide from deeper nesting. Only show grids when applicable to relative selection.

* Fix checkstyle and lint errors

* Bring back the change that made mct-init-select work

* [Inspector] Make sure the right content is displayed based on whether a view provider exists or not.

* Only show table options when editing

* Make reviewers' requested changes

* Fix broken tests

* [Frontend] Cleanups and tweaks

Fixes #1811
- Cleanups between frame, editor and selecting.scss;
- Hover and selected borders visually pumped up a bit;
- Solid borders on hover and selecting when browsing;
- Dashed borders for layouts when editing;
- Fixed cursor to only show move capability when
element is selected;

* [Frontend] Tweaks to frame.no-frame layout

Fixes #1811
- Margin set to 0;
- Overflow set to hidden;

* [Frontend] Fixed position items border width fixed

Fixes #1811
- Set to 1px;

* Add tests for inspector controller and fix broken tests. Clean up code.

* [Fixed Position] Stop event propagation on click handlers in fixed position to avoid the event reaching the selection click handlers which caused issues with toolbar and selection."

* Fix tests

* Add tests

* Add test

* Remove element from document
This commit is contained in:
Pegah Sarram 2017-12-07 13:04:46 -08:00 committed by Pete Richards
parent 50b4d5cb28
commit 425655bae0
34 changed files with 944 additions and 360 deletions

View File

@ -57,7 +57,12 @@
</div>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll">
class="abs flex-elem grows object-holder-main scroll"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
</mct-representation>
</div>
</div>

View File

@ -19,12 +19,21 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="InspectorController">
<div ng-repeat="region in regions">
<div ng-controller="InspectorController as controller">
<mct-representation
key="region.content.key"
mct-object="domainObject"
key="'object-properties'"
mct-object="controller.selectedItem()"
ng-model="ngModel">
</mct-representation>
</div>
<div ng-if="!controller.hasProviderView()">
<mct-representation
key="inspectorKey"
mct-object="controller.selectedItem()"
ng-model="ngModel">
</mct-representation>
</div>
<div class='inspector-provider-view'>
</div>
</div>

View File

@ -38,8 +38,6 @@
ng-class="{ last:($index + 1) === contextualParents.length }">
<mct-representation key="'label'"
mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item">
</mct-representation>
</span>
@ -51,8 +49,6 @@
ng-class="{ last:($index + 1) === primaryParents.length }">
<mct-representation key="'label'"
mct-object="parent"
ng-model="ngModel"
ng-click="ngModel.selectedObject = parent"
class="location-item">
</mct-representation>
</span>

View File

@ -121,7 +121,8 @@ define([
"key": "ElementsController",
"implementation": ElementsController,
"depends": [
"$scope"
"$scope",
"openmct"
]
},
{
@ -299,9 +300,6 @@ define([
{
"key": "edit-elements",
"template": elementsTemplate,
"uses": [
"composition"
],
"gestures": [
"drop"
]
@ -385,7 +383,10 @@ define([
]
},
{
"implementation": EditToolbarRepresenter
"implementation": EditToolbarRepresenter,
"depends": [
"openmct"
]
}
],
"constants": [

View File

@ -61,7 +61,12 @@
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar">
toolbar="toolbar"
mct-selectable="{
item: domainObject.useCapability('adapter'),
oldItem: domainObject
}"
mct-init-select>
</mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div>

View File

@ -25,7 +25,7 @@
ng-model="filterBy">
</mct-include>
<div class="flex-elem grows vscroll">
<ul class="tree">
<ul class="tree" ng-if="composition.length > 0">
<li ng-repeat="containedObject in composition | filter:searchElements">
<span class="tree-item">
<mct-representation
@ -36,5 +36,6 @@
</span>
</li>
</ul>
<div ng-if="composition.length === 0">No contained elements</div>
</div>
</div>

View File

@ -29,7 +29,11 @@ define(
*
* @constructor
*/
function ElementsController($scope) {
function ElementsController($scope, openmct) {
this.scope = $scope;
this.scope.composition = [];
var self = this;
function filterBy(text) {
if (typeof text === 'undefined') {
return $scope.searchText;
@ -47,10 +51,44 @@ define(
}
}
function setSelection(selection) {
self.scope.selection = selection;
self.refreshComposition(selection);
}
$scope.filterBy = filterBy;
$scope.searchElements = searchElements;
openmct.selection.on('change', setSelection);
setSelection(openmct.selection.get());
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
}
/**
* Gets the composition for the selected object and populates the scope with it.
*
* @param selection the selection object
* @private
*/
ElementsController.prototype.refreshComposition = function (selection) {
if (!selection[0]) {
return;
}
var selectedObjectComposition = selection[0].context.oldItem.useCapability('composition');
if (selectedObjectComposition) {
selectedObjectComposition.then(function (composition) {
this.scope.composition = composition;
}.bind(this));
} else {
this.scope.composition = [];
}
};
return ElementsController;
}
);

View File

@ -38,7 +38,7 @@ define(
* @constructor
* @implements {Representer}
*/
function EditToolbarRepresenter(scope, element, attrs) {
function EditToolbarRepresenter(openmct, scope, element, attrs) {
var self = this;
// Mark changes as ready to persist
@ -109,6 +109,7 @@ define(
this.updateSelection = updateSelection;
this.toolbar = undefined;
this.toolbarObject = {};
this.openmct = openmct;
// If this representation exposes a toolbar, set up watches
// to synchronize with it.
@ -146,7 +147,7 @@ define(
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
this.setSelection(new EditToolbarSelection());
this.setSelection(new EditToolbarSelection(this.openmct));
// Initialize toolbar to an empty selection
this.updateSelection([]);
};

View File

@ -38,10 +38,18 @@ define(
* @memberof platform/commonUI/edit
* @constructor
*/
function EditToolbarSelection() {
function EditToolbarSelection(openmct) {
this.selection = [{}];
this.selecting = false;
this.selectedObj = undefined;
openmct.selection.on('change', function (selection) {
if (selection[0] && selection[0].context.toolbar) {
this.select(selection[0].context.toolbar);
} else {
this.deselect();
}
}.bind(this));
}
/**

View File

@ -27,11 +27,23 @@ define(
describe("The Elements Pane controller", function () {
var mockScope,
mockOpenMCT,
mockSelection,
controller;
beforeEach(function () {
mockScope = jasmine.createSpy("$scope");
controller = new ElementsController(mockScope);
mockScope = jasmine.createSpyObj("$scope", ['$on']);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
controller = new ElementsController(mockScope, mockOpenMCT);
});
function getModel(model) {

View File

@ -29,7 +29,9 @@ define(
mockElement,
testAttrs,
mockUnwatch,
representer;
representer,
mockOpenMCT,
mockSelection;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
@ -46,7 +48,18 @@ define(
mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
representer = new EditToolbarRepresenter(
mockOpenMCT,
mockScope,
mockElement,
testAttrs

View File

@ -28,13 +28,25 @@ define(
var testProxy,
testElement,
otherElement,
selection;
selection,
mockSelection,
mockOpenMCT;
beforeEach(function () {
testProxy = { someKey: "some value" };
testElement = { someOtherKey: "some other value" };
otherElement = { yetAnotherKey: 42 };
selection = new EditToolbarSelection();
mockSelection = jasmine.createSpyObj("selection", [
// 'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn([]);
mockOpenMCT = {
selection: mockSelection
};
selection = new EditToolbarSelection(mockOpenMCT);
selection.proxy(testProxy);
});

View File

@ -41,6 +41,7 @@ define([
"./src/controllers/BannerController",
"./src/directives/MCTContainer",
"./src/directives/MCTDrag",
"./src/directives/MCTSelectable",
"./src/directives/MCTClickElsewhere",
"./src/directives/MCTResize",
"./src/directives/MCTPopup",
@ -90,6 +91,7 @@ define([
BannerController,
MCTContainer,
MCTDrag,
MCTSelectable,
MCTClickElsewhere,
MCTResize,
MCTPopup,
@ -328,6 +330,13 @@ define([
"$document"
]
},
{
"key": "mctSelectable",
"implementation": MCTSelectable,
"depends": [
"openmct"
]
},
{
"key": "mctClickElsewhere",
"implementation": MCTClickElsewhere,

View File

@ -25,6 +25,7 @@
}
.l-fixed-position-item {
border-width: 1px;
position: absolute;
&.s-not-selected {
opacity: 0.8;

View File

@ -80,23 +80,32 @@
// Editing Grids
.l-grid-holder {
display: block;
.l-grid {
&.l-grid-x { @include bgTicks($colorGridLines, 'x'); }
&.l-grid-y { @include bgTicks($colorGridLines, 'y'); }
}
}
// Prevent nested frames from showing their grids
.t-frame-outer .l-grid-holder { display: none !important; }
// Prevent nested elements from showing s-hover-border
.t-frame-outer .s-hover-border {
border: none !important;
// Display grid when selected or selection parent.
.s-selected .l-grid-holder,
.s-selected-parent .l-grid-holder {
display: block;
}
// Prevent nested frames from being selectable until we have proper sub-object editing
.t-frame-outer .t-frame-outer {
pointer-events: none;
// Display in nested frames...
.t-frame-outer {
// ...when drilled in or selection parent...
&.s-drilled-in, &.s-selected-parent {
.l-grid-holder {
display: block;
}
.t-frame-outer:not(.s-drilled-in) .l-grid-holder {
display: none;
}
}
// ...but hide otherwise.
.l-grid-holder {
display: none;
}
}
}

View File

@ -23,15 +23,14 @@
$ohH: $btnFrameH;
$bc: $colorInteriorBorder;
&.child-frame.panel {
border: 1px solid transparent;
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
&:not(.no-frame) {
background: $colorBodyBg;
border: 1px solid $bc;
&:hover {
border-color: lighten($bc, 10%);
}
border-color: $bc;
}
}
.object-browse-bar {
font-size: 0.75em;
height: $ohH;
@ -92,9 +91,9 @@
&.no-frame {
background: transparent !important;
border: none !important;
border: none;
.object-browse-bar .right {
$m: 0; // $interiorMarginSm;
$m: 0;
background: rgba(black, 0.3);
border-radius: $basicCr;
padding: $interiorMarginSm;
@ -104,7 +103,7 @@
}
&.t-frame-outer > .t-rep-frame {
&.contents {
$m: 2px;
$m: 0px;
top: $m;
right: $m;
bottom: $m;
@ -115,6 +114,7 @@
display: none;
}
> .object-holder.abs {
overflow: hidden;
top: 0 !important;
}
}

View File

@ -20,35 +20,51 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
.s-hover-border {
border: 1px dotted transparent;
&:hover {
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
}
}
.s-status-editing {
// Limit to editing mode until we have sub-object selection
// Limit to editing mode
$o: 0.5;
$oHover: 0.8;
$bc: $colorSelectableSelectedPrimary;
.s-hover-border {
// Show a border by default so user can see object bounds and empty objects
border: 1px dotted rgba($colorSelectableSelectedPrimary, 0.3) !important;
border-color: rgba($bc, $o) !important;
border-style: dotted !important;
&:hover {
border-color: rgba($colorSelectableSelectedPrimary, 0.7) !important;
border-color: rgba($bc, $oHover) !important;
}
&.t-object-type-layout {
border-style: dashed !important;
}
}
.s-selected > .s-hover-border,
.s-selected.s-hover-border {
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
border-color: $colorSelectableSelectedPrimary !important;
@include boxShdwLarge();
// Show edit-corners if you got 'em
.edit-corner {
display: block;
&:hover {
background-color: rgba($colorKey, 1);
.s-selected {
&.s-moveable {
&:not(.s-drilled-in) {
cursor: move;
}
}
}
}
.s-selected > .s-moveable,
.s-selected.s-moveable {
cursor: move;
.s-selected > .s-hover-border,
.s-selected.s-hover-border {
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
border-color: $colorSelectableSelectedPrimary !important;
@include boxShdwLarge();
// Show edit-corners if you got 'em
.edit-corner {
display: block;
&:hover {
background-color: rgba($colorKey, 1);
}
}
}
}

View File

@ -40,7 +40,7 @@ define(
// Gets an array of the contextual parents/ancestors of the selected object
function getContextualPath() {
var currentObj = $scope.ngModel.selectedObject,
var currentObj = $scope.domainObject,
currentParent,
parents = [];
@ -68,7 +68,7 @@ define(
// If this the the initial call of this recursive function
if (!current) {
current = $scope.ngModel.selectedObject;
current = $scope.domainObject;
$scope.primaryParents = [];
}
@ -87,16 +87,16 @@ define(
// Gets the metadata for the selected object
function getMetadata() {
$scope.metadata = $scope.ngModel.selectedObject &&
$scope.ngModel.selectedObject.hasCapability('metadata') &&
$scope.ngModel.selectedObject.useCapability('metadata');
$scope.metadata = $scope.domainObject &&
$scope.domainObject.hasCapability('metadata') &&
$scope.domainObject.useCapability('metadata');
}
// Set scope variables when the selected object changes
$scope.$watch('ngModel.selectedObject', function () {
$scope.isLink = $scope.ngModel.selectedObject &&
$scope.ngModel.selectedObject.hasCapability('location') &&
$scope.ngModel.selectedObject.getCapability('location').isLink();
$scope.$watch('domainObject', function () {
$scope.isLink = $scope.domainObject &&
$scope.domainObject.hasCapability('location') &&
$scope.domainObject.getCapability('location').isLink();
if ($scope.isLink) {
getPrimaryPath();
@ -109,7 +109,7 @@ define(
getMetadata();
});
var mutation = $scope.ngModel.selectedObject.getCapability('mutation');
var mutation = $scope.domainObject.getCapability('mutation');
var unlisten = mutation.listen(getMetadata);
$scope.$on('$destroy', unlisten);
}

View File

@ -0,0 +1,60 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
define(
[],
function () {
/**
* The mct-selectable directive allows selection functionality
* (click) to be attached to specific elements.
*
* @memberof platform/commonUI/general
* @constructor
*/
function MCTSelectable(openmct) {
// Link; install event handlers.
function link(scope, element, attrs) {
var removeSelectable = openmct.selection.selectable(
element[0],
scope.$eval(attrs.mctSelectable),
attrs.hasOwnProperty('mctInitSelect') && scope.$eval(attrs.mctInitSelect) !== false
);
scope.$on("$destroy", function () {
removeSelectable();
});
}
return {
// mct-selectable only makes sense as an attribute
restrict: "A",
// Link function, to install event handlers
link: link
};
}
return MCTSelectable;
}
);

View File

@ -41,16 +41,6 @@ define(
"$scope",
["$watch", "$on"]
);
mockScope.ngModel = {};
mockScope.ngModel.selectedObject = {
getCapability: function () {
return {
listen: function () {
return true;
}
};
}
};
mockObjectService = jasmine.createSpyObj(
"objectService",
@ -77,22 +67,27 @@ define(
"location capability",
["isLink"]
);
mockDomainObject.getCapability.andCallFake(function (param) {
if (param === 'location') {
return mockLocationCapability;
} else if (param === 'context') {
return mockContextCapability;
} else if (param === 'mutation') {
return {
listen: function () {
return true;
}
};
}
});
mockScope.domainObject = mockDomainObject;
controller = new ObjectInspectorController(mockScope, mockObjectService);
// Change the selected object to trigger the watch call
mockScope.ngModel.selectedObject = mockDomainObject;
});
it("watches for changes to the selected object", function () {
expect(mockScope.$watch).toHaveBeenCalledWith('ngModel.selectedObject', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
});
it("looks for contextual parent objects", function () {

View File

@ -38,7 +38,8 @@ define([
"implementation": InspectorController,
"depends": [
"$scope",
"policyService"
"openmct",
"$document"
]
}
],

View File

@ -21,44 +21,69 @@
*****************************************************************************/
define(
['../../browse/src/InspectorRegion'],
function (InspectorRegion) {
[],
function () {
/**
* The InspectorController adds region data for a domain object's type
* to the scope.
* The InspectorController listens for the selection changes and adds the selection
* object to the scope.
*
* @constructor
*/
function InspectorController($scope, policyService) {
var domainObject = $scope.domainObject,
typeCapability = domainObject.getCapability('type'),
statusListener;
function InspectorController($scope, openmct, $document) {
var self = this;
self.$scope = $scope;
/**
* Filters region parts to only those allowed by region policies
* @param regions
* @returns {{}}
* Callback handler for the selection change event.
* Adds the selection object to the scope. If the selected item has an inspector view,
* it puts the key in the scope. If provider view exists, it shows the view.
*/
function filterRegions(inspector) {
//Dupe so we're not modifying the type definition.
return inspector.regions && inspector.regions.filter(function (region) {
return policyService.allow('region', region, domainObject);
});
function setSelection(selection) {
if (selection[0]) {
var view = openmct.inspectorViews.get(selection);
var container = $document[0].querySelectorAll('.inspector-provider-view')[0];
container.innerHTML = "";
if (view) {
self.providerView = true;
view.show(container);
} else {
self.providerView = false;
$scope.inspectorKey = selection[0].context.oldItem.getCapability("type").typeDef.inspector;
}
}
self.$scope.selection = selection;
}
function setRegions() {
$scope.regions = filterRegions(typeCapability.getDefinition().inspector || new InspectorRegion());
}
openmct.selection.on("change", setSelection);
setSelection(openmct.selection.get());
statusListener = domainObject.getCapability("status").listen(setRegions);
$scope.$on("$destroy", function () {
statusListener();
openmct.selection.off("change", setSelection);
});
setRegions();
}
/**
* Gets the selected item.
*
* @returns a domain object
*/
InspectorController.prototype.selectedItem = function () {
return this.$scope.selection[0].context.oldItem;
};
/**
* Checks if a provider view exists.
*
* @returns 'true' if provider view exists, 'false' otherwise
*/
InspectorController.prototype.hasProviderView = function () {
return this.providerView;
};
return InspectorController;
}
);

View File

@ -27,82 +27,93 @@ define(
describe("The inspector controller ", function () {
var mockScope,
mockDomainObject,
mockTypeCapability,
mockTypeDefinition,
mockPolicyService,
mockStatusCapability,
capabilities = {},
controller;
mockOpenMCT,
mockSelection,
mockInspectorViews,
mockTypeDef,
controller,
container,
$document = [],
selectable = [];
beforeEach(function () {
mockTypeDefinition = {
inspector:
{
'regions': [
{'name': 'Part One'},
{'name': 'Part Two'}
]
}
mockTypeDef = {
typeDef: {
inspector: "some-key"
}
};
mockTypeCapability = jasmine.createSpyObj('typeCapability', [
'getDefinition'
]);
mockTypeCapability.getDefinition.andReturn(mockTypeDefinition);
capabilities.type = mockTypeCapability;
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
'listen'
]);
capabilities.status = mockStatusCapability;
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockDomainObject.getCapability.andCallFake(function (name) {
return capabilities[name];
});
mockPolicyService = jasmine.createSpyObj('policyService', [
'allow'
]);
mockDomainObject.getCapability.andReturn(mockTypeDef);
mockScope = jasmine.createSpyObj('$scope',
['$on']
['$on', 'selection']
);
mockScope.domainObject = mockDomainObject;
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockInspectorViews = jasmine.createSpyObj('inspectorViews', ['get']);
mockOpenMCT = {
selection: mockSelection,
inspectorViews: mockInspectorViews
};
container = jasmine.createSpy('container', ['innerHTML']);
$document[0] = jasmine.createSpyObj("$document", ['querySelectorAll']);
$document[0].querySelectorAll.andReturn([container]);
controller = new InspectorController(mockScope, mockOpenMCT, $document);
});
it("filters out regions disallowed by region policy", function () {
mockPolicyService.allow.andReturn(false);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(0);
it("listens for selection change event", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
expect(controller.selectedItem()).toEqual(mockDomainObject);
var mockItem = jasmine.createSpyObj('domainObject', [
'getCapability'
]);
mockItem.getCapability.andReturn(mockTypeDef);
selectable[0].context.oldItem = mockItem;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.selectedItem()).toEqual(mockItem);
});
it("does not filter out regions allowed by region policy", function () {
mockPolicyService.allow.andReturn(true);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(2);
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
it("Responds to status changes", function () {
mockPolicyService.allow.andReturn(true);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.regions.length).toBe(2);
expect(mockStatusCapability.listen).toHaveBeenCalled();
mockPolicyService.allow.andReturn(false);
mockStatusCapability.listen.mostRecentCall.args[0]();
expect(mockScope.regions.length).toBe(0);
});
it("Unregisters status listener", function () {
var mockListener = jasmine.createSpy('listener');
mockStatusCapability.listen.andReturn(mockListener);
controller = new InspectorController(mockScope, mockPolicyService);
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(mockListener).toHaveBeenCalled();
it("adds selection object to scope", function () {
expect(mockScope.selection).toEqual(selectable);
expect(controller.selectedItem()).toEqual(mockDomainObject);
});
});
}

View File

@ -260,7 +260,9 @@ define([
"key": "LayoutController",
"implementation": LayoutController,
"depends": [
"$scope"
"$scope",
"$element",
"openmct"
]
},
{

View File

@ -40,7 +40,7 @@
's-selected': controller.selected(element)
}"
ng-style="element.style"
ng-click="controller.select(element)">
ng-click="controller.select(element, $event)">
<mct-include key="element.template"
parameters="{ gridSize: controller.getGridSize() }"
ng-model="element">
@ -53,14 +53,16 @@
mct-drag-down="controller.moveHandle().startDrag(controller.selected())"
mct-drag="controller.moveHandle().continueDrag(delta)"
mct-drag-up="controller.moveHandle().endDrag()"
ng-style="controller.selected().style">
ng-style="controller.selected().style"
ng-click="$event.stopPropagation()">
</div>
<div ng-repeat="handle in controller.handles()"
class="l-fixed-position-item-handle edit-corner"
ng-style="handle.style()"
mct-drag-down="handle.startDrag()"
mct-drag="handle.continueDrag(delta)"
mct-drag-up="handle.endDrag()">
mct-drag-up="handle.endDrag()"
ng-click="$event.stopPropagation()">
</div>
</span>

View File

@ -22,10 +22,12 @@
<div class="abs l-layout"
ng-controller="LayoutController as controller"
ng-click="controller.clearSelection()">
ng-click="controller.bypassSelection($event)">
<!-- Background grid -->
<div class="l-grid-holder" ng-click="controller.clearSelection()">
<div class="l-grid-holder"
ng-show="!controller.drilledIn"
ng-click="controller.bypassSelection($event)">
<div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
@ -34,10 +36,12 @@
ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div>
</div>
<div class='abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border'
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-selected':controller.selected(childObject) }"
<div class="abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border {{childObject.getId() + '-' + $id}} t-object-type-{{ childObject.getModel().type }}"
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }"
ng-repeat="childObject in composition"
ng-click="controller.select($event, childObject.getId())"
ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
mct-selectable="controller.getContext(childObject, true)"
ng-dblclick="controller.drill($event, childObject)"
ng-style="controller.getFrameStyle(childObject.getId())">
<mct-representation key="'frame'"
@ -45,7 +49,7 @@
mct-object="childObject">
</mct-representation>
<!-- Drag handles -->
<span class="abs t-edit-handle-holder s-hover-border" ng-if="controller.selected(childObject)">
<span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)">
<span class="edit-handle edit-move"
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
mct-drag="controller.continueDrag(delta)"
@ -73,7 +77,6 @@
mct-drag-up="controller.endDrag()">
</span>
</span>
</div>
</div>

View File

@ -506,7 +506,11 @@ define(
* Set the active user selection in this view.
* @param element the element to select
*/
FixedController.prototype.select = function select(element) {
FixedController.prototype.select = function select(element, event) {
if (event) {
event.stopPropagation();
}
if (this.selection) {
// Update selection...
this.selection.select(element);

View File

@ -27,9 +27,11 @@
*/
define(
[
'zepto',
'./LayoutDrag'
],
function (
$,
LayoutDrag
) {
@ -50,10 +52,12 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function LayoutController($scope) {
function LayoutController($scope, $element, openmct) {
var self = this,
callbackCount = 0;
this.$element = $element;
// Update grid size when it changed
function updateGridSize(layoutGrid) {
var oldSize = self.gridSize;
@ -123,12 +127,11 @@ define(
self.layoutPanels(ids);
self.setFrames(ids);
// If there is a newly-dropped object, select it.
if (self.droppedIdToSelectAfterRefresh) {
self.select(null, self.droppedIdToSelectAfterRefresh);
delete self.droppedIdToSelectAfterRefresh;
} else if (composition.indexOf(self.selectedId) === -1) {
self.clearSelection();
if (self.selectedId &&
self.selectedId !== $scope.domainObject.getId() &&
composition.indexOf(self.selectedId) === -1) {
// Click triggers selection of layout parent.
self.$element[0].click();
}
}
});
@ -160,22 +163,39 @@ define(
}
};
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
delete self.selectedId;
return;
}
self.selectedId = selection.context.oldItem.getId();
self.drilledIn = undefined;
self.selectable = selectable;
}
this.positions = {};
this.rawPositions = {};
this.gridSize = DEFAULT_GRID_SIZE;
this.$scope = $scope;
this.drilledIn = undefined;
this.openmct = openmct;
// Watch for changes to the grid size in the model
$scope.$watch("model.layoutGrid", updateGridSize);
$scope.$watch("selection", function (selection) {
this.selection = selection;
}.bind(this));
// Update composed objects on screen, and position panes
$scope.$watchCollection("model.composition", refreshComposition);
// Position panes where they are dropped
openmct.selection.on('change', setSelection);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
});
$scope.$on("mctDrop", handleDrop);
}
@ -357,37 +377,14 @@ define(
};
/**
* Check if the object is currently selected.
* Checks if the object is currently selected.
*
* @param {string} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false
*/
LayoutController.prototype.selected = function (obj) {
return !!this.selectedId && this.selectedId === obj.getId();
};
/**
* Set the active user selection in this view.
*
* @param event the mouse event
* @param {string} id the object id
*/
LayoutController.prototype.select = function (event, id) {
if (event) {
event.stopPropagation();
if (this.selection) {
event.preventDefault();
}
}
this.selectedId = id;
var selectedObj = {};
selectedObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id);
if (this.selection) {
this.selection.select(selectedObj);
}
var sobj = this.openmct.selection.get()[0];
return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false;
};
/**
@ -396,7 +393,7 @@ define(
* @param {string} id the object id
* @private
*/
LayoutController.prototype.toggleFrame = function (id) {
LayoutController.prototype.toggleFrame = function (id, domainObject) {
var configuration = this.$scope.configuration;
if (!configuration.panels[id]) {
@ -404,21 +401,75 @@ define(
}
this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id];
this.select(undefined, id); // reselect so toolbar updates
var selection = this.openmct.selection.get();
selection[0].context.toolbar = this.getToolbar(id, domainObject);
this.openmct.selection.select(selection); // reselect so toolbar updates
};
/**
* Clear the current user selection.
* Gets the toolbar object for the given domain object.
*
* @param id the domain object id
* @param domainObject the domain object
* @returns {object}
* @private
*/
LayoutController.prototype.clearSelection = function () {
LayoutController.prototype.getToolbar = function (id, domainObject) {
var toolbarObj = {};
toolbarObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id, domainObject);
return toolbarObj;
};
/**
* Bypasses selection if drag is in progress.
*
* @param event the angular event object
*/
LayoutController.prototype.bypassSelection = function (event) {
if (this.dragInProgress) {
if (event) {
event.stopPropagation();
}
return;
}
};
/**
* Checks if the domain object is drilled in.
*
* @param domainObject the domain object
* @return true if the object is drilled in, false otherwise
*/
LayoutController.prototype.isDrilledIn = function (domainObject) {
return this.drilledIn === domainObject.getId();
};
/**
* Puts the given object in the drilled-in mode.
*
* @param event the angular event object
* @param domainObject the domain object
*/
LayoutController.prototype.drill = function (event, domainObject) {
if (event) {
event.stopPropagation();
}
if (!domainObject.getCapability('editor').inEditContext()) {
return;
}
if (this.selection) {
this.selection.deselect();
delete this.selectedId;
if (!domainObject.hasCapability('composition')) {
return;
}
// Disable since fixed position doesn't use the selection API yet
if (domainObject.getModel().type === 'telemetry.fixed') {
return;
}
this.drilledIn = domainObject.getId();
};
/**
@ -440,6 +491,36 @@ define(
return this.gridSize;
};
/**
* Gets the selection context.
*
* @param domainObject the domain object
* @returns {object} the context object which includes
* item, oldItem and toolbar
*/
LayoutController.prototype.getContext = function (domainObject, toolbar) {
return {
item: domainObject.useCapability('adapter'),
oldItem: domainObject,
toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined
};
};
/**
* Selects a newly-dropped object.
*
* @param classSelector the css class selector
* @param domainObject the domain object
*/
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
setTimeout(function () {
$('.' + selector)[0].click();
delete this.droppedIdToSelectAfterRefresh;
}.bind(this), 0);
}
};
return LayoutController;
}
);

View File

@ -21,8 +21,14 @@
*****************************************************************************/
define(
["../src/LayoutController"],
function (LayoutController) {
[
"../src/LayoutController",
"zepto"
],
function (
LayoutController,
$
) {
describe("The Layout controller", function () {
var mockScope,
@ -32,7 +38,12 @@ define(
controller,
mockCompositionCapability,
mockComposition,
mockCompositionObjects;
mockCompositionObjects,
mockOpenMCT,
mockSelection,
mockDomainObjectCapability,
$element = [],
selectable = [];
function mockPromise(value) {
return {
@ -58,21 +69,18 @@ define(
} else {
return {};
}
},
getCapability: function () {
return mockDomainObjectCapability;
},
hasCapability: function (param) {
if (param === 'composition') {
return id !== 'b';
}
}
};
}
// Utility function to find a watch for a given expression
function findWatch(expr) {
var watch;
mockScope.$watch.calls.forEach(function (call) {
if (call.args[0] === expr) {
watch = call.args[1];
}
});
return watch;
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
@ -88,7 +96,6 @@ define(
mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = {
panels: {
a: {
@ -97,27 +104,70 @@ define(
}
}
};
mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext']
);
mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
mockScope.selection = jasmine.createSpyObj(
'selection',
['select', 'get', 'selected', 'deselect']
);
selectable[0] = {
context: {
oldItem: mockScope.domainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.andReturn(selectable);
mockOpenMCT = {
selection: mockSelection
};
$element = $('<div></div>');
$(document).find('body').append($element);
spyOn($element[0], 'click');
spyOn(mockScope.domainObject, "useCapability").andCallThrough();
controller = new LayoutController(mockScope);
controller = new LayoutController(mockScope, $element, mockOpenMCT);
spyOn(controller, "layoutPanels").andCallThrough();
findWatch("selection")(mockScope.selection);
jasmine.Clock.useMock();
});
afterEach(function () {
$element.remove();
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
// Model changes will indicate that panel positions
// may have changed, for instance.
it("watches for changes to composition", function () {
@ -320,67 +370,35 @@ define(
.not.toEqual(oldStyle);
});
it("allows panels to be selected", function () {
it("allows objects to be selected", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(controller.selected(childObj)).toBe(true);
});
it("prevents event bubbling while drag is in progress", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId());
// Do a drag
controller.startDrag(childObj.getId(), [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause the parent object to be selected, this should be ignored.
controller.bypassSelection(mockEvent);
expect(mockEvent.stopPropagation).toHaveBeenCalled();
expect(controller.selected(childObj)).toBe(true);
});
it("allows selection to be cleared", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(null, childObj.getId());
controller.clearSelection();
expect(controller.selected(childObj)).toBeFalsy();
});
it("prevents clearing selection while drag is in progress", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause clearSelection to be called, this should be ignored.
controller.clearSelection();
expect(controller.selected(childObj)).toBe(true);
// Shoud be able to clear the selection after dragging is done.
// Shoud be able to select another object when dragging is done.
jasmine.Clock.tick(0);
controller.clearSelection();
mockEvent.stopPropagation.reset();
controller.bypassSelection(mockEvent);
expect(controller.selected(childObj)).toBe(false);
});
it("clears selection after moving/resizing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
var id = childObj.getId();
controller.select(mockEvent, id);
// Do a drag
controller.startDrag(id, [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
jasmine.Clock.tick(0);
controller.clearSelection();
expect(controller.selected(childObj)).toBe(false);
expect(mockEvent.stopPropagation).not.toHaveBeenCalled();
});
it("shows frames by default", function () {
@ -398,43 +416,74 @@ define(
it("hides frame when selected object has frame ", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId());
expect(mockScope.selection.select).toHaveBeenCalled();
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
expect(controller.hasFrame(childObj)).toBe(true);
expect(selectedObj.hideFrame).toBeDefined();
expect(selectedObj.hideFrame).toEqual(jasmine.any(Function));
expect(toolbarObj.hideFrame).toBeDefined();
expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function));
});
it("shows frame when selected object has no frame", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
controller.select(mockEvent, childObj.getId());
expect(mockScope.selection.select).toHaveBeenCalled();
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
expect(controller.hasFrame(childObj)).toBe(false);
expect(selectedObj.showFrame).toBeDefined();
expect(selectedObj.showFrame).toEqual(jasmine.any(Function));
expect(toolbarObj.showFrame).toBeDefined();
expect(toolbarObj.showFrame).toEqual(jasmine.any(Function));
});
it("deselects the object that is no longer in the composition", function () {
it("selects the parent object when selected object is removed", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
controller.select(mockEvent, childObj.getId());
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
var composition = ["b", "c"];
mockScope.$watchCollection.mostRecentCall.args[1](composition);
expect(controller.selected(childObj)).toBe(false);
expect($element[0].click).toHaveBeenCalled();
});
it("allows objects to be drilled-in only when editing", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[0];
childObj.getCapability().inEditContext.andReturn(false);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("allows objects to be drilled-in only if it has sub objects", function () {
mockScope.$watchCollection.mostRecentCall.args[1]();
var childObj = mockCompositionObjects[1];
childObj.getCapability().inEditContext.andReturn(true);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("selects a newly-dropped object", function () {
mockScope.$on.mostRecentCall.args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
var childObj = mockDomainObject("d");
var testElement = $("<div class='some-class'></div>");
$element.append(testElement);
spyOn(testElement[0], 'click');
controller.selectIfNew('some-class', childObj);
jasmine.Clock.tick(0);
expect(testElement[0].click).toHaveBeenCalled();
});
});
}
);

View File

@ -69,7 +69,7 @@ define([
"delegates": [
"telemetry"
],
"inspector": tableInspector,
"inspector": "table-options-edit",
"contains": [
{
"has": "telemetry"

View File

@ -19,7 +19,10 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="TableOptionsController" class="l-controls-first flex-elem grows l-inspector-part">
<div ng-if="domainObject.getCapability('editor').inEditContext()"
ng-controller="TableOptionsController"
class="l-controls-first flex-elem grows l-inspector-part">
<em class="t-inspector-part-header" title="Display properties for this object">Table Options</em>
<mct-form
ng-model="configuration.table.columns"

View File

@ -28,7 +28,8 @@ define([
'./selection/Selection',
'./api/objects/object-utils',
'./plugins/plugins',
'./ui/ViewRegistry'
'./ui/ViewRegistry',
'./ui/InspectorViewRegistry'
], function (
EventEmitter,
legacyRegistry,
@ -37,7 +38,8 @@ define([
Selection,
objectUtils,
plugins,
ViewRegistry
ViewRegistry,
InspectorViewRegistry
) {
/**
* Open MCT is an extensible web application for building mission
@ -112,15 +114,13 @@ define([
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on selection state, so
* providers should be prepared to test arbitrary objects for
* viewability.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.ViewRegistry}
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectors
* @name inspectorViews
*/
this.inspectors = new ViewRegistry();
this.inspectorViews = new InspectorViewRegistry();
/**
* Registry for views which should appear in Edit Properties
@ -196,7 +196,6 @@ define([
this.Dialog = api.Dialog;
this.on('navigation', this.selection.clear.bind(this.selection));
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -33,37 +33,96 @@ define(['EventEmitter'], function (EventEmitter) {
Selection.prototype = Object.create(EventEmitter.prototype);
Selection.prototype.add = function (context) {
this.clear(); // Only allow single select as initial simplification
this.selected.push(context);
this.emit('change');
};
Selection.prototype.remove = function (path) {
this.selected = this.selected.filter(function (otherPath) {
return !path.matches(otherPath);
});
this.emit('change');
};
Selection.prototype.contains = function (path) {
return this.selected.some(function (otherPath) {
return path.matches(otherPath);
});
};
Selection.prototype.clear = function () {
this.selected = [];
this.emit('change');
};
Selection.prototype.primary = function () {
return this.selected[this.selected.length - 1];
};
Selection.prototype.all = function () {
/**
* Gets the selected object.
* @public
*/
Selection.prototype.get = function () {
return this.selected;
};
/**
* Selects the selectable object and emits the 'change' event.
*
* @param {object} selectable an object with element and context properties
* @private
*/
Selection.prototype.select = function (selectable) {
if (!Array.isArray(selectable)) {
selectable = [selectable];
}
if (this.selected[0] && this.selected[0].element) {
this.selected[0].element.classList.remove('s-selected');
}
if (this.selected[1]) {
this.selected[1].element.classList.remove('s-selected-parent');
}
if (selectable[0] && selectable[0].element) {
selectable[0].element.classList.add('s-selected');
}
if (selectable[1]) {
selectable[1].element.classList.add('s-selected-parent');
}
this.selected = selectable;
this.emit('change', this.selected);
};
/**
* @private
*/
Selection.prototype.capture = function (selectable) {
if (!this.capturing) {
this.capturing = [];
}
this.capturing.push(selectable);
};
/**
* @private
*/
Selection.prototype.selectCapture = function (selectable) {
if (!this.capturing) {
return;
}
this.select(this.capturing.reverse());
delete this.capturing;
};
/**
* Attaches the click handlers to the element.
*
* @param element an html element
* @param context object with oldItem, item and toolbar properties
* @param select a flag to select the element if true
* @returns a function that removes the click handlers from the element
* @public
*/
Selection.prototype.selectable = function (element, context, select) {
var selectable = {
context: context,
element: element
};
var capture = this.capture.bind(this, selectable);
var selectCapture = this.selectCapture.bind(this, selectable);
element.addEventListener('click', capture, true);
element.addEventListener('click', selectCapture);
if (select) {
this.select(selectable);
}
return function () {
element.removeEventListener('click', capture);
element.removeEventListener('click', selectCapture);
};
};
return Selection;
});

View File

@ -0,0 +1,154 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, 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.
*****************************************************************************/
/*global console */
define([], function () {
/**
* A InspectorViewRegistry maintains the definitions for views
* that may occur in the inspector.
*
* @interface InspectorViewRegistry
* @memberof module:openmct
*/
function InspectorViewRegistry() {
this.providers = {};
}
/**
*
* @param {object} selection the object to be viewed
* @returns {module:openmct.InspectorViewRegistry[]} any providers
* which can provide views of this object
* @private for platform-internal use
*/
InspectorViewRegistry.prototype.get = function (selection) {
var providers = this.getAllProviders().filter(function (provider) {
return provider.canView(selection);
});
if (providers && providers.length > 0) {
return providers[0].view(selection);
}
};
/**
* @private
*/
InspectorViewRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
/**
* Registers a new type of view.
*
* @param {module:openmct.InspectorViewRegistry} provider the provider for this view
* @method addProvider
* @memberof module:openmct.InspectorViewRegistry#
*/
InspectorViewRegistry.prototype.addProvider = function (provider) {
var key = provider.key;
if (key === undefined) {
throw "View providers must have a unique 'key' property defined";
}
if (this.providers[key] !== undefined) {
console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
}
this.providers[key] = provider;
};
/**
* @private
*/
InspectorViewRegistry.prototype.getByProviderKey = function (key) {
return this.providers[key];
};
/**
* A View is used to provide displayable content, and to react to
* associated life cycle events.
*
* @name View
* @interface
* @memberof module:openmct
*/
/**
* Populate the supplied DOM element with the contents of this view.
*
* View implementations should use this method to attach any
* listeners or acquire other resources that are necessary to keep
* the contents of this view up-to-date.
*
* @param {HTMLElement} container the DOM element to populate
* @method show
* @memberof module:openmct.View#
*/
/**
* Release any resources associated with this view.
*
* View implementations should use this method to detach any
* listeners or release other resources that are no longer necessary
* once a view is no longer used.
*
* @method destroy
* @memberof module:openmct.View#
*/
/**
* Exposes types of views in inspector.
*
* @interface InspectorViewProvider
* @property {string} key a unique identifier for this view
* @property {string} name the human-readable name of this view
* @property {string} [description] a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} [cssClass] the CSS class to apply to labels for this
* view (to add icons, for instance)
* @memberof module:openmct
*/
/**
* Checks if this provider can supply views for a selection.
*
* @method canView
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection
* @returns {boolean} 'true' if the view applies to the provided selection,
* otherwise 'false'.
*/
/**
* Provides a view of the selection object in the inspector.
*
* @method view
* @memberof module:openmct.InspectorViewProvider#
* @param {module:openmct.selection} selection the selection object
* @returns {module:openmct.View} a view of this selection
*/
return InspectorViewRegistry;
});