Skip to content

Commit

Permalink
Merge pull request #1162 from AlexanderGeere/numeric-filter-format
Browse files Browse the repository at this point in the history
Numeric filter format
  • Loading branch information
dbauszus-glx authored Mar 4, 2024
2 parents c324463 + 2c0b134 commit f43d9d3
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 68 deletions.
74 changes: 48 additions & 26 deletions lib/ui/elements/_elements.mjs
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
import card from './card.mjs'

import chkbox from './chkbox.mjs'

import contextMenu from './contextMenu.mjs'

import drawer from './drawer.mjs'

import drawing from './drawing.mjs'

import dropdown from './dropdown.mjs'

import dropdown_multi from './dropdown_multi.mjs'

import btnPanel from './btnPanel.mjs'

import legendIcon from './legendIcon.mjs'

import modal from './modal.mjs'

import slider from './slider.mjs'

import slider_ab from './slider_ab.mjs'

/**
### mapp.ui.elements{}
Module to export all the ui element functions used in mapp.
* @module /ui/elements
*/

import card from './card.mjs';
import chkbox from './chkbox.mjs';
import contextMenu from './contextMenu.mjs';
import drawer from './drawer.mjs';
import drawing from './drawing.mjs';
import dropdown from './dropdown.mjs';
import dropdown_multi from './dropdown_multi.mjs';
import btnPanel from './btnPanel.mjs';
import legendIcon from './legendIcon.mjs';
import modal from './modal.mjs';
import slider from './slider.mjs';
import slider_ab from './slider_ab.mjs';
import { numericFormatter, getSeparators } from './numericFormatter.mjs';

/**
* UI elements object containing various UI components.
* @typedef {Object} UIElements
* @property {Function} btnPanel - Button panel component.
* @property {Function} card - Card component.
* @property {Function} chkbox - Checkbox component.
* @property {Function} contextMenu - Context menu component.
* @property {Function} drawer - Drawer component.
* @property {Function} drawing - Drawing component.
* @property {Function} dropdown - Dropdown component.
* @property {Function} dropdown_multi - Multi-select dropdown component.
* @property {Function} legendIcon - Legend icon component.
* @property {Function} modal - Modal component.
* @property {Function} slider - Slider component.
* @property {Function} slider_ab - Slider with A/B comparison component.
* @property {Function} numericFormatter - Numeric formatter function.
* @property {Function} getSeparators - Function to get numeric separators.
*/

/**
* Exporting UI elements.
* @type {UIElements}
*/
export default {
btnPanel,
card,
Expand All @@ -34,5 +54,7 @@ export default {
legendIcon,
modal,
slider,
slider_ab
}
slider_ab,
numericFormatter,
getSeparators
};
137 changes: 137 additions & 0 deletions lib/ui/elements/numericFormatter.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
### mapp.ui.elements.numericFormatter{}
module provides methods to format numeric import using formatterParams supplied on the entries.
The mapp.ui.elements.numericFormatter module provides methods to format numeric import using
formatterParams supplied on the entries.
@module /ui/elements/numericFormatter
*/

/**
Returns the decimal and thousand separators produced by toLocaleString or formatterParams
@param {Object} entry - An infoj entry object.
@function getSeparators
@returns {Object} - An Object containing the two keys, thousands and decimals e.g `{ thousands: ',', decimals: '.'}`.
*/
export function getSeparators(entry) {
//Do nothing if entry is null or no formatter is present
if (!entry) return;
if (!entry.formatter) return { decimals: '.' };
entry.prefix = ''
entry.suffix = ''
//Use a number to determin what the fields
//formatted input would look like
entry.value = 1000000.99
return getFormatterParams(entry).separators
}

