From 6ada9d8ef8741ea980e6633e2c46cbe6592bd725 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 1 Oct 2014 14:26:46 +0100 Subject: [PATCH 1/2] chore(docs): add directive to move pad heading anchors Closes #2070 Closes #9360 --- docs/app/src/.jshintrc | 6 ++ docs/app/src/app.js | 8 ++- docs/app/src/heading-offset.js | 87 +++++++++++++++++++++++++++++ docs/app/test/.jshintrc | 11 ++++ docs/app/test/heading-offsetSpec.js | 30 ++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 docs/app/src/.jshintrc create mode 100644 docs/app/src/heading-offset.js create mode 100644 docs/app/test/.jshintrc create mode 100644 docs/app/test/heading-offsetSpec.js diff --git a/docs/app/src/.jshintrc b/docs/app/src/.jshintrc new file mode 100644 index 000000000000..b31f0bb90ef1 --- /dev/null +++ b/docs/app/src/.jshintrc @@ -0,0 +1,6 @@ +{ + "browser": true, + "globals": { + "angular": false + } +} diff --git a/docs/app/src/app.js b/docs/app/src/app.js index af94d44a2b8d..71b4060a7494 100644 --- a/docs/app/src/app.js +++ b/docs/app/src/app.js @@ -14,10 +14,16 @@ angular.module('docsApp', [ 'tutorials', 'versions', 'bootstrap', - 'ui.bootstrap.dropdown' + 'ui.bootstrap.dropdown', + 'heading-offset' ]) .config(['$locationProvider', function($locationProvider) { $locationProvider.html5Mode(true).hashPrefix('!'); +}]) + +.run(['headingOffset', function(headingOffset) { + // Provide the initial offset for heading anchors + headingOffset.value = '120px'; }]); diff --git a/docs/app/src/heading-offset.js b/docs/app/src/heading-offset.js new file mode 100644 index 000000000000..e47d01fde33d --- /dev/null +++ b/docs/app/src/heading-offset.js @@ -0,0 +1,87 @@ +angular.module('heading-offset', []) + +/** + * @ngdoc service + * @description + * The offset to use for heading anchors if not specific offset is given using ngOffSet. + * You can set this in a `run` block or update it dynamically based on changing sizes of + * static header. + */ +.value('headingOffset', {value: '0px' }) + +/** + * @ngdoc directive + * @description + * A directive that matches id attributes on headings (h1, h2, etc) and anchors (a). + * When matched this directive injects a new element that acts as a buffer to ensure that + * when we navigate to the element by id (using a hash on the url) the element appears far + * enough down the page + * + * You can specify the offset on an element by element basis using the `ng-offset` attribute. + * The attribute can be a static value: + * + * ```html + *

Some Heading

+ * ``` + * + * or it can be interpolated: + * + * ```html + *

Some Heading

+ * ``` + * + * If no value is given for `ng-offset` then the directive will use the value given by the + * `headingOffset` service. You can set the value of this at runtime: + * + * ```html + *

Some Heading

