From 1bf15436686009b0e825cc10b375ea7f42a47462 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 8 Nov 2023 18:00:12 +0100 Subject: [PATCH 01/14] Refactor icon selector: use tabs, make options more explicit --- umap/static/umap/base.css | 27 +++++-- umap/static/umap/js/umap.forms.js | 112 +++++++++++++++++++++--------- 2 files changed, 103 insertions(+), 36 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 79a25de0..827fa350 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -194,6 +194,7 @@ input[type="submit"] { .dark a { color: #eeeeec; } +button.flat, [type="button"].flat, .dark [type="button"].flat { border: none; @@ -535,9 +536,27 @@ i.info { margin-top: -8px; padding: 0 5px; } -.umap-pictogram-grid { +.pictogram-tabs { display: flex; - flex-wrap: wrap; + justify-content: space-around; + font-size: 1.2em; + padding-bottom: 20px; +} +.pictogram-tabs button { + padding: 10px; + color: #fff; + text-decoration: none; + cursor: pointer; +} +.pictogram-tabs .on { + font-weight: bold; + border-bottom: 1px solid #fff; +} +.umap-pictogram-grid { + display: grid; + grid-template-columns: repeat(auto-fill, 30px); + justify-content: space-between; + grid-gap: 5px; } .umap-pictogram-choice { width: 30px; @@ -547,7 +566,6 @@ i.info { background-color: #999; text-align: center; margin-bottom: 5px; - margin-right: 5px; } .umap-pictogram-choice img { vertical-align: middle; @@ -556,7 +574,8 @@ i.info { .umap-pictogram-choice:hover, .umap-pictogram-choice.selected, .umap-color-picker span:hover { - box-shadow: 0 0 4px 0 black; + box-shadow: 0 0 4px 0 rgb(66, 236, 230); + background-color: #aaa; } .umap-pictogram-choice .leaflet-marker-icon { bottom: 0; diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 20cd99b3..53510ca4 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -1,4 +1,10 @@ L.FormBuilder.Element.include({ + undefine: function () { + L.DomUtil.addClass(this.wrapper, 'undefined') + this.clear() + this.sync() + }, + getParentNode: function () { if (this.options.wrapper) { return L.DomUtil.create( @@ -29,15 +35,10 @@ L.FormBuilder.Element.include({ }, this ) - L.DomEvent.on( + L.DomEvent.on(undefine, 'click', L.DomEvent.stop).on( undefine, 'click', - function (e) { - L.DomEvent.stop(e) - L.DomUtil.addClass(this.wrapper, 'undefined') - this.clear() - this.sync() - }, + this.undefine, this ) } @@ -524,16 +525,50 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ build: function () { L.FormBuilder.BlurInput.prototype.build.call(this) - // Try to guess if the icon content has been customized, and if yes - // directly display the field - this.input.type = this.value() && !this.value().startsWith('/') ? 'text' : 'hidden' - this.input.placeholder = L._('Symbol or url') this.buttonsContainer = L.DomUtil.create('div', '') this.pictogramsContainer = L.DomUtil.create('div', 'umap-pictogram-list') + this.tabsContainer = L.DomUtil.create('div', 'pictogram-tabs') L.DomUtil.before(this.input, this.buttonsContainer) + L.DomUtil.before(this.input, this.tabsContainer) L.DomUtil.before(this.input, this.pictogramsContainer) this.udpatePreview() - this.on('define', this.fetchIconList) + this.on('define', this.onDefine) + }, + + onDefine: function () { + this.buildTabs() + this.showSymbols() + }, + + buildTabs: function () { + const symbol = L.DomUtil.add( + 'button', + 'flat on', + this.tabsContainer, + L._('Symbol') + ), + char = L.DomUtil.add( + 'button', + 'flat', + this.tabsContainer, + L._('Emoji & Character') + ) + url = L.DomUtil.add('button', 'flat', this.tabsContainer, L._('URL')) + toggle = (e) => { + L.DomUtil.removeClass(symbol, 'on') + L.DomUtil.removeClass(char, 'on') + L.DomUtil.removeClass(url, 'on') + L.DomUtil.addClass(e.target, 'on') + } + L.DomEvent.on(symbol, 'click', L.DomEvent.stop) + .on(symbol, 'click', this.showSymbols, this) + .on(symbol, 'click', toggle) + L.DomEvent.on(char, 'click', L.DomEvent.stop) + .on(char, 'click', this.showChars, this) + .on(char, 'click', toggle) + L.DomEvent.on(url, 'click', L.DomEvent.stop) + .on(url, 'click', this.showURL, this) + .on(url, 'click', toggle) }, isUrl: function () { @@ -541,6 +576,10 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, udpatePreview: function () { + if (this.isDefault()) { + this.buttonsContainer.innerHTML = '' + return + } if (!L.Util.hasVar(this.value())) { // Do not try to render URL with variables if (this.isUrl()) { @@ -550,7 +589,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) img.src = this.value() - L.DomEvent.on(img, 'click', this.fetchIconList, this) + L.DomEvent.on(img, 'click', this.showSymbols, this) } else { const el = L.DomUtil.create( 'span', @@ -558,14 +597,14 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) el.textContent = this.value() - L.DomEvent.on(el, 'click', this.fetchIconList, this) + L.DomEvent.on(el, 'click', this.showSymbols, this) } } this.button = L.DomUtil.createButton( 'button action-button', this.buttonsContainer, this.value() ? L._('Change') : L._('Add'), - this.fetchIconList, + this.showSymbols, this ) }, @@ -623,7 +662,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ } }, - buildIconList: function (data) { + buildSymbolsList: function (data) { this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer) this.searchInput.type = 'search' this.searchInput.placeholder = L._('Search') @@ -647,35 +686,44 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ L._('Close'), function (e) { this.pictogramsContainer.innerHTML = '' - this.udpatePreview() + this.tabsContainer.innerHTML = '' + if (this.isDefault()) this.undefine(e) + else this.udpatePreview() }, this ) - closeButton.style.display = 'block' - closeButton.style.clear = 'both' - - const customButton = L.DomUtil.createButton( - 'flat', - this.pictogramsContainer, - L._('Toggle direct input (advanced)'), - function (e) { - this.input.type = this.input.type === 'text' ? 'hidden' : 'text' - }, - this - ) - this.builder.map.help.button(customButton, 'formatIconSymbol') }, - fetchIconList: function (e) { + isDefault: function () { + return !this.value() || this.value() === this.obj.getMap().options.default_iconUrl + }, + + showChars: function () { + // Do not show default value here, as it's not a character + // and it has not been explicitely chosen by the user. + if (this.isDefault()) this.input.value = '' + this.input.type = 'text' + this.input.placeholder = L._('Type char or paste emoji') + this.pictogramsContainer.innerHTML = '' + }, + + showSymbols: function () { + this.input.type = 'hidden' // Clean parent element before calling ajax, to prevent blinking this.pictogramsContainer.innerHTML = '' this.buttonsContainer.innerHTML = '' this.builder.map.get(this.builder.map.options.urls.pictogram_list_json, { - callback: this.buildIconList, + callback: this.buildSymbolsList, context: this, }) }, + showURL: function () { + this.input.type = 'url' + this.input.placeholder = L._('Add URL') + this.pictogramsContainer.innerHTML = '' + }, + unselectAll: function (container) { const els = container.querySelectorAll('div.selected') for (const el in els) { From e509687956c15b7e4244cc13147d17e4b62d26ab Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 9 Nov 2023 00:02:44 +0100 Subject: [PATCH 02/14] Add first test for pictogram selection --- umap/static/umap/base.css | 4 ++ umap/static/umap/js/umap.forms.js | 11 +++-- umap/tests/fixtures/circle.svg | 4 ++ umap/tests/fixtures/star.svg | 4 ++ umap/tests/integration/test_picto.py | 68 ++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 umap/tests/fixtures/circle.svg create mode 100644 umap/tests/fixtures/star.svg create mode 100644 umap/tests/integration/test_picto.py diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 827fa350..15b3a29b 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -566,6 +566,10 @@ i.info { background-color: #999; text-align: center; margin-bottom: 5px; + display: none; +} +.umap-pictogram-choice.visible { + display: block; } .umap-pictogram-choice img { vertical-align: middle; diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 53510ca4..cb8e7589 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -586,7 +586,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const img = L.DomUtil.create( 'img', '', - L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice visible', this.buttonsContainer) ) img.src = this.value() L.DomEvent.on(img, 'click', this.showSymbols, this) @@ -594,7 +594,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const el = L.DomUtil.create( 'span', '', - L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice visible', this.buttonsContainer) ) el.textContent = this.value() L.DomEvent.on(el, 'click', this.showSymbols, this) @@ -610,7 +610,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, addIconPreview: function (pictogram, parent) { - const baseClass = 'umap-pictogram-choice', + const baseClass = 'umap-pictogram-choice visible', value = pictogram.src, className = value === this.value() ? `${baseClass} selected` : baseClass, container = L.DomUtil.create('div', className, parent), @@ -618,6 +618,8 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ img.src = value if (pictogram.name && pictogram.attribution) { container.title = `${pictogram.name} — © ${pictogram.attribution}` + } else if (pictogram.name) { + container.title = pictogram.name } L.DomEvent.on( container, @@ -644,8 +646,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const icons = [...this.parentNode.querySelectorAll('.umap-pictogram-choice')], search = this.searchInput.value.toLowerCase() icons.forEach((el) => { - if (el.title.toLowerCase().indexOf(search) != -1) el.style.display = 'block' - else el.style.display = 'none' + L.DomUtil.classIf(el, 'visible', el.title.toLowerCase().indexOf(search) !== -1) }) }, diff --git a/umap/tests/fixtures/circle.svg b/umap/tests/fixtures/circle.svg new file mode 100644 index 00000000..9b579a76 --- /dev/null +++ b/umap/tests/fixtures/circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/umap/tests/fixtures/star.svg b/umap/tests/fixtures/star.svg new file mode 100644 index 00000000..2b6f5169 --- /dev/null +++ b/umap/tests/fixtures/star.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py new file mode 100644 index 00000000..6a961161 --- /dev/null +++ b/umap/tests/integration/test_picto.py @@ -0,0 +1,68 @@ +from pathlib import Path + +import pytest +from playwright.sync_api import expect +from django.core.files.base import ContentFile + +from umap.models import Map, Pictogram + +from ..base import DataLayerFactory + +pytestmark = pytest.mark.django_db + + +DATALAYER_DATA = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [13.68896484375, 48.55297816440071], + }, + "properties": {"_umap_options": {"color": "DarkCyan"}, "name": "Here"}, + } + ], + "_umap_options": {"displayOnLoad": True, "name": "FooBarFoo"}, +} +FIXTURES = Path(__file__).parent.parent / "fixtures" + + +@pytest.fixture +def pictos(): + path = FIXTURES / "star.svg" + Pictogram(name="star", pictogram=ContentFile(path.read_text(), path.name)).save() + path = FIXTURES / "circle.svg" + Pictogram(name="circle", pictogram=ContentFile(path.read_text(), path.name)).save() + + +def test_can_change_picto_at_map_level(map, live_server, page, pictos): + # Faster than doing a login + map.edit_status = Map.ANONYMOUS + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA) + page.goto(f"{live_server.url}{map.get_absolute_url()}?edit") + marker = page.locator(".umap-div-icon img") + expect(marker).to_have_count(1) + # Should have default img + expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") + edit_settings = page.get_by_title("Edit map settings") + expect(edit_settings).to_be_visible() + edit_settings.click() + shape_settings = page.get_by_text("Default shape properties") + expect(shape_settings).to_be_visible() + shape_settings.click() + define = page.locator(".umap-field-iconUrl .define") + undefine = page.locator(".umap-field-iconUrl .undefine") + expect(define).to_be_visible() + expect(undefine).to_be_hidden() + define.click() + symbols = page.locator(".umap-pictogram-choice.visible") + expect(symbols).to_have_count(2) + search = page.locator(".umap-pictogram-list input") + search.type("star") + expect(symbols).to_have_count(1) + symbols.click() + expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") + undefine.click() + expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") From d63d81fec39a7d66d8e9a336709f2aea6cdebe25 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 9 Nov 2023 08:31:24 +0100 Subject: [PATCH 03/14] Refactor icon search --- umap/static/umap/js/umap.forms.js | 54 ++++++++++++++-------------- umap/tests/integration/test_picto.py | 2 +- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index cb8e7589..c4c82fee 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -612,22 +612,21 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ addIconPreview: function (pictogram, parent) { const baseClass = 'umap-pictogram-choice visible', value = pictogram.src, - className = value === this.value() ? `${baseClass} selected` : baseClass, + search = this.searchInput.value.toLowerCase(), + title = pictogram.attribution ? `${pictogram.name} — © ${pictogram.attribution}` : pictogram.name + if (search && title.toLowerCase().indexOf(search) === -1) return + const className = value === this.value() ? `${baseClass} selected` : baseClass, container = L.DomUtil.create('div', className, parent), img = L.DomUtil.create('img', '', container) img.src = value - if (pictogram.name && pictogram.attribution) { - container.title = `${pictogram.name} — © ${pictogram.attribution}` - } else if (pictogram.name) { - container.title = pictogram.name - } + container.title = title L.DomEvent.on( container, 'click', function (e) { this.input.value = value this.sync() - this.unselectAll(this.pictogramsContainer) + this.unselectAll(this.gridContainer) L.DomUtil.addClass(container, 'selected') }, this @@ -642,19 +641,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ this.udpatePreview() }, - search: function (e) { - const icons = [...this.parentNode.querySelectorAll('.umap-pictogram-choice')], - search = this.searchInput.value.toLowerCase() - icons.forEach((el) => { - L.DomUtil.classIf(el, 'visible', el.title.toLowerCase().indexOf(search) !== -1) - }) - }, - addCategory: function (category, items) { const parent = L.DomUtil.create( 'div', 'umap-pictogram-category', - this.pictogramsContainer + this.gridContainer ), title = L.DomUtil.add('h6', '', parent, category), grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent) @@ -663,14 +654,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ } }, - buildSymbolsList: function (data) { - this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer) - this.searchInput.type = 'search' - this.searchInput.placeholder = L._('Search') - L.DomEvent.on(this.searchInput, 'input', this.search, this) + buildSymbolsList: function () { + this.gridContainer.innerHTML = '' const categories = {} let category - for (const props of data.pictogram_list) { + for (const props of this.pictogram_list) { category = props.category || L._('Generic') categories[category] = categories[category] || [] categories[category].push(props) @@ -710,13 +698,23 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ showSymbols: function () { this.input.type = 'hidden' - // Clean parent element before calling ajax, to prevent blinking - this.pictogramsContainer.innerHTML = '' this.buttonsContainer.innerHTML = '' - this.builder.map.get(this.builder.map.options.urls.pictogram_list_json, { - callback: this.buildSymbolsList, - context: this, - }) + this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer) + this.searchInput.type = 'search' + this.searchInput.placeholder = L._('Search') + this.gridContainer = L.DomUtil.create('div', '', this.pictogramsContainer) + L.DomEvent.on(this.searchInput, 'input', this.buildSymbolsList, this) + if (this.pictogram_list) { + this.buildSymbolsList() + } else { + this.builder.map.get(this.builder.map.options.urls.pictogram_list_json, { + callback: (data) => { + this.pictogram_list = data.pictogram_list + this.buildSymbolsList() + }, + context: this, + }) + } }, showURL: function () { diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index 6a961161..67e7e497 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -57,7 +57,7 @@ def test_can_change_picto_at_map_level(map, live_server, page, pictos): expect(define).to_be_visible() expect(undefine).to_be_hidden() define.click() - symbols = page.locator(".umap-pictogram-choice.visible") + symbols = page.locator(".umap-pictogram-choice") expect(symbols).to_have_count(2) search = page.locator(".umap-pictogram-list input") search.type("star") From 00a13acb83179ac02ac26129b321743c373ea7aa Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 13 Nov 2023 16:58:54 +0100 Subject: [PATCH 04/14] Add test covering changing picto from the datalayer --- umap/tests/integration/test_picto.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index 67e7e497..96675055 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -66,3 +66,37 @@ def test_can_change_picto_at_map_level(map, live_server, page, pictos): expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") undefine.click() expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") + + +def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos): + # Faster than doing a login + map.edit_status = Map.ANONYMOUS + map.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg" + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA) + page.goto(f"{live_server.url}{map.get_absolute_url()}?edit") + marker = page.locator(".umap-div-icon img") + expect(marker).to_have_count(1) + # Should have default img + expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") + # Edit datalayer + marker.click(modifiers=["Control", "Shift"]) + settings = page.get_by_text("Layer properties") + expect(settings).to_be_visible() + shape_settings = page.get_by_text("Shape properties") + expect(shape_settings).to_be_visible() + shape_settings.click() + define = page.locator(".umap-field-iconUrl .define") + undefine = page.locator(".umap-field-iconUrl .undefine") + expect(define).to_be_visible() + expect(undefine).to_be_hidden() + define.click() + symbols = page.locator(".umap-pictogram-choice") + expect(symbols).to_have_count(2) + search = page.locator(".umap-pictogram-list input") + search.type("circle") + expect(symbols).to_have_count(1) + symbols.click() + expect(marker).to_have_attribute("src", "/uploads/pictogram/circle.svg") + undefine.click() + expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") From 2026a2c7e6583c38b37683f20baffb77cf6a8c99 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 13 Nov 2023 17:02:35 +0100 Subject: [PATCH 05/14] picto field: only add close button once --- umap/static/umap/js/umap.forms.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index c4c82fee..87b8d0ab 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -669,18 +669,6 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ for (let [category, items] of sorted) { this.addCategory(category, items) } - const closeButton = L.DomUtil.createButton( - 'button action-button', - this.pictogramsContainer, - L._('Close'), - function (e) { - this.pictogramsContainer.innerHTML = '' - this.tabsContainer.innerHTML = '' - if (this.isDefault()) this.undefine(e) - else this.udpatePreview() - }, - this - ) }, isDefault: function () { @@ -704,6 +692,18 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ this.searchInput.placeholder = L._('Search') this.gridContainer = L.DomUtil.create('div', '', this.pictogramsContainer) L.DomEvent.on(this.searchInput, 'input', this.buildSymbolsList, this) + const closeButton = L.DomUtil.createButton( + 'button action-button', + this.pictogramsContainer, + L._('Close'), + function (e) { + this.pictogramsContainer.innerHTML = '' + this.tabsContainer.innerHTML = '' + if (this.isDefault()) this.undefine(e) + else this.udpatePreview() + }, + this + ) if (this.pictogram_list) { this.buildSymbolsList() } else { From eaf6c17db47a6049be55d39a2b7f3190392742ac Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 13 Nov 2023 17:11:40 +0100 Subject: [PATCH 06/14] Picto field: only add category when at least one picto matches search --- umap/static/umap/js/umap.forms.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 87b8d0ab..35d7ad36 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -631,6 +631,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, this ) + return true // Icon has been added (not filtered) }, clear: function () { @@ -645,13 +646,14 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const parent = L.DomUtil.create( 'div', 'umap-pictogram-category', - this.gridContainer ), title = L.DomUtil.add('h6', '', parent, category), grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent) + let status = false for (let item of items) { - this.addIconPreview(item, grid) + status = this.addIconPreview(item, grid) || status } + if (status) this.gridContainer.appendChild(parent) }, buildSymbolsList: function () { From 0562055b659ee15cfd7df5ce18ab801f49112420 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 13 Nov 2023 19:28:07 +0100 Subject: [PATCH 07/14] Picot fied: add test for changing picto from marker itself --- umap/tests/integration/test_picto.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index 96675055..2856fd00 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -100,3 +100,37 @@ def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos): expect(marker).to_have_attribute("src", "/uploads/pictogram/circle.svg") undefine.click() expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") + + +def test_can_change_picto_at_marker_level(map, live_server, page, pictos): + # Faster than doing a login + map.edit_status = Map.ANONYMOUS + map.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg" + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA) + page.goto(f"{live_server.url}{map.get_absolute_url()}?edit") + marker = page.locator(".umap-div-icon img") + expect(marker).to_have_count(1) + # Should have default img + expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") + # Edit marker + marker.click(modifiers=["Shift"]) + settings = page.get_by_text("Feature properties") + expect(settings).to_be_visible() + shape_settings = page.get_by_text("Shape properties") + expect(shape_settings).to_be_visible() + shape_settings.click() + define = page.locator(".umap-field-iconUrl .define") + undefine = page.locator(".umap-field-iconUrl .undefine") + expect(define).to_be_visible() + expect(undefine).to_be_hidden() + define.click() + symbols = page.locator(".umap-pictogram-choice") + expect(symbols).to_have_count(2) + search = page.locator(".umap-pictogram-list input") + search.type("circle") + expect(symbols).to_have_count(1) + symbols.click() + expect(marker).to_have_attribute("src", "/uploads/pictogram/circle.svg") + undefine.click() + expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") From 7810cba66086dec3d209c488a92d6e53d1853bad Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 13 Nov 2023 19:28:30 +0100 Subject: [PATCH 08/14] Test: reset MEDIA_ROOT after each test We need uploaded pictograms path to be deterministic. --- umap/tests/conftest.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/umap/tests/conftest.py b/umap/tests/conftest.py index 3465994b..7e5e356c 100644 --- a/umap/tests/conftest.py +++ b/umap/tests/conftest.py @@ -24,11 +24,8 @@ def pytest_configure(config): settings.MEDIA_ROOT = TMP_ROOT -def pytest_unconfigure(config): - shutil.rmtree(TMP_ROOT, ignore_errors=True) - - def pytest_runtest_teardown(): + shutil.rmtree(TMP_ROOT, ignore_errors=True) cache.clear() From 35afd02551ebb0b4b9f780a5522c76c8da1fe247 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 13 Nov 2023 19:55:37 +0100 Subject: [PATCH 09/14] Increase pictogram category title font size --- umap/static/umap/base.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 15b3a29b..11b5e62f 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -552,6 +552,9 @@ i.info { font-weight: bold; border-bottom: 1px solid #fff; } +.umap-pictogram-category h6 { + font-size: 1.3em; +} .umap-pictogram-grid { display: grid; grid-template-columns: repeat(auto-fill, 30px); From 4bfc3d6666d1c91cf138ec8ba8e199b7b7fcc693 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 14 Nov 2023 18:45:40 +0100 Subject: [PATCH 10/14] Picto field: make sure we open on the right tab --- umap/static/umap/js/umap.forms.js | 73 ++++++++++++++----------- umap/tests/integration/test_picto.py | 81 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 30 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 35d7ad36..4665fb09 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -537,38 +537,41 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ onDefine: function () { this.buildTabs() - this.showSymbols() + const value = this.value() + if (!value || value.startsWith('/')) this.showSymbolsTab() + else if (value.startsWith('http')) this.showURLTab() + else this.showCharsTab() }, buildTabs: function () { const symbol = L.DomUtil.add( 'button', - 'flat on', + 'flat tab-symbols', this.tabsContainer, L._('Symbol') ), char = L.DomUtil.add( 'button', - 'flat', + 'flat tab-chars', this.tabsContainer, L._('Emoji & Character') ) - url = L.DomUtil.add('button', 'flat', this.tabsContainer, L._('URL')) - toggle = (e) => { - L.DomUtil.removeClass(symbol, 'on') - L.DomUtil.removeClass(char, 'on') - L.DomUtil.removeClass(url, 'on') - L.DomUtil.addClass(e.target, 'on') - } + url = L.DomUtil.add('button', 'flat tab-url', this.tabsContainer, L._('URL')) L.DomEvent.on(symbol, 'click', L.DomEvent.stop) - .on(symbol, 'click', this.showSymbols, this) - .on(symbol, 'click', toggle) + .on(symbol, 'click', this.showSymbolsTab, this) L.DomEvent.on(char, 'click', L.DomEvent.stop) - .on(char, 'click', this.showChars, this) - .on(char, 'click', toggle) + .on(char, 'click', this.showCharsTab, this) L.DomEvent.on(url, 'click', L.DomEvent.stop) - .on(url, 'click', this.showURL, this) - .on(url, 'click', toggle) + .on(url, 'click', this.showURLTab, this) + }, + + highlightTab: function (name) { + const els = this.tabsContainer.querySelectorAll('button') + for (let el of els) { + L.DomUtil.removeClass(el, 'on') + } + let el = this.tabsContainer.querySelector(`.tab-${name}`) + L.DomUtil.addClass(el, 'on') }, isUrl: function () { @@ -586,25 +589,33 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const img = L.DomUtil.create( 'img', '', - L.DomUtil.create('div', 'umap-pictogram-choice visible', this.buttonsContainer) + L.DomUtil.create( + 'div', + 'umap-pictogram-choice visible', + this.buttonsContainer + ) ) img.src = this.value() - L.DomEvent.on(img, 'click', this.showSymbols, this) + L.DomEvent.on(img, 'click', this.showSymbolsTab, this) } else { const el = L.DomUtil.create( 'span', '', - L.DomUtil.create('div', 'umap-pictogram-choice visible', this.buttonsContainer) + L.DomUtil.create( + 'div', + 'umap-pictogram-choice visible', + this.buttonsContainer + ) ) el.textContent = this.value() - L.DomEvent.on(el, 'click', this.showSymbols, this) + L.DomEvent.on(el, 'click', this.showSymbolsTab, this) } } this.button = L.DomUtil.createButton( 'button action-button', this.buttonsContainer, this.value() ? L._('Change') : L._('Add'), - this.showSymbols, + this.onDefine, this ) }, @@ -613,7 +624,9 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const baseClass = 'umap-pictogram-choice visible', value = pictogram.src, search = this.searchInput.value.toLowerCase(), - title = pictogram.attribution ? `${pictogram.name} — © ${pictogram.attribution}` : pictogram.name + title = pictogram.attribution + ? `${pictogram.name} — © ${pictogram.attribution}` + : pictogram.name if (search && title.toLowerCase().indexOf(search) === -1) return const className = value === this.value() ? `${baseClass} selected` : baseClass, container = L.DomUtil.create('div', className, parent), @@ -631,7 +644,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, this ) - return true // Icon has been added (not filtered) + return true // Icon has been added (not filtered) }, clear: function () { @@ -643,10 +656,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, addCategory: function (category, items) { - const parent = L.DomUtil.create( - 'div', - 'umap-pictogram-category', - ), + const parent = L.DomUtil.create('div', 'umap-pictogram-category'), title = L.DomUtil.add('h6', '', parent, category), grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent) let status = false @@ -677,16 +687,18 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ return !this.value() || this.value() === this.obj.getMap().options.default_iconUrl }, - showChars: function () { + showCharsTab: function () { // Do not show default value here, as it's not a character // and it has not been explicitely chosen by the user. + this.highlightTab('chars') if (this.isDefault()) this.input.value = '' this.input.type = 'text' this.input.placeholder = L._('Type char or paste emoji') this.pictogramsContainer.innerHTML = '' }, - showSymbols: function () { + showSymbolsTab: function () { + this.highlightTab('symbols') this.input.type = 'hidden' this.buttonsContainer.innerHTML = '' this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer) @@ -719,7 +731,8 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ } }, - showURL: function () { + showURLTab: function () { + this.highlightTab('url') this.input.type = 'url' this.input.placeholder = L._('Add URL') this.pictogramsContainer.innerHTML = '' diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index 2856fd00..c0c12115 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -134,3 +134,84 @@ def test_can_change_picto_at_marker_level(map, live_server, page, pictos): expect(marker).to_have_attribute("src", "/uploads/pictogram/circle.svg") undefine.click() expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") + + +def test_can_use_remote_url_as_picto(map, live_server, page, pictos): + # Faster than doing a login + map.edit_status = Map.ANONYMOUS + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA) + page.goto(f"{live_server.url}{map.get_absolute_url()}?edit") + marker = page.locator(".umap-div-icon img") + expect(marker).to_have_count(1) + # Should have default img + expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") + edit_settings = page.get_by_title("Edit map settings") + expect(edit_settings).to_be_visible() + edit_settings.click() + shape_settings = page.get_by_text("Default shape properties") + expect(shape_settings).to_be_visible() + shape_settings.click() + define = page.locator(".umap-field-iconUrl .define") + expect(define).to_be_visible() + define.click() + url_tab = page.get_by_role("button", name="URL") + input_el = page.locator("input[name='iconUrl']") + expect(input_el).to_be_hidden() + expect(url_tab).to_be_visible() + url_tab.click() + expect(input_el).to_be_visible() + input_el.fill("https://foo.bar/img.jpg") + input_el.blur() + expect(marker).to_have_attribute("src", "https://foo.bar/img.jpg") + # Now close and reopen the form, it should still be the URL tab + close = page.locator("#umap-ui-container").get_by_text("Close") + expect(close).to_be_visible() + close.click() + edit_settings.click() + shape_settings.click() + modify = page.locator(".umap-field-iconUrl").get_by_text("Change") + expect(modify).to_be_visible() + modify.click() + # Should be on URL tab + expect(input_el).to_be_visible() + + +def test_can_use_char_as_picto(map, live_server, page, pictos): + # Faster than doing a login + map.edit_status = Map.ANONYMOUS + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA) + page.goto(f"{live_server.url}{map.get_absolute_url()}?edit") + marker = page.locator(".umap-div-icon span") + # Should have default img, so not a span + expect(marker).to_have_count(0) + edit_settings = page.get_by_title("Edit map settings") + expect(edit_settings).to_be_visible() + edit_settings.click() + shape_settings = page.get_by_text("Default shape properties") + expect(shape_settings).to_be_visible() + shape_settings.click() + define = page.locator(".umap-field-iconUrl .define") + define.click() + url_tab = page.get_by_role("button", name="Emoji & Character") + input_el = page.locator("input[name='iconUrl']") + expect(input_el).to_be_hidden() + expect(url_tab).to_be_visible() + url_tab.click() + expect(input_el).to_be_visible() + input_el.fill("♩") + input_el.blur() + expect(marker).to_have_count(1) + expect(marker).to_have_text("♩") + # Now close and reopen the form, it should still be the URL tab + close = page.locator("#umap-ui-container").get_by_text("Close") + expect(close).to_be_visible() + close.click() + edit_settings.click() + shape_settings.click() + modify = page.locator(".umap-field-iconUrl").get_by_text("Change") + expect(modify).to_be_visible() + modify.click() + # Should be on URL tab + expect(input_el).to_be_visible() From 8a2109948f5259af1f2c5f6574089b51f8de1c74 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 15 Nov 2023 18:15:30 +0100 Subject: [PATCH 11/14] Picto field: add hover and selected style --- umap/static/umap/base.css | 11 +++++------ umap/static/umap/js/umap.forms.js | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 11b5e62f..4e931b11 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -569,9 +569,6 @@ i.info { background-color: #999; text-align: center; margin-bottom: 5px; - display: none; -} -.umap-pictogram-choice.visible { display: block; } .umap-pictogram-choice img { @@ -579,11 +576,13 @@ i.info { max-width: 24px; } .umap-pictogram-choice:hover, -.umap-pictogram-choice.selected, .umap-color-picker span:hover { - box-shadow: 0 0 4px 0 rgb(66, 236, 230); - background-color: #aaa; + background-color: #bebebe; } +.umap-pictogram-choice.selected { + box-shadow: inset 0 0 0 1px #e9e9e9; +} + .umap-pictogram-choice .leaflet-marker-icon { bottom: 0; left: 30px; diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 4665fb09..fcd00ae9 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -591,7 +591,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ '', L.DomUtil.create( 'div', - 'umap-pictogram-choice visible', + 'umap-pictogram-choice', this.buttonsContainer ) ) @@ -603,7 +603,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ '', L.DomUtil.create( 'div', - 'umap-pictogram-choice visible', + 'umap-pictogram-choice', this.buttonsContainer ) ) @@ -621,7 +621,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, addIconPreview: function (pictogram, parent) { - const baseClass = 'umap-pictogram-choice visible', + const baseClass = 'umap-pictogram-choice', value = pictogram.src, search = this.searchInput.value.toLowerCase(), title = pictogram.attribution From c58117219730589c54112cb0431216e3d5594b4f Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 15 Nov 2023 21:06:40 +0100 Subject: [PATCH 12/14] Make icons search accent insensitive --- umap/static/umap/js/umap.core.js | 7 +++++++ umap/static/umap/js/umap.forms.js | 35 +++++++++++++++---------------- umap/static/umap/test/Util.js | 10 +++++++++ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index f392322a..391ec2e6 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -287,6 +287,13 @@ L.Util.copyToClipboard = function (textToCopy) { } } +L.Util.normalize = function (s) { + return (s || '') + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') +} + L.DomUtil.add = (tagName, className, container, content) => { const el = L.DomUtil.create(tagName, className, container) if (content) { diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index fcd00ae9..f856f99f 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -557,12 +557,19 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ L._('Emoji & Character') ) url = L.DomUtil.add('button', 'flat tab-url', this.tabsContainer, L._('URL')) - L.DomEvent.on(symbol, 'click', L.DomEvent.stop) - .on(symbol, 'click', this.showSymbolsTab, this) - L.DomEvent.on(char, 'click', L.DomEvent.stop) - .on(char, 'click', this.showCharsTab, this) - L.DomEvent.on(url, 'click', L.DomEvent.stop) - .on(url, 'click', this.showURLTab, this) + L.DomEvent.on(symbol, 'click', L.DomEvent.stop).on( + symbol, + 'click', + this.showSymbolsTab, + this + ) + L.DomEvent.on(char, 'click', L.DomEvent.stop).on( + char, + 'click', + this.showCharsTab, + this + ) + L.DomEvent.on(url, 'click', L.DomEvent.stop).on(url, 'click', this.showURLTab, this) }, highlightTab: function (name) { @@ -589,11 +596,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const img = L.DomUtil.create( 'img', '', - L.DomUtil.create( - 'div', - 'umap-pictogram-choice', - this.buttonsContainer - ) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) img.src = this.value() L.DomEvent.on(img, 'click', this.showSymbolsTab, this) @@ -601,11 +604,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const el = L.DomUtil.create( 'span', '', - L.DomUtil.create( - 'div', - 'umap-pictogram-choice', - this.buttonsContainer - ) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) el.textContent = this.value() L.DomEvent.on(el, 'click', this.showSymbolsTab, this) @@ -623,11 +622,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ addIconPreview: function (pictogram, parent) { const baseClass = 'umap-pictogram-choice', value = pictogram.src, - search = this.searchInput.value.toLowerCase(), + search = L.Util.normalize(this.searchInput.value), title = pictogram.attribution ? `${pictogram.name} — © ${pictogram.attribution}` : pictogram.name - if (search && title.toLowerCase().indexOf(search) === -1) return + if (search && L.Util.normalize(title).indexOf(search) === -1) return const className = value === this.value() ? `${baseClass} selected` : baseClass, container = L.DomUtil.create('div', className, parent), img = L.DomUtil.create('img', '', container) diff --git a/umap/static/umap/test/Util.js b/umap/static/umap/test/Util.js index 07d2b284..3449fc40 100644 --- a/umap/static/umap/test/Util.js +++ b/umap/static/umap/test/Util.js @@ -475,6 +475,16 @@ describe('L.Util', function () { }) }) + describe("#normalize()", function () { + + if('should remove accents', function () { + // French é + assert.equal(L.Util.normalize('aéroport'), 'aeroport') + // American é + assert.equal(L.Util.normalize('aéroport'), 'aeroport') + }) + }) + describe("#sortFeatures()", function () { let feat1, feat2, feat3 before(function () { From cdfcce297d1b61d838bb71cff9db01d663935b5e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 16 Nov 2023 09:51:05 +0100 Subject: [PATCH 13/14] picto field: better handling of default input values We don't want to have an URL in the "char" field, and vice versa --- umap/static/umap/js/umap.forms.js | 136 +++++++++++++++------------ umap/tests/integration/test_picto.py | 14 +-- 2 files changed, 85 insertions(+), 65 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index f856f99f..73802d15 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -525,38 +525,50 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ build: function () { L.FormBuilder.BlurInput.prototype.build.call(this) - this.buttonsContainer = L.DomUtil.create('div', '') - this.pictogramsContainer = L.DomUtil.create('div', 'umap-pictogram-list') - this.tabsContainer = L.DomUtil.create('div', 'pictogram-tabs') - L.DomUtil.before(this.input, this.buttonsContainer) - L.DomUtil.before(this.input, this.tabsContainer) - L.DomUtil.before(this.input, this.pictogramsContainer) + this.buttons = L.DomUtil.create('div', '', this.parentNode) + this.tabs = L.DomUtil.create('div', 'pictogram-tabs', this.parentNode) + this.body = L.DomUtil.create('div', 'umap-pictogram-body', this.parentNode) + this.footer = L.DomUtil.create('div', '', this.parentNode) this.udpatePreview() this.on('define', this.onDefine) }, onDefine: function () { + this.buttons.innerHTML = '' this.buildTabs() const value = this.value() if (!value || value.startsWith('/')) this.showSymbolsTab() else if (value.startsWith('http')) this.showURLTab() else this.showCharsTab() + const closeButton = L.DomUtil.createButton( + 'button action-button', + this.footer, + L._('Close'), + function (e) { + this.body.innerHTML = '' + this.tabs.innerHTML = '' + this.footer.innerHTML = '' + if (this.isDefault()) this.undefine(e) + else this.udpatePreview() + }, + this + ) }, buildTabs: function () { const symbol = L.DomUtil.add( 'button', 'flat tab-symbols', - this.tabsContainer, + this.tabs, L._('Symbol') ), char = L.DomUtil.add( 'button', 'flat tab-chars', - this.tabsContainer, + this.tabs, L._('Emoji & Character') ) - url = L.DomUtil.add('button', 'flat tab-url', this.tabsContainer, L._('URL')) + url = L.DomUtil.add('button', 'flat tab-url', this.tabs, L._('URL')) L.DomEvent.on(symbol, 'click', L.DomEvent.stop).on( symbol, 'click', @@ -572,31 +584,40 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ L.DomEvent.on(url, 'click', L.DomEvent.stop).on(url, 'click', this.showURLTab, this) }, - highlightTab: function (name) { - const els = this.tabsContainer.querySelectorAll('button') + openTab: function (name) { + const els = this.tabs.querySelectorAll('button') for (let el of els) { L.DomUtil.removeClass(el, 'on') } - let el = this.tabsContainer.querySelector(`.tab-${name}`) + let el = this.tabs.querySelector(`.tab-${name}`) L.DomUtil.addClass(el, 'on') + this.body.innerHTML = '' }, - isUrl: function () { - return this.value() && this.value().indexOf('/') !== -1 + isPath: function () { + const value = this.value() + return value && value.length && value.startsWith('/') + }, + + isRemoteUrl: function () { + const value = this.value() + return value && value.length && value.startsWith('http') + }, + + isImg: function () { + return this.isPath() || this.isRemoteUrl() }, udpatePreview: function () { - if (this.isDefault()) { - this.buttonsContainer.innerHTML = '' - return - } + this.buttons.innerHTML = '' + if (this.isDefault()) return if (!L.Util.hasVar(this.value())) { // Do not try to render URL with variables - if (this.isUrl()) { + if (this.isImg()) { const img = L.DomUtil.create( 'img', '', - L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons) ) img.src = this.value() L.DomEvent.on(img, 'click', this.showSymbolsTab, this) @@ -604,7 +625,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const el = L.DomUtil.create( 'span', '', - L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons) ) el.textContent = this.value() L.DomEvent.on(el, 'click', this.showSymbolsTab, this) @@ -612,7 +633,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ } this.button = L.DomUtil.createButton( 'button action-button', - this.buttonsContainer, + this.buttons, this.value() ? L._('Change') : L._('Add'), this.onDefine, this @@ -638,7 +659,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ function (e) { this.input.value = value this.sync() - this.unselectAll(this.gridContainer) + this.unselectAll(this.grid) L.DomUtil.addClass(container, 'selected') }, this @@ -648,9 +669,9 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ clear: function () { this.input.value = '' - this.unselectAll(this.pictogramsContainer) + this.unselectAll(this.body) this.sync() - this.pictogramsContainer.innerHTML = '' + this.body.innerHTML = '' this.udpatePreview() }, @@ -662,11 +683,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ for (let item of items) { status = this.addIconPreview(item, grid) || status } - if (status) this.gridContainer.appendChild(parent) + if (status) this.grid.appendChild(parent) }, buildSymbolsList: function () { - this.gridContainer.innerHTML = '' + this.grid.innerHTML = '' const categories = {} let category for (const props of this.pictogram_list) { @@ -686,37 +707,13 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ return !this.value() || this.value() === this.obj.getMap().options.default_iconUrl }, - showCharsTab: function () { - // Do not show default value here, as it's not a character - // and it has not been explicitely chosen by the user. - this.highlightTab('chars') - if (this.isDefault()) this.input.value = '' - this.input.type = 'text' - this.input.placeholder = L._('Type char or paste emoji') - this.pictogramsContainer.innerHTML = '' - }, - showSymbolsTab: function () { - this.highlightTab('symbols') - this.input.type = 'hidden' - this.buttonsContainer.innerHTML = '' - this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer) + this.openTab('symbols') + this.searchInput = L.DomUtil.create('input', '', this.body) this.searchInput.type = 'search' this.searchInput.placeholder = L._('Search') - this.gridContainer = L.DomUtil.create('div', '', this.pictogramsContainer) + this.grid = L.DomUtil.create('div', '', this.body) L.DomEvent.on(this.searchInput, 'input', this.buildSymbolsList, this) - const closeButton = L.DomUtil.createButton( - 'button action-button', - this.pictogramsContainer, - L._('Close'), - function (e) { - this.pictogramsContainer.innerHTML = '' - this.tabsContainer.innerHTML = '' - if (this.isDefault()) this.undefine(e) - else this.udpatePreview() - }, - this - ) if (this.pictogram_list) { this.buildSymbolsList() } else { @@ -730,11 +727,34 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ } }, + showCharsTab: function () { + this.openTab('chars') + const value = !this.isImg() ? this.value() : null + const input = this.buildInput(this.body, value) + input.placeholder = L._('Type char or paste emoji') + input.type = 'text' + }, + showURLTab: function () { - this.highlightTab('url') - this.input.type = 'url' - this.input.placeholder = L._('Add URL') - this.pictogramsContainer.innerHTML = '' + this.openTab('url') + const value = this.isRemoteUrl() ? this.value() : null + const input = this.buildInput(this.body, value) + input.placeholder = L._('Add image URL') + input.type = 'url' + }, + + buildInput: function (parent, value) { + const input = L.DomUtil.create('input', 'blur', parent) + const button = L.DomUtil.create('span', 'button blur-button', parent) + if (value) input.value = value + L.DomEvent.on(input, 'blur', () => { + // Do not clear this.input when focus-blur + // empty input + if (input.value === value) return + this.input.value = input.value + this.sync() + }) + return input }, unselectAll: function (container) { diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index c0c12115..f36d462a 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -59,7 +59,7 @@ def test_can_change_picto_at_map_level(map, live_server, page, pictos): define.click() symbols = page.locator(".umap-pictogram-choice") expect(symbols).to_have_count(2) - search = page.locator(".umap-pictogram-list input") + search = page.locator(".umap-pictogram-body input") search.type("star") expect(symbols).to_have_count(1) symbols.click() @@ -93,7 +93,7 @@ def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos): define.click() symbols = page.locator(".umap-pictogram-choice") expect(symbols).to_have_count(2) - search = page.locator(".umap-pictogram-list input") + search = page.locator(".umap-pictogram-body input") search.type("circle") expect(symbols).to_have_count(1) symbols.click() @@ -127,7 +127,7 @@ def test_can_change_picto_at_marker_level(map, live_server, page, pictos): define.click() symbols = page.locator(".umap-pictogram-choice") expect(symbols).to_have_count(2) - search = page.locator(".umap-pictogram-list input") + search = page.locator(".umap-pictogram-body input") search.type("circle") expect(symbols).to_have_count(1) symbols.click() @@ -156,7 +156,7 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos): expect(define).to_be_visible() define.click() url_tab = page.get_by_role("button", name="URL") - input_el = page.locator("input[name='iconUrl']") + input_el = page.get_by_placeholder("Add image URL") expect(input_el).to_be_hidden() expect(url_tab).to_be_visible() url_tab.click() @@ -165,7 +165,7 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos): input_el.blur() expect(marker).to_have_attribute("src", "https://foo.bar/img.jpg") # Now close and reopen the form, it should still be the URL tab - close = page.locator("#umap-ui-container").get_by_text("Close") + close = page.locator("#umap-ui-container").get_by_title("Close") expect(close).to_be_visible() close.click() edit_settings.click() @@ -195,7 +195,7 @@ def test_can_use_char_as_picto(map, live_server, page, pictos): define = page.locator(".umap-field-iconUrl .define") define.click() url_tab = page.get_by_role("button", name="Emoji & Character") - input_el = page.locator("input[name='iconUrl']") + input_el = page.get_by_placeholder("Type char or paste emoji") expect(input_el).to_be_hidden() expect(url_tab).to_be_visible() url_tab.click() @@ -205,7 +205,7 @@ def test_can_use_char_as_picto(map, live_server, page, pictos): expect(marker).to_have_count(1) expect(marker).to_have_text("♩") # Now close and reopen the form, it should still be the URL tab - close = page.locator("#umap-ui-container").get_by_text("Close") + close = page.locator("#umap-ui-container").get_by_title("Close") expect(close).to_be_visible() close.click() edit_settings.click() From c99892b51e0175616564462269c0b56d42dd5e1c Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 16 Nov 2023 11:40:18 +0100 Subject: [PATCH 14/14] Picto field: fix clicking on preview to open the form --- umap/static/umap/js/umap.forms.js | 18 ++++++------------ umap/static/umap/map.css | 1 + umap/tests/integration/test_picto.py | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 73802d15..2aa84e76 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -535,6 +535,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ onDefine: function () { this.buttons.innerHTML = '' + this.footer.innerHTML = '' this.buildTabs() const value = this.value() if (!value || value.startsWith('/')) this.showSymbolsTab() @@ -556,6 +557,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, buildTabs: function () { + this.tabs.innerHTML = '' const symbol = L.DomUtil.add( 'button', 'flat tab-symbols', @@ -613,22 +615,14 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ if (this.isDefault()) return if (!L.Util.hasVar(this.value())) { // Do not try to render URL with variables + const box = L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons) + L.DomEvent.on(box, 'click', this.onDefine, this) if (this.isImg()) { - const img = L.DomUtil.create( - 'img', - '', - L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons) - ) + const img = L.DomUtil.create('img', '', box) img.src = this.value() - L.DomEvent.on(img, 'click', this.showSymbolsTab, this) } else { - const el = L.DomUtil.create( - 'span', - '', - L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons) - ) + const el = L.DomUtil.create('span', '', box) el.textContent = this.value() - L.DomEvent.on(el, 'click', this.showSymbolsTab, this) } } this.button = L.DomUtil.createButton( diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 1a1d1d19..d32e1d87 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -1269,6 +1269,7 @@ a.add-datalayer:hover, vertical-align: middle; color: white; font-weight: bold; + font-size: 1.2rem; } .umap-circle-icon { border: 1px solid white; diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index f36d462a..5a960f20 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -210,8 +210,8 @@ def test_can_use_char_as_picto(map, live_server, page, pictos): close.click() edit_settings.click() shape_settings.click() - modify = page.locator(".umap-field-iconUrl").get_by_text("Change") - expect(modify).to_be_visible() - modify.click() + preview = page.locator(".umap-pictogram-choice") + expect(preview).to_be_visible() + preview.click() # Should be on URL tab expect(input_el).to_be_visible()