UNPKG

@wordpress/editor

Version:
494 lines (418 loc) 14.6 kB
"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