From 126e47eef92f911657fb9293f21b9b8b3b06f0fd Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 12 Feb 2024 14:09:23 +0100 Subject: [PATCH 1/2] feat: show last used pictograms in list This feature was planned in the initial rework of the pictogram form UI, but not yet done. Some recent discussion in the OSM French forum reactivated the need for it. cf https://forum.openstreetmap.fr/t/marker-and-marker-colors/21051 --- umap/static/umap/js/umap.forms.js | 40 ++++++++++++++-------------- umap/static/umap/js/umap.icon.js | 20 +++++++++++--- umap/tests/integration/test_picto.py | 13 +++++++-- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 4ee82042..0f173e5d 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -537,9 +537,13 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ this.on('define', this.onDefine) }, - onDefine: function () { + onDefine: async function () { this.buttons.innerHTML = '' this.footer.innerHTML = '' + const [{ pictogram_list }, response, error] = await this.builder.map.server.get( + this.builder.map.options.urls.pictogram_list_json + ) + if (!error) this.pictogram_list = pictogram_list this.buildTabs() const value = this.value() if (!value || L.Util.isPath(value)) this.showSymbolsTab() @@ -624,12 +628,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ search = L.Util.normalize(this.searchInput.value), title = pictogram.attribution ? `${pictogram.name} — © ${pictogram.attribution}` - : pictogram.name + : pictogram.name || pictogram.src 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) - img.src = value + container = L.DomUtil.create('div', className, parent) + U.Icon.makeIconElement(value, container) container.title = title L.DomEvent.on( container, @@ -664,6 +667,15 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ if (status) this.grid.appendChild(parent) }, + showLastUsed: function () { + if (U.Icon.LAST_USED.length) { + const items = U.Icon.LAST_USED.map((src) => ({ + src, + })) + this.addCategory(L._('Used in this map'), items) + } + }, + buildSymbolsList: function () { this.grid.innerHTML = '' const categories = {} @@ -676,6 +688,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const sorted = Object.entries(categories).toSorted(([a], [b]) => L.Util.naturalSort(a, b) ) + this.showLastUsed() for (let [category, items] of sorted) { this.addCategory(category, items) } @@ -692,17 +705,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ this.searchInput.placeholder = L._('Search') this.grid = L.DomUtil.create('div', '', this.body) L.DomEvent.on(this.searchInput, 'input', this.buildSymbolsList, this) - if (this.pictogram_list) { - this.buildSymbolsList() - } else { - const [{ pictogram_list }, response, error] = await this.builder.map.server.get( - this.builder.map.options.urls.pictogram_list_json - ) - if (!error) { - this.pictogram_list = pictogram_list - this.buildSymbolsList() - } - } + this.buildSymbolsList() }, showCharsTab: function () { @@ -989,10 +992,7 @@ L.FormBuilder.ManageEditors = L.FormBuilder.Element.extend({ on_select: L.bind(this.onSelect, this), on_unselect: L.bind(this.onUnselect, this), } - this.autocomplete = new U.AutoComplete.Ajax.SelectMultiple( - this.parentNode, - options - ) + this.autocomplete = new U.AutoComplete.Ajax.SelectMultiple(this.parentNode, options) this._values = this.toHTML() if (this._values) for (let i = 0; i < this._values.length; i++) diff --git a/umap/static/umap/js/umap.icon.js b/umap/static/umap/js/umap.icon.js index a771108e..288ca1cf 100644 --- a/umap/static/umap/js/umap.icon.js +++ b/umap/static/umap/js/umap.icon.js @@ -1,4 +1,7 @@ U.Icon = L.DivIcon.extend({ + statics: { + LAST_USED: [], + }, initialize: function (map, options) { this.map = map const default_options = { @@ -14,11 +17,22 @@ U.Icon = L.DivIcon.extend({ } }, + _setLastUsed: function (url) { + if (L.Util.hasVar(url)) return + if (url === this.map.options.default_iconUrl) return + if (U.Icon.LAST_USED.indexOf(url) === -1) { + U.Icon.LAST_USED.push(url) + } + }, + _getIconUrl: function (name) { let url - if (this.feature && this.feature._getIconUrl(name)) + if (this.feature && this.feature._getIconUrl(name)) { url = this.feature._getIconUrl(name) - else url = this.options[`${name}Url`] + this._setLastUsed(url) + } else { + url = this.options[`${name}Url`] + } return this.formatUrl(url, this.feature) }, @@ -216,7 +230,7 @@ U.Icon.setIconContrast = function (icon, parent, src, bgcolor) { * src: the raw "icon" value, can be an URL, a path, text, emoticon, etc. * bgcolor: the background color, used for caching and in case we cannot guess the * parent background color - */ + */ if (!icon) return if (L.DomUtil.contrastedColor(parent, bgcolor)) { diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index 96048b08..66bc65c6 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -94,7 +94,7 @@ def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos): expect(undefine).to_be_hidden() define.click() symbols = page.locator(".umap-pictogram-choice") - expect(symbols).to_have_count(2) + expect(symbols).to_have_count(3) search = page.locator(".umap-pictogram-body input") search.type("circle") expect(symbols).to_have_count(1) @@ -128,7 +128,7 @@ def test_can_change_picto_at_marker_level(map, live_server, page, pictos): expect(undefine).to_be_hidden() define.click() symbols = page.locator(".umap-pictogram-choice") - expect(symbols).to_have_count(2) + expect(symbols).to_have_count(3) # Include "Last used" picto search = page.locator(".umap-pictogram-body input") search.type("circle") expect(symbols).to_have_count(1) @@ -157,6 +157,10 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos): define = page.locator(".umap-field-iconUrl .define") expect(define).to_be_visible() define.click() + expect(page.get_by_text("Used in this map")).to_be_hidden() + # Only default symbols + symbols = page.locator(".umap-pictogram-choice") + expect(symbols).to_have_count(2) url_tab = page.get_by_role("button", name="URL") input_el = page.get_by_placeholder("Add image URL") expect(input_el).to_be_hidden() @@ -177,6 +181,11 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos): modify.click() # Should be on URL tab expect(input_el).to_be_visible() + # Symbol should be visible in symbols b + symbols_tab = page.get_by_role("button", name="Symbol") + symbols_tab.click() + expect(page.get_by_text("Used in this map")).to_be_visible() + expect(symbols).to_have_count(3) def test_can_use_char_as_picto(map, live_server, page, pictos): From 2dd7266d21599bf9e2810fe2c3e9221a63d087af Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 13 Feb 2024 10:17:42 +0100 Subject: [PATCH 2/2] feat: show recent picto in a separate tab --- umap/static/umap/js/umap.forms.js | 60 +++++++++++++++++++--------- umap/static/umap/js/umap.icon.js | 12 +++--- umap/tests/integration/test_picto.py | 37 ++++++++++------- 3 files changed, 71 insertions(+), 38 deletions(-) diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 0f173e5d..30d73020 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -546,7 +546,8 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ if (!error) this.pictogram_list = pictogram_list this.buildTabs() const value = this.value() - if (!value || L.Util.isPath(value)) this.showSymbolsTab() + if (U.Icon.RECENT.length) this.showRecentTab() + else if (!value || L.Util.isPath(value)) this.showSymbolsTab() else if (L.Util.isRemoteUrl(value) || L.Util.isDataImage(value)) this.showURLTab() else this.showCharsTab() const closeButton = L.DomUtil.createButton( @@ -566,6 +567,20 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ buildTabs: function () { this.tabs.innerHTML = '' + if (U.Icon.RECENT.length) { + const recent = L.DomUtil.add( + 'button', + 'flat tab-recent', + this.tabs, + L._('Recent') + ) + L.DomEvent.on(recent, 'click', L.DomEvent.stop).on( + recent, + 'click', + this.showRecentTab, + this + ) + } const symbol = L.DomUtil.add( 'button', 'flat tab-symbols', @@ -656,10 +671,10 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ this.updatePreview() }, - addCategory: function (category, items) { + addCategory: function (items, name) { 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) + if (name) L.DomUtil.add('h6', '', parent, name) let status = false for (let item of items) { status = this.addIconPreview(item, grid) || status @@ -667,15 +682,6 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ if (status) this.grid.appendChild(parent) }, - showLastUsed: function () { - if (U.Icon.LAST_USED.length) { - const items = U.Icon.LAST_USED.map((src) => ({ - src, - })) - this.addCategory(L._('Used in this map'), items) - } - }, - buildSymbolsList: function () { this.grid.innerHTML = '' const categories = {} @@ -688,23 +694,41 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const sorted = Object.entries(categories).toSorted(([a], [b]) => L.Util.naturalSort(a, b) ) - this.showLastUsed() - for (let [category, items] of sorted) { - this.addCategory(category, items) + for (let [name, items] of sorted) { + this.addCategory(items, name) } }, + buildRecentList: function () { + this.grid.innerHTML = '' + const items = U.Icon.RECENT.map((src) => ({ + src, + })) + this.addCategory(items) + }, + isDefault: function () { return !this.value() || this.value() === U.DEFAULT_ICON_URL }, - showSymbolsTab: async function () { - this.openTab('symbols') + addGrid: function (onSearch) { this.searchInput = L.DomUtil.create('input', '', this.body) this.searchInput.type = 'search' this.searchInput.placeholder = L._('Search') this.grid = L.DomUtil.create('div', '', this.body) - L.DomEvent.on(this.searchInput, 'input', this.buildSymbolsList, this) + L.DomEvent.on(this.searchInput, 'input', onSearch, this) + }, + + showRecentTab: function () { + if (!U.Icon.RECENT.length) return + this.openTab('recent') + this.addGrid(this.buildRecentList) + this.buildRecentList() + }, + + showSymbolsTab: function () { + this.openTab('symbols') + this.addGrid(this.buildSymbolsList) this.buildSymbolsList() }, diff --git a/umap/static/umap/js/umap.icon.js b/umap/static/umap/js/umap.icon.js index 288ca1cf..5a0189b3 100644 --- a/umap/static/umap/js/umap.icon.js +++ b/umap/static/umap/js/umap.icon.js @@ -1,6 +1,6 @@ U.Icon = L.DivIcon.extend({ statics: { - LAST_USED: [], + RECENT: [], }, initialize: function (map, options) { this.map = map @@ -17,11 +17,11 @@ U.Icon = L.DivIcon.extend({ } }, - _setLastUsed: function (url) { + _setRecent: function (url) { if (L.Util.hasVar(url)) return - if (url === this.map.options.default_iconUrl) return - if (U.Icon.LAST_USED.indexOf(url) === -1) { - U.Icon.LAST_USED.push(url) + if (url === U.DEFAULT_ICON_URL) return + if (U.Icon.RECENT.indexOf(url) === -1) { + U.Icon.RECENT.push(url) } }, @@ -29,7 +29,7 @@ U.Icon = L.DivIcon.extend({ let url if (this.feature && this.feature._getIconUrl(name)) { url = this.feature._getIconUrl(name) - this._setLastUsed(url) + this._setRecent(url) } else { url = this.options[`${name}Url`] } diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index 66bc65c6..a44e5a02 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -58,6 +58,8 @@ 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() + # No picto defined yet, so recent should not be visible + expect(page.get_by_text("Recent")).to_be_hidden() symbols = page.locator(".umap-pictogram-choice") expect(symbols).to_have_count(2) search = page.locator(".umap-pictogram-body input") @@ -93,8 +95,14 @@ def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos): expect(define).to_be_visible() expect(undefine).to_be_hidden() define.click() + # Map has an icon defined, so it shold open on Recent tab symbols = page.locator(".umap-pictogram-choice") - expect(symbols).to_have_count(3) + expect(page.get_by_text("Recent")).to_be_visible() + expect(symbols).to_have_count(1) + symbol_tab = page.get_by_role("button", name="Symbol") + expect(symbol_tab).to_be_visible() + symbol_tab.click() + expect(symbols).to_have_count(2) search = page.locator(".umap-pictogram-body input") search.type("circle") expect(symbols).to_have_count(1) @@ -127,8 +135,14 @@ def test_can_change_picto_at_marker_level(map, live_server, page, pictos): expect(define).to_be_visible() expect(undefine).to_be_hidden() define.click() + # Map has an icon defined, so it shold open on Recent tab symbols = page.locator(".umap-pictogram-choice") - expect(symbols).to_have_count(3) # Include "Last used" picto + expect(page.get_by_text("Recent")).to_be_visible() + expect(symbols).to_have_count(1) + symbol_tab = page.get_by_role("button", name="Symbol") + expect(symbol_tab).to_be_visible() + symbol_tab.click() + expect(symbols).to_have_count(2) search = page.locator(".umap-pictogram-body input") search.type("circle") expect(symbols).to_have_count(1) @@ -157,10 +171,6 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos): define = page.locator(".umap-field-iconUrl .define") expect(define).to_be_visible() define.click() - expect(page.get_by_text("Used in this map")).to_be_hidden() - # Only default symbols - symbols = page.locator(".umap-pictogram-choice") - expect(symbols).to_have_count(2) url_tab = page.get_by_role("button", name="URL") input_el = page.get_by_placeholder("Add image URL") expect(input_el).to_be_hidden() @@ -179,13 +189,10 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos): 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() - # Symbol should be visible in symbols b - symbols_tab = page.get_by_role("button", name="Symbol") - symbols_tab.click() - expect(page.get_by_text("Used in this map")).to_be_visible() - expect(symbols).to_have_count(3) + # Should be on Recent tab + symbols = page.locator(".umap-pictogram-choice") + expect(page.get_by_text("Recent")).to_be_visible() + expect(symbols).to_have_count(1) def test_can_use_char_as_picto(map, live_server, page, pictos): @@ -225,4 +232,6 @@ def test_can_use_char_as_picto(map, live_server, page, pictos): expect(preview).to_be_visible() preview.click() # Should be on URL tab - expect(input_el).to_be_visible() + symbols = page.locator(".umap-pictogram-choice") + expect(page.get_by_text("Recent")).to_be_visible() + expect(symbols).to_have_count(1)