diff --git a/docs/src/modules/components/AppTableOfContents.js b/docs/src/modules/components/AppTableOfContents.js
index 80cdcadaa87ef4..a9de27ac196411 100644
--- a/docs/src/modules/components/AppTableOfContents.js
+++ b/docs/src/modules/components/AppTableOfContents.js
@@ -5,44 +5,13 @@ import PropTypes from 'prop-types';
import marked from 'marked';
import warning from 'warning';
import throttle from 'lodash/throttle';
-import EventListener from 'react-event-listener';
import clsx from 'clsx';
+import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
-import { textToHash } from '@material-ui/docs/MarkdownElement/MarkdownElement';
+import textToHash from '@material-ui/docs/MarkdownElement/textToHash';
import Link from 'docs/src/modules/components/Link';
-
-let itemsCollector;
-const renderer = new marked.Renderer();
-renderer.heading = (text, level) => {
- if (level === 2) {
- itemsCollector.push({
- text,
- level,
- hash: textToHash(text),
- children: [],
- });
- } else if (level === 3) {
- if (!itemsCollector[itemsCollector.length - 1]) {
- throw new Error(`Missing parent level for: ${text}`);
- }
-
- itemsCollector[itemsCollector.length - 1].children.push({
- text,
- level,
- hash: textToHash(text),
- });
- }
-};
-
-function getItems(contents) {
- itemsCollector = [];
- marked(contents.join(''), {
- renderer,
- });
-
- return itemsCollector;
-}
+import compose from 'docs/src/modules/utils/compose';
const styles = theme => ({
root: {
@@ -64,6 +33,7 @@ const styles = theme => ({
},
contents: {
marginTop: theme.spacing(2),
+ paddingLeft: theme.spacing(1.5),
},
ul: {
padding: 0,
@@ -92,6 +62,40 @@ const styles = theme => ({
active: {},
});
+const renderer = new marked.Renderer();
+
+function setRenderer(itemsCollectorRef) {
+ renderer.heading = (text, level) => {
+ if (level === 2) {
+ itemsCollectorRef.current.push({
+ text,
+ level,
+ hash: textToHash(text),
+ children: [],
+ });
+ } else if (level === 3) {
+ if (!itemsCollectorRef.current[itemsCollectorRef.current.length - 1]) {
+ throw new Error(`Missing parent level for: ${text}`);
+ }
+
+ itemsCollectorRef.current[itemsCollectorRef.current.length - 1].children.push({
+ text,
+ level,
+ hash: textToHash(text),
+ });
+ }
+ };
+}
+
+function getItemsServer(contents, itemsCollectorRef) {
+ itemsCollectorRef.current = [];
+ marked(contents.join(''), {
+ renderer,
+ });
+
+ return itemsCollectorRef.current;
+}
+
function checkDuplication(uniq, item) {
warning(!uniq[item.hash], `Table of content: duplicated \`${item.hash}\` item`);
@@ -100,79 +104,69 @@ function checkDuplication(uniq, item) {
}
}
-class AppTableOfContents extends React.Component {
- handleScroll = throttle(() => {
- this.findActiveIndex();
- }, 166); // Corresponds to 10 frames at 60 Hz.
-
- clicked = false;
-
- constructor(props) {
- super();
- this.itemsServer = getItems(props.contents);
- }
-
- state = {
- active: null,
- };
-
- componentDidMount() {
- this.itemsClient = [];
- const uniq = {};
-
- this.itemsServer.forEach(item2 => {
- checkDuplication(uniq, item2);
- this.itemsClient.push({
- ...item2,
- node: document.getElementById(item2.hash),
- });
+function getItemsClient(items) {
+ const itemsClient = [];
+ const unique = {};
- if (item2.children.length > 0) {
- item2.children.forEach(item3 => {
- checkDuplication(uniq, item3);
- this.itemsClient.push({
- ...item3,
- node: document.getElementById(item3.hash),
- });
- });
- }
+ items.forEach(item2 => {
+ checkDuplication(unique, item2);
+ itemsClient.push({
+ ...item2,
+ node: document.getElementById(item2.hash),
});
- window.addEventListener('hashchange', this.handleHashChange);
- }
-
- componentWillUnmount() {
- this.handleScroll.cancel();
- clearTimeout(this.unsetClicked);
- window.removeEventListener('hashchange', this.handleHashChange);
- }
-
- // Update the active TOC entry if the hash changes through click on '#' icon
- handleHashChange = () => {
- const hash = window.location.hash.substring(1);
- if (this.state.active !== hash) {
- this.setState({
- active: hash,
+ if (item2.children.length > 0) {
+ item2.children.forEach(item3 => {
+ checkDuplication(unique, item3);
+ itemsClient.push({
+ ...item3,
+ node: document.getElementById(item3.hash),
+ });
});
}
- };
+ });
+ return itemsClient;
+}
+
+function useThrottledOnScroll(callback, delay) {
+ const throttledCallback = React.useMemo(() => throttle(callback, delay), [delay]);
+
+ /* eslint-disable-next-line consistent-return */
+ React.useEffect(() => {
+ if (delay != null) {
+ window.addEventListener('scroll', throttledCallback);
+ return () => {
+ throttledCallback.cancel();
+ window.removeEventListener('scroll', throttledCallback);
+ };
+ }
+ }, [throttledCallback]);
+}
- findActiveIndex = () => {
+function AppTableOfContents(props) {
+ const { classes, contents, t } = props;
+ const itemsCollectorRef = React.useRef([]);
+ const [itemsServer, setItemsServer] = React.useState([]);
+ const itemsClientRef = React.useRef([]);
+ const [activeState, setActiveState] = React.useState(null);
+ const clickedRef = React.useRef(false);
+ const unsetClickedRef = React.useRef(null);
+
+ const findActiveIndex = () => {
// Don't set the active index based on scroll if a link was just clicked
- if (this.clicked) {
+ if (clickedRef.current) {
return;
}
let active;
-
- for (let i = this.itemsClient.length - 1; i >= 0; i -= 1) {
+ for (let i = itemsClientRef.current.length - 1; i >= 0; i -= 1) {
// No hash if we're near the top of the page
if (document.documentElement.scrollTop < 200) {
active = { hash: null };
break;
}
- const item = this.itemsClient[i];
+ const item = itemsClientRef.current[i];
warning(item.node, `Missing node on the item ${JSON.stringify(item, null, 2)}`);
@@ -186,10 +180,8 @@ class AppTableOfContents extends React.Component {
}
}
- if (active && this.state.active !== active.hash) {
- this.setState({
- active: active.hash,
- });
+ if (active && activeState !== active.hash) {
+ setActiveState(active.hash);
window.history.replaceState(
null,
@@ -201,83 +193,113 @@ class AppTableOfContents extends React.Component {
}
};
- handleClick = hash => () => {
+ // Update the active TOC entry if the hash changes through click on '#' icon
+ const handleHashChange = () => {
+ const hash = window.location.hash.substring(1);
+
+ if (activeState !== hash) {
+ setActiveState(hash);
+ }
+ };
+
+ // Corresponds to 10 frames at 60 Hz
+ useThrottledOnScroll(findActiveIndex, itemsServer.length > 0 ? 166 : null);
+
+ React.useEffect(() => {
+ setRenderer(itemsCollectorRef);
+
+ window.addEventListener('hashchange', handleHashChange);
+
+ return function componentWillUnmount() {
+ clearTimeout(unsetClickedRef.current);
+ window.removeEventListener('hashchange', handleHashChange);
+ };
+ }, []);
+
+ React.useEffect(() => {
+ setItemsServer(getItemsServer(contents, itemsCollectorRef));
+ }, [contents]);
+
+ React.useEffect(() => {
+ itemsClientRef.current = getItemsClient(itemsCollectorRef.current);
+ findActiveIndex();
+ }, [itemsServer]);
+
+ const handleClick = hash => () => {
// Used to disable findActiveIndex if the page scrolls due to a click
- this.clicked = true;
- this.unsetClicked = setTimeout(() => {
- this.clicked = false;
+ clickedRef.current = true;
+ unsetClickedRef.current = setTimeout(() => {
+ clickedRef.current = false;
}, 1000);
- if (this.state.active !== hash) {
- this.setState({
- active: hash,
- });
+ if (activeState !== hash) {
+ setActiveState(hash);
}
};
- render() {
- const { classes } = this.props;
- const { active } = this.state;
-
- return (
-
- );
- }
+ return (
+
+ );
}
AppTableOfContents.propTypes = {
classes: PropTypes.object.isRequired,
contents: PropTypes.array.isRequired,
+ t: PropTypes.func.isRequired,
};
-export default withStyles(styles)(AppTableOfContents);
+export default compose(
+ connect(state => ({
+ t: state.options.t,
+ })),
+ withStyles(styles),
+)(AppTableOfContents);
diff --git a/docs/translations/translations-de.json b/docs/translations/translations-de.json
index b15e4e167f4823..745a7cd5d11f59 100644
--- a/docs/translations/translations-de.json
+++ b/docs/translations/translations-de.json
@@ -28,6 +28,7 @@
"thanks": "Vielen Dank!",
"adTitle": "Diese Werbung soll Open Source unterstützen.",
"editPage": "Helfen Sie, diese Seite zu übersetzen",
+ "tableOfContents": "Inhaltsverzeichnis",
"component": "Komponenten-Kit",
"mdbProDescr": "Material Dashboard Pro React is a Premium Material-UI Admin.",
"mkProDescr": "EIn tolles Material-UI Kit basierend auf Material Design.",
diff --git a/docs/translations/translations-es.json b/docs/translations/translations-es.json
index c4e513fec08792..f75e73db28421d 100644
--- a/docs/translations/translations-es.json
+++ b/docs/translations/translations-es.json
@@ -28,6 +28,7 @@
"thanks": "¡Gracias!",
"adTitle": "Este anuncio está diseñado para apoyar al Open Source.",
"editPage": "Ayuda a traducir esta página",
+ "tableOfContents": "Tabla de Contenido",
"component": "Kit de Componentes",
"mdbProDescr": "Material Dashboard Pro React es un tema de administración premium de Material-UI.",
"mkProDescr": "Un fantástico Kit con Material-UI basado en Material Design.",
diff --git a/docs/translations/translations-fr.json b/docs/translations/translations-fr.json
index 7892b767d6a4cc..7211b1a9ec6309 100644
--- a/docs/translations/translations-fr.json
+++ b/docs/translations/translations-fr.json
@@ -28,6 +28,7 @@
"thanks": "Merci !",
"adTitle": "Cette annonce est conçu pour soutenir l'Open Source.",
"editPage": "Aidez à traduire cette page",
+ "tableOfContents": "Matières",
"component": "Kit de composants",
"mdbProDescr": "Material Dashboard Pro React est un theme Admin Material-UI premium.",
"mkProDescr": "Un theme de composants premium Material-UI basé sur Material Design.",
diff --git a/docs/translations/translations-ja.json b/docs/translations/translations-ja.json
index e372954cc0c596..9cb9c758cb97bd 100644
--- a/docs/translations/translations-ja.json
+++ b/docs/translations/translations-ja.json
@@ -28,6 +28,7 @@
"thanks": "感謝します!",
"adTitle": "この広告はオープンソースを支援するように設計されています。",
"editPage": "このページの翻訳を手伝ってください",
+ "tableOfContents": "目次",
"component": "コンポーネントキット",
"mdbProDescr": "Material Dashboard Pro React is a Premium Material-UI Admin.",
"mkProDescr": "Material Designによる最高にクールなMaterial-UI",
diff --git a/docs/translations/translations-pt.json b/docs/translations/translations-pt.json
index dc61a5275a1962..ce7793ec0f9ae7 100644
--- a/docs/translations/translations-pt.json
+++ b/docs/translations/translations-pt.json
@@ -28,6 +28,7 @@
"thanks": "Obrigado!",
"adTitle": "Este anúncio é projetado para apoiar o Código Aberto.",
"editPage": "Ajude a traduzir esta página",
+ "tableOfContents": "Índice",
"component": "Kit de Componente",
"mdbProDescr": "Material Dashboard Pro React is a Premium Material-UI Admin.",
"mkProDescr": "Um badass Material-UI Kit baseado em Material Design.",
diff --git a/docs/translations/translations-ru.json b/docs/translations/translations-ru.json
index 7a8f1ca19d60d4..62f80ed25034d5 100644
--- a/docs/translations/translations-ru.json
+++ b/docs/translations/translations-ru.json
@@ -28,6 +28,7 @@
"thanks": "Спасибо!",
"adTitle": "Эта реклама предназначена для поддержки Open Source.",
"editPage": "Помогите перевести эту страницу",
+ "tableOfContents": "Содержание",
"component": "Набор компонентов",
"mdbProDescr": "Material Dashboard Pro React is a Premium Material-UI Admin.",
"mkProDescr": "Badass Material-UI Kit основывается на Material Design.",
diff --git a/docs/translations/translations-zh.json b/docs/translations/translations-zh.json
index e8c6bbbe291657..eb310c3ff518c9 100644
--- a/docs/translations/translations-zh.json
+++ b/docs/translations/translations-zh.json
@@ -28,6 +28,7 @@
"thanks": "感谢!",
"adTitle": "该广告旨在支持开源代码。",
"editPage": "帮助改进此页面的翻译",
+ "tableOfContents": "目录",
"component": "组件包",
"mdbProDescr": "Material Dashboard Pro React 是一个专业版的 Material-UI 管理员主题。",
"mkProDescr": "一个根据 Material Design 开发的超棒的 Material-UI 组件。",
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index ace4e58207e45b..cdb1ac2995ca37 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -30,6 +30,7 @@
"thanks": "Thank you!",
"adTitle": "This ad is designed to support Open Source.",
"editPage": "Edit this page",
+ "tableOfContents": "Contents",
"component": "Component Kit",
"mdbProDescr": "Material Dashboard Pro React is a premium Material-UI admin theme.",
"mkProDescr": "A Badass Material-UI Kit based on Material Design.",
diff --git a/packages/material-ui-docs/src/MarkdownElement/MarkdownElement.js b/packages/material-ui-docs/src/MarkdownElement/MarkdownElement.js
index 0cee732e16dd92..9077a7806ed954 100644
--- a/packages/material-ui-docs/src/MarkdownElement/MarkdownElement.js
+++ b/packages/material-ui-docs/src/MarkdownElement/MarkdownElement.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import clsx from 'clsx';
import marked from 'marked';
import { withStyles } from '@material-ui/core/styles';
+import textToHash from '@material-ui/docs/MarkdownElement/textToHash';
import prism from './prism';
// Monkey patch to preserve non-breaking spaces
@@ -18,14 +19,6 @@ marked.Lexer.prototype.lex = function lex(src) {
const renderer = new marked.Renderer();
-export function textToHash(text) {
- return text
- .toLowerCase()
- .replace(/=>|<| \/>||<\/code>/g, '')
- .replace(/\W+/g, '-')
- .replace(/-$/g, '');
-}
-
renderer.heading = (text, level) => {
// Small title. No need for an anchor.
// It's reducing the risk of duplicated id and it's fewer elements in the DOM.
diff --git a/packages/material-ui-docs/src/MarkdownElement/textToHash.js b/packages/material-ui-docs/src/MarkdownElement/textToHash.js
new file mode 100644
index 00000000000000..a2d1ae56debb5e
--- /dev/null
+++ b/packages/material-ui-docs/src/MarkdownElement/textToHash.js
@@ -0,0 +1,11 @@
+export default function textToHash(text) {
+ return encodeURI(
+ text
+ .toLowerCase()
+ .replace(/=>|<| \/>||<\/code>|'/g, '')
+ // eslint-disable-next-line no-useless-escape
+ .replace(/[!@#\$%\^&\*\(\)=_\+\[\]{}`~;:'"\|,\.<>\/\?\s]+/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/-$/g, ''),
+ );
+}
diff --git a/packages/material-ui-docs/src/MarkdownElement/textToHash.test.js b/packages/material-ui-docs/src/MarkdownElement/textToHash.test.js
new file mode 100644
index 00000000000000..2113ffd108eefe
--- /dev/null
+++ b/packages/material-ui-docs/src/MarkdownElement/textToHash.test.js
@@ -0,0 +1,13 @@
+import { assert } from 'chai';
+import textToHash from './textToHash';
+
+describe('textToHash', () => {
+ it('should hash correctly', () => {
+ assert.strictEqual(
+ textToHash('createMuiTheme(options) => theme'),
+ 'createmuitheme-options-theme',
+ );
+ assert.strictEqual(textToHash('Typography - Font family'), 'typography-font-family');
+ assert.strictEqual(textToHash('barre d'application'), 'barre-dapplication');
+ });
+});