+ * ``` + * + * ```js + * appModule.run(['headingOffset', function(headingOffset) { + * // Provide the initial offset for heading anchors + * headingOffset.value = '120px'; + * }]); + * ``` + * + * Be aware that this moves the id to a span below the original element which can play havoc with you + * CSS if you are relying on ids in your styles (which you shouldn't). + * + */ +.directive('id', ['headingOffset', '$compile', '$anchorScroll', function(headingOffset, $compile, $anchorScroll) { + + return { + restrict: 'A', + compile: function(element, attrs) { + if ( /^(h\d+)|(a)$/i.test(element[0].nodeName || attrs.ngOffset ) ) { + + console.log('processing anchor for', element[0].nodeName, attrs.id); + + return function postLink(scope) { + + // Create an anchor for this heading + var anchor = $compile('')(scope); + + // Move the id from the original heading element to the span + anchor.attr('id', attrs.id); + element.removeAttr('id'); + + // Insert this anchor as the first child of the heading + element.prepend(anchor); + + var updateStyle = function(offset) { + anchor.css('margin-top', '-'+offset); + anchor.css('height', offset); + }; + + // Work out whether we are using a specific offset or getting the global default + if ( attrs.ngOffset ) { + scope.$observe('ngOffset', updateStyle); + } else { + scope.$watch(function() { return headingOffset.value; }, updateStyle); + } + }; + } + } + }; +}]); \ No newline at end of file diff --git a/docs/app/test/.jshintrc b/docs/app/test/.jshintrc new file mode 100644 index 000000000000..62cdd7f6a764 --- /dev/null +++ b/docs/app/test/.jshintrc @@ -0,0 +1,11 @@ +{ + "browser": true, + "globals": { + "angular": false, + "module": false, + "inject": false, + "describe": false, + "it": false, + "beforeEach": false + } +} diff --git a/docs/app/test/heading-offsetSpec.js b/docs/app/test/heading-offsetSpec.js new file mode 100644 index 000000000000..cd344adf5792 --- /dev/null +++ b/docs/app/test/heading-offsetSpec.js @@ -0,0 +1,30 @@ +describe("heading-offset", function() { + describe("id directive", function() { + + var $compile, $scope; + + beforeEach(module('heading-offset')); + + beforeEach(inject(function($rootScope, _$compile_) { + $scope = $rootScope; + $compile = _$compile_; + })); + + it("should inject a span into headings with ids", function() { + var element = $compile('

')($scope); + var span = element.children(); + expect(span[0].nodeName).toMatch(/span/i); + expect(element.attr('id')).toBeUndefined(); + expect(span.attr('id')).toBe('some-id'); + }); + + it("should inject a span into anchors with ids", function() { + var element = $compile('')($scope); + var span = element.children(); + expect(span[0].nodeName).toMatch(/span/i); + expect(element.attr('id')).toBeUndefined(); + expect(span.attr('id')).toBe('some-id'); + }); + + }); +}); \ No newline at end of file From 9f63c3bf5f475fe73f0799cb69b05cc519660563 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 1 Oct 2014 18:32:01 +0100 Subject: [PATCH 2/2] chore(docs): add directive to move pad heading anchors Closes #2070 Closes #9360 --- docs/app/src/heading-offset.js | 87 +++++++++++++++++++---------- docs/app/test/heading-offsetSpec.js | 82 +++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/docs/app/src/heading-offset.js b/docs/app/src/heading-offset.js index e47d01fde33d..48bf86d78660 100644 --- a/docs/app/src/heading-offset.js +++ b/docs/app/src/heading-offset.js @@ -1,9 +1,43 @@ +(function() { + + +function createPostLinkFn(element, attrs, headingOffset, $compile) { + + return function postLink(scope) { + + // Create an anchor for this heading + var anchor = $compile('
')(scope); + + // Move the id from the original heading element to the div + anchor.attr('id', attrs.id); + element.removeAttr('id'); + + // Insert this anchor as the first child of the heading + element.prepend(anchor); + + var updateStyle = function(offset) { + offset = offset || headingOffset.value; + anchor.css('margin-top', '-'+offset); + anchor.css('height', offset); + }; + + // Work out whether we are using a specific offset or getting the global default + if ( angular.isDefined(element.attr(attrs.$attr.ngOffset)) ) { + attrs.$observe('ngOffset', updateStyle); + } else { + scope.$watch(function() { return headingOffset.value; }, updateStyle); + } + }; + +} + + angular.module('heading-offset', []) /** * @ngdoc service * @description - * The offset to use for heading anchors if not specific offset is given using ngOffSet. + * The offset to use for heading anchors if not specific offset is given using ngOffset. * You can set this in a `run` block or update it dynamically based on changing sizes of * static header. */ @@ -11,6 +45,7 @@ angular.module('heading-offset', []) /** * @ngdoc directive + * @name id / ngOffset * @description * A directive that matches id attributes on headings (h1, h2, etc) and anchors (a). * When matched this directive injects a new element that acts as a buffer to ensure that @@ -48,40 +83,32 @@ angular.module('heading-offset', []) * CSS if you are relying on ids in your styles (which you shouldn't). * */ -.directive('id', ['headingOffset', '$compile', '$anchorScroll', function(headingOffset, $compile, $anchorScroll) { +.directive('id', ['headingOffset', '$compile', function(headingOffset, $compile) { + + var ELEMENTS_TO_MATCH = /^(h\d+|a)$/i; return { restrict: 'A', compile: function(element, attrs) { - if ( /^(h\d+)|(a)$/i.test(element[0].nodeName || attrs.ngOffset ) ) { - console.log('processing anchor for', element[0].nodeName, attrs.id); - - return function postLink(scope) { - - // Create an anchor for this heading - var anchor = $compile('')(scope); - - // Move the id from the original heading element to the span - anchor.attr('id', attrs.id); - element.removeAttr('id'); - - // Insert this anchor as the first child of the heading - element.prepend(anchor); - - var updateStyle = function(offset) { - anchor.css('margin-top', '-'+offset); - anchor.css('height', offset); - }; - - // Work out whether we are using a specific offset or getting the global default - if ( attrs.ngOffset ) { - scope.$observe('ngOffset', updateStyle); - } else { - scope.$watch(function() { return headingOffset.value; }, updateStyle); - } - }; + // This directive only looks for headings and anchors with id attributes + // It doesn't handle the case where ngOffset is defined as that is handled by the other + // directive below + if ( ELEMENTS_TO_MATCH.test(element[0].nodeName) && angular.isUndefined(attrs.ngOffset)) { + return createPostLinkFn(element, attrs, headingOffset, $compile); } } }; -}]); \ No newline at end of file +}]) + +.directive('ngOffset', ['headingOffset', '$compile', function(headingOffset, $compile) { + return { + restrict: 'A', + compile: function(element, attrs) { + // This directive handles the case where ngOffset is defined as an attribute + return createPostLinkFn(element, attrs, headingOffset, $compile); + } + }; +}]); + +})(); \ No newline at end of file diff --git a/docs/app/test/heading-offsetSpec.js b/docs/app/test/heading-offsetSpec.js index cd344adf5792..4d2b2c186fc5 100644 --- a/docs/app/test/heading-offsetSpec.js +++ b/docs/app/test/heading-offsetSpec.js @@ -1,29 +1,87 @@ describe("heading-offset", function() { describe("id directive", function() { - var $compile, $scope; + function check(element, child, offset, id) { + expect(child[0].nodeName).toMatch(/div/i); + expect(element.attr('id')).toBeUndefined(); + expect(child.attr('id')).toEqual(id); + expect(child.css('margin-top')).toEqual('-'+offset); + expect(child.css('height')).toEqual(offset); + } + + var $compile, $scope, headingOffset; beforeEach(module('heading-offset')); - beforeEach(inject(function($rootScope, _$compile_) { + beforeEach(inject(function($rootScope, _$compile_, _headingOffset_) { $scope = $rootScope; $compile = _$compile_; + headingOffset = _headingOffset_; + headingOffset.value = '40px'; })); - it("should inject a span into headings with ids", function() { + it("should inject a child into headings with ids, while watching the headerOffset service value", function() { var element = $compile('

')($scope); - var span = element.children(); - expect(span[0].nodeName).toMatch(/span/i); - expect(element.attr('id')).toBeUndefined(); - expect(span.attr('id')).toBe('some-id'); + var child = element.children(); + $scope.$digest(); + + check(element, child, '40px', 'some-id'); + + headingOffset.value = '100px'; + $scope.$digest(); + + check(element, child, '100px', 'some-id'); }); - it("should inject a span into anchors with ids", function() { + it("should inject a child into anchors with ids, while watching the headerOffset service value", function() { var element = $compile('')($scope); - var span = element.children(); - expect(span[0].nodeName).toMatch(/span/i); - expect(element.attr('id')).toBeUndefined(); - expect(span.attr('id')).toBe('some-id'); + var child = element.children(); + $scope.$digest(); + + check(element, child, '40px', 'some-id'); + + headingOffset.value = '100px'; + $scope.$digest(); + + check(element, child, '100px', 'some-id'); + }); + + + it("should inject a child into heading elements, while observing the ngOffset attribute", function() { + var element = $compile('

')($scope); + var child = element.children(); + $scope.$digest(); + + check(element, child, '40px', 'some-id'); + + $scope.x = '50px'; + $scope.$digest(); + + check(element, child, '50px', 'some-id'); + + $scope.x = null; + $scope.$digest(); + + check(element, child, '40px', 'some-id'); + }); + + it("should inject a child into non-heading elements, while observing the ngOffset attribute", function() { + var element = $compile('
  • ')($scope); + var child = element.children(); + + $scope.$digest(); + + check(element, child, '40px', 'some-id'); + + $scope.x = '50px'; + $scope.$digest(); + + check(element, child, '50px', 'some-id'); + + $scope.x = null; + $scope.$digest(); + + check(element, child, '40px', 'some-id'); }); });