function getFormatterParams(entry, inValue) {
let value = entry.value || inValue;
entry.prefix ??= ''
entry.suffix ??= ''

if (!value) {
return { value: null }
}

const negative = value[0] === '-'

if (negative) {
value = value.substring(1, value.length)
}

let rawValue;

//Check if supplied value is a valid number
if (isNaN(parseFloat(value))) {
return { value: `${entry.prefix} ${entry.suffix}`, separators: { thousands: '', decimals: '' } }
}

//Framework formatter type
if (entry.formatter === 'toLocaleString') {
entry.formatterParams ??= { locale: navigator.language }
}

//Tabulator formatterOptions
if (entry.formatterParams?.decimal || entry.formatterParams?.thousand) {
rawValue = parseFloat(value)
let rawList = rawValue.toLocaleString('en-GB', entry.formatterParams.options).split('.')
rawList[0] = rawList[0].replaceAll(',', entry.formatterParams.thousand)
rawValue = rawList.join(entry.formatterParams.decimal || '.')
} else {

//Infoj formatter options
entry.formatterParams ??= { locale: 'en-GB' }
rawValue = parseFloat(value).toLocaleString(entry.formatterParams.locale, entry.formatterParams.options)
}

//Add The affixes
let formattedValue = `${entry.prefix}${rawValue}${entry.suffix}`

//Look for separators in formatterOptions.
let separators = [entry.formatterParams?.thousand, entry.formatterParams?.decimal]
let localeSeparators = Array.from(new Set(formattedValue.match(/\D/g)))

//If not supplied look in the formatted string
separators[0] = separators[0] ? separators[0] : localeSeparators[0]
separators[1] = separators[1] ? separators[1] : localeSeparators[1] || '.'

formattedValue = `${negative ? '-' : ''}${formattedValue}`
return { value: formattedValue, separators: { thousands: separators[0], decimals: separators[1] } }
}

function undoFormatting(entry) {

//Determine thousand and decimal markers
let value = entry.value
if (!entry.value) return null;

const separators = getSeparators(entry)
const negative = value[0] === '-'
value = negative ? value.substring(1, value.length) : value

//Strip out thousand and decimal markers, replacing decimal with '.'
value = value.replaceAll(separators.thousands, '')

if (separators.decimals) {
value = separators.decimals === '.' || separators.decimals === '' ? value : value.replace(separators.decimals, '.')
}

if (!Number(value)) {
value = value.replaceAll(/\D+/g, '');
}

return `${negative ? '-' : ''}${value}`
}

/**
Returns the formatted string based on the provided formatterParams or locale
@param {Object} entry - An infoj entry object.
@param {Integer|String} inValue - The value to be formatted if not available in the entry.
@param {Boolean} reverse - A true false value specifying whether the formatting should be removed or applied.
@returns {Integer|String} - Either the fomatted string (`reverse=false`) or the numeric value(`reverse=true`).
*/
export function numericFormatter(entry, inValue, reverse) {

//Do nothing if entry is null or no formatter is present
if (!entry) return entry.value || inValue;
if (!entry.formatter) return entry.value || inValue;
entry.prefix = ''
entry.suffix = ''
entry.value = inValue || entry.value

//Do the opposite of formatting
if (reverse) {
return undoFormatting(entry)
}

//Get the actual formatted value
return getFormatterParams(entry).value
}

