Skip to content

Commit 353bd4d

Browse files
feat($anchorScroll): add support for configurable scroll offset
Add support for a configurable scroll offset to $anchorScrollProvider. Related to angular#9368 Closes angular#2070 Closes angular#9371
1 parent 613d0a3 commit 353bd4d

File tree

2 files changed

+452
-73
lines changed

2 files changed

+452
-73
lines changed

src/ng/anchorScroll.js

+217-58
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,253 @@
11
'use strict';
22

33
/**
4-
* @ngdoc service
5-
* @name $anchorScroll
6-
* @kind function
7-
* @requires $window
8-
* @requires $location
9-
* @requires $rootScope
4+
* @ngdoc provider
5+
* @name $anchorScrollProvider
106
*
117
* @description
12-
* When called, it checks current value of `$location.hash()` and scrolls to the related element,
13-
* according to rules specified in
14-
* [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
15-
*
16-
* It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor.
17-
* This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`.
18-
*
19-
* @example
20-
<example module="anchorScrollExample">
21-
<file name="index.html">
22-
<div id="scrollArea" ng-controller="ScrollController">
23-
<a ng-click="gotoBottom()">Go to bottom</a>
24-
<a id="bottom"></a> You're at the bottom!
25-
</div>
26-
</file>
27-
<file name="script.js">
28-
angular.module('anchorScrollExample', [])
29-
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
30-
function ($scope, $location, $anchorScroll) {
31-
$scope.gotoBottom = function() {
32-
// set the location.hash to the id of
33-
// the element you wish to scroll to.
34-
$location.hash('bottom');
35-
36-
// call $anchorScroll()
37-
$anchorScroll();
38-
};
39-
}]);
40-
</file>
41-
<file name="style.css">
42-
#scrollArea {
43-
height: 350px;
44-
overflow: auto;
45-
}
46-
47-
#bottom {
48-
display: block;
49-
margin-top: 2000px;
50-
}
51-
</file>
52-
</example>
8+
* Use `$anchorScrollProvider` to disable automatic scrolling whenever
9+
* {@link ng.$location#hash $location.hash()} changes.
5310
*/
5411
function $AnchorScrollProvider() {
5512

5613
var autoScrollingEnabled = true;
5714

15+
/**
16+
* @ngdoc method
17+
* @name $anchorScrollProvider#disableAutoScrolling
18+
*
19+
* @description
20+
* By default, {@link ng.$anchorScroll $anchorScroll()} will automatically will detect changes to
21+
* {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.<br />
22+
* Use this method to disable automatic scrolling.
23+
*
24+
* If automatic scrolling is disabled, one must explicitly call
25+
* {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the
26+
* current hash.
27+
*/
5828
this.disableAutoScrolling = function() {
5929
autoScrollingEnabled = false;
6030
};
6131

32+
/**
33+
* @ngdoc service
34+
* @name $anchorScroll
35+
* @kind function
36+
* @requires $window
37+
* @requires $location
38+
* @requires $rootScope
39+
*
40+
* @description
41+
* When called, it checks the current value of {@link ng.$location#hash $location.hash()} and
42+
* scrolls to the related element, according to the rules specified in the
43+
* [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
44+
*
45+
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
46+
* match any anchor whenever it changes. This can be disabled by calling
47+
* {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}.
48+
*
49+
* Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a
50+
* vertical scroll-offset (either fixed or dynamic).
51+
*
52+
* @property {(number|function|jqLite)} yOffset
53+
* If set, specifies a vertical scroll-offset. This is often useful when there are fixed
54+
* positioned elements at the top of the page, such as navbars, headers etc.
55+
*
56+
* `yOffset` can be specified in various ways:
57+
* - **number**: A fixed number of pixels to be used as offset.<br /><br />
58+
* - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return
59+
* a number representing the offset (in pixels).<br /><br />
60+
* - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The sum of the
61+
* element's height and its distance from the top of the page will be used as offset.<br />
62+
* **Note**: The element will be taken into account only as long as its `position` is set to
63+
* `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust
64+
* their height and/or positioning according to the viewport's size.
65+
*
66+
* <br />
67+
* <div class="alert alert-warning">
68+
* In order for `yOffset` to work properly, scrolling should take place on the document's root and
69+
* not some child element.
70+
* </div>
71+
*
72+
* @example
73+
<example module="anchorScrollExample">
74+
<file name="index.html">
75+
<div id="scrollArea" ng-controller="ScrollController">
76+
<a ng-click="gotoBottom()">Go to bottom</a>
77+
<a id="bottom"></a> You're at the bottom!
78+
</div>
79+
</file>
80+
<file name="script.js">
81+
angular.module('anchorScrollExample', [])
82+
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
83+
function ($scope, $location, $anchorScroll) {
84+
$scope.gotoBottom = function() {
85+
// set the location.hash to the id of
86+
// the element you wish to scroll to.
87+
$location.hash('bottom');
88+
89+
// call $anchorScroll()
90+
$anchorScroll();
91+
};
92+
}]);
93+
</file>
94+
<file name="style.css">
95+
#scrollArea {
96+
height: 280px;
97+
overflow: auto;
98+
}
99+
100+
#bottom {
101+
display: block;
102+
margin-top: 2000px;
103+
}
104+
</file>
105+
</example>
106+
*
107+
* <hr />
108+
* The example below illustrates the use of a vertical scroll-offset (specified as a fixed value).
109+
* See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details.
110+
*
111+
* @example
112+
<example module="anchorScrollOffsetExample">
113+
<file name="index.html">
114+
<div class="fixed-header" ng-controller="headerCtrl">
115+
<a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
116+
Go to anchor {{x}}
117+
</a>
118+
</div>
119+
<div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]">
120+
Anchor {{x}} of 5
121+
</div>
122+
</file>
123+
<file name="script.js">
124+
angular.module('anchorScrollOffsetExample', [])
125+
.run(['$anchorScroll', function($anchorScroll) {
126+
$anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
127+
}])
128+
.controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
129+
function ($anchorScroll, $location, $scope) {
130+
$scope.gotoAnchor = function(x) {
131+
var newHash = 'anchor' + x;
132+
if ($location.hash() !== newHash) {
133+
// set the $location.hash to `newHash` and
134+
// $anchorScroll will automatically scroll to it
135+
$location.hash('anchor' + x);
136+
} else {
137+
// call $anchorScroll() explicitly,
138+
// since $location.hash hasn't changed
139+
$anchorScroll();
140+
}
141+
};
142+
}
143+
]);
144+
</file>
145+
<file name="style.css">
146+
body {
147+
padding-top: 50px;
148+
}
149+
150+
.anchor {
151+
border: 2px dashed DarkOrchid;
152+
padding: 10px 10px 200px 10px;
153+
}
154+
155+
.fixed-header {
156+
background-color: rgba(0, 0, 0, 0.2);
157+
height: 50px;
158+
position: fixed;
159+
top: 0; left: 0; right: 0;
160+
}
161+
162+
.fixed-header > a {
163+
display: inline-block;
164+
margin: 5px 15px;
165+
}
166+
</file>
167+
</example>
168+
*/
62169
this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) {
63170
var document = $window.document;
64171

65-
// helper function to get first anchor from a NodeList
66-
// can't use filter.filter, as it accepts only instances of Array
67-
// and IE can't convert NodeList to an array using [].slice
68-
// TODO(vojta): use filter if we change it to accept lists as well
172+
// Helper function to get first anchor from a NodeList
173+
// (using `Array#some()` instead of `angular#forEach()` since it's more performant
174+
// and working in all supported browsers.)
69175
function getFirstAnchor(list) {
70176
var result = null;
71-
forEach(list, function(element) {
72-
if (!result && nodeName_(element) === 'a') result = element;
177+
Array.prototype.some.call(list, function(element) {
178+
if (nodeName_(element) === 'a') {
179+
result = element;
180+
return true;
181+
}
73182
});
74183
return result;
75184
}
76185

186+
function getYOffset() {
187+
188+
var offset = scroll.yOffset;
189+
190+
if (isFunction(offset)) {
191+
offset = offset();
192+
} else if (isElement(offset)) {
193+
var elem = offset[0];
194+
var style = $window.getComputedStyle(elem);
195+
if (style.position !== 'fixed') {
196+
offset = 0;
197+
} else {
198+
var rect = elem.getBoundingClientRect();
199+
var top = rect.top;
200+
var height = rect.height;
201+
offset = top + height;
202+
}
203+
} else if (!isNumber(offset)) {
204+
offset = 0;
205+
}
206+
207+
return offset;
208+
}
209+
210+
function scrollTo(elem) {
211+
if (elem) {
212+
elem.scrollIntoView();
213+
214+
var offset = getYOffset();
215+
216+
if (offset) {
217+
// `offset` is the number of pixels we should scroll up in order to align `elem` properly.
218+
// This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the
219+
// top of the viewport. IF the number of pixels from the top of `elem` to the end of page's
220+
// content is less than the height of the viewport, then `elem.scrollIntoView()` will NOT
221+
// align the top of `elem` at the top of the viewport (but further down). This is often the
222+
// case for elements near the bottom of the page.
223+
// In such cases we do not need to scroll the whole `offset` up, just the fraction of the
224+
// offset that is necessary to align the top of `elem` at the desired position.
225+
var body = document.body;
226+
var bodyRect = body.getBoundingClientRect();
227+
var elemRect = elem.getBoundingClientRect();
228+
var necessaryOffset = offset - (elemRect.top - (bodyRect.top + body.scrollTop));
229+
230+
$window.scrollBy(0, -1 * necessaryOffset);
231+
}
232+
} else {
233+
$window.scrollTo(0, 0);
234+
}
235+
}
236+
77237
function scroll() {
78238
var hash = $location.hash(), elm;
79239

80240
// empty hash, scroll to the top of the page
81-
if (!hash) $window.scrollTo(0, 0);
241+
if (!hash) scrollTo(null);
82242

83243
// element with given id
84-
else if ((elm = document.getElementById(hash))) elm.scrollIntoView();
244+
else if ((elm = document.getElementById(hash))) scrollTo(elm);
85245

86246
// first anchor with given name :-D
87-
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView();
247+
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
88248

89249
// no element and hash == 'top', scroll to the top of the page
90-
else if (hash === 'top') $window.scrollTo(0, 0);
250+
else if (hash === 'top') scrollTo(null);
91251
}
92252

93253
// does not scroll when user clicks on anchor link that is currently on
@@ -102,4 +262,3 @@ function $AnchorScrollProvider() {
102262
return scroll;
103263
}];
104264
}
105-

0 commit comments

Comments
 (0)