Skip to content

Commit

Permalink
Hierarchical terms sorted by name and filterable (#10138)
Browse files Browse the repository at this point in the history
  • Loading branch information
notnownikki committed Sep 27, 2018
1 parent 8300578 commit 8af1f62
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { get, unescape as unescapeString, without, find, some, invoke } from 'lo
/**
* WordPress dependencies
*/
import { __, _x, sprintf } from '@wordpress/i18n';
import { __, _x, _n, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { TreeSelect, withSpokenMessages, withFilters, Button } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
Expand All @@ -24,8 +24,8 @@ import { buildTermsTree } from '../../utils/terms';
*/
const DEFAULT_QUERY = {
per_page: -1,
orderby: 'count',
order: 'desc',
orderby: 'name',
order: 'asc',
_fields: 'id,name,parent',
};

Expand All @@ -38,6 +38,8 @@ class HierarchicalTermSelector extends Component {
this.onChangeFormParent = this.onChangeFormParent.bind( this );
this.onAddTerm = this.onAddTerm.bind( this );
this.onToggleForm = this.onToggleForm.bind( this );
this.setFilterValue = this.setFilterValue.bind( this );
this.sortBySelected = this.sortBySelected.bind( this );
this.state = {
loading: true,
availableTermsTree: [],
Expand All @@ -46,6 +48,8 @@ class HierarchicalTermSelector extends Component {
formName: '',
formParent: '',
showForm: false,
filterValue: '',
filteredTermsTree: [],
};
}

Expand Down Expand Up @@ -152,7 +156,7 @@ class HierarchicalTermSelector extends Component {
formName: '',
formParent: '',
availableTerms: newAvailableTerms,
availableTermsTree: buildTermsTree( newAvailableTerms ),
availableTermsTree: this.sortBySelected( buildTermsTree( newAvailableTerms ) ),
} );
onUpdateTerms( [ ...terms, term.id ], taxonomy.rest_base );
}, ( xhr ) => {
Expand Down Expand Up @@ -191,7 +195,7 @@ class HierarchicalTermSelector extends Component {
} );
this.fetchRequest.then(
( terms ) => { // resolve
const availableTermsTree = buildTermsTree( terms );
const availableTermsTree = this.sortBySelected( buildTermsTree( terms ) );

this.fetchRequest = null;
this.setState( {
Expand All @@ -212,6 +216,101 @@ class HierarchicalTermSelector extends Component {
);
}

sortBySelected( termsTree ) {
const { terms } = this.props;
const treeHasSelection = ( termTree ) => {
if ( terms.indexOf( termTree.id ) !== -1 ) {
return true;
}
if ( undefined === termTree.children ) {
return false;
}
const anyChildIsSelected = termTree.children.map( treeHasSelection ).filter( ( child ) => child ).length > 0;
if ( anyChildIsSelected ) {
return true;
}
return false;
};
const termOrChildIsSelected = ( termA, termB ) => {
const termASelected = treeHasSelection( termA );
const termBSelected = treeHasSelection( termB );

if ( termASelected === termBSelected ) {
return 0;
}

if ( termASelected && ! termBSelected ) {
return -1;
}

if ( ! termASelected && termBSelected ) {
return 1;
}

return 0;
};
termsTree.sort( termOrChildIsSelected );
return termsTree;
}

setFilterValue( event ) {
const { availableTermsTree } = this.state;
const filterValue = event.target.value;
const filteredTermsTree = availableTermsTree.map( this.getFilterMatcher( filterValue ) ).filter( ( term ) => term );
const getResultCount = ( terms ) => {
let count = 0;
for ( let i = 0; i < terms.length; i++ ) {
count++;
if ( undefined !== terms[ i ].children ) {
count += getResultCount( terms[ i ].children );
}
}
return count;
};
this.setState(
{
filterValue,
filteredTermsTree,
}
);

const resultCount = getResultCount( filteredTermsTree );
const resultsFoundMessage = sprintf(
_n( '%d result found.', '%d results found.', resultCount, 'term' ),
resultCount
);
this.props.debouncedSpeak( resultsFoundMessage, 'assertive' );
}

getFilterMatcher( filterValue ) {
const matchTermsForFilter = ( originalTerm ) => {
if ( '' === filterValue ) {
return originalTerm;
}

// Shallow clone, because we'll be filtering the term's children and
// don't want to modify the original term.
const term = { ...originalTerm };

// Map and filter the children, recursive so we deal with grandchildren
// and any deeper levels.
if ( term.children.length > 0 ) {
term.children = term.children.map( matchTermsForFilter ).filter( ( child ) => child );
}

// If the term's name contains the filterValue, or it has children
// (i.e. some child matched at some point in the tree) then return it.
if ( -1 !== term.name.toLowerCase().indexOf( filterValue ) || term.children.length > 0 ) {
return term;
}

// Otherwise, return false. After mapping, the list of terms will need
// to have false values filtered out.
return false;
};
return matchTermsForFilter;
}

renderTerms( renderedTerms ) {
const { terms = [] } = this.props;
return renderedTerms.map( ( term ) => {
Expand Down Expand Up @@ -244,7 +343,7 @@ class HierarchicalTermSelector extends Component {
return null;
}

const { availableTermsTree, availableTerms, formName, formParent, loading, showForm } = this.state;
const { availableTermsTree, availableTerms, filteredTermsTree, formName, formParent, loading, showForm, filterValue } = this.state;
const labelWithFallback = ( labelProperty, fallbackIsCategory, fallbackIsNotCategory ) => get(
taxonomy,
[ 'data', 'labels', labelProperty ],
Expand All @@ -268,10 +367,47 @@ class HierarchicalTermSelector extends Component {
const noParentOption = `— ${ parentSelectLabel } —`;
const newTermSubmitLabel = newTermButtonLabel;
const inputId = `editor-post-taxonomies__hierarchical-terms-input-${ instanceId }`;
const filterInputId = `editor-post-taxonomies__hierarchical-terms-filter-${ instanceId }`;
const filterLabel = sprintf(
_x( 'Search %s', 'term' ),
get(
this.props.taxonomy,
[ 'name' ],
slug === 'category' ? __( 'Categories' ) : __( 'Terms' )
)
);
const groupLabel = sprintf(
_x( 'Available %s', 'term' ),
get(
this.props.taxonomy,
[ 'name' ],
slug === 'category' ? __( 'Categories' ) : __( 'Terms' )
)
);

/* eslint-disable jsx-a11y/no-onchange */
return [
...this.renderTerms( availableTermsTree ),
<label
key="filter-label"
htmlFor={ filterInputId }>
{ filterLabel }
</label>,
<input
type="search"
id={ filterInputId }
value={ filterValue }
onChange={ this.setFilterValue }
className="editor-post-taxonomies__hierarchical-terms-filter"
key="term-filter-input"
/>,
<div
className="editor-post-taxonomies__hierarchical-terms-list"
key="term-list"
tabIndex="0"
role="group"
aria-label={ groupLabel }
>
{ this.renderTerms( '' !== filterValue ? filteredTermsTree : availableTermsTree ) }
</div>,
! loading && hasCreateAction && (
<Button
key="term-add-button"
Expand Down
9 changes: 9 additions & 0 deletions packages/editor/src/components/post-taxonomies/style.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.editor-post-taxonomies__hierarchical-terms-list {
max-height: 14em;
overflow: auto;
}

.editor-post-taxonomies__hierarchical-terms-choice {
margin-bottom: 8px;
}
Expand Down Expand Up @@ -25,3 +30,7 @@
margin-top: 8px;
width: 100%;
}
.editor-post-taxonomies__hierarchical-terms-filter {
margin-bottom: 8px;
width: 100%;
}

0 comments on commit 8af1f62

Please sign in to comment.