Skip to content
Snippets Groups Projects
Tree.js 9.72 KiB
Newer Older
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
esikkala's avatar
esikkala committed
import SortableTree, { changeNodeAtPath } from 'react-sortable-tree';
//import 'react-sortable-tree/style.css'; // This only needs to be imported once in your app
import FileExplorerTheme from 'react-sortable-tree-theme-file-explorer';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
esikkala's avatar
esikkala committed
import CircularProgress from '@material-ui/core/CircularProgress';
import purple from '@material-ui/core/colors/purple';
import Input from '@material-ui/core/Input';
import IconButton from '@material-ui/core/IconButton';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore';
import Typography from '@material-ui/core/Typography';

// https://frontend-collective.github.io/react-sortable-tree/storybook/?selectedKind=Basics&selectedStory=Search&full=0&addons=0&stories=1&panelRight=0

const styles = () => ({
  facetSearchContainer: {
    width: '100%',
    height: 44,
    display: 'flex',
    alignItems: 'center'
  },
  facetSearchIconButton: {
    padding: 10
  },
  treeContainer: {
    height: '100%'
  },
  treeContainerWithSearchField: {
    height: 'calc(100% - 40px)'
esikkala's avatar
esikkala committed
  spinnerContainer: {
    display: 'flex',
    width: '100%',
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center'
  },
esikkala's avatar
esikkala committed
  checkbox: {
    padding: 0,
    marginLeft: 6,
    marginRight: 4,
  },
  sdbmLabel: {
    color: '#00796B'
  },
  bodleyLabel: {
    color: '#F50057'
  },
  bibaleLabel: {
    color: '#F57F17'
esikkala's avatar
esikkala committed
  },
  facetLink: {
    textDecoration: 'inherit'
class Tree extends Component {
  constructor(props) {
    super(props);
    this.state = {
esikkala's avatar
esikkala committed
      treeData: [],
      searchString: '',
      searchFocusIndex: 0,
      searchFoundCount: null,
    };
  }

esikkala's avatar
esikkala committed
  componentDidMount = () => {
    this.props.fetchFacet({
      facetClass: this.props.facetClass,
      facetID: this.props.facetID,
    });
esikkala's avatar
esikkala committed
  }

  componentDidUpdate = prevProps => {
    if (prevProps.facet.values != this.props.facet.values) {
esikkala's avatar
esikkala committed
      this.setState({
        treeData: this.props.facet.values
esikkala's avatar
esikkala committed
      });
    }
esikkala's avatar
esikkala committed
    if (this.props.updatedFacet !== null
      && this.props.updatedFacet !== this.props.facetID
esikkala's avatar
esikkala committed
      && prevProps.facetUpdateID !== this.props.facetUpdateID) {
      // console.log(`fetching new values for ${this.props.property}`)
      this.props.fetchFacet({
        facetClass: this.props.facetClass,
        facetID: this.props.facetID,
esikkala's avatar
esikkala committed
    }
  }

esikkala's avatar
esikkala committed
  handleCheckboxChange = treeObj => event => {
esikkala's avatar
esikkala committed
    const newTreeData = changeNodeAtPath({
      treeData: this.state.treeData,
      getNodeKey: ({ treeIndex }) =>  treeIndex,
      path: treeObj.path,
      newNode: {
        ...treeObj.node,
        selected: event.target.checked ? 'true' : 'false'
      },
    });
    this.setState({ treeData: newTreeData });
    this.props.updateFacetOption({
      facetClass: this.props.facetClass,
      facetID: this.props.facetID,
      option: this.props.facet.filterType,
      value: treeObj.node.id
esikkala's avatar
esikkala committed
    });
  handleSearchFieldOnChange = event => {
    this.setState({ searchString: event.target.value });
  }

  generateNodeProps = treeObj => {
    return {
      title: (
        <FormControlLabel
          control={
            <Checkbox
              className={this.props.classes.checkbox}
              checked={treeObj.node.selected == 'true' ? true : false}
              disabled={treeObj.node.instanceCount == 0 || treeObj.node.prefLabel == 'Unknown' ? true : false}
              onChange={this.handleCheckboxChange(treeObj)}
              value={treeObj.node.id}
              color="primary"
            />
          }
          label={this.generateLabel(treeObj.node)}
          classes={{
            label: this.generateLabelClass(this.props.classes, treeObj.node)
          }}
        />
      )
    };
  };

esikkala's avatar
esikkala committed
  generateLabel = node => {
    //let source = node.source == null ? '' : `(source: ${node.source.substring(node.source.lastIndexOf('/') + 1)}`;
esikkala's avatar
esikkala committed
    // console.log(node)
esikkala's avatar
esikkala committed
    let count = node.totalInstanceCount == null || node.totalInstanceCount == 0 ? node.instanceCount : node.totalInstanceCount;
esikkala's avatar
esikkala committed
    return (
      <React.Fragment>
        <a
          className={this.props.classes.facetLink}
          target='_blank' rel='noopener noreferrer'
          href={node.id}
        >
          {node.prefLabel}
        </a>
        <span> ({count})</span>
      </React.Fragment>
    );
esikkala's avatar
esikkala committed
  }

  generateLabelClass = (classes, node) => {
    let labelClass = classes.label;
    if (this.props.property === 'author' || this.props.property === 'source')
    {
      if (node.source === 'http://ldf.fi/mmm/schema/SDBM' || node.id === 'http://ldf.fi/mmm/schema/SDBM') {
        labelClass = classes.sdbmLabel;
      }
      if (node.source === 'http://ldf.fi/mmm/schema/Bodley' || node.id === 'http://ldf.fi/mmm/schema/Bodley') {
        labelClass = classes.bodleyLabel;
      }
      if (node.source === 'http://ldf.fi/mmm/schema/Bibale' || node.id === 'http://ldf.fi/mmm/schema/Bibale') {
        labelClass = classes.bibaleLabel;
      }
    }
    return labelClass;
  }

    const { searchString, searchFocusIndex, searchFoundCount } = this.state;
    const { classes, facet } = this.props;
    const { isFetching, searchField } = facet;

    //console.log(this.props.data)

    // Case insensitive search of `node.title`
    const customSearchMethod = ({ node, searchQuery }) => {
      let prefLabel = Array.isArray(node.prefLabel) ? node.prefLabel[0] : node.prefLabel;
      return searchQuery.length > 2  &&
      prefLabel.toLowerCase().indexOf(searchQuery.toLowerCase()) > -1;
    };
    const selectPrevMatch = () =>
      this.setState({
        searchFocusIndex:
          searchFocusIndex !== null
            ? (searchFoundCount + searchFocusIndex - 1) % searchFoundCount
            : searchFoundCount - 1,
      });
    const selectNextMatch = () =>
      this.setState({
        searchFocusIndex:
          searchFocusIndex !== null
            ? (searchFocusIndex + 1) % searchFoundCount
            : 0,
      });
esikkala's avatar
esikkala committed
      <React.Fragment>
        {isFetching ?
esikkala's avatar
esikkala committed
          <div className={classes.spinnerContainer}>
            <CircularProgress style={{ color: purple[500] }} thickness={5} />
          </div>
          :
          <React.Fragment>
            {searchField &&
              <div className={classes.facetSearchContainer}>
                <Input
                  placeholder={`Search...`}
                  onChange={this.handleSearchFieldOnChange}
                >
                </Input>
                {searchFoundCount > 0 &&
                  <React.Fragment>
                    <IconButton
                      className={classes.facetSearchIconButton}
                      aria-label="Previous"
                      onClick={selectPrevMatch}
                    >
                      <NavigateBeforeIcon />
                    </IconButton>
                    <IconButton
                      className={classes.facetSearchIconButton}
                      aria-label="Next"
                      onClick={selectNextMatch}
                    >
                      <NavigateNextIcon />
                    </IconButton>
                    <Typography>
                      {searchFoundCount > 0 ? searchFocusIndex + 1 : 0} / {searchFoundCount || 0}
                    </Typography>
                  </React.Fragment>
                }
              </div>
esikkala's avatar
esikkala committed
            }
            <div className={searchField ? classes.treeContainerWithSearchField : classes.treeContainer }>
              <SortableTree
                treeData={this.state.treeData}
                onChange={treeData => this.setState({ treeData })}
                canDrag={false}
                rowHeight={30}
                // Custom comparison for matching during search.
                // This is optional, and defaults to a case sensitive search of
                // the title and subtitle values.
                // see `defaultSearchMethod` in https://github.com/frontend-collective/react-sortable-tree/blob/master/src/utils/default-handlers.js
                searchMethod={customSearchMethod}
                searchQuery={searchString}
                // When matches are found, this property lets you highlight a specific
                // match and scroll to it. This is optional.
                searchFocusOffset={searchFocusIndex}
                // This callback returns the matches from the search,
                // including their `node`s, `treeIndex`es, and `path`s
                // Here I just use it to note how many matches were found.
                // This is optional, but without it, the only thing searches
                // do natively is outline the matching nodes.
                searchFinishCallback={matches =>
                  this.setState({
                    searchFoundCount: matches.length,
                    searchFocusIndex:
                        matches.length > 0 ? searchFocusIndex % matches.length : 0,
                  })
                }
                onlyExpandSearchedNodes={true}
                theme={FileExplorerTheme}
                generateNodeProps={this.generateNodeProps}
              />
            </div>
          </React.Fragment>
esikkala's avatar
esikkala committed
        }
      </React.Fragment>
Tree.propTypes = {
  classes: PropTypes.object.isRequired,
  facetID: PropTypes.string.isRequired,
  facet: PropTypes.object.isRequired,
esikkala's avatar
esikkala committed
  facetClass: PropTypes.string,
  resultClass: PropTypes.string,
  fetchFacet: PropTypes.func,
  updateFacetOption: PropTypes.func,
esikkala's avatar
esikkala committed
  facetUpdateID: PropTypes.number,
  updatedFacet: PropTypes.string,
export default withStyles(styles)(Tree);