Skip to content

Commit

Permalink
fixup! 56a6a48
Browse files Browse the repository at this point in the history
  • Loading branch information
3y3k0 authored and 3y3 committed Jan 27, 2023
1 parent dab3dc1 commit 9688358
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 96 deletions.
5 changes: 0 additions & 5 deletions src/components/Toc/Toc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
125 changes: 46 additions & 79 deletions src/components/Toc/Toc.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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';
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';
Expand All @@ -35,52 +35,7 @@ interface TocState {
activeId: string | null | undefined;
fixedById: Record<string, 'opened' | 'closed'>;
contentScrolled: boolean;
}

function linkTocItems(
parent: TocItem | null,
items: TocItem[],
itemById: Map<string, TocItem>,
parentById: Map<string, TocItem>,
itemIdByUrl: Map<string, string>,
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<string, TocItem>) {
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<TocProps, TocState> {
Expand All @@ -91,29 +46,19 @@ class Toc extends React.Component<TocProps, TocState> {
containerEl: HTMLElement | null = null;
footerEl: HTMLElement | null = null;

private itemById: Map<string, TocItem> = new Map();

private parentById: Map<string, TocItem> = new Map();

private itemIdByUrl: Map<string, string> = 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() {
Expand All @@ -131,18 +76,29 @@ class Toc extends React.Component<TocProps, TocState> {
}

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 ||
prevProps.router.hash !== router.hash ||
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() {
Expand All @@ -169,17 +125,16 @@ class Toc extends React.Component<TocProps, TocState> {
}

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) {
Expand All @@ -190,20 +145,26 @@ class Toc extends React.Component<TocProps, TocState> {
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<string, boolean> = activeItem
? zip([activeId].concat(getParentIds(activeItem, this.parentById)), true)
? zip([activeId].concat(this.state.registry.getParentIds(activeId)), true)
: {};

return (
<ul className={b('list')}>
{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';
Expand All @@ -227,8 +188,7 @@ class Toc extends React.Component<TocProps, TocState> {
active,
expanded,
expandable,
openItem,
closeItem,
toggleItem,
}}
/>
{expanded && this.renderList(item.items as TocItem[])}
Expand Down Expand Up @@ -354,8 +314,7 @@ class Toc extends React.Component<TocProps, TocState> {
};

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,
Expand All @@ -365,6 +324,14 @@ class Toc extends React.Component<TocProps, TocState> {
},
}));
};

private toggleItem = (id: string, opened: boolean) => {
if (opened) {
this.closeItem(id);
} else {
this.openItem(id);
}
};
}

export default Toc;
78 changes: 78 additions & 0 deletions src/components/Toc/TocItemRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {TocItem} from '../../models';

type NormalizeUrl = (path: string, hash: string | undefined) => string | null | undefined;

export class TocItemRegistry {
private itemById: Map<string, TocItem> = new Map();

private parentById: Map<string, string> = new Map();

private itemIdByUrl: Map<string, string> = 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);
}
});
}
}
15 changes: 3 additions & 12 deletions src/components/TocItem/TocItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TocItemProps> {
Expand Down Expand Up @@ -92,21 +91,13 @@ class TocItem extends React.Component<TocItemProps> {
};

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);
};
}

Expand Down

0 comments on commit 9688358

Please sign in to comment.