diff --git a/lib/ui/elements/_elements.mjs b/lib/ui/elements/_elements.mjs index 38b06847e6..f608a59e44 100644 --- a/lib/ui/elements/_elements.mjs +++ b/lib/ui/elements/_elements.mjs @@ -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, @@ -34,5 +54,7 @@ export default { legendIcon, modal, slider, - slider_ab -} \ No newline at end of file + slider_ab, + numericFormatter, + getSeparators +}; \ No newline at end of file diff --git a/lib/ui/elements/numericFormatter.mjs b/lib/ui/elements/numericFormatter.mjs new file mode 100644 index 0000000000..8dc7c31d54 --- /dev/null +++ b/lib/ui/elements/numericFormatter.mjs @@ -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 +} \ No newline at end of file diff --git a/lib/ui/elements/slider_ab.mjs b/lib/ui/elements/slider_ab.mjs index 3b7e9140fe..32b8ef6b22 100644 --- a/lib/ui/elements/slider_ab.mjs +++ b/lib/ui/elements/slider_ab.mjs @@ -12,20 +12,22 @@ export default params => {
@@ -33,35 +35,68 @@ export default params => { min=${params.min} max=${params.max} step=${params.step || 1} - value=${params.val_a} + value=${params.slider_a} 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) } diff --git a/lib/ui/layers/filters.mjs b/lib/ui/layers/filters.mjs index 9ced884343..cdc463b951 100644 --- a/lib/ui/layers/filters.mjs +++ b/lib/ui/layers/filters.mjs @@ -117,7 +117,6 @@ 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), @@ -125,22 +124,28 @@ async function filter_numeric(layer, filter){ }, 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) } diff --git a/lib/ui/layers/panels/filter.mjs b/lib/ui/layers/panels/filter.mjs index b030e28f57..8c1ff7c770 100644 --- a/lib/ui/layers/panels/filter.mjs +++ b/lib/ui/layers/panels/filter.mjs @@ -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 => { diff --git a/lib/ui/locations/entries/numeric.mjs b/lib/ui/locations/entries/numeric.mjs index 6dda79fdf4..8d38c202fa 100644 --- a/lib/ui/locations/entries/numeric.mjs +++ b/lib/ui/locations/entries/numeric.mjs @@ -27,9 +27,10 @@ export default entry => { entry.formatterParams.options.maximumFractionDigits ??= entry.round || 2 } + const localeValue = entry.formatterParams.locale ? parseFloat(entry.value).toLocaleString(entry.formatterParams.locale, entry.formatterParams.options) : parseFloat(entry.value) return mapp.utils.html.node`
- ${entry.prefix}${parseFloat(entry.value).toLocaleString(entry.formatterParams.locale || 'en-GB', entry.formatterParams.options)}${entry.suffix}`; + ${entry.prefix}${localeValue}${entry.suffix}`; } @@ -63,7 +64,7 @@ function createFormatterNumberInput(entry) { return mapp.utils.html.node` onFocus(e, entry)} onBlur=${e => onBlur(e, entry)} placeholder=${entry.edit.placeholder} @@ -77,14 +78,7 @@ function onFocus(e, entry) { function onBlur(e, entry) { e.target.type = ''; - e.target.value = getVal(entry) -} - -function getVal(entry) { - - return isNaN(parseFloat(entry.newValue || entry.value)) - ? `${entry.prefix || ''} ${entry.suffix || ''}` - : `${entry.prefix || ''}${parseFloat(entry.newValue || entry.value).toLocaleString(entry.formatterParams.locale || 'en-GB', entry.formatterParams.options)}${entry.suffix || ''}` + e.target.value = mapp.ui.elements.numericFormatter(entry) } function handleKeyUp(e, entry) { diff --git a/lib/ui/utils/tabulator.mjs b/lib/ui/utils/tabulator.mjs index f49eb0c805..8142b27da8 100644 --- a/lib/ui/utils/tabulator.mjs +++ b/lib/ui/utils/tabulator.mjs @@ -345,26 +345,63 @@ function numeric(_this) { // Create the minimum input element. const inputMin = mapp.utils.html` - NumericEvent(e, 'min')} - onchange=${(e) => NumericEvent(e, 'min')} - onblur=${(e) => NumericEvent(e, 'min')}>`; + oninput=${(e) => NumericEvent(e, 'min')}>`; // Create the maximum input element. const inputMax = mapp.utils.html` - NumericEvent(e, 'max')} - onchange=${(e) => NumericEvent(e, 'max')} - onblur=${(e) => NumericEvent(e, 'max')}>`; + oninput=${(e) => NumericEvent(e, 'max')}>`; // Function to filter the data. function NumericEvent(e, type) { + let formattedValue = e.target.value; + + //Find formatterParams + const entry = _this.layer.infoj.find(entry => entry.label === _this.label && entry.type === 'dataview') + const tableField = entry.table.columns.find(column => column.field === field) + + if(tableField.formatter){ + entry.formatter = tableField.formatter + entry.formatterParams = tableField.formatterParams + const values = getValues(entry) + formattedValue = values.formatted + e.target.value = values.numericValue + } + + function getValues(entry){ + const returnValues = { formatted: e.target.value, numericValue: e.target.value} + //Get the separators + const separators = mapp.ui.elements.getSeparators(entry) + //Ignore empty values or if the user just typed `1.` for example + const stringValue = e.target.value.toString() + if(!e.target.value && type === 'min') return returnValues; + if(stringValue[0] === '-' && stringValue.length === 1){ + return returnValues; + } + if(tableField.headerFilter === 'numeric' && stringValue.substring(stringValue.indexOf(separators.decimals), stringValue.length) === separators.decimals){ + return returnValues; + } + + //Get the numeric/integer value. + entry.value = e.target.value + const numericValue = mapp.ui.elements.numericFormatter(entry,e.target.value,true) + //Get the formatted value + entry.value = numericValue + const formattedValue = mapp.ui.elements.numericFormatter(entry) + + e.target.value = numericValue + return { formatted: formattedValue, numericValue: numericValue} + } + // If type is min, use >= filter, else use <= filter. const filterType = type === 'min' ? '>=' : '<=' const filterCurrent = type === 'min' ? 'gte' : 'lte' @@ -393,6 +430,8 @@ function numeric(_this) { _this.update(); } } + formattedValue ??= null + e.target.value = formattedValue } // flex container must be encapsulated since tabulator will strip attribute from most senior item returned.