UNPKG

@plone/volto

Version:
371 lines (350 loc) 12 kB
/** * Search component. * @module components/theme/Search/Search */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink'; import { asyncConnect } from '@plone/volto/helpers/AsyncConnect'; import { FormattedMessage } from 'react-intl'; import { createPortal } from 'react-dom'; import { Container, Pagination, Button, Header } from 'semantic-ui-react'; import qs from 'query-string'; import classNames from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; import config from '@plone/volto/registry'; import Helmet from '@plone/volto/helpers/Helmet/Helmet'; import { searchContent } from '@plone/volto/actions/search/search'; import SearchTags from '@plone/volto/components/theme/Search/SearchTags'; import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import paginationLeftSVG from '@plone/volto/icons/left-key.svg'; import paginationRightSVG from '@plone/volto/icons/right-key.svg'; const messages = defineMessages({ Search: { id: 'Search', defaultMessage: 'Search', }, }); /** * Search class. * @class SearchComponent * @extends Component */ class Search extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { searchContent: PropTypes.func.isRequired, searchableText: PropTypes.string, subject: PropTypes.string, path: PropTypes.string, items: PropTypes.arrayOf( PropTypes.shape({ '@id': PropTypes.string, '@type': PropTypes.string, title: PropTypes.string, description: PropTypes.string, }), ), pathname: PropTypes.string.isRequired, }; /** * Default properties. * @property {Object} defaultProps Default properties. * @static */ static defaultProps = { items: [], searchableText: null, subject: null, path: null, }; constructor(props) { super(props); this.defaultPageSize = config.settings.defaultPageSize; this.state = { currentPage: 1, isClient: false, active: 'relevance' }; } /** * Component did mount * @method componentDidMount * @returns {undefined} */ componentDidMount() { this.doSearch(); this.setState({ isClient: true }); } /** * Component will receive props * @method componentWillReceiveProps * @param {Object} nextProps Next properties * @returns {undefined} */ UNSAFE_componentWillReceiveProps = (nextProps) => { if (this.props.location.search !== nextProps.location.search) { this.doSearch(); } }; /** * Search based on the given searchableText, subject and path. * @method doSearch * @param {string} searchableText The searchable text string * @param {string} subject The subject (tag) * @param {string} path The path to restrict the search to * @returns {undefined} */ doSearch = () => { const options = qs.parse(this.props.history.location.search); this.setState({ currentPage: 1 }); options['use_site_search_settings'] = 1; this.props.searchContent('', { b_size: this.defaultPageSize, ...options, }); }; handleQueryPaginationChange = (e, { activePage }) => { window.scrollTo(0, 0); let options = qs.parse(this.props.history.location.search); options['use_site_search_settings'] = 1; this.setState({ currentPage: activePage }, () => { this.props.searchContent('', { b_size: this.defaultPageSize, ...options, b_start: (this.state.currentPage - 1) * (options.b_size || this.defaultPageSize), }); }); }; onSortChange = (event, sort_order) => { let options = qs.parse(this.props.history.location.search); options.sort_on = event.target.name; options.sort_order = sort_order || 'ascending'; if (event.target.name === 'relevance') { delete options.sort_on; delete options.sort_order; } let searchParams = qs.stringify(options); this.setState({ currentPage: 1, active: event.target.name }, () => { // eslint-disable-next-line no-restricted-globals this.props.history.replace({ search: searchParams, }); }); }; /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { const options = qs.parse(this.props.history.location.search); return ( <Container id="page-search"> <Helmet title={this.props.intl.formatMessage(messages.Search)} /> <div className="container"> <article id="content"> <header> <h1 className="documentFirstHeading"> {this.props.searchableText ? ( <FormattedMessage id="Search results for {term}" defaultMessage="Search results for {term}" values={{ term: <q>{this.props.searchableText}</q>, }} /> ) : ( <FormattedMessage id="Search results" defaultMessage="Search results" /> )} </h1> <SearchTags /> {this.props.search?.items_total > 0 ? ( <div className="items_total"> {this.props.search.items_total}{' '} <FormattedMessage id="results found" defaultMessage="results" /> <Header> <Header.Content className="header-content"> <div className="sort-by"> <FormattedMessage id="Sort By:" defaultMessage="Sort by:" /> </div> <Button onClick={(event) => { this.onSortChange(event); }} name="relevance" size="tiny" className={classNames('button-sort', { 'button-active': this.state.active === 'relevance', })} > <FormattedMessage id="Relevance" defaultMessage="Relevance" /> </Button> <Button onClick={(event) => { this.onSortChange(event); }} name="sortable_title" size="tiny" className={classNames('button-sort', { 'button-active': this.state.active === 'sortable_title', })} > <FormattedMessage id="Alphabetically" defaultMessage="Alphabetically" /> </Button> <Button onClick={(event) => { this.onSortChange(event, 'reverse'); }} name="effective" size="tiny" className={classNames('button-sort', { 'button-active': this.state.active === 'effective', })} > <FormattedMessage id="Date (newest first)" defaultMessage="Date (newest first)" /> </Button> </Header.Content> </Header> </div> ) : ( <div> <FormattedMessage id="No results found" defaultMessage="No results found" /> </div> )} </header> <section id="content-core"> {this.props.items.map((item) => ( <article className="tileItem" key={item['@id']}> <h2 className="tileHeadline"> <UniversalLink item={item} className="summary url" title={item['@type']} > {item.title} </UniversalLink> </h2> {item.description && ( <div className="tileBody"> <span className="description">{item.description}</span> </div> )} <div className="tileFooter"> <UniversalLink item={item}> <FormattedMessage id="Read More…" defaultMessage="Read More…" /> </UniversalLink> </div> <div className="visualClear" /> </article> ))} {this.props.search?.batching && ( <div className="search-footer"> <Pagination activePage={this.state.currentPage} totalPages={Math.ceil( this.props.search.items_total / (options.b_size || this.defaultPageSize), )} onPageChange={this.handleQueryPaginationChange} firstItem={null} lastItem={null} prevItem={{ content: <Icon name={paginationLeftSVG} size="18px" />, icon: true, 'aria-disabled': !this.props.search.batching.prev, className: !this.props.search.batching.prev ? 'disabled' : null, }} nextItem={{ content: <Icon name={paginationRightSVG} size="18px" />, icon: true, 'aria-disabled': !this.props.search.batching.next, className: !this.props.search.batching.next ? 'disabled' : null, }} /> </div> )} </section> </article> </div> {this.state.isClient && createPortal( <Toolbar pathname={this.props.pathname} hideDefaultViewButtons inner={<span />} />, document.getElementById('toolbar'), )} </Container> ); } } export const __test__ = compose( injectIntl, connect( (state, props) => ({ items: state.search.items, searchableText: qs.parse(props.history.location.search).SearchableText, pathname: props.history.location.pathname, }), { searchContent }, ), )(Search); export default compose( injectIntl, connect( (state, props) => ({ items: state.search.items, searchableText: qs.parse(props.history.location.search).SearchableText, pathname: props.location.pathname, }), { searchContent }, ), asyncConnect([ { key: 'search', promise: ({ location, store: { dispatch } }) => dispatch( searchContent('', { ...qs.parse(location.search), use_site_search_settings: 1, }), ), }, ]), )(Search);