From fc9caf5fced4d556cb48bf3f5a4611111a14aee2 Mon Sep 17 00:00:00 2001 From: Iakovleva Margarita Date: Tue, 3 Sep 2019 14:55:32 +0300 Subject: [PATCH] feat(docs): angular anchors (#223) --- .../components/anchors/anchors.component.html | 8 +- .../components/anchors/anchors.component.ts | 120 ++++++++---------- .../app/components/anchors/anchors.module.ts | 3 +- .../component-viewer.module.ts | 1 - .../main-layout/main-layout.module.ts | 4 +- packages/docs/src/app/docs.module.ts | 4 +- packages/docs/src/app/docs.scss | 4 - packages/docs/src/styles/_common.scss | 1 - packages/docs/src/styles/_markdown.scss | 11 ++ packages/mosaic/button/button.md | 4 +- tools/gulp/tasks/docs.ts | 4 +- 11 files changed, 76 insertions(+), 88 deletions(-) diff --git a/packages/docs/src/app/components/anchors/anchors.component.html b/packages/docs/src/app/components/anchors/anchors.component.html index a6aa4bff7..e4ef86214 100644 --- a/packages/docs/src/app/components/anchors/anchors.component.html +++ b/packages/docs/src/app/components/anchors/anchors.component.html @@ -1,10 +1,8 @@
-
-
- {{anchor.name}} +
diff --git a/packages/docs/src/app/components/anchors/anchors.component.ts b/packages/docs/src/app/components/anchors/anchors.component.ts index 83a5c1182..fed17ee29 100644 --- a/packages/docs/src/app/components/anchors/anchors.component.ts +++ b/packages/docs/src/app/components/anchors/anchors.component.ts @@ -27,54 +27,42 @@ export class AnchorsComponent { container: string; headerHeight: number = 64; // коэффициент для вычисления расстояния якоря над заголовком при скроле (== headerHeight * anchorHeaderCoef) - anchorHeaderCoef = 3; - debounceTime: number = 5; - readonly isSmoothScrollSupported; + anchorHeaderCoef = 2; + debounceTime: number = 10; private destroyed = new Subject(); - private urlFragment = ''; + private fragment = ''; + private activeClass = 'anchors-menu__list-element_active'; private scrollContainer: any; private currentUrl: any; private pathName: string; - private scrollTimeout: number = 1000; - private scrollOptions: ScrollIntoViewOptions = { behavior: 'smooth' }; constructor(private router: Router, private route: ActivatedRoute, private element: ElementRef, private ref: ChangeDetectorRef, @Inject(DOCUMENT) private document: Document) { + this.currentUrl = router.url.split('#')[0]; this.container = '.anchors-menu'; - this.isSmoothScrollSupported = 'scrollBehavior' in this.document.documentElement.style; + this.pathName = location.pathname; this.router.events.pipe(takeUntil(this.destroyed)).subscribe((event) => { if (event instanceof NavigationEnd) { const rootUrl = router.url.split('#')[0]; if (rootUrl !== this.currentUrl) { - this.anchors = this.createAnchors(); this.currentUrl = rootUrl; this.pathName = location.pathname; } } }); + } + ngOnInit() { // Срабатывает при изменении якоря в адресной строке руками или кликом по якорю this.route.fragment.pipe(takeUntil(this.destroyed)).subscribe((fragment) => { - this.urlFragment = fragment; - const anchorActive = this.document.querySelector('.anchors-menu__list-element_active'); - - const target = this.document.getElementById(this.urlFragment); - const index = this.getAnchorIndex(this.urlFragment); - - if (index) { this.setActiveAnchor(index); } - - if (target) { - this.click = true; + this.fragment = fragment; + const index = this.getAnchorIndex(fragment); - if (anchorActive && this.urlFragment === anchorActive.textContent.trim().toLowerCase()) { - return; - } - this.scrollTo(target); - } + if (index) { this.setFragment(index); } }); } @@ -82,18 +70,10 @@ export class AnchorsComponent { this.destroyed.next(); } - scrollTo(target) { - if (this.isSmoothScrollSupported) { - target.scrollIntoView(this.scrollOptions); - } else { - target.scrollIntoView(); - } - } - getAnchorIndex(urlFragment): number { let index = 0; this.anchors.forEach((anchor, i) => { - if (anchor.href === `#${urlFragment}`) { index = i; } + if (anchor.href === `${urlFragment}`) { index = i; } }); return index; @@ -101,30 +81,23 @@ export class AnchorsComponent { setScrollPosition() { this.anchors = this.createAnchors(); - const target = this.document.getElementById(this.urlFragment); + this.scrollContainer = this.document || window; + const target = this.document.getElementById(this.fragment); if (target) { - const index = this.getAnchorIndex(this.urlFragment); + const index = this.getAnchorIndex(this.fragment); - if (index) { this.setActiveAnchor(index); } + if (index) { this.setFragment(index); } target.scrollTop += this.headerHeight; - this.scrollTo(target); + target.scrollIntoView(); } - const scrollPromise = new Promise((resolve, reject) => { - setTimeout(() => { resolve(); }, this.scrollTimeout); - }); - - scrollPromise.then(() => { - this.scrollContainer = this.document || window; - - if (this.scrollContainer) { - fromEvent(this.scrollContainer, 'scroll').pipe( - takeUntil(this.destroyed), - debounceTime(this.debounceTime)) - .subscribe(() => this.onScroll()); - } - }); + if (this.scrollContainer) { + fromEvent(this.scrollContainer, 'scroll').pipe( + takeUntil(this.destroyed), + debounceTime(this.debounceTime)) + .subscribe(() => this.onScroll()); + } } /* TODO Техдолг: при изменении ширины экрана должен переопределяться параметр top @@ -144,17 +117,26 @@ export class AnchorsComponent { return window.pageYOffset + this.headerHeight * this.anchorHeaderCoef; } - private createAnchors(): IAnchor[] { + private isScrolledToEnd(): boolean { + const documentHeight = this.document.documentElement.scrollHeight; + const scrollTop = window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop; + const clientHeight = this.document.documentElement.clientHeight; + const scrollHeight = scrollTop + clientHeight; + + // scrollHeight должен быть строго равен documentHeight, но в Edge он чуть больше + return scrollHeight >= documentHeight; + } + private createAnchors(): IAnchor[] { const anchors = []; const headers = Array.from(this.document.querySelectorAll(this.headerSelectors)) as HTMLElement[]; if (headers.length) { + const bodyTop = this.document.body.getBoundingClientRect().top; for (let i = 0; i < headers.length; i++) { - // remove the 'link' icon name from the inner text const name = headers[i].innerText.trim(); - const href = `#${headers[i].id}`; - const {top} = headers[i].getBoundingClientRect(); + const href = `${headers[i].querySelector('span').id}`; + const top = headers[i].getBoundingClientRect().top - bodyTop + this.headerHeight; anchors.push({ href, @@ -171,15 +153,17 @@ export class AnchorsComponent { } private onScroll() { - // Если переход на якорь был инициирован не скролом выходим из функции - if (this.click) { - this.disableClickState(); + if (this.isScrolledToEnd()) { + this.setActiveAnchor(this.anchors.length - 1); + this.ref.detectChanges(); return; } for (let i = 0; i < this.anchors.length; i++) { - this.anchors[i].active = this.isLinkActive(this.anchors[i], this.anchors[i + 1]); + if (this.isLinkActive(this.anchors[i], this.anchors[i + 1])) { + this.setActiveAnchor(i); + } } this.ref.detectChanges(); } @@ -192,20 +176,20 @@ export class AnchorsComponent { return scrollOffset >= currentLink.top && !(nextLink && nextLink.top < scrollOffset); } - private setActiveAnchor(index) { + private setFragment(index) { + if (this.isScrolledToEnd()) { + this.setActiveAnchor(this.anchors.length - 1); + + return; + } this.click = true; + this.setActiveAnchor(index); + } + + private setActiveAnchor(index) { for (const anchor of this.anchors) { anchor.active = false; } this.anchors[index].active = true; - this.ref.detectChanges(); - } - - private disableClickState() { - // При клике по якорю браузер вызывает scroll несколько раз - setTimeout нужен, чтобы код отработал единожды - // Возможная альтернатива: debounceTime: number = 50; - но это ухудшает отзывчивость якорей при скролле - setTimeout(() => { - this.click = false; - }, 500); } } diff --git a/packages/docs/src/app/components/anchors/anchors.module.ts b/packages/docs/src/app/components/anchors/anchors.module.ts index 2a70460d1..9e1c8dd9a 100644 --- a/packages/docs/src/app/components/anchors/anchors.module.ts +++ b/packages/docs/src/app/components/anchors/anchors.module.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; import { AnchorsComponent } from './anchors.component'; @NgModule({ - imports: [CommonModule], + imports: [CommonModule, RouterModule], exports: [AnchorsComponent], declarations: [AnchorsComponent] }) diff --git a/packages/docs/src/app/components/component-viewer/component-viewer.module.ts b/packages/docs/src/app/components/component-viewer/component-viewer.module.ts index ef24d8b0b..07b6d7326 100644 --- a/packages/docs/src/app/components/component-viewer/component-viewer.module.ts +++ b/packages/docs/src/app/components/component-viewer/component-viewer.module.ts @@ -8,7 +8,6 @@ import { DocumentationItems } from '../../shared/documentation-items/documentati import { TableOfContentsModule } from '../../shared/table-of-contents/table-of-contents.module'; import { AnchorsModule } from '../anchors/anchors.module'; - import { ComponentOverviewComponent, ComponentViewerComponent diff --git a/packages/docs/src/app/components/main-layout/main-layout.module.ts b/packages/docs/src/app/components/main-layout/main-layout.module.ts index e5e0e4ee6..a7cafd375 100644 --- a/packages/docs/src/app/components/main-layout/main-layout.module.ts +++ b/packages/docs/src/app/components/main-layout/main-layout.module.ts @@ -1,4 +1,3 @@ -import { AnchorsModule } from '../anchors/anchors.module'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; @@ -7,11 +6,12 @@ import { McDropdownModule } from '@ptsecurity/mosaic/dropdown'; import { McIconModule } from '@ptsecurity/mosaic/icon'; import { McTreeModule } from '@ptsecurity/mosaic/tree'; +import { AnchorsModule } from '../anchors/anchors.module'; +import { FooterModule } from '../footer/footer.module'; import { NavbarModule } from '../navbar'; import { SidenavModule } from '../sidenav/sidenav.module'; import { MainLayoutComponent } from './main-layout.component'; -import { FooterModule } from '../footer/footer.module'; @NgModule({ diff --git a/packages/docs/src/app/docs.module.ts b/packages/docs/src/app/docs.module.ts index 61dc10450..4c75845ed 100644 --- a/packages/docs/src/app/docs.module.ts +++ b/packages/docs/src/app/docs.module.ts @@ -25,8 +25,8 @@ import { DocumentationItems } from './shared/documentation-items/documentation-i RouterModule.forRoot(APP_ROUTES, { scrollPositionRestoration: 'enabled', - anchorScrolling: 'enabled', - scrollOffset: [0, 64] // [x, y] + onSameUrlNavigation: 'reload', + anchorScrolling: 'enabled' }), HomepageModule, diff --git a/packages/docs/src/app/docs.scss b/packages/docs/src/app/docs.scss index a3bc7786e..bf46eda13 100644 --- a/packages/docs/src/app/docs.scss +++ b/packages/docs/src/app/docs.scss @@ -1,7 +1,3 @@ -html { - scroll-behavior: smooth; -} - docs-app { display: flex; flex-direction: column; diff --git a/packages/docs/src/styles/_common.scss b/packages/docs/src/styles/_common.scss index 1ce3e6caa..580e1b980 100644 --- a/packages/docs/src/styles/_common.scss +++ b/packages/docs/src/styles/_common.scss @@ -19,7 +19,6 @@ html { -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // 4 -webkit-touch-callout: none; - scroll-behavior: smooth; } // Body diff --git a/packages/docs/src/styles/_markdown.scss b/packages/docs/src/styles/_markdown.scss index 9ac445050..4ac88d194 100644 --- a/packages/docs/src/styles/_markdown.scss +++ b/packages/docs/src/styles/_markdown.scss @@ -51,6 +51,17 @@ } } +.docs-header-link { + position: relative; + + & span { + position: absolute; + display: block; + left: 0; + top: -60px; + } +} + @mixin docs-markdown-theme($theme) { $primary: map-get($theme, primary); $second: map-get($theme, second); diff --git a/packages/mosaic/button/button.md b/packages/mosaic/button/button.md index 08a6b50e7..327d133a8 100644 --- a/packages/mosaic/button/button.md +++ b/packages/mosaic/button/button.md @@ -2,8 +2,8 @@ Mosaic buttons are available using native `