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.