Newer
Older
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import SortableTree, { changeNodeAtPath } from 'react-sortable-tree';
import FileExplorerTheme from 'react-sortable-tree-theme-file-explorer';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
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';
facetSearchContainer: {
width: '100%',
height: 44,
display: 'flex',
alignItems: 'center'
},
facetSearchIconButton: {
padding: 10
},
treeContainer: {
spinnerContainer: {
display: 'flex',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center'
},
padding: 0,
marginLeft: 6,
marginRight: 4,
},
sdbmLabel: {
color: '#00796B'
},
bodleyLabel: {
color: '#F50057'
},
bibaleLabel: {
color: '#F57F17'
/*
This component is based on the React Sortable Tree example at:
https://frontend-collective.github.io/react-sortable-tree/storybook/?selectedKind=Basics&selectedStory=Search&full=0&addons=0&stories=1&panelRight=0
*/
constructor(props) {
super(props);
this.state = {
searchString: '',
searchFocusIndex: 0,
searchFoundCount: null,
};
}
if (this.props.facet.filterType === 'uriFilter') {
this.props.fetchFacet({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
});
}
if (prevProps.facetUpdateID !== this.props.facetUpdateID) {
// update component state if the user modified this facet
if (this.props.updatedFacet === this.props.facetID) {
if (has(this.props.updatedFilter, 'path')) {
const treeObj = this.props.updatedFilter;
let newTreeData = changeNodeAtPath({
treeData: this.state.treeData,
getNodeKey: ({ treeIndex }) => treeIndex,
path: treeObj.path,
newNode: () => {
const oldNode = treeObj.node;
if (has(oldNode, 'children')) {
return {
...oldNode,
selected: treeObj.added ? 'true' : 'false',
children: this.recursiveSelect(oldNode.children, treeObj.added)
};
} else {
return {
...oldNode,
selected: treeObj.added ? 'true' : 'false',
};
}
}
});
this.setState({ treeData: newTreeData });
}
}
// else fetch new values, because some other facet was updated
else {
this.props.fetchFacet({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
});
}
// fetch new values if the user changes the filter type or sort order
if (prevProps.facet.filterType !== this.props.facet.filterType
&& this.props.facet.filterType === 'uriFilter') {
this.props.fetchFacet({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
});
}
if (prevProps.facet.sortBy !== this.props.facet.sortBy) {
this.props.fetchFacet({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
});
}
// when values have been fetched, update component's state
if (prevProps.facet.values != this.props.facet.values) {
this.setState({
treeData: this.props.facet.values
});
}
recursiveSelect = (nodes, selected) => {
nodes.forEach(node => {
// if a child has been previously selected, remove it
if (has(this.props.facet.uriFilter, node.id)) {
this.props.updateFacetOption({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
option: this.props.facet.filterType,
value: { node }
});
}
node.selected = selected ? 'true' : 'false';
node.disabled = selected ? 'true' : 'false';
if (has(node, 'children')) {
this.recursiveSelect(node.children, selected);
}
});
return nodes;
};
facetID: this.props.facetID,
option: this.props.facet.filterType,
handleSearchFieldOnChange = event => {
this.setState({ searchString: event.target.value });
}
let selectedCount = uriFilter == null ? 0 : Object.keys(this.props.facet.uriFilter).length;
let isSelected = node.selected === 'true' ? true : false;
return {
title: (
<FormControlLabel
control={
<Checkbox
className={this.props.classes.checkbox}
/* non-hierarchical facet:
prevent selecting values with 0 hits (which may appear based on earlier selections): */
(this.props.facet.type !== 'hierarchical'
&& node.instanceCount == 0
&& node.selected === 'false')
// prevent selecting when another facet is still updating:
|| this.props.someFacetIsFetching
|| (selectedCount >= this.props.facet.distinctValueCount - 1 && !isSelected)
// prevent selecting when parent has been selected
|| node.disabled === 'true'
onChange={this.handleCheckboxChange(treeObj)}
value={treeObj.node.id}
color="primary"
/>
}
label={this.generateLabel(treeObj.node)}
classes={{
label: this.generateLabelClass(this.props.classes, treeObj.node)
}}
/>
)
};
};
generateLabel = node => {
let count = node.totalInstanceCount == null || node.totalInstanceCount == 0 ? node.instanceCount : node.totalInstanceCount;
let missingValue = node.id === 'http://ldf.fi/MISSING_VALUE';
{!missingValue &&
<a
className={this.props.classes.facetLink}
target='_blank' rel='noopener noreferrer'
href={node.id}
>
{node.prefLabel}
</a>
}
{missingValue && node.prefLabel}
generateLabelClass = classes => {
// if (this.props.facetID === 'author' || this.props.facetID === '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;
// }
// }
const { searchString, searchFocusIndex, searchFoundCount } = this.state;
const { classes, facet } = this.props;
const { isFetching, searchField } = facet;
// if (this.props.facetID == 'owner') {
// console.log(this.state.treeData)
// }
// 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,
});
<div className={classes.spinnerContainer}>
<CircularProgress style={{ color: purple[500] }} thickness={5} />
</div>
:
{searchField && facet.filterType !== 'spatialFilter' &&
<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>
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
{facet.filterType !== 'spatialFilter' &&
<div className={searchField ? classes.treeContainerWithSearchField : classes.treeContainer }>
<SortableTree
treeData={this.state.treeData}
onChange={treeData => this.setState({ treeData })}
canDrag={false}
rowHeight={30}
searchMethod={customSearchMethod}
searchQuery={searchString}
searchFocusOffset={searchFocusIndex}
searchFinishCallback={matches =>
this.setState({
searchFoundCount: matches.length,
searchFocusIndex:
matches.length > 0 ? searchFocusIndex % matches.length : 0,
})
}
onlyExpandSearchedNodes={true}
theme={FileExplorerTheme}
generateNodeProps={this.generateNodeProps}
/>
</div>}
{facet.filterType === 'spatialFilter' &&
<div className={classes.spinnerContainer}>
<Typography>
Draw a bounding box on the map to filter by {this.props.facet.label.toLowerCase()}.
</Typography>
</div>
}
classes: PropTypes.object.isRequired,
facetID: PropTypes.string.isRequired,
facet: PropTypes.object.isRequired,
someFacetIsFetching: PropTypes.bool.isRequired,
updatedFilter: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.array]),