Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 09c39d2

Browse files
feat($anchorScroll): support a configurable vertical scroll offset
Add support for a configurable vertical scroll offset to `$anchorScroll`. The offset can be defined by a specific number of pixels, a callback function that returns the number of pixels on demand or a jqLite/JQuery wrapped DOM element whose height and position are used if it has fixed position. The offset algorithm takes into account items that are near the bottom of the page preventing over-zealous offset correction. Closes #9368 Closes #2070 Closes #9360
1 parent 0dd316e commit 09c39d2

File tree

2 files changed

+661
-163
lines changed

2 files changed

+661
-163
lines changed

src/ng/anchorScroll.js

+211-53
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,174 @@
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 distance from
61+
* the top of the page to the element's bottom 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;
171+
var scrollScheduled = false;
64172

65173
// Helper function to get first anchor from a NodeList
66174
// (using `Array#some()` instead of `angular#forEach()` since it's more performant
@@ -76,20 +184,69 @@ function $AnchorScrollProvider() {
76184
return result;
77185
}
78186

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

82239
// empty hash, scroll to the top of the page
83-
if (!hash) $window.scrollTo(0, 0);
240+
if (!hash) scrollTo(null);
84241

85242
// element with given id
86-
else if ((elm = document.getElementById(hash))) elm.scrollIntoView();
243+
else if ((elm = document.getElementById(hash))) scrollTo(elm);
87244

88245
// first anchor with given name :-D
89-
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView();
246+
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
90247

91248
// no element and hash == 'top', scroll to the top of the page
92-
else if (hash === 'top') $window.scrollTo(0, 0);
249+
else if (hash === 'top') scrollTo(null);
93250
}
94251

95252
// does not scroll when user clicks on anchor link that is currently on
@@ -100,11 +257,12 @@ function $AnchorScrollProvider() {
100257
// skip the initial scroll if $location.hash is empty
101258
if (newVal === oldVal && newVal === '') return;
102259

103-
$rootScope.$evalAsync(scroll);
260+
jqLiteDocumentLoaded(function() {
261+
$rootScope.$evalAsync(scroll);
262+
});
104263
});
105264
}
106265

107266
return scroll;
108267
}];
109268
}
110-

0 commit comments

Comments
 (0)