Source

Body/Help/Help.js

import React from 'react';
import PropTypes from "prop-types";
import {
    Classification,
    ClassUnions,
    CreatingProject,
    CrossValidation,
    Data,
    DeletingProject,
    DominanceCones,
    ExportingProject,
    ImportingProject,
    Navigating,
    ProjectFiles,
    ProjectSettings,
    RenamingProject,
    Rules
} from "./Chapters";
import { StyledDrawer, StyledListItem, StyledScrollable } from "./Utils";
import List from "@material-ui/core/List";
import styles from "./styles/Help.module.css";

const CHAPTERS = [
    "Navigating",
    "Creating project",
    "Importing project",
    "Exporting project",
    "Renaming project",
    "Deleting project",
    "Project settings",
    "Project files",
    "Data",
    "Dominance cones",
    "Class unions",
    "Rules",
    "Classification",
    "Cross-validation"
];

/**
 * <h3>Overview</h3>
 * The Help section of RuLeStudio. Contains a formatted copy of manual.
 * The navigation drawer listens to scroll changes and highlights corresponding chapter titles.
 * Chapter titles in scrollable container are used as a reference to their positions.
 *
 * @constructor
 * @category Help
 * @param {Object} props
 * @param {string} props.selected - The id of selected chapter.
 * @param {number|string} props.upperMargin - The top-margin of the navigation drawer.
 * @returns{React.PureComponent}
 */
class Help extends React.PureComponent {
    constructor(props) {
        super(props);

        this.state = {
            chapterPositions: [],
            marginRight: 0,
            selected: 1
        };

        this.drawerRef = React.createRef();
        this.timerPositions = null;
        this.timerScroll = null
    }

    componentDidMount() {
        if (this.drawerRef.current != null) {
            this.updatePositions();
        }

        window.addEventListener("resize", this.updatePositions);
    }

    componentWillUnmount() {
        clearTimeout(this.timerPositions);
        clearTimeout(this.timerScroll);
        window.removeEventListener("resize", this.updatePositions);
    }

    /**
     * Updates the <code>margin-right</code> of the scrollable container.
     * Then, sets timeout for 1 second with a callback to update the positions of chapter titles.
     *
     * @function
     * @memberOf Help
     */
    updatePositions = () => {
        this.setState({
            marginRight: this.drawerRef.current.offsetWidth
        }, () => {
            clearTimeout(this.timerPositions);
            this.timerPositions = setTimeout(() => {
                let chapterPositions = [];

                for (let i = 1; i <= CHAPTERS.length; i++) {
                    const chapter = document.getElementById(`chapter-${i}`);

                    chapterPositions.push(chapter.offsetTop - chapter.offsetHeight - 32);
                }

                this.setState({
                    chapterPositions: chapterPositions
                });
            }, 1000);
        });
    }

    /**
     * Callback fired when user scrolls to certain position.
     * Function counts and updates the position of the scroll using chapter positions.
     *
     * @function
     * @memberOf Help
     */
    onScroll = () => {
        const { chapterPositions } = this.state;
        const scrollable = document.getElementById("scrollable");

        clearTimeout(this.timerScroll);
        this.timerScroll = setTimeout(() => {
            let i = 0;

            while (i < CHAPTERS.length && chapterPositions[i] - 32 <= scrollable.scrollTop) i++;
            if (i <= 0) i = 1;

            this.setState({
                selected: i
            });
        }, 100);
    };

    /**
     * Callback fired when user clicked on an element from navigation drawer.
     * Function updates scroll position using the <code>offsetTop</code>
     * and <code>offsetHeight</code> attributes of a chapter.
     *
     * @function
     * @memberOf Help
     * @param {number} id - The numerical part of the chapter id.
     */
    scrollTo = (id) => {
        const chapter = document.getElementById(`chapter-${id}`);
        const scrollable = document.getElementById("scrollable");

        scrollable.scrollTop = chapter.offsetTop - chapter.offsetHeight - 32;

        this.setState({
            selected: id
        });
    };

    render() {
        const { marginRight, selected } = this.state;
        const { upperMargin } = this.props;

        return (
            <div className={styles.Root} id={"help"}>
                <StyledScrollable
                    className={styles.Text}
                    onScroll={this.onScroll}
                    style={{marginRight: marginRight}}
                >
                    <div aria-label={"text-container"} className={styles.Scrollable}>
                        <Navigating chapterId={"chapter-1"} />
                        <CreatingProject chapterId={"chapter-2"} />
                        <ImportingProject chapterId={"chapter-3"} />
                        <ExportingProject chapterId={"chapter-4"} />
                        <RenamingProject chapterId={"chapter-5"} />
                        <DeletingProject chapterId={"chapter-6"} />
                        <ProjectSettings chapterId={"chapter-7"} />
                        <ProjectFiles chapterId={"chapter-8"} />
                        <Data chapterId={"chapter-9"} />
                        <DominanceCones chapterId={"chapter-10"} />
                        <ClassUnions chapterId={"chapter-11"} />
                        <Rules chapterId={"chapter-12"} />
                        <Classification chapterId={"chapter-13"} />
                        <CrossValidation chapterId={"chapter-14"} />
                    </div>
                </StyledScrollable>
                <StyledDrawer
                    anchor={"right"}
                    PaperProps={{
                        className: styles.Drawer,
                        id: "help-drawer",
                        ref: this.drawerRef,
                        style: { marginTop: upperMargin }
                    }}
                    variant={"permanent"}
                >
                    <List disablePadding={true}>
                        {CHAPTERS.map((value, index) => (
                            <StyledListItem
                                onClick={() => this.scrollTo(index + 1)}
                                key={index}
                                selected={selected === index + 1}
                            >
                                {value}
                            </StyledListItem>
                        ))}
                    </List>
                </StyledDrawer>
            </div>
        );
    }
}

Help.propTypes = {
    selected: PropTypes.string,
    upperMargin:  PropTypes.number
};

Help.default = {
    selected: 1
};

export default Help;