/** @jsx h */ import { h, render } from 'preact'; import { cx } from '@algolia/ui-components-shared'; import type { RefinementListComponentCSSClasses } from '../../components/RefinementList/RefinementList'; import RefinementList from '../../components/RefinementList/RefinementList'; import type { RefinementListRenderState, RefinementListConnectorParams, RefinementListWidgetDescription, } from '../../connectors/refinement-list/connectRefinementList'; import connectRefinementList from '../../connectors/refinement-list/connectRefinementList'; import { getContainerNode, createDocumentationMessageGenerator, } from '../../lib/utils'; import { prepareTemplateProps } from '../../lib/templating'; import { component } from '../../lib/suit'; import type { Template, WidgetFactory, Renderer } from '../../types'; import type { PreparedTemplateProps } from '../../lib/templating'; import searchBoxDefaultTemplates from '../search-box/defaultTemplates'; import type { SearchBoxTemplates } from '../search-box/search-box'; import type { SearchBoxComponentTemplates } from '../../components/SearchBox/SearchBox'; import defaultTemplates from './defaultTemplates'; const withUsage = createDocumentationMessageGenerator({ name: 'refinement-list', }); const suit = component('RefinementList'); const searchBoxSuit = component('SearchBox'); export type RefinementListOwnCSSClasses = Partial<{ /** * CSS class to add to the root element. */ root: string | string[]; /** * CSS class to add to the root element when no refinements. */ noRefinementRoot: string | string[]; /** * CSS class to add to the root element with no results. */ noResults: string | string[]; /** * CSS class to add to the list element. */ list: string | string[]; /** * CSS class to add to each item element. */ item: string | string[]; /** * CSS class to add to each selected element. */ selectedItem: string | string[]; /** * CSS class to add to each label element (when using the default template). */ label: string | string[]; /** * CSS class to add to each checkbox element (when using the default template). */ checkbox: string | string[]; /** * CSS class to add to each label text element. */ labelText: string | string[]; /** * CSS class to add to the show more element */ showMore: string | string[]; /** * CSS class to add to the disabled show more element */ disabledShowMore: string | string[]; /** * CSS class to add to each count element (when using the default template). */ count: string | string[]; /** * CSS class to add to the searchable container. */ searchBox: string | string[]; }>; type RefinementListSearchableCSSClasses = Partial<{ searchableRoot: string | string[]; searchableForm: string | string[]; searchableInput: string | string[]; searchableSubmit: string | string[]; searchableSubmitIcon: string | string[]; searchableReset: string | string[]; searchableResetIcon: string | string[]; searchableLoadingIndicator: string | string[]; searchableLoadingIcon: string | string[]; }>; export type RefinementListCSSClasses = RefinementListOwnCSSClasses & RefinementListSearchableCSSClasses; export type RefinementListItemData = { /** * The number of occurrences of the facet in the result set. */ count: number; /** * True if the value is selected. */ isRefined: boolean; /** * The label to display. */ label: string; /** * The value used for refining. */ value: string; /** * The label highlighted (when using search for facet values). This value is displayed in the default template. */ highlighted: string; /** * The url with this refinement selected. */ url: string; /** * Object containing all the classes computed for the item. */ cssClasses: RefinementListCSSClasses; /** * Whether the `items` prop contains facet values from the global search or from the search inside the items. */ isFromSearch: boolean; }; export type RefinementListOwnTemplates = Partial<{ /** * Item template, provided with `label`, `highlighted`, `value`, `count`, `isRefined`, `url` data properties. */ item: Template; /** * Template used for the show more text, provided with `isShowingMore` data property. */ showMoreText: Template<{ isShowingMore: boolean }>; /** * Templates to use for search for facet values when there are no results. */ searchableNoResults: Template; }>; type RefinementListSearchableTemplates = Partial<{ /** * Templates to use for search for facet values submit button. */ searchableSubmit: SearchBoxTemplates['submit']; /** * Templates to use for search for facet values reset button. */ searchableReset: SearchBoxTemplates['reset']; /** * Templates to use for the search for facet values loading indicator. */ searchableLoadingIndicator: SearchBoxTemplates['loadingIndicator']; }>; export type RefinementListTemplates = RefinementListOwnTemplates & RefinementListSearchableTemplates; export type RefinementListComponentTemplates = Required; export type RefinementListWidgetParams = { /** * CSS Selector or HTMLElement to insert the widget. */ container: string | HTMLElement; /** * Add a search input to let the user search for more facet values. In order * to make this feature work, you need to make the attribute searchable * [using the API](https://www.algolia.com/doc/guides/searching/faceting/?language=js#declaring-a-searchable-attribute-for-faceting) * or [the dashboard](https://www.algolia.com/explorer/display/) */ searchable?: boolean; /** * Value of the search field placeholder. */ searchablePlaceholder?: string; /** * When `false` the search field will become disabled if there are less items * to display than the `options.limit`, otherwise the search field is always usable. */ searchableIsAlwaysActive?: boolean; /** * When activated, it will escape the facet values that are returned from Algolia. * In this case, the surrounding tags will always be ``. */ searchableEscapeFacetValues?: boolean; /** * Templates to use for the widget. */ templates?: RefinementListTemplates; /** * CSS classes to add to the wrapping elements. */ cssClasses?: RefinementListCSSClasses; }; const renderer = ({ containerNode, cssClasses, templates, searchBoxTemplates, renderState, showMore, searchable, searchablePlaceholder, searchableIsAlwaysActive, }: { containerNode: HTMLElement; cssClasses: RefinementListComponentCSSClasses; renderState: { templateProps?: PreparedTemplateProps; searchBoxTemplateProps?: PreparedTemplateProps; }; templates: RefinementListOwnTemplates; searchBoxTemplates: SearchBoxTemplates; showMore?: boolean; searchable?: boolean; searchablePlaceholder?: string; searchableIsAlwaysActive?: boolean; }): Renderer => ( { refine, items, createURL, searchForItems, isFromSearch, instantSearchInstance, toggleShowMore, isShowingMore, hasExhaustiveItems, canToggleShowMore, }, isFirstRendering ) => { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ defaultTemplates, templatesConfig: instantSearchInstance.templatesConfig, templates, }); renderState.searchBoxTemplateProps = prepareTemplateProps({ defaultTemplates: searchBoxDefaultTemplates, templatesConfig: instantSearchInstance.templatesConfig, templates: searchBoxTemplates, }); return; } render( 0} toggleShowMore={toggleShowMore} isShowingMore={isShowingMore} hasExhaustiveItems={hasExhaustiveItems} canToggleShowMore={canToggleShowMore} />, containerNode ); }; export type RefinementListWidget = WidgetFactory< RefinementListWidgetDescription & { $$widgetType: 'ais.refinementList' }, RefinementListConnectorParams, RefinementListWidgetParams >; /** * The refinement list widget is one of the most common widget that you can find * in a search UI. With this widget, the user can filter the dataset based on facets. * * The refinement list displays only the most relevant facets for the current search * context. The sort option only affects the facet that are returned by the engine, * not which facets are returned. * * This widget also implements search for facet values, which is a mini search inside the * values of the facets. This makes easy to deal with uncommon facet values. * * @requirements * * The attribute passed to `attribute` must be declared as an * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting) * in your Algolia settings. * * If you also want to use search for facet values on this attribute, you need to make it searchable using the [dashboard](https://www.algolia.com/explorer/display/) or using the [API](https://www.algolia.com/doc/guides/searching/faceting/#search-for-facet-values). */ const refinementList: RefinementListWidget = function refinementList( widgetParams ) { const { container, attribute, operator, sortBy, limit, showMore, showMoreLimit, searchable = false, searchablePlaceholder = 'Search...', searchableEscapeFacetValues = true, searchableIsAlwaysActive = true, cssClasses: userCssClasses = {}, templates = {}, transformItems, } = widgetParams || {}; if (!container) { throw new Error(withUsage('The `container` option is required.')); } const escapeFacetValues = searchable ? Boolean(searchableEscapeFacetValues) : false; const containerNode = getContainerNode(container); const cssClasses = { root: cx(suit(), userCssClasses.root), noRefinementRoot: cx( suit({ modifierName: 'noRefinement' }), userCssClasses.noRefinementRoot ), list: cx(suit({ descendantName: 'list' }), userCssClasses.list), item: cx(suit({ descendantName: 'item' }), userCssClasses.item), selectedItem: cx( suit({ descendantName: 'item', modifierName: 'selected' }), userCssClasses.selectedItem ), searchBox: cx( suit({ descendantName: 'searchBox' }), userCssClasses.searchBox ), label: cx(suit({ descendantName: 'label' }), userCssClasses.label), checkbox: cx(suit({ descendantName: 'checkbox' }), userCssClasses.checkbox), labelText: cx( suit({ descendantName: 'labelText' }), userCssClasses.labelText ), count: cx(suit({ descendantName: 'count' }), userCssClasses.count), noResults: cx( suit({ descendantName: 'noResults' }), userCssClasses.noResults ), showMore: cx(suit({ descendantName: 'showMore' }), userCssClasses.showMore), disabledShowMore: cx( suit({ descendantName: 'showMore', modifierName: 'disabled' }), userCssClasses.disabledShowMore ), searchable: { root: cx(searchBoxSuit(), userCssClasses.searchableRoot), form: cx( searchBoxSuit({ descendantName: 'form' }), userCssClasses.searchableForm ), input: cx( searchBoxSuit({ descendantName: 'input' }), userCssClasses.searchableInput ), submit: cx( searchBoxSuit({ descendantName: 'submit' }), userCssClasses.searchableSubmit ), submitIcon: cx( searchBoxSuit({ descendantName: 'submitIcon' }), userCssClasses.searchableSubmitIcon ), reset: cx( searchBoxSuit({ descendantName: 'reset' }), userCssClasses.searchableReset ), resetIcon: cx( searchBoxSuit({ descendantName: 'resetIcon' }), userCssClasses.searchableResetIcon ), loadingIndicator: cx( searchBoxSuit({ descendantName: 'loadingIndicator' }), userCssClasses.searchableLoadingIndicator ), loadingIcon: cx( searchBoxSuit({ descendantName: 'loadingIcon' }), userCssClasses.searchableLoadingIcon ), }, }; const specializedRenderer = renderer({ containerNode, cssClasses, templates, searchBoxTemplates: { submit: templates.searchableSubmit, reset: templates.searchableReset, loadingIndicator: templates.searchableLoadingIndicator, }, renderState: {}, searchable, searchablePlaceholder, searchableIsAlwaysActive, showMore, }); const makeWidget = connectRefinementList(specializedRenderer, () => render(null, containerNode) ); return { ...makeWidget({ attribute, operator, limit, showMore, showMoreLimit, sortBy, escapeFacetValues, transformItems, }), $$widgetType: 'ais.refinementList', }; }; export default refinementList;