diff --git a/platform/representation/bundle.json b/platform/representation/bundle.json index 00a4e5ee41..85939333f1 100644 --- a/platform/representation/bundle.json +++ b/platform/representation/bundle.json @@ -28,6 +28,13 @@ "implementation": "gestures/ContextMenuGesture.js", "depends": [ "$compile", "$document", "$window", "$rootScope" ] } + ], + "components": [ + { + "provides": "gestureService", + "type": "provider", + "implementation": "gestures/GestureProvider.js" + } ] } } \ No newline at end of file diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js index 1ce2f61970..dde6add94c 100644 --- a/platform/representation/src/MCTRepresentation.js +++ b/platform/representation/src/MCTRepresentation.js @@ -12,7 +12,7 @@ define( * * @constructor */ - function MCTRepresentation(representations, views, gestures, $q, $log) { + function MCTRepresentation(representations, views, gestureService, $q, $log) { var pathMap = {}, representationMap = {}, gestureMap = {}; @@ -32,37 +32,9 @@ define( representationMap[representation.key] = representation; }); - // Assemble all gestures into a map, similarly - gestures.forEach(function (gesture) { - gestureMap[gesture.key] = gesture; - }); - - function findRepresentation(key, domainObject) { - return representationMap[key]; - } - - function createGestures(element, domainObject, gestureKeys) { - return gestureKeys.map(function (key) { - return gestureMap[key]; - }).filter(function (Gesture) { - return Gesture !== undefined && (Gesture.appliesTo ? - Gesture.appliesTo(domainObject) : - true); - }).map(function (Gesture) { - return new Gesture(element, domainObject); - }); - } - - function releaseGestures(gestures) { - gestures.forEach(function (gesture) { - if (gesture && gesture.destroy) { - gesture.destroy(); - } - }); - } function link($scope, element) { - var linkedGestures = []; + var gestureHandle; function refresh() { var representation = representationMap[$scope.key], @@ -74,7 +46,9 @@ define( $scope.inclusion = pathMap[$scope.key]; // Any existing gestures are no longer valid; release them. - releaseGestures(linkedGestures); + if (gestureHandle) { + gestureHandle.destroy(); + } if (!representation && $scope.key) { $log.warn("No representation found for " + $scope.key); @@ -96,7 +70,7 @@ define( }); }); - linkedGestures = createGestures( + gestureHandle = gestureService.attachGestures( element, domainObject, gestureKeys diff --git a/platform/representation/src/gestures/GestureProvider.js b/platform/representation/src/gestures/GestureProvider.js new file mode 100644 index 0000000000..1bf7307f4f --- /dev/null +++ b/platform/representation/src/gestures/GestureProvider.js @@ -0,0 +1,57 @@ +/*global define,Promise*/ + +/** + * Module defining GestureProvider. Created by vwoeltje on 11/22/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function GestureProvider(gestures) { + var gestureMap = {}; + + function releaseGestures(gestures) { + gestures.forEach(function (gesture) { + if (gesture && gesture.destroy) { + gesture.destroy(); + } + }); + } + + function attachGestures(element, domainObject, gestureKeys) { + var attachedGestures = gestureKeys.map(function (key) { + return gestureMap[key]; + }).filter(function (Gesture) { + return Gesture !== undefined && (Gesture.appliesTo ? + Gesture.appliesTo(domainObject) : + true); + }).map(function (Gesture) { + return new Gesture(element, domainObject); + }); + + return { + destroy: function () { + releaseGestures(attachedGestures); + } + }; + } + + // Assemble all gestures into a map, for easy look up + gestures.forEach(function (gesture) { + gestureMap[gesture.key] = gesture; + }); + + + return { + attachGestures: attachGestures + }; + } + + return GestureProvider; + } +); \ No newline at end of file diff --git a/platform/representation/test/MCTRepresentationSpec.js b/platform/representation/test/MCTRepresentationSpec.js index 5e79c6deeb..51cadf6594 100644 --- a/platform/representation/test/MCTRepresentationSpec.js +++ b/platform/representation/test/MCTRepresentationSpec.js @@ -8,8 +8,175 @@ define( function (MCTRepresentation) { "use strict"; - describe("", function () { + var JQLITE_FUNCTIONS = [ "on", "off", "attr", "removeAttr" ], + LOG_FUNCTIONS = [ "error", "warn", "info", "debug"], + DOMAIN_OBJECT_METHODS = [ "getId", "getModel", "getCapability", "hasCapability", "useCapability"]; + describe("The mct-representation directive", function () { + var testRepresentations, + testViews, + mockGestureService, + mockGestureHandle, + mockQ, + mockLog, + mockScope, + mockElement, + mockDomainObject, + mctRepresentation; + + function mockPromise(value) { + return (value && value.then) ? value : { + then: function (callback) { + return mockPromise(callback(value)); + } + }; + } + + beforeEach(function () { + testRepresentations = [ + { + key: "abc", + bundle: { path: "a", resources: "b" }, + templateUrl: "c/template.html" + }, + { + key: "def", + bundle: { path: "d", resources: "e" }, + templateUrl: "f/template.html", + uses: [ "testCapability", "otherTestCapability" ] + } + ]; + + testViews = [ + { + key: "uvw", + bundle: { path: "u", resources: "v" }, + templateUrl: "w/template.html", + gestures: [ "testGesture", "otherTestGesture" ] + }, + { + key: "xyz", + bundle: { path: "x", resources: "y" }, + templateUrl: "z/template.html" + } + ]; + + mockGestureService = jasmine.createSpyObj("gestureService", [ "attachGestures" ]); + mockGestureHandle = jasmine.createSpyObj("gestureHandle", [ "destroy" ]); + + mockGestureService.attachGestures.andReturn(mockGestureHandle); + + mockQ = { when: mockPromise }; + mockLog = jasmine.createSpyObj("$log", LOG_FUNCTIONS); + + mockScope = jasmine.createSpyObj("scope", [ "$watch" ]); + mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS); + mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS); + + mctRepresentation = new MCTRepresentation( + testRepresentations, + testViews, + mockGestureService, + mockQ, + mockLog + ); + }); + + + it("has a built-in template, with ng-include src=inclusion", function () { + // Not rigorous, but should detect many cases when template is broken. + expect(mctRepresentation.template.indexOf("ng-include")).not.toEqual(-1); + expect(mctRepresentation.template.indexOf("inclusion")).not.toEqual(-1); + }); + + it("is restricted to elements", function () { + expect(mctRepresentation.restrict).toEqual("E"); + }); + + it("watches scope when linked", function () { + mctRepresentation.link(mockScope, mockElement); + expect(mockScope.$watch).toHaveBeenCalledWith("key", 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 () { + mctRepresentation.link(mockScope, mockElement); + + mockScope.key = "abc"; + + // Trigger the watch + mockScope.$watch.mostRecentCall.args[1](); + + expect(mockScope.inclusion).toEqual("a/b/c/template.html"); + }); + + it("recognizes keys for views", function () { + mctRepresentation.link(mockScope, mockElement); + + mockScope.key = "xyz"; + + // Trigger the watch + mockScope.$watch.mostRecentCall.args[1](); + + expect(mockScope.inclusion).toEqual("x/y/z/template.html"); + }); + + it("loads declared capabilities", function () { + mctRepresentation.link(mockScope, mockElement); + + mockScope.key = "def"; + mockScope.domainObject = mockDomainObject; + + // Trigger the watch + mockScope.$watch.mostRecentCall.args[1](); + + expect(mockDomainObject.useCapability) + .toHaveBeenCalledWith("testCapability"); + expect(mockDomainObject.useCapability) + .toHaveBeenCalledWith("otherTestCapability"); + }); + + it("attaches declared gestures, and detaches on refresh", function () { + mctRepresentation.link(mockScope, mockElement); + + mockScope.key = "uvw"; + mockScope.domainObject = mockDomainObject; + + // Trigger the watch + mockScope.$watch.mostRecentCall.args[1](); + + expect(mockGestureService.attachGestures).toHaveBeenCalledWith( + mockElement, + mockDomainObject, + [ "testGesture", "otherTestGesture" ] + ); + + expect(mockGestureHandle.destroy).not.toHaveBeenCalled(); + + // Refresh, expect a detach + mockScope.key = "abc"; + mockScope.$watch.mostRecentCall.args[1](); + + // Should have destroyed those old gestures + expect(mockGestureHandle.destroy).toHaveBeenCalled(); + }); + + + it("logs when no representation is available for a key", function () { + mctRepresentation.link(mockScope, mockElement); + + mockScope.key = "someUnknownThing"; + + // Verify precondition + expect(mockLog.warn).not.toHaveBeenCalled(); + + // Trigger the watch + mockScope.$watch.mostRecentCall.args[1](); + + // Should have gotten a warning - that's an unknown key + expect(mockLog.warn).toHaveBeenCalled(); + }); }); } ); \ No newline at end of file