Merge remote-tracking branch 'origin/wtd535' into open-master

This commit is contained in:
bwyu 2014-12-09 14:12:50 -08:00
commit ce608eedc3
36 changed files with 23379 additions and 142 deletions

View File

@ -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": [

View File

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

View File

@ -1,6 +1,5 @@
[
"BrowseController",
"ViewSwitcherController",
"creation/CreateAction",
"creation/CreateActionProvider",
"creation/CreateMenuController",

View File

@ -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" ]
}
]
}

View File

@ -1,22 +1,23 @@
<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>
<div class="splitter" style="right: 200px"></div>
<div class='split-pane-component edit-objects pane menus-to-left'
style="right: 0px; width: 200px">
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>

View File

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

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

View 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" }
}
});
});
});
}
);

View File

@ -1,6 +1,7 @@
[
"EditActionController",
"EditController",
"EditRepresenter",
"actions/CancelAction",
"actions/EditAction",
"actions/PropertiesAction",

View File

@ -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" ]
}
]
}

View File

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

View File

@ -1,28 +1,33 @@
<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="toggle.isActive()">
<ul>
<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>
{{option.name}}
</a>
</li>
</ul>
</div>
</div>
<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 class='ui-symbol icon invoke-menu'>v</span>
</span>
<div class="menu dropdown" ng-show="ngModel.expanded">
<ul>
<li ng-repeat="option in ngModel.options">
<a href="" ng-click="ngModel.selected = option; ngModel.expanded = false;">
<span class="ui-symbol type-icon icon">
{{option.glyph}}
</span>
{{option.name}}
</a>
</li>
</ul>
</div>
</div>
<span class="btn"
ng-if="ngModel.options.length === 1">
<span class="ui-symbol icon type-icon">{{ngModel.selected.glyph}}</span>
<span>{{ngModel.selected.name}}</span>
</span>

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

View File

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

View File

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

View 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();
});
});
}
);

View File

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

View File

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

View File

@ -3,6 +3,8 @@
"ClickAwayController",
"ContextMenuController",
"MCTContainer",
"MCTDrag",
"ToggleController",
"TreeNodeController"
"TreeNodeController",
"ViewSwitcherController"
]

View File

@ -59,7 +59,11 @@ define(
// Allow mutators to change their mind by
// returning false.
if (mutationResult !== false) {
copyValues(model, result);
// 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();
}

View File

@ -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",

View 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>

View 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>

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

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

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

View 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 ]
});
});
});
}
);

View File

@ -0,0 +1,4 @@
[
"LayoutController",
"LayoutDrag"
]

22030
platform/framework/lib/angular.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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" ]
}
]
}
}

View File

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

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
"gestures/DragGesture",
"gestures/DropGesture",
"gestures/GestureProvider",
"gestures/GestureRepresenter",
"MCTInclude",
"MCTRepresentation"
]