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 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