From 47c6473285d31e06d8b168227cfea890a51f9589 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 17 Apr 2024 17:16:30 +0200 Subject: [PATCH] chore: refactor facet date and number HTML widgets --- umap/static/umap/base.css | 12 ++- umap/static/umap/js/umap.controls.js | 94 ++++++++++------- umap/static/umap/js/umap.core.js | 7 -- umap/static/umap/js/umap.features.js | 10 +- umap/static/umap/js/umap.forms.js | 149 +++++++++++++-------------- umap/static/umap/js/umap.js | 22 ++-- umap/static/umap/map.css | 1 - 7 files changed, 151 insertions(+), 144 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index af8fa881..dfeb9b02 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -135,7 +135,7 @@ ul { /* forms */ /* *********** */ input[type="text"], input[type="password"], input[type="date"], -input[type="datetime"], input[type="email"], input[type="number"], +input[type="datetime-local"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="time"], input[type="url"], textarea { background-color: white; @@ -263,9 +263,8 @@ input[type="checkbox"] + label { display: inline; padding: 0 14px; } -input[type="radio"] + label { - display: inline; - padding: 0 14px; +label input[type="radio"] { + margin-right: 10px; } select + .error, input + .error { @@ -347,6 +346,11 @@ input:invalid { .fieldset.toggle.on .legend:before { background-position: -144px -51px; } +fieldset legend { + font-size: 1.1rem; + padding: 0 5px; +} +} /* Switch */ input.switch:empty { display: none; diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index c9e709b2..884633dc 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -669,27 +669,31 @@ const ControlsMixin = { const facetCriteria = {} keys.forEach((key) => { - const inputType = facetKeys[key]["inputType"] - if (["date", "datetime-local", "number"].includes(inputType)) { - if (!facetCriteria[key]) facetCriteria[key] = { - "inputType": facetKeys[key]["inputType"], - "min": undefined, - "max": undefined - } - if (!this.facets[key]) this.facets[key] = { - "inputType": facetKeys[key]["inputType"], - "min": undefined, - "max": undefined - } + const type = facetKeys[key]['type'] + if (['date', 'datetime', 'number'].includes(type)) { + if (!facetCriteria[key]) + facetCriteria[key] = { + type: facetKeys[key]['type'], + min: undefined, + max: undefined, + } + if (!this.facets[key]) + this.facets[key] = { + type: facetKeys[key]['type'], + min: undefined, + max: undefined, + } } else { - if (!facetCriteria[key]) facetCriteria[key] = { - "inputType": facetKeys[key]["inputType"], - "choices": [] - } - if (!this.facets[key]) this.facets[key] = { - "inputType": facetKeys[key]["inputType"], - "choices": [] - } + if (!facetCriteria[key]) + facetCriteria[key] = { + type: facetKeys[key]['type'], + choices: [], + } + if (!this.facets[key]) + this.facets[key] = { + type: facetKeys[key]['type'], + choices: [], + } } }) @@ -697,22 +701,28 @@ const ControlsMixin = { datalayer.eachFeature((feature) => { keys.forEach((key) => { let value = feature.properties[key] - 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 + const type = facetKeys[key]['type'] + if (['date', 'datetime', 'number'].includes(type)) { + value = value != null ? value : undefined + if (['date', 'datetime'].includes(type)) value = new Date(value) + if (['number'].includes(type)) value = parseFloat(value) + if ( + !isNaN(value) && + (isNaN(facetCriteria[key]['min']) || facetCriteria[key]['min'] > value) + ) { + facetCriteria[key]['min'] = value } - if (!isNaN(value) && (isNaN(facetCriteria[key]["max"]) || facetCriteria[key]["max"] < value)) { - 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 : L._("empty string")) - if (!!value && !facetCriteria[key]["choices"].includes(value)) { - facetCriteria[key]["choices"].push(value) + value = value.length ? value : L._('empty string') + if (!!value && !facetCriteria[key]['choices'].includes(value)) { + facetCriteria[key]['choices'].push(value) } } }) @@ -726,19 +736,31 @@ const ControlsMixin = { if (datalayer.hasDataVisible()) found = true }) // TODO: display a results counter in the panel instead. - if (!found) + if (!found) { this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) + } } const fields = keys.map((key) => { let criteria = facetCriteria[key] - let handler = ["date", "datetime-local", "number"].includes(criteria["inputType"]) ? 'FacetSearchMinMax' : 'FacetSearchChoices' - let label = facetKeys[key]["label"] + let handler = 'FacetSearchChoices' + switch (criteria['type']) { + case 'number': + handler = 'FacetSearchNumber' + break + case 'date': + handler = 'FacetSearchDate' + break + case 'datetime': + handler = 'FacetSearchDateTime' + break + } + let label = facetKeys[key]['label'] return [ `facets.${key}`, { criteria: criteria, handler: handler, - label: label + label: label, }, ] }) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index b9b7f659..06313859 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -67,13 +67,6 @@ L.Util.setNullableBooleanFromQueryString = function (options, name) { } } -L.Util.calculateStepFromNumber = function (n) { - // calculate step for number input field from significant digits of number - let step = String(n).replace(/^\d+?(0*)((\.)(\d*?)0*|)$/, "1$1$3$4").split('.') - step = parseFloat((step[1] || "").replace(/\d/g, "0").replace(/^0/, "0.0").replace(/0$/, "1") || (step[0] || "").replace(/0$/, "") || "1") - return step -} - L.DomUtil.add = (tagName, className, container, content) => { const el = L.DomUtil.create(tagName, className, container) if (content) { diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 5ec54115..b6fce1b7 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -496,17 +496,17 @@ U.FeatureMixin = { const facets = this.map.facets for (const [property, criteria] of Object.entries(facets)) { let value = this.properties[property] - const inputType = criteria["inputType"] - if (["date", "datetime-local", "number"].includes(inputType)) { + const type = criteria["type"] + if (["date", "datetime", "number"].includes(type)) { let min = criteria["min"] let max = criteria["max"] value = (value != null ? value : undefined) - if (["date", "datetime-local"].includes(inputType)) { + if (["date", "datetime"].includes(type)) { min = new Date(min) max = new Date(max) value = new Date(value) } - if (["number"].includes(inputType)) { + if (["number"].includes(type)) { min = parseFloat(min) max = parseFloat(max) value = parseFloat(value) @@ -517,7 +517,7 @@ U.FeatureMixin = { const choices = criteria["choices"] value = String(value) value = (value.length ? value : L._("empty string")) - if (choices.length && (!value || !choices.includes(value))) return false + if (choices?.length && (!value || !choices.includes(value))) return false } } return true diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index cb9ce2c7..42ab4c1a 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -527,11 +527,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ ) } const symbol = L.DomUtil.add( - 'button', - 'flat tab-symbols', - this.tabs, - L._('Symbol') - ), + 'button', + 'flat tab-symbols', + this.tabs, + L._('Symbol') + ), char = L.DomUtil.add( 'button', 'flat tab-chars', @@ -746,127 +746,122 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ build: function () { - this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) + this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) + this.container.appendChild(this.label) this.ul = L.DomUtil.create('ul', '', this.container) - this.inputType = this.options.criteria["inputType"] + this.type = this.options.criteria['type'] - const choices = this.options.criteria["choices"] + const choices = this.options.criteria['choices'] choices.sort() choices.forEach((value) => this.buildLi(value)) }, buildLabel: function () { - this.label = L.DomUtil.add('h5', '', this.parentNode, this.options.label) + this.label = L.DomUtil.element('legend', {textContent: this.options.label}) }, buildLi: function (value) { - const property_li = L.DomUtil.create('li', '', this.ul), - input = L.DomUtil.create('input', '', property_li), - label = L.DomUtil.create('label', '', property_li) + const property_li = L.DomUtil.create('li', '', this.ul) + const label = L.DomUtil.add('label', '', property_li) + const input = L.DomUtil.create('input', '', label) + L.DomUtil.add('span', '', label, value) - input.type = this.inputType - input.name = `${this.inputType}_${this.name}` - input.id = `${this.inputType}_${this.name}_${value}` + input.type = this.type + input.name = `${this.type}_${this.name}` input.checked = this.get()['choices'].includes(value) input.dataset.value = value - label.htmlFor = `${this.inputType}_${this.name}_${value}` - label.innerHTML = value L.DomEvent.on(input, 'change', (e) => this.sync()) }, toJS: function () { return { - 'inputType': this.inputType, - 'choices': [...this.ul.querySelectorAll('input:checked')].map((i) => i.dataset.value) + type: this.type, + choices: [...this.ul.querySelectorAll('input:checked')].map( + (i) => i.dataset.value + ), } }, }) -L.FormBuilder.FacetSearchMinMax = L.FormBuilder.Element.extend({ +L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ + getInputType: function (type) { + return type + }, + + getLabels: function () { + return [L._('Min'), L._('Max')] + }, + + castValue: function (value) { + return value.valueOf() + }, + build: function () { - this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) - this.table = L.DomUtil.create('table', '', this.container) - this.inputType = this.options.criteria["inputType"] + this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) + this.container.appendChild(this.label) + const {min, max, type} = this.options.criteria + this.type = type + this.inputType = this.getInputType(this.type) - const min = this.options.criteria['min'] - const max = this.options.criteria['max'] + const [minLabel, maxLabel] = this.getLabels() - this.minTr = L.DomUtil.create('tr', '', this.table) + this.minLabel = L.DomUtil.create('label', '', this.container) + this.minLabel.innerHTML = minLabel - this.minTdLabel = L.DomUtil.create('td', '', this.minTr) - this.minLabel = L.DomUtil.create('label', '', this.minTdLabel) - this.minLabel.innerHTML = L._('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 = L.DomUtil.create('input', '', this.minLabel) this.minInput.type = this.inputType - this.minInput.id = `${this.inputType}_${this.name}_min` - this.minInput.step = '1' + this.minInput.step = 'any' if (min != null) { - this.minInput.valueAsNumber = min.valueOf() + this.minInput.valueAsNumber = this.castValue(min) this.minInput.dataset.value = min } - 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 = L._('Max') - this.maxLabel.htmlFor = `${this.inputType}_${this.name}_max` + this.maxLabel = L.DomUtil.create('label', '', this.container) + this.maxLabel.innerHTML = maxLabel - this.maxTdInput = L.DomUtil.create('td', '', this.maxTr) - this.maxInput = L.DomUtil.create('input', '', this.maxTdInput) + this.maxInput = L.DomUtil.create('input', '', this.maxLabel) this.maxInput.type = this.inputType - this.maxInput.id = `${this.inputType}_${this.name}_max` - this.maxInput.step = '1' + this.maxInput.step = 'any' if (max != null) { - this.maxInput.valueAsNumber = max.valueOf() + this.maxInput.valueAsNumber = this.castValue(max) this.maxInput.dataset.value = max } - if (["date", "datetime-local"].includes(this.inputType)) { - this.minLabel.innerHTML = L._('From') - this.maxLabel.innerHTML = L._('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 - const step = Math.min(L.Util.calculateStepFromNumber(min), L.Util.calculateStepFromNumber(max)) - 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 () { - this.label = L.DomUtil.add('h5', '', this.parentNode, this.options.label) + this.label = L.DomUtil.element('legend', {textContent: this.options.label}) }, toJS: function () { return { - 'inputType': this.inputType, - 'min': this.minInput.value, - 'max': this.maxInput.value, - }; + type: this.type, + min: this.minInput.value, + max: this.maxInput.value, + } }, -}); +}) + +L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({}) + +L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({ + castValue: function (value) { + return value.valueOf() - value.getTimezoneOffset() * 60000 + }, + getLabels: function () { + return [L._('From'), L._('Until')] + }, +}) + +L.FormBuilder.FacetSearchDateTime = L.FormBuilder.FacetSearchDate.extend({ + getInputType: function (type) { + return 'datetime-local' + }, +}) L.FormBuilder.MultiChoice = L.FormBuilder.Element.extend({ default: 'null', diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 5d7244a4..7f4bdc90 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -218,6 +218,7 @@ U.Map = L.Map.extend({ this.panel.mode = 'condensed' this.displayCaption() } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { + this.panel.mode = 'expanded' this.openFacet() } if (L.Util.queryString('edit')) { @@ -1846,21 +1847,14 @@ U.Map = L.Map.extend({ }, getFacetKeys: function () { - const allowedInputTypes = { - "checkbox": "checkbox", - "radio": "radio", - "number": "number", - "date": "date", - "datetime": "datetime-local", - } + const defaultType = 'checkbox' + const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime'] return (this.options.facetKey || '').split(',').reduce((acc, curr) => { - const els = curr.split('|') - acc[els[0]] = { - "label": els[1] || els[0], - "inputType": ( - (els[2] in allowedInputTypes) ? allowedInputTypes[els[2]] : - Object.values(allowedInputTypes)[0] - ) + let [key, label, type] = curr.split('|') + type = allowedTypes.includes(type) ? type : defaultType + acc[key] = { + label: label || key, + type: type, } return acc }, {}) diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 4d5c5677..553f6047 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -927,7 +927,6 @@ a.umap-control-caption, overflow: hidden; text-overflow: ellipsis; } -.umap-facet-search li:nth-child(even), .umap-browser .datalayer li:nth-child(even) { background-color: #efefef; }