mirror of
https://github.com/nasa/openmct.git
synced 2024-12-23 23:12:23 +00:00
Merge remote-tracking branch 'origin/wtd535' into open-master
This commit is contained in:
commit
ce608eedc3
@ -16,11 +16,6 @@
|
||||
"implementation": "BrowseController.js",
|
||||
"depends": [ "$scope", "objectService", "navigationService" ]
|
||||
},
|
||||
{
|
||||
"key": "ViewSwitcherController",
|
||||
"implementation": "ViewSwitcherController.js",
|
||||
"depends": [ "$scope" ]
|
||||
},
|
||||
{
|
||||
"key": "CreateMenuController",
|
||||
"implementation": "creation/CreateMenuController",
|
||||
@ -112,7 +107,8 @@
|
||||
"description": "Grid of available items.",
|
||||
"templateUrl": "templates/items/items.html",
|
||||
"uses": [ "composition" ],
|
||||
"gestures": [ "drop" ]
|
||||
"gestures": [ "drop" ],
|
||||
"type": "folder"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
@ -1,4 +1,4 @@
|
||||
<span ng-controller="ViewSwitcherController">
|
||||
<span>
|
||||
<div class="object-browse-bar bar abs">
|
||||
|
||||
<div class="items-select left abs">
|
||||
@ -13,14 +13,17 @@
|
||||
parameters="{ category: 'view-control' }">
|
||||
</mct-representation>
|
||||
|
||||
<mct-include key="'switcher'" ng-model="switcher" ng-if="switcher.options.length > 0">
|
||||
</mct-include>
|
||||
<mct-representation key="'switcher'"
|
||||
mct-object="domainObject"
|
||||
ng-model="representation">
|
||||
</mct-representation>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='object-holder abs vscroll'>
|
||||
<mct-representation key="switcher.selected.key" mct-object="domainObject">
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</span>
|
@ -1,6 +1,5 @@
|
||||
[
|
||||
"BrowseController",
|
||||
"ViewSwitcherController",
|
||||
"creation/CreateAction",
|
||||
"creation/CreateActionProvider",
|
||||
"creation/CreateMenuController",
|
||||
|
@ -63,10 +63,6 @@
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
{
|
||||
"key": "topbar-edit",
|
||||
"templateUrl": "templates/topbar-edit.html"
|
||||
},
|
||||
{
|
||||
"key": "edit-library",
|
||||
"templateUrl": "templates/library.html"
|
||||
@ -88,6 +84,16 @@
|
||||
"templateUrl": "templates/elements.html",
|
||||
"uses": [ "composition" ],
|
||||
"gestures": [ "drop" ]
|
||||
},
|
||||
{
|
||||
"key": "topbar-edit",
|
||||
"templateUrl": "templates/topbar-edit.html"
|
||||
}
|
||||
],
|
||||
"representers": [
|
||||
{
|
||||
"implementation": "EditRepresenter.js",
|
||||
"depends": [ "$q", "$log" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
<span ng-controller="ViewSwitcherController">
|
||||
<mct-include key="'topbar-edit'"
|
||||
parameters="{ switcher: switcher, object: domainObject }">
|
||||
</mct-include>
|
||||
<mct-representation key="'topbar-edit'"
|
||||
mct-object="domainObject"
|
||||
ng-model="representation">
|
||||
</mct-representation>
|
||||
<div class="holder edit-area outline abs">
|
||||
<!-- edit toolbar goes here -->
|
||||
<div class='split-layout vertical contents abs work-area'>
|
||||
<div class='split-pane-component edit-main pane' style="right: 200px; left: 0px;">
|
||||
<div class='holder abs object-holder'>
|
||||
<mct-representation key="switcher.selected.key" mct-object="domainObject">
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
@ -16,7 +17,7 @@
|
||||
style="right: 0px; width: 200px">
|
||||
<div class='holder abs split-layout horizontal'>
|
||||
|
||||
<mct-container key="accordion" title="Library" style="position: relative; top: 0px;">
|
||||
<mct-container key="accordion" title="Library" style="position: relative; top: 0px; height: 200px;">
|
||||
<mct-representation key="'tree'" mct-object="context.getRoot()">
|
||||
</mct-representation>
|
||||
</mct-container>
|
||||
@ -30,4 +31,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
@ -1,17 +1,19 @@
|
||||
<div class='top-bar edit abs'>
|
||||
<mct-representation key="'object-header'" mct-object="parameters.object" parameters="{ mode: 'Edit' }">
|
||||
<mct-representation key="'object-header'"
|
||||
mct-object="domainObject"
|
||||
parameters="{ mode: 'Edit' }">
|
||||
</mct-representation>
|
||||
<div class='buttons-main btn-bar buttons abs'>
|
||||
<mct-include key="'switcher'"
|
||||
ng-model="parameters.switcher"
|
||||
ng-if="parameters.switcher.options.length > 0">
|
||||
</mct-include>
|
||||
<mct-representation key="'switcher'"
|
||||
mct-object="domainObject"
|
||||
ng-model="ngModel">
|
||||
</mct-representation>
|
||||
|
||||
<mct-representation key="'edit-action-buttons'"
|
||||
mct-object="parameters.object"
|
||||
mct-object="domainObject"
|
||||
class='conclude-editing'>
|
||||
<!--a class='btn major' href=''>Save<span id='save-actions-menu' class='ui-symbol invoke-menu'>v</span></a>
|
||||
<a class='btn subtle' href=''>Cancel</a-->
|
||||
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
107
platform/commonUI/edit/src/EditRepresenter.js
Normal file
107
platform/commonUI/edit/src/EditRepresenter.js
Normal file
@ -0,0 +1,107 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The EditRepresenter is responsible for implementing
|
||||
* representation-level behavior relevant to Edit mode.
|
||||
* Specifically, this listens for changes to view configuration
|
||||
* or to domain object models, and triggers persistence when
|
||||
* these are detected.
|
||||
*
|
||||
* This is exposed as an extension of category `representers`,
|
||||
* which mct-representation will utilize to add additional
|
||||
* behavior to each representation.
|
||||
*
|
||||
* This will be called once per mct-representation directive,
|
||||
* and may be reused for different domain objects and/or
|
||||
* representations resulting from changes there.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function EditRepresenter($q, $log, scope) {
|
||||
var domainObject,
|
||||
key;
|
||||
|
||||
// Mutate and persist a new version of a domain object's model.
|
||||
function doPersist(model) {
|
||||
// First, mutate; then, persist.
|
||||
return $q.when(domainObject.useCapability("mutation", function () {
|
||||
return model;
|
||||
})).then(function (result) {
|
||||
// Only persist when mutation was successful
|
||||
return result &&
|
||||
domainObject.getCapability("persistence").persist();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle changes to model and/or view configuration
|
||||
function commit(message) {
|
||||
// Look up from scope; these will have been populated by
|
||||
// mct-representation.
|
||||
var model = scope.model,
|
||||
configuration = scope.configuration;
|
||||
|
||||
// Log the commit message
|
||||
$log.debug([
|
||||
"Committing ",
|
||||
domainObject && domainObject.getModel().name,
|
||||
"(" + (domainObject && domainObject.getId()) + "):",
|
||||
message
|
||||
].join(" "));
|
||||
|
||||
// Update the configuration stored in the model, and persist.
|
||||
if (domainObject && domainObject.hasCapability("persistence")) {
|
||||
// Configurations for specific views are stored by
|
||||
// key in the "configuration" field of the model.
|
||||
if (key && configuration) {
|
||||
model.configuration = model.configuration || {};
|
||||
model.configuration[key] = configuration;
|
||||
}
|
||||
doPersist(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to the destruction of the current representation.
|
||||
function destroy() {
|
||||
// Nothing to clean up
|
||||
}
|
||||
|
||||
// Handle a specific representation of a specific domain object
|
||||
function represent(representation, representedObject) {
|
||||
// Track the key, to know which view configuration to save to.
|
||||
key = representation.key;
|
||||
// Track the represented object
|
||||
domainObject = representedObject;
|
||||
|
||||
// Ensure existing watches are released
|
||||
destroy();
|
||||
}
|
||||
|
||||
// Place the "commit" method in the scope
|
||||
scope.commit = commit;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Set the current representation in use, and the domain
|
||||
* object being represented.
|
||||
*
|
||||
* @param {RepresentationDefinition} representation the
|
||||
* definition of the representation in use
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* being represented
|
||||
*/
|
||||
represent: represent,
|
||||
/**
|
||||
* Release any resources associated with this representer.
|
||||
*/
|
||||
destroy: destroy
|
||||
};
|
||||
}
|
||||
|
||||
return EditRepresenter;
|
||||
}
|
||||
);
|
83
platform/commonUI/edit/test/EditRepresenterSpec.js
Normal file
83
platform/commonUI/edit/test/EditRepresenterSpec.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/EditRepresenter"],
|
||||
function (EditRepresenter) {
|
||||
"use strict";
|
||||
|
||||
describe("The Edit mode representer", function () {
|
||||
var mockQ,
|
||||
mockLog,
|
||||
mockScope,
|
||||
testRepresentation,
|
||||
mockDomainObject,
|
||||
mockPersistence,
|
||||
representer;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = { when: mockPromise };
|
||||
mockLog = jasmine.createSpyObj("$log", ["info", "debug"]);
|
||||
mockScope = jasmine.createSpyObj("$scope", ["$watch"]);
|
||||
testRepresentation = { key: "test" };
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", [
|
||||
"getId",
|
||||
"getModel",
|
||||
"getCapability",
|
||||
"useCapability",
|
||||
"hasCapability"
|
||||
]);
|
||||
mockPersistence =
|
||||
jasmine.createSpyObj("persistence", ["persist"]);
|
||||
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.useCapability.andReturn(true);
|
||||
mockDomainObject.getCapability.andReturn(mockPersistence);
|
||||
|
||||
representer = new EditRepresenter(mockQ, mockLog, mockScope);
|
||||
representer.represent(testRepresentation, mockDomainObject);
|
||||
});
|
||||
|
||||
it("provides a commit method in scope", function () {
|
||||
expect(mockScope.commit).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("mutates and persists upon observed changes", function () {
|
||||
mockScope.model = { someKey: "some value" };
|
||||
mockScope.configuration = { someConfiguration: "something" };
|
||||
|
||||
mockScope.commit("Some message");
|
||||
|
||||
// Should have mutated the object...
|
||||
expect(mockDomainObject.useCapability).toHaveBeenCalledWith(
|
||||
"mutation",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// ... and should have persisted the mutation
|
||||
expect(mockPersistence.persist).toHaveBeenCalled();
|
||||
|
||||
// Finally, check that the provided mutation function
|
||||
// includes both model and configuratioon
|
||||
expect(
|
||||
mockDomainObject.useCapability.mostRecentCall.args[1]()
|
||||
).toEqual({
|
||||
someKey: "some value",
|
||||
configuration: {
|
||||
test: { someConfiguration: "something" }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -1,6 +1,7 @@
|
||||
[
|
||||
"EditActionController",
|
||||
"EditController",
|
||||
"EditRepresenter",
|
||||
"actions/CancelAction",
|
||||
"actions/EditAction",
|
||||
"actions/PropertiesAction",
|
||||
|
@ -8,10 +8,6 @@
|
||||
"key": "bottombar",
|
||||
"templateUrl": "templates/bottombar.html"
|
||||
},
|
||||
{
|
||||
"key": "switcher",
|
||||
"templateUrl": "templates/controls/switcher.html"
|
||||
},
|
||||
{
|
||||
"key": "action-button",
|
||||
"templateUrl": "templates/controls/action-button.html"
|
||||
@ -21,7 +17,7 @@
|
||||
{
|
||||
"key": "TreeNodeController",
|
||||
"implementation": "TreeNodeController.js",
|
||||
"depends": [ "$scope", "navigationService" ]
|
||||
"depends": [ "$scope", "$timeout" ]
|
||||
},
|
||||
{
|
||||
"key": "ActionGroupController",
|
||||
@ -41,6 +37,11 @@
|
||||
"key": "ClickAwayController",
|
||||
"implementation": "ClickAwayController.js",
|
||||
"depends": [ "$scope", "$document" ]
|
||||
},
|
||||
{
|
||||
"key": "ViewSwitcherController",
|
||||
"implementation": "ViewSwitcherController.js",
|
||||
"depends": [ "$scope" ]
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
@ -48,6 +49,11 @@
|
||||
"key": "mctContainer",
|
||||
"implementation": "MCTContainer.js",
|
||||
"depends": [ "containers[]" ]
|
||||
},
|
||||
{
|
||||
"key": "mctDrag",
|
||||
"implementation": "MCTDrag.js",
|
||||
"depends": [ "$document" ]
|
||||
}
|
||||
],
|
||||
"containers": [
|
||||
@ -93,6 +99,11 @@
|
||||
"key": "context-menu",
|
||||
"templateUrl": "templates/menu/context-menu.html",
|
||||
"uses": [ "action" ]
|
||||
},
|
||||
{
|
||||
"key": "switcher",
|
||||
"templateUrl": "templates/controls/switcher.html",
|
||||
"uses": [ "view" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
{{container.title}}
|
||||
</div>
|
||||
<div class="accordion-contents"
|
||||
ng-show="toggle.isActive()"
|
||||
ng-show="!toggle.isActive()"
|
||||
style="height: 180px;"
|
||||
ng-transclude>
|
||||
</div>
|
||||
|
@ -1,16 +1,19 @@
|
||||
<div class="menu-element btn icon-btn very-subtle btn-menu dropdown click-invoke"
|
||||
ng-if="ngModel.options.length > 1">
|
||||
<span ng-controller="ViewSwitcherController">
|
||||
|
||||
<span ng-click="ngModel.expanded = !ngModel.expanded">
|
||||
<div class="menu-element btn icon-btn very-subtle btn-menu dropdown click-invoke"
|
||||
ng-if="view.length > 1"
|
||||
ng-controller="ClickAwayController as toggle">
|
||||
|
||||
<span ng-click="toggle.toggle()">
|
||||
<span class="ui-symbol icon type-icon">{{ngModel.selected.glyph}}</span>
|
||||
<span>{{ngModel.selected.name}}</span>
|
||||
<span class='ui-symbol icon invoke-menu'>v</span>
|
||||
</span>
|
||||
|
||||
<div class="menu dropdown" ng-show="ngModel.expanded">
|
||||
<div class="menu dropdown" ng-show="toggle.isActive()">
|
||||
<ul>
|
||||
<li ng-repeat="option in ngModel.options">
|
||||
<a href="" ng-click="ngModel.selected = option; ngModel.expanded = false;">
|
||||
<li ng-repeat="option in view">
|
||||
<a href="" ng-click="ngModel.selected = option; toggle.setState(false)">
|
||||
<span class="ui-symbol type-icon icon">
|
||||
{{option.glyph}}
|
||||
</span>
|
||||
@ -19,10 +22,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="btn"
|
||||
ng-if="ngModel.options.length === 1">
|
||||
<span class="btn"
|
||||
ng-if="view.length === 1">
|
||||
<span class="ui-symbol icon type-icon">{{ngModel.selected.glyph}}</span>
|
||||
<span>{{ngModel.selected.name}}</span>
|
||||
</span>
|
||||
|
||||
</span>
|
130
platform/commonUI/general/src/MCTDrag.js
Normal file
130
platform/commonUI/general/src/MCTDrag.js
Normal file
@ -0,0 +1,130 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The mct-drag directive allows drag functionality
|
||||
* (in the mousedown-mousemove-mouseup sense, as opposed to
|
||||
* the drag-and-drop sense) to be attached to specific
|
||||
* elements. This takes the form of three attributes:
|
||||
*
|
||||
* * `mct-drag`: An Angular expression to evaluate during
|
||||
* drag movement.
|
||||
* * `mct-drag-down`: An Angular expression to evaluate
|
||||
* when the drag begins.
|
||||
* * `mct-drag-up`: An Angular expression to evaluate when
|
||||
* dragging ends.
|
||||
*
|
||||
* In each case, a variable `delta` will be provided to the
|
||||
* expression; this is a two-element array or the horizontal
|
||||
* and vertical pixel offset of the current mouse position
|
||||
* relative to the mouse position where dragging began.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
*/
|
||||
function MCTDrag($document) {
|
||||
|
||||
// Link; install event handlers.
|
||||
function link(scope, element, attrs) {
|
||||
// Keep a reference to the body, to attach/detach
|
||||
// mouse event handlers; mousedown and mouseup cannot
|
||||
// only be attached to the element being linked, as the
|
||||
// mouse may leave this element during the drag.
|
||||
var body = $document.find('body'),
|
||||
initialPosition,
|
||||
delta;
|
||||
|
||||
// Utility function to cause evaluation of mctDrag,
|
||||
// mctDragUp, etc
|
||||
function fireListener(name) {
|
||||
// Evaluate the expression, with current delta
|
||||
scope.$eval(attrs[name], { delta: delta });
|
||||
|
||||
// Trigger prompt digestion
|
||||
scope.$apply();
|
||||
}
|
||||
|
||||
// Update positions (both actual and relative)
|
||||
// based on a new mouse event object.
|
||||
function updatePosition(event) {
|
||||
// Get the current position, as an array
|
||||
var currentPosition = [ event.pageX, event.pageY ];
|
||||
|
||||
// Track the initial position, if one hasn't been observed
|
||||
initialPosition = initialPosition || currentPosition;
|
||||
|
||||
// Compute relative position
|
||||
delta = currentPosition.map(function (v, i) {
|
||||
return v - initialPosition[i];
|
||||
});
|
||||
}
|
||||
|
||||
// Called during a drag, on mousemove
|
||||
function continueDrag(event) {
|
||||
updatePosition(event);
|
||||
fireListener("mctDrag");
|
||||
|
||||
// Don't show selection highlights, etc
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called only when the drag ends (on mouseup)
|
||||
function endDrag(event) {
|
||||
// Detach event handlers
|
||||
body.off("mouseup", endDrag);
|
||||
body.off("mousemove", continueDrag);
|
||||
|
||||
// Also call continueDrag, to fire mctDrag
|
||||
// and do its usual position update
|
||||
continueDrag(event);
|
||||
|
||||
fireListener("mctDragUp");
|
||||
|
||||
// Clear out start-of-drag position
|
||||
initialPosition = undefined;
|
||||
|
||||
// Don't show selection highlights, etc
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called on mousedown on the element
|
||||
function startDrag(event) {
|
||||
// Listen for mouse events at the body level,
|
||||
// since the mouse may leave the element during
|
||||
// the drag.
|
||||
body.on("mouseup", endDrag);
|
||||
body.on("mousemove", continueDrag);
|
||||
|
||||
// Set an initial position
|
||||
updatePosition(event);
|
||||
|
||||
// Fire listeners, including mctDrag
|
||||
fireListener("mctDragDown");
|
||||
fireListener("mctDrag");
|
||||
|
||||
// Don't show selection highlights, etc
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Listen for mousedown on the element
|
||||
element.on("mousedown", startDrag);
|
||||
}
|
||||
|
||||
return {
|
||||
// mct-drag only makes sense as an attribute
|
||||
restrict: "A",
|
||||
// Link function, to install event handlers
|
||||
link: link
|
||||
};
|
||||
}
|
||||
|
||||
return MCTDrag;
|
||||
}
|
||||
);
|
@ -29,7 +29,7 @@ define(
|
||||
* expand-to-show-navigated-object behavior.)
|
||||
* @constructor
|
||||
*/
|
||||
function TreeNodeController($scope) {
|
||||
function TreeNodeController($scope, $timeout) {
|
||||
var selectedObject = ($scope.ngModel || {}).selectedObject,
|
||||
isSelected = false,
|
||||
hasBeenExpanded = false;
|
||||
@ -90,6 +90,17 @@ define(
|
||||
return false; // No context to judge by
|
||||
}
|
||||
|
||||
// Track that a node has been expanded, either by the
|
||||
// user or automatically to show a selection.
|
||||
function trackExpansion() {
|
||||
if (!hasBeenExpanded) {
|
||||
// Run on a timeout; if a lot of expansion needs to
|
||||
// occur (e.g. if the selection is several nodes deep) we
|
||||
// want this to be spread across multiple digest cycles.
|
||||
$timeout(function () { hasBeenExpanded = true; }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Consider the currently-navigated object and update
|
||||
// parameters which support display.
|
||||
function checkSelection() {
|
||||
@ -107,7 +118,7 @@ define(
|
||||
if (isOnSelectionPath(nodeObject, selectedObject) &&
|
||||
$scope.toggle !== undefined) {
|
||||
$scope.toggle.setState(true);
|
||||
hasBeenExpanded = true;
|
||||
trackExpansion();
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,9 +139,7 @@ define(
|
||||
* to record that this has occurred, to support one-time
|
||||
* lazy loading of the node's subtree.
|
||||
*/
|
||||
trackExpansion: function () {
|
||||
hasBeenExpanded = true;
|
||||
},
|
||||
trackExpansion: trackExpansion,
|
||||
/**
|
||||
* Check if this not has ever been expanded.
|
||||
* @returns true if it has been expanded
|
||||
|
@ -32,15 +32,10 @@ define(
|
||||
|
||||
// Get list of views, read from capability
|
||||
function updateOptions(views) {
|
||||
var options = views || [];
|
||||
|
||||
$scope.switcher = {
|
||||
options: options,
|
||||
selected: findMatchingOption(
|
||||
options,
|
||||
($scope.switcher || {}).selected
|
||||
)
|
||||
};
|
||||
$scope.ngModel.selected = findMatchingOption(
|
||||
views || [],
|
||||
($scope.ngModel || {}).selected
|
||||
);
|
||||
}
|
||||
|
||||
// Update view options when the in-scope results of using the
|
129
platform/commonUI/general/test/MCTDragSpec.js
Normal file
129
platform/commonUI/general/test/MCTDragSpec.js
Normal file
@ -0,0 +1,129 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/MCTDrag"],
|
||||
function (MCTDrag) {
|
||||
"use strict";
|
||||
|
||||
var JQLITE_METHODS = [ "on", "off", "find" ];
|
||||
|
||||
describe("The mct-drag directive", function () {
|
||||
var mockDocument,
|
||||
mockScope,
|
||||
mockElement,
|
||||
testAttrs,
|
||||
mockBody,
|
||||
mctDrag;
|
||||
|
||||
function testEvent(x, y) {
|
||||
return {
|
||||
pageX: x,
|
||||
pageY: y,
|
||||
preventDefault: jasmine.createSpy("preventDefault")
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockDocument =
|
||||
jasmine.createSpyObj("$document", JQLITE_METHODS);
|
||||
mockScope =
|
||||
jasmine.createSpyObj("$scope", [ "$eval", "$apply" ]);
|
||||
mockElement =
|
||||
jasmine.createSpyObj("element", JQLITE_METHODS);
|
||||
mockBody =
|
||||
jasmine.createSpyObj("body", JQLITE_METHODS);
|
||||
|
||||
testAttrs = {
|
||||
mctDragDown: "starting a drag",
|
||||
mctDrag: "continuing a drag",
|
||||
mctDragUp: "ending a drag"
|
||||
};
|
||||
|
||||
mockDocument.find.andReturn(mockBody);
|
||||
|
||||
mctDrag = new MCTDrag(mockDocument);
|
||||
mctDrag.link(mockScope, mockElement, testAttrs);
|
||||
});
|
||||
|
||||
it("is valid as an attribute", function () {
|
||||
expect(mctDrag.restrict).toEqual("A");
|
||||
});
|
||||
|
||||
it("listens for mousedown on its element", function () {
|
||||
expect(mockElement.on).toHaveBeenCalledWith(
|
||||
"mousedown",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Verify no interactions with body as well
|
||||
expect(mockBody.on).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes mctDragDown when dragging begins", function () {
|
||||
mockElement.on.mostRecentCall.args[1](testEvent(42, 60));
|
||||
expect(mockScope.$eval).toHaveBeenCalledWith(
|
||||
testAttrs.mctDragDown,
|
||||
{ delta: [0, 0] }
|
||||
);
|
||||
});
|
||||
|
||||
it("listens for mousemove after dragging begins", function () {
|
||||
mockElement.on.mostRecentCall.args[1](testEvent(42, 60));
|
||||
expect(mockBody.on).toHaveBeenCalledWith(
|
||||
"mousemove",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
expect(mockBody.on).toHaveBeenCalledWith(
|
||||
"mouseup",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("invokes mctDrag expression during drag", function () {
|
||||
mockElement.on.mostRecentCall.args[1](testEvent(42, 60));
|
||||
|
||||
// Find and invoke the mousemove listener
|
||||
mockBody.on.calls.forEach(function (call) {
|
||||
if (call.args[0] === 'mousemove') {
|
||||
call.args[1](testEvent(52, 200));
|
||||
}
|
||||
});
|
||||
|
||||
// Should have passed that delta to mct-drag expression
|
||||
expect(mockScope.$eval).toHaveBeenCalledWith(
|
||||
testAttrs.mctDrag,
|
||||
{ delta: [10, 140] }
|
||||
);
|
||||
});
|
||||
|
||||
it("invokes mctDragUp expression after drag", function () {
|
||||
mockElement.on.mostRecentCall.args[1](testEvent(42, 60));
|
||||
|
||||
// Find and invoke the mousemove listener
|
||||
mockBody.on.calls.forEach(function (call) {
|
||||
if (call.args[0] === 'mousemove') {
|
||||
call.args[1](testEvent(52, 200));
|
||||
}
|
||||
});
|
||||
// Find and invoke the mousemove listener
|
||||
mockBody.on.calls.forEach(function (call) {
|
||||
if (call.args[0] === 'mouseup') {
|
||||
call.args[1](testEvent(40, 71));
|
||||
}
|
||||
});
|
||||
|
||||
// Should have passed that delta to mct-drag-up expression
|
||||
// and that delta should have been relative to the
|
||||
// initial position
|
||||
expect(mockScope.$eval).toHaveBeenCalledWith(
|
||||
testAttrs.mctDragUp,
|
||||
{ delta: [-2, 11] }
|
||||
);
|
||||
|
||||
// Should also have unregistered listeners
|
||||
expect(mockBody.off).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -7,6 +7,7 @@ define(
|
||||
|
||||
describe("The tree node controller", function () {
|
||||
var mockScope,
|
||||
mockTimeout,
|
||||
controller;
|
||||
|
||||
function TestObject(id, context) {
|
||||
@ -19,13 +20,9 @@ define(
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch", "$on" ]
|
||||
);
|
||||
controller = new TreeNodeController(
|
||||
mockScope
|
||||
);
|
||||
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
|
||||
mockTimeout = jasmine.createSpy("$timeout");
|
||||
controller = new TreeNodeController(mockScope, mockTimeout);
|
||||
});
|
||||
|
||||
it("allows tracking of expansion state", function () {
|
||||
@ -34,6 +31,12 @@ define(
|
||||
// portion of the tree.
|
||||
expect(controller.hasBeenExpanded()).toBeFalsy();
|
||||
controller.trackExpansion();
|
||||
|
||||
// Expansion is tracked on a timeout, because too
|
||||
// much expansion can result in an unstable digest.
|
||||
expect(mockTimeout).toHaveBeenCalled();
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
|
||||
expect(controller.hasBeenExpanded()).toBeTruthy();
|
||||
controller.trackExpansion();
|
||||
expect(controller.hasBeenExpanded()).toBeTruthy();
|
||||
@ -85,6 +88,12 @@ define(
|
||||
// Invoke the watch with the new selection
|
||||
mockScope.$watch.calls[0].args[1](child);
|
||||
|
||||
// Expansion is tracked on a timeout, because too
|
||||
// much expansion can result in an unstable digest.
|
||||
// Trigger that timeout.
|
||||
expect(mockTimeout).toHaveBeenCalled();
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
|
||||
expect(mockScope.toggle.setState).toHaveBeenCalledWith(true);
|
||||
expect(controller.hasBeenExpanded()).toBeTruthy();
|
||||
expect(controller.isSelected()).toBeFalsy();
|
||||
|
@ -14,6 +14,7 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
|
||||
mockScope.ngModel = {};
|
||||
controller = new ViewSwitcherController(mockScope);
|
||||
});
|
||||
|
||||
@ -28,17 +29,6 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
it("updates available options to reflect views", function () {
|
||||
var views = [
|
||||
{ key: "a", name: "View A" },
|
||||
{ key: "b", name: "View B" },
|
||||
{ key: "c", name: "View C" },
|
||||
{ key: "d", name: "View D" }
|
||||
];
|
||||
mockScope.$watch.mostRecentCall.args[1](views);
|
||||
expect(mockScope.switcher.options).toEqual(views);
|
||||
});
|
||||
|
||||
it("maintains the current selection when views change", function () {
|
||||
var views = [
|
||||
{ key: "a", name: "View A" },
|
||||
@ -47,7 +37,7 @@ define(
|
||||
{ key: "d", name: "View D" }
|
||||
];
|
||||
mockScope.$watch.mostRecentCall.args[1](views);
|
||||
mockScope.switcher.selected = views[1];
|
||||
mockScope.ngModel.selected = views[1];
|
||||
|
||||
// Change the set of applicable views
|
||||
mockScope.$watch.mostRecentCall.args[1]([
|
||||
@ -57,7 +47,7 @@ define(
|
||||
]);
|
||||
|
||||
// "b" is still in there, should remain selected
|
||||
expect(mockScope.switcher.selected).toEqual(views[1]);
|
||||
expect(mockScope.ngModel.selected).toEqual(views[1]);
|
||||
});
|
||||
|
||||
it("chooses a default if a selected view becomes inapplicable", function () {
|
||||
@ -68,7 +58,7 @@ define(
|
||||
{ key: "d", name: "View D" }
|
||||
];
|
||||
mockScope.$watch.mostRecentCall.args[1](views);
|
||||
mockScope.switcher.selected = views[1];
|
||||
mockScope.ngModel.selected = views[1];
|
||||
|
||||
// Change the set of applicable views
|
||||
mockScope.$watch.mostRecentCall.args[1]([
|
||||
@ -78,7 +68,7 @@ define(
|
||||
]);
|
||||
|
||||
// "b" is still in there, should remain selected
|
||||
expect(mockScope.switcher.selected).not.toEqual(views[1]);
|
||||
expect(mockScope.ngModel.selected).not.toEqual(views[1]);
|
||||
});
|
||||
|
||||
});
|
@ -3,6 +3,8 @@
|
||||
"ClickAwayController",
|
||||
"ContextMenuController",
|
||||
"MCTContainer",
|
||||
"MCTDrag",
|
||||
"ToggleController",
|
||||
"TreeNodeController"
|
||||
"TreeNodeController",
|
||||
"ViewSwitcherController"
|
||||
]
|
@ -59,7 +59,11 @@ define(
|
||||
// Allow mutators to change their mind by
|
||||
// returning false.
|
||||
if (mutationResult !== false) {
|
||||
// Copy values if result was a different object
|
||||
// (either our clone or some other new thing)
|
||||
if (model !== result) {
|
||||
copyValues(model, result);
|
||||
}
|
||||
model.modified = Date.now();
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,66 @@
|
||||
"name": "Layout components.",
|
||||
"description": "Plug in adding Layout capabiltiies.",
|
||||
"extensions": {
|
||||
"views": [
|
||||
{
|
||||
"key": "layout",
|
||||
"name": "Layout",
|
||||
"glyph": "L",
|
||||
"type": "layout",
|
||||
"templateUrl": "templates/layout.html",
|
||||
"uses": [ "composition" ]
|
||||
}
|
||||
],
|
||||
"representations": [
|
||||
{
|
||||
"key": "frame",
|
||||
"templateUrl": "templates/frame.html"
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "LayoutController",
|
||||
"implementation": "LayoutController.js",
|
||||
"depends": [ "$scope" ]
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"key": "layout",
|
||||
"name": "Layout",
|
||||
"glyph": "L",
|
||||
"description": "A layout in which multiple telemetry panels may be displayed.",
|
||||
"features": "creation",
|
||||
"model": { "composition": [] },
|
||||
"properties": [
|
||||
{
|
||||
"name": "Preferred Size",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{ "name": "Fit to Window", "value": "FIT" },
|
||||
{ "name": "Fixed Size", "value": "FIXED" }
|
||||
],
|
||||
"key": "preferredSize"
|
||||
},
|
||||
{
|
||||
"label": "Layout Grid",
|
||||
"control": "composite",
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$",
|
||||
"items": [
|
||||
{
|
||||
"name": "Horizontal grid (px)",
|
||||
"control": "textfield"
|
||||
},
|
||||
{
|
||||
"name": "Vertical grid (px)",
|
||||
"control": "textfield"
|
||||
}
|
||||
],
|
||||
"key": "layoutGrid",
|
||||
"conversion": "number[]"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "telemetry.panel",
|
||||
"name": "Telemetry Panel",
|
||||
|
20
platform/features/layout/res/templates/frame.html
Normal file
20
platform/features/layout/res/templates/frame.html
Normal file
@ -0,0 +1,20 @@
|
||||
<div class="frame frame-template abs">
|
||||
<div class="bar abs object-header object-top-bar">
|
||||
<div class="title left abs">
|
||||
<mct-representation key="'node'"
|
||||
mct-object="domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
<div class="btn-bar right abs">
|
||||
<mct-representation key="'switcher'"
|
||||
ng-model="representation"
|
||||
mct-object="domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
<div class="abs object-holder">
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="domainObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
68
platform/features/layout/res/templates/layout.html
Normal file
68
platform/features/layout/res/templates/layout.html
Normal file
@ -0,0 +1,68 @@
|
||||
<div style="width: 100%; height: 100%;"
|
||||
ng-controller="LayoutController as controller">
|
||||
|
||||
<div class='frame child-frame panel abs'
|
||||
ng-repeat="childObject in composition"
|
||||
ng-style="controller.getFrameStyle(childObject.getId())">
|
||||
|
||||
<div class="frame child-frame holder contents abs">
|
||||
<mct-representation key="'frame'"
|
||||
mct-object="childObject">
|
||||
</mct-representation>
|
||||
</div>
|
||||
|
||||
<!-- Drag handles -->
|
||||
<span ng-show="domainObject.hasCapability('editor')">
|
||||
<span style="position: absolute; left: 12px; right: 12px; top: 12px; bottom: 12px; cursor: move;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
|
||||
<span style="position: absolute; left: 0px; width: 12px; top: 12px; bottom: 12px; cursor: w-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [1,0], [-1,0])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
<span style="position: absolute; right: 0px; width: 12px; top: 12px; bottom: 12px; cursor: e-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [0,0], [1,0])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
|
||||
<span style="position: absolute; left: 12px; right: 12px; top: 0px; height: 12px; cursor: n-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [0,1], [0,-1])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
<span style="position: absolute; left: 12px; right: 12px; bottom: 0px; height: 12px; cursor: s-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [0,0], [0,1])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
|
||||
<span style="position: absolute; left: 0px; width: 12px; top: 0px; height: 12px; cursor: nw-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [-1,-1])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
<span style="position: absolute; right: 0px; width: 12px; top: 0px; height: 12px; cursor: ne-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [0,1], [1,-1])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
<span style="position: absolute; left: 0px; width: 12px; bottom: 0px; height: 12px; cursor: sw-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [1,0], [-1,1])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
<span style="position: absolute; right: 0px; width: 12px; bottom: 0px; height: 12px; cursor: se-resize;"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [0,0], [1,1])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
172
platform/features/layout/src/LayoutController.js
Normal file
172
platform/features/layout/src/LayoutController.js
Normal file
@ -0,0 +1,172 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./LayoutDrag'],
|
||||
function (LayoutDrag) {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_DIMENSIONS = [ 12, 8 ],
|
||||
DEFAULT_GRID_SIZE = [32, 32];
|
||||
|
||||
/**
|
||||
* The LayoutController is responsible for supporting the
|
||||
* Layout view. It arranges frames according to saved configuration
|
||||
* and provides methods for updating these based on mouse
|
||||
* movement.
|
||||
* @constructor
|
||||
* @param {Scope} $scope the controller's Angular scope
|
||||
*/
|
||||
function LayoutController($scope) {
|
||||
var gridSize = DEFAULT_GRID_SIZE,
|
||||
activeDrag,
|
||||
activeDragId,
|
||||
rawPositions = {},
|
||||
positions = {};
|
||||
|
||||
// Utility function to copy raw positions from configuration,
|
||||
// without writing directly to configuration (to avoid triggering
|
||||
// persistence from watchers during drags).
|
||||
function shallowCopy(obj, keys) {
|
||||
var copy = {};
|
||||
keys.forEach(function (k) {
|
||||
copy[k] = obj[k];
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
// Convert from { positions: ..., dimensions: ... } to an
|
||||
// apropriate ng-style argument, to position frames.
|
||||
function convertPosition(raw) {
|
||||
// Multiply position/dimensions by grid size
|
||||
return {
|
||||
left: (gridSize[0] * raw.position[0]) + 'px',
|
||||
top: (gridSize[1] * raw.position[1]) + 'px',
|
||||
width: (gridSize[0] * raw.dimensions[0]) + 'px',
|
||||
height: (gridSize[1] * raw.dimensions[1]) + 'px'
|
||||
};
|
||||
}
|
||||
|
||||
// Generate a default position (in its raw format) for a frame.
|
||||
// Use an index to ensure that default positions are unique.
|
||||
function defaultPosition(index) {
|
||||
return {
|
||||
position: [index, index],
|
||||
dimensions: DEFAULT_DIMENSIONS
|
||||
};
|
||||
}
|
||||
|
||||
// Store a computed position for a contained frame by its
|
||||
// domain object id. Called in a forEach loop, so arguments
|
||||
// are as expected there.
|
||||
function populatePosition(id, index) {
|
||||
rawPositions[id] =
|
||||
rawPositions[id] || defaultPosition(index || 0);
|
||||
positions[id] =
|
||||
convertPosition(rawPositions[id]);
|
||||
}
|
||||
|
||||
// Compute panel positions based on the layout's object model
|
||||
function lookupPanels(model) {
|
||||
var configuration = $scope.configuration || {},
|
||||
ids = (model || {}).composition || [];
|
||||
|
||||
// Pull panel positions from configuration
|
||||
rawPositions = shallowCopy(configuration.panels || {}, ids);
|
||||
|
||||
// Clear prior computed positions
|
||||
positions = {};
|
||||
|
||||
// Update width/height that we are tracking
|
||||
gridSize = (model || {}).layoutGrid || DEFAULT_GRID_SIZE;
|
||||
|
||||
// Compute positions and add defaults where needed
|
||||
ids.forEach(populatePosition);
|
||||
}
|
||||
|
||||
// Position panes when the model field changes
|
||||
$scope.$watch("model", lookupPanels);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get a style object for a frame with the specified domain
|
||||
* object identifier, suitable for use in an `ng-style`
|
||||
* directive to position a frame as configured for this layout.
|
||||
* @param {string} id the object identifier
|
||||
* @returns {Object.<string, string>} an object with
|
||||
* appropriate left, width, etc fields for positioning
|
||||
*/
|
||||
getFrameStyle: function (id) {
|
||||
// Called in a loop, so just look up; the "positions"
|
||||
// object is kept up to date by a watch.
|
||||
return positions[id];
|
||||
},
|
||||
/**
|
||||
* Start a drag gesture to move/resize a frame.
|
||||
*
|
||||
* The provided position and dimensions factors will determine
|
||||
* whether this is a move or a resize, and what type it
|
||||
* will be. For instance, a position factor of [1, 1]
|
||||
* will move a frame along with the mouse as the drag
|
||||
* proceeds, while a dimension factor of [0, 0] will leave
|
||||
* dimensions unchanged. Combining these in different
|
||||
* ways results in different handles; a position factor of
|
||||
* [1, 0] and a dimensions factor of [-1, 0] will implement
|
||||
* a left-edge resize, as the horizontal position will move
|
||||
* with the mouse while the horizontal dimensions shrink in
|
||||
* kind (and vertical properties remain unmodified.)
|
||||
*
|
||||
* @param {string} id the identifier of the domain object
|
||||
* in the frame being manipulated
|
||||
* @param {number[]} posFactor the position factor
|
||||
* @param {number[]} dimFactor the dimensions factor
|
||||
*/
|
||||
startDrag: function (id, posFactor, dimFactor) {
|
||||
activeDragId = id;
|
||||
activeDrag = new LayoutDrag(
|
||||
rawPositions[id],
|
||||
posFactor,
|
||||
dimFactor,
|
||||
gridSize
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Continue an active drag gesture.
|
||||
* @param {number[]} delta the offset, in pixels,
|
||||
* of the current pointer position, relative
|
||||
* to its position when the drag started
|
||||
*/
|
||||
continueDrag: function (delta) {
|
||||
if (activeDrag) {
|
||||
rawPositions[activeDragId] =
|
||||
activeDrag.getAdjustedPosition(delta);
|
||||
populatePosition(activeDragId);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* End the active drag gesture. This will update the
|
||||
* view configuration.
|
||||
*/
|
||||
endDrag: function () {
|
||||
// Write to configuration; this is watched and
|
||||
// saved by the EditRepresenter.
|
||||
$scope.configuration =
|
||||
$scope.configuration || {};
|
||||
// Make sure there is a "panels" field in the
|
||||
// view configuration.
|
||||
$scope.configuration.panels =
|
||||
$scope.configuration.panels || {};
|
||||
// Store the position of this panel.
|
||||
$scope.configuration.panels[activeDragId] =
|
||||
rawPositions[activeDragId];
|
||||
// Mark this object as dirty to encourage persistence
|
||||
if ($scope.commit) {
|
||||
$scope.commit("Moved frame.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return LayoutController;
|
||||
}
|
||||
);
|
93
platform/features/layout/src/LayoutDrag.js
Normal file
93
platform/features/layout/src/LayoutDrag.js
Normal file
@ -0,0 +1,93 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Handles drag interactions on frames in layouts. This will
|
||||
* provides new positions/dimensions for frames based on
|
||||
* relative pixel positions provided; these will take into account
|
||||
* the grid size (in a snap-to sense) and will enforce some minimums
|
||||
* on both position and dimensions.
|
||||
*
|
||||
* The provided position and dimensions factors will determine
|
||||
* whether this is a move or a resize, and what type of resize it
|
||||
* will be. For instance, a position factor of [1, 1]
|
||||
* will move a frame along with the mouse as the drag
|
||||
* proceeds, while a dimension factor of [0, 0] will leave
|
||||
* dimensions unchanged. Combining these in different
|
||||
* ways results in different handles; a position factor of
|
||||
* [1, 0] and a dimensions factor of [-1, 0] will implement
|
||||
* a left-edge resize, as the horizontal position will move
|
||||
* with the mouse while the horizontal dimensions shrink in
|
||||
* kind (and vertical properties remain unmodified.)
|
||||
*
|
||||
* @param {object} rawPosition the initial position/dimensions
|
||||
* of the frame being interacted with
|
||||
* @param {number[]} posFactor the position factor
|
||||
* @param {number[]} dimFactor the dimensions factor
|
||||
* @param {number[]} the size of each grid element, in pixels
|
||||
*/
|
||||
function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) {
|
||||
// Convert a delta from pixel coordinates to grid coordinates,
|
||||
// rounding to whole-number grid coordinates.
|
||||
function toGridDelta(pixelDelta) {
|
||||
return pixelDelta.map(function (v, i) {
|
||||
return Math.round(v / gridSize[i]);
|
||||
});
|
||||
}
|
||||
|
||||
// Utility function to perform element-by-element multiplication
|
||||
function multiply(array, factors) {
|
||||
return array.map(function (v, i) {
|
||||
return v * factors[i];
|
||||
});
|
||||
}
|
||||
|
||||
// Utility function to perform element-by-element addition
|
||||
function add(array, other) {
|
||||
return array.map(function (v, i) {
|
||||
return v + other[i];
|
||||
});
|
||||
}
|
||||
|
||||
// Utility function to perform element-by-element max-choosing
|
||||
function max(array, other) {
|
||||
return array.map(function (v, i) {
|
||||
return Math.max(v, other[i]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAdjustedPosition(pixelDelta) {
|
||||
var gridDelta = toGridDelta(pixelDelta);
|
||||
return {
|
||||
position: max(add(
|
||||
rawPosition.position,
|
||||
multiply(gridDelta, posFactor)
|
||||
), [0, 0]),
|
||||
dimensions: max(add(
|
||||
rawPosition.dimensions,
|
||||
multiply(gridDelta, dimFactor)
|
||||
), [1, 1])
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get a new position object in grid coordinates, with
|
||||
* position and dimensions both offset appropriately
|
||||
* according to the factors supplied in the constructor.
|
||||
* @param {number[]} pixelDelta the offset from the
|
||||
* original position, in pixels
|
||||
*/
|
||||
getAdjustedPosition: getAdjustedPosition
|
||||
};
|
||||
}
|
||||
|
||||
return LayoutDrag;
|
||||
|
||||
}
|
||||
);
|
115
platform/features/layout/test/LayoutControllerSpec.js
Normal file
115
platform/features/layout/test/LayoutControllerSpec.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/LayoutController"],
|
||||
function (LayoutController) {
|
||||
"use strict";
|
||||
|
||||
describe("The Layout controller", function () {
|
||||
var mockScope,
|
||||
testModel,
|
||||
testConfiguration,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$watch" ]
|
||||
);
|
||||
|
||||
testModel = {
|
||||
composition: [ "a", "b", "c" ]
|
||||
};
|
||||
|
||||
testConfiguration = {
|
||||
panels: {
|
||||
a: {
|
||||
position: [20, 10],
|
||||
dimensions: [5, 5]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockScope.model = testModel;
|
||||
mockScope.configuration = testConfiguration;
|
||||
|
||||
controller = new LayoutController(mockScope);
|
||||
});
|
||||
|
||||
// Model changes will indicate that panel positions
|
||||
// may have changed, for instance.
|
||||
it("watches for changes to model", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"model",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("provides styles for frames, from configuration", function () {
|
||||
mockScope.$watch.mostRecentCall.args[1](testModel);
|
||||
expect(controller.getFrameStyle("a")).toEqual({
|
||||
top: "320px",
|
||||
left: "640px",
|
||||
width: "160px",
|
||||
height: "160px"
|
||||
});
|
||||
});
|
||||
|
||||
it("provides default styles for frames", function () {
|
||||
var styleB, styleC;
|
||||
|
||||
// b and c do not have configured positions
|
||||
mockScope.$watch.mostRecentCall.args[1](testModel);
|
||||
|
||||
styleB = controller.getFrameStyle("b");
|
||||
styleC = controller.getFrameStyle("c");
|
||||
|
||||
// Should have a position, but we don't care what
|
||||
expect(styleB.left).toBeDefined();
|
||||
expect(styleB.top).toBeDefined();
|
||||
expect(styleC.left).toBeDefined();
|
||||
expect(styleC.top).toBeDefined();
|
||||
|
||||
// Should have ensured some difference in position
|
||||
expect(styleB).not.toEqual(styleC);
|
||||
});
|
||||
|
||||
it("allows panels to be dragged", function () {
|
||||
// Populate scope
|
||||
mockScope.$watch.mostRecentCall.args[1](testModel);
|
||||
|
||||
// Verify precondtion
|
||||
expect(testConfiguration.panels.b).not.toBeDefined();
|
||||
|
||||
// Do a drag
|
||||
controller.startDrag("b", [1, 1], [0, 0]);
|
||||
controller.continueDrag([100, 100]);
|
||||
controller.endDrag();
|
||||
|
||||
// We do not look closely at the details here;
|
||||
// that is tested in LayoutDragSpec. Just make sure
|
||||
// that a configuration for b has been defined.
|
||||
expect(testConfiguration.panels.b).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
it("invokes commit after drag", function () {
|
||||
// Populate scope
|
||||
mockScope.$watch.mostRecentCall.args[1](testModel);
|
||||
|
||||
// Add a commit method to scope
|
||||
mockScope.commit = jasmine.createSpy("commit");
|
||||
|
||||
// Do a drag
|
||||
controller.startDrag("b", [1, 1], [0, 0]);
|
||||
controller.continueDrag([100, 100]);
|
||||
controller.endDrag();
|
||||
|
||||
// Should have triggered commit (provided by
|
||||
// EditRepresenter) with some message.
|
||||
expect(mockScope.commit)
|
||||
.toHaveBeenCalledWith(jasmine.any(String));
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
62
platform/features/layout/test/LayoutDragSpec.js
Normal file
62
platform/features/layout/test/LayoutDragSpec.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/LayoutDrag"],
|
||||
function (LayoutDrag) {
|
||||
"use strict";
|
||||
|
||||
describe("A Layout drag handler", function () {
|
||||
var testPosition = {
|
||||
position: [ 8, 11 ],
|
||||
dimensions: [ 3, 2 ]
|
||||
};
|
||||
|
||||
it("changes position by a supplied factor, rounding by grid size", function () {
|
||||
var handler = new LayoutDrag(
|
||||
testPosition,
|
||||
[ 1, 1 ],
|
||||
[ 0, 0 ],
|
||||
[ 10, 20 ]
|
||||
);
|
||||
|
||||
expect(handler.getAdjustedPosition([ 37, 84 ])).toEqual({
|
||||
position: [ 12, 15 ],
|
||||
dimensions: [ 3, 2 ]
|
||||
});
|
||||
expect(handler.getAdjustedPosition([ -37, 84 ])).toEqual({
|
||||
position: [ 4, 15 ],
|
||||
dimensions: [ 3, 2 ]
|
||||
});
|
||||
});
|
||||
|
||||
it("changes dimensions by a supplied factor, rounding by grid size", function () {
|
||||
var handler = new LayoutDrag(
|
||||
testPosition,
|
||||
[ 0, 0 ],
|
||||
[ 1, 1 ],
|
||||
[ 10, 20 ]
|
||||
);
|
||||
|
||||
expect(handler.getAdjustedPosition([ 37, 84 ])).toEqual({
|
||||
position: [ 8, 11 ],
|
||||
dimensions: [ 7, 6 ]
|
||||
});
|
||||
});
|
||||
|
||||
it("allows mixing dimension and position factors", function () {
|
||||
var handler = new LayoutDrag(
|
||||
testPosition,
|
||||
[ 0, 1 ],
|
||||
[ -1, 0 ],
|
||||
[ 10, 20 ]
|
||||
);
|
||||
|
||||
expect(handler.getAdjustedPosition([ 11, 84 ])).toEqual({
|
||||
position: [ 8, 15 ],
|
||||
dimensions: [ 2, 2 ]
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
4
platform/features/layout/test/suite.json
Normal file
4
platform/features/layout/test/suite.json
Normal file
@ -0,0 +1,4 @@
|
||||
[
|
||||
"LayoutController",
|
||||
"LayoutDrag"
|
||||
]
|
22030
platform/framework/lib/angular.js
vendored
Normal file
22030
platform/framework/lib/angular.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8
platform/framework/lib/angular.min.js.map
Normal file
8
platform/framework/lib/angular.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -9,7 +9,7 @@
|
||||
{
|
||||
"key": "mctRepresentation",
|
||||
"implementation": "MCTRepresentation.js",
|
||||
"depends": [ "representations[]", "views[]", "gestureService", "$q", "$log" ]
|
||||
"depends": [ "representations[]", "views[]", "representers[]", "$q", "$log" ]
|
||||
}
|
||||
],
|
||||
"gestures": [
|
||||
@ -36,6 +36,12 @@
|
||||
"implementation": "gestures/GestureProvider.js",
|
||||
"depends": ["gestures[]"]
|
||||
}
|
||||
],
|
||||
"representers": [
|
||||
{
|
||||
"implementation": "gestures/GestureRepresenter.js",
|
||||
"depends": [ "gestureService" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ define(
|
||||
* representation extensions
|
||||
* @param {ViewDefinition[]} views an array of view extensions
|
||||
*/
|
||||
function MCTRepresentation(representations, views, gestureService, $q, $log) {
|
||||
function MCTRepresentation(representations, views, representers, $q, $log) {
|
||||
var pathMap = {},
|
||||
representationMap = {},
|
||||
gestureMap = {};
|
||||
@ -52,8 +52,10 @@ define(
|
||||
});
|
||||
|
||||
|
||||
function link($scope, element) {
|
||||
var gestureHandle;
|
||||
function link($scope, element, attrs) {
|
||||
var activeRepresenters = representers.map(function (Representer) {
|
||||
return new Representer($scope, element, attrs);
|
||||
});
|
||||
|
||||
// General-purpose refresh mechanism; should set up the scope
|
||||
// as appropriate for current representation key and
|
||||
@ -73,9 +75,9 @@ define(
|
||||
$scope.inclusion = pathMap[$scope.key];
|
||||
|
||||
// Any existing gestures are no longer valid; release them.
|
||||
if (gestureHandle) {
|
||||
gestureHandle.destroy();
|
||||
}
|
||||
activeRepresenters.forEach(function (activeRepresenter) {
|
||||
activeRepresenter.destroy();
|
||||
});
|
||||
|
||||
// Log if a key was given, but no matching representation
|
||||
// was found.
|
||||
@ -89,6 +91,11 @@ define(
|
||||
// Always provide the model, as "model"
|
||||
$scope.model = domainObject.getModel();
|
||||
|
||||
// Also provide the view configuration,
|
||||
// for the specific view
|
||||
$scope.configuration =
|
||||
($scope.model.configuration || {})[$scope.key] || {};
|
||||
|
||||
// Also provide any of the capabilities requested
|
||||
uses.forEach(function (used) {
|
||||
$log.debug([
|
||||
@ -104,13 +111,11 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, wire up any gestures that should be
|
||||
// associated with this representation.
|
||||
gestureHandle = gestureService.attachGestures(
|
||||
element,
|
||||
domainObject,
|
||||
gestureKeys
|
||||
);
|
||||
// Finally, wire up any additional behavior (such as
|
||||
// gestures) associated with this representation.
|
||||
activeRepresenters.forEach(function (representer) {
|
||||
representer.represent(representation, domainObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +131,12 @@ define(
|
||||
// same domain object; these changes should be tracked in the
|
||||
// model's "modified" field, by the mutation capability.
|
||||
$scope.$watch("domainObject.getModel().modified", refresh);
|
||||
|
||||
// Do one initial refresh, so that we don't need another
|
||||
// digest iteration just to populate the scope. Failure to
|
||||
// do this can result in unstable digest cycles, which
|
||||
// Angular will detect, and throw an Error about.
|
||||
refresh();
|
||||
}
|
||||
|
||||
return {
|
||||
|
62
platform/representation/src/gestures/GestureRepresenter.js
Normal file
62
platform/representation/src/gestures/GestureRepresenter.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The GestureRepresenter is responsible for installing predefined
|
||||
* gestures upon mct-representation instances.
|
||||
* Gestures themselves are pulled from the gesture service; this
|
||||
* simply wraps that behavior in a Representer interface, such that
|
||||
* it may be included among other such Representers used to prepare
|
||||
* specific representations.
|
||||
* @param {GestureService} gestureService the service which provides
|
||||
* gestures
|
||||
* @param {Scope} scope the Angular scope for this representation
|
||||
* @param element the JQLite-wrapped mct-representation element
|
||||
*/
|
||||
function GestureRepresenter(gestureService, scope, element) {
|
||||
var gestureHandle;
|
||||
|
||||
function destroy() {
|
||||
// Release any resources associated with these gestures
|
||||
if (gestureHandle) {
|
||||
gestureHandle.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function represent(representation, domainObject) {
|
||||
// Clear out any existing gestures
|
||||
destroy();
|
||||
|
||||
// Attach gestures - by way of the service.
|
||||
gestureHandle = gestureService.attachGestures(
|
||||
element,
|
||||
domainObject,
|
||||
(representation || {}).gestures || []
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Set the current representation in use, and the domain
|
||||
* object being represented.
|
||||
*
|
||||
* @param {RepresentationDefinition} representation the
|
||||
* definition of the representation in use
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* being represented
|
||||
*/
|
||||
represent: represent,
|
||||
/**
|
||||
* Release any resources associated with this representer.
|
||||
*/
|
||||
destroy: destroy
|
||||
};
|
||||
}
|
||||
|
||||
return GestureRepresenter;
|
||||
}
|
||||
);
|
@ -15,13 +15,13 @@ define(
|
||||
describe("The mct-representation directive", function () {
|
||||
var testRepresentations,
|
||||
testViews,
|
||||
mockGestureService,
|
||||
mockGestureHandle,
|
||||
mockRepresenters,
|
||||
mockQ,
|
||||
mockLog,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockDomainObject,
|
||||
testModel,
|
||||
mctRepresentation;
|
||||
|
||||
function mockPromise(value) {
|
||||
@ -61,10 +61,17 @@ define(
|
||||
}
|
||||
];
|
||||
|
||||
mockGestureService = jasmine.createSpyObj("gestureService", [ "attachGestures" ]);
|
||||
mockGestureHandle = jasmine.createSpyObj("gestureHandle", [ "destroy" ]);
|
||||
testModel = { someKey: "some value" };
|
||||
|
||||
mockGestureService.attachGestures.andReturn(mockGestureHandle);
|
||||
mockRepresenters = ["A", "B"].map(function (name) {
|
||||
var constructor = jasmine.createSpy("Representer" + name),
|
||||
representer = jasmine.createSpyObj(
|
||||
"representer" + name,
|
||||
[ "represent", "destroy" ]
|
||||
);
|
||||
constructor.andReturn(representer);
|
||||
return constructor;
|
||||
});
|
||||
|
||||
mockQ = { when: mockPromise };
|
||||
mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS);
|
||||
@ -73,10 +80,12 @@ define(
|
||||
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
|
||||
|
||||
mockDomainObject.getModel.andReturn(testModel);
|
||||
|
||||
mctRepresentation = new MCTRepresentation(
|
||||
testRepresentations,
|
||||
testViews,
|
||||
mockGestureService,
|
||||
mockRepresenters,
|
||||
mockQ,
|
||||
mockLog
|
||||
);
|
||||
@ -137,32 +146,6 @@ define(
|
||||
.toHaveBeenCalledWith("otherTestCapability");
|
||||
});
|
||||
|
||||
it("attaches declared gestures, and detaches on refresh", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
mockScope.key = "uvw";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Trigger the watch
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
|
||||
expect(mockGestureService.attachGestures).toHaveBeenCalledWith(
|
||||
mockElement,
|
||||
mockDomainObject,
|
||||
[ "testGesture", "otherTestGesture" ]
|
||||
);
|
||||
|
||||
expect(mockGestureHandle.destroy).not.toHaveBeenCalled();
|
||||
|
||||
// Refresh, expect a detach
|
||||
mockScope.key = "abc";
|
||||
mockScope.$watch.mostRecentCall.args[1]();
|
||||
|
||||
// Should have destroyed those old gestures
|
||||
expect(mockGestureHandle.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("logs when no representation is available for a key", function () {
|
||||
mctRepresentation.link(mockScope, mockElement);
|
||||
|
||||
|
@ -0,0 +1,62 @@
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/gestures/GestureRepresenter"],
|
||||
function (GestureRepresenter) {
|
||||
"use strict";
|
||||
|
||||
describe("A gesture representer", function () {
|
||||
var mockGestureService,
|
||||
mockGestureHandle,
|
||||
mockScope,
|
||||
mockElement,
|
||||
representer;
|
||||
|
||||
beforeEach(function () {
|
||||
mockGestureService = jasmine.createSpyObj(
|
||||
"gestureService",
|
||||
[ "attachGestures" ]
|
||||
);
|
||||
mockGestureHandle = jasmine.createSpyObj(
|
||||
"gestureHandle",
|
||||
[ "destroy" ]
|
||||
);
|
||||
|
||||
mockElement = { someKey: "some value" };
|
||||
|
||||
mockGestureService.attachGestures.andReturn(mockGestureHandle);
|
||||
|
||||
representer = new GestureRepresenter(
|
||||
mockGestureService,
|
||||
undefined, // Scope is not used
|
||||
mockElement
|
||||
);
|
||||
});
|
||||
|
||||
it("attaches declared gestures, and detaches on request", function () {
|
||||
// Pass in some objects, which we expect to be passed into the
|
||||
// gesture service accordingly.
|
||||
var domainObject = { someOtherKey: "some other value" },
|
||||
representation = { gestures: ["a", "b", "c"] };
|
||||
|
||||
representer.represent(representation, domainObject);
|
||||
|
||||
expect(mockGestureService.attachGestures).toHaveBeenCalledWith(
|
||||
mockElement,
|
||||
domainObject,
|
||||
[ "a", "b", "c" ]
|
||||
);
|
||||
|
||||
// Should not have been destroyed yet...
|
||||
expect(mockGestureHandle.destroy).not.toHaveBeenCalled();
|
||||
|
||||
// Destroy
|
||||
representer.destroy();
|
||||
|
||||
// Should have destroyed those old gestures
|
||||
expect(mockGestureHandle.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -3,6 +3,7 @@
|
||||
"gestures/DragGesture",
|
||||
"gestures/DropGesture",
|
||||
"gestures/GestureProvider",
|
||||
"gestures/GestureRepresenter",
|
||||
"MCTInclude",
|
||||
"MCTRepresentation"
|
||||
]
|
Loading…
Reference in New Issue
Block a user