mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-28 04:00:23 +00:00
feat(service.$autoScroll): scroll to hash fragment
- whenever hash part of the url changes - after ng:view / ng:include load
This commit is contained in:
parent
29f9e2665d
commit
3548fe3139
7 changed files with 251 additions and 5 deletions
1
angularFiles.js
vendored
1
angularFiles.js
vendored
|
|
@ -8,6 +8,7 @@ angularFiles = {
|
|||
'src/sanitizer.js',
|
||||
'src/jqLite.js',
|
||||
'src/apis.js',
|
||||
'src/service/autoScroll.js',
|
||||
'src/service/browser.js',
|
||||
'src/service/compiler.js',
|
||||
'src/service/cookieStore.js',
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ function DocsController($location, $window, $cookies, $filter) {
|
|||
scope.loading--;
|
||||
scope.partialTitle = scope.futurePartialTitle;
|
||||
SyntaxHighlighter.highlight();
|
||||
$window.scrollTo(0,0);
|
||||
$window._gaq.push(['_trackPageview', currentPageId]);
|
||||
loadDisqus(currentPageId);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ function ngModule($provide, $injector) {
|
|||
$provide.service('$locale', $LocaleProvider);
|
||||
});
|
||||
|
||||
$provide.service('$autoScroll', $AutoScrollProvider);
|
||||
$provide.service('$browser', $BrowserProvider);
|
||||
$provide.service('$compile', $CompileProvider);
|
||||
$provide.service('$cookies', $CookiesProvider);
|
||||
|
|
|
|||
67
src/service/autoScroll.js
Normal file
67
src/service/autoScroll.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @ngdoc function
|
||||
* @name angular.module.ng.$autoScroll
|
||||
* @requires $window
|
||||
* @requires $location
|
||||
* @requires $rootScope
|
||||
*
|
||||
* @description
|
||||
* When called, it checks current value of `$location.hash()` and scroll to related element,
|
||||
* according to rules specified in
|
||||
* {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}.
|
||||
*
|
||||
* If `$location` uses `hashbang` url (running in `hashbang` mode or `html5` mode on browser without
|
||||
* history API support), `$autoScroll` watches the `$location.hash()` and scroll whenever it
|
||||
* changes.
|
||||
*
|
||||
* You can disable `$autoScroll` service by calling `disable()` on `$autoScrollProvider`.
|
||||
* Note: disabling is only possible before the service is instantiated !
|
||||
*/
|
||||
function $AutoScrollProvider() {
|
||||
|
||||
this.disable = function() {
|
||||
this.$get = function() {return noop;};
|
||||
};
|
||||
|
||||
this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) {
|
||||
var document = $window.document;
|
||||
|
||||
// helper function to get first anchor from a NodeList
|
||||
// can't use filter.filter, as it accepts only instances of Array
|
||||
// and IE can't convert NodeList to an array using [].slice
|
||||
// TODO(vojta): use filter if we change it to accept lists as well
|
||||
function getFirstAnchor(list) {
|
||||
var result = null;
|
||||
forEach(list, function(element) {
|
||||
if (!result && lowercase(element.nodeName) === 'a') result = element;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
var hash = $location.hash(), elm;
|
||||
|
||||
// empty hash, scroll to the top of the page
|
||||
if (!hash) $window.scrollTo(0, 0);
|
||||
|
||||
// element with given id
|
||||
else if ((elm = document.getElementById(hash))) elm.scrollIntoView();
|
||||
|
||||
// first anchor with given name :-D
|
||||
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView();
|
||||
|
||||
// no element and hash == 'top', scroll to the top of the page
|
||||
else if (hash === 'top') $window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// scroll whenever hash changes (with hashbang url, regular urls are handled by browser)
|
||||
if ($location instanceof LocationHashbangUrl) {
|
||||
$rootScope.$watch(function() {return $location.hash();}, function() {
|
||||
$rootScope.$evalAsync(scroll);
|
||||
});
|
||||
}
|
||||
|
||||
return scroll;
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ function LocationHashbangUrl(url, hashPrefix) {
|
|||
}
|
||||
|
||||
|
||||
LocationUrl.prototype = LocationHashbangUrl.prototype = {
|
||||
LocationUrl.prototype = {
|
||||
|
||||
/**
|
||||
* Has any change been replacing ?
|
||||
|
|
@ -374,6 +374,7 @@ LocationUrl.prototype = LocationHashbangUrl.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
LocationHashbangUrl.prototype = inherit(LocationUrl.prototype);
|
||||
|
||||
function locationGetter(property) {
|
||||
return function() {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ angularWidget('ng:include', function(element){
|
|||
this.directives(true);
|
||||
} else {
|
||||
element[0]['ng:compiled'] = true;
|
||||
return ['$xhr.cache', '$element', function(xhr, element){
|
||||
return ['$xhr.cache', '$autoScroll', '$element', function($xhr, $autoScroll, element) {
|
||||
var scope = this,
|
||||
changeCounter = 0,
|
||||
releaseScopes = [],
|
||||
|
|
@ -114,7 +114,7 @@ angularWidget('ng:include', function(element){
|
|||
releaseScopes.pop().$destroy();
|
||||
}
|
||||
if (src) {
|
||||
xhr('GET', src, null, function(code, response){
|
||||
$xhr('GET', src, null, function(code, response) {
|
||||
element.html(response);
|
||||
if (useScope) {
|
||||
childScope = useScope;
|
||||
|
|
@ -122,6 +122,7 @@ angularWidget('ng:include', function(element){
|
|||
releaseScopes.push(childScope = scope.$new());
|
||||
}
|
||||
compiler.compile(element)(childScope);
|
||||
$autoScroll();
|
||||
scope.$eval(onloadExp);
|
||||
}, false, true);
|
||||
} else {
|
||||
|
|
@ -555,7 +556,7 @@ angularWidget('ng:view', function(element) {
|
|||
|
||||
if (!element[0]['ng:compiled']) {
|
||||
element[0]['ng:compiled'] = true;
|
||||
return ['$xhr.cache', '$route', '$element', function($xhr, $route, element){
|
||||
return ['$xhr.cache', '$route', '$autoScroll', '$element', function($xhr, $route, $autoScroll, element) {
|
||||
var template;
|
||||
var changeCounter = 0;
|
||||
|
||||
|
|
@ -572,6 +573,7 @@ angularWidget('ng:view', function(element) {
|
|||
if (newChangeCounter == changeCounter) {
|
||||
element.html(response);
|
||||
compiler.compile(element)($route.current.scope);
|
||||
$autoScroll();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
175
test/service/autoScrollSpec.js
Normal file
175
test/service/autoScrollSpec.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
describe('$autoScroll', function() {
|
||||
|
||||
var elmSpy;
|
||||
|
||||
function addElements() {
|
||||
var elements = sliceArgs(arguments);
|
||||
|
||||
return function() {
|
||||
forEach(elements, function(identifier) {
|
||||
var match = identifier.match(/(\w* )?(\w*)=(\w*)/),
|
||||
jqElm = jqLite('<' + (match[1] || 'a ') + match[2] + '="' + match[3] + '"/>'),
|
||||
elm = jqElm[0];
|
||||
|
||||
elmSpy[identifier] = spyOn(elm, 'scrollIntoView');
|
||||
jqLite(document.body).append(jqElm);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function changeHashAndScroll(hash) {
|
||||
return function($location, $autoScroll) {
|
||||
$location.hash(hash);
|
||||
$autoScroll();
|
||||
};
|
||||
}
|
||||
|
||||
function expectScrollingToTop($window) {
|
||||
forEach(elmSpy, function(spy, id) {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect($window.scrollTo).toHaveBeenCalledWith(0, 0);
|
||||
}
|
||||
|
||||
function expectScrollingTo(identifier) {
|
||||
return function($window) {
|
||||
forEach(elmSpy, function(spy, id) {
|
||||
if (identifier === id) expect(spy).toHaveBeenCalledOnce();
|
||||
else expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
expect($window.scrollTo).not.toHaveBeenCalled();
|
||||
};
|
||||
}
|
||||
|
||||
function expectNoScrolling() {
|
||||
return expectScrollingTo(NaN);
|
||||
}
|
||||
|
||||
function disableScroller() {
|
||||
return function($autoScrollProvider) {
|
||||
$autoScrollProvider.disable();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
beforeEach(inject(function($provide) {
|
||||
elmSpy = {};
|
||||
$provide.value('$window', {
|
||||
scrollTo: jasmine.createSpy('$window.scrollTo'),
|
||||
document: document
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should scroll to top of the window if empty hash', inject(
|
||||
changeHashAndScroll(''),
|
||||
expectScrollingToTop));
|
||||
|
||||
|
||||
it('should not scroll if hash does not match any element', inject(
|
||||
addElements('id=one', 'id=two'),
|
||||
changeHashAndScroll('non-existing'),
|
||||
expectNoScrolling()));
|
||||
|
||||
|
||||
it('should scroll to anchor element with name', inject(
|
||||
addElements('a name=abc'),
|
||||
changeHashAndScroll('abc'),
|
||||
expectScrollingTo('a name=abc')));
|
||||
|
||||
|
||||
it('should not scroll to other than anchor element with name', inject(
|
||||
addElements('input name=xxl', 'select name=xxl', 'form name=xxl'),
|
||||
changeHashAndScroll('xxl'),
|
||||
expectNoScrolling()));
|
||||
|
||||
|
||||
it('should scroll to anchor even if other element with given name exist', inject(
|
||||
addElements('input name=some', 'a name=some'),
|
||||
changeHashAndScroll('some'),
|
||||
expectScrollingTo('a name=some')));
|
||||
|
||||
|
||||
it('should scroll to element with id with precedence over name', inject(
|
||||
addElements('name=abc', 'id=abc'),
|
||||
changeHashAndScroll('abc'),
|
||||
expectScrollingTo('id=abc')));
|
||||
|
||||
|
||||
it('should scroll to top if hash == "top" and no matching element', inject(
|
||||
changeHashAndScroll('top'),
|
||||
expectScrollingToTop));
|
||||
|
||||
|
||||
it('should scroll to element with id "top" if present', inject(
|
||||
addElements('id=top'),
|
||||
changeHashAndScroll('top'),
|
||||
expectScrollingTo('id=top')));
|
||||
|
||||
|
||||
it('should not scroll when disabled', inject(
|
||||
addElements('id=fake', 'a name=fake', 'input name=fake'),
|
||||
disableScroller(),
|
||||
changeHashAndScroll('fake'),
|
||||
expectNoScrolling()));
|
||||
|
||||
|
||||
describe('watcher', function() {
|
||||
|
||||
function initLocation(config) {
|
||||
return function($provide, $locationProvider) {
|
||||
$provide.value('$sniffer', {history: config.historyApi});
|
||||
$locationProvider.html5Mode(config.html5Mode);
|
||||
};
|
||||
}
|
||||
|
||||
function changeHashAndDigest(hash) {
|
||||
return function ($location, $rootScope, $autoScroll) {
|
||||
$location.hash(hash);
|
||||
$rootScope.$digest();
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(inject(function($document) {
|
||||
dealoc($document);
|
||||
}));
|
||||
|
||||
|
||||
it('should scroll to element when hash change in hashbang mode', inject(
|
||||
initLocation({html5Mode: false, historyApi: true}),
|
||||
addElements('id=some'),
|
||||
changeHashAndDigest('some'),
|
||||
expectScrollingTo('id=some')));
|
||||
|
||||
|
||||
it('should scroll to element when hash change in html5 mode with no history api', inject(
|
||||
initLocation({html5Mode: true, historyApi: false}),
|
||||
addElements('id=some'),
|
||||
changeHashAndDigest('some'),
|
||||
expectScrollingTo('id=some')));
|
||||
|
||||
|
||||
it('should not scroll when element does not exist', inject(
|
||||
initLocation({html5Mode: false, historyApi: false}),
|
||||
addElements('id=some'),
|
||||
changeHashAndDigest('other'),
|
||||
expectNoScrolling()));
|
||||
|
||||
|
||||
it('should not scroll when html5 mode with history api', inject(
|
||||
initLocation({html5Mode: true, historyApi: true}),
|
||||
addElements('id=some'),
|
||||
changeHashAndDigest('some'),
|
||||
expectNoScrolling()));
|
||||
|
||||
|
||||
it('should not scroll when disabled', inject(
|
||||
disableScroller(),
|
||||
initLocation({html5Mode: false, historyApi: false}),
|
||||
addElements('id=fake'),
|
||||
changeHashAndDigest('fake'),
|
||||
expectNoScrolling()));
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in a new issue