UNPKG

addsearch-search-ui

Version:

JavaScript library to develop Search UIs for the web

339 lines (290 loc) 10 kB
import './rangefacets.scss'; import handlebars from 'handlebars'; import { setActiveRangeFacets, toggleRangeFacetFilter } from '../../actions/filters'; import { observeStoreByKey } from '../../store'; import { validateContainer } from '../../util/dom'; import { createFilterObject } from "../filters/filterstateobserver"; import { clearFieldStats, setFieldStats } from "../../actions/fieldstats"; import { roundDownToNearestTenth, roundUpToNearestTenth } from "../../util/maths"; import { isEmpty } from "../../util/objects"; import PRECOMPILED_RANGE_FACETS_TEMPLATE from './precompile-templates/rangefacets.handlebars'; import PRECOMPILED_SLIDER_RANGE_FACETS_TEMPLATE from './precompile-templates/sliderrangefacets.handlebars'; import { RANGE_FACETS_TYPE } from './index'; import UiRangeSlider from "../../util/sliders"; export default class RangeFacets { constructor(client, reduxStore, conf) { this.client = client; this.reduxStore = reduxStore; this.conf = conf; this.maxNumberOfRangeBuckets = this.conf.maxNumberOfRangeBuckets || 5; this.ranges = []; var IGNORE_RENDERING_ON_REQUEST_BY = [ 'component.loadMore', 'component.pagination', 'component.sortby' ]; if (this.conf.type === RANGE_FACETS_TYPE.SLIDER) { this.maxNumberOfRangeBuckets = 1; this.conf.styles = this.conf.styles || { trackColor: '#C6C6C6', progressColor: '#25daa5' }; } else { this.conf.type = RANGE_FACETS_TYPE.CHECKBOX; } function _hasActiveFacet() { var activeRangeFacets = reduxStore.getState().filters.activeRangeFacets[conf.field]; if (activeRangeFacets) { return !isEmpty(activeRangeFacets); } else { return false; } } function _buildRanges(min, max, numberOfBuckets) { const minTransformed = min >= 0 ? roundDownToNearestTenth(min) : roundUpToNearestTenth(min * -1) * -1; const maxTransformed = max >= 0 ? roundUpToNearestTenth(max) : roundDownToNearestTenth(max * -1) * -1; const ranges = []; let current = minTransformed; const step = roundUpToNearestTenth((maxTransformed - minTransformed) / numberOfBuckets); for (var i = 0; i < numberOfBuckets; i++) { ranges.push({ from: current, to: current + step }); current += step; } return ranges; } function _buildActiveRanges(activeRangeFacets) { const ranges = []; for (const key in activeRangeFacets) { ranges.push({ from: activeRangeFacets[key].gte, to: activeRangeFacets[key].lt }); } return ranges; } if (validateContainer(conf.containerId)) { observeStoreByKey(this.reduxStore, 'search', (search) => { const isActive = _hasActiveFacet(); if (!search.started || search.loading || (search.callBy === this.conf.field && isActive) || IGNORE_RENDERING_ON_REQUEST_BY.indexOf(search.callBy) > -1) { return; } if (!search.results.hits || !search.results.hits.length) { this.renderClear(); return; } if (isActive && this.conf.type === RANGE_FACETS_TYPE.SLIDER) { var activeSliderRange = this.getActiveRangeFacets(this.conf.field)[0]; this.reduxStore.dispatch(setFieldStats({ [this.conf.field]: { min: activeSliderRange.gte, max: activeSliderRange.lte } }, this.conf.field)); } else if (isActive && this.conf.type === RANGE_FACETS_TYPE.CHECKBOX) { const filterObjectCustom = createFilterObject( this.reduxStore.getState().filters, this.reduxStore.getState().configuration.baseFilters, this.conf.field ); this.client.fetchCustomApi(this.conf.field, filterObjectCustom, res => { this.reduxStore.dispatch(setFieldStats(res.fieldStats, this.conf.field)); }); } else if (search.callBy === 'component.activeFilters') { this.reduxStore.dispatch(clearFieldStats()); this.reduxStore.dispatch(setFieldStats(search.results.fieldStats, this.conf.field)); } else { this.reduxStore.dispatch(setFieldStats(search.results.fieldStats, this.conf.field)); } this.handleCheckboxStates(false); }); observeStoreByKey(this.reduxStore, 'fieldstats', (state) => { var fieldStats = state.fieldStats[this.conf.field]; if (state.callBy !== this.conf.field || fieldStats === undefined) { return; } if (fieldStats === null) { this.renderRangeSlider(); return; } if (!_hasActiveFacet()) { this.ranges = _buildRanges(fieldStats.min, fieldStats.max, this.maxNumberOfRangeBuckets); } else { this.ranges = _buildActiveRanges(this.reduxStore.getState().filters.activeRangeFacets[this.conf.field]); } if (this.conf.type === RANGE_FACETS_TYPE.SLIDER) { this.renderRangeSlider(state); return; } var rangeFacetsOptions = { field: this.conf.field, ranges: this.ranges }; const filterObjectCustom = createFilterObject( this.reduxStore.getState().filters, this.reduxStore.getState().configuration.baseFilters, this.conf.field ); this.client.fetchRangeFacets(rangeFacetsOptions, filterObjectCustom, res => { this.render(res); }); }); } } setRangeFilter(key, valueMin, valueMax) { // Dispatch facet and refresh search const values = { min: valueMin, max: valueMax }; this.reduxStore.dispatch(toggleRangeFacetFilter(this.conf.field, values, key, true)); } setRangeSlider(activeRange) { this.reduxStore.dispatch(setActiveRangeFacets( buildRangeFacetsJson(this.conf.field, activeRange[0], activeRange[1]), true, this.conf.field ) ); } render(results) { const container = document.getElementById(this.conf.containerId); if (results) { // render data const data = { conf: this.conf, rangeFacets: results.rangeFacets[this.conf.field] }; let html; if (this.conf.precompiledTemplate) { html = this.conf.precompiledTemplate(data); } else if (this.conf.template) { html = handlebars.compile(this.conf.template)(data); } else { html = PRECOMPILED_RANGE_FACETS_TEMPLATE(data); } container.innerHTML = html; } else { container.innerHTML = ''; } this.handleCheckboxStates(true); } renderClear() { this.reduxStore.dispatch(clearFieldStats()); const container = document.getElementById(this.conf.containerId); container.innerHTML = ''; } renderRangeSlider(results) { const _this = this; const container = document.getElementById(this.conf.containerId); const data = { conf: this.conf, }; if (!results) { container.innerHTML = ''; return; } const activeRangeFacets = this.getActiveRangeFacets(this.conf.field); data.sliderConfig = Object.assign({}, getSliderRange(results, this.conf.field), getSelectedSliderRange(activeRangeFacets) ); let html; if (this.conf.precompiledTemplate) { html = this.conf.precompiledTemplate(data); } else if (this.conf.template) { html = handlebars.compile(this.conf.template)(data); } else { html = PRECOMPILED_SLIDER_RANGE_FACETS_TEMPLATE(data); } container.innerHTML = html; UiRangeSlider.init( this.conf.containerId, function (data) { if (data.activeRange.length) { _this.setRangeSlider(data.activeRange); } }, { styles: { trackColor: this.conf.styles.trackColor, progressColor: this.conf.styles.progressColor }, step: this.conf.step || 1 } ); } handleCheckboxStates(attachEvent) { const container = document.getElementById(this.conf.containerId); const activeRangeFacets = this.getActiveRangeFacets(this.conf.field); const options = container.getElementsByTagName('input'); for (let i = 0; i < options.length; i++) { let checkbox = options[i]; checkbox.checked = !!activeRangeFacets.find((facet) => { return facet.key === checkbox.value; }); if (attachEvent) { checkbox.onchange = (e) => { this.setRangeFilter( e.target.value, e.target.getAttribute('data-value-min'), e.target.getAttribute('data-value-max') ); }; } } } getActiveRangeFacets(facetField) { // Read active facets from redux state let activeRangeFacets = []; const activeRangeFacetState = this.reduxStore.getState().filters.activeRangeFacets; if (activeRangeFacetState[facetField]) { for (let key in activeRangeFacetState[facetField]) { activeRangeFacets.push(activeRangeFacetState[facetField][key]); } } return activeRangeFacets; } } export function getSliderRange(data, field) { if (!data.fieldStats && !data.fieldStats[field]) { return {min: null, max: null}; } return { min: data.fieldStats[field].min, max: data.fieldStats[field].max }; } export function getSelectedSliderRange(selectedFacetsGroup) { let min; let max; selectedFacetsGroup.forEach(range => { const start = range.gte; const end = range.lte; if (min === undefined || start < min) { min = start; } if (max === undefined || end > max) { max = end; } }); return { start: min, end: max }; } export function buildRangeFacetsJson(field, valueStart, valueEnd) { const facetKey = `${valueStart}-${valueEnd}`; return { [field]: { [facetKey]: { "gte": valueStart, "lte": valueEnd } } } }