diff --git a/platform/commonUI/general/README.md b/platform/commonUI/general/README.md new file mode 100644 index 0000000000..817d99d4e9 --- /dev/null +++ b/platform/commonUI/general/README.md @@ -0,0 +1,8 @@ +# Directives + +* `mct-scroll-x` is an attribute whose value is an assignable + Angular expression. This two-way binds that expression to the + horizontal scroll state of the element on which it is applied. +* `mct-scroll-y` is an attribute whose value is an assignable + Angular expression. This two-way binds that expression to the + vertical scroll state of the element on which it is applied. \ No newline at end of file diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json index f7b60b096d..bd94eaabdf 100644 --- a/platform/commonUI/general/bundle.json +++ b/platform/commonUI/general/bundle.json @@ -105,6 +105,34 @@ "key": "mctResize", "implementation": "directives/MCTResize.js", "depends": [ "$timeout" ] + }, + { + "key": "mctScrollX", + "implementation": "directives/MCTScroll.js", + "depends": [ "$parse", "MCT_SCROLL_X_PROPERTY", "MCT_SCROLL_X_ATTRIBUTE" ] + }, + { + "key": "mctScrollY", + "implementation": "directives/MCTScroll.js", + "depends": [ "$parse", "MCT_SCROLL_Y_PROPERTY", "MCT_SCROLL_Y_ATTRIBUTE" ] + } + ], + "constants": [ + { + "key": "MCT_SCROLL_X_PROPERTY", + "value": "scrollLeft" + }, + { + "key": "MCT_SCROLL_X_ATTRIBUTE", + "value": "mctScrollX" + }, + { + "key": "MCT_SCROLL_Y_PROPERTY", + "value": "scrollTop" + }, + { + "key": "MCT_SCROLL_Y_ATTRIBUTE", + "value": "mctScrollY" } ], "containers": [ diff --git a/platform/commonUI/general/src/directives/MCTScroll.js b/platform/commonUI/general/src/directives/MCTScroll.js new file mode 100644 index 0000000000..c0f0dbfcc5 --- /dev/null +++ b/platform/commonUI/general/src/directives/MCTScroll.js @@ -0,0 +1,62 @@ +/*global define*/ + +define( + [], + function () { + 'use strict'; + + /** + * Implements `mct-scroll-x` and `mct-scroll-y` directives. Listens + * for scroll events and publishes their results into scope; watches + * scope and updates scroll state to match. This varies for x- and y- + * directives only by the attribute name chosen to find the expression, + * and the property (scrollLeft or scrollTop) managed within the + * element. + * + * This is exposed as two directives in `bundle.json`; the difference + * is handled purely by parameterization. + * + * @constructor + * @param $parse Angular's $parse + * @param {string} property property to manage within the HTML element + * @param {string} attribute attribute to look at for the assignable + * Angular expression + */ + function MCTScroll($parse, property, attribute) { + function link(scope, element, attrs) { + var expr = attrs[attribute], + parsed = $parse(expr); + + // Set the element's scroll to match the scope's state + function updateElement(value) { + element[0][property] = value; + } + + // Handle event; assign to scroll state to scope + function updateScope() { + parsed.assign(scope, element[0][property]); + scope.$apply(expr); + } + + // Initialize state in scope + parsed.assign(scope, element[0][property]); + + // Update element state when value in scope changes + scope.$watch(expr, updateElement); + + // Update state in scope when element is scrolled + element.on('scroll', updateScope); + } + + return { + // Restrict to attributes + restrict: "A", + // Use this link function + link: link + }; + } + + return MCTScroll; + + } +); \ No newline at end of file diff --git a/platform/commonUI/general/test/directives/MCTScrollSpec.js b/platform/commonUI/general/test/directives/MCTScrollSpec.js new file mode 100644 index 0000000000..bf55d90aca --- /dev/null +++ b/platform/commonUI/general/test/directives/MCTScrollSpec.js @@ -0,0 +1,96 @@ +/*global define,describe,it,expect,jasmine,beforeEach*/ +define( + ['../../src/directives/MCTScroll'], + function (MCTScroll) { + "use strict"; + + var EVENT_PROPERTY = "testProperty", + ATTRIBUTE = "testAttribute", + EXPRESSION = "some.expression"; + + + // MCTScroll is the commonality between mct-scroll-x and + // mct-scroll-y; it gets the event property to watch and + // the attribute which contains the associated assignable + // expression. + describe("An mct-scroll-* directive", function () { + var mockParse, + mockParsed, + mockScope, + mockElement, + testAttrs, + mctScroll; + + beforeEach(function () { + mockParse = jasmine.createSpy('$parse'); + mockParsed = jasmine.createSpy('parsed'); + mockParsed.assign = jasmine.createSpy('assign'); + + mockScope = jasmine.createSpyObj('$scope', ['$watch', '$apply']); + mockElement = [{ testProperty: 42 }]; + mockElement.on = jasmine.createSpy('on'); + + mockParse.andReturn(mockParsed); + + testAttrs = {}; + testAttrs[ATTRIBUTE] = EXPRESSION; + + mctScroll = new MCTScroll( + mockParse, + EVENT_PROPERTY, + ATTRIBUTE + ); + mctScroll.link(mockScope, mockElement, testAttrs); + }); + + it("is available for attributes", function () { + expect(mctScroll.restrict).toEqual('A'); + }); + + it("does not create an isolate scope", function () { + expect(mctScroll.scope).toBeUndefined(); + }); + + it("watches for changes in observed expression", function () { + expect(mockScope.$watch).toHaveBeenCalledWith( + EXPRESSION, + jasmine.any(Function) + ); + // Should have been only watch (other tests need this to be true) + expect(mockScope.$watch.calls.length).toEqual(1); + }); + + it("listens for scroll events", function () { + expect(mockElement.on).toHaveBeenCalledWith( + 'scroll', + jasmine.any(Function) + ); + // Should have been only listener (other tests need this to be true) + expect(mockElement.on.calls.length).toEqual(1); + }); + + it("publishes initial scroll state", function () { + expect(mockParse).toHaveBeenCalledWith(EXPRESSION); + expect(mockParsed.assign).toHaveBeenCalledWith(mockScope, 42); + }); + + it("updates scroll state when scope changes", function () { + mockScope.$watch.mostRecentCall.args[1](64); + expect(mockElement[0].testProperty).toEqual(64); + }); + + it("updates scope when scroll state changes", function () { + mockElement[0].testProperty = 12321; + mockElement.on.mostRecentCall.args[1]({ target: mockElement[0] }); + expect(mockParsed.assign).toHaveBeenCalledWith(mockScope, 12321); + expect(mockScope.$apply).toHaveBeenCalledWith(EXPRESSION); + }); + + // This would trigger an infinite digest exception + it("does not call $apply during construction", function () { + expect(mockScope.$apply).not.toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/commonUI/general/test/suite.json b/platform/commonUI/general/test/suite.json index ae3a54f37b..58d94a4d95 100644 --- a/platform/commonUI/general/test/suite.json +++ b/platform/commonUI/general/test/suite.json @@ -11,5 +11,6 @@ "directives/MCTContainer", "directives/MCTDrag", "directives/MCTResize", + "directives/MCTScroll", "StyleSheetLoader" ] \ No newline at end of file