UNPKG

design-comuni-plone-theme

Version:
949 lines (919 loc) 34.5 kB
/* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useLocation } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import mapValues from 'lodash/mapValues'; import toPairs from 'lodash/toPairs'; import fromPairs from 'lodash/fromPairs'; import cx from 'classnames'; import moment from 'moment'; import qs from 'query-string'; import { Modal, ModalHeader, ModalBody, Container, Row, Col, Button, ButtonToolbar, Nav, NavItem, NavLink, TabContent, TabPane, FormGroup, Input, Label, Toggle, Spinner, } from 'design-react-kit'; import { Icon } from 'design-comuni-plone-theme/components/ItaliaTheme'; import { getSearchFilters } from 'design-comuni-plone-theme/actions'; import { SearchUtils, Checkbox } from 'design-comuni-plone-theme/components'; const { defaultOptions, isGroupChecked, isGroupIndeterminate, updateGroupCheckedStatus, parseFetchedSections, parseFetchedTopics, parseFetchedOptions, getSearchParamsURL, setSectionFilterChecked, setGroupChecked, } = SearchUtils; const messages = defineMessages({ closeSearch: { id: 'closeSearch', defaultMessage: 'Chiudi cerca', }, backToSearch: { id: 'backToSearch', defaultMessage: 'Torna alla ricerca', }, search: { id: 'search', defaultMessage: 'Cerca', }, searchLabel: { id: 'searchLabel', defaultMessage: 'Cerca nel sito', }, filters: { id: 'filters', defaultMessage: 'Filtri', }, sections: { id: 'sections', defaultMessage: 'Sezioni', }, topics: { id: 'topics', defaultMessage: 'Argomenti', }, options: { id: 'options', defaultMessage: 'Opzioni', }, allFilters: { id: 'allFilters', defaultMessage: 'Tutto', }, searchAllSections: { id: 'searchAllSections', defaultMessage: 'Cerca in tutte le sezioni', }, searchInSection: { id: 'searchInSection', defaultMessage: 'Cerca nella sezione', }, deselectSection: { id: 'deselectSearchSection', defaultMessage: 'Non cercare nella sezione', }, advandedSectionsFilters: { id: 'advandedSectionsFilters', defaultMessage: 'Vai alla ricerca per sezioni avanzata', }, allTopics: { id: 'allTopics', defaultMessage: 'Tutti gli argomenti', }, searchAllTopics: { id: 'searchAllTopics', defaultMessage: 'Cerca tutti gli argomenti', }, selectTopicFilters: { id: 'selectTopicFilters', defaultMessage: 'Seleziona gli argomenti che vuoi cercare', }, allOptions: { id: 'allOptions', defaultMessage: 'Tutte le date', }, setMoreSearchOptions: { id: 'setMoreSearchOptions', defaultMessage: 'Vai alle altre opzioni di ricerca', }, removeTopic: { id: 'removeTopic', defaultMessage: 'Rimuovi argomento', }, removeOption: { id: 'removeOption', defaultMessage: 'Rimuovi opzione', }, optionActiveContentButton: { id: 'optionActiveContentButton', defaultMessage: 'Contenuto attivo', }, optionActiveContentLabel: { id: 'optionActiveContentLabel', defaultMessage: 'Cerca solo tra i contenuti attivi', }, optionActiveContentInfo: { id: 'optionActiveContentInfo', defaultMessage: 'Verranno esclusi dalla ricerca i contenuti archiviati e non più validi come gli eventi terminati o i bandi scaduti.', }, optionDateStart: { id: 'optionDateStart', defaultMessage: 'Data inizio', }, optionDateEnd: { id: 'optionDateEnd', defaultMessage: 'Data fine', }, optionDateStartButton: { id: 'optionDateStartButton', defaultMessage: 'Dal', }, optionDateEndButton: { id: 'optionDateEndButton', defaultMessage: 'Al', }, optionDatePlaceholder: { id: 'optionDatePlaceholder', defaultMessage: 'inserisci la data in formato gg/mm/aaaa', }, current: { id: 'currentActive', defaultMessage: 'attivo', }, searchLabel: { id: 'searchLabel', defaultMessage: 'Cerca nel sito', }, }); const SearchModal = ({ closeModal, show }) => { const intl = useIntl(); const dispatch = useDispatch(); const location = useLocation(); const [redirectingToResults, setRedirectingToResults] = useState(false); const [advancedSearch, setAdvancedSearch] = useState(false); const [advancedTab, setAdvancedTab] = useState(null); const [searchableText, setSearchableText] = useState( qs.parse(location.search)?.SearchableText ?? '', ); const [sections, setSections] = useState({}); const [topics, setTopics] = useState({}); const [options, setOptions] = useState({ ...defaultOptions }); const subsite = useSelector((state) => state.subsite?.data); const selectedTopics = fromPairs(toPairs(topics).filter((t) => t[1].value)); const inputRef = React.useRef(null); let checkedGroups = {}; Object.keys(sections).forEach((k) => { checkedGroups[k] = isGroupChecked(sections[k]); }); const searchFilters = useSelector((state) => state.searchFilters.result); useEffect(() => { if (!searchFilters || Object.keys(searchFilters).length === 0) dispatch(getSearchFilters()); }, []); const handleBackTabbingFromPane = (event) => { if (event.shiftKey && event.key === 'Tab') { const activeTab = document.querySelectorAll('[aria-selected=true] a')[0]; if (!activeTab) { return; } const firstSectionItem = document.querySelectorAll( '[role="tabpanel"][aria-expanded="true"] input', )[0]; const lastFocusedElement = document.activeElement; const modal = document.querySelectorAll('div.modal[role="dialog"]')[0]; // That input is nasty, make exception in logic const isModalContainer = modal === lastFocusedElement || lastFocusedElement === document.getElementById('options-date-end'); if ( lastFocusedElement === firstSectionItem || !lastFocusedElement || isModalContainer ) { event.preventDefault(); // Prevent default Shift+Tab behavior activeTab.focus(); } } }; useEffect(() => { document.addEventListener('keydown', handleBackTabbingFromPane); return () => { document.removeEventListener('keydown', handleBackTabbingFromPane); }; }, []); useEffect(() => { if (show) { setTimeout(() => { inputRef.current && inputRef.current.focus(); //setta il focus sul campo di ricerca all'apertura della modale }, 100); document.body.classList.add('search-modal-opened'); //to prevent scroll body } else { document.body.classList.remove('search-modal-opened'); //re-enable scroll body } }, [show, inputRef]); useEffect(() => { if (Object.keys(searchFilters?.sections ?? {}).length > 0) { let pfs = parseFetchedSections(searchFilters.sections, location, subsite); setSections(pfs); } if (searchFilters?.topics?.length > 0) { setTopics(parseFetchedTopics(searchFilters.topics, location)); } setOptions(parseFetchedOptions(defaultOptions, location)); }, [searchFilters, location, subsite]); // The "all" filter is checked if all groups are unchecked const allSectionsChecked = Object.keys(checkedGroups).reduce( (checked, groupId) => checked && !checkedGroups[groupId], true, ); const allTopicsChecked = Object.keys(selectedTopics).length === 0; const allOptionsSet = !options.activeContent && !options.dateStart && !options.dateEnd; const resetSections = () => { setSections((prevSections) => mapValues(prevSections, (group) => ({ ...group, items: updateGroupCheckedStatus(group, false), })), ); }; const resetTopics = () => { setTopics((prevTopics) => mapValues(prevTopics, (topic) => ({ ...topic, value: false, })), ); }; const setTopicChecked = (topicId, checked) => { setTopics((prevTopics) => ({ ...prevTopics, [topicId]: { ...prevTopics[topicId], value: checked, }, })); }; const resetOptions = () => setOptions({ ...defaultOptions }); const setOptionValue = (optId, value) => setOptions((prevOptions) => ({ ...prevOptions, [optId]: value })); const submitSearch = () => { setRedirectingToResults(true); setAdvancedSearch(false); // setTimeout(() => { // closeModal(); // }, 500); }; const handleEnterSearch = (e) => { if (e.key === 'Enter') { submitSearch(); if (__CLIENT__) { window.location.href = window.location.origin + getSearchParamsURL( searchableText, sections, topics, options, {}, null, null, null, subsite, intl.locale, ); } } }; return ( <Modal wrapClassName="public-ui" id="search-modal" isOpen={show} toggle={closeModal} role="alertdialog" > <ModalHeader toggle={closeModal}> <Container> <div className="d-flex align-items-center"> {!advancedSearch && ( <Button color="link" title={intl.formatMessage(messages.closeSearch)} onClick={closeModal} > <Icon color="" icon="it-arrow-left-circle" padding={false} /> </Button> )} {advancedSearch && ( <Button color="link" title={intl.formatMessage(messages.backToSearch)} className="back-to-search text-reset" onClick={() => setAdvancedSearch(false)} > <Icon color="" icon="it-arrow-left-circle" padding={false} /> </Button> )} <p className="modal-title-centered h1"> {intl.formatMessage( advancedSearch ? messages.filters : messages.search, )} </p> <a href={getSearchParamsURL( searchableText, sections, topics, options, {}, null, null, null, subsite, intl.locale, false, )} className="ms-auto btn btn-outline-primary text-capitalize" style={{ visibility: advancedSearch ? 'visible' : 'hidden' }} onClick={submitSearch} > {intl.formatMessage(messages.search)} </a> </div> </Container> </ModalHeader> <ModalBody> <Container> {!advancedSearch && ( <> <div className="search-filters search-filters-text"> <div className="form-group"> <div className="input-group mb-3" role="search" aria-label={intl.formatMessage(messages.searchLabel)} > <input id="search-text" type="text" value={searchableText} onChange={(e) => setSearchableText(e.target.value)} onKeyDown={handleEnterSearch} className="form-control" placeholder={intl.formatMessage(messages.searchLabel)} aria-label={intl.formatMessage(messages.searchLabel)} aria-describedby="search-button" ref={inputRef} /> <a href={getSearchParamsURL( searchableText, sections, topics, options, {}, null, null, null, subsite, intl.locale, false, )} onClick={submitSearch} className="btn btn-link rounded-0 py-0" title={intl.formatMessage(messages.search)} id="search-button" > <Icon icon="it-search" aria-hidden={true} size="sm" /> </a> </div> </div> </div> {Object.keys(sections)?.length > 0 && ( <div className="search-filters search-filters-section"> <div className="h6"> {intl.formatMessage(messages.sections)} </div> <ButtonToolbar className="mb-3"> <Button color="primary" outline={!allSectionsChecked} onClick={resetSections} size="sm" className="me-2 mb-2" aria-label={intl.formatMessage( messages.searchAllSections, )} > {intl.formatMessage(messages.allFilters)} </Button> {Object.keys(sections).map((groupId) => ( <Button key={groupId} color="primary" outline={!checkedGroups[groupId]} size="sm" className="me-2 mb-2" aria-label={ (!checkedGroups[groupId] ? intl.formatMessage(messages.searchInSection) : intl.formatMessage(messages.deselectSection)) + ' ' + sections[groupId].title } onClick={() => setGroupChecked( groupId, !checkedGroups[groupId], setSections, ) } > {sections[groupId].title} </Button> ))} <Button color="primary" outline size="sm" className="mb-2" onClick={() => { setAdvancedTab('sections'); setAdvancedSearch(true); }} aria-label={intl.formatMessage( messages.advandedSectionsFilters, )} > ... </Button> </ButtonToolbar> </div> )} <div className="search-filters search-filters-topics"> <div className="h6">{intl.formatMessage(messages.topics)}</div> <button className={cx('chip chip-simple chip-lg me-2 mb-2', { selected: allTopicsChecked, })} type="button" onClick={resetTopics} aria-label={intl.formatMessage(messages.searchAllTopics)} > <span className="chip-label"> {intl.formatMessage(messages.allTopics)} </span> </button> {Object.keys(selectedTopics).map((topicId) => ( <div role="presentation" className="chip chip-lg selected" key={topicId} onClick={() => setTopicChecked(topicId, false)} > <span className="chip-label"> {selectedTopics[topicId].label} </span> <button type="button"> <Icon color="" icon="it-close" padding={false} /> <span className="visually-hidden"> {intl.formatMessage(messages.removeTopic)}{' '} {selectedTopics[topicId].label} </span> </button> </div> ))} <button className="chip chip-lg" onClick={() => { setAdvancedTab('topics'); setAdvancedSearch(true); }} type="button" aria-label={intl.formatMessage(messages.selectTopicFilters)} > <span className="chip-label">...</span> </button> </div> <div className="search-filters search-filters-options"> <div className="h6">{intl.formatMessage(messages.options)}</div> <button className={cx('chip chip-simple chip-lg me-2 mb-2', { selected: allOptionsSet, })} type="button" onClick={resetOptions} > <span className="chip-label"> {intl.formatMessage(messages.allOptions)} </span> </button> {options.activeContent && ( <div role="presentation" className="chip chip-lg selected" onClick={() => setOptionValue('activeContent', false)} > <span className="chip-label"> {intl.formatMessage(messages.optionActiveContentButton)} </span> <button type="button"> <Icon color="" icon="it-close" padding={false} /> <span className="visually-hidden"> {intl.formatMessage(messages.removeOption)}{' '} {intl.formatMessage(messages.optionActiveContentButton)} </span> </button> </div> )} {options.dateStart && ( <div role="presentation" className="chip chip-lg selected" onClick={() => setOptionValue('dateStart', null)} > <span className="chip-label"> {`${intl.formatMessage( messages.optionDateStartButton, )} ${moment(options.dateStart) .locale(intl.locale) .format('LL')}`} </span> <button type="button"> <Icon color="" icon="it-close" padding={false} /> <span className="visually-hidden"> {intl.formatMessage(messages.removeOption)}{' '} {`${intl.formatMessage( messages.optionDateStartButton, )} ${moment(options.dateStart) .locale(intl.locale) .format('LL')}`} </span> </button> </div> )} {options.dateEnd && ( <div role="presentation" className="chip chip-lg selected" onClick={() => setOptionValue('dateEnd', null)} > <span className="chip-label"> {`${intl.formatMessage( messages.optionDateEndButton, )} ${moment(options.dateEnd) .locale(intl.locale) .format('LL')}`} </span> <button type="button"> <Icon color="" icon="it-close" padding={false} /> <span className="visually-hidden"> {intl.formatMessage(messages.removeOption)}{' '} {`${intl.formatMessage( messages.optionDateEndButton, )} ${moment(options.dateEnd) .locale(intl.locale) .format('LL')}`} </span> </button> </div> )} <button className="chip chip-lg" onClick={() => { setAdvancedTab('options'); setAdvancedSearch(true); }} type="button" aria-label={intl.formatMessage(messages.setMoreSearchOptions)} > <span className="chip-label">...</span> </button> </div> <div className="search-filters text-center"> <a href={getSearchParamsURL( searchableText, sections, topics, options, {}, null, null, null, subsite, intl.locale, false, )} onClick={submitSearch} className="btn-icon btn btn-primary" title={intl.formatMessage(messages.search)} > <Icon icon="it-search" aria-hidden={true} size="sm" /> <span className="ms-2"> {intl.formatMessage(messages.search)} </span> </a> </div> </> )} {advancedSearch && ( <div> <Nav tabs className="mb-3 nav-fill" role="tablist"> {Object.keys(sections)?.length > 0 && ( <NavItem role="tab" aria-controls="sections-tab" aria-selected={advancedTab === 'sections'} > <NavLink href="#" active={advancedTab === 'sections'} onClick={() => setAdvancedTab('sections')} id={'sections'} > <span>{intl.formatMessage(messages.sections)}</span> {advancedTab === 'sections' && ( <span className="visually-hidden"> {' '} {intl.formatMessage(messages.current)} </span> )} </NavLink> </NavItem> )} <NavItem role="tab" aria-controls="topics-tab" aria-selected={advancedTab === 'topics'} > <NavLink href="#" active={advancedTab === 'topics'} onClick={() => setAdvancedTab('topics')} id={'topics'} > <span>{intl.formatMessage(messages.topics)}</span> {advancedTab === 'topics' && ( <span className="visually-hidden"> {' '} {intl.formatMessage(messages.current)} </span> )} </NavLink> </NavItem> <NavItem role="tab" aria-controls="options-tab" aria-selected={advancedTab === 'options'} > <NavLink href="#" active={advancedTab === 'options'} onClick={() => setAdvancedTab('options')} id={'options'} > <span>{intl.formatMessage(messages.options)}</span> {advancedTab === 'options' && ( <span className="visually-hidden"> {' '} {intl.formatMessage(messages.current)} </span> )} </NavLink> </NavItem> </Nav> <TabContent activeTab={advancedTab}> {Object.keys(sections)?.length > 0 && ( <TabPane className="p-3" tabId="sections" role="tabpanel" id="sections-tab" aria-expanded={advancedTab === 'sections'} hidden={!(advancedTab === 'sections')} > <Row> <div className="offset-lg-2 col-lg-8 offset-md-1 col-md-10 col-sm-12"> <Row> {Object.keys(sections).map((groupId) => ( <Col sm={6} key={groupId} className="group-col"> <div role="tablist"> <FormGroup check tag="div"> <Checkbox id={`modal-search-${groupId}`} indeterminate={isGroupIndeterminate( sections[groupId], checkedGroups[groupId], )} checked={checkedGroups[groupId]} onChange={(e) => setGroupChecked( groupId, e.currentTarget.checked, setSections, ) } role="tab" aria-controls={groupId + '-section'} aria-selected={checkedGroups[groupId]} /> <Label check for={`modal-search-${groupId}`} tag="label" className={cx( 'group-head fw-bold text-primary', { indeterminate: isGroupIndeterminate( sections[groupId], checkedGroups[groupId], ), }, )} widths={['xs', 'sm', 'md', 'lg', 'xl']} > {sections[groupId].title} </Label> </FormGroup> {Object.keys(sections[groupId].items).map( (filterId) => ( <FormGroup check tag="div" key={filterId} role="tabpanel" id={groupId + '-section'} aria-label={sections[groupId].title} > <Checkbox id={`modal-search-${filterId}`} checked={ sections[groupId].items[filterId] .value } onChange={(e) => setSectionFilterChecked( groupId, filterId, e.currentTarget.checked, setSections, ) } /> <Label check for={`modal-search-${filterId}`} tag="label" widths={['xs', 'sm', 'md', 'lg', 'xl']} > { sections[groupId].items[filterId] .label } </Label> </FormGroup> ), )} </div> </Col> ))} </Row> </div> </Row> </TabPane> )} <TabPane className="p-3" tabId="topics" role="tabpanel" id="topics-tab" aria-expanded={advancedTab === 'topics'} hidden={!(advancedTab === 'topics')} > <Row> <div className="offset-lg-2 col-lg-8 offset-md-1 col-md-10 col-sm-12"> <div className="group-col columns"> {Object.keys(topics).map((topicId) => ( <div key={topicId}> <FormGroup check tag="div"> <Input id={`modal-search-${topicId}`} type="checkbox" checked={topics[topicId].value} onChange={(e) => setTopicChecked( topicId, e.currentTarget.checked, ) } /> <Label check for={`modal-search-${topicId}`} tag="label" widths={['xs', 'sm', 'md', 'lg', 'xl']} > {topics[topicId].label} </Label> </FormGroup> </div> ))} </div> </div> </Row> </TabPane> <TabPane className="p-3" tabId="options" role="tabpanel" id="options-tab" aria-expanded={advancedTab === 'options'} hidden={!(advancedTab === 'options')} > <Row> <div className="offset-lg-2 col-lg-8 offset-md-1 col-md-10 col-sm-12"> <FormGroup check className="form-check-group mb-5"> <Toggle label={intl.formatMessage( messages.optionActiveContentLabel, )} checked={options.activeContent} onChange={(e) => setOptionValue('activeContent', e.target?.checked) } /> <p className="small"> {intl.formatMessage(messages.optionActiveContentInfo)} </p> </FormGroup> <div className="row mt-5"> <FormGroup className="col-sm"> <Input id="options-date-start" type="date" label={intl.formatMessage(messages.optionDateStart)} placeholder={intl.formatMessage( messages.optionDatePlaceholder, )} value={options.dateStart} onChange={(e) => setOptionValue('dateStart', e.target.value) } /> </FormGroup> <FormGroup className="col-sm"> <Input id="options-date-end" type="date" label={intl.formatMessage(messages.optionDateEnd)} placeholder={intl.formatMessage( messages.optionDatePlaceholder, )} value={options.dateEnd} onChange={(e) => setOptionValue('dateEnd', e.target.value) } /> </FormGroup> </div> </div> </Row> </TabPane> </TabContent> </div> )} </Container> {redirectingToResults && ( <div className="overlay loading-results"> <Spinner active /> </div> )} </ModalBody> </Modal> ); }; export default SearchModal;