1
1
'use strict' ;
2
2
3
3
/**
4
- * @ngdoc service
5
- * @name $anchorScroll
6
- * @kind function
7
- * @requires $window
8
- * @requires $location
9
- * @requires $rootScope
4
+ * @ngdoc provider
5
+ * @name $anchorScrollProvider
10
6
*
11
7
* @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.
114
10
*/
115
11
function $AnchorScrollProvider ( ) {
116
- // TODO(gkalpak): The $anchorScrollProvider should be documented as well
117
- // (under the providers section).
118
12
119
13
var autoScrollingEnabled = true ;
120
14
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
+ */
121
28
this . disableAutoScrolling = function ( ) {
122
29
autoScrollingEnabled = false ;
123
30
} ;
124
31
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
+ */
125
169
this . $get = [ '$window' , '$location' , '$rootScope' , function ( $window , $location , $rootScope ) {
126
170
var document = $window . document ;
171
+ var scrollScheduled = false ;
127
172
128
173
// Helper function to get first anchor from a NodeList
129
174
// (using `Array#some()` instead of `angular#forEach()` since it's more performant
@@ -143,34 +188,72 @@ function $AnchorScrollProvider() {
143
188
144
189
var offset = scroll . yOffset ;
145
190
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 ;
161
206
}
162
207
208
+ return offset ;
163
209
}
164
210
165
211
function scrollTo ( elem ) {
166
212
if ( elem ) {
167
213
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
+ }
169
233
} else {
170
234
$window . scrollTo ( 0 , 0 ) ;
171
235
}
172
236
}
173
237
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
+
174
257
function scroll ( ) {
175
258
var hash = $location . hash ( ) , elm ;
176
259
@@ -195,7 +278,7 @@ function $AnchorScrollProvider() {
195
278
// skip the initial scroll if $location.hash is empty
196
279
if ( newVal === oldVal && newVal === '' ) return ;
197
280
198
- $rootScope . $evalAsync ( scroll ) ;
281
+ scrollWhenReady ( ) ;
199
282
} ) ;
200
283
}
201
284
0 commit comments