2018-05-17 07:33:50 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2018-05-17 07:33:50 -04:00
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2020-12-22 09:34:37 -08:00
|
|
|
import {ɵɵdefineInjectable, ɵɵinject} from '@angular/core';
|
2018-05-17 07:33:50 -04:00
|
|
|
|
|
|
|
import {DOCUMENT} from './dom_tokens';
|
|
|
|
|
2019-03-28 15:00:51 -07:00
|
|
|
|
|
|
|
|
2018-05-17 07:33:50 -04:00
|
|
|
/**
|
2018-11-30 12:16:35 -08:00
|
|
|
* Defines a scroll position manager. Implemented by `BrowserViewportScroller`.
|
2018-10-19 15:06:08 +01:00
|
|
|
*
|
|
|
|
* @publicApi
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
export abstract class ViewportScroller {
|
|
|
|
// De-sugared tree-shakable injection
|
|
|
|
// See #23917
|
|
|
|
/** @nocollapse */
|
2021-03-05 18:20:04 -08:00
|
|
|
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
|
2019-06-07 10:12:07 -07:00
|
|
|
token: ViewportScroller,
|
2019-03-28 15:00:51 -07:00
|
|
|
providedIn: 'root',
|
2020-12-22 09:34:37 -08:00
|
|
|
factory: () => new BrowserViewportScroller(ɵɵinject(DOCUMENT), window)
|
2019-03-28 15:00:51 -07:00
|
|
|
});
|
2018-05-17 07:33:50 -04:00
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Configures the top offset used when scrolling to an anchor.
|
2018-11-30 12:16:35 -08:00
|
|
|
* @param offset A position in screen coordinates (a tuple with x and y values)
|
|
|
|
* or a function that returns the top offset position.
|
2018-05-17 07:33:50 -04:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
abstract setOffset(offset: [number, number]|(() => [number, number])): void;
|
|
|
|
|
|
|
|
/**
|
2018-11-30 12:16:35 -08:00
|
|
|
* Retrieves the current scroll position.
|
|
|
|
* @returns A position in screen coordinates (a tuple with x and y values).
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
abstract getScrollPosition(): [number, number];
|
|
|
|
|
|
|
|
/**
|
2018-11-30 12:16:35 -08:00
|
|
|
* Scrolls to a specified position.
|
|
|
|
* @param position A position in screen coordinates (a tuple with x and y values).
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
abstract scrollToPosition(position: [number, number]): void;
|
|
|
|
|
|
|
|
/**
|
2018-11-30 12:16:35 -08:00
|
|
|
* Scrolls to an anchor element.
|
|
|
|
* @param anchor The ID of the anchor element.
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
abstract scrollToAnchor(anchor: string): void;
|
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Disables automatic scroll restoration provided by the browser.
|
2018-05-17 07:33:50 -04:00
|
|
|
* See also [window.history.scrollRestoration
|
2018-11-30 12:16:35 -08:00
|
|
|
* info](https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration).
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
abstract setHistoryScrollRestoration(scrollRestoration: 'auto'|'manual'): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-11-30 12:16:35 -08:00
|
|
|
* Manages the scroll position for a browser window.
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
export class BrowserViewportScroller implements ViewportScroller {
|
|
|
|
private offset: () => [number, number] = () => [0, 0];
|
|
|
|
|
2020-12-22 11:44:14 -08:00
|
|
|
constructor(private document: Document, private window: Window) {}
|
2018-05-17 07:33:50 -04:00
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Configures the top offset used when scrolling to an anchor.
|
2018-11-30 12:16:35 -08:00
|
|
|
* @param offset A position in screen coordinates (a tuple with x and y values)
|
|
|
|
* or a function that returns the top offset position.
|
2018-05-17 07:33:50 -04:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
setOffset(offset: [number, number]|(() => [number, number])): void {
|
|
|
|
if (Array.isArray(offset)) {
|
|
|
|
this.offset = () => offset;
|
|
|
|
} else {
|
|
|
|
this.offset = offset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-11-30 12:16:35 -08:00
|
|
|
* Retrieves the current scroll position.
|
|
|
|
* @returns The position in screen coordinates.
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
getScrollPosition(): [number, number] {
|
2020-08-14 10:32:55 -07:00
|
|
|
if (this.supportsScrolling()) {
|
2019-01-20 14:12:52 +01:00
|
|
|
return [this.window.pageXOffset, this.window.pageYOffset];
|
2018-05-17 07:33:50 -04:00
|
|
|
} else {
|
|
|
|
return [0, 0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Sets the scroll position.
|
2018-11-30 12:16:35 -08:00
|
|
|
* @param position The new position in screen coordinates.
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
scrollToPosition(position: [number, number]): void {
|
2020-08-14 10:32:55 -07:00
|
|
|
if (this.supportsScrolling()) {
|
2018-05-17 07:33:50 -04:00
|
|
|
this.window.scrollTo(position[0], position[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-12-22 11:44:14 -08:00
|
|
|
* Scrolls to an element and attempts to focus the element.
|
|
|
|
*
|
|
|
|
* Note that the function name here is misleading in that the target string may be an ID for a
|
|
|
|
* non-anchor element.
|
|
|
|
*
|
|
|
|
* @param target The ID of an element or name of the anchor.
|
|
|
|
*
|
|
|
|
* @see https://html.spec.whatwg.org/#the-indicated-part-of-the-document
|
|
|
|
* @see https://html.spec.whatwg.org/#scroll-to-fragid
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
2020-12-22 11:44:14 -08:00
|
|
|
scrollToAnchor(target: string): void {
|
|
|
|
if (!this.supportsScrolling()) {
|
|
|
|
return;
|
2018-05-17 07:33:50 -04:00
|
|
|
}
|
2020-12-22 11:44:14 -08:00
|
|
|
// TODO(atscott): The correct behavior for `getElementsByName` would be to also verify that the
|
|
|
|
// element is an anchor. However, this could be considered a breaking change and should be
|
|
|
|
// done in a major version.
|
2021-04-15 17:01:25 +02:00
|
|
|
const elSelected = findAnchorFromDocument(this.document, target);
|
2020-12-22 11:44:14 -08:00
|
|
|
|
2021-04-15 17:01:25 +02:00
|
|
|
if (elSelected) {
|
|
|
|
this.scrollToElement(elSelected);
|
|
|
|
// After scrolling to the element, the spec dictates that we follow the focus steps for the
|
|
|
|
// target. Rather than following the robust steps, simply attempt focus.
|
|
|
|
this.attemptFocus(elSelected);
|
|
|
|
}
|
2018-05-17 07:33:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Disables automatic scroll restoration provided by the browser.
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
setHistoryScrollRestoration(scrollRestoration: 'auto'|'manual'): void {
|
|
|
|
if (this.supportScrollRestoration()) {
|
|
|
|
const history = this.window.history;
|
|
|
|
if (history && history.scrollRestoration) {
|
|
|
|
history.scrollRestoration = scrollRestoration;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-22 11:44:14 -08:00
|
|
|
/**
|
|
|
|
* Scrolls to an element using the native offset and the specified offset set on this scroller.
|
|
|
|
*
|
|
|
|
* The offset can be used when we know that there is a floating header and scrolling naively to an
|
|
|
|
* element (ex: `scrollIntoView`) leaves the element hidden behind the floating header.
|
|
|
|
*/
|
|
|
|
private scrollToElement(el: HTMLElement): void {
|
2018-05-17 07:33:50 -04:00
|
|
|
const rect = el.getBoundingClientRect();
|
|
|
|
const left = rect.left + this.window.pageXOffset;
|
|
|
|
const top = rect.top + this.window.pageYOffset;
|
|
|
|
const offset = this.offset();
|
|
|
|
this.window.scrollTo(left - offset[0], top - offset[1]);
|
|
|
|
}
|
2020-12-22 11:44:14 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Calls `focus` on the `focusTarget` and returns `true` if the element was focused successfully.
|
|
|
|
*
|
|
|
|
* If `false`, further steps may be necessary to determine a valid substitute to be focused
|
|
|
|
* instead.
|
|
|
|
*
|
|
|
|
* @see https://html.spec.whatwg.org/#get-the-focusable-area
|
|
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus
|
|
|
|
* @see https://html.spec.whatwg.org/#focusable-area
|
|
|
|
*/
|
|
|
|
private attemptFocus(focusTarget: HTMLElement): boolean {
|
|
|
|
focusTarget.focus();
|
|
|
|
return this.document.activeElement === focusTarget;
|
|
|
|
}
|
2018-05-17 07:33:50 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* We only support scroll restoration when we can get a hold of window.
|
|
|
|
* This means that we do not support this behavior when running in a web worker.
|
|
|
|
*
|
|
|
|
* Lifting this restriction right now would require more changes in the dom adapter.
|
|
|
|
* Since webworkers aren't widely used, we will lift it once RouterScroller is
|
|
|
|
* battle-tested.
|
|
|
|
*/
|
|
|
|
private supportScrollRestoration(): boolean {
|
|
|
|
try {
|
2019-01-20 14:12:52 +01:00
|
|
|
if (!this.supportsScrolling()) {
|
2019-05-23 14:44:46 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// The `scrollRestoration` property could be on the `history` instance or its prototype.
|
|
|
|
const scrollRestorationDescriptor = getScrollRestorationProperty(this.window.history) ||
|
|
|
|
getScrollRestorationProperty(Object.getPrototypeOf(this.window.history));
|
|
|
|
// We can write to the `scrollRestoration` property if it is a writable data field or it has a
|
|
|
|
// setter function.
|
|
|
|
return !!scrollRestorationDescriptor &&
|
|
|
|
!!(scrollRestorationDescriptor.writable || scrollRestorationDescriptor.set);
|
2018-08-14 15:34:51 +02:00
|
|
|
} catch {
|
2018-05-17 07:33:50 -04:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2020-08-14 10:32:55 -07:00
|
|
|
|
|
|
|
private supportsScrolling(): boolean {
|
|
|
|
try {
|
2019-01-20 14:12:52 +01:00
|
|
|
return !!this.window && !!this.window.scrollTo && 'pageXOffset' in this.window;
|
2020-08-14 10:32:55 -07:00
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2018-05-17 07:33:50 -04:00
|
|
|
}
|
|
|
|
|
2019-05-23 14:44:46 +02:00
|
|
|
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
|
|
|
|
return Object.getOwnPropertyDescriptor(obj, 'scrollRestoration');
|
|
|
|
}
|
2018-05-17 07:33:50 -04:00
|
|
|
|
2021-04-15 17:01:25 +02:00
|
|
|
function findAnchorFromDocument(document: Document, target: string): HTMLElement|null {
|
|
|
|
const documentResult = document.getElementById(target) || document.getElementsByName(target)[0];
|
|
|
|
|
|
|
|
if (documentResult) {
|
|
|
|
return documentResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
// `getElementById` and `getElementsByName` won't pierce through the shadow DOM so we
|
|
|
|
// have to traverse the DOM manually and do the lookup through the shadow roots.
|
|
|
|
if (typeof document.createTreeWalker === 'function' && document.body &&
|
|
|
|
((document.body as any).createShadowRoot || document.body.attachShadow)) {
|
|
|
|
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
|
|
let currentNode = treeWalker.currentNode as HTMLElement | null;
|
|
|
|
|
|
|
|
while (currentNode) {
|
|
|
|
const shadowRoot = currentNode.shadowRoot;
|
|
|
|
|
|
|
|
if (shadowRoot) {
|
|
|
|
// Note that `ShadowRoot` doesn't support `getElementsByName`
|
|
|
|
// so we have to fall back to `querySelector`.
|
|
|
|
const result =
|
|
|
|
shadowRoot.getElementById(target) || shadowRoot.querySelector(`[name="${target}"]`);
|
|
|
|
if (result) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
currentNode = treeWalker.nextNode() as HTMLElement | null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-17 07:33:50 -04:00
|
|
|
/**
|
2020-09-14 15:48:03 -07:00
|
|
|
* Provides an empty implementation of the viewport scroller.
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
export class NullViewportScroller implements ViewportScroller {
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Empty implementation
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
setOffset(offset: [number, number]|(() => [number, number])): void {}
|
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Empty implementation
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
2020-04-13 16:40:21 -07:00
|
|
|
getScrollPosition(): [number, number] {
|
|
|
|
return [0, 0];
|
|
|
|
}
|
2018-05-17 07:33:50 -04:00
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Empty implementation
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
scrollToPosition(position: [number, number]): void {}
|
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Empty implementation
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
scrollToAnchor(anchor: string): void {}
|
|
|
|
|
|
|
|
/**
|
2018-09-20 14:28:00 +01:00
|
|
|
* Empty implementation
|
2018-05-17 07:33:50 -04:00
|
|
|
*/
|
|
|
|
setHistoryScrollRestoration(scrollRestoration: 'auto'|'manual'): void {}
|
2019-03-11 19:20:40 -05:00
|
|
|
}
|