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:
parent
b0c5018c70
commit
966eb2fbd0
|
@ -35,9 +35,7 @@
|
||||||
|
|
||||||
</md-sidenav-container>
|
</md-sidenav-container>
|
||||||
|
|
||||||
|
<div *ngIf="showFloatingToc" class="toc-container" [style.max-height.px]="tocMaxHeight">
|
||||||
|
|
||||||
<div class="toc-container" [style.max-height.px]="tocMaxHeight">
|
|
||||||
<aio-toc></aio-toc>
|
<aio-toc></aio-toc>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,13 @@ describe('AppComponent', () => {
|
||||||
component.onResize(500);
|
component.onResize(500);
|
||||||
expect(component.isSideBySide).toBe(false);
|
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', () => {
|
describe('onScroll', () => {
|
||||||
|
|
|
@ -65,8 +65,12 @@ export class AppComponent implements OnInit {
|
||||||
sideNavNodes: NavigationNode[];
|
sideNavNodes: NavigationNode[];
|
||||||
topMenuNodes: NavigationNode[];
|
topMenuNodes: NavigationNode[];
|
||||||
topMenuNarrowNodes: NavigationNode[];
|
topMenuNarrowNodes: NavigationNode[];
|
||||||
|
|
||||||
|
showFloatingToc = false;
|
||||||
|
showFloatingTocWidth = 800;
|
||||||
tocMaxHeight: string;
|
tocMaxHeight: string;
|
||||||
private tocMaxHeightOffset = 0;
|
private tocMaxHeightOffset = 0;
|
||||||
|
|
||||||
versionInfo: VersionInfo;
|
versionInfo: VersionInfo;
|
||||||
|
|
||||||
get homeImageUrl() {
|
get homeImageUrl() {
|
||||||
|
@ -198,6 +202,7 @@ export class AppComponent implements OnInit {
|
||||||
@HostListener('window:resize', ['$event.target.innerWidth'])
|
@HostListener('window:resize', ['$event.target.innerWidth'])
|
||||||
onResize(width) {
|
onResize(width) {
|
||||||
this.isSideBySide = width > this.sideBySideWidth;
|
this.isSideBySide = width > this.sideBySideWidth;
|
||||||
|
this.showFloatingToc = width > this.showFloatingTocWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
|
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<div *ngIf="hasToc" [class.collapsed]="isCollapsed">
|
<div *ngIf="type !== 'None'" class="toc-inner" [class.collapsed]="isCollapsed">
|
||||||
<button *ngIf="!isEmbedded" type="button" class="toc-heading"
|
|
||||||
(click)="toTop()" title="Top of page">Contents</button>
|
|
||||||
|
|
||||||
<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"
|
<button *ngIf="type === 'EmbeddedExpandable'" type="button" (click)="toggle(false)"
|
||||||
(click)="toggle(false)"
|
class="toc-heading embedded secondary"
|
||||||
title="Expand/collapse contents"
|
title="Expand/collapse contents"
|
||||||
aria-label="Expand/collapse contents"
|
aria-label="Expand/collapse contents"
|
||||||
[attr.aria-pressed]="!isCollapsed">
|
[attr.aria-pressed]="!isCollapsed">
|
||||||
|
@ -14,13 +14,15 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="toc-list">
|
<ul class="toc-list">
|
||||||
<li #tocItem *ngFor="let toc of tocList; let i = index" title="{{toc.title}}"
|
<ng-container *ngFor="let toc of tocList; let i = index">
|
||||||
class="{{toc.level}}" [class.secondary]="toc.isSecondary" [class.active]="i === activeIndex">
|
<li #tocItem title="{{toc.title}}" *ngIf="type === 'Floating' || toc.level !== 'h1'"
|
||||||
<a [href]="toc.href" [innerHTML]="toc.content"></a>
|
class="{{toc.level}}" [class.secondary]="type === 'EmbeddedExpandable' && i >= primaryMax" [class.active]="i === activeIndex">
|
||||||
</li>
|
<a [href]="toc.href" [innerHTML]="toc.content"></a>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
</ul>
|
</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"
|
class="toc-more-items embedded material-icons" [class.collapsed]="isCollapsed"
|
||||||
title="Expand/collapse contents"
|
title="Expand/collapse contents"
|
||||||
aria-label="Expand/collapse contents"
|
aria-label="Expand/collapse contents"
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
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 { By, DOCUMENT } from '@angular/platform-browser';
|
||||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
import { asap } from 'rxjs/scheduler/asap';
|
||||||
|
|
||||||
import { ScrollService } from 'app/shared/scroll.service';
|
import { ScrollService } from 'app/shared/scroll.service';
|
||||||
import { TocComponent } from './toc.component';
|
import { TocComponent } from './toc.component';
|
||||||
|
@ -16,7 +17,7 @@ describe('TocComponent', () => {
|
||||||
listItems: DebugElement[];
|
listItems: DebugElement[];
|
||||||
tocHeading: DebugElement;
|
tocHeading: DebugElement;
|
||||||
tocHeadingButtonEmbedded: DebugElement;
|
tocHeadingButtonEmbedded: DebugElement;
|
||||||
tocHeadingButtonSide: DebugElement;
|
tocH1Heading: DebugElement;
|
||||||
tocMoreButton: DebugElement;
|
tocMoreButton: DebugElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ describe('TocComponent', () => {
|
||||||
listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')),
|
listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')),
|
||||||
tocHeading: tocComponentDe.query(By.css('.toc-heading')),
|
tocHeading: tocComponentDe.query(By.css('.toc-heading')),
|
||||||
tocHeadingButtonEmbedded: tocComponentDe.query(By.css('button.toc-heading.embedded')),
|
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')),
|
tocMoreButton: tocComponentDe.query(By.css('button.toc-more-items')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -60,40 +61,51 @@ describe('TocComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display a ToC initially', () => {
|
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', () => {
|
it('should not display anything when no h2 or h3 TocItems', () => {
|
||||||
tocService.tocList.next([]);
|
tocService.tocList.next([tocItem('H1', 'h1')]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(tocComponentDe.children.length).toEqual(0);
|
expect(tocComponentDe.children.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update when the TocItems are updated', () => {
|
it('should update when the TocItems are updated', () => {
|
||||||
tocService.tocList.next([{}] as TocItem[]);
|
tocService.tocList.next([tocItem('Heading A')]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
|
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();
|
fixture.detectChanges();
|
||||||
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(3);
|
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', () => {
|
it('should stop listening for TocItems once destroyed', () => {
|
||||||
tocService.tocList.next([{}] as TocItem[]);
|
tocService.tocList.next([tocItem('Heading A')]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
|
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
|
||||||
|
|
||||||
tocComponent.ngOnDestroy();
|
tocComponent.ngOnDestroy();
|
||||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
tocService.tocList.next([tocItem('Heading A', 'h1'), tocItem('Heading B'), tocItem('Heading C')]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
|
expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when four TocItems', () => {
|
describe('when fewer than `maxPrimary` TocItems', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tocService.tocList.next([{}, {}, {}, {}] as TocItem[]);
|
tocService.tocList.next([tocItem('Heading A'), tocItem('Heading B'), tocItem('Heading C'), tocItem('Heading D')]);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
page = setPage();
|
page = setPage();
|
||||||
});
|
});
|
||||||
|
@ -103,7 +115,7 @@ describe('TocComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have secondary items', () => {
|
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);
|
const aSecond = page.listItems.find(item => item.classes.secondary);
|
||||||
expect(aSecond).toBeFalsy('should not find a secondary');
|
expect(aSecond).toBeFalsy('should not find a secondary');
|
||||||
});
|
});
|
||||||
|
@ -128,7 +140,10 @@ describe('TocComponent', () => {
|
||||||
tocService.tocList.subscribe(v => tocList = v);
|
tocService.tocList.subscribe(v => tocList = v);
|
||||||
|
|
||||||
expect(page.listItems.length).toBeGreaterThan(4);
|
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', () => {
|
it('should be in "collapsed" (not expanded) state at the start', () => {
|
||||||
|
@ -145,14 +160,13 @@ describe('TocComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have secondary items', () => {
|
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', () => {
|
it('should have secondary item with a secondary class', () => {
|
||||||
const aSecondary = page.listItems.find(item => item.classes.secondary);
|
const aSecondary = page.listItems.find(item => item.classes.secondary);
|
||||||
expect(aSecondary).toBeTruthy('should find a secondary');
|
expect(aSecondary).toBeTruthy('should find a secondary');
|
||||||
expect(aSecondary.classes.secondary).toEqual(true, 'has secondary class');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after click tocHeading button', () => {
|
describe('after click tocHeading button', () => {
|
||||||
|
@ -245,17 +259,15 @@ describe('TocComponent', () => {
|
||||||
|
|
||||||
it('should not be in embedded state', () => {
|
it('should not be in embedded state', () => {
|
||||||
expect(tocComponent.isEmbedded).toEqual(false);
|
expect(tocComponent.isEmbedded).toEqual(false);
|
||||||
|
expect(tocComponent.type).toEqual('Floating');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display all items', () => {
|
it('should display all items (including h1s)', () => {
|
||||||
let tocList: TocItem[];
|
expect(page.listItems.length).toEqual(getTestTocList().length);
|
||||||
tocService.tocList.subscribe(v => tocList = v);
|
|
||||||
|
|
||||||
expect(page.listItems.length).toEqual(tocList.length);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have secondary items', () => {
|
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);
|
const aSecond = page.listItems.find(item => item.classes.secondary);
|
||||||
expect(aSecond).toBeFalsy('should not find a secondary');
|
expect(aSecond).toBeFalsy('should not find a secondary');
|
||||||
});
|
});
|
||||||
|
@ -265,37 +277,31 @@ describe('TocComponent', () => {
|
||||||
expect(page.tocMoreButton).toBeFalsy('bottom more button');
|
expect(page.tocMoreButton).toBeFalsy('bottom more button');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display "Contents" button', () => {
|
it('should display H1 title', () => {
|
||||||
expect(page.tocHeadingButtonSide).toBeTruthy();
|
expect(page.tocH1Heading).toBeTruthy();
|
||||||
});
|
|
||||||
|
|
||||||
it('should scroll to top when "Contents" button clicked', () => {
|
|
||||||
page.tocHeadingButtonSide.nativeElement.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
expect(scrollToTopSpy).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#activeIndex', () => {
|
describe('#activeIndex', () => {
|
||||||
it('should keep track of `TocService`\'s `activeItemIndex`', () => {
|
it('should keep track of `TocService`\'s `activeItemIndex`', () => {
|
||||||
expect(tocComponent.activeIndex).toBeNull();
|
expect(tocComponent.activeIndex).toBeNull();
|
||||||
|
|
||||||
tocService.activeItemIndex.next(42);
|
tocService.setActiveIndex(42);
|
||||||
expect(tocComponent.activeIndex).toBe(42);
|
expect(tocComponent.activeIndex).toBe(42);
|
||||||
|
|
||||||
tocService.activeItemIndex.next(null);
|
tocService.setActiveIndex(null);
|
||||||
expect(tocComponent.activeIndex).toBeNull();
|
expect(tocComponent.activeIndex).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop tracking `activeItemIndex` once destroyed', () => {
|
it('should stop tracking `activeItemIndex` once destroyed', () => {
|
||||||
tocService.activeItemIndex.next(42);
|
tocService.setActiveIndex(42);
|
||||||
expect(tocComponent.activeIndex).toBe(42);
|
expect(tocComponent.activeIndex).toBe(42);
|
||||||
|
|
||||||
tocComponent.ngOnDestroy();
|
tocComponent.ngOnDestroy();
|
||||||
|
|
||||||
tocService.activeItemIndex.next(43);
|
tocService.setActiveIndex(43);
|
||||||
expect(tocComponent.activeIndex).toBe(42);
|
expect(tocComponent.activeIndex).toBe(42);
|
||||||
|
|
||||||
tocService.activeItemIndex.next(null);
|
tocService.setActiveIndex(null);
|
||||||
expect(tocComponent.activeIndex).toBe(42);
|
expect(tocComponent.activeIndex).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -334,19 +340,19 @@ describe('TocComponent', () => {
|
||||||
|
|
||||||
tocComponent.activeIndex = 1;
|
tocComponent.activeIndex = 1;
|
||||||
fixture.detectChanges();
|
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();
|
fixture.detectChanges();
|
||||||
page = setPage();
|
page = setPage();
|
||||||
expect(getActiveTextContent()).toBe('New 2');
|
expect(getActiveTextContent()).toBe('New 2');
|
||||||
|
|
||||||
tocComponent.tocList.unshift({content: 'New 0'} as any as TocItem);
|
tocComponent.tocList.unshift(tocItem('New 0'));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
page = setPage();
|
page = setPage();
|
||||||
expect(getActiveTextContent()).toBe('New 1');
|
expect(getActiveTextContent()).toBe('New 1');
|
||||||
|
|
||||||
tocComponent.tocList = [{content: 'Very New 1'}] as any as TocItem[];
|
tocComponent.tocList = [tocItem('Very New 1')];
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
page = setPage();
|
page = setPage();
|
||||||
expect(page.listItems.findIndex(By.css('.active'))).toBe(-1);
|
expect(page.listItems.findIndex(By.css('.active'))).toBe(-1);
|
||||||
|
@ -373,17 +379,17 @@ describe('TocComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when the `activeIndex` changes', () => {
|
it('when the `activeIndex` changes', () => {
|
||||||
tocService.activeItemIndex.next(0);
|
tocService.setActiveIndex(0);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(parentScrollTop).toBe(0);
|
expect(parentScrollTop).toBe(0);
|
||||||
|
|
||||||
tocService.activeItemIndex.next(1);
|
tocService.setActiveIndex(1);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(parentScrollTop).toBe(0);
|
expect(parentScrollTop).toBe(0);
|
||||||
|
|
||||||
tocService.activeItemIndex.next(page.listItems.length - 1);
|
tocService.setActiveIndex(page.listItems.length - 1);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(parentScrollTop).toBeGreaterThan(0);
|
expect(parentScrollTop).toBeGreaterThan(0);
|
||||||
|
@ -397,7 +403,7 @@ describe('TocComponent', () => {
|
||||||
|
|
||||||
expect(parentScrollTop).toBe(0);
|
expect(parentScrollTop).toBe(0);
|
||||||
|
|
||||||
tocService.activeItemIndex.next(tocList.length - 1);
|
tocService.setActiveIndex(tocList.length - 1);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(parentScrollTop).toBe(0);
|
expect(parentScrollTop).toBe(0);
|
||||||
|
@ -412,7 +418,7 @@ describe('TocComponent', () => {
|
||||||
const tocList = tocComponent.tocList;
|
const tocList = tocComponent.tocList;
|
||||||
tocComponent.ngOnDestroy();
|
tocComponent.ngOnDestroy();
|
||||||
|
|
||||||
tocService.activeItemIndex.next(page.listItems.length - 1);
|
tocService.setActiveIndex(page.listItems.length - 1);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(parentScrollTop).toBe(0);
|
expect(parentScrollTop).toBe(0);
|
||||||
|
@ -453,46 +459,26 @@ class TestScrollService {
|
||||||
class TestTocService {
|
class TestTocService {
|
||||||
tocList = new BehaviorSubject<TocItem[]>(getTestTocList());
|
tocList = new BehaviorSubject<TocItem[]>(getTestTocList());
|
||||||
activeItemIndex = new BehaviorSubject<number | null>(null);
|
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() {
|
function getTestTocList() {
|
||||||
return [
|
return [
|
||||||
{
|
tocItem('Title', 'h1', 'fizz/buzz#title', 'Title'),
|
||||||
"content": "Heading one",
|
tocItem('Heading one', 'h2', 'fizz/buzz#heading-one-special-id', 'Heading one'),
|
||||||
"href": "fizz/buzz#heading-one-special-id",
|
tocItem('H2 Two', 'h2', 'fizz/buzz#h2-two', 'H2 Two'),
|
||||||
"level": "h2",
|
tocItem('H2 Three', 'h2', 'fizz/buzz#h2-three', 'H2 <b>Three</b>'),
|
||||||
"title": "Heading one"
|
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>'),
|
||||||
"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"
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { Subject } from 'rxjs/Subject';
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import { asap } from 'rxjs/scheduler/asap';
|
||||||
import 'rxjs/add/observable/combineLatest';
|
import 'rxjs/add/observable/combineLatest';
|
||||||
|
import 'rxjs/add/operator/subscribeOn';
|
||||||
import 'rxjs/add/operator/takeUntil';
|
import 'rxjs/add/operator/takeUntil';
|
||||||
|
|
||||||
import { ScrollService } from 'app/shared/scroll.service';
|
import { ScrollService } from 'app/shared/scroll.service';
|
||||||
import { TocItem, TocService } from 'app/shared/toc.service';
|
import { TocItem, TocService } from 'app/shared/toc.service';
|
||||||
|
|
||||||
|
type TocType = 'None' | 'Floating' | 'EmbeddedSimple' | 'EmbeddedExpandable';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-toc',
|
selector: 'aio-toc',
|
||||||
templateUrl: 'toc.component.html',
|
templateUrl: 'toc.component.html',
|
||||||
|
@ -15,9 +19,7 @@ import { TocItem, TocService } from 'app/shared/toc.service';
|
||||||
export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
activeIndex: number | null = null;
|
activeIndex: number | null = null;
|
||||||
hasSecondary = false;
|
type: TocType = 'None';
|
||||||
hasToc = false;
|
|
||||||
hostElement: HTMLElement;
|
|
||||||
isCollapsed = true;
|
isCollapsed = true;
|
||||||
isEmbedded = false;
|
isEmbedded = false;
|
||||||
@ViewChildren('tocItem') private items: QueryList<ElementRef>;
|
@ViewChildren('tocItem') private items: QueryList<ElementRef>;
|
||||||
|
@ -29,37 +31,35 @@ export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private scrollService: ScrollService,
|
private scrollService: ScrollService,
|
||||||
elementRef: ElementRef,
|
elementRef: ElementRef,
|
||||||
private tocService: TocService) {
|
private tocService: TocService) {
|
||||||
this.hostElement = elementRef.nativeElement;
|
this.isEmbedded = elementRef.nativeElement.className.indexOf('embedded') !== -1;
|
||||||
this.isEmbedded = this.hostElement.className.indexOf('embedded') !== -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.tocService.tocList
|
this.tocService.tocList
|
||||||
.takeUntil(this.onDestroy)
|
.takeUntil(this.onDestroy)
|
||||||
.subscribe(tocList => {
|
.subscribe(tocList => {
|
||||||
const count = tocList.length;
|
|
||||||
|
|
||||||
this.hasToc = count > 0;
|
|
||||||
this.hasSecondary = this.isEmbedded && this.hasToc && (count > this.primaryMax);
|
|
||||||
this.tocList = tocList;
|
this.tocList = tocList;
|
||||||
|
const itemCount = count(this.tocList, item => item.level !== 'h1');
|
||||||
|
|
||||||
if (this.hasSecondary) {
|
this.type = (itemCount > 0) ?
|
||||||
for (let i = this.primaryMax; i < count; i++) {
|
this.isEmbedded ?
|
||||||
tocList[i].isSecondary = true;
|
(itemCount > this.primaryMax) ?
|
||||||
}
|
'EmbeddedExpandable' :
|
||||||
}
|
'EmbeddedSimple' :
|
||||||
|
'Floating' :
|
||||||
|
'None';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
if (!this.isEmbedded) {
|
if (!this.isEmbedded) {
|
||||||
this.tocService.activeItemIndex
|
// We use the `asap` scheduler because updates to `activeItemIndex` are triggered by DOM changes,
|
||||||
.takeUntil(this.onDestroy)
|
// which, in turn, are caused by the rendering that happened due to a ChangeDetection.
|
||||||
.subscribe(index => this.activeIndex = index);
|
// 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))
|
||||||
Observable.combineLatest(this.tocService.activeItemIndex, this.items.changes.startWith(this.items))
|
|
||||||
.takeUntil(this.onDestroy)
|
.takeUntil(this.onDestroy)
|
||||||
.subscribe(([index, items]) => {
|
.subscribe(([index, items]) => {
|
||||||
|
this.activeIndex = index;
|
||||||
if (index === null || index >= items.length) {
|
if (index === null || index >= items.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -92,3 +92,7 @@ export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.scrollService.scrollToTop();
|
this.scrollService.scrollToTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function count<T>(array: T[], fn: (T) => boolean) {
|
||||||
|
return array.reduce((count, item) => fn(item) ? count + 1 : count, 0);
|
||||||
|
}
|
||||||
|
|
|
@ -37,28 +37,28 @@ describe('TocService', () => {
|
||||||
|
|
||||||
describe('tocList', () => {
|
describe('tocList', () => {
|
||||||
it('should emit the latest value to new subscribers', () => {
|
it('should emit the latest value to new subscribers', () => {
|
||||||
const expectedValue1 = {} as TocItem;
|
const expectedValue1 = tocItem('Heading A');
|
||||||
const expectedValue2 = {} as TocItem;
|
const expectedValue2 = tocItem('Heading B');
|
||||||
let value1: TocItem[];
|
let value1: TocItem[];
|
||||||
let value2: TocItem[];
|
let value2: TocItem[];
|
||||||
|
|
||||||
tocService.tocList.next([] as TocItem[]);
|
tocService.tocList.next([]);
|
||||||
tocService.tocList.subscribe(v => value1 = v);
|
tocService.tocList.subscribe(v => value1 = v);
|
||||||
expect(value1).toEqual([]);
|
expect(value1).toEqual([]);
|
||||||
|
|
||||||
tocService.tocList.next([expectedValue1, expectedValue2] as TocItem[]);
|
tocService.tocList.next([expectedValue1, expectedValue2]);
|
||||||
tocService.tocList.subscribe(v => value2 = v);
|
tocService.tocList.subscribe(v => value2 = v);
|
||||||
expect(value2).toEqual([expectedValue1, expectedValue2]);
|
expect(value2).toEqual([expectedValue1, expectedValue2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the same values to all subscribers', () => {
|
it('should emit the same values to all subscribers', () => {
|
||||||
const expectedValue1 = {} as TocItem;
|
const expectedValue1 = tocItem('Heading A');
|
||||||
const expectedValue2 = {} as TocItem;
|
const expectedValue2 = tocItem('Heading B');
|
||||||
const emittedValues: TocItem[][] = [];
|
const emittedValues: TocItem[][] = [];
|
||||||
|
|
||||||
tocService.tocList.subscribe(v => emittedValues.push(v));
|
tocService.tocList.subscribe(v => emittedValues.push(v));
|
||||||
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([
|
expect(emittedValues).toEqual([
|
||||||
[expectedValue1, expectedValue2],
|
[expectedValue1, expectedValue2],
|
||||||
|
@ -153,7 +153,9 @@ describe('TocService', () => {
|
||||||
describe('should clear tocList', () => {
|
describe('should clear tocList', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Start w/ dummy data from previous usage
|
// 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([]);
|
expect(lastTocList).not.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,8 +174,8 @@ describe('TocService', () => {
|
||||||
expect(lastTocList).toEqual([]);
|
expect(lastTocList).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when given doc element w/ headings other than h2 & h3', () => {
|
it('when given doc element w/ headings other than h1, h2 & h3', () => {
|
||||||
callGenToc('<h1>This</h1><h4>and</h4><h5>that</h5>');
|
callGenToc('<h4>and</h4><h5>that</h5>');
|
||||||
expect(lastTocList).toEqual([]);
|
expect(lastTocList).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -232,60 +234,60 @@ describe('TocService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have tocList with expect number of TocItems', () => {
|
it('should have tocList with expect number of TocItems', () => {
|
||||||
// should ignore h1, h4, and the no-toc h2
|
// should ignore h4, and the no-toc h2
|
||||||
expect(lastTocList.length).toEqual(headings.length - 3);
|
expect(lastTocList.length).toEqual(headings.length - 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have href with docId and heading\'s id', () => {
|
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`);
|
expect(tocItem.href).toEqual(`${docId}#heading-one-special-id`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have level "h2" for an <h2>', () => {
|
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');
|
expect(tocItem.level).toEqual('h2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have level "h3" for an <h3>', () => {
|
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');
|
expect(tocItem.level).toEqual('h3');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have title which is heading\'s innerText ', () => {
|
it('should have title which is heading\'s innerText ', () => {
|
||||||
const heading = headings[3];
|
const heading = headings[3];
|
||||||
const tocItem = lastTocList[2];
|
const tocItem = lastTocList[3];
|
||||||
expect(heading.innerText).toEqual(tocItem.title);
|
expect(heading.innerText).toEqual(tocItem.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have "SafeHtml" content which is heading\'s innerHTML ', () => {
|
it('should have "SafeHtml" content which is heading\'s innerHTML ', () => {
|
||||||
const heading = headings[3];
|
const heading = headings[3];
|
||||||
const content = lastTocList[2].content;
|
const content = lastTocList[3].content;
|
||||||
expect((<TestSafeHtml>content).changingThisBreaksApplicationSecurity)
|
expect((<TestSafeHtml>content).changingThisBreaksApplicationSecurity)
|
||||||
.toEqual(heading.innerHTML);
|
.toEqual(heading.innerHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should calculate and set id of heading without an id', () => {
|
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');
|
const id = headings[2].getAttribute('id');
|
||||||
expect(id).toEqual('h2-two');
|
expect(id).toEqual('h2-two');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have href with docId and calculated heading id', () => {
|
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`);
|
expect(tocItem.href).toEqual(`${docId}#h2-two`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore HTML in heading when calculating id', () => {
|
it('should ignore HTML in heading when calculating id', () => {
|
||||||
const id = headings[3].getAttribute('id');
|
const id = headings[3].getAttribute('id');
|
||||||
const tocItem = lastTocList[2];
|
const tocItem = lastTocList[3];
|
||||||
expect(id).toEqual('h2-three', 'heading id');
|
expect(id).toEqual('h2-three', 'heading id');
|
||||||
expect(tocItem.href).toEqual(`${docId}#h2-three`, 'tocItem href');
|
expect(tocItem.href).toEqual(`${docId}#h2-three`, 'tocItem href');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should avoid repeating an id when calculating', () => {
|
it('should avoid repeating an id when calculating', () => {
|
||||||
const tocItem4a = lastTocList[5];
|
const tocItems = lastTocList.filter(item => item.title === 'H2 4 repeat');
|
||||||
const tocItem4b = lastTocList[6];
|
expect(tocItems[0].href).toEqual(`${docId}#h2-4-repeat`, 'first');
|
||||||
expect(tocItem4a.href).toEqual(`${docId}#h2-4-repeat`, 'first');
|
expect(tocItems[1].href).toEqual(`${docId}#h2-4-repeat-2`, 'second');
|
||||||
expect(tocItem4b.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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||||
|
|
||||||
import { ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service';
|
import { ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service';
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +65,7 @@ export class TocService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private findTocHeadings(docElement: Element): HTMLHeadingElement[] {
|
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);
|
const skipNoTocHeadings = (heading: HTMLHeadingElement) => !/(?:no-toc|notoc)/i.test(heading.className);
|
||||||
|
|
||||||
return Array.prototype.filter.call(headings, skipNoTocHeadings);
|
return Array.prototype.filter.call(headings, skipNoTocHeadings);
|
||||||
|
|
|
@ -6,11 +6,6 @@
|
||||||
bottom: 32px;
|
bottom: 32px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aio-toc {
|
aio-toc {
|
||||||
|
@ -19,16 +14,9 @@ aio-toc {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.embedded) {
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aio-toc > div {
|
.toc-inner {
|
||||||
border-left: 4px solid #4285f4;
|
border-left: 4px solid #4285f4;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
@ -59,6 +47,7 @@ aio-toc > div {
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
text-align: start;
|
||||||
|
|
||||||
&:focus.embedded {
|
&:focus.embedded {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -67,7 +56,8 @@ aio-toc > div {
|
||||||
}
|
}
|
||||||
|
|
||||||
button.toc-heading,
|
button.toc-heading,
|
||||||
div.toc-heading {
|
div.toc-heading,
|
||||||
|
ul.toc-list li.h1 {
|
||||||
font-size: 115%;
|
font-size: 115%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,10 +121,10 @@ aio-toc > div {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3 ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-size: 12px;
|
font-size: inherit;
|
||||||
color: lighten($darkgray, 10);
|
color: lighten($darkgray, 10);
|
||||||
display:table-cell;
|
display:table-cell;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
@ -154,6 +144,17 @@ aio-toc > div {
|
||||||
ul.toc-list li.h3 {
|
ul.toc-list li.h3 {
|
||||||
margin-left: 15px;
|
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 {
|
aio-toc.embedded > div.collapsed li.secondary {
|
||||||
|
|
Loading…
Reference in New Issue