@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
494 lines (418 loc) • 14.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _element = require("@wordpress/element");
var _lodash = require("lodash");
var _i18n = require("@wordpress/i18n");
var _components = require("@wordpress/components");
var _data = require("@wordpress/data");
var _compose = require("@wordpress/compose");
var _apiFetch = _interopRequireDefault(require("@wordpress/api-fetch"));
var _url = require("@wordpress/url");
var _terms = require("../../utils/terms");
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
/**
* Module Constants
*/
const DEFAULT_QUERY = {
per_page: -1,
orderby: 'name',
order: 'asc',
_fields: 'id,name,parent'
};
const MIN_TERMS_COUNT_FOR_FILTER = 8;
class HierarchicalTermSelector extends _element.Component {
constructor() {
super(...arguments);
this.findTerm = this.findTerm.bind(this);
this.onChange = this.onChange.bind(this);
this.onChangeFormName = this.onChangeFormName.bind(this);
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: [],
availableTerms: [],
adding: false,
formName: '',
formParent: '',
showForm: false,
filterValue: '',
filteredTermsTree: []
};
}
onChange(termId) {
const {
onUpdateTerms,
terms = [],
taxonomy
} = this.props;
const hasTerm = terms.indexOf(termId) !== -1;
const newTerms = hasTerm ? (0, _lodash.without)(terms, termId) : [...terms, termId];
onUpdateTerms(newTerms, taxonomy.rest_base);
}
onChangeFormName(event) {
const newValue = event.target.value.trim() === '' ? '' : event.target.value;
this.setState({
formName: newValue
});
}
onChangeFormParent(newParent) {
this.setState({
formParent: newParent
});
}
onToggleForm() {
this.setState(state => ({
showForm: !state.showForm
}));
}
findTerm(terms, parent, name) {
return (0, _lodash.find)(terms, term => {
return (!term.parent && !parent || parseInt(term.parent) === parseInt(parent)) && term.name.toLowerCase() === name.toLowerCase();
});
}
onAddTerm(event) {
event.preventDefault();
const {
onUpdateTerms,
taxonomy,
terms,
slug
} = this.props;
const {
formName,
formParent,
adding,
availableTerms
} = this.state;
if (formName === '' || adding) {
return;
} // check if the term we are adding already exists
const existingTerm = this.findTerm(availableTerms, formParent, formName);
if (existingTerm) {
// if the term we are adding exists but is not selected select it
if (!(0, _lodash.some)(terms, term => term === existingTerm.id)) {
onUpdateTerms([...terms, existingTerm.id], taxonomy.rest_base);
}
this.setState({
formName: '',
formParent: ''
});
return;
}
this.setState({
adding: true
});
this.addRequest = (0, _apiFetch.default)({
path: `/wp/v2/${taxonomy.rest_base}`,
method: 'POST',
data: {
name: formName,
parent: formParent ? formParent : undefined
}
}); // Tries to create a term or fetch it if it already exists
const findOrCreatePromise = this.addRequest.catch(error => {
const errorCode = error.code;
if (errorCode === 'term_exists') {
// search the new category created since last fetch
this.addRequest = (0, _apiFetch.default)({
path: (0, _url.addQueryArgs)(`/wp/v2/${taxonomy.rest_base}`, { ...DEFAULT_QUERY,
parent: formParent || 0,
search: formName
})
});
return this.addRequest.then(searchResult => {
return this.findTerm(searchResult, formParent, formName);
});
}
return Promise.reject(error);
});
findOrCreatePromise.then(term => {
const hasTerm = !!(0, _lodash.find)(this.state.availableTerms, availableTerm => availableTerm.id === term.id);
const newAvailableTerms = hasTerm ? this.state.availableTerms : [term, ...this.state.availableTerms];
const termAddedMessage = (0, _i18n.sprintf)(
/* translators: %s: taxonomy name */
(0, _i18n._x)('%s added', 'term'), (0, _lodash.get)(this.props.taxonomy, ['labels', 'singular_name'], slug === 'category' ? (0, _i18n.__)('Category') : (0, _i18n.__)('Term')));
this.props.speak(termAddedMessage, 'assertive');
this.addRequest = null;
this.setState({
adding: false,
formName: '',
formParent: '',
availableTerms: newAvailableTerms,
availableTermsTree: this.sortBySelected((0, _terms.buildTermsTree)(newAvailableTerms))
});
onUpdateTerms([...terms, term.id], taxonomy.rest_base);
}, xhr => {
if (xhr.statusText === 'abort') {
return;
}
this.addRequest = null;
this.setState({
adding: false
});
});
}
componentDidMount() {
this.fetchTerms();
}
componentWillUnmount() {
(0, _lodash.invoke)(this.fetchRequest, ['abort']);
(0, _lodash.invoke)(this.addRequest, ['abort']);
}
componentDidUpdate(prevProps) {
if (this.props.taxonomy !== prevProps.taxonomy) {
this.fetchTerms();
}
}
fetchTerms() {
const {
taxonomy
} = this.props;
if (!taxonomy) {
return;
}
this.fetchRequest = (0, _apiFetch.default)({
path: (0, _url.addQueryArgs)(`/wp/v2/${taxonomy.rest_base}`, DEFAULT_QUERY)
});
this.fetchRequest.then(terms => {
// resolve
const availableTermsTree = this.sortBySelected((0, _terms.buildTermsTree)(terms));
this.fetchRequest = null;
this.setState({
loading: false,
availableTermsTree,
availableTerms: terms
});
}, xhr => {
// reject
if (xhr.statusText === 'abort') {
return;
}
this.fetchRequest = null;
this.setState({
loading: false
});
});
}
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 = (0, _i18n.sprintf)(
/* translators: %d: number of results */
(0, _i18n._n)('%d result found.', '%d results found.', resultCount), 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.toLowerCase()) || 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 => {
return (0, _element.createElement)("div", {
key: term.id,
className: "editor-post-taxonomies__hierarchical-terms-choice"
}, (0, _element.createElement)(_components.CheckboxControl, {
checked: terms.indexOf(term.id) !== -1,
onChange: () => {
const termId = parseInt(term.id, 10);
this.onChange(termId);
},
label: (0, _lodash.unescape)(term.name)
}), !!term.children.length && (0, _element.createElement)("div", {
className: "editor-post-taxonomies__hierarchical-terms-subchoices"
}, this.renderTerms(term.children)));
});
}
render() {
const {
slug,
taxonomy,
instanceId,
hasCreateAction,
hasAssignAction
} = this.props;
if (!hasAssignAction) {
return null;
}
const {
availableTermsTree,
availableTerms,
filteredTermsTree,
formName,
formParent,
loading,
showForm,
filterValue
} = this.state;
const labelWithFallback = (labelProperty, fallbackIsCategory, fallbackIsNotCategory) => (0, _lodash.get)(taxonomy, ['labels', labelProperty], slug === 'category' ? fallbackIsCategory : fallbackIsNotCategory);
const newTermButtonLabel = labelWithFallback('add_new_item', (0, _i18n.__)('Add new category'), (0, _i18n.__)('Add new term'));
const newTermLabel = labelWithFallback('new_item_name', (0, _i18n.__)('Add new category'), (0, _i18n.__)('Add new term'));
const parentSelectLabel = labelWithFallback('parent_item', (0, _i18n.__)('Parent Category'), (0, _i18n.__)('Parent Term'));
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 = (0, _lodash.get)(this.props.taxonomy, ['labels', 'search_items'], (0, _i18n.__)('Search Terms'));
const groupLabel = (0, _lodash.get)(this.props.taxonomy, ['name'], (0, _i18n.__)('Terms'));
const showFilter = availableTerms.length >= MIN_TERMS_COUNT_FOR_FILTER;
return [showFilter && (0, _element.createElement)("label", {
key: "filter-label",
htmlFor: filterInputId
}, filterLabel), showFilter && (0, _element.createElement)("input", {
type: "search",
id: filterInputId,
value: filterValue,
onChange: this.setFilterValue,
className: "editor-post-taxonomies__hierarchical-terms-filter",
key: "term-filter-input"
}), (0, _element.createElement)("div", {
className: "editor-post-taxonomies__hierarchical-terms-list",
key: "term-list",
tabIndex: "0",
role: "group",
"aria-label": groupLabel
}, this.renderTerms('' !== filterValue ? filteredTermsTree : availableTermsTree)), !loading && hasCreateAction && (0, _element.createElement)(_components.Button, {
key: "term-add-button",
onClick: this.onToggleForm,
className: "editor-post-taxonomies__hierarchical-terms-add",
"aria-expanded": showForm,
isLink: true
}, newTermButtonLabel), showForm && (0, _element.createElement)("form", {
onSubmit: this.onAddTerm,
key: "hierarchical-terms-form"
}, (0, _element.createElement)("label", {
htmlFor: inputId,
className: "editor-post-taxonomies__hierarchical-terms-label"
}, newTermLabel), (0, _element.createElement)("input", {
type: "text",
id: inputId,
className: "editor-post-taxonomies__hierarchical-terms-input",
value: formName,
onChange: this.onChangeFormName,
required: true
}), !!availableTerms.length && (0, _element.createElement)(_components.TreeSelect, {
label: parentSelectLabel,
noOptionLabel: noParentOption,
onChange: this.onChangeFormParent,
selectedId: formParent,
tree: availableTermsTree
}), (0, _element.createElement)(_components.Button, {
isSecondary: true,
type: "submit",
className: "editor-post-taxonomies__hierarchical-terms-submit"
}, newTermSubmitLabel))];
}
}
var _default = (0, _compose.compose)([(0, _data.withSelect)((select, {
slug
}) => {
const {
getCurrentPost
} = select('core/editor');
const {
getTaxonomy
} = select('core');
const taxonomy = getTaxonomy(slug);
return {
hasCreateAction: taxonomy ? (0, _lodash.get)(getCurrentPost(), ['_links', 'wp:action-create-' + taxonomy.rest_base], false) : false,
hasAssignAction: taxonomy ? (0, _lodash.get)(getCurrentPost(), ['_links', 'wp:action-assign-' + taxonomy.rest_base], false) : false,
terms: taxonomy ? select('core/editor').getEditedPostAttribute(taxonomy.rest_base) : [],
taxonomy
};
}), (0, _data.withDispatch)(dispatch => ({
onUpdateTerms(terms, restBase) {
dispatch('core/editor').editPost({
[restBase]: terms
});
}
})), _components.withSpokenMessages, _compose.withInstanceId, (0, _components.withFilters)('editor.PostTaxonomyType')])(HierarchicalTermSelector);
exports.default = _default;
//# sourceMappingURL=hierarchical-term-selector.js.map