diff --git a/aio/src/app/embedded/toc/toc.component.spec.ts b/aio/src/app/embedded/toc/toc.component.spec.ts
index f82864c07b..dd241847b1 100644
--- a/aio/src/app/embedded/toc/toc.component.spec.ts
+++ b/aio/src/app/embedded/toc/toc.component.spec.ts
@@ -1,6 +1,7 @@
import { Component, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By, DOCUMENT } from '@angular/platform-browser';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { TocComponent } from './toc.component';
import { TocItem, TocService } from 'app/shared/toc.service';
@@ -55,15 +56,36 @@ describe('TocComponent', () => {
});
it('should not display anything when no TocItems', () => {
- tocService.tocList = [];
+ tocService.tocList.next([]);
fixture.detectChanges();
expect(tocComponentDe.children.length).toEqual(0);
});
+ it('should update when the TocItems are updated', () => {
+ tocService.tocList.next([{}] as TocItem[]);
+ fixture.detectChanges();
+ expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
+
+ tocService.tocList.next([{}, {}, {}] as TocItem[]);
+ fixture.detectChanges();
+ expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(3);
+ });
+
+ it('should stop listening for TocItems once destroyed', () => {
+ tocService.tocList.next([{}] as TocItem[]);
+ fixture.detectChanges();
+ expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
+
+ tocComponent.ngOnDestroy();
+ tocService.tocList.next([{}, {}, {}] as TocItem[]);
+ fixture.detectChanges();
+ expect(tocComponentDe.queryAllNodes(By.css('li')).length).toBe(1);
+ });
+
describe('when four TocItems', () => {
beforeEach(() => {
- tocService.tocList.length = 4;
+ tocService.tocList.next([{}, {}, {}, {}] as TocItem[]);
fixture.detectChanges();
page = setPage();
});
@@ -92,8 +114,11 @@ describe('TocComponent', () => {
});
it('should have more than 4 displayed items', () => {
+ let tocList: TocItem[];
+ tocService.tocList.subscribe(v => tocList = v);
+
expect(page.listItems.length).toBeGreaterThan(4);
- expect(page.listItems.length).toEqual(tocService.tocList.length);
+ expect(page.listItems.length).toEqual(tocList.length);
});
it('should be in "closed" (not expanded) state at the start', () => {
@@ -154,7 +179,10 @@ describe('TocComponent', () => {
});
it('should display all items', () => {
- expect(page.listItems.length).toEqual(tocService.tocList.length);
+ let tocList: TocItem[];
+ tocService.tocList.subscribe(v => tocList = v);
+
+ expect(page.listItems.length).toEqual(tocList.length);
});
it('should not have secondary items', () => {
@@ -185,7 +213,7 @@ class HostEmbeddedTocComponent {}
class HostNotEmbeddedTocComponent {}
class TestTocService {
- tocList: TocItem[] = getTestTocList();
+ tocList = new BehaviorSubject
(getTestTocList());
}
// tslint:disable:quotemark
diff --git a/aio/src/app/embedded/toc/toc.component.ts b/aio/src/app/embedded/toc/toc.component.ts
index ccf8e4dfd1..504ebb33ac 100644
--- a/aio/src/app/embedded/toc/toc.component.ts
+++ b/aio/src/app/embedded/toc/toc.component.ts
@@ -1,4 +1,6 @@
-import { Component, ElementRef, OnInit } from '@angular/core';
+import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+import 'rxjs/add/operator/takeUntil';
import { TocItem, TocService } from 'app/shared/toc.service';
@@ -7,13 +9,14 @@ import { TocItem, TocService } from 'app/shared/toc.service';
templateUrl: 'toc.component.html',
styles: []
})
-export class TocComponent implements OnInit {
+export class TocComponent implements OnInit, OnDestroy {
hasSecondary = false;
hasToc = true;
isClosed = true;
isEmbedded = false;
private primaryMax = 4;
+ private onDestroy = new Subject();
tocList: TocItem[];
constructor(
@@ -24,16 +27,25 @@ export class TocComponent implements OnInit {
}
ngOnInit() {
- const tocList = this.tocList = this.tocService.tocList;
- const count = tocList.length;
- this.hasToc = count > 0;
- if (this.isEmbedded && this.hasToc) {
- // If TOC is embedded in doc, mark secondary (sometimes hidden) items
- this.hasSecondary = tocList.length > this.primaryMax;
- for (let i = this.primaryMax; i < count; i++) {
- tocList[i].isSecondary = true;
- }
- }
+ this.tocService.tocList
+ .takeUntil(this.onDestroy)
+ .subscribe((tocList: TocItem[]) => {
+ const count = tocList.length;
+
+ this.hasToc = count > 0;
+ this.hasSecondary = this.isEmbedded && this.hasToc && (count > this.primaryMax);
+ this.tocList = tocList;
+
+ if (this.hasSecondary) {
+ for (let i = this.primaryMax; i < count; i++) {
+ tocList[i].isSecondary = true;
+ }
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.onDestroy.next();
}
toggle() {
diff --git a/aio/src/app/shared/toc.service.spec.ts b/aio/src/app/shared/toc.service.spec.ts
index 780243f6df..9232e3ba82 100644
--- a/aio/src/app/shared/toc.service.spec.ts
+++ b/aio/src/app/shared/toc.service.spec.ts
@@ -6,6 +6,7 @@ import { TocItem, TocService } from './toc.service';
describe('TocService', () => {
let injector: ReflectiveInjector;
let tocService: TocService;
+ let lastTocList: TocItem[];
// call TocService.genToc
function callGenToc(html = '', docId = 'fizz/buzz'): HTMLDivElement {
@@ -22,34 +23,66 @@ describe('TocService', () => {
TocService,
]);
tocService = injector.get(TocService);
+ tocService.tocList.subscribe(tocList => lastTocList = tocList);
});
it('should be creatable', () => {
expect(tocService).toBeTruthy();
});
+ describe('tocList', () => {
+ it('should emit the latest value to new subscribers', () => {
+ let value1: TocItem[];
+ let value2: TocItem[];
+
+ tocService.tocList.next([] as TocItem[]);
+ tocService.tocList.subscribe(v => value1 = v);
+ expect(value1).toEqual([]);
+
+ tocService.tocList.next([{}, {}] as TocItem[]);
+ tocService.tocList.subscribe(v => value2 = v);
+ expect(value2).toEqual([{}, {}]);
+ });
+
+ it('should emit the same values to all subscribers', () => {
+ const emittedValues: TocItem[][] = [];
+
+ tocService.tocList.subscribe(v => emittedValues.push(v));
+ tocService.tocList.subscribe(v => emittedValues.push(v));
+ tocService.tocList.next([{ title: 'A' }, { title: 'B' }] as TocItem[]);
+
+ expect(emittedValues).toEqual([
+ [{ title: 'A' }, { title: 'B' }],
+ [{ title: 'A' }, { title: 'B' }]
+ ]);
+ });
+ });
+
describe('should clear tocList', () => {
- // Start w/ dummy data from previous usage
- beforeEach(() => tocService.tocList = [{}, {}] as TocItem[]);
+ beforeEach(() => {
+ // Start w/ dummy data from previous usage
+ tocService.tocList.next([{}, {}] as TocItem[]);
+ expect(lastTocList).not.toEqual([]);
+ });
it('when reset()', () => {
tocService.reset();
- expect(tocService.tocList.length).toEqual(0);
+ expect(lastTocList).toEqual([]);
});
it('when given undefined doc element', () => {
tocService.genToc(undefined);
- expect(tocService.tocList.length).toEqual(0);
+ expect(lastTocList).toEqual([]);
});
it('when given doc element w/ no headings', () => {
callGenToc('This
and
that
');
- expect(tocService.tocList.length).toEqual(0);
+ expect(lastTocList).toEqual([]);
});
it('when given doc element w/ headings other than h2 & h3', () => {
callGenToc('This
and
that
');
- expect(tocService.tocList.length).toEqual(0);
+ expect(lastTocList).toEqual([]);
});
it('when given doc element w/ no-toc headings', () => {
@@ -60,14 +93,13 @@ describe('TocService', () => {
three
some three
four
some four
`);
- expect(tocService.tocList.length).toEqual(0);
+ expect(lastTocList).toEqual([]);
});
});
describe('when given many headings', () => {
let docId: string;
let docEl: HTMLDivElement;
- let tocList: TocItem[];
let headings: NodeListOf;
beforeEach(() => {
@@ -104,39 +136,38 @@ describe('TocService', () => {
H3 6a
h3 toc 8
`, docId);
- tocList = tocService.tocList;
headings = docEl.querySelectorAll('h1,h2,h3,h4') as NodeListOf;
});
it('should have tocList with expect number of TocItems', () => {
// should ignore h1, h4, and the no-toc h2
- expect(tocList.length).toEqual(headings.length - 3);
+ expect(lastTocList.length).toEqual(headings.length - 3);
});
it('should have href with docId and heading\'s id', () => {
- const tocItem = tocList[0];
+ const tocItem = lastTocList[0];
expect(tocItem.href).toEqual(`${docId}#heading-one-special-id`);
});
it('should have level "h2" for an ', () => {
- const tocItem = tocList[0];
+ const tocItem = lastTocList[0];
expect(tocItem.level).toEqual('h2');
});
it('should have level "h3" for an ', () => {
- const tocItem = tocList[3];
+ const tocItem = lastTocList[3];
expect(tocItem.level).toEqual('h3');
});
it('should have title which is heading\'s innerText ', () => {
const heading = headings[3];
- const tocItem = tocList[2];
+ const tocItem = lastTocList[2];
expect(heading.innerText).toEqual(tocItem.title);
});
it('should have "SafeHtml" content which is heading\'s innerHTML ', () => {
const heading = headings[3];
- const content = tocList[2].content;
+ const content = lastTocList[2].content;
expect((content).changingThisBreaksApplicationSecurity)
.toEqual(heading.innerHTML);
});
@@ -147,20 +178,20 @@ describe('TocService', () => {
});
it('should have href with docId and calculated heading id', () => {
- const tocItem = tocList[1];
+ const tocItem = lastTocList[1];
expect(tocItem.href).toEqual(`${docId}#h2-two`);
});
it('should ignore HTML in heading when calculating id', () => {
const id = headings[3].getAttribute('id');
- const tocItem = tocList[2];
+ const tocItem = lastTocList[2];
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 = tocList[5];
- const tocItem4b = tocList[6];
+ 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');
});
@@ -174,7 +205,7 @@ describe('TocService', () => {
beforeEach(() => {
docId = 'fizz/buzz/';
- expectedTocContent = 'Setup to develop locally.';
+ expectedTocContent = 'Setup to develop locally.';
// An almost-actual ... with extra whitespace
docEl = callGenToc(`
@@ -186,7 +217,7 @@ describe('TocService', () => {
`, docId);
- tocItem = tocService.tocList[0];
+ tocItem = lastTocList[0];
});
it('should have expected href', () => {
diff --git a/aio/src/app/shared/toc.service.ts b/aio/src/app/shared/toc.service.ts
index b429ea9471..c44907ecc3 100644
--- a/aio/src/app/shared/toc.service.ts
+++ b/aio/src/app/shared/toc.service.ts
@@ -1,10 +1,7 @@
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
-
import { ReplaySubject } from 'rxjs/ReplaySubject';
-import { DocumentContents } from 'app/documents/document.service';
-
export interface TocItem {
content: SafeHtml;
href: string;
@@ -15,35 +12,40 @@ export interface TocItem {
@Injectable()
export class TocService {
- tocList: TocItem[];
+ tocList = new ReplaySubject(1);
constructor(@Inject(DOCUMENT) private document: any, private domSanitizer: DomSanitizer) { }
genToc(docElement: Element, docId = '') {
- const tocList = this.tocList = [];
- if (!docElement) { return; }
+ const tocList = [];
- const headings = docElement.querySelectorAll('h2,h3');
- const idMap = new Map();
+ if (docElement) {
+ const headings = docElement.querySelectorAll('h2,h3');
+ const idMap = new Map();
- for (let i = 0; i < headings.length; i++) {
- const heading = headings[i] as HTMLHeadingElement;
- // skip if heading class is 'no-toc'
- if (/(no-toc|notoc)/i.test(heading.className)) { continue; }
+ for (let i = 0; i < headings.length; i++) {
+ const heading = headings[i] as HTMLHeadingElement;
- const id = this.getId(heading, idMap);
- const toc: TocItem = {
- content: this.extractHeadingSafeHtml(heading),
- href: `${docId}#${id}`,
- level: heading.tagName.toLowerCase(),
- title: heading.innerText.trim(),
- };
- tocList.push(toc);
+ // skip if heading class is 'no-toc'
+ if (/(no-toc|notoc)/i.test(heading.className)) { continue; }
+
+ const id = this.getId(heading, idMap);
+ const toc: TocItem = {
+ content: this.extractHeadingSafeHtml(heading),
+ href: `${docId}#${id}`,
+ level: heading.tagName.toLowerCase(),
+ title: heading.innerText.trim(),
+ };
+
+ tocList.push(toc);
+ }
}
+
+ this.tocList.next(tocList);
}
reset() {
- this.tocList = [];
+ this.tocList.next([]);
}
// This bad boy exists only to strip off the anchor link attached to a heading
diff --git a/aio/src/styles/2-modules/_toc.scss b/aio/src/styles/2-modules/_toc.scss
index ca6807863f..0134e501ed 100644
--- a/aio/src/styles/2-modules/_toc.scss
+++ b/aio/src/styles/2-modules/_toc.scss
@@ -1,9 +1,9 @@
.toc-container {
width: 18%;
position: fixed;
- top: 84px;
+ top: 96px;
right: 0;
- bottom: 18px;
+ bottom: 32px;
overflow-y: auto;
overflow-x: hidden;
@@ -13,6 +13,20 @@
}
}
+aio-toc {
+ &.embedded {
+ @media (min-width: 801px) {
+ display: none;
+ }
+ }
+
+ &:not(.embedded) {
+ @media (max-width: 800px) {
+ display: none;
+ }
+ }
+}
+
aio-toc > div {
font-size: 13px;
overflow-y: visible;
@@ -87,7 +101,7 @@ aio-toc > div {
@media (max-width: 800px) {
width: auto;
}
-
+
}
ul.toc-list li {
@@ -141,6 +155,6 @@ aio-toc > div {
}
}
-aio-toc > div.closed li.secondary {
+aio-toc.embedded > div.closed li.secondary {
display: none;
}