diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index a3ddc527..5d77ffff 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -669,26 +669,25 @@ const ControlsMixin = { const facetCriteria = {} keys.forEach((key) => { - if (facetKeys[key]["dataType"] === "date") { + const inputType = facetKeys[key]["inputType"] + if (["date", "datetime-local", "number"].includes(inputType)) { if (!facetCriteria[key]) facetCriteria[key] = { - "dataType": facetKeys[key]["dataType"], "inputType": facetKeys[key]["inputType"], "min": undefined, "max": undefined } if (!this.facets[key]) this.facets[key] = { - "dataType": facetKeys[key]["dataType"], + "inputType": facetKeys[key]["inputType"], "min": undefined, "max": undefined } } else { if (!facetCriteria[key]) facetCriteria[key] = { - "dataType": facetKeys[key]["dataType"], "inputType": facetKeys[key]["inputType"], "choices": [] } if (!this.facets[key]) this.facets[key] = { - "dataType": facetKeys[key]["dataType"], + "inputType": facetKeys[key]["inputType"], "choices": [] } } @@ -698,15 +697,20 @@ const ControlsMixin = { datalayer.eachFeature((feature) => { keys.forEach((key) => { let value = feature.properties[key] - if (facetKeys[key]["dataType"] === "date") { - value = L.Util.parseDateField(value) - if (!!value && (!facetCriteria[key]["min"] || facetCriteria[key]["min"] > value)) { + const inputType = facetKeys[key]["inputType"] + if (["date", "datetime-local", "number"].includes(inputType)) { + value = (value != null ? value : undefined) + if (["date", "datetime-local"].includes(inputType)) value = new Date(value); + if (["number"].includes(inputType)) value = parseFloat(value); + if (!isNaN(value) && (isNaN(facetCriteria[key]["min"]) || facetCriteria[key]["min"] > value)) { facetCriteria[key]["min"] = value } - if (!!value && (!facetCriteria[key]["max"] || facetCriteria[key]["max"] < value)) { + if (!isNaN(value) && (isNaN(facetCriteria[key]["max"]) || facetCriteria[key]["max"] < value)) { facetCriteria[key]["max"] = value } } else { + value = String(value) + value = (value.length ? value : "empty string") if (!!value && !facetCriteria[key]["choices"].includes(value)) { facetCriteria[key]["choices"].push(value) } @@ -725,11 +729,10 @@ const ControlsMixin = { if (!found) this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) } - const fields = keys.map((key) => [ `facets.${key}`, { - handler: facetCriteria[key]["inputType"] === "datetime-local" ? 'FacetSearchDate' : 'FacetSearchCheckbox', + handler: ["date", "datetime-local", "number"].includes(facetCriteria[key]["inputType"]) ? 'FacetSearchMinMax' : 'FacetSearchChoices', criteria: facetCriteria[key], label: facetKeys[key]["label"] }, diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 055362ed..06313859 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -67,18 +67,6 @@ L.Util.setNullableBooleanFromQueryString = function (options, name) { } } -L.Util.parseDateField = function (value) { - if (value != null && parseFloat(value).toString() === value.toString()) { - value = parseFloat(value); - if (Math.abs(value) < 10000000000) { - value = value * 1000; - } else if (Math.abs(value) > 10000000000000) { - value = value / 1000; - } - } - return new Date(value); -} - L.DomUtil.add = (tagName, className, container, content) => { const el = L.DomUtil.create(tagName, className, container) if (content) { @@ -553,7 +541,7 @@ U.Help = L.Class.extend({ slugKey: L._('The name of the property to use as feature unique identifier.'), filterKey: L._('Comma separated list of properties to use when filtering features'), facetKey: L._( - 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key). To control data type, add it after another | (eg.: mykey|My Key|enum,otherkey|Other Key|date). Allowed values for the data type are date and enum (default). To control input field type, add it after another | (eg.: mykey|My Key|enum|checkbox,otherkey|Other Key|date|datetime-local). Allowed values for the input field type are checkbox (default) or radio for data type enum and datetime-local (default) for data type date.' + 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key). To control input field type, add it after another | (eg.: mykey|My Key|checkbox,otherkey|Other Key|datetime). Allowed values for the input field type are checkbox (default), radio, number, date and datetime.' ), interactive: L._( 'If false, the polygon or line will act as a part of the underlying map.' diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 09d23a3b..96688a04 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -496,15 +496,27 @@ U.FeatureMixin = { const facets = this.map.facets for (const [property, criteria] of Object.entries(facets)) { let value = this.properties[property] - const dataType = criteria["dataType"] - if (dataType === "date") { - const min = new Date(criteria["min"]) - const max = new Date(criteria["max"]) - value = L.Util.parseDateField(value) - if (!!min && (!value || min > value)) return false - if (!!max && (!value || max < value)) return false + const inputType = criteria["inputType"] + if (["date", "datetime-local", "number"].includes(inputType)) { + let min = criteria["min"] + let max = criteria["max"] + value = (value != null ? value : undefined) + if (["date", "datetime-local"].includes(inputType)) { + min = new Date(min) + max = new Date(max) + value = new Date(value) + } + if (["number"].includes(inputType)) { + min = parseFloat(min) + max = parseFloat(max) + value = parseFloat(value) + } + if (!isNaN(min) && !isNaN(value) && min > value) return false + if (!isNaN(max) && !isNaN(value) && max < value) return false } else { const choices = criteria["choices"] + value = String(value) + value = (value.length ? value : "empty string") if (choices.length && (!value || !choices.includes(value))) return false } } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 15eedd8d..63a36592 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -744,11 +744,10 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ }, }) -L.FormBuilder.FacetSearchCheckbox = L.FormBuilder.Element.extend({ +L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ build: function () { this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) this.ul = L.DomUtil.create('ul', '', this.container) - this.dataType = this.options.criteria["dataType"] this.inputType = this.options.criteria["inputType"] const choices = this.options.criteria["choices"] @@ -778,54 +777,87 @@ L.FormBuilder.FacetSearchCheckbox = L.FormBuilder.Element.extend({ toJS: function () { return { - 'dataType': this.dataType, + 'inputType': this.inputType, 'choices': [...this.ul.querySelectorAll('input:checked')].map((i) => i.dataset.value) } }, }) -L.FormBuilder.FacetSearchDate = L.FormBuilder.Element.extend({ +L.FormBuilder.FacetSearchMinMax = L.FormBuilder.Element.extend({ build: function () { - this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode); - this.table = L.DomUtil.create('table', '', this.container); - this.dataType = this.options.criteria["dataType"]; - this.inputType = this.options.criteria["inputType"]; + this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) + this.table = L.DomUtil.create('table', '', this.container) + this.inputType = this.options.criteria["inputType"] - const min = this.options.criteria['min']; - const max = this.options.criteria['max']; + const min = this.options.criteria['min'] + const max = this.options.criteria['max'] - this.minTr = L.DomUtil.create('tr', '', this.table); + this.minTr = L.DomUtil.create('tr', '', this.table) - this.minTdLabel = L.DomUtil.create('td', '', this.minTr); - this.minLabel = L.DomUtil.create('label', '', this.minTdLabel); - this.minLabel.innerHTML = 'From'; - this.minLabel.htmlFor = `${this.inputType}_${this.name}_min`; + this.minTdLabel = L.DomUtil.create('td', '', this.minTr) + this.minLabel = L.DomUtil.create('label', '', this.minTdLabel) + this.minLabel.innerHTML = 'Min' + this.minLabel.htmlFor = `${this.inputType}_${this.name}_min` - this.minTdInput = L.DomUtil.create('td', '', this.minTr); - this.minInput = L.DomUtil.create('input', '', this.minTdInput); - this.minInput.type = this.inputType; - this.minInput.step = '0.001'; - this.minInput.id = `${this.inputType}_${this.name}_min`; - this.minInput.valueAsNumber = (min.valueOf() - min.getTimezoneOffset() * 60000);; - this.minInput.dataset.value = min; + this.minTdInput = L.DomUtil.create('td', '', this.minTr) + this.minInput = L.DomUtil.create('input', '', this.minTdInput) + this.minInput.type = this.inputType + this.minInput.id = `${this.inputType}_${this.name}_min` + this.minInput.step = '1' + if (min != null) { + this.minInput.valueAsNumber = min.valueOf() + this.minInput.dataset.value = min + } - this.maxTr = L.DomUtil.create('tr', '', this.table); + this.maxTr = L.DomUtil.create('tr', '', this.table) - this.maxTdLabel = L.DomUtil.create('td', '', this.maxTr); - this.maxLabel = L.DomUtil.create('label', '', this.maxTdLabel); - this.maxLabel.innerHTML = 'Until'; - this.maxLabel.htmlFor = `${this.inputType}_${this.name}_max`; + this.maxTdLabel = L.DomUtil.create('td', '', this.maxTr) + this.maxLabel = L.DomUtil.create('label', '', this.maxTdLabel) + this.maxLabel.innerHTML = 'Max' + this.maxLabel.htmlFor = `${this.inputType}_${this.name}_max` - this.maxTdInput = L.DomUtil.create('td', '', this.maxTr); - this.maxInput = L.DomUtil.create('input', '', this.maxTdInput); - this.maxInput.type = this.inputType; - this.maxInput.step = '0.001'; - this.maxInput.id = `${this.inputType}_${this.name}_max`; - this.maxInput.valueAsNumber = (max.valueOf() - max.getTimezoneOffset() * 60000);; - this.maxInput.dataset.value = max; + this.maxTdInput = L.DomUtil.create('td', '', this.maxTr) + this.maxInput = L.DomUtil.create('input', '', this.maxTdInput) + this.maxInput.type = this.inputType + this.maxInput.id = `${this.inputType}_${this.name}_max` + this.maxInput.step = '1' + if (max != null) { + this.maxInput.valueAsNumber = max.valueOf() + this.maxInput.dataset.value = max + } - L.DomEvent.on(this.minInput, 'change', (e) => this.sync()); - L.DomEvent.on(this.maxInput, 'change', (e) => this.sync()); + if (["date", "datetime-local"].includes(this.inputType)) { + this.minLabel.innerHTML = 'From' + this.maxLabel.innerHTML = 'Until' + if (min != null) { + this.minInput.valueAsNumber = (min.valueOf() - min.getTimezoneOffset() * 60000) + } + if (max != null) { + this.maxInput.valueAsNumber = (max.valueOf() - max.getTimezoneOffset() * 60000) + } + } + + if (["datetime-local"].includes(this.inputType)) { + this.minInput.step = '0.001' + this.maxInput.step = '0.001' + } + + if (["number"].includes(this.inputType)) { + if (min != null && max != null) { + // calculate step from significant digits of min and max values + let minStep = String(min).replace(/^\d+?(0*)((\.)(\d*?)0*|)$/, "1$1$3$4").split('.') + let maxStep = String(max).replace(/^\d+?(0*)((\.)(\d*?)0*|)$/, "1$1$3$4").split('.') + minStep = parseFloat((minStep[1] || "").replace(/\d/g, "0").replace(/^0/, "0.0").replace(/0$/, "1") || (minStep[0] || "").replace(/0$/, "") || "1") + maxStep = parseFloat((maxStep[1] || "").replace(/\d/g, "0").replace(/^0/, "0.0").replace(/0$/, "1") || (maxStep[0] || "").replace(/0$/, "") || "1") + + const step = Math.min(minStep, maxStep) + this.minInput.step = String(step) + this.maxInput.step = String(step) + } + } + + L.DomEvent.on(this.minInput, 'change', (e) => this.sync()) + L.DomEvent.on(this.maxInput, 'change', (e) => this.sync()) }, buildLabel: function () { @@ -834,7 +866,7 @@ L.FormBuilder.FacetSearchDate = L.FormBuilder.Element.extend({ toJS: function () { return { - 'dataType': this.dataType, + 'inputType': this.inputType, 'min': this.minInput.value, 'max': this.maxInput.value, }; diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 956d45bd..12e1714e 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1846,25 +1846,22 @@ U.Map = L.Map.extend({ }, getFacetKeys: function () { - const allowedTypes = { - "enum": ["checkbox", "radio"], - "date": ["datetime-local"], + const allowedInputTypes = { + "checkbox": "checkbox", + "radio": "radio", + "number": "number", + "date": "date", + "datetime": "datetime-local", } - console.log(this.options.facetKey) return (this.options.facetKey || '').split(',').reduce((acc, curr) => { const els = curr.split('|') acc[els[0]] = { "label": els[1] || els[0], - "dataType": ( - (els[2] in allowedTypes) ? els[2] : - Object.keys(allowedTypes)[0] + "inputType": ( + (els[2] in allowedInputTypes) ? allowedInputTypes[els[2]] : + Object.values(allowedInputTypes)[0] ) } - acc[els[0]]["inputType"] = ( - allowedTypes[acc[els[0]]["dataType"]].includes(els[3]) ? els[3] : - allowedTypes[acc[els[0]]["dataType"]][0] - ) - console.log(acc) return acc }, {}) },