Merge branch 'master' of https://github.com/nasa/openmctweb into open26

This commit is contained in:
Sarah Hale 2015-06-29 09:10:23 -07:00
commit d120c8b139
46 changed files with 737 additions and 158 deletions

View File

@ -16,7 +16,7 @@
{ {
"key": "BrowseController", "key": "BrowseController",
"implementation": "BrowseController.js", "implementation": "BrowseController.js",
"depends": [ "$scope", "$route", "$location", "objectService", "navigationService", "urlService"] "depends": [ "$scope", "$route", "$location", "objectService", "navigationService", "urlService" ]
}, },
{ {
"key": "BrowseObjectController", "key": "BrowseObjectController",
@ -78,11 +78,6 @@
"key": "navigationService", "key": "navigationService",
"implementation": "navigation/NavigationService.js" "implementation": "navigation/NavigationService.js"
}, },
{
"key": "urlService",
"implementation": "services/UrlService.js",
"depends": [ "$location" ]
},
{ {
"key": "creationService", "key": "creationService",
"implementation": "creation/CreationService.js", "implementation": "creation/CreationService.js",

View File

@ -52,13 +52,18 @@ define(
unlisten; unlisten;
unlisten = $scope.$on('$locationChangeSuccess', function () { unlisten = $scope.$on('$locationChangeSuccess', function () {
$route.current = priorRoute; // Checks path to make sure /browse/ is at front
// if so, change $route.current
if ($location.path().indexOf("/browse/") === 0) {
$route.current = priorRoute;
}
unlisten(); unlisten();
}); });
// urlService.urlFor used to adjust current // urlService.urlForLocation used to adjust current
// path to new, addressed, path based on // path to new, addressed, path based on
// domainObject // domainObject
$location.path(urlService.urlFor("browse", domainObject)); $location.path(urlService.urlForLocation("browse", domainObject));
} }
// Callback for updating the in-scope reference to the object // Callback for updating the in-scope reference to the object

View File

@ -54,7 +54,11 @@ define(
if (viewKey) { if (viewKey) {
$location.search('view', viewKey); $location.search('view', viewKey);
unlisten = $scope.$on('$locationChangeSuccess', function () { unlisten = $scope.$on('$locationChangeSuccess', function () {
$route.current = priorRoute; // Checks path to make sure /browse/ is at front
// if so, change $route.current
if ($location.path().indexOf("/browse/") === 0) {
$route.current = priorRoute;
}
unlisten(); unlisten();
}); });
} }

View File

@ -56,7 +56,7 @@ define(
// (browse) and the domainObject is passed in and // (browse) and the domainObject is passed in and
// the path is returned and opened in a new tab // the path is returned and opened in a new tab
perform: function () { perform: function () {
$window.open(urlService.urlFor("browse", getSelectedObject()), $window.open(urlService.urlForNewTab("browse", getSelectedObject()),
"_blank"); "_blank");
} }
}; };

View File

@ -61,7 +61,7 @@ define(
); );
mockUrlService = jasmine.createSpyObj( mockUrlService = jasmine.createSpyObj(
"urlService", "urlService",
["urlFor"] ["urlForLocation"]
); );
mockObjectService = jasmine.createSpyObj( mockObjectService = jasmine.createSpyObj(
"objectService", "objectService",
@ -222,15 +222,20 @@ define(
mockNavigationService.addListener.mostRecentCall.args[0]( mockNavigationService.addListener.mostRecentCall.args[0](
mockNextObject mockNextObject
); );
// location.path to be called with the urlService's
// urlFor function with the next domainObject and mode // Allows the path index to be checked
expect(mockLocation.path).toHaveBeenCalledWith( // prior to setting $route.current
mockUrlService.urlFor(mockMode, mockNextObject) mockLocation.path.andReturn("/browse/");
);
// Exercise the Angular workaround // Exercise the Angular workaround
mockScope.$on.mostRecentCall.args[1](); mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled(); expect(mockUnlisten).toHaveBeenCalled();
// location.path to be called with the urlService's
// urlFor function with the next domainObject and mode
expect(mockLocation.path).toHaveBeenCalledWith(
mockUrlService.urlForLocation(mockMode, mockNextObject)
);
}); });
}); });

View File

