1
/* eslint-disable react/prop-types */
2

3 1
import React from 'react';
4 1
import FocusTrap from 'focus-trap-react';
5

6 1
import { STRINGS, MAX_EXPLORER_PAGES } from '../../config/wagtailConfig';
7

8 1
import Button from '../Button/Button';
9 1
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner';
10 1
import Transition, { PUSH, POP } from '../Transition/Transition';
11 1
import ExplorerHeader from './ExplorerHeader';
12 1
import ExplorerItem from './ExplorerItem';
13 1
import PageCount from './PageCount';
14
import { State as NodeState, PageState } from './reducers/nodes';
15

16
interface ExplorerPanelProps {
17
  nodes: NodeState;
18
  depth: number;
19
  page: PageState;
20
  onClose(): void;
21
  gotoPage(id: number, transition: number): void;
22
}
23

24
interface ExplorerPanelState {
25
  transition: typeof PUSH | typeof POP;
26
  paused: boolean;
27
}
28

29
/**
30
 * The main panel of the page explorer menu, with heading,
31
 * menu items, and special states.
32
 */
33 1
class ExplorerPanel extends React.Component<ExplorerPanelProps, ExplorerPanelState> {
34 1
  constructor(props) {
35 1
    super(props);
36

37 1
    this.state = {
38
      transition: PUSH,
39
      paused: false,
40
    };
41

42 1
    this.onItemClick = this.onItemClick.bind(this);
43 1
    this.onHeaderClick = this.onHeaderClick.bind(this);
44 1
    this.clickOutside = this.clickOutside.bind(this);
45
  }
46

47 1
  componentWillReceiveProps(newProps) {
48 1
    const { depth } = this.props;
49 1
    const isPush = newProps.depth > depth;
50

51 1
    this.setState({
52 1
      transition: isPush ? PUSH : POP,
53
    });
54
  }
55

56 1
  componentDidMount() {
57 1
    document.querySelector('[data-explorer-menu-item]')?.classList.add('submenu-active');
58 1
    document.body.classList.add('explorer-open');
59 1
    document.addEventListener('mousedown', this.clickOutside);
60 1
    document.addEventListener('touchend', this.clickOutside);
61
  }
62

63 1
  componentWillUnmount() {
64 1
    document.querySelector('[data-explorer-menu-item]')?.classList.remove('submenu-active');
65 1
    document.body.classList.remove('explorer-open');
66 1
    document.removeEventListener('mousedown', this.clickOutside);
67 1
    document.removeEventListener('touchend', this.clickOutside);
68
  }
69

70 1
  clickOutside(e) {
71 1
    const { onClose } = this.props;
72 1
    const explorer = document.querySelector('[data-explorer-menu]');
73 1
    const toggle = document.querySelector('[data-explorer-menu-item]');
74

75 1
    if (!explorer || !toggle) {
76 0
      return;
77
    }
78

79 1
    const isInside = explorer.contains(e.target) || toggle.contains(e.target);
80 1
    if (!isInside) {
81 1
      onClose();
82
    }
83

84 1
    if (toggle.contains(e.target)) {
85 1
      this.setState({
86
        paused: true,
87
      });
88
    }
89
  }
90

91 1
  onItemClick(id, e) {
92 1
    const { gotoPage } = this.props;
93

94 1
    e.preventDefault();
95 1
    e.stopPropagation();
96

97 1
    gotoPage(id, 1);
98
  }
99

100 1
  onHeaderClick(e) {
101 1
    const { page, depth, gotoPage } = this.props;
102 1
    const parent = page.meta.parent?.id;
103

104
    // Note: Checking depth as well in case the user started deep in the tree
105 1
    if (depth > 0 && parent) {
106 1
      e.preventDefault();
107 1
      e.stopPropagation();
108

109 1
      gotoPage(parent, -1);
110
    }
111
  }
112

113 1
  renderChildren() {
114 1
    const { page, nodes } = this.props;
115
    let children;
116

117 1
    if (!page.isFetching && !page.children.items) {
118 1
      children = (
119
        <div key="empty" className="c-explorer__placeholder">
120
          {STRINGS.NO_RESULTS}
121
        </div>
122
      );
123
    } else {
124 1
      children = (
125
        <div key="children">
126 1
          {page.children.items.map((id) => (
127
            <ExplorerItem
128
              key={id}
129
              item={nodes[id]}
130
              onClick={this.onItemClick.bind(null, id)}
131
            />
132
          ))}
133
        </div>
134
      );
135
    }
136

137 1
    return (
138
      <div className="c-explorer__drawer">
139
        {children}
140
        {page.isFetching ? (
141 1
          <div key="fetching" className="c-explorer__placeholder">
142
            <LoadingSpinner />
143
          </div>
144 1
        ) : null}
145
        {page.isError ? (
146 1
          <div key="error" className="c-explorer__placeholder">
147
            {STRINGS.SERVER_ERROR}
148
          </div>
149 1
        ) : null}
150
      </div>
151
    );
152
  }
153

154 1
  render() {
155 1
    const { page, onClose, depth } = this.props;
156 1
    const { transition, paused } = this.state;
157

158 1
    return (
159
      <FocusTrap
160
        tag="div"
161
        role="dialog"
162
        className="explorer"
163 1
        paused={paused || !page || page.isFetching}
164
        focusTrapOptions={{
165
          initialFocus: '.c-explorer__header__title',
166
          onDeactivate: onClose,
167
        }}
168
      >
169
        <Button className="c-explorer__close">
170
          {STRINGS.CLOSE_EXPLORER}
171
        </Button>
172
        <Transition name={transition} className="c-explorer" component="nav" label={STRINGS.PAGE_EXPLORER}>
173
          <div key={depth} className="c-transition-group">
174
            <ExplorerHeader
175
              depth={depth}
176
              page={page}
177
              onClick={this.onHeaderClick}
178
            />
179

180
            {this.renderChildren()}
181

182 1
            {page.isError || page.children.items && page.children.count > MAX_EXPLORER_PAGES ? (
183 1
              <PageCount page={page} />
184 1
            ) : null}
185
          </div>
186
        </Transition>
187
      </FocusTrap>
188
    );
189
  }
190 1
}
191

192 1
export default ExplorerPanel;

Read our documentation on viewing source code .

Loading