diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index b7f11b0e..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; @@ -290,67 +332,59 @@ 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; padding: 0 5px; } +[data-badge] { + position: relative; +} +[data-badge]:after { + position: absolute; + right: -6px; + top: -6px; + min-width: 8px; + min-height: 8px; + line-height: 8px; + padding: 2px; + font-weight: bold; + background-color: var(--color-accent); + color: var(--color-darkBlue); + text-align: center; + font-size: .75rem; + border-radius: 50%; + content: attr(data-badge); +} + /* Switch */ input.switch:empty { display: none; @@ -408,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; @@ -458,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/css/icon.css b/umap/static/umap/css/icon.css index 2bf10a74..59a73aaa 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; } @@ -76,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/css/panel.css b/umap/static/umap/css/panel.css index 9759de54..80f168b7 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/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 f68dc457..3c147a37 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -9,11 +9,22 @@ export default class Browser { filter: '', inBbox: false, } + this._mode = 'layers' + } + + set mode(value) { + // 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( @@ -98,15 +109,29 @@ 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) }) + this.toggleBadge() + } + + redraw() { + if (this.isOpen()) this.open() } isOpen() { - return !!document.querySelector('.umap-browser') + return !!document.querySelector('.on .umap-browser') + } + + hasFilters() { + return !!this.options.filter || this.map.facets.isActive() } onMoveEnd() { @@ -126,7 +151,9 @@ export default class Browser { }) } - open() { + open(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') @@ -135,18 +162,46 @@ export default class Browser { DomEvent.disableClickPropagation(container) 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', + className: 'filters', + icon: 'icon-filters', + }) + this.filtersTitle = container.querySelector('summary') + this.toggleBadge() 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(), }) + 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() + filtersBuilder = new L.FormBuilder(this.map.facets, fields, { + callback: () => this.onFormChange(), + }) + 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, @@ -166,18 +221,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) - 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}`) - L.DomUtil.addClass(el, 'on') - } } diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index 00900b7b..461a8ec3 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) => { @@ -53,42 +56,20 @@ export default class Facets { return properties } - redraw() { - if (this.isOpen()) this.open() + isActive() { + for (let { type, min, max, choices } of Object.values(this.selected)) { + if (min !== undefined || max != undefined || choices?.length) { + return true + } + } + return false } - 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') + build() { const defined = this.getDefined() 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' @@ -114,13 +95,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 504708b5..3db6f726 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/modules/utils.js b/umap/static/umap/js/modules/utils.js index f5c36a31..2f0cb57a 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 a104cfc9..770bcda5 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -499,8 +499,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({ @@ -510,6 +513,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() }, @@ -663,15 +670,11 @@ const ControlsMixin = { 'star', 'tilelayers', ], - _openFacet: function () { - this.facets.open() - }, displayCaption: function () { 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', diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 156d15c7..2180b05c 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -81,18 +81,18 @@ 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.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 + if (options.callback) { + L.DomEvent.on(details, 'toggle', () => { + if (details.open) options.callback.call(options.context || this) + }) + } + return fieldset } L.DomUtil.createButton = (className, container, content, callback, context) => { @@ -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.features.js b/umap/static/umap/js/umap.features.js index 5d4b0d86..7c81921b 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() + const 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++) { @@ -513,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 9873b7e4..3297ff7f 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 }, @@ -796,7 +799,7 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ return [L._('Min'), L._('Max')] }, - castValue: function (value) { + prepareForHTML: function (value) { return value.valueOf() }, @@ -804,6 +807,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) @@ -815,9 +822,17 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.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.valueAsNumber = this.castValue(min) - this.minInput.dataset.value = min + // 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) @@ -826,37 +841,76 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.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.valueAsNumber = this.castValue(max) - 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() - 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()) }, - buildLabel: function () { - this.label = L.DomUtil.element({ - tagName: 'legend', - textContent: this.options.label, - }) + toggleStatus: function () { + this.minInput.dataset.modified = this.isMinModified() + this.maxInput.dataset.modified = this.isMaxModified() + }, + + sync: function () { + L.FormBuilder.Element.prototype.sync.call(this) + this.toggleStatus() + }, + + isMinModified: function () { + const default_ = this.minInput.getAttribute("value") + const current = this.minInput.value + return current != default_ + }, + + isMaxModified: function () { + const default_ = this.maxInput.getAttribute("value") + const current = this.maxInput.value + return current != default_ }, toJS: function () { - return { + const opts = { type: this.type, - min: this.minInput.value, - max: this.maxInput.value, } + if (this.minInput.value !== '' && this.isMinModified()) { + opts.min = this.prepareForJS(this.minInput.value) + } + if (this.maxInput.value !== '' && this.isMaxModified()) { + 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({ - castValue: function (value) { - return value.valueOf() - value.getTimezoneOffset() * 60000 + prepareForJS: function (value) { + return new Date(value) }, + + 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')] }, @@ -866,6 +920,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({ diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index f11054bb..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 { @@ -211,15 +214,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 === '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.openFacet() } if (L.Util.queryString('edit')) { if (this.hasEditMode()) this.enableEdit() @@ -252,7 +255,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 +911,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) }) }, @@ -1229,7 +1225,7 @@ U.Map = L.Map.extend({ handler: 'Input', helpEntries: 'filterKey', placeholder: L._('Default: name'), - label: L._('Filter keys'), + label: L._('Search keys'), inheritable: true, }, ], @@ -1239,7 +1235,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'), }, ], [ @@ -1602,15 +1598,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 + L._('Filter data'), + () => this.openBrowser('filters') ) } } @@ -1747,17 +1742,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 fa739e35..8d4ca998 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..8d872b92 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; } @@ -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..16d5a7f7 100644 --- a/umap/static/umap/vars.css +++ b/umap/static/umap/vars.css @@ -4,6 +4,13 @@ --color-darkBlue: #263B58; --color-lightGray: #ddd; --color-darkGray: #323737; + --color-light: white; + --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); @@ -20,3 +27,6 @@ --footer-height: 46px; --control-size: 36px; } +.dark { + --background-color: var(--color-darkGray); +} diff --git a/umap/tests/integration/test_browser.py b/umap/tests/integration/test_browser.py index 11a31460..90e3be9c 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.locator(".filters summary").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.locator(".filters summary").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.locator(".filters summary").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.locator(".filters summary").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.locator(".filters summary").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.locator(".filters summary").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.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( diff --git a/umap/tests/integration/test_facets_browser.py b/umap/tests/integration/test_facets_browser.py index f72d3124..0ba5a621 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) @@ -205,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()