chore: refactor facet date and number HTML widgets

This commit is contained in:
Yohan Boniface 2024-04-17 17:16:30 +02:00
parent 39857decda
commit 47c6473285
7 changed files with 151 additions and 144 deletions

View file

@ -135,7 +135,7 @@ ul {
/* forms */ /* forms */
/* *********** */ /* *********** */
input[type="text"], input[type="password"], input[type="date"], 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="search"], input[type="tel"], input[type="time"],
input[type="url"], textarea { input[type="url"], textarea {
background-color: white; background-color: white;
@ -263,9 +263,8 @@ input[type="checkbox"] + label {
display: inline; display: inline;
padding: 0 14px; padding: 0 14px;
} }
input[type="radio"] + label { label input[type="radio"] {
display: inline; margin-right: 10px;
padding: 0 14px;
} }
select + .error, select + .error,
input + .error { input + .error {
@ -347,6 +346,11 @@ input:invalid {
.fieldset.toggle.on .legend:before { .fieldset.toggle.on .legend:before {
background-position: -144px -51px; background-position: -144px -51px;
} }
fieldset legend {
font-size: 1.1rem;
padding: 0 5px;
}
}
/* Switch */ /* Switch */
input.switch:empty { input.switch:empty {
display: none; display: none;

View file

@ -669,26 +669,30 @@ const ControlsMixin = {
const facetCriteria = {} const facetCriteria = {}
keys.forEach((key) => { keys.forEach((key) => {
const inputType = facetKeys[key]["inputType"] const type = facetKeys[key]['type']
if (["date", "datetime-local", "number"].includes(inputType)) { if (['date', 'datetime', 'number'].includes(type)) {
if (!facetCriteria[key]) facetCriteria[key] = { if (!facetCriteria[key])
"inputType": facetKeys[key]["inputType"], facetCriteria[key] = {
"min": undefined, type: facetKeys[key]['type'],
"max": undefined min: undefined,
max: undefined,
} }
if (!this.facets[key]) this.facets[key] = { if (!this.facets[key])
"inputType": facetKeys[key]["inputType"], this.facets[key] = {
"min": undefined, type: facetKeys[key]['type'],
"max": undefined min: undefined,
max: undefined,
} }
} else { } else {
if (!facetCriteria[key]) facetCriteria[key] = { if (!facetCriteria[key])
"inputType": facetKeys[key]["inputType"], facetCriteria[key] = {
"choices": [] type: facetKeys[key]['type'],
choices: [],
} }
if (!this.facets[key]) this.facets[key] = { if (!this.facets[key])
"inputType": facetKeys[key]["inputType"], this.facets[key] = {
"choices": [] type: facetKeys[key]['type'],
choices: [],
} }
} }
}) })
@ -697,22 +701,28 @@ const ControlsMixin = {
datalayer.eachFeature((feature) => { datalayer.eachFeature((feature) => {
keys.forEach((key) => { keys.forEach((key) => {
let value = feature.properties[key] let value = feature.properties[key]
const inputType = facetKeys[key]["inputType"] const type = facetKeys[key]['type']
if (["date", "datetime-local", "number"].includes(inputType)) { if (['date', 'datetime', 'number'].includes(type)) {
value = (value != null ? value : undefined) value = value != null ? value : undefined
if (["date", "datetime-local"].includes(inputType)) value = new Date(value); if (['date', 'datetime'].includes(type)) value = new Date(value)
if (["number"].includes(inputType)) value = parseFloat(value); if (['number'].includes(type)) value = parseFloat(value)
if (!isNaN(value) && (isNaN(facetCriteria[key]["min"]) || facetCriteria[key]["min"] > value)) { if (
facetCriteria[key]["min"] = value !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)) { if (
facetCriteria[key]["max"] = value !isNaN(value) &&
(isNaN(facetCriteria[key]['max']) || facetCriteria[key]['max'] < value)
) {
facetCriteria[key]['max'] = value
} }
} else { } else {
value = String(value) value = String(value)
value = (value.length ? value : L._("empty string")) value = value.length ? value : L._('empty string')
if (!!value && !facetCriteria[key]["choices"].includes(value)) { if (!!value && !facetCriteria[key]['choices'].includes(value)) {
facetCriteria[key]["choices"].push(value) facetCriteria[key]['choices'].push(value)
} }
} }
}) })
@ -726,19 +736,31 @@ const ControlsMixin = {
if (datalayer.hasDataVisible()) found = true if (datalayer.hasDataVisible()) found = true
}) })
// TODO: display a results counter in the panel instead. // 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' }) this.ui.alert({ content: L._('No results for these facets'), level: 'info' })
} }
}
const fields = keys.map((key) => { const fields = keys.map((key) => {
let criteria = facetCriteria[key] let criteria = facetCriteria[key]
let handler = ["date", "datetime-local", "number"].includes(criteria["inputType"]) ? 'FacetSearchMinMax' : 'FacetSearchChoices' let handler = 'FacetSearchChoices'
let label = facetKeys[key]["label"] switch (criteria['type']) {
case 'number':
handler = 'FacetSearchNumber'
break
case 'date':
handler = 'FacetSearchDate'
break
case 'datetime':
handler = 'FacetSearchDateTime'
break
}
let label = facetKeys[key]['label']
return [ return [
`facets.${key}`, `facets.${key}`,
{ {
criteria: criteria, criteria: criteria,
handler: handler, handler: handler,
label: label label: label,
}, },
] ]
}) })

View file

@ -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) => { L.DomUtil.add = (tagName, className, container, content) => {
const el = L.DomUtil.create(tagName, className, container) const el = L.DomUtil.create(tagName, className, container)
if (content) { if (content) {

View file

@ -496,17 +496,17 @@ U.FeatureMixin = {
const facets = this.map.facets const facets = this.map.facets
for (const [property, criteria] of Object.entries(facets)) { for (const [property, criteria] of Object.entries(facets)) {
let value = this.properties[property] let value = this.properties[property]
const inputType = criteria["inputType"] const type = criteria["type"]
if (["date", "datetime-local", "number"].includes(inputType)) { if (["date", "datetime", "number"].includes(type)) {
let min = criteria["min"] let min = criteria["min"]
let max = criteria["max"] let max = criteria["max"]
value = (value != null ? value : undefined) value = (value != null ? value : undefined)
if (["date", "datetime-local"].includes(inputType)) { if (["date", "datetime"].includes(type)) {
min = new Date(min) min = new Date(min)
max = new Date(max) max = new Date(max)
value = new Date(value) value = new Date(value)
} }
if (["number"].includes(inputType)) { if (["number"].includes(type)) {
min = parseFloat(min) min = parseFloat(min)
max = parseFloat(max) max = parseFloat(max)
value = parseFloat(value) value = parseFloat(value)
@ -517,7 +517,7 @@ U.FeatureMixin = {
const choices = criteria["choices"] const choices = criteria["choices"]
value = String(value) value = String(value)
value = (value.length ? value : L._("empty string")) 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 return true

View file

@ -746,127 +746,122 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({
L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({
build: function () { 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.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.sort()
choices.forEach((value) => this.buildLi(value)) choices.forEach((value) => this.buildLi(value))
}, },
buildLabel: function () { 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) { buildLi: function (value) {
const property_li = L.DomUtil.create('li', '', this.ul), const property_li = L.DomUtil.create('li', '', this.ul)
input = L.DomUtil.create('input', '', property_li), const label = L.DomUtil.add('label', '', property_li)
label = L.DomUtil.create('label', '', property_li) const input = L.DomUtil.create('input', '', label)
L.DomUtil.add('span', '', label, value)
input.type = this.inputType input.type = this.type
input.name = `${this.inputType}_${this.name}` input.name = `${this.type}_${this.name}`
input.id = `${this.inputType}_${this.name}_${value}`
input.checked = this.get()['choices'].includes(value) input.checked = this.get()['choices'].includes(value)
input.dataset.value = value input.dataset.value = value
label.htmlFor = `${this.inputType}_${this.name}_${value}`
label.innerHTML = value
L.DomEvent.on(input, 'change', (e) => this.sync()) L.DomEvent.on(input, 'change', (e) => this.sync())
}, },
toJS: function () { toJS: function () {
return { return {
'inputType': this.inputType, type: this.type,
'choices': [...this.ul.querySelectorAll('input:checked')].map((i) => i.dataset.value) 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 () { build: function () {
this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode)
this.table = L.DomUtil.create('table', '', this.container) this.container.appendChild(this.label)
this.inputType = this.options.criteria["inputType"] const {min, max, type} = this.options.criteria
this.type = type
this.inputType = this.getInputType(this.type)
const min = this.options.criteria['min'] const [minLabel, maxLabel] = this.getLabels()
const max = this.options.criteria['max']
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.minInput = L.DomUtil.create('input', '', this.minLabel)
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.type = this.inputType this.minInput.type = this.inputType
this.minInput.id = `${this.inputType}_${this.name}_min` this.minInput.step = 'any'
this.minInput.step = '1'
if (min != null) { if (min != null) {
this.minInput.valueAsNumber = min.valueOf() this.minInput.valueAsNumber = this.castValue(min)
this.minInput.dataset.value = 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.container)
this.maxLabel = L.DomUtil.create('label', '', this.maxTdLabel) this.maxLabel.innerHTML = maxLabel
this.maxLabel.innerHTML = L._('Max')
this.maxLabel.htmlFor = `${this.inputType}_${this.name}_max`
this.maxTdInput = L.DomUtil.create('td', '', this.maxTr) this.maxInput = L.DomUtil.create('input', '', this.maxLabel)
this.maxInput = L.DomUtil.create('input', '', this.maxTdInput)
this.maxInput.type = this.inputType this.maxInput.type = this.inputType
this.maxInput.id = `${this.inputType}_${this.name}_max` this.maxInput.step = 'any'
this.maxInput.step = '1'
if (max != null) { if (max != null) {
this.maxInput.valueAsNumber = max.valueOf() this.maxInput.valueAsNumber = this.castValue(max)
this.maxInput.dataset.value = 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.minInput, 'change', (e) => this.sync())
L.DomEvent.on(this.maxInput, 'change', (e) => this.sync()) L.DomEvent.on(this.maxInput, 'change', (e) => this.sync())
}, },
buildLabel: function () { 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 () { toJS: function () {
return { return {
'inputType': this.inputType, type: this.type,
'min': this.minInput.value, min: this.minInput.value,
'max': this.maxInput.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({ L.FormBuilder.MultiChoice = L.FormBuilder.Element.extend({
default: 'null', default: 'null',

View file

@ -218,6 +218,7 @@ U.Map = L.Map.extend({
this.panel.mode = 'condensed' this.panel.mode = 'condensed'
this.displayCaption() this.displayCaption()
} else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) {
this.panel.mode = 'expanded'
this.openFacet() this.openFacet()
} }
if (L.Util.queryString('edit')) { if (L.Util.queryString('edit')) {
@ -1846,21 +1847,14 @@ U.Map = L.Map.extend({
}, },
getFacetKeys: function () { getFacetKeys: function () {
const allowedInputTypes = { const defaultType = 'checkbox'
"checkbox": "checkbox", const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime']
"radio": "radio",
"number": "number",
"date": "date",
"datetime": "datetime-local",
}
return (this.options.facetKey || '').split(',').reduce((acc, curr) => { return (this.options.facetKey || '').split(',').reduce((acc, curr) => {
const els = curr.split('|') let [key, label, type] = curr.split('|')
acc[els[0]] = { type = allowedTypes.includes(type) ? type : defaultType
"label": els[1] || els[0], acc[key] = {
"inputType": ( label: label || key,
(els[2] in allowedInputTypes) ? allowedInputTypes[els[2]] : type: type,
Object.values(allowedInputTypes)[0]
)
} }
return acc return acc
}, {}) }, {})

View file

@ -927,7 +927,6 @@ a.umap-control-caption,
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.umap-facet-search li:nth-child(even),
.umap-browser .datalayer li:nth-child(even) { .umap-browser .datalayer li:nth-child(even) {
background-color: #efefef; background-color: #efefef;
} }