UNPKG

@wordpress/editor

Version:
395 lines (393 loc) 14.1 kB
"use strict"; 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