@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
361 lines (360 loc) • 11.7 kB
JavaScript
// packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
import { __, _n, _x, sprintf } from "@wordpress/i18n";
import { useMemo, useState } from "@wordpress/element";
import { store as noticesStore } from "@wordpress/notices";
import {
Button,
CheckboxControl,
TextControl,
TreeSelect,
withFilters,
Flex,
FlexItem,
SearchControl,
Spinner,
privateApis as componentsPrivateApis
} from "@wordpress/components";
import { useDispatch, useSelect } from "@wordpress/data";
import { useDebounce } from "@wordpress/compose";
import {
store as coreStore,
privateApis as coreDataPrivateApis
} from "@wordpress/core-data";
import { speak } from "@wordpress/a11y";
import { decodeEntities } from "@wordpress/html-entities";
import { buildTermsTree } from "../../utils/terms.mjs";
import { store as editorStore } from "../../store/index.mjs";
import { unlock } from "../../lock-unlock.mjs";
import { jsx, jsxs } from "react/jsx-runtime";
var { normalizeTextString } = unlock(componentsPrivateApis);
var { RECEIVE_INTERMEDIATE_RESULTS } = unlock(coreDataPrivateApis);
var DEFAULT_QUERY = {
per_page: -1,
orderby: "name",
order: "asc",
_fields: "id,name,parent",
context: "view",
[RECEIVE_INTERMEDIATE_RESULTS]: true
};
var MIN_TERMS_COUNT_FOR_FILTER = 8;
var EMPTY_ARRAY = [];
function sortBySelected(termsTree, terms) {
const treeHasSelection = (termTree) => {
if (terms.indexOf(termTree.id) !== -1) {
return true;
}
if (void 0 === termTree.children) {
return false;
}
return termTree.children.map(treeHasSelection).filter((child) => child).length > 0;
};
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;
};
const newTermTree = [...termsTree];
newTermTree.sort(termOrChildIsSelected);
return newTermTree;
}
function findTerm(terms, parent, name) {
return terms.find((term) => {
return (!term.parent && !parent || parseInt(term.parent) === parseInt(parent)) && term.name.toLowerCase() === name.toLowerCase();
});
}
function getFilterMatcher(filterValue) {
const matchTermsForFilter = (originalTerm) => {
if ("" === filterValue) {
return originalTerm;
}
const term = { ...originalTerm };
if (term.children.length > 0) {
term.children = term.children.map(matchTermsForFilter).filter((child) => child);
}
if (-1 !== normalizeTextString(term.name).indexOf(
normalizeTextString(filterValue)
) || term.children.length > 0) {
return term;
}
return false;
};
return matchTermsForFilter;
}
function HierarchicalTermSelector({ slug }) {
const [adding, setAdding] = useState(false);
const [formName, setFormName] = useState("");
const [formParent, setFormParent] = useState("");
const [showForm, setShowForm] = useState(false);
const [filterValue, setFilterValue] = useState("");
const [filteredTermsTree, setFilteredTermsTree] = useState([]);
const debouncedSpeak = useDebounce(speak, 500);
const {
hasCreateAction,
hasAssignAction,
terms,
loading,
availableTerms,
taxonomy
} = useSelect(
(select) => {
const { getCurrentPost, getEditedPostAttribute } = select(editorStore);
const { getEntityRecord, getEntityRecords, isResolving } = select(coreStore);
const _taxonomy = getEntityRecord("root", "taxonomy", slug);
const post = getCurrentPost();
return {
hasCreateAction: _taxonomy ? !!post._links?.["wp:action-create-" + _taxonomy.rest_base] : false,
hasAssignAction: _taxonomy ? !!post._links?.["wp:action-assign-" + _taxonomy.rest_base] : false,
terms: _taxonomy ? getEditedPostAttribute(_taxonomy.rest_base) : EMPTY_ARRAY,
loading: isResolving("getEntityRecords", [
"taxonomy",
slug,
DEFAULT_QUERY
]),
availableTerms: getEntityRecords("taxonomy", slug, DEFAULT_QUERY) || EMPTY_ARRAY,
taxonomy: _taxonomy
};
},
[slug]
);
const { editPost } = useDispatch(editorStore);
const { saveEntityRecord } = useDispatch(coreStore);
const availableTermsTree = useMemo(
() => sortBySelected(buildTermsTree(availableTerms), terms),
// Remove `terms` from the dependency list to avoid reordering every time
// checking or unchecking a term.
[availableTerms]
);
const { createErrorNotice } = useDispatch(noticesStore);
if (!hasAssignAction) {
return null;
}
const addTerm = (term) => {
return saveEntityRecord("taxonomy", slug, term, {
throwOnError: true
});
};
const onUpdateTerms = (termIds) => {
editPost({ [taxonomy.rest_base]: termIds });
};
const onChange = (termId) => {
const hasTerm = terms.includes(termId);
const newTerms = hasTerm ? terms.filter((id) => id !== termId) : [...terms, termId];
onUpdateTerms(newTerms);
};
const onChangeFormName = (value) => {
setFormName(value);
};
const onChangeFormParent = (parentId) => {
setFormParent(parentId);
};
const onToggleForm = () => {
setShowForm(!showForm);
};
const onAddTerm = async (event) => {
event.preventDefault();
if (formName === "" || adding) {
return;
}
const existingTerm = findTerm(availableTerms, formParent, formName);
if (existingTerm) {
if (!terms.some((term) => term === existingTerm.id)) {
onUpdateTerms([...terms, existingTerm.id]);
}
setFormName("");
setFormParent("");
return;
}
setAdding(true);
let newTerm;
try {
newTerm = await addTerm({
name: formName,
parent: formParent ? formParent : void 0
});
} catch (error) {
createErrorNotice(error.message, {
type: "snackbar"
});
return;
}
const defaultName = slug === "category" ? __("Category") : __("Term");
const termAddedMessage = sprintf(
/* translators: %s: term name. */
_x("%s added", "term"),
taxonomy?.labels?.singular_name ?? defaultName
);
speak(termAddedMessage, "assertive");
setAdding(false);
setFormName("");
setFormParent("");
onUpdateTerms([...terms, newTerm.id]);
};
const setFilter = (value) => {
const newFilteredTermsTree = availableTermsTree.map(getFilterMatcher(value)).filter((term) => term);
const getResultCount = (termsTree) => {
let count = 0;
for (let i = 0; i < termsTree.length; i++) {
count++;
if (void 0 !== termsTree[i].children) {
count += getResultCount(termsTree[i].children);
}
}
return count;
};
setFilterValue(value);
setFilteredTermsTree(newFilteredTermsTree);
const resultCount = getResultCount(newFilteredTermsTree);
const resultsFoundMessage = sprintf(
/* translators: %d: number of results. */
_n("%d result found.", "%d results found.", resultCount),
resultCount
);
debouncedSpeak(resultsFoundMessage, "assertive");
};
const renderTerms = (renderedTerms) => {
return renderedTerms.map((term) => {
return /* @__PURE__ */ jsxs(
"div",
{
className: "editor-post-taxonomies__hierarchical-terms-choice",
children: [
/* @__PURE__ */ jsx(
CheckboxControl,
{
checked: terms.indexOf(term.id) !== -1,
onChange: () => {
const termId = parseInt(term.id, 10);
onChange(termId);
},
label: decodeEntities(term.name)
}
),
!!term.children.length && /* @__PURE__ */ jsx("div", { className: "editor-post-taxonomies__hierarchical-terms-subchoices", children: renderTerms(term.children) })
]
},
term.id
);
});
};
const labelWithFallback = (labelProperty, fallbackIsCategory, fallbackIsNotCategory) => taxonomy?.labels?.[labelProperty] ?? (slug === "category" ? fallbackIsCategory : fallbackIsNotCategory);
const newTermButtonLabel = labelWithFallback(
"add_new_item",
__("Add Category"),
__("Add Term")
);
const newTermLabel = labelWithFallback(
"new_item_name",
__("Add Category"),
__("Add Term")
);
const parentSelectLabel = labelWithFallback(
"parent_item",
__("Parent Category"),
__("Parent Term")
);
const noParentOption = `\u2014 ${parentSelectLabel} \u2014`;
const newTermSubmitLabel = newTermButtonLabel;
const filterLabel = taxonomy?.labels?.search_items ?? __("Search Terms");
const groupLabel = taxonomy?.name ?? __("Terms");
const showFilter = availableTerms.length >= MIN_TERMS_COUNT_FOR_FILTER;
return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "4", children: [
showFilter && !loading && /* @__PURE__ */ jsx(
SearchControl,
{
__next40pxDefaultSize: true,
label: filterLabel,
placeholder: filterLabel,
value: filterValue,
onChange: setFilter
}
),
loading && /* @__PURE__ */ jsx(
Flex,
{
justify: "center",
style: {
// Match SearchControl height to prevent layout shift.
height: "40px"
},
children: /* @__PURE__ */ jsx(Spinner, {})
}
),
/* @__PURE__ */ jsx(
"div",
{
className: "editor-post-taxonomies__hierarchical-terms-list",
tabIndex: "0",
role: "group",
"aria-label": groupLabel,
children: renderTerms(
"" !== filterValue ? filteredTermsTree : availableTermsTree
)
}
),
!loading && hasCreateAction && /* @__PURE__ */ jsx(FlexItem, { children: /* @__PURE__ */ jsx(
Button,
{
__next40pxDefaultSize: true,
onClick: onToggleForm,
className: "editor-post-taxonomies__hierarchical-terms-add",
"aria-expanded": showForm,
variant: "link",
children: newTermButtonLabel
}
) }),
showForm && /* @__PURE__ */ jsx("form", { onSubmit: onAddTerm, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "4", children: [
/* @__PURE__ */ jsx(
TextControl,
{
__next40pxDefaultSize: true,
className: "editor-post-taxonomies__hierarchical-terms-input",
label: newTermLabel,
value: formName,
onChange: onChangeFormName,
required: true
}
),
!!availableTerms.length && /* @__PURE__ */ jsx(
TreeSelect,
{
__next40pxDefaultSize: true,
label: parentSelectLabel,
noOptionLabel: noParentOption,
onChange: onChangeFormParent,
selectedId: formParent,
tree: availableTermsTree
}
),
/* @__PURE__ */ jsx(FlexItem, { children: /* @__PURE__ */ jsx(
Button,
{
__next40pxDefaultSize: true,
variant: "secondary",
type: "submit",
className: "editor-post-taxonomies__hierarchical-terms-submit",
children: newTermSubmitLabel
}
) })
] }) })
] });
}
var hierarchical_term_selector_default = withFilters("editor.PostTaxonomyType")(
HierarchicalTermSelector
);
export {
HierarchicalTermSelector,
hierarchical_term_selector_default as default,
findTerm,
getFilterMatcher,
sortBySelected
};
//# sourceMappingURL=hierarchical-term-selector.mjs.map