diff --git a/platform/commonUI/edit/README.md b/platform/commonUI/edit/README.md
index 3691e8211a..a8c552fb3e 100644
--- a/platform/commonUI/edit/README.md
+++ b/platform/commonUI/edit/README.md
@@ -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
diff --git a/platform/commonUI/edit/res/templates/edit-object.html b/platform/commonUI/edit/res/templates/edit-object.html
index 1f9d5763ae..2ff322e37c 100644
--- a/platform/commonUI/edit/res/templates/edit-object.html
+++ b/platform/commonUI/edit/res/templates/edit-object.html
@@ -12,7 +12,7 @@
-
-
-
-
-
-
-
- {{childObject.getModel().name}}
-
-
- {{controller.getValue(childObject.getId())}}
+
+
+
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js
index 07f0f99731..d18d286840 100644
--- a/platform/features/layout/src/FixedController.js
+++ b/platform/features/layout/src/FixedController.js
@@ -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.
} 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.");
}
}
diff --git a/platform/features/layout/src/FixedProxy.js b/platform/features/layout/src/FixedProxy.js
new file mode 100644
index 0000000000..c99a0e3f92
--- /dev/null
+++ b/platform/features/layout/src/FixedProxy.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/layout/src/LayoutSelection.js b/platform/features/layout/src/LayoutSelection.js
new file mode 100644
index 0000000000..bb1574f0ff
--- /dev/null
+++ b/platform/features/layout/src/LayoutSelection.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/layout/src/elements/AccessorMutator.js b/platform/features/layout/src/elements/AccessorMutator.js
new file mode 100644
index 0000000000..ac296ddd05
--- /dev/null
+++ b/platform/features/layout/src/elements/AccessorMutator.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/layout/src/elements/ElementProxies.js b/platform/features/layout/src/elements/ElementProxies.js
new file mode 100644
index 0000000000..e8d6b032e5
--- /dev/null
+++ b/platform/features/layout/src/elements/ElementProxies.js
@@ -0,0 +1,12 @@
+/*global define*/
+
+define(
+ ['./TelemetryProxy'],
+ function (TelemetryProxy) {
+ "use strict";
+
+ return {
+ "fixed.telemetry": TelemetryProxy
+ };
+ }
+);
\ No newline at end of file
diff --git a/platform/features/layout/src/elements/ElementProxy.js b/platform/features/layout/src/elements/ElementProxy.js
new file mode 100644
index 0000000000..f580b509cd
--- /dev/null
+++ b/platform/features/layout/src/elements/ElementProxy.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/layout/src/elements/TelemetryProxy.js b/platform/features/layout/src/elements/TelemetryProxy.js
new file mode 100644
index 0000000000..610edb895c
--- /dev/null
+++ b/platform/features/layout/src/elements/TelemetryProxy.js
@@ -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;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js
index 61c8773907..460228e362 100644
--- a/platform/features/layout/test/FixedControllerSpec.js
+++ b/platform/features/layout/test/FixedControllerSpec.js
@@ -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();
+ });
});
}
);
\ No newline at end of file
diff --git a/platform/features/layout/test/FixedProxySpec.js b/platform/features/layout/test/FixedProxySpec.js
new file mode 100644
index 0000000000..224b19f022
--- /dev/null
+++ b/platform/features/layout/test/FixedProxySpec.js
@@ -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;
+ });
+ });
+ }
+);
diff --git a/platform/features/layout/test/LayoutSelectionSpec.js b/platform/features/layout/test/LayoutSelectionSpec.js
new file mode 100644
index 0000000000..3712afec3f
--- /dev/null
+++ b/platform/features/layout/test/LayoutSelectionSpec.js
@@ -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]);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/layout/test/elements/AccessorMutatorSpec.js b/platform/features/layout/test/elements/AccessorMutatorSpec.js
new file mode 100644
index 0000000000..afcede7176
--- /dev/null
+++ b/platform/features/layout/test/elements/AccessorMutatorSpec.js
@@ -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
+ });
+ });
+
+ });
+ }
+);
diff --git a/platform/features/layout/test/elements/ElementProxiesSpec.js b/platform/features/layout/test/elements/ElementProxiesSpec.js
new file mode 100644
index 0000000000..cf771a043a
--- /dev/null
+++ b/platform/features/layout/test/elements/ElementProxiesSpec.js
@@ -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);
+ });
+ });
+ }
+);
diff --git a/platform/features/layout/test/elements/ElementProxySpec.js b/platform/features/layout/test/elements/ElementProxySpec.js
new file mode 100644
index 0000000000..e2a9c2880e
--- /dev/null
+++ b/platform/features/layout/test/elements/ElementProxySpec.js
@@ -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([{}, {}, {}]);
+ });
+ });
+ }
+);
diff --git a/platform/features/layout/test/elements/TelemetryProxySpec.js b/platform/features/layout/test/elements/TelemetryProxySpec.js
new file mode 100644
index 0000000000..fdd7e555d0
--- /dev/null
+++ b/platform/features/layout/test/elements/TelemetryProxySpec.js
@@ -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');
+ });
+ });
+ }
+);
diff --git a/platform/features/layout/test/suite.json b/platform/features/layout/test/suite.json
index 6e62994ff5..af82513e01 100644
--- a/platform/features/layout/test/suite.json
+++ b/platform/features/layout/test/suite.json
@@ -1,5 +1,11 @@
[
"FixedController",
+ "FixedProxy",
"LayoutController",
- "LayoutDrag"
+ "LayoutDrag",
+ "LayoutSelection",
+ "elements/AccessorMutator",
+ "elements/ElementProxies",
+ "elements/ElementProxy",
+ "elements/TelemetryProxy"
]
\ No newline at end of file
diff --git a/platform/representation/test/gestures/DropGestureSpec.js b/platform/representation/test/gestures/DropGestureSpec.js
index 2182cf0997..d24dcf2132 100644
--- a/platform/representation/test/gestures/DropGestureSpec.js
+++ b/platform/representation/test/gestures/DropGestureSpec.js
@@ -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 }
+ );
+ });
+
});
}
);
\ No newline at end of file
diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js
index b7deb5d449..cce24d48dd 100644
--- a/platform/telemetry/src/TelemetrySubscription.js
+++ b/platform/telemetry/src/TelemetrySubscription.js
@@ -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;
}
diff --git a/platform/telemetry/test/TelemetrySubscriptionSpec.js b/platform/telemetry/test/TelemetrySubscriptionSpec.js
index 469015ee93..6dd27320c9 100644
--- a/platform/telemetry/test/TelemetrySubscriptionSpec.js
+++ b/platform/telemetry/test/TelemetrySubscriptionSpec.js
@@ -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);
+ });
});
}
);
\ No newline at end of file