diff --git a/umap/static/umap/js/modules/autocomplete.js b/umap/static/umap/js/modules/autocomplete.js index 31d5d196..e693d6be 100644 --- a/umap/static/umap/js/modules/autocomplete.js +++ b/umap/static/umap/js/modules/autocomplete.js @@ -6,6 +6,7 @@ import { } from '../../vendors/leaflet/leaflet-src.esm.js' import { translate } from './i18n.js' import { Request, ServerRequest } from './request.js' +import { escapeHTML, generateId } from './utils.js' export class BaseAutocomplete { constructor(el, options) { @@ -46,7 +47,7 @@ export class BaseAutocomplete { placeholder: this.options.placeholder, autocomplete: 'off', className: this.options.className, - name: this.options.name || 'autocomplete' + name: this.options.name || 'autocomplete', }) DomEvent.on(this.input, 'keydown', this.onKeyDown, this) DomEvent.on(this.input, 'keyup', this.onKeyUp, this) @@ -350,3 +351,19 @@ export const MultipleMixin = (Base) => export class AjaxAutocompleteMultiple extends MultipleMixin(BaseServerAjax) {} export class AjaxAutocomplete extends SingleMixin(BaseServerAjax) {} + +export class AutocompleteDatalist { + constructor(input) { + this.input = input + this.datalist = document.createElement('datalist') + this.datalist.id = generateId() + this.input.setAttribute('list', this.datalist.id) + this.input.parentElement.appendChild(this.datalist) + } + + set suggestions(values) { + this.datalist.innerHTML = values + .map((value) => ``) + .join('') + } +} diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 11b62b2b..af96433e 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -3,7 +3,7 @@ import { uMapAlertConflict as AlertConflict, uMapAlertCreation as AlertCreation, } from '../components/alerts/alert.js' -import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js' +import { AjaxAutocomplete, AjaxAutocompleteMultiple, AutocompleteDatalist } from './autocomplete.js' import Browser from './browser.js' import Caption from './caption.js' import Facets from './facets.js' @@ -33,6 +33,7 @@ window.U = { AlertConflict, AjaxAutocomplete, AjaxAutocompleteMultiple, + AutocompleteDatalist, Browser, Caption, Dialog, diff --git a/umap/static/umap/js/modules/rules.js b/umap/static/umap/js/modules/rules.js index 518339c1..0c945059 100644 --- a/umap/static/umap/js/modules/rules.js +++ b/umap/static/umap/js/modules/rules.js @@ -1,6 +1,7 @@ import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js' import { translate } from './i18n.js' import * as Utils from './utils.js' +import { AutocompleteDatalist } from './autocomplete.js' class Rule { get condition() { @@ -123,7 +124,18 @@ class Rule { const builder = new U.FormBuilder(this, options) const defaultShapeProperties = DomUtil.add('div', '', container) defaultShapeProperties.appendChild(builder.build()) - + const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input) + const properties = this.map.allProperties() + autocomplete.suggestions = properties + autocomplete.input.addEventListener('input', (event) => { + const value = event.target.value + if (properties.includes(value)) { + autocomplete.suggestions = [`${value}=`, `${value}!=`, `${value}>`, `${value}<`] + } else if (value.endsWith('=')) { + const key = value.split('!')[0].split('=')[0] + autocomplete.suggestions = this.map.sortedValues(key).map((str) => `${value}${str || ''}`) + } + }) this.map.editPanel.open({ content: container }) } diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 021734f0..0d9af4e2 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1895,4 +1895,15 @@ U.Map = L.Map.extend({ }) await this.server.post(sendLink, {}, formData) }, + + allProperties: function () { + return [].concat(...this.datalayers_index.map((dl) => dl._propertiesIndex)) + }, + + sortedValues: function (property) { + return [] + .concat(...this.datalayers_index.map((dl) => dl.sortedValues(property))) + .filter((val, idx, arr) => arr.indexOf(val) === idx) + .sort(U.Utils.naturalSort) + }, }) diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index c7d4d1a3..0eee5b9e 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -1052,6 +1052,13 @@ U.DataLayer = L.Evented.extend({ if (idx !== -1) this._propertiesIndex.splice(idx, 1) }, + sortedValues: function (property) { + return Object.values(this._layers) + .map((feature) => feature.properties[property]) + .filter((val, idx, arr) => arr.indexOf(val) === idx) + .sort(U.Utils.naturalSort) + }, + addData: function (geojson, sync) { try { // Do not fail if remote data is somehow invalid, diff --git a/umap/tests/integration/test_conditional_rules.py b/umap/tests/integration/test_conditional_rules.py index d0c9cc03..3db4b5b4 100644 --- a/umap/tests/integration/test_conditional_rules.py +++ b/umap/tests/integration/test_conditional_rules.py @@ -199,3 +199,24 @@ def test_can_deactive_rule_from_list(live_server, page, openmap): page.get_by_role("button", name="Show/hide layer").click() colors = getColors(markers) assert colors.count("rgb(240, 248, 255)") == 2 + + +def test_autocomplete_datalist(live_server, page, openmap): + DataLayerFactory(map=openmap, data=DATALAYER_DATA1) + page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit#6/48.948/1.670") + page.get_by_role("link", name="Map advanced properties").click() + page.locator("summary").filter(has_text="Conditional style rules").click() + page.get_by_role("button", name="Add rule").click() + panel = page.locator(".panel.right.on") + datalist = panel.locator(".umap-field-condition datalist option") + expect(datalist).to_have_count(5) + values = {option.inner_text() for option in datalist.all()} + assert values == {"myboolean", "mytype", "mynumber", "mydate", "name"} + page.get_by_placeholder("key=value or key!=value").fill("mytype") + expect(datalist).to_have_count(4) + values = {option.inner_text() for option in datalist.all()} + assert values == {"mytype=", "mytype!=", "mytype>", "mytype<"} + page.get_by_placeholder("key=value or key!=value").fill("mytype=") + expect(datalist).to_have_count(2) + values = {option.inner_text() for option in datalist.all()} + assert values == {"mytype=even", "mytype=odd"}