export default {
numericFormatter,
getSeparators
}
61 changes: 48 additions & 13 deletions lib/ui/elements/slider_ab.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,91 @@ export default params => {
<div
class="label-row">
<label>${params.label_a || 'A'}
<input data-id="a" type="number"
<input data-id="a" type="text"
value=${params.val_a}
min=${params.min}
max=${params.max}
style="--c: var(--a)"
oninput=${onInput}></input>
style="--c: var(--a);text-align:end"
oninput=${onInput}
onfocus="this.value=''"></input>
</label>
<label>${params.label_b || 'B'}
<input data-id="b" type="number"
<input data-id="b" type="text"
value=${params.val_b}
min=${params.min}
max=${params.max}
style="--c: var(--b)"
oninput=${onInput}></input>
style="--c: var(--b);text-align:end"
oninput=${onInput}
onfocus="this.value=''"}></input>
</label>
</div>
<div class="track-bg"></div>
<input data-id="a" type="range"
min=${params.min}
max=${params.max}
step=${params.step || 1}
value=${params.val_a}
value=${params.slider_a}
oninput=${onInput}/>
<input data-id="b" type="range"
min=${params.min}
max=${params.max}
step=${params.step || 1}
value=${params.val_b}
value=${params.slider_b}
oninput=${onInput}/>`

function onInput(e) {
let currMax;
let currMin;
const replaceValue = e.target.dataset.id === 'b' ? params.max : params.min
e.target.value = e.target.value === '' ? replaceValue : e.target.value;

e.target.value = e.target.value > params.max ? params.max : e.target.value;
//Determine thousand and decimal markers
const separators = mapp.ui.elements.getSeparators(params.entry) || {decimals: '.'}

element.style.setProperty(`--${e.target.dataset.id}`, e.target.value)
//Ignore empty values or if the user just typed `1.` for example.
//Until they type `1.1`.
const stringValue = e.target.value.toString()
if(stringValue[0] === '-' && stringValue.length === 1) return;
if(params.entry.type !== 'integer'){
if(stringValue.substring(stringValue.indexOf(separators.decimals), stringValue.length) === separators.decimals ) return;
}

//Get the number value and the formatted value
const numericValue = Number(e.target.value) || mapp.ui.elements.numericFormatter(params.entry,e.target.value,true)
let value = Number(numericValue)
e.target.value = value > params.max ? params.max : value
element.style.setProperty(`--${e.target.dataset.id}`, e.target.value < params.min ? params.min : e.target.value)

element.querySelectorAll('input')
.forEach(el => {
if (el.dataset.id != e.target.dataset.id) return;
if (el == e.target) return;
el.value = e.target.value
if(e.target.type === 'text' && el.type === 'range'){
el.value = value
params.entry.value = value
e.target.value = mapp.ui.elements.numericFormatter(params.entry)
}
else{
params.entry.value = e.target.value
el.value = mapp.ui.elements.numericFormatter(params.entry,e.target.value)
}
if(el.dataset.id === 'a'){
currMin = Number(el.value) || e.target.value
currMin = currMin < params.min ? params.max : currMin;
}
if(el.dataset.id === 'b'){
currMax = Number(el.value) || e.target.value
currMax = currMax > params.max ? params.max : currMax;
}
})

e.target.dataset.id === 'a'
&& typeof params.callback_a === 'function'
&& params.callback_a(e)
&& params.callback_a(currMin)

e.target.dataset.id === 'b'
&& typeof params.callback_b === 'function'
&& params.callback_b(e)
&& params.callback_b(currMax)

}

Expand Down
21 changes: 13 additions & 8 deletions lib/ui/layers/filters.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,30 +117,35 @@ async function filter_numeric(layer, filter){
filter.step = filter.type === 'integer' ? 1 : 0.01;
}


layer.filter.current[filter.field] = Object.assign(
{
gte: Number(filter.min),
lte: Number(filter.max)
},
layer.filter.current[filter.field]);

const entry = layer.infoj.find(entry => entry.field === filter.field)
entry.formatter ??= entry.formatterParams
let affix = entry.prefix || entry.suffix
affix = affix ? `(${affix.trim()})` : ''
applyFilter(layer);

return mapp.ui.elements.slider_ab({
min: Number(filter.min),
max: Number(filter.max),
step: filter.step,
label_a: mapp.dictionary.layer_filter_greater_than, // Greater than
val_a: Number(filter.min),
entry: entry,
label_a: `${mapp.dictionary.layer_filter_greater_than} ${affix}`, // Greater than
val_a: mapp.ui.elements.numericFormatter(entry,filter.min),
slider_a: Number(filter.min),
callback_a: e => {
layer.filter.current[filter.field].gte = Number(e.target.value)
layer.filter.current[filter.field].gte = Number(e)
applyFilter(layer)
},
label_b: mapp.dictionary.layer_filter_less_than, // Less than
val_b: Number(filter.max),
label_b: `${mapp.dictionary.layer_filter_less_than} ${affix}`, // Less than
val_b: mapp.ui.elements.numericFormatter(entry,filter.max),
slider_b: Number(filter.max),
callback_b: e => {
layer.filter.current[filter.field].lte = Number(e.target.value)
layer.filter.current[filter.field].lte = Number(e)
applyFilter(layer)
}

Expand Down
1 change: 0 additions & 1 deletion lib/ui/layers/panels/filter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ export default layer => {
class="primary-colour"
style="display: none; margin-bottom: 5px;"
onclick=${e => {
layer.filter.list
.filter((filter) => filter.card)
.forEach(filter => {
Expand Down
Loading

0 comments on commit f43d9d3

Please sign in to comment.