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

Conflicts:
	platform/commonUI/edit/res/templates/edit-object.html
This commit is contained in:
bwyu 2015-02-26 11:01:00 -08:00
commit 970b5e70ba
27 changed files with 896 additions and 184 deletions

View File

@ -15,6 +15,8 @@ view's scope.) These additional properties are:
then that function is assumed to be an accessor-mutator function
(that is, it will be called with no arguments to get, and with
an argument to set.)
* `method`: Name of a method to invoke upon a selected object when
a control is activated, e.g. on a button click.
* `inclusive`: Optional; true if this control should be considered
applicable whenever at least one element in the selection has
the associated property. Otherwise, all members of the current

View File

@ -12,7 +12,7 @@
<div class='holder abs object-holder'>
<mct-representation key="representation.selected.key"
toolbar="toolbar"
mct-object="domainObject">
mct-object="representation.selected.key && domainObject">
</mct-representation>
</div>
</div>

View File

@ -17,9 +17,10 @@ define(
*
* @param structure toolbar structure, as provided by view definition
* @param {Array} selection the current selection state
* @param {Function} commit callback to invoke after changes
* @constructor
*/
function EditToolbar(structure, selection) {
function EditToolbar(structure, selection, commit) {
var toolbarStructure = Object.create(structure || {}),
toolbarState,
properties = [];
@ -106,23 +107,46 @@ define(
// to the current selection.
function isApplicable(item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !(item || {}).inclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
return selected[property] !== undefined;
return (property && (selected[property] !== undefined)) ||
(method && (typeof selected[method] === 'function'));
}
return property && selection.map(hasProperty).reduce(
return selection.map(hasProperty).reduce(
exclusive ? and : or,
exclusive
) && isConsistent(property);
}
// Invoke all functions in selections with the given name
function invoke(method, value) {
if (method) {
// Make the change in the selection
selection.forEach(function (selected) {
if (typeof selected[method] === 'function') {
selected[method](value);
}
});
// ...and commit!
commit();
}
}
// Prepare a toolbar item based on current selection
function convertItem(item) {
var converted = Object.create(item || {});
converted.key = addKey(item.property);
if (item.property) {
converted.key = addKey(item.property);
}
if (item.method) {
converted.click = function (v) {
invoke(item.method, v);
};
}
return converted;
}

View File

@ -20,6 +20,13 @@ define(
toolbar,
toolbarObject = {};
// Mark changes as ready to persist
function commit(message) {
if (scope.commit) {
scope.commit(message);
}
}
// Handle changes to the current selection
function updateSelection(selection) {
// Make sure selection is array-like
@ -28,7 +35,7 @@ define(
(selection ? [selection] : []);
// Instantiate a new toolbar...
toolbar = new EditToolbar(definition, selection);
toolbar = new EditToolbar(definition, selection, commit);
// ...and expose its structure/state
toolbarObject.structure = toolbar.getStructure();
@ -37,9 +44,12 @@ define(
// Update selection models to match changed toolbar state
function updateState(state) {
// Update underlying state based on toolbar changes
state.forEach(function (value, index) {
toolbar.updateState(index, value);
});
// Commit the changes.
commit("Changes from toolbar.");
}
// Represent a domain object using this definition

View File

@ -15,7 +15,7 @@ define(
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
[ '$on', '$watch', '$watchCollection' ]
[ '$on', '$watch', '$watchCollection', "commit" ]
);
mockElement = {};
testAttrs = { toolbar: 'testToolbar' };

View File

@ -11,7 +11,8 @@ define(
testABC,
testABC2,
testABCXYZ,
testABCYZ;
testABCYZ,
testM;
beforeEach(function () {
testStructure = {
@ -29,6 +30,11 @@ define(
{ name: "Y", property: "y" },
{ name: "Z", property: "z" }
]
},
{
items: [
{ name: "M", method: "m" }
]
}
]
};
@ -37,6 +43,7 @@ define(
testABC2 = { a: 4, b: 1, c: 2 }; // For inconsistent-state checking
testABCXYZ = { a: 0, b: 1, c: 2, x: 'X!', y: 'Y!', z: 'Z!' };
testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' };
testM = { m: jasmine.createSpy("method") };
});
it("provides properties from the original structure", function () {
@ -182,6 +189,19 @@ define(
.length
).toEqual(2);
});
it("adds click functions when a method is specified", function () {
var testCommit = jasmine.createSpy('commit'),
toolbar = new EditToolbar(testStructure, [ testM ], testCommit);
// Verify precondition
expect(testM.m).not.toHaveBeenCalled();
// Click!
toolbar.getStructure().sections[0].items[0].click();
// Should have called the underlying function
expect(testM.m).toHaveBeenCalled();
// Should also have committed the change
expect(testCommit).toHaveBeenCalled();
});
});
}
);

View File

@ -19,7 +19,31 @@
"type": "telemetry.panel",
"templateUrl": "templates/fixed.html",
"uses": [ "composition" ],
"gestures": [ "drop" ]
"gestures": [ "drop" ],
"toolbar": {
"sections": [
{
"items": [
{
"method": "add",
"control": "button",
"text": "Add",
"inclusive": true
}
]
},
{
"items": [
{
"method": "remove",
"control": "button",
"text": "Remove",
"inclusive": true
}
]
}
]
}
}
],
"representations": [
@ -40,6 +64,12 @@
"depends": [ "$scope", "telemetrySubscriber", "telemetryFormatter" ]
}
],
"templates": [
{
"key": "fixed.telemetry",
"templateUrl": "templates/elements/telemetry.html"
}
],
"types": [
{
"key": "layout",

View File

@ -0,0 +1,8 @@
<div style="background: #444;">
<div style="position: absolute; left: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;">
{{ngModel.name}}
</div>
<div style="position: absolute; right: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;">
{{ngModel.value}}
</div>
</div>

View File

@ -1,36 +1,25 @@
<div style="width: 100%; height: 100%; position: absolute; left: 0px; top: 0px;"
ng-controller="FixedController as controller"
mct-resize="controller.setBounds(bounds)">
<!-- Background grid -->
<div ng-repeat="cell in controller.getCellStyles()"
style="position: absolute; border: 1px gray solid; background: black;"
ng-style="cell">
</div>
<!-- Telemetry elements -->
<div ng-repeat="childObject in composition"
style="position: absolute; background: #444;"
ng-style="controller.getStyle(childObject.getId())">
<div style="position: absolute; left: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;">
{{childObject.getModel().name}}
</div>
<div style="position: absolute; right: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;">
{{controller.getValue(childObject.getId())}}
<span ng-click="controller.clearSelection()">
<div ng-repeat="cell in controller.getCellStyles()"
style="position: absolute; border: 1px gray solid; background: black;"
ng-style="cell">
</div>
</span>
<!-- Drag handles -->
<span ng-show="domainObject.hasCapability('editor')">
<span style="position: absolute; left: 0px; right: 0px; top: 0px; bottom: 0px; 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>
</div>
<!-- Fixed position elements -->
<mct-include ng-repeat="element in controller.getElements()"
style="position: absolute;"
key="element.template"
ng-class="{ test: controller.selected(element) }"
ng-style="element.style"
ng-click="controller.select(element)"
ng-model="element"
mct-drag-down="controller.startDrag(element); controller.select(element)"
mct-drag="controller.continueDrag(delta)"
mct-drag-up="controller.endDrag()">
</mct-include>
</div>

View File

@ -1,8 +1,8 @@
/*global define*/
define(
['./LayoutDrag'],
function (LayoutDrag) {
['./LayoutDrag', './LayoutSelection', './FixedProxy', './elements/ElementProxies'],
function (LayoutDrag, LayoutSelection, FixedProxy, ElementProxies) {
"use strict";
var DEFAULT_DIMENSIONS = [ 2, 1 ],
@ -20,31 +20,23 @@ define(
function FixedController($scope, telemetrySubscriber, telemetryFormatter) {
var gridSize = DEFAULT_GRID_SIZE,
gridExtent = DEFAULT_GRID_EXTENT,
activeDrag,
activeDragId,
dragging,
subscription,
values = {},
cellStyles = [],
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;
}
elementProxies = [],
elementProxiesById = {},
selection;
// Refresh cell styles (e.g. because grid extent changed)
function refreshCellStyles() {
var x, y;
// Clear previous styles
cellStyles = [];
// Update grid size from model
gridSize = ($scope.model || {}).layoutGrid || gridSize;
for (x = 0; x < gridExtent[0]; x += 1) {
for (y = 0; y < gridExtent[1]; y += 1) {
// Position blocks; subtract out border size from w/h
@ -58,62 +50,28 @@ define(
}
}
// Convert from { positions: ..., dimensions: ... } to an
// apropriate ng-style argument, to position frames.
// Convert from element x/y/width/height to an
// apropriate ng-style argument, to position elements.
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'
left: (gridSize[0] * raw.x) + 'px',
top: (gridSize[1] * raw.y) + 'px',
width: (gridSize[0] * raw.width) + 'px',
height: (gridSize[1] * raw.height) + '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(ids) {
var configuration = $scope.configuration || {};
ids = ids || [];
// Pull panel positions from configuration
rawPositions = shallowCopy(configuration.elements || {}, ids);
// Clear prior computed positions
positions = {};
// Update width/height that we are tracking
gridSize = ($scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
// Compute positions and add defaults where needed
ids.forEach(populatePosition);
}
// Update the displayed value for this object
function updateValue(telemetryObject) {
var id = telemetryObject && telemetryObject.getId();
if (id) {
values[id] = telemetryFormatter.formatRangeValue(
subscription.getRangeValue(telemetryObject)
);
(elementProxiesById[id] || []).forEach(function (element) {
element.name = telemetryObject.getModel().name;
element.value = telemetryFormatter.formatRangeValue(
subscription.getRangeValue(telemetryObject)
);
});
}
}
@ -124,6 +82,59 @@ define(
}
}
// Decorate an element for display
function makeProxyElement(element, index, elements) {
var ElementProxy = ElementProxies[element.type],
e = ElementProxy && new ElementProxy(element, index, elements);
if (e) {
// Provide a displayable position (convert from grid to px)
e.style = convertPosition(element);
// Template names are same as type names, presently
e.template = element.type;
}
return e;
}
// Decorate elements in the current configuration
function refreshElements() {
// Cache selection; we are instantiating new proxies
// so we may want to restore this.
var selected = selection && selection.get(),
elements = (($scope.configuration || {}).elements || []),
index = -1; // Start with a 'not-found' value
// Find the selection in the new array
if (selected !== undefined) {
index = elements.indexOf(selected.element);
}
// Create the new proxies...
elementProxies = elements.map(makeProxyElement);
// Clear old selection, and restore if appropriate
if (selection) {
selection.deselect();
if (index > -1) {
selection.select(elementProxies[index]);
}
}
// Finally, rebuild lists of elements by id to
// facilitate faster update when new telemetry comes in.
elementProxiesById = {};
elementProxies.forEach(function (elementProxy) {
var id = elementProxy.id;
if (elementProxy.element.type === 'fixed.telemetry') {
elementProxiesById[id] = elementProxiesById[id] || [];
elementProxiesById[id].push(elementProxy);
}
});
// TODO: Ensure elements for all domain objects?
}
// Free up subscription to telemetry
function releaseSubscription() {
if (subscription) {
@ -134,9 +145,6 @@ define(
// Subscribe to telemetry updates for this domain object
function subscribe(domainObject) {
// Clear any old values
values = {};
// Release existing subscription (if any)
if (subscription) {
subscription.unsubscribe();
@ -150,7 +158,7 @@ define(
// Handle changes in the object's composition
function updateComposition(ids) {
// Populate panel positions
lookupPanels(ids);
// TODO: Ensure defaults here
// Resubscribe - objects in view have changed
subscribe($scope.domainObject);
}
@ -162,23 +170,33 @@ define(
// Make sure there is a "elements" field in the
// view configuration.
$scope.configuration.elements =
$scope.configuration.elements || {};
$scope.configuration.elements || [];
// Store the position of this element.
$scope.configuration.elements[id] = {
position: [
Math.floor(position.x / gridSize[0]),
Math.floor(position.y / gridSize[1])
],
dimensions: DEFAULT_DIMENSIONS
};
$scope.configuration.elements.push({
type: "fixed.telemetry",
x: Math.floor(position.x / gridSize[0]),
y: Math.floor(position.y / gridSize[1]),
id: id,
width: DEFAULT_DIMENSIONS[0],
height: DEFAULT_DIMENSIONS[1]
});
// Mark change as persistable
if ($scope.commit) {
$scope.commit("Dropped a frame.");
$scope.commit("Dropped an element.");
}
// Populate template-facing position for this id
populatePosition(id);
}
// Track current selection state
if (Array.isArray($scope.selection)) {
selection = new LayoutSelection(
$scope.selection,
new FixedProxy($scope.configuration)
);
}
// Refresh list of elements whenever model changes
$scope.$watch("model.modified", refreshElements);
// Position panes when the model field changes
$scope.$watch("model.composition", updateComposition);
@ -204,15 +222,6 @@ define(
getCellStyles: function () {
return cellStyles;
},
/**
* Get the current data value for the specified domain object.
* @memberof FixedController#
* @param {string} id the domain object identifier
* @returns {string} the displayable data value
*/
getValue: function (id) {
return values[id];
},
/**
* Set the size of the viewable fixed position area.
* @memberof FixedController#
@ -227,17 +236,36 @@ define(
}
},
/**
* 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
* Get an array of elements in this panel; these are
* decorated proxies for both selection and display.
* @returns {Array} elements in this panel
*/
getStyle: function (id) {
// Called in a loop, so just look up; the "positions"
// object is kept up to date by a watch.
return positions[id];
getElements: function () {
return elementProxies;
},
/**
* Check if the element is currently selected.
* @returns {boolean} true if selected
*/
selected: function (element) {
return selection && selection.selected(element);
},
/**
* Set the active user selection in this view.
* @param element the element to select
*/
select: function (element) {
if (selection) {
selection.select(element);
}
},
/**
* Clear the current user selection.
*/
clearSelection: function () {
if (selection) {
selection.deselect();
}
},
/**
* Start a drag gesture to move/resize a frame.
@ -254,19 +282,18 @@ define(
* 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
* @param element the raw (undecorated) element to drag
*/
startDrag: function (id, posFactor, dimFactor) {
activeDragId = id;
activeDrag = new LayoutDrag(
rawPositions[id],
posFactor,
dimFactor,
gridSize
);
startDrag: function (element) {
// Only allow dragging in edit mode
if ($scope.domainObject &&
$scope.domainObject.hasCapability('editor')) {
dragging = {
element: element,
x: element.x(),
y: element.y()
};
}
},
/**
* Continue an active drag gesture.
@ -275,10 +302,10 @@ define(
* to its position when the drag started
*/
continueDrag: function (delta) {
if (activeDrag) {
rawPositions[activeDragId] =
activeDrag.getAdjustedPosition(delta);
populatePosition(activeDragId);
if (dragging) {
dragging.element.x(dragging.x + Math.round(delta[0] / gridSize[0]));
dragging.element.y(dragging.y + Math.round(delta[1] / gridSize[1]));
dragging.element.style = convertPosition(dragging.element.element);
}
},
/**
@ -286,19 +313,9 @@ define(
* 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.elements =
$scope.configuration.elements || {};
// Store the position of this panel.
$scope.configuration.elements[activeDragId] =
rawPositions[activeDragId];
// Mark this object as dirty to encourage persistence
if ($scope.commit) {
if (dragging && $scope.commit) {
dragging = undefined;
$scope.commit("Moved element.");
}
}

View File

@ -0,0 +1,26 @@
/*global define,window*/
define(
[],
function () {
"use strict";
/**
* Proxy for configuring a fixed position view via the toolbar.
* @constructor
* @param configuration the view configuration object
*/
function FixedProxy(configuration) {
return {
/**
* Add a new visual element to this view.
*/
add: function (type) {
window.alert("Placeholder. Should add a " + type + ".");
}
};
}
return FixedProxy;
}
);

View File

@ -0,0 +1,126 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Tracks selection state for Layout and Fixed Position views.
* This manages and mutates the provided selection array in-place,
* and takes care to only modify the array elements it manages
* (the view's proxy, and the single selection); selections may be
* added or removed elsewhere provided that similar care is taken
* elsewhere.
*
* @param {Array} selection the selection array from the view's scope
* @param [proxy] an object which represents the selection of the view
* itself (which handles view-level toolbar behavior)
*/
function LayoutSelection(selection, proxy) {
var selecting = false,
selected;
// Find the proxy in the array; our selected objects will be
// positioned next to that
function proxyIndex() {
return selection.indexOf(proxy);
}
// Remove the currently-selected object
function deselect() {
// Nothing to do if we don't have a selected object
if (selecting) {
// Clear state tracking
selecting = false;
selected = undefined;
// Remove the selection
selection.splice(proxyIndex() + 1, 1);
return true;
}
return false;
}
// Select an object
function select(obj) {
// We want this selection to end up near the proxy
var index = proxyIndex() + 1;
// Proxy is always selected
if (obj === proxy) {
return false;
}
// Clear any existing selection
deselect();
// Note the current selection state
selected = obj;
selecting = true;
// Are we at the end of the array?
if (selection.length === index) {
// Add it to the end
selection.push(obj);
} else {
// Splice it into the array
selection.splice(index, 0, obj);
}
}
// Remove any selected object, and the proxy itself
function destroy() {
deselect();
selection.splice(proxyIndex(), 1);
}
// Check if an object is selected
function isSelected(obj) {
return (obj === selected) || (obj === proxy);
}
// Getter for current selection
function get() {
return selected;
}
// Start with the proxy selected
selection.push(proxy);
return {
/**
* Check if an object is currently selected.
* @returns true if selected, otherwise false
*/
selected: isSelected,
/**
* Select an object.
* @param obj the object to select
* @returns {boolean} true if selection changed
*/
select: select,
/**
* Clear the current selection.
* @returns {boolean} true if selection changed
*/
deselect: deselect,
/**
* Get the currently-selected object.
* @returns the currently selected object
*/
get: get,
/**
* Clear the selection, including the proxy, and dispose
* of this selection scope. No other calls to methods on
* this object are expected after `destroy` has been
* called; their behavior will be undefined.
*/
destroy: destroy
};
}
return LayoutSelection;
}
);

View File

@ -0,0 +1,26 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Utility function for creating getter-setter functions,
* since these are frequently useful for element proxies.
* @constructor
* @param {Object} object the object to get/set values upon
* @param {string} key the property to get/set
*/
function AccessorMutator(object, key) {
return function (value) {
if (arguments.length > 0) {
object[key] = value;
}
return object[key];
};
}
return AccessorMutator;
}
);

View File

@ -0,0 +1,12 @@
/*global define*/
define(
['./TelemetryProxy'],
function (TelemetryProxy) {
"use strict";
return {
"fixed.telemetry": TelemetryProxy
};
}
);

View File

@ -0,0 +1,36 @@
/*global define*/
define(
['./AccessorMutator'],
function (AccessorMutator) {
"use strict";
/**
* Abstract superclass for other classes which provide useful
* interfaces upon an elements in a fixed position view.
* This handles the generic operations (e.g. remove) so that
* subclasses only need to implement element-specific behaviors.
* @constructor
* @param element the telemetry element
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function ElementProxy(element, index, elements) {
return {
element: element,
x: new AccessorMutator(element, 'x'),
y: new AccessorMutator(element, 'y'),
z: new AccessorMutator(element, 'z'),
width: new AccessorMutator(element, 'width'),
height: new AccessorMutator(element, 'height'),
remove: function () {
if (elements[index] === element) {
elements.splice(index, 1);
}
}
};
}
return ElementProxy;
}
);

View File

@ -0,0 +1,21 @@
/*global define*/
define(
['./ElementProxy'],
function (ElementProxy) {
'use strict';
/**
*
*/
function TelemetryProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
proxy.id = element.id;
return proxy;
}
return TelemetryProxy;
}
);

View File

@ -1,4 +1,4 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
/*global define,describe,it,expect,beforeEach,jasmine,xit*/
define(
["../src/FixedController"],
@ -14,6 +14,7 @@ define(
testGrid,
testModel,
testValues,
testConfiguration,
controller;
// Utility function; find a watch for a given expression
@ -41,9 +42,10 @@ define(
function makeMockDomainObject(id) {
var mockObject = jasmine.createSpyObj(
'domainObject-' + id,
[ 'getId' ]
[ 'getId', 'getModel' ]
);
mockObject.getId.andReturn(id);
mockObject.getModel.andReturn({ name: "Point " + id});
return mockObject;
}
@ -75,6 +77,11 @@ define(
layoutGrid: testGrid
};
testValues = { a: 10, b: 42, c: 31.42 };
testConfiguration = { elements: [
{ type: "fixed.telemetry", id: 'a', x: 1, y: 1 },
{ type: "fixed.telemetry", id: 'b', x: 1, y: 1 },
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1 }
]};
mockSubscriber.subscribe.andReturn(mockSubscription);
mockSubscription.getTelemetryObjects.andReturn(
@ -86,6 +93,9 @@ define(
mockFormatter.formatRangeValue.andCallFake(function (v) {
return "Formatted " + v;
});
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
mockScope.selection = []; // Act like edit mode
controller = new FixedController(
mockScope,
@ -122,30 +132,85 @@ define(
expect(mockSubscriber.subscribe.calls.length).toEqual(2);
});
it("configures view based on model", function () {
it("exposes visible elements based on configuration", function () {
var elements;
mockScope.model = testModel;
findWatch("model.composition")(mockScope.model.composition);
// Should have styles for all elements of composition
expect(controller.getStyle('a')).toBeDefined();
expect(controller.getStyle('b')).toBeDefined();
expect(controller.getStyle('c')).toBeDefined();
expect(controller.getStyle('d')).not.toBeDefined();
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
expect(elements.length).toEqual(3);
expect(elements[0].id).toEqual('a');
expect(elements[1].id).toEqual('b');
expect(elements[2].id).toEqual('c');
});
it("allows elements to be selected", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
controller.select(elements[1]);
expect(controller.selected(elements[0])).toBeFalsy();
expect(controller.selected(elements[1])).toBeTruthy();
});
it("allows selections to be cleared", function () {
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
controller.select(elements[1]);
controller.clearSelection();
expect(controller.selected(elements[1])).toBeFalsy();
});
it("retains selections during refresh", function () {
// Get elements; remove one of them; trigger refresh.
// Same element (at least by index) should still be selected.
var elements;
testModel.modified = 1;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
controller.select(elements[1]);
elements[2].remove();
testModel.modified = 2;
findWatch("model.modified")(testModel.modified);
elements = controller.getElements();
// Verify removal, as test assumes this
expect(elements.length).toEqual(2);
expect(controller.selected(elements[1])).toBeTruthy();
});
it("provides values for telemetry elements", function () {
var elements;
// Initialize
mockScope.domainObject = mockDomainObject;
mockScope.model = testModel;
findWatch("domainObject")(mockDomainObject);
findWatch("model.modified")(1);
findWatch("model.composition")(mockScope.model.composition);
// Invoke the subscription callback
mockSubscriber.subscribe.mostRecentCall.args[1]();
// Get elements that controller is now exposing
elements = controller.getElements();
// Formatted values should be available
expect(controller.getValue('a')).toEqual("Formatted 10");
expect(controller.getValue('b')).toEqual("Formatted 42");
expect(controller.getValue('c')).toEqual("Formatted 31.42");
expect(elements[0].value).toEqual("Formatted 10");
expect(elements[1].value).toEqual("Formatted 42");
expect(elements[2].value).toEqual("Formatted 31.42");
});
it("adds grid cells to fill boundaries", function () {
@ -179,7 +244,7 @@ define(
);
// Verify precondition
expect(controller.getStyle('d')).not.toBeDefined();
expect(testConfiguration.elements.length).toEqual(3);
// Notify that a drop occurred
testModel.composition.push('d');
@ -188,13 +253,28 @@ define(
'd',
{ x: 300, y: 100 }
);
expect(controller.getStyle('d')).toBeDefined();
// Should have added an element
expect(testConfiguration.elements.length).toEqual(4);
// Should have triggered commit (provided by
// EditRepresenter) with some message.
expect(mockScope.commit)
.toHaveBeenCalledWith(jasmine.any(String));
});
it("unsubscribes when destroyed", function () {
// Make an object available
findWatch('domainObject')(mockDomainObject);
// Also verify precondition
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
findOn('$destroy')();
// Should have unsubscribed
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
});
});
}
);

View File

@ -0,0 +1,18 @@
/*global define,describe,it,expect,beforeEach,jasmine,xit*/
define(
['../src/FixedProxy'],
function (FixedProxy) {
"use strict";
describe("Fixed Position view's selection proxy", function () {
it("has a placeholder message when clicked", function () {
var oldAlert = window.alert;
window.alert = jasmine.createSpy('alert');
new FixedProxy({}).add('');
expect(window.alert).toHaveBeenCalledWith(jasmine.any(String));
window.alert = oldAlert;
});
});
}
);

View File

@ -0,0 +1,85 @@
/*global define,describe,it,expect,beforeEach,jasmine,xit*/
define(
['../src/LayoutSelection'],
function (LayoutSelection) {
"use strict";
describe("Layout/fixed position selection manager", function () {
var testSelection,
testProxy,
testElement,
otherElement,
selection;
beforeEach(function () {
testSelection = [];
testProxy = { someKey: "some value" };
testElement = { someOtherKey: "some other value" };
otherElement = { yetAnotherKey: 42 };
selection = new LayoutSelection(testSelection, testProxy);
});
it("adds the proxy to the selection array", function () {
expect(testSelection).toEqual([testProxy]);
});
it("includes selected objects alongside the proxy", function () {
selection.select(testElement);
expect(testSelection).toEqual([testProxy, testElement]);
});
it("allows elements to be deselected", function () {
selection.select(testElement);
selection.deselect();
expect(testSelection).toEqual([testProxy]);
});
it("replaces old selections with new ones", function () {
selection.select(testElement);
selection.select(otherElement);
expect(testSelection).toEqual([testProxy, otherElement]);
});
it("allows retrieval of the current selection", function () {
selection.select(testElement);
expect(selection.get()).toBe(testElement);
selection.select(otherElement);
expect(selection.get()).toBe(otherElement);
});
it("can check if an element is selected", function () {
selection.select(testElement);
expect(selection.selected(testElement)).toBeTruthy();
expect(selection.selected(otherElement)).toBeFalsy();
selection.select(otherElement);
expect(selection.selected(testElement)).toBeFalsy();
expect(selection.selected(otherElement)).toBeTruthy();
});
it("cleans up the selection on destroy", function () {
selection.destroy();
expect(testSelection).toEqual([]);
});
it("preserves other elements in the array", function () {
testSelection.push(42);
selection.select(testElement);
expect(testSelection).toEqual([testProxy, testElement, 42]);
});
it("considers the proxy to be selected", function () {
expect(selection.selected(testProxy)).toBeTruthy();
selection.select(testElement);
// Even when something else is selected...
expect(selection.selected(testProxy)).toBeTruthy();
});
it("treats selection of the proxy as a no-op", function () {
selection.select(testProxy);
expect(testSelection).toEqual([testProxy]);
});
});
}
);

View File

@ -0,0 +1,31 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/AccessorMutator'],
function (AccessorMutator) {
"use strict";
describe("An accessor-mutator", function () {
var testObject,
am;
beforeEach(function () {
testObject = { t: 42, other: 100 };
am = new AccessorMutator(testObject, 't');
});
it("allows access to a property", function () {
expect(am()).toEqual(42);
});
it("allows mutation of a property", function () {
expect(am("some other value")).toEqual("some other value");
expect(testObject).toEqual({
t: "some other value",
other: 100
});
});
});
}
);

View File

@ -0,0 +1,27 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/ElementProxies'],
function (ElementProxies) {
"use strict";
var ELEMENT_TYPES = [
"fixed.telemetry"
];
// Verify that the set of proxies exposed matches the specific
// list above.
describe("The set of element proxies", function () {
ELEMENT_TYPES.forEach(function (t) {
it("exposes a proxy wrapper for " + t + " elements", function () {
expect(typeof ElementProxies[t]).toEqual('function');
});
});
it("exposes no additional wrappers", function () {
expect(Object.keys(ElementProxies).length)
.toEqual(ELEMENT_TYPES.length);
});
});
}
);

View File

@ -0,0 +1,41 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/ElementProxy'],
function (ElementProxy) {
"use strict";
describe("A fixed position element proxy", function () {
var testElement,
testElements,
proxy;
beforeEach(function () {
testElement = {
x: 1,
y: 2,
z: 3,
width: 42,
height: 24
};
testElements = [ {}, {}, testElement, {} ];
proxy = new ElementProxy(
testElement,
testElements.indexOf(testElement),
testElements
);
});
it("exposes element properties", function () {
Object.keys(testElement).forEach(function (k) {
expect(proxy[k]()).toEqual(testElement[k]);
});
});
it("allows elements to be removed", function () {
proxy.remove();
expect(testElements).toEqual([{}, {}, {}]);
});
});
}
);

View File

@ -0,0 +1,35 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/TelemetryProxy'],
function (TelemetryProxy) {
"use strict";
describe("A fixed position telemetry proxy", function () {
var testElement,
testElements,
proxy;
beforeEach(function () {
testElement = {
x: 1,
y: 2,
z: 3,
width: 42,
height: 24,
id: "test-id"
};
testElements = [ {}, {}, testElement, {} ];
proxy = new TelemetryProxy(
testElement,
testElements.indexOf(testElement),
testElements
);
});
it("exposes the element's id", function () {
expect(proxy.id).toEqual('test-id');
});
});
}
);

View File

@ -1,5 +1,11 @@
[
"FixedController",
"FixedProxy",
"LayoutController",
"LayoutDrag"
"LayoutDrag",
"LayoutSelection",
"elements/AccessorMutator",
"elements/ElementProxies",
"elements/ElementProxy",
"elements/TelemetryProxy"
]

View File

@ -10,7 +10,7 @@ define(
// Methods to mock
var JQLITE_FUNCTIONS = [ "on", "off", "attr", "removeAttr" ],
var JQLITE_FUNCTIONS = [ "on", "off", "attr", "removeAttr", "scope" ],
DOMAIN_OBJECT_METHODS = [ "getId", "getModel", "getCapability", "hasCapability", "useCapability"],
TEST_ID = "test-id",
DROP_ID = "drop-id";
@ -21,7 +21,10 @@ define(
mockDomainObject,
mockPersistence,
mockEvent,
mockScope,
mockUnwrappedElement,
testModel,
testRect,
gesture,
callbacks;
@ -35,6 +38,7 @@ define(
beforeEach(function () {
testModel = { composition: [] };
testRect = {};
mockQ = { when: mockPromise };
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
@ -42,11 +46,17 @@ define(
mockPersistence = jasmine.createSpyObj("persistence", [ "persist" ]);
mockEvent = jasmine.createSpyObj("event", ["preventDefault"]);
mockEvent.dataTransfer = jasmine.createSpyObj("dataTransfer", [ "getData" ]);
mockScope = jasmine.createSpyObj("$scope", ["$broadcast"]);
mockUnwrappedElement = jasmine.createSpyObj("unwrapped", ["getBoundingClientRect"]);
mockDomainObject.getId.andReturn(TEST_ID);
mockDomainObject.getModel.andReturn(testModel);
mockDomainObject.getCapability.andReturn(mockPersistence);
mockDomainObject.useCapability.andReturn(true);
mockEvent.dataTransfer.getData.andReturn(DROP_ID);
mockElement[0] = mockUnwrappedElement;
mockElement.scope.andReturn(mockScope);
mockUnwrappedElement.getBoundingClientRect.andReturn(testRect);
gesture = new DropGesture(mockQ, mockElement, mockDomainObject);
@ -114,6 +124,19 @@ define(
expect(mockDomainObject.getCapability).toHaveBeenCalledWith("persistence");
});
it("broadcasts drop position", function () {
testRect.left = 42;
testRect.top = 36;
mockEvent.pageX = 52;
mockEvent.pageY = 64;
callbacks.drop(mockEvent);
expect(mockScope.$broadcast).toHaveBeenCalledWith(
'mctDrop',
DROP_ID,
{ x: 10, y: 28 }
);
});
});
}
);

View File

@ -138,6 +138,11 @@ define(
function cacheObjectReferences(objects) {
telemetryObjects = objects;
metadatas = objects.map(lookupMetadata);
// Fire callback, as this will be the first time that
// telemetry objects are available
if (callback) {
callback();
}
return objects;
}

View File

@ -76,7 +76,10 @@ define(
});
it("fires callbacks when subscriptions update", function () {
expect(mockCallback).not.toHaveBeenCalled();
// Callback fires when telemetry objects become available,
// so track initial call count instead of verifying that
// it hasn't been called at all.
var initialCalls = mockCallback.calls.length;
mockTelemetry.subscribe.mostRecentCall.args[0](mockSeries);
// This gets fired via a timeout, so trigger that
expect(mockTimeout).toHaveBeenCalledWith(
@ -86,12 +89,15 @@ define(
mockTimeout.mostRecentCall.args[0]();
// Should have triggered the callback to alert that
// new data was available
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback.calls.length).toEqual(initialCalls + 1);
});
it("fires subscription callbacks once per cycle", function () {
var i;
// Verify precondition - one call for telemetryObjects
expect(mockCallback.calls.length).toEqual(1);
for (i = 0; i < 100; i += 1) {
mockTelemetry.subscribe.mostRecentCall.args[0](mockSeries);
}
@ -100,7 +106,7 @@ define(
call.args[0]();
});
// Should have only triggered the
expect(mockCallback.calls.length).toEqual(1);
expect(mockCallback.calls.length).toEqual(2);
});
it("reports its latest observed data values", function () {
@ -129,7 +135,8 @@ define(
// telemetrySubscription, where failure to callback
// once-per-update results in loss of data, WTD-784
it("fires one event per update if requested", function () {
var i, domains = [], ranges = [], lastCall;
var i, domains = [], ranges = [], lastCall, initialCalls;
// Clear out the subscription from beforeEach
subscription.unsubscribe();
@ -142,6 +149,9 @@ define(
true // Don't drop updates!
);
// Track calls at this point
initialCalls = mockCallback.calls.length;
// Snapshot getDomainValue, getRangeValue at time of callback
mockCallback.andCallFake(function () {
domains.push(subscription.getDomainValue(mockDomainObject));
@ -163,13 +173,17 @@ define(
}
// Should have only triggered the
expect(mockCallback.calls.length).toEqual(100);
expect(mockCallback.calls.length).toEqual(100 + initialCalls);
});
it("provides domain object metadata", function () {
expect(subscription.getMetadata()[0])
.toEqual(testMetadata);
});
it("fires callback when telemetry objects are available", function () {
expect(mockCallback.calls.length).toEqual(1);
});
});
}
);