diff --git a/umap/migrations/0016_pictogram_category.py b/umap/migrations/0016_pictogram_category.py new file mode 100644 index 00000000..88db888c --- /dev/null +++ b/umap/migrations/0016_pictogram_category.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2023-10-30 17:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("umap", "0015_alter_pictogram_pictogram"), + ] + + operations = [ + migrations.AddField( + model_name="pictogram", + name="category", + field=models.CharField(blank=True, max_length=300, null=True), + ), + ] diff --git a/umap/models.py b/umap/models.py index 40588399..cc676602 100644 --- a/umap/models.py +++ b/umap/models.py @@ -284,6 +284,7 @@ class Pictogram(NamedModel): """ attribution = models.CharField(max_length=300) + category = models.CharField(max_length=300, null=True, blank=True) pictogram = models.FileField(upload_to="pictogram") @property @@ -292,6 +293,7 @@ class Pictogram(NamedModel): "id": self.pk, "attribution": self.attribution, "name": self.name, + "category": self.category, "src": self.pictogram.url, } diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index e92d39fd..5152f4ce 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -521,16 +521,14 @@ i.info { margin-top: -8px; padding: 0 5px; } -.umap-icon-list, .umap-pictogram-list { - clear: both; +.umap-pictogram-grid { + display: flex; + flex-wrap: wrap; } -.umap-icon-choice { - display: block; - float: left; +.umap-pictogram-choice { width: 30px; height: 30px; line-height: 30px; - position: relative; cursor: pointer; background-image: url('./img/icon-bg.png'); text-align: center; @@ -538,16 +536,16 @@ i.info { margin-bottom: 5px; margin-right: 5px; } -.umap-icon-choice img { +.umap-pictogram-choice img { vertical-align: middle; max-width: 24px; } -.umap-icon-choice:hover, -.umap-icon-choice.selected, +.umap-pictogram-choice:hover, +.umap-pictogram-choice.selected, .umap-color-picker span:hover { box-shadow: 0 0 4px 0 black; } -.umap-icon-choice .leaflet-marker-icon { +.umap-pictogram-choice .leaflet-marker-icon { bottom: 0; left: 30px; position: absolute; diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index cf62745e..f392322a 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -201,6 +201,16 @@ L.Util.greedyTemplate = (str, data, ignore) => { ) } +L.Util.naturalSort = (a, b) => { + return a + .toString() + .toLowerCase() + .localeCompare(b.toString().toLowerCase(), L.lang || 'en', { + sensitivity: 'base', + numeric: true, + }) +} + L.Util.sortFeatures = (features, sortKey) => { const sortKeys = (sortKey || 'name').split(',') @@ -214,19 +224,9 @@ L.Util.sortFeatures = (features, sortKey) => { let score const valA = a.properties[sortKey] || '' const valB = b.properties[sortKey] || '' - if (!valA) { - score = -1 - } else if (!valB) { - score = 1 - } else { - score = valA - .toString() - .toLowerCase() - .localeCompare(valB.toString().toLowerCase(), L.lang || 'en', { - sensitivity: 'base', - numeric: true, - }) - } + if (!valA) score = -1 + else if (!valB) score = 1 + else score = L.Util.naturalSort(valA, valB) if (score === 0 && sortKeys[i + 1]) return sort(a, b, i + 1) return score * reverse } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index d5411ef5..20cd99b3 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -547,7 +547,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const img = L.DomUtil.create( 'img', '', - L.DomUtil.create('div', 'umap-icon-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) img.src = this.value() L.DomEvent.on(img, 'click', this.fetchIconList, this) @@ -555,7 +555,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const el = L.DomUtil.create( 'span', '', - L.DomUtil.create('div', 'umap-icon-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) el.textContent = this.value() L.DomEvent.on(el, 'click', this.fetchIconList, this) @@ -570,11 +570,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ ) }, - addIconPreview: function (pictogram) { - const baseClass = 'umap-icon-choice', + addIconPreview: function (pictogram, parent) { + const baseClass = 'umap-pictogram-choice', value = pictogram.src, className = value === this.value() ? `${baseClass} selected` : baseClass, - container = L.DomUtil.create('div', className, this.pictogramsContainer), + container = L.DomUtil.create('div', className, parent), img = L.DomUtil.create('img', '', container) img.src = value if (pictogram.name && pictogram.attribution) { @@ -602,7 +602,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, search: function (e) { - const icons = [...this.parentNode.querySelectorAll('.umap-icon-choice')], + 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' @@ -610,13 +610,36 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }) }, + addCategory: function (category, items) { + const parent = L.DomUtil.create( + 'div', + 'umap-pictogram-category', + this.pictogramsContainer + ), + title = L.DomUtil.add('h6', '', parent, category), + grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent) + for (let item of items) { + this.addIconPreview(item, grid) + } + }, + buildIconList: 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) - for (const idx in data.pictogram_list) { - this.addIconPreview(data.pictogram_list[idx]) + const categories = {} + let category + for (const props of data.pictogram_list) { + category = props.category || L._('Generic') + categories[category] = categories[category] || [] + categories[category].push(props) + } + const sorted = Object.entries(categories).toSorted(([a], [b]) => + L.Util.naturalSort(a, b) + ) + for (let [category, items] of sorted) { + this.addCategory(category, items) } const closeButton = L.DomUtil.createButton( 'button action-button',