aio: add h1 title to floating table of contents (#16959)

* refactor(aio): use explicit CSS class for TOC container

This makes the styling less fragile to changes in the HTML

* fix(aio): schedule TocComponent.activeIndex updates via AsapScheduler

We use the `asap` scheduler because updates to `activeItemIndex` are triggered by DOM changes,
which, in turn, are caused by the rendering that happened due to a ChangeDetection.

Without asap, we would be updating the model while still in a ChangeDetection handler,
which is disallowed by Angular.

* refactor(aio): do not instantiate floating ToC if not displayed

* feat(aio): display the h1 at the top of the floating TOC

Closes #16900

* refactor(aio): combine the TOC booleans flags into a "type" state

* refactor(aio): remove unnecessary `hostElement` property

* fix(aio): ensure that transition works on TOC

* fix(aio): use strict equality in ToC template
This commit is contained in:
Pete Bacon Darwin 2017-05-24 21:09:55 +01:00 committed by Chuck Jazdzewski
parent b0c5018c70
commit 966eb2fbd0
9 changed files with 164 additions and 156 deletions

View File

@ -35,9 +35,7 @@
</md-sidenav-container>
<div class="toc-container" [style.max-height.px]="tocMaxHeight">
<div *ngIf="showFloatingToc" class="toc-container" [style.max-height.px]="tocMaxHeight">
<aio-toc></aio-toc>
</div>

View File

@ -75,6 +75,13 @@ describe('AppComponent', () => {
component.onResize(500);
expect(component.isSideBySide).toBe(false);
});
it('should update `showFloatingToc` accordingly', () => {
component.onResize(801);
expect(component.showFloatingToc).toBe(true);
component.onResize(800);
expect(component.showFloatingToc).toBe(false);
});
});
describe('onScroll', () => {

View File

@ -65,8 +65,12 @@ export class AppComponent implements OnInit {
sideNavNodes: NavigationNode[];
topMenuNodes: NavigationNode[];
topMenuNarrowNodes: NavigationNode[];
showFloatingToc = false;
showFloatingTocWidth = 800;
tocMaxHeight: string;
private tocMaxHeightOffset = 0;
versionInfo: VersionInfo;
get homeImageUrl() {
@ -198,6 +202,7 @@ export class AppComponent implements OnInit {
@HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width) {
this.isSideBySide = width > this.sideBySideWidth;
this.showFloatingToc = width > this.showFloatingTocWidth;
}
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])

View File

@ -1,11 +1,11 @@
<div *ngIf="hasToc" [class.collapsed]="isCollapsed">
<button *ngIf="!isEmbedded" type="button" class="toc-heading"
(click)="toTop()" title="Top of page">Contents</button>
<div *ngIf="type !== 'None'" class="toc-inner" [class.collapsed]="isCollapsed">
<div *ngIf="!hasSecondary && isEmbedded" class="toc-heading embedded">Contents</div>
<div *ngIf="type === 'EmbeddedSimple'" class="toc-heading embedded">
Contents
</div>
<button *ngIf="hasSecondary" type="button" class="toc-heading embedded secondary"
(click)="toggle(false)"
<button *ngIf="type === 'EmbeddedExpandable'" type="button" (click)="toggle(false)"
class="toc-heading embedded secondary"
title="Expand/collapse contents"
aria-label="Expand/collapse contents"
[attr.aria-pressed]="!isCollapsed">
@ -14,13 +14,15 @@
</button>
<ul class="toc-list">
<li #tocItem *ngFor="let toc of tocList; let i = index" title="{{toc.title}}"
class="{{toc.level}}" [class.secondary]="toc.isSecondary" [class.active]="i === activeIndex">
<a [href]="toc.href" [innerHTML]="toc.content"></a>
</li>
<ng-container *ngFor="let toc of tocList; let i = index">
<li #tocItem title="{{toc.title}}" *ngIf="type === 'Floating' || toc.level !== 'h1'"
class="{{toc.level}}" [class.secondary]="type === 'EmbeddedExpandable' && i >= primaryMax" [class.active]="i === activeIndex">
<a [href]="toc.href" [innerHTML]="toc.content"></a>
</li>
</ng-container>
</ul>
<button type="button" (click)="toggle()" *ngIf="hasSecondary"
<button *ngIf="type === 'EmbeddedExpandable'" type="button" (click)="toggle()"
class="toc-more-items embedded material-icons" [class.collapsed]="isCollapsed"
title="Expand/collapse contents"
aria-label="Expand/collapse contents"

View File

@ -1,7 +1,8 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By, DOCUMENT } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { asap } from 'rxjs/scheduler/asap';
import { ScrollService } from 'app/shared/scroll.service';
import { TocComponent } from './toc.component';
@ -16,7 +17,7 @@ describe('TocComponent', () => {
listItems: DebugElement[];
tocHeading: DebugElement;
tocHeadingButtonEmbedded: DebugElement;
tocHeadingButtonSide: DebugElement;
tocH1Heading: DebugElement;
tocMoreButton: DebugElement;
};
@ -25,7 +26,7 @@ describe('TocComponent', () => {
listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')),
tocHeading: tocComponentDe.query(By.css('.toc-heading')),
tocHeadingButtonEmbedded: tocComponentDe.query(By.css('button.toc-heading.embedded')),
tocHeadingButtonSide: tocComponentDe.query(By.css('button.toc-heading:not(.embedded)')),
tocH1Heading: tocComponentDe.query(By.css('.h1')),
tocMoreButton: tocComponentDe.query(By.css('button.toc-more-items')),
};
}
@ -60,40 +61,51 @@ describe('TocComponent', () => {
});
it('should not display a ToC initially', () => {
expect(tocComponent.hasToc).toBe(false);
expect(tocComponent.type).toEqual('None');
});
it('should not display anything when no TocItems', () => {
tocService.tocList.next([]);
it('should not display anything when no h2 or h3 TocItems', () => {
tocService.tocList.next([tocItem('H1', 'h1')]);
fixture.detectChanges();
expect(tocComponentDe.children.length).toEqual(0);
});
it('should update when the TocItems are updated', () => {
tocService.tocList.next([{}] as TocItem[]);
tocService.tocList.next([tocItem('Heading A')]);
fixture.detectChanges();
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
tocService.tocList.next([{}, {}, {}] as TocItem[]);
tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C')]);
fixture.detectChanges();
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(3);
});
it('should only display H2 and H3 TocItems', () => {
tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C', 'h3')]);
fixture.detectChanges();
const items = tocComponentDe.queryAllNodes(By.css('li'));
expect(items.length).toBe(2);
expect(items.find(item => item.nativeNode.innerText === 'Heading A')).toBeFalsy();
expect(items.find(item => item.nativeNode.innerText === 'Heading B')).toBeTruthy();
expect(items.find(item => item.nativeNode.innerText === 'Heading C')).toBeTruthy();
expect(setPage().tocH1Heading).toBeFalsy();
});
it('should stop listening for TocItems once destroyed', () => {
tocService.tocList.next([{}] as TocItem[]);
tocService.tocList.next([tocItem('Heading A')]);
fixture.detectChanges();
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
tocComponent.ngOnDestroy();
tocService.tocList.next([{}, {}, {}] as TocItem[]);
tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C')]);
fixture.detectChanges();
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
});
describe('when four TocItems', () => {
describe('when fewer than `maxPrimary` TocItems', () => {
beforeEach(() => {
tocService.tocList.next([{}, {}, {}, {}] as TocItem[]);
tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C'), tocItem('Heading D')]);
fixture.detectChanges();
page = setPage();
});
@ -103,7 +115,7 @@ describe('TocComponent', () => {
});
it('should not have secondary items', () => {
expect(tocComponent.hasSecondary).toEqual(false, 'hasSecondary flag');
expect(tocComponent.type).toEqual('EmbeddedSimple');
const aSecond = page.listItems.find(item => item.classes.secondary);
expect(aSecond).toBeFalsy('should not find a secondary');
});
@ -128,7 +140,10 @@ describe('TocComponent', () => {
tocService.tocList.subscribe(v => tocList = v);
expect(page.listItems.length).toBeGreaterThan(4);
expect(page.listItems.length).toEqual(tocList.length);
});
it('should not display the h1 item', () => {
expect(page.listItems.find(item => item.classes.h1)).toBeFalsy('should not find h1 item');
});
it('should be in "collapsed" (not expanded) state at the start', () => {
@ -145,14 +160,13 @@ describe('TocComponent', () => {
});
it('should have secondary items', () => {
expect(tocComponent.hasSecondary).toEqual(true, 'hasSecondary flag');
expect(tocComponent.type).toEqual('EmbeddedExpandable');
});
// CSS should hide items with the secondary class when collapsed
// CSS will hide items with the secondary class when collapsed
it('should have secondary item with a secondary class', () => {
const aSecondary = page.listItems.find(item => item.classes.secondary);
expect(aSecondary).toBeTruthy('should find a secondary');
expect(aSecondary.classes.secondary).toEqual(true, 'has secondary class');
});
describe('after click tocHeading button', () => {
@ -245,17 +259,15 @@ describe('TocComponent', () => {
it('should not be in embedded state', () => {
expect(tocComponent.isEmbedded).toEqual(false);
expect(tocComponent.type).toEqual('Floating');
});
it('should display all items', () => {
let tocList: TocItem[];
tocService.tocList.subscribe(v => tocList = v);
expect(page.listItems.length).toEqual(tocList.length);
it('should display all items (including h1s)', () => {
expect(page.listItems.length).toEqual(getTestTocList().length);
});
it('should not have secondary items', () => {
expect(tocComponent.hasSecondary).toEqual(false, 'hasSecondary flag');
expect(tocComponent.type).toEqual('Floating');
const aSecond = page.listItems.find(item => item.classes.secondary);
expect(aSecond).toBeFalsy('should not find a secondary');
});
@ -265,37 +277,31 @@ describe('TocComponent', () => {
expect(page.tocMoreButton).toBeFalsy('bottom more button');
});
it('should display "Contents" button', () => {
expect(page.tocHeadingButtonSide).toBeTruthy();
});
it('should scroll to top when "Contents" button clicked', () => {
page.tocHeadingButtonSide.nativeElement.click();
fixture.detectChanges();
expect(scrollToTopSpy).toHaveBeenCalled();
it('should display H1 title', () => {
expect(page.tocH1Heading).toBeTruthy();
});
describe('#activeIndex', () => {
it('should keep track of `TocService`\'s `activeItemIndex`', () => {
expect(tocComponent.activeIndex).toBeNull();
tocService.activeItemIndex.next(42);
tocService.setActiveIndex(42);
expect(tocComponent.activeIndex).toBe(42);
tocService.activeItemIndex.next(null);
tocService.setActiveIndex(null);
expect(tocComponent.activeIndex).toBeNull();
});
it('should stop tracking `activeItemIndex` once destroyed', () => {
tocService.activeItemIndex.next(42);
tocService.setActiveIndex(42);
expect(tocComponent.activeIndex).toBe(42);
tocComponent.ngOnDestroy();
tocService.activeItemIndex.next(43);
tocService.setActiveIndex(43);
expect(tocComponent.activeIndex).toBe(42);
tocService.activeItemIndex.next(null);
tocService.setActiveIndex(null);
expect(tocComponent.activeIndex).toBe(42);
});
@ -334,19 +340,19 @@ describe('TocComponent', () => {
tocComponent.activeIndex = 1;
fixture.detectChanges();
expect(getActiveTextContent()).toBe('H2 Two');
expect(getActiveTextContent()).toBe('Heading one');
tocComponent.tocList = [{content: 'New 1'}, {content: 'New 2'}] as any as TocItem[];
tocComponent.tocList = [tocItem('New 1'), tocItem('New 2')];
fixture.detectChanges();
page = setPage();
expect(getActiveTextContent()).toBe('New 2');
tocComponent.tocList.unshift({content: 'New 0'} as any as TocItem);
tocComponent.tocList.unshift(tocItem('New 0'));
fixture.detectChanges();
page = setPage();
expect(getActiveTextContent()).toBe('New 1');
tocComponent.tocList = [{content: 'Very New 1'}] as any as TocItem[];
tocComponent.tocList = [tocItem('Very New 1')];
fixture.detectChanges();
page = setPage();
expect(page.listItems.findIndex(By.css('.active'))).toBe(-1);
@ -373,17 +379,17 @@ describe('TocComponent', () => {
});
it('when the `activeIndex` changes', () => {
tocService.activeItemIndex.next(0);
tocService.setActiveIndex(0);
fixture.detectChanges();
expect(parentScrollTop).toBe(0);
tocService.activeItemIndex.next(1);
tocService.setActiveIndex(1);
fixture.detectChanges();
expect(parentScrollTop).toBe(0);
tocService.activeItemIndex.next(page.listItems.length - 1);
tocService.setActiveIndex(page.listItems.length - 1);
fixture.detectChanges();
expect(parentScrollTop).toBeGreaterThan(0);
@ -397,7 +403,7 @@ describe('TocComponent', () => {
expect(parentScrollTop).toBe(0);
tocService.activeItemIndex.next(tocList.length - 1);
tocService.setActiveIndex(tocList.length - 1);
fixture.detectChanges();
expect(parentScrollTop).toBe(0);
@ -412,7 +418,7 @@ describe('TocComponent', () => {
const tocList = tocComponent.tocList;
tocComponent.ngOnDestroy();
tocService.activeItemIndex.next(page.listItems.length - 1);
tocService.setActiveIndex(page.listItems.length - 1);
fixture.detectChanges();
expect(parentScrollTop).toBe(0);
@ -453,46 +459,26 @@ class TestScrollService {
class TestTocService {
tocList = new BehaviorSubject<TocItem[]>(getTestTocList());
activeItemIndex = new BehaviorSubject<number | null>(null);
setActiveIndex(index) {
this.activeItemIndex.next(index);
if (asap.scheduled) {
asap.flush();
}
}
}
function tocItem(title: string, level = 'h2', href = '', content = title) {
return { title, href, level, content };
}
// tslint:disable:quotemark
function getTestTocList() {
return [
{
"content": "Heading one",
"href": "fizz/buzz#heading-one-special-id",
"level": "h2",
"title": "Heading one"
},
{
"content": "H2 Two",
"href": "fizz/buzz#h2-two",
"level": "h2",
"title": "H2 Two"
},
{
"content": "H2 <b>Three</b>",
"href": "fizz/buzz#h2-three",
"level": "h2",
"title": "H2 Three"
},
{
"content": "H3 3a",
"href": "fizz/buzz#h3-3a",
"level": "h3",
"title": "H3 3a"
},
{
"content": "H3 3b",
"href": "fizz/buzz#h3-3b",
"level": "h3",
"title": "H3 3b"
},
{
"content": "<i>H2 <b>four</b></i>",
"href": "fizz/buzz#h2-four",
"level": "h2",
"title": "H2 4"
}
tocItem('Title', 'h1', 'fizz/buzz#title', 'Title'),
tocItem('Heading one', 'h2', 'fizz/buzz#heading-one-special-id', 'Heading one'),
tocItem('H2 Two', 'h2', 'fizz/buzz#h2-two', 'H2 Two'),
tocItem('H2 Three', 'h2', 'fizz/buzz#h2-three', 'H2 <b>Three</b>'),
tocItem('H3 3a', 'h3', 'fizz/buzz#h3-3a', 'H3 3a'),
tocItem('H3 3b', 'h3', 'fizz/buzz#h3-3b', 'H3 3b'),
tocItem('H2 4', 'h2', 'fizz/buzz#h2-four', '<i>H2 <b>four</b></i>'),
];
}

View File

@ -1,12 +1,16 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { asap } from 'rxjs/scheduler/asap';
import 'rxjs/add/observable/combineLatest';
import 'rxjs/add/operator/subscribeOn';
import 'rxjs/add/operator/takeUntil';
import { ScrollService } from 'app/shared/scroll.service';
import { TocItem, TocService } from 'app/shared/toc.service';
type TocType = 'None' | 'Floating' | 'EmbeddedSimple' | 'EmbeddedExpandable';
@Component({
selector: 'aio-toc',
templateUrl: 'toc.component.html',
@ -15,9 +19,7 @@ import { TocItem, TocService } from 'app/shared/toc.service';
export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
activeIndex: number | null = null;
hasSecondary = false;
hasToc = false;
hostElement: HTMLElement;
type: TocType = 'None';
isCollapsed = true;
isEmbedded = false;
@ViewChildren('tocItem') private items: QueryList<ElementRef>;
@ -29,37 +31,35 @@ export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
private scrollService: ScrollService,
elementRef: ElementRef,
private tocService: TocService) {
this.hostElement = elementRef.nativeElement;
this.isEmbedded = this.hostElement.className.indexOf('embedded') !== -1;
this.isEmbedded = elementRef.nativeElement.className.indexOf('embedded') !== -1;
}
ngOnInit() {
this.tocService.tocList
.takeUntil(this.onDestroy)
.subscribe(tocList => {
const count = tocList.length;
this.hasToc = count > 0;
this.hasSecondary = this.isEmbedded && this.hasToc && (count > this.primaryMax);
this.tocList = tocList;
const itemCount = count(this.tocList, item => item.level !== 'h1');
if (this.hasSecondary) {
for (let i = this.primaryMax; i < count; i++) {
tocList[i].isSecondary = true;
}
}
this.type = (itemCount > 0) ?
this.isEmbedded ?
(itemCount > this.primaryMax) ?
'EmbeddedExpandable' :
'EmbeddedSimple' :
'Floating' :
'None';
});
}
ngAfterViewInit() {
if (!this.isEmbedded) {
this.tocService.activeItemIndex
.takeUntil(this.onDestroy)
.subscribe(index => this.activeIndex = index);
Observable.combineLatest(this.tocService.activeItemIndex, this.items.changes.startWith(this.items))
// We use the `asap` scheduler because updates to `activeItemIndex` are triggered by DOM changes,
// which, in turn, are caused by the rendering that happened due to a ChangeDetection.
// Without asap, we would be updating the model while still in a ChangeDetection handler, which is disallowed by Angular.
Observable.combineLatest(this.tocService.activeItemIndex.subscribeOn(asap), this.items.changes.startWith(this.items))
.takeUntil(this.onDestroy)
.subscribe(([index, items]) => {
this.activeIndex = index;
if (index === null || index >= items.length) {
return;
}
@ -92,3 +92,7 @@ export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
this.scrollService.scrollToTop();
}
}
function count<T>(array: T[], fn: (T) => boolean) {
return array.reduce((count, item) => fn(item) ? count + 1 : count, 0);
}

View File

@ -37,28 +37,28 @@ describe('TocService', () => {
describe('tocList', () => {
it('should emit the latest value to new subscribers', () => {
const expectedValue1 = {} as TocItem;
const expectedValue2 = {} as TocItem;
const expectedValue1 = tocItem('Heading A');
const expectedValue2 = tocItem('Heading B');
let value1: TocItem[];
let value2: TocItem[];
tocService.tocList.next([] as TocItem[]);
tocService.tocList.next([]);
tocService.tocList.subscribe(v => value1 = v);
expect(value1).toEqual([]);
tocService.tocList.next([expectedValue1, expectedValue2] as TocItem[]);
tocService.tocList.next([expectedValue1, expectedValue2]);
tocService.tocList.subscribe(v => value2 = v);
expect(value2).toEqual([expectedValue1, expectedValue2]);
});
it('should emit the same values to all subscribers', () => {
const expectedValue1 = {} as TocItem;
const expectedValue2 = {} as TocItem;
const expectedValue1 = tocItem('Heading A');
const expectedValue2 = tocItem('Heading B');
const emittedValues: TocItem[][] = [];
tocService.tocList.subscribe(v => emittedValues.push(v));
tocService.tocList.subscribe(v => emittedValues.push(v));
tocService.tocList.next([expectedValue1, expectedValue2] as TocItem[]);
tocService.tocList.next([expectedValue1, expectedValue2]);
expect(emittedValues).toEqual([
[expectedValue1, expectedValue2],
@ -153,7 +153,9 @@ describe('TocService', () => {
describe('should clear tocList', () => {
beforeEach(() => {
// Start w/ dummy data from previous usage
tocService.tocList.next([{}, {}] as TocItem[]);
const expectedValue1 = tocItem('Heading A');
const expectedValue2 = tocItem('Heading B');
tocService.tocList.next([expectedValue1, expectedValue2]);
expect(lastTocList).not.toEqual([]);
});
@ -172,8 +174,8 @@ describe('TocService', () => {
expect(lastTocList).toEqual([]);
});
it('when given doc element w/ headings other than h2 & h3', () => {
callGenToc('<h1>This</h1><h4>and</h4><h5>that</h5>');
it('when given doc element w/ headings other than h1, h2 & h3', () => {
callGenToc('<h4>and</h4><h5>that</h5>');
expect(lastTocList).toEqual([]);
});
@ -232,60 +234,60 @@ describe('TocService', () => {
});
it('should have tocList with expect number of TocItems', () => {
// should ignore h1, h4, and the no-toc h2
expect(lastTocList.length).toEqual(headings.length - 3);
// should ignore h4, and the no-toc h2
expect(lastTocList.length).toEqual(headings.length - 2);
});
it('should have href with docId and heading\'s id', () => {
const tocItem = lastTocList[0];
const tocItem = lastTocList.find(item => item.title === 'Heading one');
expect(tocItem.href).toEqual(`${docId}#heading-one-special-id`);
});
it('should have level "h2" for an <h2>', () => {
const tocItem = lastTocList[0];
const tocItem = lastTocList.find(item => item.title === 'Heading one');
expect(tocItem.level).toEqual('h2');
});
it('should have level "h3" for an <h3>', () => {
const tocItem = lastTocList[3];
const tocItem = lastTocList.find(item => item.title === 'H3 3a');
expect(tocItem.level).toEqual('h3');
});
it('should have title which is heading\'s innerText ', () => {
const heading = headings[3];
const tocItem = lastTocList[2];
const tocItem = lastTocList[3];
expect(heading.innerText).toEqual(tocItem.title);
});
it('should have "SafeHtml" content which is heading\'s innerHTML ', () => {
const heading = headings[3];
const content = lastTocList[2].content;
const content = lastTocList[3].content;
expect((<TestSafeHtml>content).changingThisBreaksApplicationSecurity)
.toEqual(heading.innerHTML);
});
it('should calculate and set id of heading without an id', () => {
const tocItem = lastTocList.find(item => item.title === 'H2 Two');
const id = headings[2].getAttribute('id');
expect(id).toEqual('h2-two');
});
it('should have href with docId and calculated heading id', () => {
const tocItem = lastTocList[1];
const tocItem = lastTocList.find(item => item.title === 'H2 Two');
expect(tocItem.href).toEqual(`${docId}#h2-two`);
});
it('should ignore HTML in heading when calculating id', () => {
const id = headings[3].getAttribute('id');
const tocItem = lastTocList[2];
const tocItem = lastTocList[3];
expect(id).toEqual('h2-three', 'heading id');
expect(tocItem.href).toEqual(`${docId}#h2-three`, 'tocItem href');
});
it('should avoid repeating an id when calculating', () => {
const tocItem4a = lastTocList[5];
const tocItem4b = lastTocList[6];
expect(tocItem4a.href).toEqual(`${docId}#h2-4-repeat`, 'first');
expect(tocItem4b.href).toEqual(`${docId}#h2-4-repeat-2`, 'second');
const tocItems = lastTocList.filter(item => item.title === 'H2 4 repeat');
expect(tocItems[0].href).toEqual(`${docId}#h2-4-repeat`, 'first');
expect(tocItems[1].href).toEqual(`${docId}#h2-4-repeat-2`, 'second');
});
});
@ -363,3 +365,7 @@ class MockScrollSpyService {
}
}
function tocItem(title: string, level = 'h2', href = '', content = title) {
return { title, href, level, content };
}

View File

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service';
@ -66,7 +65,7 @@ export class TocService {
}
private findTocHeadings(docElement: Element): HTMLHeadingElement[] {
const headings = docElement.querySelectorAll('h2,h3');
const headings = docElement.querySelectorAll('h1,h2,h3');
const skipNoTocHeadings = (heading: HTMLHeadingElement) => !/(?:no-toc|notoc)/i.test(heading.className);
return Array.prototype.filter.call(headings, skipNoTocHeadings);

View File

@ -6,11 +6,6 @@
bottom: 32px;
overflow-y: auto;
overflow-x: hidden;
@media (max-width: 800px) {
display: none;
width: 0;
}
}
aio-toc {
@ -19,16 +14,9 @@ aio-toc {
display: none;
}
}
&:not(.embedded) {
@media (max-width: 800px) {
display: none;
}
}
}
aio-toc > div {
.toc-inner {
border-left: 4px solid #4285f4;
font-size: 13px;
overflow-y: visible;
@ -59,6 +47,7 @@ aio-toc > div {
border: none;
box-shadow: none;
padding: 0;
text-align: start;
&:focus.embedded {
outline: none;
@ -67,7 +56,8 @@ aio-toc > div {
}
button.toc-heading,
div.toc-heading {
div.toc-heading,
ul.toc-list li.h1 {
font-size: 115%;
}
@ -131,10 +121,10 @@ aio-toc > div {
margin: 6px 0;
padding: 0;
position: relative;
transition: all 0.3 ease-in-out;
transition: all 0.3s ease-in-out;
a {
font-size: 12px;
font-size: inherit;
color: lighten($darkgray, 10);
display:table-cell;
overflow: visible;
@ -154,6 +144,17 @@ aio-toc > div {
ul.toc-list li.h3 {
margin-left: 15px;
}
ul.toc-list li.h1:after {
content: "";
display: block;
height: 1px;
width: 40%;
margin: 24px 0px 10px;
background: #DBDBDB;
clear: both;
}
}
aio-toc.embedded > div.collapsed li.secondary {