@ -68,6 +68,10 @@ define(
fireWatch("representation.selected.key", "xyz"); fireWatch("representation.selected.key", "xyz");
expect(mockLocation.search).toHaveBeenCalledWith('view', "xyz"); expect(mockLocation.search).toHaveBeenCalledWith('view', "xyz");
// Allows the path index to be checked
// prior to setting $route.current
mockLocation.path.andReturn("/browse/");
// Exercise the Angular workaround // Exercise the Angular workaround
mockScope.$on.mostRecentCall.args[1](); mockScope.$on.mostRecentCall.args[1]();
expect(mockUnlisten).toHaveBeenCalled(); expect(mockUnlisten).toHaveBeenCalled();

View File

@ -9,7 +9,6 @@
"creation/LocatorController", "creation/LocatorController",
"navigation/NavigateAction", "navigation/NavigateAction",
"navigation/NavigationService", "navigation/NavigationService",
"services/UrlService",
"windowing/FullscreenAction", "windowing/FullscreenAction",
"windowing/NewTabAction", "windowing/NewTabAction",
"windowing/WindowTitler" "windowing/WindowTitler"

View File

@ -53,7 +53,7 @@ define(
// Mocks the urlService used to make the new tab's url from a // Mocks the urlService used to make the new tab's url from a
// domainObject and mode // domainObject and mode
mockUrlService = jasmine.createSpyObj("urlService", ["urlFor"]); mockUrlService = jasmine.createSpyObj("urlService", ["urlForNewTab"]);
// Action done using the current context or mockContextCurrent // Action done using the current context or mockContextCurrent
actionCurrent = new NewTabAction(mockUrlService, mockWindow, actionCurrent = new NewTabAction(mockUrlService, mockWindow,

View File

@ -67,7 +67,7 @@
"implementation": "actions/SaveAction.js", "implementation": "actions/SaveAction.js",
"name": "Save", "name": "Save",
"description": "Save changes made to these objects.", "description": "Save changes made to these objects.",
"depends": [ "$location" ], "depends": [ "$location", "urlService" ],
"priority": "mandatory" "priority": "mandatory"
}, },
{ {
@ -76,7 +76,7 @@
"implementation": "actions/CancelAction.js", "implementation": "actions/CancelAction.js",
"name": "Cancel", "name": "Cancel",
"description": "Discard changes made to these objects.", "description": "Discard changes made to these objects.",
"depends": [ "$location" ] "depends": [ "$location", "urlService" ]
} }
], ],
"policies": [ "policies": [

View File

@ -30,7 +30,7 @@ define(
* Edit Mode. Exits the editing user interface and invokes object * Edit Mode. Exits the editing user interface and invokes object
* capabilities to persist the changes that have been made. * capabilities to persist the changes that have been made.
*/ */
function CancelAction($location, context) { function CancelAction($location, urlService, context) {
var domainObject = context.domainObject; var domainObject = context.domainObject;
// Look up the object's "editor.completion" capability; // Look up the object's "editor.completion" capability;
@ -50,7 +50,10 @@ define(
// Discard the current root view (which will be the editing // Discard the current root view (which will be the editing
// UI, which will have been pushed atop the Browise UI.) // UI, which will have been pushed atop the Browise UI.)
function returnToBrowse() { function returnToBrowse() {
$location.path("/browse"); $location.path($location.path(urlService.urlForLocation(
"browse",
domainObject
)));
} }
return { return {

View File

@ -31,7 +31,7 @@ define(
* Edit Mode. Exits the editing user interface and invokes object * Edit Mode. Exits the editing user interface and invokes object
* capabilities to persist the changes that have been made. * capabilities to persist the changes that have been made.
*/ */
function SaveAction($location, context) { function SaveAction($location, urlService, context) {
var domainObject = context.domainObject; var domainObject = context.domainObject;
// Invoke any save behavior introduced by the editor capability; // Invoke any save behavior introduced by the editor capability;
@ -45,7 +45,10 @@ define(
// Discard the current root view (which will be the editing // Discard the current root view (which will be the editing
// UI, which will have been pushed atop the Browise UI.) // UI, which will have been pushed atop the Browise UI.)
function returnToBrowse() { function returnToBrowse() {
return $location.path("/browse"); return $location.path(urlService.urlForLocation(
"browse",
domainObject
));
} }
return { return {

View File

@ -30,6 +30,7 @@ define(
var mockLocation, var mockLocation,
mockDomainObject, mockDomainObject,
mockEditorCapability, mockEditorCapability,
mockUrlService,
actionContext, actionContext,
action; action;
@ -54,7 +55,10 @@ define(
"editor", "editor",
[ "save", "cancel" ] [ "save", "cancel" ]
); );
mockUrlService = jasmine.createSpyObj(
"urlService",
["urlForLocation"]
);
actionContext = { actionContext = {
domainObject: mockDomainObject domainObject: mockDomainObject
@ -64,7 +68,7 @@ define(
mockDomainObject.getCapability.andReturn(mockEditorCapability); mockDomainObject.getCapability.andReturn(mockEditorCapability);
mockEditorCapability.cancel.andReturn(mockPromise(true)); mockEditorCapability.cancel.andReturn(mockPromise(true));
action = new CancelAction(mockLocation, actionContext); action = new CancelAction(mockLocation, mockUrlService, actionContext);
}); });
@ -91,7 +95,9 @@ define(
it("returns to browse when performed", function () { it("returns to browse when performed", function () {
action.perform(); action.perform();
expect(mockLocation.path).toHaveBeenCalledWith("/browse"); expect(mockLocation.path).toHaveBeenCalledWith(
mockUrlService.urlForLocation("browse", mockDomainObject)
);
}); });
}); });
} }

View File

@ -30,6 +30,7 @@ define(
var mockLocation, var mockLocation,
mockDomainObject, mockDomainObject,
mockEditorCapability, mockEditorCapability,
mockUrlService,
actionContext, actionContext,
action; action;
@ -54,6 +55,10 @@ define(
"editor", "editor",
[ "save", "cancel" ] [ "save", "cancel" ]
); );
mockUrlService = jasmine.createSpyObj(
"urlService",
["urlForLocation"]
);
actionContext = { actionContext = {
@ -64,7 +69,7 @@ define(
mockDomainObject.getCapability.andReturn(mockEditorCapability); mockDomainObject.getCapability.andReturn(mockEditorCapability);
mockEditorCapability.save.andReturn(mockPromise(true)); mockEditorCapability.save.andReturn(mockPromise(true));
action = new SaveAction(mockLocation, actionContext); action = new SaveAction(mockLocation, mockUrlService, actionContext);
}); });
@ -91,7 +96,9 @@ define(
it("returns to browse when performed", function () { it("returns to browse when performed", function () {
action.perform(); action.perform();
expect(mockLocation.path).toHaveBeenCalledWith("/browse"); expect(mockLocation.path).toHaveBeenCalledWith(
mockUrlService.urlForLocation("browse", mockDomainObject)
);
}); });
}); });
} }

View File

@ -3,6 +3,13 @@
"description": "General UI elements, meant to be reused across modes", "description": "General UI elements, meant to be reused across modes",
"resources": "res", "resources": "res",
"extensions": { "extensions": {
"services": [
{
"key": "urlService",
"implementation": "/services/UrlService.js",
"depends": [ "$location" ]
}
],
"runs": [ "runs": [
{ {
"implementation": "StyleSheetLoader.js", "implementation": "StyleSheetLoader.js",

View File

@ -75,4 +75,5 @@
<glyph unicode="&#xf5;" d="M638 898c0 35.4-28.6 64-64 64h-128c-35.4 0-64-28.6-64-64s28.6-64 64-64h128c35.4 0 64 28.6 64 64zM510 834c-247.4 0-448-200.6-448-448s200.6-448 448-448 448 200.6 448 448-200.6 448-448 448zM510 386h-336c0 185.2 150.8 336 336 336v-336z" /> <glyph unicode="&#xf5;" d="M638 898c0 35.4-28.6 64-64 64h-128c-35.4 0-64-28.6-64-64s28.6-64 64-64h128c35.4 0 64 28.6 64 64zM510 834c-247.4 0-448-200.6-448-448s200.6-448 448-448 448 200.6 448 448-200.6 448-448 448zM510 386h-336c0 185.2 150.8 336 336 336v-336z" />
<glyph unicode="&#xf6;" d="M448 578c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024 578c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM448 2c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024 2c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320z" /> <glyph unicode="&#xf6;" d="M448 578c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024 578c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM448 2c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024 2c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320z" />
<glyph unicode="&#xe603;" d="M998.208 111.136l-422.702 739.728c-34.928 61.124-92.084 61.124-127.012 0l-422.702-739.728c-34.928-61.126-5.906-111.136 64.494-111.136h843.428c70.4 0 99.422 50.010 64.494 111.136zM512 128c-35.2 0-64 28.8-64 64s28.8 64 64 64 64-28.8 64-64c0-35.2-28.8-64-64-64zM627.448 577.242l-38.898-194.486c-6.902-34.516-41.35-62.756-76.55-62.756s-69.648 28.24-76.552 62.758l-38.898 194.486c-6.902 34.516 16.25 62.756 51.45 62.756h128c35.2 0 58.352-28.24 51.448-62.758z" /> <glyph unicode="&#xe603;" d="M998.208 111.136l-422.702 739.728c-34.928 61.124-92.084 61.124-127.012 0l-422.702-739.728c-34.928-61.126-5.906-111.136 64.494-111.136h843.428c70.4 0 99.422 50.010 64.494 111.136zM512 128c-35.2 0-64 28.8-64 64s28.8 64 64 64 64-28.8 64-64c0-35.2-28.8-64-64-64zM627.448 577.242l-38.898-194.486c-6.902-34.516-41.35-62.756-76.55-62.756s-69.648 28.24-76.552 62.758l-38.898 194.486c-6.902 34.516 16.25 62.756 51.45 62.756h128c35.2 0 58.352-28.24 51.448-62.758z" />
<glyph unicode="&#xe60d;" d="M1024 448l-448-512v1024zM448 960l-448-512 448-512z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -57,13 +57,17 @@ define(
// mouse may leave this element during the drag. // mouse may leave this element during the drag.
var body = $document.find('body'), var body = $document.find('body'),
initialPosition, initialPosition,
$event,
delta; delta;
// Utility function to cause evaluation of mctDrag, // Utility function to cause evaluation of mctDrag,
// mctDragUp, etc // mctDragUp, etc
function fireListener(name) { function fireListener(name) {
// Evaluate the expression, with current delta // Evaluate the expression, with current delta
scope.$eval(attrs[name], { delta: delta }); scope.$eval(attrs[name], {
delta: delta,
$event: $event
});
// Trigger prompt digestion // Trigger prompt digestion
scope.$apply(); scope.$apply();
@ -82,6 +86,9 @@ define(
delta = currentPosition.map(function (v, i) { delta = currentPosition.map(function (v, i) {
return v - initialPosition[i]; return v - initialPosition[i];
}); });
// Also track the plain event for firing listeners
$event = event;
} }
// Called during a drag, on mousemove // Called during a drag, on mousemove
@ -106,7 +113,7 @@ define(
fireListener("mctDragUp"); fireListener("mctDragUp");
// Clear out start-of-drag position // Clear out start-of-drag position, target
initialPosition = undefined; initialPosition = undefined;
// Don't show selection highlights, etc // Don't show selection highlights, etc
@ -131,6 +138,7 @@ define(
// Don't show selection highlights, etc // Don't show selection highlights, etc
event.preventDefault(); event.preventDefault();
return false; return false;
} }

View File

@ -39,34 +39,52 @@ define(
// is returned. The view is defaulted to // is returned. The view is defaulted to
// the current location's (current object's) // the current location's (current object's)
// view set. // view set.
function urlFor(mode, domainObject) { function urlForLocation(mode, domainObject) {
var context = domainObject && var context = domainObject &&
domainObject.getCapability('context'), domainObject.getCapability('context'),
objectPath = context ? context.getPath() : [], objectPath = context ? context.getPath() : [],
ids = objectPath.map(function (domainObject) { ids = objectPath.map(function (domainObject) {
return domainObject.getId(); return domainObject.getId();
}), }),
viewPath = "?view=" + $location.search().view,
// Parses the path together. Starts with the // Parses the path together. Starts with the
// default index.html file, then the mode passed // default index.html file, then the mode passed
// into the service, followed by ids in the url // into the service, followed by ids in the url
// joined by '/', and lastly the view path from // joined by '/', and lastly the view path from
// the current location // the current location
path = "index.html#/" + mode + "/" + path = mode + "/" + ids.slice(1).join("/");
ids.slice(1).join("/") + viewPath;
return path; return path;
} }
// Uses the Url for the current location
// from the urlForLocation function and
// includes the view and the index path
function urlForNewTab(mode, domainObject) {
var viewPath = "?view=" + $location.search().view,
newTabPath =
"index.html#" + urlForLocation(mode, domainObject) + viewPath;
return newTabPath;
}
return { return {
/** /**
* Returns the Url path for a specific domain object * Returns the Url path for a specific domain object
* without the index.html path and the view path
* @param {value} value of the browse or edit mode * @param {value} value of the browse or edit mode
* for the path * for the path
* @param {DomainObject} value of the domain object * @param {DomainObject} value of the domain object
* to get the path of * to get the path of
*/ */
urlFor: urlFor urlForNewTab: urlForNewTab,
/**
* Returns the Url path for a specific domain object
* including the index.html path and the view path
* allowing a new tab to hold the correct characteristics
* @param {value} value of the browse or edit mode
* for the path
* @param {DomainObject} value of the domain object
* to get the path of
*/
urlForLocation: urlForLocation
}; };
} }

View File

@ -81,10 +81,11 @@ define(
}); });
it("invokes mctDragDown when dragging begins", function () { it("invokes mctDragDown when dragging begins", function () {
mockElement.on.mostRecentCall.args[1](testEvent(42, 60)); var event = testEvent(42, 60);
mockElement.on.mostRecentCall.args[1](event);
expect(mockScope.$eval).toHaveBeenCalledWith( expect(mockScope.$eval).toHaveBeenCalledWith(
testAttrs.mctDragDown, testAttrs.mctDragDown,
{ delta: [0, 0] } { delta: [0, 0], $event: event }
); );
}); });
@ -101,23 +102,27 @@ define(
}); });
it("invokes mctDrag expression during drag", function () { it("invokes mctDrag expression during drag", function () {
var event;
mockElement.on.mostRecentCall.args[1](testEvent(42, 60)); mockElement.on.mostRecentCall.args[1](testEvent(42, 60));
// Find and invoke the mousemove listener // Find and invoke the mousemove listener
mockBody.on.calls.forEach(function (call) { mockBody.on.calls.forEach(function (call) {
if (call.args[0] === 'mousemove') { if (call.args[0] === 'mousemove') {
call.args[1](testEvent(52, 200)); call.args[1](event = testEvent(52, 200));
} }
}); });
// Should have passed that delta to mct-drag expression // Should have passed that delta to mct-drag expression
expect(mockScope.$eval).toHaveBeenCalledWith( expect(mockScope.$eval).toHaveBeenCalledWith(
testAttrs.mctDrag, testAttrs.mctDrag,
{ delta: [10, 140] } { delta: [10, 140], $event: event }
); );
}); });
it("invokes mctDragUp expression after drag", function () { it("invokes mctDragUp expression after drag", function () {
var event;
mockElement.on.mostRecentCall.args[1](testEvent(42, 60)); mockElement.on.mostRecentCall.args[1](testEvent(42, 60));
// Find and invoke the mousemove listener // Find and invoke the mousemove listener
@ -129,7 +134,7 @@ define(
// Find and invoke the mousemove listener // Find and invoke the mousemove listener
mockBody.on.calls.forEach(function (call) { mockBody.on.calls.forEach(function (call) {
if (call.args[0] === 'mouseup') { if (call.args[0] === 'mouseup') {
call.args[1](testEvent(40, 71)); call.args[1](event = testEvent(40, 71));
} }
}); });
@ -138,7 +143,7 @@ define(
// initial position // initial position
expect(mockScope.$eval).toHaveBeenCalledWith( expect(mockScope.$eval).toHaveBeenCalledWith(
testAttrs.mctDragUp, testAttrs.mctDragUp,
{ delta: [-2, 11] } { delta: [-2, 11], $event: event }
); );
// Should also have unregistered listeners // Should also have unregistered listeners

View File

@ -31,7 +31,11 @@ define(
describe("The url service", function () { describe("The url service", function () {
var urlService, var urlService,
mockLocation; mockLocation,
mockDomainObject,
mockContext,
mockMode,
testViews;
beforeEach(function () { beforeEach(function () {
// Creates a mockLocation, used to // Creates a mockLocation, used to
@ -41,24 +45,20 @@ define(
[ "path", "search" ] [ "path", "search" ]
); );
urlService = new UrlService(mockLocation); // The mockDomainObject is initialized as a
});
it("get url for a domainObject and mode", function () {
// The mockDomainObject is initialized as a
// spy object to ultimately be passed into the // spy object to ultimately be passed into the
// urlService urlFor function // urlService urlFor function
var mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
"domainObject", "domainObject",
[ "getId", "getCapability", "getModel", "useCapability" ] [ "getId", "getCapability", "getModel", "useCapability" ]
), );
mockContext = jasmine.createSpyObj('context', ['getPath']), mockContext = jasmine.createSpyObj('context', ['getPath']);
testViews = [ testViews = [
{ key: 'abc' }, { key: 'abc' },
{ key: 'def', someKey: 'some value' }, { key: 'def', someKey: 'some value' },
{ key: 'xyz' } { key: 'xyz' }
], ];
mockMode = "browse"; mockMode = "browse";
// The mockContext is set a path // The mockContext is set a path
// for the mockDomainObject // for the mockDomainObject
@ -81,7 +81,16 @@ define(
// Uses the mockLocation to get the current // Uses the mockLocation to get the current
// "mock" website's view // "mock" website's view
mockLocation.search.andReturn({ view: 'def' }); mockLocation.search.andReturn({ view: 'def' });
urlService.urlFor(mockMode, mockDomainObject);
urlService = new UrlService(mockLocation);
});
it("get url for a location using domainObject and mode", function () {
urlService.urlForLocation(mockMode, mockDomainObject);
});
it("get url for a new tab using domainObject and mode", function () {
urlService.urlForNewTab(mockMode, mockDomainObject);
}); });
}); });
} }

View File

@ -13,5 +13,6 @@
"directives/MCTDrag", "directives/MCTDrag",
"directives/MCTResize", "directives/MCTResize",
"directives/MCTScroll", "directives/MCTScroll",
"services/UrlService",
"StyleSheetLoader" "StyleSheetLoader"
] ]

View File

@ -183,7 +183,7 @@
{ {
"key": "mutation", "key": "mutation",
"implementation": "capabilities/MutationCapability.js", "implementation": "capabilities/MutationCapability.js",
"depends": [ "now" ] "depends": [ "topic", "now" ]
}, },
{ {
"key": "delegation", "key": "delegation",
@ -200,6 +200,10 @@
"key": "throttle", "key": "throttle",
"implementation": "services/Throttle.js", "implementation": "services/Throttle.js",
"depends": [ "$timeout" ] "depends": [ "$timeout" ]
},
{
"key": "topic",
"implementation": "services/Topic.js"
} }
], ],
"roots": [ "roots": [

View File

@ -29,6 +29,8 @@ define(
function () { function () {
"use strict"; "use strict";
var TOPIC_PREFIX = "mutation:";
// Utility function to overwrite a destination object // Utility function to overwrite a destination object
// with the contents of a source object. // with the contents of a source object.
function copyValues(destination, source) { function copyValues(destination, source) {
@ -71,7 +73,8 @@ define(
* which will expose this capability * which will expose this capability
* @constructor * @constructor
*/ */
function MutationCapability(now, domainObject) { function MutationCapability(topic, now, domainObject) {
var t = topic(TOPIC_PREFIX + domainObject.getId());
function mutate(mutator, timestamp) { function mutate(mutator, timestamp) {
// Get the object's model and clone it, so the // Get the object's model and clone it, so the
@ -96,6 +99,7 @@ define(
copyValues(model, result); copyValues(model, result);
} }
model.modified = useTimestamp ? timestamp : now(); model.modified = useTimestamp ? timestamp : now();
t.notify(model);
} }
// Report the result of the mutation // Report the result of the mutation
@ -107,6 +111,10 @@ define(
return fastPromise(mutator(clone)).then(handleMutation); return fastPromise(mutator(clone)).then(handleMutation);
} }
function listen(listener) {
return t.listen(listener);
}
return { return {
/** /**
* Alias of `mutate`, used to support useCapability. * Alias of `mutate`, used to support useCapability.
@ -139,7 +147,16 @@ define(
* @returns {Promise.<boolean>} a promise for the result * @returns {Promise.<boolean>} a promise for the result
* of the mutation; true if changes were made. * of the mutation; true if changes were made.
*/ */
mutate: mutate mutate: mutate,
/**
* Listen for mutations of this domain object's model.
* The provided listener will be invoked with the domain
* object's new model after any changes. To stop listening,
* invoke the function returned by this method.
* @param {Function} listener function to call on mutation
* @returns {Function} a function to stop listening
*/
listen: listen
}; };
} }

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/ /*global define*/
define( define(

View File

@ -0,0 +1,87 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
/**
* The `topic` service provides a way to create both named,
* shared listeners and anonymous, private listeners.
*
* Usage:
*
* ```
* var t = topic('foo'); // Use/create a named topic
* t.listen(function () { ... });
* t.notify({ some: "message" });
* ```
*
* Named topics are shared; multiple calls to `topic`
* with the same argument will return a single object instance.
* Anonymous topics (where `topic` has been called with no
* arguments) are private; each call returns a new instance.
*
* @returns {Function}
*/
function Topic() {
var topics = {};
function createTopic() {
var listeners = [];
return {
listen: function (listener) {
listeners.push(listener);
return function unlisten() {
listeners = listeners.filter(function (l) {
return l !== listener;
});
};
},
notify: function (message) {
listeners.forEach(function (listener) {
listener(message);
});
}
};
}
/**
* Use and (if necessary) create a new topic.
* @param {string} [key] name of the topic to use
*/
return function (key) {
if (arguments.length < 1) {
return createTopic();
} else {
topics[key] = topics[key] || createTopic();
return topics[key];
}
};
}
return Topic;
}
);

View File

@ -25,21 +25,33 @@
* MutationCapabilitySpec. Created by vwoeltje on 11/6/14. * MutationCapabilitySpec. Created by vwoeltje on 11/6/14.
*/ */
define( define(
["../../src/capabilities/MutationCapability"], [
function (MutationCapability) { "../../src/capabilities/MutationCapability",
"../../src/services/Topic"
],
function (MutationCapability, Topic) {
"use strict"; "use strict";
describe("The mutation capability", function () { describe("The mutation capability", function () {
var testModel, var testModel,
topic,
mockNow, mockNow,
domainObject = { getModel: function () { return testModel; } }, domainObject = {
getId: function () { return "test-id"; },
getModel: function () { return testModel; }
},
mutation; mutation;
beforeEach(function () { beforeEach(function () {
testModel = { number: 6 }; testModel = { number: 6 };
topic = new Topic();
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
mockNow.andReturn(12321); mockNow.andReturn(12321);
mutation = new MutationCapability(mockNow, domainObject); mutation = new MutationCapability(
topic,
mockNow,
domainObject
);
}); });
it("allows mutation of a model", function () { it("allows mutation of a model", function () {
@ -83,6 +95,42 @@ define(
// Should have gotten a timestamp from 'now' // Should have gotten a timestamp from 'now'
expect(testModel.modified).toEqual(42); expect(testModel.modified).toEqual(42);
}); });
it("notifies listeners of mutation", function () {
var mockCallback = jasmine.createSpy('callback');
mutation.listen(mockCallback);
mutation.invoke(function (m) {
m.number = 8;
});
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback.mostRecentCall.args[0].number)
.toEqual(8);
});
it("allows listeners to stop listening", function () {
var mockCallback = jasmine.createSpy('callback');
mutation.listen(mockCallback)(); // Unlisten immediately
mutation.invoke(function (m) {
m.number = 8;
});
expect(mockCallback).not.toHaveBeenCalled();
});
it("shares listeners across instances", function () {
var mockCallback = jasmine.createSpy('callback'),
otherMutation = new MutationCapability(
topic,
mockNow,
domainObject
);
mutation.listen(mockCallback);
otherMutation.invoke(function (m) {
m.number = 8;
});
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback.mostRecentCall.args[0].number)
.toEqual(8);
});
}); });
} }
); );

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define( define(

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/services/Topic"],
function (Topic) {
"use strict";
describe("The 'topic' service", function () {
var topic,
testMessage,
mockCallback;
beforeEach(function () {
testMessage = { someKey: "some value"};
mockCallback = jasmine.createSpy('callback');
topic = new Topic();
});
it("notifies listeners on a topic", function () {
topic("abc").listen(mockCallback);
topic("abc").notify(testMessage);
expect(mockCallback).toHaveBeenCalledWith(testMessage);
});
it("does not notify listeners across topics", function () {
topic("abc").listen(mockCallback);
topic("xyz").notify(testMessage);
expect(mockCallback).not.toHaveBeenCalledWith(testMessage);
});
it("does not notify listeners after unlistening", function () {
topic("abc").listen(mockCallback)(); // Unlisten immediately
topic("abc").notify(testMessage);
expect(mockCallback).not.toHaveBeenCalledWith(testMessage);
});
it("provides anonymous private topics", function () {
var t1 = topic(), t2 = topic();
t1.listen(mockCallback);
t2.notify(testMessage);
expect(mockCallback).not.toHaveBeenCalledWith(testMessage);
t1.notify(testMessage);
expect(mockCallback).toHaveBeenCalledWith(testMessage);
});
});
}
);

View File

@ -25,6 +25,7 @@
"services/Now", "services/Now",
"services/Throttle", "services/Throttle",
"services/Topic",
"types/MergeModels", "types/MergeModels",
"types/TypeCapability", "types/TypeCapability",

View File

@ -23,7 +23,21 @@
{ {
"key": "PlotController", "key": "PlotController",
"implementation": "PlotController.js", "implementation": "PlotController.js",
"depends": [ "$scope", "telemetryFormatter", "telemetryHandler", "throttle" ] "depends": [
"$scope",
"telemetryFormatter",
"telemetryHandler",
"throttle",
"PLOT_FIXED_DURATION"
]
}
],
"constants": [
{
"key": "PLOT_FIXED_DURATION",
"value": 900000,
"priority": "fallback",
"comment": "Fifteen minutes."
} }
], ],
"policies": [ "policies": [

View File

@ -82,8 +82,9 @@
<mct-chart draw="subplot.getDrawingObject()" <mct-chart draw="subplot.getDrawingObject()"
ng-mousemove="subplot.hover($event)" ng-mousemove="subplot.hover($event)"
ng-mousedown="subplot.startMarquee($event)" mct-drag="subplot.continueDrag($event)"
ng-mouseup="subplot.endMarquee($event); plot.update()"> mct-drag-down="subplot.startDrag($event)"
mct-drag-up="subplot.endDrag($event); plot.update()">
</mct-chart> </mct-chart>
<!-- TODO: Move into correct position; make part of group; infer from set of actions --> <!-- TODO: Move into correct position; make part of group; infer from set of actions -->

View File

@ -51,7 +51,13 @@ define(
* *
* @constructor * @constructor
*/ */
function PlotController($scope, telemetryFormatter, telemetryHandler, throttle) { function PlotController(
$scope,
telemetryFormatter,
telemetryHandler,
throttle,
PLOT_FIXED_DURATION
) {
var subPlotFactory = new SubPlotFactory(telemetryFormatter), var subPlotFactory = new SubPlotFactory(telemetryFormatter),
modeOptions = new PlotModeOptions([], subPlotFactory), modeOptions = new PlotModeOptions([], subPlotFactory),
subplots = [], subplots = [],
@ -99,7 +105,8 @@ define(
updater = new PlotUpdater( updater = new PlotUpdater(
handle, handle,
($scope.axes[0].active || {}).key, ($scope.axes[0].active || {}).key,
($scope.axes[1].active || {}).key ($scope.axes[1].active || {}).key,
PLOT_FIXED_DURATION
); );
} }

View File

@ -56,6 +56,9 @@ define(
domainOffset, domainOffset,
mousePosition, mousePosition,
marqueeStart, marqueeStart,
panStart,
panStartBounds,
subPlotBounds,
hoverCoordinates, hoverCoordinates,
isHovering = false; isHovering = false;
@ -88,8 +91,7 @@ define(
// pixel coordinates in the canvas area) from a mouse // pixel coordinates in the canvas area) from a mouse
// event object. // event object.
function toMousePosition($event) { function toMousePosition($event) {
var target = $event.target, var bounds = subPlotBounds;
bounds = target.getBoundingClientRect();
return { return {
x: $event.clientX - bounds.left, x: $event.clientX - bounds.left,
@ -155,6 +157,25 @@ define(
tickGenerator.generateRangeTicks(RANGE_TICKS); tickGenerator.generateRangeTicks(RANGE_TICKS);
} }
function updatePan() {
var start, current, delta, nextOrigin;
// Clear the previous panning pan-zoom state
panZoomStack.popPanZoom();
// Calculate what the new resulting pan-zoom should be
start = mousePositionToDomainRange(panStart);
current = mousePositionToDomainRange(mousePosition);
delta = [ current[0] - start[0], current[1] - start[1] ];
nextOrigin = [
panStartBounds.origin[0] - delta[0],
panStartBounds.origin[1] - delta[1]
];
// ...and push a new one at the current mouse position
panZoomStack.pushPanZoom(nextOrigin, panStartBounds.dimensions);
}
// Perform a marquee zoom. // Perform a marquee zoom.
function marqueeZoom(start, end) { function marqueeZoom(start, end) {
@ -241,31 +262,77 @@ define(
*/ */
hover: function ($event) { hover: function ($event) {
isHovering = true; isHovering = true;
subPlotBounds = $event.target.getBoundingClientRect();
mousePosition = toMousePosition($event); mousePosition = toMousePosition($event);
updateHoverCoordinates(); updateHoverCoordinates();
if (marqueeStart) { if (marqueeStart) {
updateMarqueeBox(); updateMarqueeBox();
} }
if (panStart) {
updatePan();
updateDrawingBounds();
updateTicks();
}
},
/**
* Continue a previously-start pan or zoom gesture.
* @param $event the mouse event
*/
continueDrag: function ($event) {
mousePosition = toMousePosition($event);
if (marqueeStart) {
updateMarqueeBox();
}
if (panStart) {
updatePan();
updateDrawingBounds();
updateTicks();
}
}, },
/** /**
* Initiate a marquee zoom action. * Initiate a marquee zoom action.
* @param $event the mouse event * @param $event the mouse event
*/ */
startMarquee: function ($event) { startDrag: function ($event) {
mousePosition = marqueeStart = toMousePosition($event); subPlotBounds = $event.target.getBoundingClientRect();
updateMarqueeBox(); mousePosition = toMousePosition($event);
// Treat any modifier key as a pan
if ($event.altKey || $event.shiftKey || $event.ctrlKey) {
// Start panning
panStart = mousePosition;
panStartBounds = panZoomStack.getPanZoom();
// We're starting a pan, so add this back as a
// state on the stack; it will get replaced
// during the pan.
panZoomStack.pushPanZoom(
panStartBounds.origin,
panStartBounds.dimensions
);
$event.preventDefault();
} else {
// Start marquee zooming
marqueeStart = mousePosition;
updateMarqueeBox();
}
}, },
/** /**
* Complete a marquee zoom action. * Complete a marquee zoom action.
* @param $event the mouse event * @param $event the mouse event
*/ */
endMarquee: function ($event) { endDrag: function ($event) {
mousePosition = toMousePosition($event); mousePosition = toMousePosition($event);
subPlotBounds = undefined;
if (marqueeStart) { if (marqueeStart) {
marqueeZoom(marqueeStart, mousePosition); marqueeZoom(marqueeStart, mousePosition);
marqueeStart = undefined; marqueeStart = undefined;
updateMarqueeBox(); updateMarqueeBox();
updateDrawingBounds(); updateDrawingBounds();
updateTicks();
}
if (panStart) {
// End panning
panStart = undefined;
panStartBounds = undefined;
} }
}, },
/** /**

View File

@ -43,9 +43,9 @@ define(
var mid = Math.floor((min + max) / 2), var mid = Math.floor((min + max) / 2),
found = buffer[mid * 2]; found = buffer[mid * 2];
// Collisions are not wanted // On collisions, insert at same index
if (found === value) { if (found === value) {
return -1; return mid;
} }
// Otherwise, if we're down to a single index, // Otherwise, if we're down to a single index,

View File

@ -42,14 +42,17 @@ define(
* @param {TelemetryHandle} handle the handle to telemetry access * @param {TelemetryHandle} handle the handle to telemetry access
* @param {string} domain the key to use when looking up domain values * @param {string} domain the key to use when looking up domain values
* @param {string} range the key to use when looking up range values * @param {string} range the key to use when looking up range values
* @param {number} maxDuration maximum plot duration to display
* @param {number} maxPoints maximum number of points to display
*/ */
function PlotUpdater(handle, domain, range, maxPoints) { function PlotUpdater(handle, domain, range, fixedDuration, maxPoints) {
var ids = [], var ids = [],
lines = {}, lines = {},
dimensions = [0, 0], dimensions = [0, 0],
origin = [0, 0], origin = [0, 0],
domainExtrema, domainExtrema,
rangeExtrema, rangeExtrema,
buffers = {},
bufferArray = [], bufferArray = [],
domainOffset; domainOffset;
@ -61,11 +64,10 @@ define(
// Check if this set of ids matches the current set of ids // Check if this set of ids matches the current set of ids
// (used to detect if line preparation can be skipped) // (used to detect if line preparation can be skipped)
function idsMatch(nextIds) { function idsMatch(nextIds) {
return nextIds.map(function (id, index) { return ids.length === nextIds.length &&
return ids[index] === id; nextIds.every(function (id, index) {
}).reduce(function (a, b) { return ids[index] === id;
return a && b; });
}, true);
} }
// Prepare plot lines for this group of telemetry objects // Prepare plot lines for this group of telemetry objects
@ -74,7 +76,7 @@ define(
next = {}; next = {};
// Detect if we already have everything we need prepared // Detect if we already have everything we need prepared
if (ids.length === nextIds.length && idsMatch(nextIds)) { if (idsMatch(nextIds)) {
// Nothing to prepare, move on // Nothing to prepare, move on
return; return;
} }
@ -88,13 +90,13 @@ define(
// Create buffers for these objects // Create buffers for these objects
bufferArray = ids.map(function (id) { bufferArray = ids.map(function (id) {
var buffer = new PlotLineBuffer( buffers[id] = buffers[id] || new PlotLineBuffer(
domainOffset, domainOffset,
INITIAL_SIZE, INITIAL_SIZE,
maxPoints maxPoints
); );
next[id] = lines[id] || new PlotLine(buffer); next[id] = lines[id] || new PlotLine(buffers[id]);
return buffer; return buffers[id];
}); });
} }
@ -107,6 +109,7 @@ define(
lines = next; lines = next;
} }
// Initialize the domain offset, based on these observed values // Initialize the domain offset, based on these observed values
function initializeDomainOffset(values) { function initializeDomainOffset(values) {
domainOffset = domainOffset =
@ -133,7 +136,7 @@ define(
} }
// Update dimensions and origin based on extrema of plots // Update dimensions and origin based on extrema of plots
function updateExtrema() { function updateBounds() {
if (bufferArray.length > 0) { if (bufferArray.length > 0) {
domainExtrema = bufferArray.map(function (lineBuffer) { domainExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getDomainExtrema(); return lineBuffer.getDomainExtrema();
@ -143,10 +146,40 @@ define(
return lineBuffer.getRangeExtrema(); return lineBuffer.getRangeExtrema();
}).reduce(reduceExtrema); }).reduce(reduceExtrema);
// Calculate best-fit dimensions
dimensions = (rangeExtrema[0] === rangeExtrema[1]) ? dimensions = (rangeExtrema[0] === rangeExtrema[1]) ?
[dimensionsOf(domainExtrema), 2.0 ] : [dimensionsOf(domainExtrema), 2.0 ] :
[dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)]; [dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)];
origin = [originOf(domainExtrema), originOf(rangeExtrema)]; origin = [originOf(domainExtrema), originOf(rangeExtrema)];
// ...then enforce a fixed duration if needed
if (fixedDuration !== undefined) {
origin[0] = origin[0] + dimensions[0] - fixedDuration;
dimensions[0] = fixedDuration;
}
}
}
// Enforce maximum duration on all plot lines; not that
// domain extrema must be up-to-date for this to behave correctly.
function enforceDuration() {
var cutoff;
function enforceDurationForBuffer(plotLineBuffer) {
var index = plotLineBuffer.findInsertionIndex(cutoff);
if (index > 0) {
// Leave one point untrimmed, such that line will
// continue off left edge of visible plot area.
plotLineBuffer.trim(index - 1);
}
}
if (fixedDuration !== undefined &&
domainExtrema !== undefined &&
(domainExtrema[1] - domainExtrema[0] > fixedDuration)) {
cutoff = domainExtrema[1] - fixedDuration;
bufferArray.forEach(enforceDurationForBuffer);
updateBounds(); // Extrema may have changed now
} }
} }
@ -180,8 +213,8 @@ define(
// Add new data // Add new data
objects.forEach(addPointFor); objects.forEach(addPointFor);
// Finally, update extrema // Then, update extrema
updateExtrema(); updateBounds();
} }
// Add historical data for this domain object // Add historical data for this domain object
@ -213,12 +246,12 @@ define(
line.addSeries(series, domain, range); line.addSeries(series, domain, range);
} }
// Finally, update extrema // Update extrema
updateExtrema(); updateBounds();
} }
// Use a default MAX_POINTS if none is provided // Use a default MAX_POINTS if none is provided
maxPoints = maxPoints || MAX_POINTS; maxPoints = maxPoints !== undefined ? maxPoints : MAX_POINTS;
// Initially prepare state for these objects. // Initially prepare state for these objects.
// Note that this may be an empty array at this time, // Note that this may be an empty array at this time,

View File

@ -127,7 +127,7 @@ define(
// Simulate a marquee zoom. Note that the mockElement // Simulate a marquee zoom. Note that the mockElement
// is 100 by 100 and starts at 10,20 // is 100 by 100 and starts at 10,20
subplot.startMarquee({ subplot.startDrag({
target: mockElement, target: mockElement,
clientX: 60, clientX: 60,
clientY: 45 clientY: 45
@ -137,7 +137,7 @@ define(
clientX: 75, clientX: 75,
clientY: 85 clientY: 85
}); });
subplot.endMarquee({ subplot.endDrag({
target: mockElement, target: mockElement,
clientX: 80, clientX: 80,
clientY: 95 clientY: 95
@ -162,7 +162,7 @@ define(
// Simulate a marquee zoom. Note that the mockElement // Simulate a marquee zoom. Note that the mockElement
// is 100 by 100 and starts at 10,20 // is 100 by 100 and starts at 10,20
subplot.startMarquee({ subplot.startDrag({
target: mockElement, target: mockElement,
clientX: 60, clientX: 60,
clientY: 45 clientY: 45
@ -172,7 +172,7 @@ define(
clientX: 75, clientX: 75,
clientY: 85 clientY: 85
}); });
subplot.endMarquee({ subplot.endDrag({
target: mockElement, target: mockElement,
clientX: 60, clientX: 60,
clientY: 45 clientY: 45

View File

@ -83,9 +83,6 @@ define(
expect(buffer.findInsertionIndex(10)).toEqual(4); expect(buffer.findInsertionIndex(10)).toEqual(4);
expect(buffer.findInsertionIndex(14.5)).toEqual(5); expect(buffer.findInsertionIndex(14.5)).toEqual(5);
expect(buffer.findInsertionIndex(20)).toEqual(6); expect(buffer.findInsertionIndex(20)).toEqual(6);
// 9 is already in there, disallow insertion
expect(buffer.findInsertionIndex(9)).toEqual(-1);
}); });
it("allows insertion in the middle", function () { it("allows insertion in the middle", function () {

View File

@ -4,12 +4,12 @@
{ {
"key": "mctInclude", "key": "mctInclude",
"implementation": "MCTInclude.js", "implementation": "MCTInclude.js",
"depends": [ "templates[]" ] "depends": [ "templates[]", "$sce" ]
}, },
{ {
"key": "mctRepresentation", "key": "mctRepresentation",
"implementation": "MCTRepresentation.js", "implementation": "MCTRepresentation.js",
"depends": [ "representations[]", "views[]", "representers[]", "$q", "$log" ] "depends": [ "representations[]", "views[]", "representers[]", "$q", "$sce", "$log" ]
} }
], ],
"gestures": [ "gestures": [

View File

@ -53,17 +53,17 @@ define(
* @param {TemplateDefinition[]} templates an array of * @param {TemplateDefinition[]} templates an array of
* template extensions * template extensions
*/ */
function MCTInclude(templates) { function MCTInclude(templates, $sce) {
var templateMap = {}; var templateMap = {};
// Prepopulate templateMap for easy look up by key // Prepopulate templateMap for easy look up by key
templates.forEach(function (template) { templates.forEach(function (template) {
var key = template.key, var key = template.key,
path = [ path = $sce.trustAsResourceUrl([
template.bundle.path, template.bundle.path,
template.bundle.resources, template.bundle.resources,
template.templateUrl template.templateUrl
].join("/"); ].join("/"));
// First found should win (priority ordering) // First found should win (priority ordering)
templateMap[key] = templateMap[key] || path; templateMap[key] = templateMap[key] || path;
}); });

View File

@ -52,7 +52,7 @@ define(
* representation extensions * representation extensions
* @param {ViewDefinition[]} views an array of view extensions * @param {ViewDefinition[]} views an array of view extensions
*/ */
function MCTRepresentation(representations, views, representers, $q, $log) { function MCTRepresentation(representations, views, representers, $q, $sce, $log) {
var representationMap = {}, var representationMap = {},
gestureMap = {}; gestureMap = {};
@ -69,11 +69,11 @@ define(
// Get a path to a representation // Get a path to a representation
function getPath(representation) { function getPath(representation) {
return [ return $sce.trustAsResourceUrl([
representation.bundle.path, representation.bundle.path,
representation.bundle.resources, representation.bundle.resources,
representation.templateUrl representation.templateUrl
].join("/"); ].join("/"));
} }
// Look up a matching representation for this domain object // Look up a matching representation for this domain object

View File

@ -31,6 +31,7 @@ define(
describe("The mct-include directive", function () { describe("The mct-include directive", function () {
var testTemplates, var testTemplates,
mockSce,
mctInclude; mctInclude;
beforeEach(function () { beforeEach(function () {
@ -46,7 +47,14 @@ define(
templateUrl: "z/template.html" templateUrl: "z/template.html"
} }
]; ];
mctInclude = new MCTInclude(testTemplates); mockSce = jasmine.createSpyObj(
'$sce',
['trustAsResourceUrl']
);
mockSce.trustAsResourceUrl.andCallFake(function (url) {
return url;
});
mctInclude = new MCTInclude(testTemplates, mockSce);
}); });
it("has a built-in template, with ng-include src=inclusion", function () { it("has a built-in template, with ng-include src=inclusion", function () {
@ -69,6 +77,12 @@ define(
expect(scope.inclusion).toEqual("x/y/z/template.html"); expect(scope.inclusion).toEqual("x/y/z/template.html");
}); });
it("trusts template URLs", function () {
mctInclude.controller({ key: "xyz" });
expect(mockSce.trustAsResourceUrl)
.toHaveBeenCalledWith("x/y/z/template.html");
});
}); });
} }
); );

View File

@ -38,6 +38,7 @@ define(
testViews, testViews,
mockRepresenters, mockRepresenters,
mockQ, mockQ,
mockSce,
mockLog, mockLog,
mockScope, mockScope,
mockElement, mockElement,
@ -95,8 +96,16 @@ define(
}); });
mockQ = { when: mockPromise }; mockQ = { when: mockPromise };
mockSce = jasmine.createSpyObj(
'$sce',
['trustAsResourceUrl']
);
mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS); mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS);
mockSce.trustAsResourceUrl.andCallFake(function (url) {
return url;
});
mockScope = jasmine.createSpyObj("scope", [ "$watch" ]); mockScope = jasmine.createSpyObj("scope", [ "$watch" ]);
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS); mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS); mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
@ -108,6 +117,7 @@ define(
testViews, testViews,
mockRepresenters, mockRepresenters,
mockQ, mockQ,
mockSce,
mockLog mockLog
); );
}); });
@ -125,9 +135,18 @@ define(
it("watches scope when linked", function () { it("watches scope when linked", function () {
mctRepresentation.link(mockScope, mockElement); mctRepresentation.link(mockScope, mockElement);
expect(mockScope.$watch).toHaveBeenCalledWith("key", jasmine.any(Function)); expect(mockScope.$watch).toHaveBeenCalledWith(
expect(mockScope.$watch).toHaveBeenCalledWith("domainObject", jasmine.any(Function)); "key",
expect(mockScope.$watch).toHaveBeenCalledWith("domainObject.getModel().modified", jasmine.any(Function)); jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject",
jasmine.any(Function)
);
expect(mockScope.$watch).toHaveBeenCalledWith(
"domainObject.getModel().modified",
jasmine.any(Function)
);
}); });
it("recognizes keys for representations", function () { it("recognizes keys for representations", function () {
@ -152,6 +171,18 @@ define(
expect(mockScope.inclusion).toEqual("x/y/z/template.html"); expect(mockScope.inclusion).toEqual("x/y/z/template.html");
}); });
it("trusts template URLs", function () {
mctRepresentation.link(mockScope, mockElement);
mockScope.key = "xyz";
// Trigger the watch
mockScope.$watch.calls[0].args[1]();
expect(mockSce.trustAsResourceUrl)
.toHaveBeenCalledWith("x/y/z/template.html");
});
it("loads declared capabilities", function () { it("loads declared capabilities", function () {
mctRepresentation.link(mockScope, mockElement); mctRepresentation.link(mockScope, mockElement);

View File

@ -59,6 +59,7 @@ define(
telemetryObjects = [], telemetryObjects = [],
pool = lossless ? new TelemetryQueue() : new TelemetryTable(), pool = lossless ? new TelemetryQueue() : new TelemetryTable(),
metadatas, metadatas,
unlistenToMutation,
updatePending; updatePending;
// Look up domain objects which have telemetry capabilities. // Look up domain objects which have telemetry capabilities.
@ -146,23 +147,59 @@ define(
telemetryObjects = objects; telemetryObjects = objects;
metadatas = objects.map(lookupMetadata); metadatas = objects.map(lookupMetadata);
// Fire callback, as this will be the first time that // Fire callback, as this will be the first time that
// telemetry objects are available // telemetry objects are available, or these objects
// will have changed.
if (callback) { if (callback) {
callback(); callback();
} }
return objects; return objects;
} }
// Get a reference to relevant objects (those with telemetry function unsubscribeAll() {
// capabilities) and subscribe to their telemetry updates. return unsubscribePromise.then(function (unsubscribes) {
// Keep a reference to their promised return values, as these return $q.all(unsubscribes.map(function (unsubscribe) {
// will be unsubscribe functions. (This must be a promise return unsubscribe();
// because delegation is supported, and retrieving delegate }));
// telemetry-capable objects may be an asynchronous operation.) });
telemetryObjectPromise = promiseRelevantObjects(domainObject); }
unsubscribePromise = telemetryObjectPromise
.then(cacheObjectReferences) function initialize() {
.then(subscribeAll); // Get a reference to relevant objects (those with telemetry
// capabilities) and subscribe to their telemetry updates.
// Keep a reference to their promised return values, as these
// will be unsubscribe functions. (This must be a promise
// because delegation is supported, and retrieving delegate
// telemetry-capable objects may be an asynchronous operation.)
telemetryObjectPromise = promiseRelevantObjects(domainObject);
unsubscribePromise = telemetryObjectPromise
.then(cacheObjectReferences)
.then(subscribeAll);
}
function idsMatch(ids) {
return ids.length === telemetryObjects.length &&
ids.every(function (id, index) {
return telemetryObjects[index].getId() === id;
});
}
function modelChange(model) {
if (!idsMatch((model || {}).composition || [])) {
// Reinitialize if composition has changed
unsubscribeAll().then(initialize);
}
}
function addMutationListener() {
var mutation = domainObject &&
domainObject.getCapability('mutation');
if (mutation) {
return mutation.listen(modelChange);
}
}
initialize();
unlistenToMutation = addMutationListener();
return { return {
/** /**
@ -172,11 +209,10 @@ define(
* @memberof TelemetrySubscription * @memberof TelemetrySubscription
*/ */
unsubscribe: function () { unsubscribe: function () {
return unsubscribePromise.then(function (unsubscribes) { if (unlistenToMutation) {
return $q.all(unsubscribes.map(function (unsubscribe) { unlistenToMutation();
return unsubscribe(); }
})); return unsubscribeAll();
});
}, },
/** /**
* Get the most recent domain value that has been observed * Get the most recent domain value that has been observed

View File

@ -32,7 +32,9 @@ define(
mockDomainObject, mockDomainObject,
mockCallback, mockCallback,
mockTelemetry, mockTelemetry,
mockMutation,
mockUnsubscribe, mockUnsubscribe,
mockUnlisten,
mockSeries, mockSeries,
testMetadata, testMetadata,
subscription; subscription;
@ -59,7 +61,12 @@ define(
"telemetry", "telemetry",
["subscribe", "getMetadata"] ["subscribe", "getMetadata"]
); );
mockMutation = jasmine.createSpyObj(
"mutation",
["mutate", "listen"]
);
mockUnsubscribe = jasmine.createSpy("unsubscribe"); mockUnsubscribe = jasmine.createSpy("unsubscribe");
mockUnlisten = jasmine.createSpy("unlisten");
mockSeries = jasmine.createSpyObj( mockSeries = jasmine.createSpyObj(
"series", "series",
[ "getPointCount", "getDomainValue", "getRangeValue" ] [ "getPointCount", "getDomainValue", "getRangeValue" ]
@ -68,12 +75,19 @@ define(
mockQ.when.andCallFake(mockPromise); mockQ.when.andCallFake(mockPromise);
mockDomainObject.hasCapability.andReturn(true); mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockTelemetry); mockDomainObject.getCapability.andCallFake(function (c) {
return {
telemetry: mockTelemetry,
mutation: mockMutation
}[c];
});
mockDomainObject.getId.andReturn('test-id'); mockDomainObject.getId.andReturn('test-id');
mockTelemetry.subscribe.andReturn(mockUnsubscribe); mockTelemetry.subscribe.andReturn(mockUnsubscribe);
mockTelemetry.getMetadata.andReturn(testMetadata); mockTelemetry.getMetadata.andReturn(testMetadata);
mockMutation.listen.andReturn(mockUnlisten);
mockSeries.getPointCount.andReturn(42); mockSeries.getPointCount.andReturn(42);
mockSeries.getDomainValue.andReturn(123456); mockSeries.getDomainValue.andReturn(123456);
mockSeries.getRangeValue.andReturn(789); mockSeries.getRangeValue.andReturn(789);
@ -213,6 +227,22 @@ define(
expect(mockCallback2) expect(mockCallback2)
.toHaveBeenCalledWith([ mockDomainObject ]); .toHaveBeenCalledWith([ mockDomainObject ]);
}); });
it("reinitializes on mutation", function () {
expect(mockTelemetry.subscribe.calls.length).toEqual(1);
// Notify of a mutation which appears to change composition
mockMutation.listen.mostRecentCall.args[0]({
composition: ['Z']
});
// Use subscribe call as an indication of reinitialization
expect(mockTelemetry.subscribe.calls.length).toEqual(2);
});
it("stops listening for mutation on unsubscribe", function () {
expect(mockUnlisten).not.toHaveBeenCalled();
subscription.unsubscribe();
expect(mockUnlisten).toHaveBeenCalled();
});
}); });
} }
); );