Newer
Older
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import intl from 'react-intl-universal'
import { withStyles } from '@material-ui/core/styles'
import { has } from 'lodash'
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,
boxShadow: '0 2px 0 #673ab7'
/**
* A component for checkbox facets with or without hierarchy.
* Based on https://github.com/frontend-collective/react-sortable-tree
*/
constructor (props) {
super(props)
treeData: this.props.facetedSearchMode === 'clientFS' || this.props.facetedSearchMode === 'storybook'
searchString: '',
searchFocusIndex: 0,
searchFoundCount: null,
if (this.props.facet.filterType === 'uriFilter') {
this.props.fetchFacet({
facetClass: this.props.facetClass,
facetID: this.props.facetID
})
// console.log(this.props.facetedSearchMode)
// console.log(this.props)
this.props.facetedSearchMode === 'clientFS'
? this.clientFScomponentDidUpdate(prevProps) : this.serverFScomponentDidUpdate(prevProps)
}
clientFScomponentDidUpdate = prevProps => {
if (prevProps.facetUpdateID !== this.props.facetUpdateID) {
this.setState({ treeData: this.props.facet.values })
}
}
serverFScomponentDidUpdate = prevProps => {
if (prevProps.facetUpdateID !== this.props.facetUpdateID) {
// update component state if the user modified this facet
if (!this.props.facet.useConjuction && this.props.updatedFacet === this.props.facetID) {
const treeObj = this.props.updatedFilter
const newTreeData = changeNodeAtPath({
getNodeKey: ({ treeIndex }) => treeIndex,
newNode: () => {
const oldNode = treeObj.node
if (has(oldNode, 'children')) {
return {
...oldNode,
selected: treeObj.added ? 'true' : 'false',
// select also children by default if 'selectAlsoSubconcepts' is not defined
...((!Object.prototype.hasOwnProperty.call(this.props.facet, 'selectAlsoSubconcepts') || this.props.facet.selectAlsoSubconcepts) &&
{ children: this.recursiveSelect(oldNode.children, treeObj.added) })
} else {
return {
...oldNode,
selected: treeObj.added ? 'true' : 'false'
}
})
this.setState({ treeData: newTreeData })
} else { // else fetch new values, because some other facet was updated
// console.log(`fetching new values for ${this.props.facetID}`)
this.props.fetchFacet({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
constrainSelf: this.props.facet.useConjuction
// 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 || prevProps.facet.sortDirection !== this.props.facet.sortDirection) {
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) {
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)
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
handleCheckboxChange = treeObj => event => {
if (this.props.facetedSearchMode === 'clientFS') {
// const newTreeData = changeNodeAtPath({
// treeData: this.state.treeData,
// getNodeKey: ({ treeIndex }) => treeIndex,
// path: treeObj.path,
// newNode: {
// ...treeObj.node,
// selected: event.target.checked
// }
// })
// this.setState({ treeData: newTreeData })
this.props.clientFSUpdateFacet({
facetID: this.props.facetID,
value: treeObj.node.prefLabel,
latestValues: this.props.facet.values
})
} else {
this.props.updateFacetOption({
facetClass: this.props.facetClass,
facetID: this.props.facetID,
option: this.props.facet.filterType,
value: treeObj
})
}
}
handleSearchFieldOnChange = event => {
this.setState({ searchString: event.target.value })
const { uriFilter } = this.props.facet
const { node } = treeObj
const selectedCount = uriFilter == null ? 0 : Object.keys(this.props.facet.uriFilter).length
let isSelected
if (this.props.facetedSearchMode === 'clientFS') {
isSelected = this.props.facet.selectionsSet.has(node.id)
} else {
isSelected = node.selected === 'true'
}
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') ||
// node.id === 'http://ldf.fi/MISSING_VALUE' ||
// prevent selecting when another facet is still updating:
this.props.someFacetIsFetching ||
// prevent selecting all facet values when there is a logical OR between the selections:
(!this.props.facet.useConjuction && !isSelected && selectedCount >= this.props.facet.distinctValueCount - 1) ||
// prevent selecting when parent has been selected
node.disabled === 'true'
onChange={this.handleCheckboxChange(treeObj)}
value={treeObj.node.id}
/>
}
label={this.generateLabel(treeObj.node)}
/>
)
const count = node.totalInstanceCount == null || node.totalInstanceCount === 0 ? node.instanceCount : node.totalInstanceCount
let isSearchMatch = false
if (this.state.matches.length > 0) {
isSearchMatch = this.state.matches.some(match => match.node.id === node.id)
<Typography className={isSearchMatch ? this.props.classes.searchMatch : ''} variant='body2'>
render () {
const { searchString, searchFocusIndex, searchFoundCount } = this.state
const { classes, facet, facetClass, facetID } = this.props
const { isFetching, searchField } = facet
// Case insensitive search of `node.title`
const customSearchMethod = ({ node, searchQuery }) => {
const 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
<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}
value={this.state.searchString}
<IconButton
className={classes.facetSearchIconButton}
onClick={selectPrevMatch}
>
<NavigateBeforeIcon />
</IconButton>
<IconButton
className={classes.facetSearchIconButton}
onClick={selectNextMatch}
>
<NavigateNextIcon />
</IconButton>
<Typography>
{searchFoundCount > 0 ? searchFocusIndex + 1 : 0} / {searchFoundCount || 0}
</Typography>
{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
theme={FileExplorerTheme}
generateNodeProps={this.generateNodeProps}
isVirtualized={this.props.facetedSearchMode !== 'storybook'} // virtualization does not work in Storybook
/>
</div>}
{facet.filterType === 'spatialFilter' &&
<div className={classes.spinnerContainer}>
<Typography>
Draw a bounding box on the map to filter by {intl.get(`perspectives.${facetClass}.properties.${facetID}.label`)}.
</div>}
</>
)}
</>
)
/**
* Material-UI styles.
*/
classes: PropTypes.object.isRequired,
/**
* Unique id of the facet.
*/
/**
* An object containing the client-side config and values of the facet.
*/
/**
* The class of the facet for server-side configs.
*/
/**
* A facet should be disabled while some other facet is updating.
*/
someFacetIsFetching: PropTypes.bool.isRequired,
/**
* An integer for detecting if some other facet was updated.
*/
/**
* Lastly updated facet filter, from the Redux state.
*/
updatedFilter: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.array]),
/**
* Faceted search mode. Storybook mode disables virtualization of react-sortable-tree.
*/
facetedSearchMode: PropTypes.oneOf(['serverFS', 'clientFS', 'storybook']),
/**
* Redux action for fetching the facet values.
*/
fetchFacet: PropTypes.func,
/**
* Redux action for updating the client-side config of the facet.
*/
updateFacetOption: PropTypes.func,
/**
* Redux action for updating the facet in clientFS mode.
*/
clientFSUpdateFacet: PropTypes.func
export const HierarchicalFacetComponent = HierarchicalFacet
export default withStyles(styles)(HierarchicalFacet)