UNPKG

@woocommerce/components

Version:
322 lines (321 loc) 14.1 kB
"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;