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 */
5411function $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 ;
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,90 @@ 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+ 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 ;
206+ }
207+
208+ return offset ;
209+ }
210+
211+ function scrollTo ( elem ) {
212+ if ( elem ) {
213+ elem . scrollIntoView ( ) ;
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+ }
233+ } else {
234+ $window . scrollTo ( 0 , 0 ) ;
235+ }
236+ }
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+
79257 function scroll ( ) {
80258 var hash = $location . hash ( ) , elm ;
81259
82260 // empty hash, scroll to the top of the page
83- if ( ! hash ) $window . scrollTo ( 0 , 0 ) ;
261+ if ( ! hash ) scrollTo ( null ) ;
84262
85263 // element with given id
86- else if ( ( elm = document . getElementById ( hash ) ) ) elm . scrollIntoView ( ) ;
264+ else if ( ( elm = document . getElementById ( hash ) ) ) scrollTo ( elm ) ;
87265
88266 // first anchor with given name :-D
89- else if ( ( elm = getFirstAnchor ( document . getElementsByName ( hash ) ) ) ) elm . scrollIntoView ( ) ;
267+ else if ( ( elm = getFirstAnchor ( document . getElementsByName ( hash ) ) ) ) scrollTo ( elm ) ;
90268
91269 // no element and hash == 'top', scroll to the top of the page
92- else if ( hash === 'top' ) $window . scrollTo ( 0 , 0 ) ;
270+ else if ( hash === 'top' ) scrollTo ( null ) ;
93271 }
94272
95273 // does not scroll when user clicks on anchor link that is currently on
@@ -100,11 +278,10 @@ function $AnchorScrollProvider() {
100278 // skip the initial scroll if $location.hash is empty
101279 if ( newVal === oldVal && newVal === '' ) return ;
102280
103- $rootScope . $evalAsync ( scroll ) ;
281+ scrollWhenReady ( ) ;
104282 } ) ;
105283 }
106284
107285 return scroll ;
108286 } ] ;
109287}
110-
0 commit comments