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

Commit 92e5a05

Browse files
committed
WIP - feat($anchorScroll): add docs and tests and make small improvements
Finalize the implementation, fix scrolling for elements near the end of the page, make sure the values calculated for top-offset and height are accurate (and unaffected by box-sizing or offset-parents). Separately document $anchorScrollProvider, document the newly added `yOffset` property of $anchorScroll, fix minor issues in the examples and overall improve the docs. Add tests for `$anchorScroll.yOffset` and make some necessary modifications to the existing test helper functions.
1 parent d5bad75 commit 92e5a05

File tree

2 files changed

+658
-190
lines changed

2 files changed

+658
-190
lines changed

src/ng/anchorScroll.js

Lines changed: 210 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +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-
* Additionally, you can specify a scroll offset (in pixels) during the configuration phase by
20-
* calling `$anchorScrollProvider.setScrollOffset(<valueOrGetter>)`. The offset can be either a
21-
* fixed value or a getter function that returns a value dynamically.
22-
*
23-
* @example
24-
<example module="anchorScrollExample">
25-
<file name="index.html">
26-
<div id="scrollArea" ng-controller="ScrollController">
27-
<a ng-click="gotoBottom()">Go to bottom</a>
28-
<a id="bottom"></a> You're at the bottom!
29-
</div>
30-
</file>
31-
<file name="script.js">
32-
angular.module('anchorScrollExample', [])
33-
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
34-
function ($scope, $location, $anchorScroll) {
35-
$scope.gotoBottom = function() {
36-
// set the location.hash to the id of
37-
// the element you wish to scroll to.
38-
$location.hash('bottom');
39-
40-
// call $anchorScroll()
41-
$anchorScroll();
42-
};
43-
}]);
44-
</file>
45-
<file name="style.css">
46-
#scrollArea {
47-
height: 350px;
48-
overflow: auto;
49-
}
50-
51-
#bottom {
52-
display: block;
53-
margin-top: 2000px;
54-
}
55-
</file>
56-
</example>
57-
*
58-
* <hr />
59-
* The example below illustrates the use of scroll offset (specified as a fixed value).
60-
*
61-
* @example
62-
<example module="anchorScrollOffsetExample">
63-
<file name="index.html">
64-
<div class="fixed-header" ng-controller="headerCtrl">
65-
<a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
66-
Go to anchor {{x}}
67-
</a>
68-
</div>
69-
<div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]">
70-
Anchor {{x}} of 5
71-
</div>
72-
</file>
73-
<file name="script.js">
74-
angular.module('anchorScrollOffsetExample', [])
75-
.config(['$anchorScrollProvider', function($anchorScrollProvider) {
76-
$anchorScrollProvider.setScrollOffset(50); // always scroll by 50 extra pixels
77-
}])
78-
.controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
79-
function ($anchorScroll, $location, $scope) {
80-
$scope.gotoAnchor = function(x) {
81-
// Set the location.hash to the id of
82-
// the element you wish to scroll to.
83-
$location.hash('anchor' + x);
84-
85-
// Call $anchorScroll()
86-
$anchorScroll();
87-
};
88-
}
89-
]);
90-
</file>
91-
<file name="style.css">
92-
body {
93-
padding-top: 50px;
94-
}
95-
96-
.anchor {
97-
border: 2px dashed DarkOrchid;
98-
padding: 10px 10px 200px 10px;
99-
}
100-
101-
.fixed-header {
102-
background-color: rgba(0, 0, 0, 0.2);
103-
height: 50px;
104-
position: fixed;
105-
top: 0; left: 0; right: 0;
106-
}
107-
108-
.fixed-header > a {
109-
display: inline-block;
110-
margin: 5px 15px;
111-
}
112-
</file>
113-
</example>
8+
* Use `$anchorScrollProvider` to disable automatic scrolling whenever
9+
* {@link ng.$location#hash $location.hash()} changes.
11410
*/
11511
function $AnchorScrollProvider() {
116-
// TODO(gkalpak): The $anchorScrollProvider should be documented as well
117-
// (under the providers section).
11812

11913
var autoScrollingEnabled = true;
12014

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+
*/
12128
this.disableAutoScrolling = function() {
12229
autoScrollingEnabled = false;
12330
};
12431

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+
*/
125169
this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) {
126170
var document = $window.document;
171+
var scrollScheduled = false;
127172

128173
// Helper function to get first anchor from a NodeList
129174
// (using `Array#some()` instead of `angular#forEach()` since it's more performant
@@ -143,34 +188,72 @@ function $AnchorScrollProvider() {
143188

144189
var offset = scroll.yOffset;
145190

146-
if (isElement(offset)) {
147-
148-
var style = $window.getComputedStyle(scroll.yOffset[0]);
149-
var top = parseInt(style.top,10);
150-
var height = parseInt(style.height,10);
151-
return style.position === 'fixed' ? (top + height) : 0;
152-
153-
} else if (isFunction(offset)) {
154-
return offset();
155-
156-
} else if (isNumber(offset)) {
157-
return offset;
158-
159-
} else {
160-
return 0;
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+
var rect = elem.getBoundingClientRect();
200+
var top = rect.top;
201+
var height = rect.height;
202+
offset = top + height;
203+
}
204+
} else if (!isNumber(offset)) {
205+
offset = 0;
161206
}
162207

208+
return offset;
163209
}
164210

165211
function scrollTo(elem) {
166212
if (elem) {
167213
elem.scrollIntoView();
168-
$window.scrollBy(0, -1 * getYOffset());
214+
215+
var offset = getYOffset();
216+
217+
if (offset) {
218+
// `offset` is the number of pixels we should scroll up in order to align `elem` properly.
219+
// This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the
220+
// top of the viewport. IF the number of pixels from the top of `elem` to the end of the
221+
// page's content is less than the height of the viewport, then `elem.scrollIntoView()`
222+
// will NOT align the top of `elem` at the top of the viewport (but further down). This is
223+
// often the case for elements near the bottom of the page.
224+
// In such cases we do not need to scroll the whole `offset` up, just the fraction of the
225+
// offset that is necessary to align the top of `elem` at the desired position.
226+
var elemTop = elem.getBoundingClientRect().top;
227+
var bodyTop = document.body.getBoundingClientRect().top;
228+
var scrollTop = $window.pageYOffset;
229+
var necessaryOffset = offset - (elemTop - (bodyTop + scrollTop));
230+
231+
$window.scrollBy(0, -1 * necessaryOffset);
232+
}
169233
} else {
170234
$window.scrollTo(0, 0);
171235
}
172236
}
173237

238+
function scrollWhenReady() {
239+
if (document.readyState === 'complete') {
240+
$rootScope.$evalAsync(scroll);
241+
} else if (!scrollScheduled) {
242+
scrollScheduled = true;
243+
document.addEventListener('readystatechange', function unbindAndScroll() {
244+
// When navigating to a page with a URL including a hash,
245+
// Firefox overwrites our `yOffset` if `$apply()` is used instead.
246+
$rootScope.$evalAsync(function() {
247+
if (document.readyState === 'complete') {
248+
scrollScheduled = false;
249+
document.removeEventListener('readystatechange', unbindAndScroll);
250+
scroll();
251+
}
252+
});
253+
});
254+
}
255+
}
256+
174257
function scroll() {
175258
var hash = $location.hash(), elm;
176259

@@ -195,7 +278,7 @@ function $AnchorScrollProvider() {
195278
// skip the initial scroll if $location.hash is empty
196279
if (newVal === oldVal && newVal === '') return;
197280

198-
$rootScope.$evalAsync(scroll);
281+
scrollWhenReady();
199282
});
200283
}
201284

0 commit comments

Comments
 (0)