From aa78b13f8e4ade77e991ecccf376df8ec53d0883 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 3 May 2024 12:53:06 +0200 Subject: [PATCH 01/21] feat: integrate facets into browser filters --- umap/static/umap/js/modules/browser.js | 46 +++++++++++++++---- umap/static/umap/js/modules/facets.js | 25 +--------- umap/static/umap/js/modules/schema.js | 6 +-- umap/static/umap/js/umap.controls.js | 3 -- umap/static/umap/js/umap.core.js | 1 + umap/static/umap/js/umap.features.js | 8 ++++ umap/static/umap/js/umap.js | 30 +++++------- umap/static/umap/js/umap.layer.js | 5 +- umap/static/umap/map.css | 4 +- umap/tests/integration/test_browser.py | 7 +++ umap/tests/integration/test_facets_browser.py | 37 +++++++++------ 11 files changed, 95 insertions(+), 77 deletions(-) diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index f68dc457..2c835431 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -9,11 +9,24 @@ export default class Browser { filter: '', inBbox: false, } + this._mode = 'layers' + } + + set mode(value) { + // Force only if mode is known, otherwise keep current mode. + if (!value) return + // Store the mode so we can respect it when we redraw + if (['data', 'filters'].includes(value)) this.map.panel.mode = 'expanded' + else if (value === 'layers') this.map.panel.mode = 'condensed' + this._mode = value + } + + get mode() { + return this._mode } addFeature(feature, parent) { - const filter = this.options.filter - if (filter && !feature.matchFilter(filter, this.filterKeys)) return + if (feature.isFiltered()) return if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return const row = DomUtil.create('li', `${feature.getClassName()} feature`) const zoom_to = DomUtil.createButtonIcon( @@ -105,6 +118,10 @@ export default class Browser { }) } + redraw() { + if (this.isOpen()) this.open() + } + isOpen() { return !!document.querySelector('.umap-browser') } @@ -126,7 +143,8 @@ export default class Browser { }) } - open() { + open(mode) { + this.mode = mode // Get once but use it for each feature later this.filterKeys = this.map.getFilterKeys() const container = DomUtil.create('div') @@ -136,17 +154,29 @@ export default class Browser { DomUtil.createTitle(container, translate('Browse data'), 'icon-layers') this.tabsMenu(container, 'browse') - const formContainer = DomUtil.create('div', '', container) + const formContainer = DomUtil.createFieldset(container, L._('Filters'), { + on: this.mode === 'filters', + }) this.dataContainer = DomUtil.create('div', '', container) - const fields = [ - ['options.filter', { handler: 'Input', placeholder: translate('Filter') }], + let fields = [ + [ + 'options.filter', + { handler: 'Input', placeholder: translate('Search map features…') }, + ], ['options.inBbox', { handler: 'Switch', label: translate('Current map view') }], ] const builder = new L.FormBuilder(this, fields, { callback: () => this.onFormChange(), }) formContainer.appendChild(builder.build()) + if (this.map.options.facetKey) { + fields = this.map.facets.build() + const builder = new L.FormBuilder(this.map.facets, fields, { + callback: () => this.onFormChange(), + }) + formContainer.appendChild(builder.build()) + } this.map.panel.open({ content: container, @@ -171,10 +201,6 @@ export default class Browser { const tabs = L.DomUtil.create('div', 'flat-tabs', container) const browse = L.DomUtil.add('button', 'flat tab-browse', tabs, L._('Data')) DomEvent.on(browse, 'click', this.open, this) - if (this.map.options.facetKey) { - const facets = L.DomUtil.add('button', 'flat tab-facets', tabs, L._('Filters')) - DomEvent.on(facets, 'click', this.map.facets.open, this.map.facets) - } const info = L.DomUtil.add('button', 'flat tab-info', tabs, L._('About')) DomEvent.on(info, 'click', this.map.displayCaption, this.map) let el = tabs.querySelector(`.tab-${active}`) diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index 00900b7b..8e736fd4 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -53,23 +53,8 @@ export default class Facets { return properties } - redraw() { - if (this.isOpen()) this.open() - } + build() { - isOpen() { - return !!document.querySelector('.umap-facet-search') - } - - open() { - const container = L.DomUtil.create('div', 'umap-facet-search') - const title = L.DomUtil.add( - 'h3', - 'umap-filter-title', - container, - translate('Facet search') - ) - this.map.browser.tabsMenu(container, 'facets') const defined = this.getDefined() const names = Object.keys(defined) const facetProperties = this.compute(names, defined) @@ -114,13 +99,7 @@ export default class Facets { ] }) - const builder = new L.FormBuilder(this, fields, { - callback: filterFeatures, - callbackContext: this, - }) - container.appendChild(builder.build()) - - this.map.panel.open({ content: container }) + return fields } getDefined() { diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index e733739b..30bc9e3f 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -272,9 +272,9 @@ export const SCHEMA = { choices: [ ['none', translate('None')], ['caption', translate('Caption')], - ['databrowser', translate('Data browser')], - ['datalayers', translate('Layers')], - ['facet', translate('Facet search')], + ['databrowser', translate('Browser in data mode')], + ['datalayers', translate('Browser in layers mode')], + ['datafilters', translate('Browser in filters mode')], ], default: 'none', }, diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 8948489d..0cf90bb0 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -660,9 +660,6 @@ const ControlsMixin = { 'star', 'tilelayers', ], - _openFacet: function () { - this.facets.open() - }, displayCaption: function () { const container = L.DomUtil.create('div', 'umap-caption') diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 156d15c7..988757b9 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -84,6 +84,7 @@ L.DomUtil.createFieldset = (container, legend, options) => { const fieldset = L.DomUtil.create('div', 'fieldset toggle', container) const legendEl = L.DomUtil.add('h5', 'legend style_options_toggle', fieldset, legend) const fieldsEl = L.DomUtil.add('div', 'fields with-transition', fieldset) + L.DomUtil.classIf(fieldset, 'on', options.on) L.DomEvent.on(legendEl, 'click', function () { if (L.DomUtil.hasClass(fieldset, 'on')) { L.DomUtil.removeClass(fieldset, 'on') diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 5d4b0d86..0daaf553 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -494,6 +494,14 @@ U.FeatureMixin = { this.bindTooltip(U.Utils.escapeHTML(displayName), options) }, + isFiltered: function () { + const filterKeys = this.map.getFilterKeys(), + filter = this.map.browser.options.filter + if (filter && !this.matchFilter(filter, filterKeys)) return true + if (!this.matchFacets()) return true + return false + }, + matchFilter: function (filter, keys) { filter = filter.toLowerCase() for (let i = 0, value; i < keys.length; i++) { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index f11054bb..e7e02465 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -211,15 +211,15 @@ U.Map = L.Map.extend({ if (L.Util.queryString('share')) { this.share.open() } else if (this.options.onLoadPanel === 'databrowser') { - this.openBrowser('expanded') + this.openBrowser('data') } else if (this.options.onLoadPanel === 'datalayers') { - this.openBrowser('condensed') + this.openBrowser('layers') } else if (this.options.onLoadPanel === 'caption') { this.panel.mode = 'condensed' this.displayCaption() } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { this.panel.mode = 'expanded' - this.openFacet() + this.openBrowser('filters') } if (L.Util.queryString('edit')) { if (this.hasEditMode()) this.enableEdit() @@ -252,7 +252,7 @@ U.Map = L.Map.extend({ this.initCaptionBar() this.renderEditToolbar() this.renderControls() - this.facets.redraw() + this.browser.redraw() break case 'data': this.redrawVisibleDataLayers() @@ -908,15 +908,8 @@ U.Map = L.Map.extend({ }, openBrowser: function (mode) { - if (mode) this.panel.mode = mode this.onceDatalayersLoaded(function () { - this.browser.open() - }) - }, - - openFacet: function () { - this.onceDataLoaded(function () { - this._openFacet() + this.browser.open(mode) }) }, @@ -1602,15 +1595,14 @@ U.Map = L.Map.extend({ 'umap-open-browser-link flat', container, L._('Browse data'), - () => this.openBrowser('expanded') + () => this.openBrowser('data') ) if (this.options.facetKey) { L.DomUtil.createButton( 'umap-open-filter-link flat', container, L._('Select data'), - this.openFacet, - this + () => this.openBrowser('filters') ) } } @@ -1747,17 +1739,17 @@ U.Map = L.Map.extend({ '-', { text: L._('See layers'), - callback: () => this.openBrowser('condensed'), + callback: () => this.openBrowser('layers'), }, { text: L._('Browse data'), - callback: () => this.openBrowser('expanded'), + callback: () => this.openBrowser('data'), } ) if (this.options.facetKey) { items.push({ - text: L._('Facet search'), - callback: this.openFacet, + text: L._('Filter data'), + callback: () => this.openBrowser('filters'), }) } items.push( diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 8a137e8b..b346f40b 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -875,10 +875,7 @@ U.DataLayer = L.Evented.extend({ }, showFeature: function (feature) { - const filterKeys = this.map.getFilterKeys(), - filter = this.map.browser.options.filter - if (filter && !feature.matchFilter(filter, filterKeys)) return - if (!feature.matchFacets()) return + if (feature.isFiltered()) return this.layer.addLayer(feature) }, diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 666694f1..af5f7e20 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -888,10 +888,10 @@ a.umap-control-caption, .umap-browser .datalayer i { cursor: pointer; } -.umap-browser ul { +.umap-browser .datalayer ul { display: none; } -.show-list ul { +.umap-browser .show-list ul { display: block; } diff --git a/umap/tests/integration/test_browser.py b/umap/tests/integration/test_browser.py index 11a31460..fcb35d96 100644 --- a/umap/tests/integration/test_browser.py +++ b/umap/tests/integration/test_browser.py @@ -77,6 +77,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): paths = page.locator(".leaflet-overlay-pane path") expect(markers).to_have_count(1) expect(paths).to_have_count(2) + page.get_by_role("heading", name="filters").click() filter_ = page.locator("input[name='filter']") expect(filter_).to_be_visible() filter_.type("poly") @@ -103,6 +104,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): def test_data_browser_can_show_only_visible_features(live_server, page, bootstrap, map): # Zoom on France page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -114,6 +116,7 @@ def test_data_browser_can_show_only_visible_features(live_server, page, bootstra def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map): # Zoom on north west page.goto(f"{live_server.url}{map.get_absolute_url()}#4/61.98/-2.68") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -131,6 +134,7 @@ def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map) def test_data_browser_bbox_limit_should_be_dynamic(live_server, page, bootstrap, map): # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -156,6 +160,7 @@ def test_data_browser_bbox_filter_should_be_persistent( ): # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -181,6 +186,7 @@ def test_data_browser_bbox_filtered_is_clickable(live_server, page, bootstrap, m popup = page.locator(".leaflet-popup") # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -202,6 +208,7 @@ def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map): expect(page.get_by_text("one point in france (point)")).to_be_visible() expect(page.get_by_text("one line in new zeland (line)")).to_be_visible() expect(page.get_by_text("one polygon in greenland (polygon)")).to_be_visible() + page.get_by_role("heading", name="filters").click() filter_ = page.locator("input[name='filter']") expect(filter_).to_be_visible() filter_.type("foobar") # Hide all diff --git a/umap/tests/integration/test_facets_browser.py b/umap/tests/integration/test_facets_browser.py index f72d3124..371152b8 100644 --- a/umap/tests/integration/test_facets_browser.py +++ b/umap/tests/integration/test_facets_browser.py @@ -93,7 +93,7 @@ DATALAYER_DATA3 = { def test_simple_facet_search(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number" map.settings["properties"]["showLabel"] = True map.save() @@ -101,7 +101,7 @@ def test_simple_facet_search(live_server, page, map): DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA3) page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") - panel = page.locator(".umap-facet-search") + panel = page.locator(".umap-browser") # From a non browsable datalayer, should not be impacted paths = page.locator(".leaflet-overlay-pane path") expect(paths).to_be_visible() @@ -117,17 +117,28 @@ def test_simple_facet_search(live_server, page, map): markers = page.locator(".leaflet-marker-icon") expect(markers).to_have_count(4) # Tooltips - expect(page.get_by_text("Point 1")).to_be_visible() - expect(page.get_by_text("Point 2")).to_be_visible() - expect(page.get_by_text("Point 3")).to_be_visible() - expect(page.get_by_text("Point 4")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 1")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 2")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 3")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 4")).to_be_visible() + + # Datalist + expect(panel.get_by_text("Point 1")).to_be_visible() + expect(panel.get_by_text("Point 2")).to_be_visible() + expect(panel.get_by_text("Point 3")).to_be_visible() + expect(panel.get_by_text("Point 4")).to_be_visible() + # Now let's filter odd.click() expect(markers).to_have_count(2) - expect(page.get_by_text("Point 2")).to_be_hidden() - expect(page.get_by_text("Point 4")).to_be_hidden() - expect(page.get_by_text("Point 1")).to_be_visible() - expect(page.get_by_text("Point 3")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 2")).to_be_hidden() + expect(page.get_by_role("tooltip", name="Point 4")).to_be_hidden() + expect(page.get_by_role("tooltip", name="Point 1")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 3")).to_be_visible() + expect(panel.get_by_text("Point 2")).to_be_hidden() + expect(panel.get_by_text("Point 4")).to_be_hidden() + expect(panel.get_by_text("Point 1")).to_be_visible() + expect(panel.get_by_text("Point 3")).to_be_visible() expect(paths).to_be_visible # Now let's filter odd.click() @@ -156,7 +167,7 @@ def test_simple_facet_search(live_server, page, map): def test_date_facet_search(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mydate|Date filter|date" map.save() DataLayerFactory(map=map, data=DATALAYER_DATA1) @@ -174,7 +185,7 @@ def test_date_facet_search(live_server, page, map): def test_choice_with_empty_value(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mytype|My type" map.save() data = copy.deepcopy(DATALAYER_DATA1) @@ -191,7 +202,7 @@ def test_choice_with_empty_value(live_server, page, map): def test_number_with_zero_value(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mynumber|Filter|number" map.save() data = copy.deepcopy(DATALAYER_DATA1) From f78e95b0884450783eda345179f398a06392aedd Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 3 May 2024 16:06:51 +0200 Subject: [PATCH 02/21] wip: remove tabs from browser --- umap/static/umap/js/modules/browser.js | 11 ----------- umap/static/umap/js/umap.controls.js | 1 - 2 files changed, 12 deletions(-) diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 2c835431..cd1a45ff 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -153,7 +153,6 @@ export default class Browser { DomEvent.disableClickPropagation(container) DomUtil.createTitle(container, translate('Browse data'), 'icon-layers') - this.tabsMenu(container, 'browse') const formContainer = DomUtil.createFieldset(container, L._('Filters'), { on: this.mode === 'filters', }) @@ -196,14 +195,4 @@ export default class Browser { DomEvent.on(button, 'click', map.openBrowser, map) return button } - - tabsMenu(container, active) { - const tabs = L.DomUtil.create('div', 'flat-tabs', container) - const browse = L.DomUtil.add('button', 'flat tab-browse', tabs, L._('Data')) - DomEvent.on(browse, 'click', this.open, this) - const info = L.DomUtil.add('button', 'flat tab-info', tabs, L._('About')) - DomEvent.on(info, 'click', this.map.displayCaption, this.map) - let el = tabs.querySelector(`.tab-${active}`) - L.DomUtil.addClass(el, 'on') - } } diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 0cf90bb0..7d2c4cf1 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -665,7 +665,6 @@ const ControlsMixin = { const container = L.DomUtil.create('div', 'umap-caption') L.DomUtil.createTitle(container, this.options.name, 'icon-caption') this.permissions.addOwnerLink('h5', container) - this.browser.tabsMenu(container, 'info') if (this.options.description) { const description = L.DomUtil.element({ tagName: 'div', From 406198882a8f58a2908052b895c786e37b3eaebb Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 3 May 2024 18:48:26 +0200 Subject: [PATCH 03/21] wip: add filter icon for filters button --- umap/static/umap/css/icon.css | 3 +++ umap/static/umap/img/16.svg | 4 ++-- umap/static/umap/img/source/16.svg | 6 +++--- umap/static/umap/js/modules/browser.js | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/umap/static/umap/css/icon.css b/umap/static/umap/css/icon.css index 2bf10a74..69778b26 100644 --- a/umap/static/umap/css/icon.css +++ b/umap/static/umap/css/icon.css @@ -55,6 +55,9 @@ .off .icon-edit { background-position: -51px -73px; } +.icon-filters { + background-position: -4px -24px; +} .icon-key { background-position: -144px -121px; } diff --git a/umap/static/umap/img/16.svg b/umap/static/umap/img/16.svg index 4cf58b6b..d30369be 100644 --- a/umap/static/umap/img/16.svg +++ b/umap/static/umap/img/16.svg @@ -28,7 +28,6 @@ - @@ -36,7 +35,8 @@ - + +   diff --git a/umap/static/umap/img/source/16.svg b/umap/static/umap/img/source/16.svg index 494ee015..c7569e88 100644 --- a/umap/static/umap/img/source/16.svg +++ b/umap/static/umap/img/source/16.svg @@ -10,7 +10,7 @@ - + @@ -46,7 +46,6 @@ - @@ -55,7 +54,8 @@ - + +   diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index cd1a45ff..38801b50 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -155,6 +155,8 @@ export default class Browser { DomUtil.createTitle(container, translate('Browse data'), 'icon-layers') const formContainer = DomUtil.createFieldset(container, L._('Filters'), { on: this.mode === 'filters', + className: 'fieldset filters toggle', + icon: 'icon-filters', }) this.dataContainer = DomUtil.create('div', '', container) From 0b98ef2f9d84d24f78af5306795e114f36c507ed Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 3 May 2024 18:53:05 +0200 Subject: [PATCH 04/21] wip: replace fake fieldset by proper details/summary tags --- umap/static/umap/base.css | 48 ++++++-------------------- umap/static/umap/css/panel.css | 3 +- umap/static/umap/js/modules/browser.js | 2 +- umap/static/umap/js/umap.core.js | 20 ++++------- umap/static/umap/map.css | 15 ++++++++ umap/static/umap/vars.css | 6 ++++ 6 files changed, 40 insertions(+), 54 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index b7f11b0e..62db0ee4 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -290,61 +290,33 @@ input:invalid { border-color: #1b1f20; color: #efefef; } -.fieldset { +details { margin-bottom: 5px; border-top-left-radius: 4px; border-top-right-radius: 4px; } -.dark .fieldset { +.dark details { border: 1px solid #222; } -.fieldset .fields { - visibility: hidden; - opacity: 0; - transition: visibility 0s, opacity 0.5s linear; - height: 0; +details fieldset { overflow: hidden; + border: 1px solid var(--color-lightGray); + margin: 0; + padding-top: 10px; } -.fieldset.toggle.on .fields { - visibility: visible; - opacity: 1; - height: initial; - padding: 10px; -} -.fieldset.toggle .legend { - text-align: left; - display: block; +details summary { cursor: pointer; background-color: var(--color-lightGray); - height: 30px; line-height: 30px; - margin: 0; - font-family: fira_sans; - font-weight: normal; font-size: 1.2em; padding: 0 5px; } -.dark .fieldset.toggle .legend { +.dark details summary { background-color: #232729; color: #fff; } -.fieldset.toggle .legend:before { - background-repeat: no-repeat; - text-indent: 24px; - height: 24px; - width: 24px; - line-height: 24px; - display: inline-block; - background-image: url('./img/16.svg'); - vertical-align: bottom; - content: " "; - background-position: -144px -76px; -} -.dark .fieldset.toggle .legend:before { - background-image: url('./img/16-white.svg'); -} -.fieldset.toggle.on .legend:before { - background-position: -144px -51px; +.dark details fieldset { + border: 1px solid var(--color-darkGray); } fieldset legend { font-size: 1.1rem; diff --git a/umap/static/umap/css/panel.css b/umap/static/umap/css/panel.css index e980e430..a02178c4 100644 --- a/umap/static/umap/css/panel.css +++ b/umap/static/umap/css/panel.css @@ -6,7 +6,7 @@ bottom: var(--panel-bottom); overflow-x: auto; z-index: 1010; - background-color: #fff; + background-color: var(--background-color); opacity: 0.98; cursor: initial; border-radius: 5px; @@ -14,7 +14,6 @@ } .panel.dark { border: 1px solid #222; - background-color: var(--color-darkGray); color: #efefef; } .panel.full { diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 38801b50..c2b8ff2b 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -155,7 +155,7 @@ export default class Browser { DomUtil.createTitle(container, translate('Browse data'), 'icon-layers') const formContainer = DomUtil.createFieldset(container, L._('Filters'), { on: this.mode === 'filters', - className: 'fieldset filters toggle', + className: 'filters', icon: 'icon-filters', }) this.dataContainer = DomUtil.create('div', '', container) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 988757b9..28833df7 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -81,19 +81,13 @@ L.DomUtil.add = (tagName, className, container, content) => { L.DomUtil.createFieldset = (container, legend, options) => { options = options || {} - const fieldset = L.DomUtil.create('div', 'fieldset toggle', container) - const legendEl = L.DomUtil.add('h5', 'legend style_options_toggle', fieldset, legend) - const fieldsEl = L.DomUtil.add('div', 'fields with-transition', fieldset) - L.DomUtil.classIf(fieldset, 'on', options.on) - L.DomEvent.on(legendEl, 'click', function () { - if (L.DomUtil.hasClass(fieldset, 'on')) { - L.DomUtil.removeClass(fieldset, 'on') - } else { - L.DomUtil.addClass(fieldset, 'on') - if (options.callback) options.callback.call(options.context || this) - } - }) - return fieldsEl + const details = L.DomUtil.create('details', options.className || '', container) + const summary = L.DomUtil.add('summary', '', details) + if (options.icon) L.DomUtil.createIcon(summary, options.icon) + L.DomUtil.add('span', '', summary, legend) + const fieldset = L.DomUtil.add('fieldset', '', details) + details.open = options.on === true + return fieldset } L.DomUtil.createButton = (className, container, content, callback, context) => { diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index af5f7e20..8d872b92 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -982,6 +982,21 @@ a.umap-control-caption, .umap-browser .show-list .datalayer-toggle-list { background-position: -145px -45px; } +.umap-browser .filters summary { + background: none; + border: 1px solid var(--color-lightGray); + width: fit-content; + padding: 0 10px; + margin-bottom: var(--block-margin); +} +.umap-browser .filters summary { + list-style: none; + display: inline-block; +} +.umap-browser details[open].filters summary { + margin-bottom: -1px; + border-bottom: 1px solid var(--background-color); +} .datalayer-name { cursor: pointer; } diff --git a/umap/static/umap/vars.css b/umap/static/umap/vars.css index c63a1669..86a1f38d 100644 --- a/umap/static/umap/vars.css +++ b/umap/static/umap/vars.css @@ -4,6 +4,9 @@ --color-darkBlue: #263B58; --color-lightGray: #ddd; --color-darkGray: #323737; + --color-light: white; + + --background-color: var(--color-light); /* Buttons. */ --button-primary-background: var(--color-waterMint); @@ -20,3 +23,6 @@ --footer-height: 46px; --control-size: 36px; } +.dark { + --background-color: var(--color-darkGray); +} From d805653e3c52c324a739fc77a9c891c4b779793c Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 12:20:40 +0200 Subject: [PATCH 05/21] wip: make explicit in browser.build that we set mode only if defined --- umap/static/umap/js/modules/browser.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index c2b8ff2b..4f00f41c 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -13,8 +13,6 @@ export default class Browser { } set mode(value) { - // Force only if mode is known, otherwise keep current mode. - if (!value) return // Store the mode so we can respect it when we redraw if (['data', 'filters'].includes(value)) this.map.panel.mode = 'expanded' else if (value === 'layers') this.map.panel.mode = 'condensed' @@ -144,7 +142,8 @@ export default class Browser { } open(mode) { - this.mode = mode + // Force only if mode is known, otherwise keep current mode. + if (mode) this.mode = mode // Get once but use it for each feature later this.filterKeys = this.map.getFilterKeys() const container = DomUtil.create('div') From 701b00f4f727052ecd7efab7b00258f81d2ca1e6 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 12:21:31 +0200 Subject: [PATCH 06/21] wip: do not override selected values when recomputing facets --- umap/static/umap/js/modules/facets.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index 8e736fd4..0cf3a592 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -10,15 +10,18 @@ export default class Facets { compute(names, defined) { const properties = {} + let selected names.forEach((name) => { const type = defined[name]['type'] properties[name] = { type: type } - this.selected[name] = { type: type } + selected = this.selected[name] || {} + selected.type = type if (!['date', 'datetime', 'number'].includes(type)) { properties[name].choices = [] - this.selected[name].choices = [] + selected.choices = selected.choices || [] } + this.selected[name] = selected }) this.map.eachBrowsableDataLayer((datalayer) => { From 43755c81fa6eebdf54ae179cd45392d94d971a64 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 12:23:17 +0200 Subject: [PATCH 07/21] fix: only set date/number facets selected if values have changed For displaying the badge, we need to know when custom values have been set. Also, this prevent useless facet checks when user has changed one value then changed it back to default. --- umap/static/umap/js/umap.forms.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 4098e0ec..85bda31d 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -843,11 +843,22 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ }, toJS: function () { - return { + const opts = { type: this.type, - min: this.minInput.value, - max: this.maxInput.value, } + if ( + this.minInput.value !== '' && + this.minInput.value !== this.minInput.dataset.value + ) { + opts.min = this.minInput.value + } + if ( + this.maxInput.value !== '' && + this.maxInput.value !== this.maxInput.dataset.value + ) { + opts.max = this.maxInput.value + } + return opts }, }) From ba0ba1a85d2b9296440009d78358e80ea31943d5 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 12:28:13 +0200 Subject: [PATCH 08/21] wip: add a badge on filters title and datalayers icon when active --- umap/static/umap/base.css | 21 +++++++++++++++++++++ umap/static/umap/js/modules/browser.js | 8 ++++++++ umap/static/umap/js/modules/facets.js | 10 +++++++++- umap/static/umap/js/modules/utils.js | 8 ++++++++ umap/static/umap/js/umap.controls.js | 7 +++++++ umap/static/umap/vars.css | 1 + 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 62db0ee4..6abd0c78 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -323,6 +323,27 @@ fieldset legend { padding: 0 5px; } +[data-badge] { + position: relative; +} +[data-badge]:after { + position: absolute; + right: -8px; + top: -8px; + min-width: 8px; + min-height: 8px; + line-height: 8px; + padding: 2px; + font-weight: bold; + background-color: var(--color-flashyGreen); + color: var(--color-darkBlue); + text-align: center; + font-size: .75rem; + border-radius: 50%; + content: attr(data-badge); + border: solid .5px var(--color-darkBlue); +} + /* Switch */ input.switch:empty { display: none; diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 4f00f41c..22811c86 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -114,6 +114,8 @@ export default class Browser { datalayer.resetLayer(true) this.updateDatalayer(datalayer) }) + U.Utils.toggleBadge(this.filtersTitle, this.hasFilters()) + U.Utils.toggleBadge('.umap-control-browse', this.hasFilters()) } redraw() { @@ -124,6 +126,10 @@ export default class Browser { return !!document.querySelector('.umap-browser') } + hasFilters() { + return !!this.options.filter || this.map.facets.isActive() + } + onMoveEnd() { if (!this.isOpen()) return const isListDynamic = this.options.inBbox @@ -157,6 +163,8 @@ export default class Browser { className: 'filters', icon: 'icon-filters', }) + this.filtersTitle = container.querySelector('summary') + U.Utils.toggleBadge(this.filtersTitle, this.hasFilters()) this.dataContainer = DomUtil.create('div', '', container) let fields = [ diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index 0cf3a592..c1ecf5b7 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -56,8 +56,16 @@ export default class Facets { return properties } - build() { + isActive() { + for (let { type, min, max, choices } of Object.values(this.selected)) { + if (min !== undefined || max != undefined || choices?.length) { + return true + } + } + return false + } + build() { const defined = this.getDefined() const names = Object.keys(defined) const facetProperties = this.compute(names, defined) diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index 340d1122..e7974a17 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -362,3 +362,11 @@ export function parseNaiveDate(value) { // Let's pretend naive date are UTC, and remove time… return new Date(Date.UTC(naive.getFullYear(), naive.getMonth(), naive.getDate())) } + +export function toggleBadge(element, value) { + if (!element.nodeType) element = document.querySelector(element) + if (!element) return + // True means simple badge, without content + if (value) element.dataset.badge = value === true ? ' ' : value + else delete element.dataset.badge +} diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 7d2c4cf1..73793296 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -496,8 +496,11 @@ L.Control.Button = L.Control.extend({ this ) L.DomEvent.on(button, 'dblclick', L.DomEvent.stopPropagation) + this.afterAdd(container) return container }, + + afterAdd: function (container) {}, }) U.DataLayersControl = L.Control.Button.extend({ @@ -507,6 +510,10 @@ U.DataLayersControl = L.Control.Button.extend({ title: L._('See layers'), }, + afterAdd: function (container) { + U.Utils.toggleBadge(container, this.map.browser.hasFilters()) + }, + onClick: function () { this.map.openBrowser() }, diff --git a/umap/static/umap/vars.css b/umap/static/umap/vars.css index 86a1f38d..658ca890 100644 --- a/umap/static/umap/vars.css +++ b/umap/static/umap/vars.css @@ -5,6 +5,7 @@ --color-lightGray: #ddd; --color-darkGray: #323737; --color-light: white; + --color-flashyGreen: #b9f5d2; --background-color: var(--color-light); From cd48267cf2a29cc9bc14c7564b3cbc8337efecac Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 12:29:48 +0200 Subject: [PATCH 09/21] wip: remove unused code --- umap/static/umap/js/modules/facets.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index c1ecf5b7..461a8ec3 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -70,21 +70,6 @@ export default class Facets { const names = Object.keys(defined) const facetProperties = this.compute(names, defined) - const filterFeatures = function () { - let found = false - this.map.eachBrowsableDataLayer((datalayer) => { - datalayer.resetLayer(true) - if (datalayer.hasDataVisible()) found = true - }) - // TODO: display a results counter in the panel instead. - if (!found) { - this.map.ui.alert({ - content: translate('No results for these facets'), - level: 'info', - }) - } - } - const fields = names.map((name) => { let criteria = facetProperties[name] let handler = 'FacetSearchChoices' From 2f3e7d03ab98b4e9fcb5d3934cbb82115959bec2 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 13:29:03 +0200 Subject: [PATCH 10/21] chore: fix tests after fieldset refactor --- umap/static/umap/js/umap.core.js | 5 +++++ umap/tests/integration/test_browser.py | 14 +++++++------- umap/tests/integration/test_choropleth.py | 2 +- umap/tests/integration/test_edit_datalayer.py | 10 +++++----- umap/tests/integration/test_edit_map.py | 8 ++++---- umap/tests/integration/test_edit_marker.py | 12 ++++++------ umap/tests/integration/test_edit_polygon.py | 12 ++++++------ 7 files changed, 34 insertions(+), 29 deletions(-) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 28833df7..11d1dc53 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -87,6 +87,11 @@ L.DomUtil.createFieldset = (container, legend, options) => { L.DomUtil.add('span', '', summary, legend) const fieldset = L.DomUtil.add('fieldset', '', details) details.open = options.on === true + if (options.callback) { + L.DomEvent.on(details, 'toggle', () => { + if (details.open) options.callback.call(options.context || this) + }) + } return fieldset } diff --git a/umap/tests/integration/test_browser.py b/umap/tests/integration/test_browser.py index fcb35d96..90e3be9c 100644 --- a/umap/tests/integration/test_browser.py +++ b/umap/tests/integration/test_browser.py @@ -77,7 +77,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): paths = page.locator(".leaflet-overlay-pane path") expect(markers).to_have_count(1) expect(paths).to_have_count(2) - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() filter_ = page.locator("input[name='filter']") expect(filter_).to_be_visible() filter_.type("poly") @@ -104,7 +104,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): def test_data_browser_can_show_only_visible_features(live_server, page, bootstrap, map): # Zoom on France page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -116,7 +116,7 @@ def test_data_browser_can_show_only_visible_features(live_server, page, bootstra def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map): # Zoom on north west page.goto(f"{live_server.url}{map.get_absolute_url()}#4/61.98/-2.68") - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -134,7 +134,7 @@ def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map) def test_data_browser_bbox_limit_should_be_dynamic(live_server, page, bootstrap, map): # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -160,7 +160,7 @@ def test_data_browser_bbox_filter_should_be_persistent( ): # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -186,7 +186,7 @@ def test_data_browser_bbox_filtered_is_clickable(live_server, page, bootstrap, m popup = page.locator(".leaflet-popup") # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -208,7 +208,7 @@ def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map): expect(page.get_by_text("one point in france (point)")).to_be_visible() expect(page.get_by_text("one line in new zeland (line)")).to_be_visible() expect(page.get_by_text("one polygon in greenland (polygon)")).to_be_visible() - page.get_by_role("heading", name="filters").click() + page.locator(".filters summary").click() filter_ = page.locator("input[name='filter']") expect(filter_).to_be_visible() filter_.type("foobar") # Hide all diff --git a/umap/tests/integration/test_choropleth.py b/umap/tests/integration/test_choropleth.py index e896dba1..65ef4ddd 100644 --- a/umap/tests/integration/test_choropleth.py +++ b/umap/tests/integration/test_choropleth.py @@ -50,7 +50,7 @@ def test_basic_choropleth_map_with_custom_brewer(openmap, live_server, page): page.get_by_role("button", name="Edit").click() page.get_by_role("link", name="Manage layers").click() page.locator(".panel").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Choropleth: settings").click() + page.get_by_text("Choropleth: settings").click() page.locator('select[name="brewer"]').select_option("Greens") # Hauts-de-France diff --git a/umap/tests/integration/test_edit_datalayer.py b/umap/tests/integration/test_edit_datalayer.py index ecfba705..efbbdb80 100644 --- a/umap/tests/integration/test_edit_datalayer.py +++ b/umap/tests/integration/test_edit_datalayer.py @@ -80,7 +80,7 @@ def test_can_clone_datalayer(live_server, openmap, login, datalayer, page): expect(markers).to_have_count(1) page.get_by_role("link", name="Manage layers").click() page.locator(".panel.right").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Advanced actions").click() + page.get_by_text("Advanced actions").click() page.get_by_role("button", name="Clone").click() expect(layers).to_have_count(2) expect(markers).to_have_count(2) @@ -104,7 +104,7 @@ def test_can_change_icon_class(live_server, openmap, page): page.get_by_role("link", name="Manage layers").click() expect(page.locator(".umap-circle-icon")).to_be_hidden() page.locator(".panel.right").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator(".umap-field-iconClass a.define").click() page.get_by_text("Circle").click() expect(page.locator(".umap-circle-icon")).to_be_visible() @@ -165,14 +165,14 @@ def test_can_restore_version(live_server, openmap, page, datalayer): marker = page.locator(".leaflet-marker-icon") expect(marker).to_have_class(re.compile(".*umap-ball-icon.*")) marker.click(modifiers=["Shift"]) - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator("#umap-feature-shape-properties").get_by_text("Default").click() with page.expect_response(re.compile(".*/datalayer/update/.*")): page.get_by_role("button", name="Save").click() expect(marker).to_have_class(re.compile(".*umap-div-icon.*")) page.get_by_role("link", name="Manage layers").click() page.locator(".panel.right").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Versions").click() + page.get_by_text("Versions").click() page.once("dialog", lambda dialog: dialog.accept()) page.get_by_role("button", name="Restore this version").last.click() expect(marker).to_have_class(re.compile(".*umap-ball-icon.*")) @@ -182,4 +182,4 @@ def test_can_edit_layer_on_ctrl_shift_click(live_server, openmap, page, datalaye modifier = "Meta" if platform.system() == "Darwin" else "Control" page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.locator(".leaflet-marker-icon").click(modifiers=[modifier, "Shift"]) - expect(page.get_by_role("heading", name="Layer properties")).to_be_visible() + expect(page.get_by_text("Layer properties")).to_be_visible() diff --git a/umap/tests/integration/test_edit_map.py b/umap/tests/integration/test_edit_map.py index 122eb864..abe9d7a8 100644 --- a/umap/tests/integration/test_edit_map.py +++ b/umap/tests/integration/test_edit_map.py @@ -58,7 +58,7 @@ def test_zoomcontrol_impacts_ui(live_server, page, tilelayer): expect(zoom_out).to_be_visible() # Hide them - page.get_by_role("heading", name="User interface options").click() + page.get_by_text("User interface options").click() hide_zoom_controls = ( page.locator("div") .filter(has_text=re.compile(r"^Display the zoom control")) @@ -90,7 +90,7 @@ def test_map_color_impacts_data(live_server, page, tilelayer): expect(marker_pane_p1).to_have_count(1) # Change the default color - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator("#umap-feature-shape-properties").get_by_text("define").first.click() page.get_by_title("Lime", exact=True).click() @@ -108,7 +108,7 @@ def test_limitbounds_impacts_ui(live_server, page, tilelayer): expect(gear_icon).to_be_visible() gear_icon.click() - page.get_by_role("heading", name="Limit bounds").click() + page.get_by_text("Limit bounds").click() default_zoom_url = f"{live_server.url}/en/map/new/#5/51.110/7.053" page.goto(default_zoom_url) page.get_by_role("button", name="Use current bounds").click() @@ -183,7 +183,7 @@ def test_sortkey_impacts_datalayerindex(map, live_server, page): # Change the default sortkey to be "key" page.get_by_role("button", name="Edit").click() page.get_by_role("link", name="Map advanced properties").click() - page.get_by_role("heading", name="Default properties").click() + page.get_by_text("Default properties").click() # Click "define" page.locator(".panel .umap-field-sortKey .define").click() diff --git a/umap/tests/integration/test_edit_marker.py b/umap/tests/integration/test_edit_marker.py index 731de01c..99a4eaf9 100644 --- a/umap/tests/integration/test_edit_marker.py +++ b/umap/tests/integration/test_edit_marker.py @@ -36,7 +36,7 @@ def test_can_edit_on_shift_click(live_server, openmap, page, datalayer): modifier = "Meta" if platform.system() == "Darwin" else "Control" page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.locator(".leaflet-marker-icon").click(modifiers=[modifier, "Shift"]) - expect(page.get_by_role("heading", name="Layer properties")).to_be_visible() + expect(page.get_by_text("Layer properties")).to_be_visible() def test_marker_style_should_have_precedence(live_server, openmap, page, bootstrap): @@ -45,7 +45,7 @@ def test_marker_style_should_have_precedence(live_server, openmap, page, bootstr # Change colour at layer level page.get_by_role("link", name="Manage layers").click() page.locator(".panel").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator(".umap-field-color .define").click() expect(page.locator(".leaflet-marker-icon .icon_container")).to_have_css( "background-color", "rgb(0, 0, 139)" @@ -57,7 +57,7 @@ def test_marker_style_should_have_precedence(live_server, openmap, page, bootstr # Now change at marker level, it should take precedence page.locator(".leaflet-marker-icon").click(modifiers=["Shift"]) - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator("#umap-feature-shape-properties").get_by_text("define").first.click() page.get_by_title("GoldenRod", exact=True).click() expect(page.locator(".leaflet-marker-icon .icon_container")).to_have_css( @@ -67,7 +67,7 @@ def test_marker_style_should_have_precedence(live_server, openmap, page, bootstr # Now change again at layer level again, it should not change the marker color page.get_by_role("link", name="Manage layers").click() page.locator(".panel").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator(".umap-field-color input").click() page.get_by_title("DarkViolet").first.click() expect(page.locator(".leaflet-marker-icon .icon_container")).to_have_css( @@ -87,12 +87,12 @@ def test_should_update_open_popup_on_edit(live_server, openmap, page, bootstrap) expect(page.locator(".umap-icon-active")).to_be_hidden() page.locator(".leaflet-marker-icon").click() expect(page.locator(".leaflet-popup-content-wrapper")).to_be_visible() - expect(page.get_by_role("heading", name="test marker")).to_be_visible() + expect(page.get_by_text("test marker")).to_be_visible() expect(page.get_by_text("Some description")).to_be_visible() page.get_by_role("button", name="Edit").click() page.locator(".leaflet-marker-icon").click(modifiers=["Shift"]) page.locator('input[name="name"]').fill("test marker edited") - expect(page.get_by_role("heading", name="test marker edited")).to_be_visible() + expect(page.get_by_text("test marker edited")).to_be_visible() def test_should_follow_datalayer_style_when_changing_datalayer( diff --git a/umap/tests/integration/test_edit_polygon.py b/umap/tests/integration/test_edit_polygon.py index b812e181..8ae4ca26 100644 --- a/umap/tests/integration/test_edit_polygon.py +++ b/umap/tests/integration/test_edit_polygon.py @@ -50,7 +50,7 @@ def test_can_edit_on_shift_click(live_server, openmap, page, datalayer): modifier = "Meta" if platform.system() == "Darwin" else "Control" page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.locator(".leaflet-marker-icon").click(modifiers=[modifier, "Shift"]) - expect(page.get_by_role("heading", name="Layer properties")).to_be_visible() + expect(page.get_by_text("Layer properties")).to_be_visible() def test_marker_style_should_have_precedence(live_server, openmap, page, bootstrap): @@ -59,7 +59,7 @@ def test_marker_style_should_have_precedence(live_server, openmap, page, bootstr # Change colour at layer level page.get_by_role("link", name="Manage layers").click() page.locator(".panel").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator(".umap-field-color .define").click() expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1) page.get_by_title("DarkRed").first.click() @@ -67,7 +67,7 @@ def test_marker_style_should_have_precedence(live_server, openmap, page, bootstr # Now change at polygon level, it should take precedence page.locator("path").click(modifiers=["Shift"]) - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator("#umap-feature-shape-properties").get_by_text("define").first.click() page.get_by_title("GoldenRod", exact=True).first.click() expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count( @@ -77,7 +77,7 @@ def test_marker_style_should_have_precedence(live_server, openmap, page, bootstr # Now change again at layer level again, it should not change the marker color page.get_by_role("link", name="Manage layers").click() page.locator(".panel").get_by_title("Edit", exact=True).click() - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator(".umap-field-color input").click() page.get_by_title("DarkViolet").first.click() expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count( @@ -99,7 +99,7 @@ def test_can_remove_stroke(live_server, openmap, page, bootstrap): ) page.locator("path").click() page.get_by_role("link", name="Toggle edit mode").click() - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator(".umap-field-stroke .define").first.click() page.locator(".umap-field-stroke label").first.click() expect(page.locator(".leaflet-overlay-pane path[stroke='DarkBlue']")).to_have_count( @@ -111,7 +111,7 @@ def test_can_remove_stroke(live_server, openmap, page, bootstrap): def test_should_reset_style_on_cancel(live_server, openmap, page, bootstrap): page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.locator("path").click(modifiers=["Shift"]) - page.get_by_role("heading", name="Shape properties").click() + page.get_by_text("Shape properties").click() page.locator("#umap-feature-shape-properties").get_by_text("define").first.click() page.get_by_title("GoldenRod", exact=True).first.click() expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count( From 8ef8ad538ed1f1558404e2fdfc0309168ed70363 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 15:43:27 +0200 Subject: [PATCH 11/21] wip: one const per line --- umap/static/umap/js/umap.features.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 0daaf553..4c8c1cb2 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -495,8 +495,8 @@ U.FeatureMixin = { }, isFiltered: function () { - const filterKeys = this.map.getFilterKeys(), - filter = this.map.browser.options.filter + const filterKeys = this.map.getFilterKeys() + const filter = this.map.browser.options.filter if (filter && !this.matchFilter(filter, filterKeys)) return true if (!this.matchFacets()) return true return false From fa1752c992241f0709602b613441c5cd1a336cbc Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 16:39:28 +0200 Subject: [PATCH 12/21] fix: keep current selected date/number value when reopening browser --- umap/static/umap/js/umap.features.js | 2 -- umap/static/umap/js/umap.forms.js | 39 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 4c8c1cb2..7c81921b 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -521,8 +521,6 @@ U.FeatureMixin = { case 'date': case 'datetime': case 'number': - min = parser(min) - max = parser(max) if (!isNaN(min) && !isNaN(value) && min > value) return false if (!isNaN(max) && !isNaN(value) && max < value) return false break diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 85bda31d..5fec5b65 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -796,7 +796,7 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ return [L._('Min'), L._('Max')] }, - castValue: function (value) { + prepareForHTML: function (value) { return value.valueOf() }, @@ -804,6 +804,10 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) this.container.appendChild(this.label) const { min, max, type } = this.options.criteria + const { min: modifiedMin, max: modifiedMax } = this.get() + + const currentMin = modifiedMin !== undefined ? modifiedMin : min + const currentMax = modifiedMax !== undefined ? modifiedMax : max this.type = type this.inputType = this.getInputType(this.type) @@ -816,7 +820,7 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ this.minInput.type = this.inputType this.minInput.step = 'any' if (min != null) { - this.minInput.valueAsNumber = this.castValue(min) + this.minInput.valueAsNumber = this.prepareForHTML(currentMin) this.minInput.dataset.value = min } @@ -827,7 +831,7 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ this.maxInput.type = this.inputType this.maxInput.step = 'any' if (max != null) { - this.maxInput.valueAsNumber = this.castValue(max) + this.maxInput.valueAsNumber = this.prepareForHTML(currentMax) this.maxInput.dataset.value = max } @@ -842,21 +846,27 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ }) }, + isMinModified: function () { + return this.minInput.value !== this.minInput.dataset.value + }, + + isMaxModified: function () { + return this.maxInput.value !== this.maxInput.dataset.value + }, + + isModified: function () { + return this.isMinModified() || this.isMaxModified() + }, + toJS: function () { const opts = { type: this.type, } - if ( - this.minInput.value !== '' && - this.minInput.value !== this.minInput.dataset.value - ) { - opts.min = this.minInput.value + if (this.minInput.value !== '' && this.isMinModified()) { + opts.min = new Date(this.minInput.value) } - if ( - this.maxInput.value !== '' && - this.maxInput.value !== this.maxInput.dataset.value - ) { - opts.max = this.maxInput.value + if (this.maxInput.value !== '' && this.isMaxModified()) { + opts.max = new Date(this.maxInput.value) } return opts }, @@ -865,7 +875,8 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({}) L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({ - castValue: function (value) { + prepareForHTML: function (value) { + // Deal with timezone return value.valueOf() - value.getTimezoneOffset() * 60000 }, getLabels: function () { From 63d936a069d910fa622e9ba24e70f6168e8bc40f Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 20:26:13 +0200 Subject: [PATCH 13/21] wip: review form style with Aurelie --- umap/static/umap/base.css | 66 +++++++++++++++++++++++++++++++++------ umap/static/umap/vars.css | 5 ++- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 6abd0c78..f7c9969a 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -159,6 +159,51 @@ input[type="range"] { input[type="checkbox"] { margin: 0 5px; vertical-align: middle; + appearance: none; +} +input[type="checkbox"]:after { + display: inline-block; + content: ' '; + width: 12px; + height: 12px; + border: 1px solid var(--color-lightGray); + cursor: pointer; + text-align: center; + font-size: 1.3rem; + line-height: 1rem; +} +input[type=checkbox]:checked:after { + background-color: var(--color-lightCyan); + content: '✓'; +} +label input[type="radio"] { + appearance: none; + margin-right: 10px; +} +input[type="radio"]:after { + display: inline-block; + content: ' '; + width: 12px; + height: 12px; + border-radius: 50%; + border: 1px solid var(--color-lightGray); + cursor: pointer; + text-align: center; + font-size: 1.3rem; + line-height: 1rem; + vertical-align: bottom; +} +label input[type="radio"]:checked:after { + background-color: var(--color-lightCyan); + content: '•'; + font-size: 3rem; + line-height: 1.1rem; + color: var(--color-darkGray); +} + +input[data-modified=true] { + background-color: var(--color-lightCyan); + border: 1px solid var(--color-darkGray); } textarea { height: inherit; @@ -263,9 +308,6 @@ input[type="checkbox"] + label { display: inline; padding: 0 14px; } -label input[type="radio"] { - margin-right: 10px; -} select + .error, input + .error { display: block; @@ -328,20 +370,19 @@ fieldset legend { } [data-badge]:after { position: absolute; - right: -8px; - top: -8px; + right: -6px; + top: -6px; min-width: 8px; min-height: 8px; line-height: 8px; padding: 2px; font-weight: bold; - background-color: var(--color-flashyGreen); + background-color: var(--color-accent); color: var(--color-darkBlue); text-align: center; font-size: .75rem; border-radius: 50%; content: attr(data-badge); - border: solid .5px var(--color-darkBlue); } /* Switch */ @@ -401,11 +442,16 @@ input.switch:checked:empty ~ label:after { } .dark input.switch:checked ~ label:before, input.switch:checked ~ label:before { - background-color: #215d9c; + background-color: var(--color-lightCyan); + border: 1px solid var(--color-lightGray); + color: var(--color-darkGray); content: "ON"; text-indent: 0.7em; text-align: left; font-weight: bold; +.dark input.switch:checked ~ label:before { + border: none; +} } input.switch:checked ~ label:after { margin-left: 3em; @@ -451,9 +497,9 @@ input.switch:checked ~ label:after { background-color: #2c3233; } .umap-multiplechoice input[type='radio']:checked + label { - background-color: #215d9c; + background-color: var(--color-lightCyan); box-shadow: inset 0 0 6px 0px #2c3233; - color: #ededed; + color: var(--color-darkGray); } .inheritable .header, .inheritable { diff --git a/umap/static/umap/vars.css b/umap/static/umap/vars.css index 658ca890..16d5a7f7 100644 --- a/umap/static/umap/vars.css +++ b/umap/static/umap/vars.css @@ -5,9 +5,12 @@ --color-lightGray: #ddd; --color-darkGray: #323737; --color-light: white; - --color-flashyGreen: #b9f5d2; + --color-limeGreen: #b9f5d2; + --color-brightCyan: #46ece6; + --color-lightCyan: #d4fbf9; --background-color: var(--color-light); + --color-accent: var(--color-brightCyan); /* Buttons. */ --button-primary-background: var(--color-waterMint); From 46088f32133e837de543a79123d0c2fd1100d22e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 7 May 2024 20:28:44 +0200 Subject: [PATCH 14/21] wip: highlight modified inputs instead of fieldset in filters --- umap/static/umap/js/umap.forms.js | 60 +++++++++++++++++++------------ 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 5fec5b65..deecd6dc 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -744,7 +744,17 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ }, }) -L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ +L.FormBuilder.FacetSearchBase = L.FormBuilder.Element.extend({ + + buildLabel: function () { + this.label = L.DomUtil.element({ + tagName: 'legend', + textContent: this.options.label, + }) + } + +}) +L.FormBuilder.FacetSearchChoices = L.FormBuilder.FacetSearchBase.extend({ build: function () { this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) this.container.appendChild(this.label) @@ -756,13 +766,6 @@ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ choices.forEach((value) => this.buildLi(value)) }, - buildLabel: function () { - this.label = L.DomUtil.element({ - tagName: 'legend', - textContent: this.options.label, - }) - }, - buildLi: function (value) { const property_li = L.DomUtil.create('li', '', this.ul) const label = L.DomUtil.create('label', '', property_li) @@ -787,7 +790,7 @@ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ }, }) -L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ +L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ getInputType: function (type) { return type }, @@ -823,6 +826,7 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ this.minInput.valueAsNumber = this.prepareForHTML(currentMin) this.minInput.dataset.value = min } + this.minInput.dataset.modified = this.isMinModified() this.maxLabel = L.DomUtil.create('label', '', this.container) this.maxLabel.textContent = maxLabel @@ -834,28 +838,30 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ this.maxInput.valueAsNumber = this.prepareForHTML(currentMax) this.maxInput.dataset.value = max } + this.maxInput.dataset.modified = this.isMaxModified() L.DomEvent.on(this.minInput, 'change', (e) => this.sync()) L.DomEvent.on(this.maxInput, 'change', (e) => this.sync()) }, - buildLabel: function () { - this.label = L.DomUtil.element({ - tagName: 'legend', - textContent: this.options.label, - }) + sync: function () { + L.FormBuilder.Element.prototype.sync.call(this) + this.minInput.dataset.modified = this.isMinModified() + this.maxInput.dataset.modified = this.isMaxModified() }, isMinModified: function () { - return this.minInput.value !== this.minInput.dataset.value + return ( + this.prepareForHTML(this.prepareForJS(this.minInput.value)) !== + this.prepareForHTML(this.prepareForJS(this.minInput.dataset.value)) + ) }, isMaxModified: function () { - return this.maxInput.value !== this.maxInput.dataset.value - }, - - isModified: function () { - return this.isMinModified() || this.isMaxModified() + return ( + this.prepareForHTML(this.prepareForJS(this.maxInput.value)) !== + this.prepareForHTML(this.prepareForJS(this.maxInput.dataset.value)) + ) }, toJS: function () { @@ -863,18 +869,26 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ type: this.type, } if (this.minInput.value !== '' && this.isMinModified()) { - opts.min = new Date(this.minInput.value) + opts.min = this.prepareForJS(this.minInput.value) } if (this.maxInput.value !== '' && this.isMaxModified()) { - opts.max = new Date(this.maxInput.value) + opts.max = this.prepareForJS(this.maxInput.value) } return opts }, }) -L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({}) +L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({ + prepareForJS: function (value) { + return new Number(value) + }, +}) L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({ + prepareForJS: function (value) { + return new Date(value) + }, + prepareForHTML: function (value) { // Deal with timezone return value.valueOf() - value.getTimezoneOffset() * 60000 From 76c719f3b1604dcb9a1bede35ce53312846369b8 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 May 2024 18:40:00 +0200 Subject: [PATCH 15/21] wip: add a reset button to filters form in browser --- umap/static/umap/css/icon.css | 3 ++ umap/static/umap/js/modules/browser.js | 27 +++++++++++++---- umap/static/umap/js/umap.forms.js | 40 +++++++++++++++++++------- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/umap/static/umap/css/icon.css b/umap/static/umap/css/icon.css index 69778b26..59a73aaa 100644 --- a/umap/static/umap/css/icon.css +++ b/umap/static/umap/css/icon.css @@ -79,6 +79,9 @@ .icon-resize { background-position: -74px -144px; } +.icon-restore { + background-position: -121px -74px; +} .expanded .icon-resize { background-position: -50px -144px; } diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 22811c86..db281a19 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -109,13 +109,17 @@ export default class Browser { counter.title = translate(`Features in this layer: ${count}`) } + toggleBadge() { + U.Utils.toggleBadge(this.filtersTitle, this.hasFilters()) + U.Utils.toggleBadge('.umap-control-browse', this.hasFilters()) + } + onFormChange() { this.map.eachBrowsableDataLayer((datalayer) => { datalayer.resetLayer(true) this.updateDatalayer(datalayer) }) - U.Utils.toggleBadge(this.filtersTitle, this.hasFilters()) - U.Utils.toggleBadge('.umap-control-browse', this.hasFilters()) + this.toggleBadge() } redraw() { @@ -164,7 +168,7 @@ export default class Browser { icon: 'icon-filters', }) this.filtersTitle = container.querySelector('summary') - U.Utils.toggleBadge(this.filtersTitle, this.hasFilters()) + this.toggleBadge() this.dataContainer = DomUtil.create('div', '', container) let fields = [ @@ -177,14 +181,27 @@ export default class Browser { const builder = new L.FormBuilder(this, fields, { callback: () => this.onFormChange(), }) + let filtersBuilder formContainer.appendChild(builder.build()) + DomEvent.on(builder.form, 'reset', () => { + window.setTimeout(builder.syncAll.bind(builder)) + }) if (this.map.options.facetKey) { fields = this.map.facets.build() - const builder = new L.FormBuilder(this.map.facets, fields, { + filtersBuilder = new L.FormBuilder(this.map.facets, fields, { callback: () => this.onFormChange(), }) - formContainer.appendChild(builder.build()) + DomEvent.on(filtersBuilder.form, 'reset', () => { + window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder)) + }) + formContainer.appendChild(filtersBuilder.build()) } + const reset = DomUtil.createButton('flat', formContainer, '', () => { + builder.form.reset() + if (filtersBuilder) filtersBuilder.form.reset() + }) + DomUtil.createIcon(reset, 'icon-restore') + DomUtil.element({ tagName: 'span', parent: reset, textContent: translate('Reset all') }) this.map.panel.open({ content: container, diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index deecd6dc..3bb0e5df 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -823,10 +823,12 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ this.minInput.type = this.inputType this.minInput.step = 'any' if (min != null) { - this.minInput.valueAsNumber = this.prepareForHTML(currentMin) this.minInput.dataset.value = min + // Use setAttribute so to restore to this value when resetting + // form. + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset + this.minInput.setAttribute('value', this.prepareForHTML(min)) } - this.minInput.dataset.modified = this.isMinModified() this.maxLabel = L.DomUtil.create('label', '', this.container) this.maxLabel.textContent = maxLabel @@ -835,19 +837,23 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ this.maxInput.type = this.inputType this.maxInput.step = 'any' if (max != null) { - this.maxInput.valueAsNumber = this.prepareForHTML(currentMax) this.maxInput.dataset.value = max + this.maxInput.setAttribute('value', this.prepareForHTML(max)) } - this.maxInput.dataset.modified = this.isMaxModified() + this.toggleStatus() - L.DomEvent.on(this.minInput, 'change', (e) => this.sync()) - L.DomEvent.on(this.maxInput, 'change', (e) => this.sync()) + L.DomEvent.on(this.minInput, 'change', () => this.sync()) + L.DomEvent.on(this.maxInput, 'change', () => this.sync()) + }, + + toggleStatus: function () { + this.minInput.dataset.modified = this.isMinModified() + this.maxInput.dataset.modified = this.isMaxModified() }, sync: function () { L.FormBuilder.Element.prototype.sync.call(this) - this.minInput.dataset.modified = this.isMinModified() - this.maxInput.dataset.modified = this.isMaxModified() + this.toggleStatus() }, isMinModified: function () { @@ -889,10 +895,16 @@ L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({ return new Date(value) }, - prepareForHTML: function (value) { - // Deal with timezone - return value.valueOf() - value.getTimezoneOffset() * 60000 + toLocaleDateTime: function (dt) { + return new Date(dt.valueOf() - dt.getTimezoneOffset() * 60000) }, + + prepareForHTML: function (value) { + // Value must be in local time + if (isNaN(value)) return + return this.toLocaleDateTime(value).toISOString().substr(0, 10) + }, + getLabels: function () { return [L._('From'), L._('Until')] }, @@ -902,6 +914,12 @@ L.FormBuilder.FacetSearchDateTime = L.FormBuilder.FacetSearchDate.extend({ getInputType: function (type) { return 'datetime-local' }, + + prepareForHTML: function (value) { + // Value must be in local time + if (isNaN(value)) return + return this.toLocaleDateTime(value).toISOString().slice(0, -1) + }, }) L.FormBuilder.MultiChoice = L.FormBuilder.Element.extend({ From 0741d2a943c34474fc012735a571ed29d2f04b59 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 May 2024 18:40:40 +0200 Subject: [PATCH 16/21] wip: add min/max attributes to minmax filters inputs --- umap/static/umap/js/umap.forms.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 3bb0e5df..1e27898f 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -822,6 +822,8 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ this.minInput = L.DomUtil.create('input', '', this.minLabel) this.minInput.type = this.inputType this.minInput.step = 'any' + this.minInput.min = this.prepareForHTML(min) + this.minInput.max = this.prepareForHTML(max) if (min != null) { this.minInput.dataset.value = min // Use setAttribute so to restore to this value when resetting @@ -836,6 +838,8 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ this.maxInput = L.DomUtil.create('input', '', this.maxLabel) this.maxInput.type = this.inputType this.maxInput.step = 'any' + this.maxInput.min = this.prepareForHTML(min) + this.maxInput.max = this.prepareForHTML(max) if (max != null) { this.maxInput.dataset.value = max this.maxInput.setAttribute('value', this.prepareForHTML(max)) From 99effc880b49240351215958e5bd536037232b31 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 May 2024 18:46:47 +0200 Subject: [PATCH 17/21] wip: rename facet by filters in user facing labels --- umap/static/umap/js/umap.core.js | 4 ++-- umap/static/umap/js/umap.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 11d1dc53..2180b05c 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -556,9 +556,9 @@ U.Help = L.Class.extend({ 'Comma separated list of properties to use for sorting features. To reverse the sort, put a minus sign (-) before. Eg. mykey,-otherkey.' ), 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'), + filterKey: L._('Comma separated list of properties to use when filtering features by text input'), 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 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.' + 'Comma separated list of properties to use for filters (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.js b/umap/static/umap/js/umap.js index e7e02465..bcb6b2e6 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1222,7 +1222,7 @@ U.Map = L.Map.extend({ handler: 'Input', helpEntries: 'filterKey', placeholder: L._('Default: name'), - label: L._('Filter keys'), + label: L._('Search keys'), inheritable: true, }, ], @@ -1232,7 +1232,7 @@ U.Map = L.Map.extend({ handler: 'BlurInput', helpEntries: 'facetKey', placeholder: L._('Example: key1,key2|Label 2,key3|Label 3|checkbox'), - label: L._('Facet keys'), + label: L._('Filters keys'), }, ], [ @@ -1601,7 +1601,7 @@ U.Map = L.Map.extend({ L.DomUtil.createButton( 'umap-open-filter-link flat', container, - L._('Select data'), + L._('Filter data'), () => this.openBrowser('filters') ) } From 53458053a753b7c2e9e98cbddb0041cde52ddc21 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 May 2024 19:05:52 +0200 Subject: [PATCH 18/21] wip: properly deal with old onLoadPanel 'facet' value We need to replace it, so the new value is saved and used elsewhere (eg. in the "UI options" form) --- umap/static/umap/js/umap.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index bcb6b2e6..b37767a5 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -149,6 +149,9 @@ U.Map = L.Map.extend({ if (this.options.datalayersControl === 'expanded') { this.options.onLoadPanel = 'datalayers' } + if (this.options.onLoadPanel === 'facet') { + this.options.onLoadPanel = 'datafilters' + } let isDirty = false // self status try { @@ -214,12 +217,12 @@ U.Map = L.Map.extend({ this.openBrowser('data') } else if (this.options.onLoadPanel === 'datalayers') { this.openBrowser('layers') + } else if (this.options.onLoadPanel === 'datafilters') { + this.panel.mode = 'expanded' + this.openBrowser('filters') } else if (this.options.onLoadPanel === 'caption') { this.panel.mode = 'condensed' this.displayCaption() - } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { - this.panel.mode = 'expanded' - this.openBrowser('filters') } if (L.Util.queryString('edit')) { if (this.hasEditMode()) this.enableEdit() From 86ae6bb816ee3917123cdbaa28e6378724d68c06 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 May 2024 19:27:23 +0200 Subject: [PATCH 19/21] wip: better way of computing isMin/MaxModified in facets fields --- umap/static/umap/js/umap.forms.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 1e27898f..9f852e48 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -825,11 +825,14 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ this.minInput.min = this.prepareForHTML(min) this.minInput.max = this.prepareForHTML(max) if (min != null) { - this.minInput.dataset.value = min - // Use setAttribute so to restore to this value when resetting - // form. + // The value stored using setAttribute is not modified by + // user input, and will be used as initial value when calling + // form.reset(), and can also be retrieve later on by using + // getAttributing, to compare with current value and know + // if this value has been modified by the user // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset this.minInput.setAttribute('value', this.prepareForHTML(min)) + this.minInput.value = this.prepareForHTML(currentMin) } this.maxLabel = L.DomUtil.create('label', '', this.container) @@ -841,8 +844,9 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ this.maxInput.min = this.prepareForHTML(min) this.maxInput.max = this.prepareForHTML(max) if (max != null) { - this.maxInput.dataset.value = max + // Cf comment above about setAttribute vs value this.maxInput.setAttribute('value', this.prepareForHTML(max)) + this.maxInput.value = this.prepareForHTML(currentMax) } this.toggleStatus() @@ -861,17 +865,15 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({ }, isMinModified: function () { - return ( - this.prepareForHTML(this.prepareForJS(this.minInput.value)) !== - this.prepareForHTML(this.prepareForJS(this.minInput.dataset.value)) - ) + const default_ = this.minInput.getAttribute("value") + const current = this.minInput.value + return current != default_ }, isMaxModified: function () { - return ( - this.prepareForHTML(this.prepareForJS(this.maxInput.value)) !== - this.prepareForHTML(this.prepareForJS(this.maxInput.dataset.value)) - ) + const default_ = this.maxInput.getAttribute("value") + const current = this.maxInput.value + return current != default_ }, toJS: function () { From b0b740f9b530682fb2ea0a4327837218bb2c13bb Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 May 2024 19:37:28 +0200 Subject: [PATCH 20/21] wip: fix CSS selector to determine if browser is open or not --- umap/static/umap/js/modules/browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index db281a19..3c147a37 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -127,7 +127,7 @@ export default class Browser { } isOpen() { - return !!document.querySelector('.umap-browser') + return !!document.querySelector('.on .umap-browser') } hasFilters() { From 6c5bdf8670e6f6e8170109f7fb76212f0bb21591 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 9 May 2024 15:57:14 +0200 Subject: [PATCH 21/21] wip: add integration test to make sure filters are kept when closing panel --- umap/tests/integration/test_facets_browser.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/umap/tests/integration/test_facets_browser.py b/umap/tests/integration/test_facets_browser.py index 371152b8..0ba5a621 100644 --- a/umap/tests/integration/test_facets_browser.py +++ b/umap/tests/integration/test_facets_browser.py @@ -216,3 +216,61 @@ def test_number_with_zero_value(live_server, page, map): page.keyboard.press("Tab") # Move out of the input, so the "change" event is sent markers = page.locator(".leaflet-marker-icon") expect(markers).to_have_count(3) + + +def test_facets_search_are_persistent_when_closing_panel(live_server, page, map): + map.settings["properties"]["onLoadPanel"] = "datafilters" + map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number" + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA1) + DataLayerFactory(map=map, data=DATALAYER_DATA2) + page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") + panel = page.locator(".umap-browser") + + # Facet values + odd = page.get_by_label("odd") + markers = page.locator(".leaflet-marker-icon") + expect(markers).to_have_count(4) + + # Datalist in the browser + expect(panel.get_by_text("Point 1")).to_be_visible() + expect(panel.get_by_text("Point 2")).to_be_visible() + expect(panel.get_by_text("Point 3")).to_be_visible() + expect(panel.get_by_text("Point 4")).to_be_visible() + + # Now let's filter + odd.click() + expect(page.locator("summary")).to_have_attribute("data-badge", " ") + expect(page.locator(".umap-control-browse")).to_have_attribute("data-badge", " ") + expect(markers).to_have_count(2) + expect(panel.get_by_text("Point 2")).to_be_hidden() + expect(panel.get_by_text("Point 4")).to_be_hidden() + expect(panel.get_by_text("Point 1")).to_be_visible() + expect(panel.get_by_text("Point 3")).to_be_visible() + + # Let's filter using the number facet + expect(panel.get_by_label("Min")).to_have_value("10") + expect(panel.get_by_label("Max")).to_have_value("14") + page.get_by_label("Min").fill("13") + page.keyboard.press("Tab") # Move out of the input, so the "change" event is sent + expect(panel.get_by_label("Min")).to_have_attribute("data-modified", "true") + expect(markers).to_have_count(1) + expect(panel.get_by_text("Point 2")).to_be_hidden() + expect(panel.get_by_text("Point 4")).to_be_hidden() + expect(panel.get_by_text("Point 1")).to_be_hidden() + expect(panel.get_by_text("Point 3")).to_be_visible() + + # Close panel + expect(panel.locator("summary")).to_have_attribute("data-badge", " ") + expect(page.locator(".umap-control-browse")).to_have_attribute("data-badge", " ") + page.get_by_role("listitem", name="Close").click() + page.get_by_role("button", name="See layers").click() + expect(panel.get_by_label("Min")).to_have_value("13") + expect(panel.get_by_label("Min")).to_have_attribute("data-modified", "true") + expect(panel.get_by_label("odd")).to_be_checked() + + # Datalist in the browser should be inchanged + expect(panel.get_by_text("Point 2")).to_be_hidden() + expect(panel.get_by_text("Point 4")).to_be_hidden() + expect(panel.get_by_text("Point 1")).to_be_hidden() + expect(panel.get_by_text("Point 3")).to_be_visible()