@woocommerce/components
Version:
UI components for WooCommerce.
322 lines (321 loc) • 14.1 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.DEFAULT_FILTER = void 0;
/**
* External dependencies
*/
const i18n_1 = require("@wordpress/i18n");
const components_1 = require("@wordpress/components");
const dom_1 = require("@wordpress/dom");
const clsx_1 = __importDefault(require("clsx"));
const element_1 = require("@wordpress/element");
const lodash_1 = require("lodash");
const prop_types_1 = __importDefault(require("prop-types"));
const icons_1 = require("@wordpress/icons");
const navigation_1 = require("@woocommerce/navigation");
/**
* Internal dependencies
*/
const animation_slider_1 = __importDefault(require("../animation-slider"));
const dropdown_button_1 = __importDefault(require("../dropdown-button"));
const search_1 = __importDefault(require("../search"));
exports.DEFAULT_FILTER = 'all';
/**
* Modify a url query parameter via a dropdown selection of configurable options.
* This component manipulates the `filter` query parameter.
*/
class FilterPicker extends element_1.Component {
constructor(props) {
super(props);
const selectedFilter = this.getFilter();
this.state = {
nav: selectedFilter.path || [],
animate: null,
selectedTag: null,
};
this.selectSubFilter = this.selectSubFilter.bind(this);
this.getVisibleFilters = this.getVisibleFilters.bind(this);
this.updateSelectedTag = this.updateSelectedTag.bind(this);
this.onTagChange = this.onTagChange.bind(this);
this.onContentMount = this.onContentMount.bind(this);
this.goBack = this.goBack.bind(this);
if (selectedFilter.settings && selectedFilter.settings.getLabels) {
const { query } = this.props;
const { param: filterParam, getLabels } = selectedFilter.settings;
getLabels(query[filterParam], query).then(this.updateSelectedTag);
}
}
componentDidUpdate({ query: prevQuery }) {
const { query: nextQuery, config } = this.props;
if (prevQuery[config.param] !== nextQuery[[config.param]]) {
const selectedFilter = this.getFilter();
if (selectedFilter && selectedFilter.component === 'Search') {
/* eslint-disable react/no-did-update-set-state */
this.setState({ nav: selectedFilter.path || [] });
/* eslint-enable react/no-did-update-set-state */
const { param: filterParam, getLabels } = selectedFilter.settings;
getLabels(nextQuery[filterParam], nextQuery).then(this.updateSelectedTag);
}
}
}
updateSelectedTag(tags) {
this.setState({ selectedTag: tags[0] });
}
getFilter(value) {
const { config, query } = this.props;
const allFilters = (0, navigation_1.flattenFilters)(config.filters);
value =
value ||
query[config.param] ||
config.defaultValue ||
exports.DEFAULT_FILTER;
return (0, lodash_1.find)(allFilters, { value }) || {};
}
getButtonLabel(selectedFilter) {
if (selectedFilter.component === 'Search') {
const { selectedTag } = this.state;
return [
selectedTag && selectedTag.label,
(0, lodash_1.get)(selectedFilter, 'settings.labels.button'),
];
}
return selectedFilter ? [selectedFilter.label] : [];
}
getVisibleFilters(filters, nav) {
if (nav.length === 0) {
return filters;
}
const value = nav[0];
const nextFilters = (0, lodash_1.find)(filters, { value });
return this.getVisibleFilters(nextFilters && nextFilters.subFilters, nav.slice(1));
}
selectSubFilter(value) {
// Add the value onto the nav path
this.setState((prevState) => ({
nav: [...prevState.nav, value],
animate: 'left',
}));
}
goBack() {
// Remove the last item from the nav path
this.setState((prevState) => ({
nav: prevState.nav.slice(0, -1),
animate: 'right',
}));
}
getAllFilterParams() {
const { config } = this.props;
const params = [];
const getParam = (filters) => {
filters.forEach((filter) => {
if (filter.settings &&
!params.includes(filter.settings.param)) {
params.push(filter.settings.param);
}
if (filter.subFilters) {
getParam(filter.subFilters);
}
});
};
getParam(config.filters);
return params;
}
update(value, additionalQueries = {}) {
const { path, query, config, onFilterSelect, advancedFilters } = this.props;
let update = {
[config.param]: (config.defaultValue || exports.DEFAULT_FILTER) === value
? undefined
: value,
...additionalQueries,
};
// Keep any url parameters as designated by the config
config.staticParams.forEach((param) => {
update[param] = query[param];
});
// Remove all of this filter's params not associated with the update while
// leaving any other params from any other filter an extension may have added.
this.getAllFilterParams().forEach((param) => {
if (!update[param]) {
// Explicitly give value of undefined so it can be removed from the query.
update[param] = undefined;
}
});
// If the main filter is being set to anything but advanced, remove any advancedFilters.
if (config.param === 'filter' && value !== 'advanced') {
const resetAdvancedFilters = (0, navigation_1.getQueryFromActiveFilters)([], query, advancedFilters.filters || {});
update = {
...update,
...resetAdvancedFilters,
};
}
(0, navigation_1.updateQueryString)(update, path, query);
onFilterSelect(update);
}
onTagChange(filter, onClose, config, tags) {
const tag = (0, lodash_1.last)(tags);
const { value, settings } = filter;
const { param: filterParam } = settings;
if (tag) {
this.update(value, { [filterParam]: tag.key });
onClose();
}
else {
this.update(config.defaultValue || exports.DEFAULT_FILTER);
}
this.updateSelectedTag([tag]);
}
renderButton(filter, onClose, config) {
if (filter.component) {
const { type, labels, autocompleter } = filter.settings;
const persistedFilter = this.getFilter();
const selectedTag = persistedFilter.value === filter.value
? this.state.selectedTag
: null;
return ((0, element_1.createElement)(search_1.default, { autocompleter: autocompleter, className: "woocommerce-filters-filter__search", type: type, placeholder: labels.placeholder, selected: selectedTag ? [selectedTag] : [], onChange: (0, lodash_1.partial)(this.onTagChange, filter, onClose, config), inlineTags: true, staticResults: true }));
}
const selectFilter = (event) => {
onClose(event);
this.update(filter.value, filter.query || {});
this.setState({ selectedTag: null });
};
const selectSubFilter = (0, lodash_1.partial)(this.selectSubFilter, filter.value);
const selectedFilter = this.getFilter();
const buttonIsSelected = selectedFilter.value === filter.value ||
(selectedFilter.path &&
(0, lodash_1.includes)(selectedFilter.path, filter.value));
const onClick = (event) => {
if (buttonIsSelected) {
// Don't navigate if the button is already selected.
onClose(event);
return;
}
if (filter.subFilters) {
selectSubFilter(event);
return;
}
selectFilter(event);
};
return ((0, element_1.createElement)(components_1.Button, { className: "woocommerce-filters-filter__button", onClick: onClick }, filter.label));
}
onContentMount(content) {
const { nav } = this.state;
const parentFilter = nav.length
? this.getFilter(nav[nav.length - 1])
: false;
const focusableIndex = parentFilter ? 1 : 0;
const focusable = dom_1.focus.tabbable.find(content)[focusableIndex];
setTimeout(() => {
focusable.focus();
}, 0);
}
render() {
const { config } = this.props;
const { nav, animate } = this.state;
const visibleFilters = this.getVisibleFilters(config.filters, nav);
const parentFilter = nav.length
? this.getFilter(nav[nav.length - 1])
: false;
const selectedFilter = this.getFilter();
return ((0, element_1.createElement)("div", { className: "woocommerce-filters-filter" },
config.label && ((0, element_1.createElement)("span", { className: "woocommerce-filters-label" },
config.label,
":")),
(0, element_1.createElement)(components_1.Dropdown, { contentClassName: "woocommerce-filters-filter__content", popoverProps: {
placement: 'bottom',
}, expandOnMobile: true, headerTitle: (0, i18n_1.__)('filter report to show:', 'woocommerce'), renderToggle: ({ isOpen, onToggle }) => ((0, element_1.createElement)(dropdown_button_1.default, { onClick: onToggle, isOpen: isOpen, labels: this.getButtonLabel(selectedFilter) })), renderContent: ({ onClose }) => ((0, element_1.createElement)(animation_slider_1.default, { animationKey: nav, animate: animate, onExited: this.onContentMount }, () => ((0, element_1.createElement)("ul", { className: "woocommerce-filters-filter__content-list" },
parentFilter && ((0, element_1.createElement)("li", { className: "woocommerce-filters-filter__content-list-item" },
(0, element_1.createElement)(components_1.Button, { className: "woocommerce-filters-filter__button", onClick: this.goBack },
(0, element_1.createElement)(icons_1.Icon, { icon: icons_1.chevronLeft }),
parentFilter.label))),
visibleFilters.map((filter) => ((0, element_1.createElement)("li", { key: filter.value, className: (0, clsx_1.default)('woocommerce-filters-filter__content-list-item', {
'is-selected': selectedFilter.value ===
filter.value ||
(selectedFilter.path &&
(0, lodash_1.includes)(selectedFilter.path, filter.value)),
}) }, this.renderButton(filter, onClose, config)))))))) })));
}
}
FilterPicker.propTypes = {
/**
* An array of filters and subFilters to construct the menu.
*/
config: prop_types_1.default.shape({
/**
* A label above the filter selector.
*/
label: prop_types_1.default.string,
/**
* Url parameters to persist when selecting a new filter.
*/
staticParams: prop_types_1.default.array.isRequired,
/**
* The url parameter this filter will modify.
*/
param: prop_types_1.default.string.isRequired,
/**
* The default parameter value to use instead of 'all'.
*/
defaultValue: prop_types_1.default.string,
/**
* Determine if the filter should be shown. Supply a function with the query object as an argument returning a boolean.
*/
showFilters: prop_types_1.default.func.isRequired,
/**
* An array of filter a user can select.
*/
filters: prop_types_1.default.arrayOf(prop_types_1.default.shape({
/**
* The chart display mode to use for charts displayed when this filter is active.
*/
chartMode: prop_types_1.default.oneOf([
'item-comparison',
'time-comparison',
]),
/**
* A custom component used instead of a button, might have special handling for filtering. TBD, not yet implemented.
*/
component: prop_types_1.default.string,
/**
* The label for this filter. Optional only for custom component filters.
*/
label: prop_types_1.default.string,
/**
* An array representing the "path" to this filter, if nested.
*/
path: prop_types_1.default.string,
/**
* An array of more filter objects that act as "children" to this item.
* This set of filters is shown if the parent filter is clicked.
*/
subFilters: prop_types_1.default.array,
/**
* The value for this filter, used to set the `filter` query param when clicked, if there are no `subFilters`.
*/
value: prop_types_1.default.string.isRequired,
})),
}).isRequired,
/**
* The `path` parameter supplied by React-Router.
*/
path: prop_types_1.default.string.isRequired,
/**
* The query string represented in object form.
*/
query: prop_types_1.default.object,
/**
* Function to be called after filter selection.
*/
onFilterSelect: prop_types_1.default.func,
/**
* Advanced Filters configuration object.
*/
advancedFilters: prop_types_1.default.object,
};
FilterPicker.defaultProps = {
query: {},
onFilterSelect: () => { },
};
exports.default = FilterPicker;