@woocommerce/components
Version:
UI components for WooCommerce.
233 lines (232 loc) • 10 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SearchListControl = void 0;
/**
* External dependencies
*/
const i18n_1 = require("@wordpress/i18n");
const components_1 = require("@wordpress/components");
const element_1 = require("@wordpress/element");
const compose_1 = require("@wordpress/compose");
const lodash_1 = require("lodash");
const notice_outline_1 = __importDefault(require("gridicons/dist/notice-outline"));
const prop_types_1 = __importDefault(require("prop-types"));
const clsx_1 = __importDefault(require("clsx"));
/**
* Internal dependencies
*/
const hierarchy_1 = require("./hierarchy");
const item_1 = __importDefault(require("./item"));
const tag_1 = __importDefault(require("../tag"));
const defaultMessages = {
clear: (0, i18n_1.__)('Clear all selected items', 'woocommerce'),
noItems: (0, i18n_1.__)('No items found.', 'woocommerce'),
/* translators: %s: search term */
noResults: (0, i18n_1.__)('No results for %s', 'woocommerce'),
search: (0, i18n_1.__)('Search for items', 'woocommerce'),
selected: (n) => (0, i18n_1.sprintf)(
/* translators: Number of items selected from list. */
(0, i18n_1._n)('%d item selected', '%d items selected', n, 'woocommerce'), n),
updated: (0, i18n_1.__)('Search results updated.', 'woocommerce'),
};
/**
* Component to display a searchable, selectable list of items.
*
* @param {Object} props
*/
const SearchListControl = (props) => {
const [searchValue, setSearchValue] = (0, element_1.useState)(props.search || '');
const { isSingle, isLoading, onChange, selected, instanceId, messages: propsMessages, isCompact, debouncedSpeak, onSearch, className = '', } = props;
const messages = { ...defaultMessages, ...propsMessages };
(0, element_1.useEffect)(() => {
if (typeof onSearch === 'function') {
onSearch(searchValue);
}
}, [onSearch, searchValue]);
const onRemove = (id) => {
return () => {
if (isSingle) {
onChange([]);
}
const i = (0, lodash_1.findIndex)(selected, { id });
onChange([
...selected.slice(0, i),
...selected.slice(i + 1),
]);
};
};
const isSelected = (item) => (0, lodash_1.findIndex)(selected, { id: item.id }) !== -1;
const getFilteredList = (list, search) => {
const { isHierarchical } = props;
if (!search) {
return isHierarchical ? (0, hierarchy_1.buildTermsTree)(list) : list;
}
const re = new RegExp((0, lodash_1.escapeRegExp)(search), 'i');
debouncedSpeak(messages.updated);
const filteredList = list
.map((item) => (re.test(item.name) ? item : false))
.filter(Boolean);
return isHierarchical
? (0, hierarchy_1.buildTermsTree)(filteredList, list)
: filteredList;
};
const onSelect = (item) => {
return () => {
if (isSelected(item)) {
onRemove(item.id)();
return;
}
if (isSingle) {
onChange([item]);
}
else {
onChange([...selected, item]);
}
};
};
const defaultRenderItem = (args) => {
return (0, element_1.createElement)(item_1.default, { ...args });
};
const renderList = (list, depth = 0) => {
const renderItem = props.renderItem || defaultRenderItem;
if (!list) {
return null;
}
return list.map((item) => ((0, element_1.createElement)(element_1.Fragment, { key: item.id },
(0, element_1.createElement)("li", null, renderItem({
item,
isSelected: isSelected(item),
onSelect,
isSingle,
search: searchValue,
depth,
controlId: instanceId,
})),
renderList(item.children, depth + 1))));
};
const renderListSection = () => {
if (isLoading) {
return ((0, element_1.createElement)("div", { className: "woocommerce-search-list__list is-loading" },
(0, element_1.createElement)(components_1.Spinner, null)));
}
const list = getFilteredList(props.list, searchValue);
if (!list.length) {
return ((0, element_1.createElement)("div", { className: "woocommerce-search-list__list is-not-found" },
(0, element_1.createElement)("span", { className: "woocommerce-search-list__not-found-icon" },
(0, element_1.createElement)(notice_outline_1.default, { role: "img", "aria-hidden": "true", focusable: "false" })),
(0, element_1.createElement)("span", { className: "woocommerce-search-list__not-found-text" }, searchValue
? // eslint-disable-next-line @wordpress/valid-sprintf
(0, i18n_1.sprintf)(messages.noResults || '', searchValue)
: messages.noItems)));
}
return ((0, element_1.createElement)("ul", { className: "woocommerce-search-list__list" }, renderList(list)));
};
const renderSelectedSection = () => {
if (isLoading || isSingle || !selected) {
return null;
}
const selectedCount = selected.length;
return ((0, element_1.createElement)("div", { className: "woocommerce-search-list__selected" },
(0, element_1.createElement)("div", { className: "woocommerce-search-list__selected-header" },
(0, element_1.createElement)("strong", null, messages.selected(selectedCount)),
selectedCount > 0 ? ((0, element_1.createElement)(components_1.Button, { isLink: true, isDestructive: true, onClick: onChange([]), "aria-label": messages.clear }, (0, i18n_1.__)('Clear all', 'woocommerce'))) : null),
selectedCount > 0 ? ((0, element_1.createElement)("ul", null, selected.map((item, i) => ((0, element_1.createElement)("li", { key: i },
(0, element_1.createElement)(tag_1.default, { label: item.name, id: item.id, remove: onRemove })))))) : null));
};
return ((0, element_1.createElement)("div", { className: (0, clsx_1.default)('woocommerce-search-list', className, {
'is-compact': isCompact,
}) },
renderSelectedSection(),
(0, element_1.createElement)("div", { className: "woocommerce-search-list__search" },
(0, element_1.createElement)(components_1.TextControl, { label: messages.search, type: "search", value: searchValue, onChange: (value) => setSearchValue(value) })),
renderListSection()));
};
exports.SearchListControl = SearchListControl;
exports.SearchListControl.propTypes = {
/**
* Additional CSS classes.
*/
className: prop_types_1.default.string,
/**
* Whether it should be displayed in a compact way, so it occupies less space.
*/
isCompact: prop_types_1.default.bool,
/**
* Whether the list of items is hierarchical or not. If true, each list item is expected to
* have a parent property.
*/
isHierarchical: prop_types_1.default.bool,
/**
* Whether the list of items is still loading.
*/
isLoading: prop_types_1.default.bool,
/**
* Restrict selections to one item.
*/
isSingle: prop_types_1.default.bool,
/**
* A complete list of item objects, each with id, name properties. This is displayed as a
* clickable/keyboard-able list, and possibly filtered by the search term (searches name).
*/
list: prop_types_1.default.arrayOf(prop_types_1.default.shape({
id: prop_types_1.default.number,
name: prop_types_1.default.string,
})),
/**
* Messages displayed or read to the user. Configure these to reflect your object type.
* See `defaultMessages` above for examples.
*/
messages: prop_types_1.default.shape({
/**
* A more detailed label for the "Clear all" button, read to screen reader users.
*/
clear: prop_types_1.default.string,
/**
* Message to display when the list is empty (implies nothing loaded from the server
* or parent component).
*/
noItems: prop_types_1.default.string,
/**
* Message to display when no matching results are found. %s is the search term.
*/
noResults: prop_types_1.default.string,
/**
* Label for the search input
*/
search: prop_types_1.default.string,
/**
* Label for the selected items. This is actually a function, so that we can pass
* through the count of currently selected items.
*/
selected: prop_types_1.default.func,
/**
* Label indicating that search results have changed, read to screen reader users.
*/
updated: prop_types_1.default.string,
}),
/**
* Callback fired when selected items change, whether added, cleared, or removed.
* Passed an array of item objects (as passed in via props.list).
*/
onChange: prop_types_1.default.func.isRequired,
/**
* Callback fired when the search field is used.
*/
onSearch: prop_types_1.default.func,
/**
* Callback to render each item in the selection list, allows any custom object-type rendering.
*/
renderItem: prop_types_1.default.func,
/**
* The list of currently selected items.
*/
selected: prop_types_1.default.array.isRequired,
// from withSpokenMessages
debouncedSpeak: prop_types_1.default.func,
// from withInstanceId
instanceId: prop_types_1.default.number,
};
exports.default = (0, compose_1.compose)([components_1.withSpokenMessages, compose_1.withInstanceId])(exports.SearchListControl);