@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
395 lines (393 loc) • 14.1 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
var hierarchical_term_selector_exports = {};
__export(hierarchical_term_selector_exports, {
HierarchicalTermSelector: () => HierarchicalTermSelector,
default: () => hierarchical_term_selector_default,
findTerm: () => findTerm,
getFilterMatcher: () => getFilterMatcher,
sortBySelected: () => sortBySelected
});
module.exports = __toCommonJS(hierarchical_term_selector_exports);
var import_i18n = require("@wordpress/i18n");
var import_element = require("@wordpress/element");
var import_notices = require("@wordpress/notices");
var import_components = require("@wordpress/components");
var import_ui = require("@wordpress/ui");
var import_data = require("@wordpress/data");
var import_compose = require("@wordpress/compose");
var import_core_data = require("@wordpress/core-data");
var import_a11y = require("@wordpress/a11y");
var import_html_entities = require("@wordpress/html-entities");
var import_terms = require("../../utils/terms.cjs");
var import_normalize_text_string = require("../../utils/normalize-text-string.cjs");
var import_store = require("../../store/index.cjs");
var import_lock_unlock = require("../../lock-unlock.cjs");
var import_jsx_runtime = require("react/jsx-runtime");
var { RECEIVE_INTERMEDIATE_RESULTS } = (0, import_lock_unlock.unlock)(import_core_data.privateApis);
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 = [];
var SPEAK_DEBOUNCE_MS = 500;
function getResultCount(termsTree) {
let count = 0;
for (const term of termsTree) {
count++;
if (void 0 !== term.children) {
count += getResultCount(term.children);
}
}
return count;
}
var TermCheckbox = (0, import_element.memo)(function Checkbox({
id,
name,
checked,
onToggle
}) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_components.CheckboxControl,
{
checked,
onChange: () => onToggle(id),
label: (0, import_html_entities.decodeEntities)(name)
}
);
});
function TermRow({ term, selectedTerms, onToggle }) {
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "editor-post-taxonomies__hierarchical-terms-choice", children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
TermCheckbox,
{
id: term.id,
name: term.name,
checked: selectedTerms.has(term.id),
onToggle
}
),
!!term.children.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "editor-post-taxonomies__hierarchical-terms-subchoices", children: term.children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
TermRow,
{
term: child,
selectedTerms,
onToggle
},
child.id
)) })
] });
}
function sortBySelected(termsTree, terms) {
const selectedTerms = new Set(terms);
const treeHasSelection = (termTree) => {
if (selectedTerms.has(termTree.id)) {
return true;
}
return !!termTree.children?.some(treeHasSelection);
};
const selected = [];
const unselected = [];
for (const termTree of termsTree) {
if (treeHasSelection(termTree)) {
selected.push(termTree);
} else {
unselected.push(termTree);
}
}
return [...selected, ...unselected];
}
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 normalizedFilterValue = (0, import_normalize_text_string.normalizeTextString)(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 !== (0, import_normalize_text_string.normalizeTextString)(term.name).indexOf(
normalizedFilterValue
) || term.children.length > 0) {
return term;
}
return false;
};
return matchTermsForFilter;
}
function HierarchicalTermSelector({ slug }) {
const [adding, setAdding] = (0, import_element.useState)(false);
const [formName, setFormName] = (0, import_element.useState)("");
const [formParent, setFormParent] = (0, import_element.useState)("");
const [showForm, setShowForm] = (0, import_element.useState)(false);
const [filterValue, setFilterValue] = (0, import_element.useState)("");
const deferredFilterValue = (0, import_element.useDeferredValue)(filterValue);
const debouncedSpeak = (0, import_compose.useDebounce)(import_a11y.speak, SPEAK_DEBOUNCE_MS);
const {
hasCreateAction,
hasAssignAction,
terms,
loading,
availableTerms,
taxonomy
} = (0, import_data.useSelect)(
(select) => {
const { getCurrentPost, getEditedPostAttribute } = select(import_store.store);
const { getEntityRecord, getEntityRecords, isResolving } = select(import_core_data.store);
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 } = (0, import_data.useDispatch)(import_store.store);
const { saveEntityRecord } = (0, import_data.useDispatch)(import_core_data.store);
const { createErrorNotice } = (0, import_data.useDispatch)(import_notices.store);
const selectedTerms = (0, import_element.useMemo)(() => new Set(terms), [terms]);
const availableTermsTree = (0, import_element.useMemo)(
() => sortBySelected((0, import_terms.buildTermsTree)(availableTerms), terms),
// Remove `terms` from the dependency list to avoid reordering every time
// checking or unchecking a term.
[availableTerms]
);
const shownTerms = (0, import_element.useMemo)(() => {
if ("" === deferredFilterValue) {
return availableTermsTree;
}
return availableTermsTree.map(getFilterMatcher(deferredFilterValue)).filter((term) => term);
}, [availableTermsTree, deferredFilterValue]);
const resultCount = getResultCount(shownTerms);
(0, import_element.useEffect)(() => {
if ("" === deferredFilterValue) {
return;
}
debouncedSpeak(
(0, import_i18n.sprintf)(
/* translators: %d: number of results. */
(0, import_i18n._n)("%d result found.", "%d results found.", resultCount),
resultCount
),
"polite"
);
return () => debouncedSpeak.cancel();
}, [resultCount, deferredFilterValue, debouncedSpeak]);
const onUpdateTerms = (termIds) => {
editPost({ [taxonomy.rest_base]: termIds });
};
const onToggleTerm = (0, import_compose.useEvent)((termId) => {
const id = parseInt(termId, 10);
onUpdateTerms(
selectedTerms.has(id) ? terms.filter((term) => term !== id) : [...terms, id]
);
});
if (!hasAssignAction) {
return null;
}
const addTerm = (term) => {
return saveEntityRecord("taxonomy", slug, term, {
throwOnError: true
});
};
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" ? (0, import_i18n.__)("Category") : (0, import_i18n.__)("Term");
const termAddedMessage = (0, import_i18n.sprintf)(
/* translators: %s: term name. */
(0, import_i18n._x)("%s added", "term"),
taxonomy?.labels?.singular_name ?? defaultName
);
(0, import_a11y.speak)(termAddedMessage, "assertive");
setAdding(false);
setFormName("");
setFormParent("");
onUpdateTerms([...terms, newTerm.id]);
};
const labelWithFallback = (labelProperty, fallbackIsCategory, fallbackIsNotCategory) => taxonomy?.labels?.[labelProperty] ?? (slug === "category" ? fallbackIsCategory : fallbackIsNotCategory);
const newTermButtonLabel = labelWithFallback(
"add_new_item",
(0, import_i18n.__)("Add Category"),
(0, import_i18n.__)("Add Term")
);
const newTermLabel = labelWithFallback(
"new_item_name",
(0, import_i18n.__)("Add Category"),
(0, import_i18n.__)("Add Term")
);
const parentSelectLabel = labelWithFallback(
"parent_item",
(0, import_i18n.__)("Parent Category"),
(0, import_i18n.__)("Parent Term")
);
const noParentOption = `— ${parentSelectLabel} —`;
const newTermSubmitLabel = newTermButtonLabel;
const filterLabel = taxonomy?.labels?.search_items ?? (0, import_i18n.__)("Search Terms");
const groupLabel = taxonomy?.name ?? (0, import_i18n.__)("Terms");
const showFilter = availableTerms.length >= MIN_TERMS_COUNT_FOR_FILTER;
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ui.Stack, { direction: "column", gap: "lg", children: [
showFilter && !loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_components.SearchControl,
{
label: filterLabel,
placeholder: filterLabel,
value: filterValue,
onChange: setFilterValue
}
),
loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_ui.Stack,
{
justify: "center",
style: {
// Match SearchControl height to prevent layout shift.
height: "40px"
},
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Spinner, {})
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
className: "editor-post-taxonomies__hierarchical-terms-list",
tabIndex: "0",
role: "group",
"aria-label": groupLabel,
children: shownTerms.map((term) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
TermRow,
{
term,
selectedTerms,
onToggle: onToggleTerm
},
term.id
))
}
),
!loading && hasCreateAction && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_components.Button,
{
__next40pxDefaultSize: true,
onClick: onToggleForm,
className: "editor-post-taxonomies__hierarchical-terms-add",
"aria-expanded": showForm,
variant: "link",
children: newTermButtonLabel
}
),
showForm && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { onSubmit: onAddTerm, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ui.Stack, { direction: "column", gap: "lg", children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_ui.InputControl,
{
className: "editor-post-taxonomies__hierarchical-terms-input",
label: newTermLabel,
value: formName,
onValueChange: onChangeFormName,
required: true
}
),
!!availableTerms.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_components.TreeSelect,
{
label: parentSelectLabel,
noOptionLabel: noParentOption,
onChange: onChangeFormParent,
selectedId: formParent,
tree: availableTermsTree
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_components.Button,
{
__next40pxDefaultSize: true,
variant: "secondary",
type: "submit",
className: "editor-post-taxonomies__hierarchical-terms-submit",
children: newTermSubmitLabel
}
)
] }) })
] });
}
var hierarchical_term_selector_default = (0, import_components.withFilters)("editor.PostTaxonomyType")(
HierarchicalTermSelector
);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
HierarchicalTermSelector,
findTerm,
getFilterMatcher,
sortBySelected
});
//# sourceMappingURL=hierarchical-term-selector.cjs.map