diff --git a/README.md b/README.md index af585f2..5f47847 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ csvTemplateFormat | boolean | apply template formatting to data before csv expor defaultStyles | boolean | apply default styles from style.css | true hiddenColumns | array | columns that should not display | [] nPaginateRows | number | items per page setting | 25 -solo | object | item that should be displayed solo | null +solo | object | active solo filters by dimension | {} sortBy | string | name of column to use for record sort | null sortDir | string | sort direction, either 'asc' or 'desc' | 'asc' tableClassName | string | assign css class to table containing react-pivot elements | '' diff --git a/index.jsx b/index.jsx index 6896fdf..869c4e0 100644 --- a/index.jsx +++ b/index.jsx @@ -13,6 +13,10 @@ import PivotTable from './lib/pivot-table.jsx' import Dimensions from './lib/dimensions.jsx' import ColumnControl from './lib/column-control.jsx' import SoloControl from './lib/solo-control.jsx' +import { + serializeSoloValue, + createSoloFilter +} from './lib/solo-utils.js' const _ = { filter, map, find } @@ -89,6 +93,10 @@ export default createReactClass({ this.updateRows() } + + if (this.props.solo !== prevProps.solo) { + this.setState({solo: this.props.solo}, this.updateRows) + } }, getColumns: function() { @@ -185,20 +193,15 @@ export default createReactClass({ compact: this.props.compact } - var filter = this.state.solo - if (filter) { - calcOpts.filter = function(dVals) { - var pass = true - Object.keys(filter).forEach(function (title) { - if (dVals[title] !== filter[title]) pass = false - }) - return pass - } + var soloFilter = createSoloFilter(this.state.solo, this.state.dimensions) + if (soloFilter) { + calcOpts.filter = soloFilter } var rows = this.dataFrame .calculate(calcOpts) .filter(function (row) { return hideRows ? !hideRows(row) : true }) + this.setState({rows: rows}) this.props.onData(rows) }, @@ -232,21 +235,59 @@ export default createReactClass({ }, setSolo: function(solo) { + if (!solo || typeof solo !== 'object') return + + var dimension = solo.title + if (!dimension) return + + var valueKey = serializeSoloValue(solo.value) + if (!valueKey) return + var newSolo = Object.assign({}, this.state.solo) - newSolo[solo.title] = solo.value + var valueMap = newSolo[dimension] || {} + + if (Object.prototype.hasOwnProperty.call(valueMap, valueKey)) { + newSolo[dimension] = this.removeSoloValue(valueMap, valueKey) + if (!newSolo[dimension]) delete newSolo[dimension] + } else { + newSolo[dimension] = this.addSoloValue(valueMap, valueKey) + } + this.props.eventBus.emit('solo', newSolo) - this.setState({solo: newSolo }) - setTimeout(this.updateRows, 0) + this.setState({solo: newSolo}, this.updateRows) + }, + + addSoloValue: function(valueMap, key) { + var updated = Object.assign({}, valueMap) + updated[key] = true + return updated + }, + + removeSoloValue: function(valueMap, key) { + var updated = Object.assign({}, valueMap) + delete updated[key] + return Object.keys(updated).length > 0 ? updated : null }, - clearSolo: function(title) { - if (typeof title === 'undefined' || title === null) return + clearSolo: function(payload) { + if (!payload) return + + // If clearing a specific value, just toggle it + if (typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'value')) { + this.setSolo({title: payload.title, value: payload.value}) + return + } + + // Otherwise, clear the entire dimension + var dimension = typeof payload === 'string' ? payload : payload.title + if (!dimension) return var newSolo = Object.assign({}, this.state.solo) - delete newSolo[title] + if (!Object.prototype.hasOwnProperty.call(newSolo, dimension)) return + + delete newSolo[dimension] this.props.eventBus.emit('solo', newSolo) - this.setState({solo: newSolo}) - setTimeout(this.updateRows, 0) + this.setState({solo: newSolo}, this.updateRows) }, hideColumn: function(cTitle) { diff --git a/lib/solo-control.jsx b/lib/solo-control.jsx index 778cb7a..77f967b 100644 --- a/lib/solo-control.jsx +++ b/lib/solo-control.jsx @@ -1,5 +1,19 @@ import React from 'react' import createReactClass from 'create-react-class' +import { soloEntries, safeParseSoloPayload } from './solo-utils.js' + +function formatSoloValue(value) { + if (value === null) return 'null' + if (typeof value === 'object') { + try { + return JSON.stringify(value) + } catch (err) { + return '[object]' + } + } + + return String(value) +} export default createReactClass({ getDefaultProps: function () { @@ -10,7 +24,7 @@ export default createReactClass({ }, render: function () { - var entries = Object.keys(this.props.solo) + var entries = soloEntries(this.props.solo) if (!entries.length) { return ( @@ -18,14 +32,25 @@ export default createReactClass({ ) } - var options = entries.map(function(title) { - var value = this.props.solo[title] - var labelValue = typeof value === 'object' && value !== null - ? JSON.stringify(value) - : String(value) - var label = title + ': ' + labelValue - return - }, this) + var options = entries.map(function(entry) { + var valueLabel = formatSoloValue(entry.value) + var label = entry.title + ': ' + valueLabel + + var payload + try { + payload = JSON.stringify({title: entry.title, value: entry.value}) + } catch (err) { + return null + } + + return + }).filter(Boolean) + + if (!options.length) { + return ( +
+ ) + } return (
@@ -38,9 +63,10 @@ export default createReactClass({ }, handleClear: function (evt) { - var title = evt.target.value - if (!title) return + var payload = safeParseSoloPayload(evt.target.value) + if (!payload) return - this.props.onClear(title) + evt.target.value = '' + this.props.onClear(payload) } }) diff --git a/lib/solo-utils.js b/lib/solo-utils.js new file mode 100644 index 0000000..22955ac --- /dev/null +++ b/lib/solo-utils.js @@ -0,0 +1,108 @@ +export function serializeSoloValue(value) { + try { + return JSON.stringify(value) + } catch (err) { + return null + } +} + +export function normalizeSolo(raw) { + if (!raw || typeof raw !== 'object') return {} + + var normalized = {} + + Object.keys(raw).forEach(function(dimension) { + var valueMap = raw[dimension] + if (!valueMap || typeof valueMap !== 'object' || Array.isArray(valueMap)) return + + var filteredMap = {} + Object.keys(valueMap).forEach(function(key) { + if (valueMap[key]) filteredMap[key] = true + }) + + if (Object.keys(filteredMap).length) normalized[dimension] = filteredMap + }) + + return normalized +} + +export function soloEntries(solo) { + if (!solo || typeof solo !== 'object') return [] + + var entries = [] + + Object.keys(solo).forEach(function(dimension) { + var valueMap = solo[dimension] + if (!valueMap || typeof valueMap !== 'object') return + + Object.keys(valueMap).forEach(function(key) { + if (!valueMap[key]) return + + var value + try { + value = JSON.parse(key) + } catch (err) { + return + } + + entries.push({ title: dimension, key: key, value: value }) + }) + }) + + return entries +} + +export function createSoloFilter(solo, dimensions) { + if (!solo || typeof solo !== 'object') return null + if (!Array.isArray(dimensions)) return null + + var activeDimensions = dimensions.filter(function(dimension) { + var valueMap = solo[dimension] + return valueMap && typeof valueMap === 'object' && Object.keys(valueMap).length > 0 + }) + + if (!activeDimensions.length) return null + + return function(dimensionValues) { + return matchesSoloFilters(dimensionValues, solo, activeDimensions) + } +} + +export function matchesSoloFilters(dimensionValues, soloFilters, soloTitles) { + return soloTitles.every(function(title) { + var valueMap = soloFilters[title] + if (!valueMap) return true + + if (!Object.prototype.hasOwnProperty.call(dimensionValues, title)) return true + + var key = serializeSoloValue(dimensionValues[title]) + if (!key) return false + + return Object.prototype.hasOwnProperty.call(valueMap, key) + }) +} + +export function isSoloValueActive(solo, title, value) { + var key = serializeSoloValue(value) + if (!key) return false + + var valueMap = solo && solo[title] + if (!valueMap || typeof valueMap !== 'object') return false + + return Object.prototype.hasOwnProperty.call(valueMap, key) +} + +export function soloMapsEqual(a, b) { + return JSON.stringify(a || {}) === JSON.stringify(b || {}) +} + +export function safeParseSoloPayload(raw) { + if (!raw || typeof raw !== 'string') return null + + try { + var parsed = JSON.parse(raw) + return (parsed && typeof parsed === 'object') ? parsed : null + } catch (err) { + return null + } +}