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

Commit 93faa82

Browse files
committed
WIP - feat($anchorScroll): add support for configurable scroll offset
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), add fallback for IE8's lack of `getComputedStyle()`. Also, remove two redundant calls to `$anchorScroll()` from the docs' examples. There still needs to be docs and tests.
1 parent 697643b commit 93faa82

File tree

1 file changed

+71
-31
lines changed

1 file changed

+71
-31
lines changed

src/ng/anchorScroll.js

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,9 @@
3333
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
3434
function ($scope, $location, $anchorScroll) {
3535
$scope.gotoBottom = function() {
36-
// set the location.hash to the id of
37-
// the element you wish to scroll to.
36+
// Set the location.hash to the id of the element you wish to scroll to
37+
// and $anchorScroll will kick in automatically.
3838
$location.hash('bottom');
39-
40-
// call $anchorScroll()
41-
$anchorScroll();
4239
};
4340
}]);
4441
</file>
@@ -72,18 +69,15 @@
7269
</file>
7370
<file name="script.js">
7471
angular.module('anchorScrollOffsetExample', [])
75-
.config(['$anchorScrollProvider', function($anchorScrollProvider) {
76-
$anchorScrollProvider.setScrollOffset(50); // always scroll by 50 extra pixels
72+
.run(['$anchorScroll', function($anchorScroll) {
73+
$anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
7774
}])
7875
.controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
7976
function ($anchorScroll, $location, $scope) {
8077
$scope.gotoAnchor = function(x) {
81-
// Set the location.hash to the id of
82-
// the element you wish to scroll to.
78+
// Set the location.hash to the id of the element you wish to scroll to
79+
// and $anchorScroll will kick in automatically.
8380
$location.hash('anchor' + x);
84-
85-
// Call $anchorScroll()
86-
$anchorScroll();
8781
};
8882
}
8983
]);
@@ -113,8 +107,6 @@
113107
</example>
114108
*/
115109
function $AnchorScrollProvider() {
116-
// TODO(gkalpak): The $anchorScrollProvider should be documented as well
117-
// (under the providers section).
118110

119111
var autoScrollingEnabled = true;
120112

@@ -125,9 +117,38 @@ function $AnchorScrollProvider() {
125117
this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) {
126118
var document = $window.document;
127119

128-
// Helper function to get first anchor from a NodeList
129-
// (using `Array#some()` instead of `angular#forEach()` since it's more performant
130-
// and working in all supported browsers.)
120+
// Helper function to increase code readability and DRYness
121+
function asInt(val) {
122+
return parseInt(val, 10) || 0;
123+
}
124+
125+
// IE8 does not support `window.getComputedStyle()`, but it's proprietary `elem.currentStyle`
126+
// seems to work the same. If neither exists, fall back to `elem.style`.
127+
function computedStyleGetter(elem) {
128+
return $window.getComputedStyle ?
129+
$window.getComputedStyle(elem) :
130+
elem.currentStyle || elem.style || {};
131+
}
132+
133+
// `offsetTop` works consistently across browsers, but returns the offset from the element's
134+
// "offsetParent", which is (either <body> or the first ancestor with `position !== 'static'` or
135+
// (if elem has `position: static`) a <table>, <th>, <td>).
136+
// This function will recursively add the offsetTop of each offsetParent along with the width of
137+
// its top-border.
138+
function offsetTopGetter(elem) {
139+
var totalOffsetTop = asInt(elem.offsetTop);
140+
141+
while (elem = elem.offsetParent) {
142+
var computedStyle = computedStyleGetter(elem);
143+
var borderTop = asInt(computedStyle.borderTopWidth);
144+
var offsetTop = asInt(elem.offsetTop);
145+
totalOffsetTop += offsetTop + borderTop;
146+
}
147+
148+
return totalOffsetTop;
149+
}
150+
151+
131152
function getFirstAnchor(list) {
132153
var result = null;
133154
Array.prototype.some.call(list, function(element) {
@@ -144,28 +165,47 @@ function $AnchorScrollProvider() {
144165
var offset = scroll.yOffset;
145166

146167
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-
168+
var elem = offset[0];
169+
170+
// TODO: Document that yOffset must be a jqLite/jQuery wrapped element
171+
var style = computedStyleGetter(elem);
172+
if (style.position !== 'fixed') {
173+
offset = 0;
174+
} else {
175+
// TODO: Make sure this works well on all supported browsers.
176+
// Tested on Chrome 37, Firefox 32.0.3 and IE8-IE11 and works as expected.
177+
// I.e. `elem.offsetHeight` consistently accounts for the height of the content,
178+
// the padding, the scrollbars (if any) and the border of the element
179+
// (for both `content-box` and `border-box` boxSizings).
180+
// (See also this fiddle: http://jsfiddle.net/ExpertSystem/5dx7fg2r/
181+
var top = offsetTopGetter(elem.offsetTop);
182+
var height = asInt(elem.offsetHeight);
183+
offset = top + height;
184+
}
153185
} else if (isFunction(offset)) {
154-
return offset();
155-
156-
} else if (isNumber(offset)) {
157-
return offset;
158-
159-
} else {
160-
return 0;
186+
offset = asInt(offset());
187+
} else if (!isNumber(offset)) {
188+
offset = 0;
161189
}
162190

191+
return offset;
163192
}
164193

165194
function scrollTo(elem) {
166195
if (elem) {
167196
elem.scrollIntoView();
168-
$window.scrollBy(0, -1 * getYOffset());
197+
198+
var offset = getYOffset();
199+
// `offset` is the number of pixels we should scroll up in order to align `elem` properly.
200+
// This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the
201+
// top of the viewport. IF the number of pixels from the top of `elem` to the end of page's
202+
// content is less than the height of the viewport, then `elem.scrollIntoView()` will NOT
203+
// align the top of `elem` at the top of the viewport (but further down). This is often the
204+
// case for elements near the bottom of the page.
205+
// In such cases we do not need to scroll the whole `offset` up, just the fraction of the
206+
// offset that is necessary to align the top of `elem` at the desired position.
207+
var necessaryOffset = offset && (offset - (offsetTopGetter(elem) - document.body.scrollTop));
208+
$window.scrollBy(0, -1 * necessaryOffset);
169209
} else {
170210
$window.scrollTo(0, 0);
171211
}

0 commit comments

Comments
 (0)