mirror of
https://github.com/nasa/openmct.git
synced 2025-02-20 17:33:23 +00:00
Merge remote-tracking branch 'origin/open879' into open-master
Conflicts: platform/commonUI/edit/res/templates/edit-object.html
This commit is contained in:
commit
970b5e70ba
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -15,7 +15,7 @@ define(
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
'$scope',
|
||||
[ '$on', '$watch', '$watchCollection' ]
|
||||
[ '$on', '$watch', '$watchCollection', "commit" ]
|
||||
);
|
||||
mockElement = {};
|
||||
testAttrs = { toolbar: 'testToolbar' };
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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>
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
26
platform/features/layout/src/FixedProxy.js
Normal file
26
platform/features/layout/src/FixedProxy.js
Normal 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;
|
||||
}
|
||||
);
|
126
platform/features/layout/src/LayoutSelection.js
Normal file
126
platform/features/layout/src/LayoutSelection.js
Normal 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;
|
||||
}
|
||||
);
|
26
platform/features/layout/src/elements/AccessorMutator.js
Normal file
26
platform/features/layout/src/elements/AccessorMutator.js
Normal 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;
|
||||
}
|
||||
);
|
12
platform/features/layout/src/elements/ElementProxies.js
Normal file
12
platform/features/layout/src/elements/ElementProxies.js
Normal file
@ -0,0 +1,12 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./TelemetryProxy'],
|
||||
function (TelemetryProxy) {
|
||||
"use strict";
|
||||
|
||||
return {
|
||||
"fixed.telemetry": TelemetryProxy
|
||||
};
|
||||
}
|
||||
);
|
36
platform/features/layout/src/elements/ElementProxy.js
Normal file
36
platform/features/layout/src/elements/ElementProxy.js
Normal 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;
|
||||
}
|
||||
);
|
21
platform/features/layout/src/elements/TelemetryProxy.js
Normal file
21
platform/features/layout/src/elements/TelemetryProxy.js
Normal 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;
|
||||
}
|
||||
);
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
18
platform/features/layout/test/FixedProxySpec.js
Normal file
18
platform/features/layout/test/FixedProxySpec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
85
platform/features/layout/test/LayoutSelectionSpec.js
Normal file
85
platform/features/layout/test/LayoutSelectionSpec.js
Normal 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]);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
27
platform/features/layout/test/elements/ElementProxiesSpec.js
Normal file
27
platform/features/layout/test/elements/ElementProxiesSpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
41
platform/features/layout/test/elements/ElementProxySpec.js
Normal file
41
platform/features/layout/test/elements/ElementProxySpec.js
Normal 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([{}, {}, {}]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
35
platform/features/layout/test/elements/TelemetryProxySpec.js
Normal file
35
platform/features/layout/test/elements/TelemetryProxySpec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,5 +1,11 @@
|
||||
[
|
||||
"FixedController",
|
||||
"FixedProxy",
|
||||
"LayoutController",
|
||||
"LayoutDrag"
|
||||
"LayoutDrag",
|
||||
"LayoutSelection",
|
||||
"elements/AccessorMutator",
|
||||
"elements/ElementProxies",
|
||||
"elements/ElementProxy",
|
||||
"elements/TelemetryProxy"
|
||||
]
|
@ -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 }
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user