diff --git a/src/components/Toc/Toc.scss b/src/components/Toc/Toc.scss index d541c517..fc1c0fd3 100644 --- a/src/components/Toc/Toc.scss +++ b/src/components/Toc/Toc.scss @@ -101,11 +101,6 @@ $leftOffset: 57px; color: var(--yc-color-text-primary); } - //&_active { - // border-radius: 3px; - // background: var(--yc-color-base-selection); - //} - &:not(&_opened) > #{$class}__list { display: none; } diff --git a/src/components/Toc/Toc.tsx b/src/components/Toc/Toc.tsx index 7a021026..70531d2c 100644 --- a/src/components/Toc/Toc.tsx +++ b/src/components/Toc/Toc.tsx @@ -1,7 +1,6 @@ import React from 'react'; import block from 'bem-cn-lite'; -import {parse} from 'url'; import {omit} from 'lodash'; import {ControlSizes, Lang, Router, TocData, TocItem} from '../../models'; import {TocItem as Item} from '../TocItem'; @@ -9,6 +8,7 @@ import {HTML} from '../HTML'; import {Controls} from '../Controls'; import {isActiveItem, normalizeHash, normalizePath} from '../../utils'; +import {TocItemRegistry} from './TocItemRegistry'; import './Toc.scss'; import {PopperPosition} from '../../hooks'; @@ -35,52 +35,7 @@ interface TocState { activeId: string | null | undefined; fixedById: Record; contentScrolled: boolean; -} - -function linkTocItems( - parent: TocItem | null, - items: TocItem[], - itemById: Map, - parentById: Map, - itemIdByUrl: Map, - singlePage?: boolean, -) { - items.forEach((item) => { - itemById.set(item.id, item); - - if (item.href) { - const {pathname, hash} = parse(item.href); - const url = singlePage ? normalizeHash(hash) : normalizePath(pathname); - - itemIdByUrl.set(url as string, item.id); - } - - if (parent) { - parentById.set(item.id, parent); - } - - if (item.items) { - linkTocItems(item, item.items, itemById, parentById, itemIdByUrl, singlePage); - } - }); -} - -function getChildIds(item: TocItem): string[] { - return (item.items || ([] as TocItem[])).reduce((acc, child) => { - return acc.concat([child.id], getChildIds(child)); - }, [] as string[]); -} - -function getParentIds(item: TocItem, parents: Map) { - const result = []; - - let parent = parents.get(item.id); - while (parent) { - result.push(parent.id); - parent = parents.get(parent.id); - } - - return result; + registry: TocItemRegistry; } class Toc extends React.Component { @@ -91,29 +46,19 @@ class Toc extends React.Component { containerEl: HTMLElement | null = null; footerEl: HTMLElement | null = null; - private itemById: Map = new Map(); - - private parentById: Map = new Map(); - - private itemIdByUrl: Map = new Map(); - constructor(props: TocProps) { super(props); - linkTocItems( - null, - props.items, - this.itemById, - this.parentById, - this.itemIdByUrl, - props.singlePage, - ); + this.state = this.computeState(this.getInitialState()); + } - this.state = this.computeState({ + getInitialState() { + return { + registry: new TocItemRegistry(this.props.items, this.normalizeUrl), fixedById: {}, activeId: null, contentScrolled: false, - }); + }; } componentDidMount() { @@ -131,7 +76,13 @@ class Toc extends React.Component { } componentDidUpdate(prevProps: TocProps, prevState: TocState) { - const {router, singlePage} = this.props; + const {router, singlePage, items} = this.props; + + let nextState; + + if (prevProps.items !== items) { + nextState = this.getInitialState(); + } if ( prevProps.router.pathname !== router.pathname || @@ -139,10 +90,15 @@ class Toc extends React.Component { prevProps.singlePage !== singlePage ) { this.setTocHeight(); - this.setState(this.computeState(prevState)); + + nextState = this.computeState(nextState || prevState); } else if (prevState.activeId !== this.state.activeId) { this.scrollToActiveItem(); } + + if (nextState) { + this.setState(nextState); + } } componentWillUnmount() { @@ -169,17 +125,16 @@ class Toc extends React.Component { } computeState(prevState: TocState) { - const {singlePage, router} = this.props; + const {router} = this.props; const {pathname, hash} = router; - const activeUrl = singlePage ? normalizeHash(hash) : normalizePath(pathname); - const activeId = activeUrl && this.itemIdByUrl.get(activeUrl as string); - const activeItem = activeId && (this.itemById.get(activeId) as TocItem); + const activeUrl = this.normalizeUrl(pathname, hash); + const activeId = activeUrl && this.state.registry.getIdByUrl(activeUrl as string); let fixedById = prevState.fixedById; - if (activeItem && prevState.activeId && activeId !== prevState.activeId) { - const expandedIds = [activeId].concat(getParentIds(activeItem, this.parentById)); + if (activeId && prevState.activeId && activeId !== prevState.activeId) { + const expandedIds = [activeId].concat(this.state.registry.getParentIds(activeId)); const dropClosedSign = expandedIds.filter((id) => prevState.fixedById[id] === 'closed'); if (dropClosedSign.length) { @@ -190,20 +145,26 @@ class Toc extends React.Component { return {...prevState, activeId, fixedById}; } + private normalizeUrl = (path: string, hash: string | undefined) => { + const {singlePage} = this.props; + + return singlePage ? normalizeHash(hash) : normalizePath(path); + }; + private renderList = (items: TocItem[]) => { - const {openItem, closeItem} = this; + const {toggleItem} = this; const {singlePage} = this.props; const {activeId, fixedById} = this.state; - const activeItem = activeId && this.itemById.get(activeId); + const activeItem = activeId && this.state.registry.getItemById(activeId); const activeScope: Record = activeItem - ? zip([activeId].concat(getParentIds(activeItem, this.parentById)), true) + ? zip([activeId].concat(this.state.registry.getParentIds(activeId)), true) : {}; return (
    {items.map((item, index) => { - const main = !this.parentById.get(item.id); + const main = !this.state.registry.getParentId(item.id); const active = (singlePage && !activeId && index === 0 && main) || item.id === activeId; const opened = fixedById[item.id] === 'opened'; @@ -227,8 +188,7 @@ class Toc extends React.Component { active, expanded, expandable, - openItem, - closeItem, + toggleItem, }} /> {expanded && this.renderList(item.items as TocItem[])} @@ -354,8 +314,7 @@ class Toc extends React.Component { }; private closeItem = (id: string) => { - const item = this.itemById.get(id) as TocItem; - const ids = getChildIds(item); + const ids = this.state.registry.getChildIds(id); this.setState((prevState) => ({ ...prevState, @@ -365,6 +324,14 @@ class Toc extends React.Component { }, })); }; + + private toggleItem = (id: string, opened: boolean) => { + if (opened) { + this.closeItem(id); + } else { + this.openItem(id); + } + }; } export default Toc; diff --git a/src/components/Toc/TocItemRegistry.ts b/src/components/Toc/TocItemRegistry.ts new file mode 100644 index 00000000..7b9f141c --- /dev/null +++ b/src/components/Toc/TocItemRegistry.ts @@ -0,0 +1,78 @@ +import {TocItem} from '../../models'; + +type NormalizeUrl = (path: string, hash: string | undefined) => string | null | undefined; + +export class TocItemRegistry { + private itemById: Map = new Map(); + + private parentById: Map = new Map(); + + private itemIdByUrl: Map = new Map(); + + private normalizeUrl: NormalizeUrl; + + constructor(items: TocItem[], normalizeUrl: NormalizeUrl) { + this.normalizeUrl = normalizeUrl; + + this.consumeItems(items); + } + + getIdByUrl(url: string): string | undefined { + return this.itemIdByUrl.get(url); + } + + getItemById(id: string): TocItem | undefined { + return this.itemById.get(id); + } + + getParentId(id: string): string { + return this.parentById.get(id) || ''; + } + + getParentIds(id: string): string[] { + const result = []; + + let parentId = this.getParentId(id); + while (parentId) { + result.push(parentId); + parentId = this.getParentId(parentId); + } + + return result; + } + + getChildIds(id: string): string[] { + const item = this.itemById.get(id); + + if (!item) { + return []; + } + + return (item.items || ([] as TocItem[])).reduce((acc, child) => { + return acc.concat([child.id], this.getChildIds(child.id)); + }, [] as string[]); + } + + private consumeItems(items: TocItem[], parent?: TocItem) { + items.forEach((item) => { + this.itemById.set(item.id, item); + + if (item.href) { + const [pathname, hash] = item.href.split('#'); + const url = this.normalizeUrl(pathname, hash); + + if (url) { + this.itemIdByUrl.set(url, item.id); + } + } + + if (parent) { + this.parentById.set(item.id, parent.id); + } + + if (item.items) { + this.consumeItems(item.items, item); + } + }); + } +} diff --git a/src/components/TocItem/TocItem.tsx b/src/components/TocItem/TocItem.tsx index f85eaf69..d33d263a 100644 --- a/src/components/TocItem/TocItem.tsx +++ b/src/components/TocItem/TocItem.tsx @@ -18,8 +18,7 @@ export interface TocItemProps extends ITocItem { active: boolean; expandable: boolean; expanded: boolean; - openItem: (id: string) => void; - closeItem: (id: string) => void; + toggleItem: (id: string, opened: boolean) => void; } class TocItem extends React.Component { @@ -92,21 +91,13 @@ class TocItem extends React.Component { }; private handleClick = () => { - const {id, href, active, expanded, openItem, closeItem} = this.props; + const {id, href, active, expanded, toggleItem} = this.props; if (!active && href) { return; } - if (expanded) { - closeItem(id); - return; - } - - if (!expanded) { - openItem(id); - return; - } + toggleItem(id, expanded); }; }