Compare commits

..

6 commits

Author SHA1 Message Date
Yohan Boniface
e3c6dcf3fa chore: add test for template_list view 2025-04-22 15:48:42 +02:00
Yohan Boniface
6de878551b wip: mark template maps in maps list 2025-04-22 15:42:41 +02:00
Yohan Boniface
2c8022b422 wip: add link to open template in a new window 2025-04-19 14:52:52 +02:00
Yohan Boniface
9f2d78caeb wip: display templates in importer by origin (me/staff/community)
Co-authored-by: David Larlet <david@larlet.fr>
2025-04-18 18:13:13 +02:00
Yohan Boniface
41117d4a4a fixup: fix tests
Co-authored-by: David Larlet <david@larlet.fr>
2025-04-18 16:39:46 +02:00
Yohan Boniface
754addb03a wip: first naive version of map templates
Co-authored-by: David Larlet <david@larlet.fr>
2025-04-18 16:39:46 +02:00
21 changed files with 534 additions and 593 deletions

View file

@ -1,5 +1,5 @@
# Force rtfd to use a recent version of mkdocs # Force rtfd to use a recent version of mkdocs
mkdocs==1.6.1 mkdocs==1.6.1
pymdown-extensions==10.14.3 pymdown-extensions==10.14.3
mkdocs-material==9.6.12 mkdocs-material==9.6.11
mkdocs-static-i18n==1.3.0 mkdocs-static-i18n==1.3.0

View file

@ -1,5 +1,5 @@
# Force rtfd to use a recent version of mkdocs # Force rtfd to use a recent version of mkdocs
mkdocs==1.6.1 mkdocs==1.6.1
pymdown-extensions==10.14.3 pymdown-extensions==10.14.3
mkdocs-material==9.6.12 mkdocs-material==9.6.11
mkdocs-static-i18n==1.3.0 mkdocs-static-i18n==1.3.0

View file

@ -44,10 +44,10 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"hatch==1.14.1", "hatch==1.14.1",
"ruff==0.11.6", "ruff==0.11.4",
"djlint==1.36.4", "djlint==1.36.4",
"mkdocs==1.6.1", "mkdocs==1.6.1",
"mkdocs-material==9.6.12", "mkdocs-material==9.6.11",
"mkdocs-static-i18n==1.3.0", "mkdocs-static-i18n==1.3.0",
"vermin==1.6.0", "vermin==1.6.0",
"pymdown-extensions==10.14.3", "pymdown-extensions==10.14.3",
@ -62,10 +62,10 @@ test = [
"pytest-playwright==0.7.0", "pytest-playwright==0.7.0",
"pytest-rerunfailures==15.0", "pytest-rerunfailures==15.0",
"pytest-xdist>=3.5.0,<4", "pytest-xdist>=3.5.0,<4",
"moto[s3]==5.1.4" "moto[s3]==5.1.3"
] ]
docker = [ docker = [
"uvicorn==0.34.2", "uvicorn==0.34.1",
] ]
s3 = [ s3 = [
"django-storages[s3]==1.14.6", "django-storages[s3]==1.14.6",

View file

@ -59,16 +59,3 @@
.tooltip-accent li:last-of-type { .tooltip-accent li:last-of-type {
margin-bottom: 0; margin-bottom: 0;
} }
.umap-tooltip-container.tooltip-right:before {
right: 100%;
top: calc(50% - var(--arrow-size));
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-right-color: var(--tooltip-color);
border-width: var(--arrow-size);
}

View file

@ -7,7 +7,6 @@ import {
import { translate } from './i18n.js' import { translate } from './i18n.js'
import { Request, ServerRequest } from './request.js' import { Request, ServerRequest } from './request.js'
import { escapeHTML, generateId } from './utils.js' import { escapeHTML, generateId } from './utils.js'
import * as Utils from './utils.js'
export class BaseAutocomplete { export class BaseAutocomplete {
constructor(parent, options) { constructor(parent, options) {
@ -292,9 +291,10 @@ class BaseServerAjax extends BaseAjax {
export const SingleMixin = (Base) => export const SingleMixin = (Base) =>
class extends Base { class extends Base {
initSelectedContainer() { initSelectedContainer() {
const el = Utils.loadTemplate('<div class="umap-singleresult"></div>') return DomUtil.after(
this.input.parentNode.insertBefore(el, this.input.nextSibling) this.input,
return el DomUtil.element({ tagName: 'div', className: 'umap-singleresult' })
)
} }
displaySelected(result) { displaySelected(result) {
@ -322,9 +322,10 @@ export const SingleMixin = (Base) =>
export const MultipleMixin = (Base) => export const MultipleMixin = (Base) =>
class extends Base { class extends Base {
initSelectedContainer() { initSelectedContainer() {
const el = Utils.loadTemplate('<ul class="umap-multiresult"></ul>') return DomUtil.after(
this.input.parentNode.insertBefore(el, this.input.nextSibling) this.input,
return el DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' })
)
} }
displaySelected(result) { displaySelected(result) {

View file

@ -5,7 +5,6 @@ import { translate } from './i18n.js'
import * as Icon from './rendering/icon.js' import * as Icon from './rendering/icon.js'
import ContextMenu from './ui/contextmenu.js' import ContextMenu from './ui/contextmenu.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
import { SCHEMA } from './schema.js'
export default class Browser { export default class Browser {
constructor(umap, leafletMap) { constructor(umap, leafletMap) {
@ -22,24 +21,35 @@ export default class Browser {
addFeature(feature, parent) { addFeature(feature, parent) {
if (feature.isFiltered()) return if (feature.isFiltered()) return
if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return
const template = ` const row = DomUtil.create('li', `${feature.getClassName()} feature`)
<li class="feature ${feature.getClassName()}"> const zoom_to = DomUtil.createButtonIcon(
<button class="icon icon-16 icon-zoom" title="${translate('Bring feature to center')}" data-ref=zoom></button> row,
<button class="icon icon-16 show-on-edit icon-edit" title="${translate('Edit this feature')}" data-ref=edit></button> 'icon-zoom',
<button class="icon icon-16 show-on-edit icon-delete" title="${translate('Delete this feature')}" data-ref=remove></button> translate('Bring feature to center')
<i class="icon icon-16 icon-${feature.getClassName()} feature-color" data-ref=colorBox></i> )
<span class="feature-title" data-ref=label></span> const edit = DomUtil.createButtonIcon(
</li> row,
` 'show-on-edit icon-edit',
const [row, { zoom, edit, remove, colorBox, label }] = translate('Edit this feature')
Utils.loadTemplateWithRefs(template) )
label.textContent = label.title = feature.getDisplayName() || '—' const del = DomUtil.createButtonIcon(
row,
'show-on-edit icon-delete',
translate('Delete this feature')
)
const colorBox = DomUtil.create(
'i',
`icon icon-16 icon-${feature.getClassName()} feature-color`,
row
)
const title = DomUtil.create('span', 'feature-title', row)
const symbol = feature._getIconUrl const symbol = feature._getIconUrl
? Icon.formatUrl(feature._getIconUrl(), feature) ? Icon.formatUrl(feature._getIconUrl(), feature)
: null : null
title.textContent = title.title = feature.getDisplayName() || '—'
const bgcolor = feature.getPreviewColor() const bgcolor = feature.getPreviewColor()
colorBox.style.backgroundColor = bgcolor colorBox.style.backgroundColor = bgcolor
if (symbol && symbol !== SCHEMA.iconUrl.default) { if (symbol && symbol !== U.SCHEMA.iconUrl.default) {
const icon = Icon.makeElement(symbol, colorBox) const icon = Icon.makeElement(symbol, colorBox)
Icon.setContrast(icon, colorBox, symbol, bgcolor) Icon.setContrast(icon, colorBox, symbol, bgcolor)
} else if (DomUtil.contrastedColor(colorBox, bgcolor)) { } else if (DomUtil.contrastedColor(colorBox, bgcolor)) {
@ -48,10 +58,10 @@ export default class Browser {
const viewFeature = (e) => { const viewFeature = (e) => {
feature.zoomTo({ ...e, callback: () => feature.view() }) feature.zoomTo({ ...e, callback: () => feature.view() })
} }
zoom.addEventListener('click', viewFeature) DomEvent.on(zoom_to, 'click', viewFeature)
label.addEventListener('click', viewFeature) DomEvent.on(title, 'click', viewFeature)
edit.addEventListener('click', () => feature.edit()) DomEvent.on(edit, 'click', feature.edit, feature)
remove.addEventListener('click', () => feature.del()) DomEvent.on(del, 'click', feature.del, feature)
// HOTFIX. Remove when this is released: // HOTFIX. Remove when this is released:
// https://github.com/Leaflet/Leaflet/pull/9052 // https://github.com/Leaflet/Leaflet/pull/9052
DomEvent.disableClickPropagation(row) DomEvent.disableClickPropagation(row)
@ -65,51 +75,45 @@ export default class Browser {
addDataLayer(datalayer, parent) { addDataLayer(datalayer, parent) {
let className = `datalayer ${datalayer.getHidableClass()}` let className = `datalayer ${datalayer.getHidableClass()}`
if (this.mode !== 'layers') className += ' show-list' if (this.mode !== 'layers') className += ' show-list'
const [container, { headline, toolbox, toggle, label }] = const container = DomUtil.create('div', className, parent)
Utils.loadTemplateWithRefs(` const headline = DomUtil.create('h5', '', container)
<div class="${className}" id="${this.datalayerId(datalayer)}"> container.id = this.datalayerId(datalayer)
<h5 data-ref=headline> const ul = DomUtil.create('ul', '', container)
<i class="icon icon-16 datalayer-toggle-list" data-ref=toggle></i>
<span data-ref=toolbox></span>
<span class="datalayer-name" data-id="${datalayer.id}" data-ref=label></span>
<span class="datalayer-counter"></span>
</h5>
<ul></ul>
</div>
`)
datalayer.renderToolbox(toolbox)
parent.appendChild(container)
const toggleList = () => parent.classList.toggle('show-list')
toggle.addEventListener('click', toggleList)
label.addEventListener('click', toggleList)
this.updateDatalayer(datalayer) this.updateDatalayer(datalayer)
} }
updateDatalayer(datalayer) { updateDatalayer(datalayer) {
// Compute once, but use it for each feature later. // Compute once, but use it for each feature later.
this.bounds = this._leafletMap.getBounds() this.bounds = this._leafletMap.getBounds()
const id = this.datalayerId(datalayer) const parent = DomUtil.get(this.datalayerId(datalayer))
const parent = document.getElementById(id)
// Panel is not open // Panel is not open
if (!parent) return if (!parent) return
parent.classList.toggle('off', !datalayer.isVisible()) parent.classList.toggle('off', !datalayer.isVisible())
const label = parent.querySelector('.datalayer-name')
const container = parent.querySelector('ul') const container = parent.querySelector('ul')
const headline = parent.querySelector('h5')
const toggleList = () => parent.classList.toggle('show-list')
headline.innerHTML = ''
const toggle = DomUtil.create('i', 'icon icon-16 datalayer-toggle-list', headline)
DomEvent.on(toggle, 'click', toggleList)
datalayer.renderToolbox(headline)
const name = DomUtil.create('span', 'datalayer-name', headline)
name.textContent = name.title = datalayer.options.name
DomEvent.on(name, 'click', toggleList)
container.innerHTML = '' container.innerHTML = ''
datalayer.eachFeature((feature) => this.addFeature(feature, container)) datalayer.eachFeature((feature) => this.addFeature(feature, container))
datalayer.propagate(['properties.name'])
const total = datalayer.count() const total = datalayer.count()
if (!total) return if (!total) return
const current = container.querySelectorAll('li').length const current = container.querySelectorAll('li').length
const count = total === current ? total : `${current}/${total}` const count = total === current ? total : `${current}/${total}`
const counter = parent.querySelector('.datalayer-counter') const counter = DomUtil.create('span', 'datalayer-counter', headline)
counter.textContent = `(${count})` counter.textContent = `(${count})`
counter.title = translate(`Features in this layer: ${count}`) counter.title = translate(`Features in this layer: ${count}`)
} }
toggleBadge() { toggleBadge() {
Utils.toggleBadge(this.filtersTitle, this.hasFilters()) U.Utils.toggleBadge(this.filtersTitle, this.hasFilters())
Utils.toggleBadge('.umap-control-browse', this.hasFilters()) U.Utils.toggleBadge('.umap-control-browse', this.hasFilters())
} }
onFormChange() { onFormChange() {
@ -153,51 +157,21 @@ export default class Browser {
open(mode) { open(mode) {
// Force only if mode is known, otherwise keep current mode. // Force only if mode is known, otherwise keep current mode.
if (mode) this.mode = mode if (mode) this.mode = mode
const template = ` const container = DomUtil.create('div')
<div>
<h3><i class="icon icon-16 icon-layers"></i>${translate('Data browser')}</h3>
<details class="filters" data-ref="details">
<summary data-ref=filtersTitle><i class="icon icon-16 icon-filters"></i>${translate('Filters')}</summary>
<fieldset>
<div data-ref=formContainer>
</div>
<button class="flat" type="button" data-ref=reset><i class="icon icon-16 icon-restore" title=""></i>${translate('Reset all')}</button>
</fieldset>
</details>
<div class="main-toolbox">
<i class="icon icon-16 icon-eye" title="${translate('show/hide all layers')}" data-ref="toggle"></i>
<i class="icon icon-16 icon-zoom" title="${translate('zoom to data extent')}" data-ref="fitBounds"></i>
<i class="icon icon-16 icon-download" title="${translate('download visible data')}" data-ref="download"></i>
</div>
<div data-ref=dataContainer></div>
</div>
`
const [
container,
{
details,
filtersTitle,
toggle,
fitBounds,
download,
dataContainer,
formContainer,
reset,
},
] = Utils.loadTemplateWithRefs(template)
// HOTFIX. Remove when this is released: // HOTFIX. Remove when this is released:
// https://github.com/Leaflet/Leaflet/pull/9052 // https://github.com/Leaflet/Leaflet/pull/9052
DomEvent.disableClickPropagation(container) DomEvent.disableClickPropagation(container)
details.open = this.mode === 'filters'
toggle.addEventListener('click', () => this.toggleLayers())
fitBounds.addEventListener('click', () => this._umap.fitDataBounds())
download.addEventListener('click', () => this.downloadVisible(download))
download.hidden = this._umap.getProperty('embedControl') === false
this.filtersTitle = filtersTitle DomUtil.createTitle(container, translate('Data browser'), 'icon-layers')
this.dataContainer = dataContainer this.formContainer = DomUtil.createFieldset(container, L._('Filters'), {
this.formContainer = formContainer on: this.mode === 'filters',
className: 'filters',
icon: 'icon-filters',
})
this.filtersTitle = container.querySelector('summary')
this.toggleBadge() this.toggleBadge()
this.addMainToolbox(container)
this.dataContainer = DomUtil.create('div', '', container)
let fields = [ let fields = [
[ [
@ -210,19 +184,27 @@ export default class Browser {
builder.on('set', () => this.onFormChange()) builder.on('set', () => this.onFormChange())
let filtersBuilder let filtersBuilder
this.formContainer.appendChild(builder.build()) this.formContainer.appendChild(builder.build())
builder.form.addEventListener('reset', () => { DomEvent.on(builder.form, 'reset', () => {
window.setTimeout(builder.syncAll.bind(builder)) window.setTimeout(builder.syncAll.bind(builder))
}) })
if (this._umap.properties.facetKey) { if (this._umap.properties.facetKey) {
fields = this._umap.facets.build() fields = this._umap.facets.build()
filtersBuilder = new Form(this._umap.facets, fields) filtersBuilder = new Form(this._umap.facets, fields)
filtersBuilder.on('set', () => this.onFormChange()) filtersBuilder.on('set', () => this.onFormChange())
filtersBuilder.form.addEventListener('reset', () => { DomEvent.on(filtersBuilder.form, 'reset', () => {
window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder)) window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder))
}) })
this.formContainer.appendChild(filtersBuilder.build()) this.formContainer.appendChild(filtersBuilder.build())
} }
reset.addEventListener('click', () => this.resetFilters()) const reset = DomUtil.createButton('flat', this.formContainer, '', () =>
this.resetFilters()
)
DomUtil.createIcon(reset, 'icon-restore')
DomUtil.element({
tagName: 'span',
parent: reset,
textContent: translate('Reset all'),
})
this._umap.panel.open({ this._umap.panel.open({
content: container, content: container,
@ -238,6 +220,21 @@ export default class Browser {
} }
} }
addMainToolbox(container) {
const [toolbox, { toggle, fitBounds, download }] = Utils.loadTemplateWithRefs(`
<div class="main-toolbox">
<i class="icon icon-16 icon-eye" title="${translate('show/hide all layers')}" data-ref="toggle"></i>
<i class="icon icon-16 icon-zoom" title="${translate('zoom to data extent')}" data-ref="fitBounds"></i>
<i class="icon icon-16 icon-download" title="${translate('download visible data')}" data-ref="download"></i>
</div>
`)
container.appendChild(toolbox)
toggle.addEventListener('click', () => this.toggleLayers())
fitBounds.addEventListener('click', () => this._umap.fitDataBounds())
download.addEventListener('click', () => this.downloadVisible(download))
download.hidden = this._umap.getProperty('embedControl') === false
}
downloadVisible(element) { downloadVisible(element) {
const menu = new ContextMenu({ fixed: true }) const menu = new ContextMenu({ fixed: true })
const items = [] const items = []
@ -268,13 +265,15 @@ export default class Browser {
} }
static backButton(umap) { static backButton(umap) {
const button = Utils.loadTemplate( const button = DomUtil.createButtonIcon(
`<button class="icon icon-16 icon-back" title="${translate('Back to browser')}"></button>` DomUtil.create('li', '', undefined),
'icon-back',
translate('Back to browser')
) )
// Fixme: remove me when this is merged and released // Fixme: remove me when this is merged and released
// https://github.com/Leaflet/Leaflet/pull/9052 // https://github.com/Leaflet/Leaflet/pull/9052
DomEvent.disableClickPropagation(button) DomEvent.disableClickPropagation(button)
button.addEventListener('click', () => umap.openBrowser()) DomEvent.on(button, 'click', () => umap.openBrowser())
return button return button
} }
} }

View file

@ -130,10 +130,6 @@ export class DataLayer {
} }
render(fields, builder) { render(fields, builder) {
// Propagate will remove the fields it has already
// processed
fields = this.propagate(fields)
const impacts = Utils.getImpactsFromSchema(fields) const impacts = Utils.getImpactsFromSchema(fields)
for (const impact of impacts) { for (const impact of impacts) {
@ -157,29 +153,6 @@ export class DataLayer {
} }
} }
// This method does a targeted update of the UI,
// it whould be merged with `render`` method and the
// SCHEMA at some point
propagate(fields = []) {
const impacts = {
'properties.name': () => {
Utils.eachElement('.datalayer-name', (el) => {
if (el.dataset.id === this.id) {
el.textContent = this.getName()
el.title = this.getName()
}
})
},
}
for (const [field, impact] of Object.entries(impacts)) {
if (!fields.length || fields.includes(field)) {
impact()
fields = fields.filter((item) => item !== field)
}
}
return fields
}
showAtLoad() { showAtLoad() {
return this.autoLoaded() && this.showAtZoom() return this.autoLoaded() && this.showAtZoom()
} }
@ -519,19 +492,8 @@ export class DataLayer {
const features = [] const features = []
this.sortFeatures(collection) this.sortFeatures(collection)
for (const featureJson of collection) { for (const featureJson of collection) {
if (featureJson.geometry?.type === 'GeometryCollection') { const feature = this.makeFeature(featureJson, sync)
for (const geometry of featureJson.geometry.geometries) { if (feature) features.push(feature)
const feature = this.makeFeature({
type: 'Feature',
geometry,
properties: featureJson.properties,
})
if (feature) features.push(feature)
}
} else {
const feature = this.makeFeature(featureJson, sync)
if (feature) features.push(feature)
}
} }
return features return features
} }
@ -683,7 +645,7 @@ export class DataLayer {
} }
edit() { edit() {
if (!this._umap.editEnabled) { if (!this._umap.editEnabled || !this.isLoaded()) {
return return
} }
const container = DomUtil.create('div', 'umap-layer-properties-container') const container = DomUtil.create('div', 'umap-layer-properties-container')
@ -1143,7 +1105,7 @@ export class DataLayer {
async save() { async save() {
if (this.isDeleted) return await this.saveDelete() if (this.isDeleted) return await this.saveDelete()
if (!this.isRemoteLayer() && !this.isLoaded()) return if (!this.isLoaded()) return
const geojson = this.umapGeoJSON() const geojson = this.umapGeoJSON()
const formData = new FormData() const formData = new FormData()
formData.append('name', this.options.name) formData.append('name', this.options.name)
@ -1184,9 +1146,6 @@ export class DataLayer {
await this._umap.saveAll() await this._umap.saveAll()
} }
) )
} else {
console.debug(error)
Alert.error(translate('Cannot save layer, please try again in a few minutes.'))
} }
} else { } else {
// Response contains geojson only if save has conflicted and conflicts have // Response contains geojson only if save has conflicted and conflicts have

View file

@ -1,251 +0,0 @@
import { Control } from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { translate } from '../i18n.js'
export const HomeControl = Control.extend({
options: {
position: 'topleft',
},
onAdd: (map) => {
const path = map._umap.getStaticPathFor('home.svg')
const container = Utils.loadTemplate(
`<a href="/" class="home-button" title="${translate('Back to home')}"><img src="${path}" alt="${translate('Home logo')}" width="38px" height="38px" /></a>`
)
return container
},
})
export const EditControl = Control.extend({
options: {
position: 'topright',
},
onAdd: (map) => {
const template = `
<div class="edit-enable">
<button type="button" data-ref="button">${translate('Edit')}</button>
</div>
`
const [container, { button }] = Utils.loadTemplateWithRefs(template)
button.addEventListener('click', () => map._umap.enableEdit())
button.addEventListener('mouseover', () => {
map._umap.tooltip.open({
content: map._umap.help.displayLabel('TOGGLE_EDIT'),
anchor: button,
position: 'bottom',
delay: 750,
duration: 5000,
})
})
return container
},
})
export const MoreControl = Control.extend({
options: {
position: 'topleft',
},
onAdd: function (map) {
const pos = this.getPosition()
const corner = map._controlCorners[pos]
const className = 'umap-more-controls'
const template = `
<div class="umap-control-text">
<button class="umap-control-more" type="button" data-ref="button"></button>
</div>
`
const [container, { button }] = Utils.loadTemplateWithRefs(template)
button.addEventListener('click', () => corner.classList.toggle(className))
button.addEventListener('mouseover', () => {
const extended = corner.classList.contains(className)
map._umap.tooltip.open({
content: extended ? translate('Hide controls') : translate('More controls'),
anchor: button,
position: 'right',
delay: 750,
})
})
return container
},
})
export const PermanentCreditsControl = Control.extend({
options: {
position: 'bottomleft',
},
onAdd: (map) => {
const container = Utils.loadTemplate(
`<div class="umap-permanent-credits-container text">${Utils.toHTML(map.options.permanentCredit)}</div>`
)
const background = map.options.permanentCreditBackground ? '#FFFFFFB0' : ''
container.style.backgroundColor = background
return container
},
})
const BaseButton = Control.extend({
initialize: function (umap, options) {
this._umap = umap
Control.prototype.initialize.call(this, options)
},
onAdd: function (map) {
const template = `
<div class="${this.options.className} umap-control">
<button type="button" title="${this.options.title}" data-ref="button"></button>
</div>
`
const [container, { button }] = Utils.loadTemplateWithRefs(template)
button.addEventListener('click', (event) => {
event.stopPropagation()
this.onClick()
})
button.addEventListener('dblclick', (event) => {
event.stopPropagation()
})
this.afterAdd(container)
return container
},
afterAdd: (container) => {},
})
export const DataLayersControl = BaseButton.extend({
options: {
position: 'topleft',
className: 'umap-control-browse',
title: translate('Open browser'),
},
afterAdd: function (container) {
Utils.toggleBadge(container, this._umap.browser?.hasFilters())
},
onClick: function () {
this._umap.openBrowser()
},
})
export const CaptionControl = BaseButton.extend({
options: {
position: 'topleft',
className: 'umap-control-caption',
title: translate('About'),
},
onClick: function () {
this._umap.openCaption()
},
})
export const EmbedControl = BaseButton.extend({
options: {
position: 'topleft',
title: translate('Share and download'),
className: 'leaflet-control-embed',
},
onClick: function () {
this._umap.share.open()
},
})
export const AttributionControl = Control.Attribution.extend({
options: {
prefix: '',
},
_update: function () {
// Layer is no more on the map
if (!this._map) return
Control.Attribution.prototype._update.call(this)
const shortCredit = this._map._umap.getProperty('shortCredit')
const captionMenus = this._map._umap.getProperty('captionMenus')
// Use our own container, so we can hide/show on small screens
const originalCredits = this._container.innerHTML
this._container.innerHTML = ''
const template = `
<div class="attribution-container">
${originalCredits}
<span data-ref="short"> ${Utils.toHTML(shortCredit)}</span>
<a href="#" data-ref="caption"> ${translate('Open caption')}</a>
<a href="/" data-ref="home"> ${translate('Home')}</a>
<a href="https://umap-project.org/" data-ref="site"> ${translate('Powered by uMap')}</a>
<a href="#" class="attribution-toggle"></a>
</div>
`
const [container, { short, caption, home, site }] =
Utils.loadTemplateWithRefs(template)
caption.addEventListener('click', () => this._map._umap.openCaption())
this._container.appendChild(container)
short.hidden = !shortCredit
caption.hidden = !captionMenus
site.hidden = !captionMenus
home.hidden = this._map._umap.isEmbed || !captionMenus
},
})
/* Used in edit mode to define the default tilelayer */
export const TileLayerChooser = BaseButton.extend({
options: {
position: 'topleft',
},
onClick: function () {
this.openSwitcher({ edit: true })
},
openSwitcher: function (options = {}) {
const template = `
<div class="umap-edit-tilelayers">
<h3><i class="icon icon-16 icon-tilelayer" title=""></i><span class="">${translate('Change tilelayers')}</span></h3>
<ul data-ref="tileContainer"></ul>
</div>
`
const [container, { tileContainer }] = Utils.loadTemplateWithRefs(template)
this.buildList(tileContainer, options)
const panel = options.edit ? this._umap.editPanel : this._umap.panel
panel.open({ content: container, highlight: 'tilelayers' })
},
buildList: function (container, options) {
this._umap._leafletMap.eachTileLayer((tilelayer) => {
const browserIsHttps = window.location.protocol === 'https:'
const tileLayerIsHttp = tilelayer.options.url_template.indexOf('http:') === 0
if (browserIsHttps && tileLayerIsHttp) return
container.appendChild(this.addTileLayerElement(tilelayer, options))
})
},
addTileLayerElement: function (tilelayer, options) {
const selectedClass = this._umap._leafletMap.hasLayer(tilelayer) ? 'selected' : ''
const src = Utils.template(
tilelayer.options.url_template,
this._umap._leafletMap.options.demoTileInfos
)
const template = `
<li>
<img src="${src}" loading="lazy" />
<div>${tilelayer.options.name}</div>
</li>
`
const li = Utils.loadTemplate(template)
li.addEventListener('click', () => {
const oldTileLayer = this._umap.properties.tilelayer
this._umap._leafletMap.selectTileLayer(tilelayer)
this._umap._leafletMap._controls.tilelayers.setLayers()
if (options?.edit) {
this._umap.properties.tilelayer = tilelayer.toJSON()
this._umap.sync.update(
'properties.tilelayer',
this._umap.properties.tilelayer,
oldTileLayer
)
}
})
return li
},
})

View file

@ -11,17 +11,6 @@ import {
import { uMapAlert as Alert } from '../../components/alerts/alert.js' import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import DropControl from '../drop.js' import DropControl from '../drop.js'
import { translate } from '../i18n.js' import { translate } from '../i18n.js'
import {
AttributionControl,
CaptionControl,
DataLayersControl,
EmbedControl,
EditControl,
HomeControl,
MoreControl,
PermanentCreditsControl,
TileLayerChooser,
} from './controls.js'
import * as Utils from '../utils.js' import * as Utils from '../utils.js'
import * as Icon from './icon.js' import * as Icon from './icon.js'
@ -51,15 +40,15 @@ const ControlsMixin = {
this._controls = {} this._controls = {}
if (this._umap.hasEditMode() && !this.options.noControl) { if (this._umap.hasEditMode() && !this.options.noControl) {
new EditControl(this).addTo(this) new U.EditControl(this).addTo(this)
} }
this._controls.home = new HomeControl(this._umap) this._controls.home = new U.HomeControl(this._umap)
this._controls.zoom = new Control.Zoom({ this._controls.zoom = new Control.Zoom({
zoomInTitle: translate('Zoom in'), zoomInTitle: translate('Zoom in'),
zoomOutTitle: translate('Zoom out'), zoomOutTitle: translate('Zoom out'),
}) })
this._controls.datalayers = new DataLayersControl(this._umap) this._controls.datalayers = new U.DataLayersControl(this._umap)
this._controls.caption = new CaptionControl(this._umap) this._controls.caption = new U.CaptionControl(this._umap)
this._controls.locate = new U.Locate(this, { this._controls.locate = new U.Locate(this, {
strings: { strings: {
title: translate('Center map on your location'), title: translate('Center map on your location'),
@ -80,8 +69,8 @@ const ControlsMixin = {
}, },
}) })
this._controls.search = new U.SearchControl() this._controls.search = new U.SearchControl()
this._controls.embed = new EmbedControl(this._umap) this._controls.embed = new Control.Embed(this._umap)
this._controls.tilelayersChooser = new TileLayerChooser(this._umap) this._controls.tilelayersChooser = new U.TileLayerChooser(this)
this._controls.editinosm = new Control.EditInOSM({ this._controls.editinosm = new Control.EditInOSM({
position: 'topleft', position: 'topleft',
widgetOptions: { widgetOptions: {
@ -91,9 +80,9 @@ const ControlsMixin = {
}, },
}) })
this._controls.measure = new L.MeasureControl().initHandler(this) this._controls.measure = new L.MeasureControl().initHandler(this)
this._controls.more = new MoreControl() this._controls.more = new U.MoreControls()
this._controls.scale = L.control.scale() this._controls.scale = L.control.scale()
this._controls.permanentCredit = new PermanentCreditsControl(this) this._controls.permanentCredit = new U.PermanentCreditsControl(this)
this._umap.drop = new DropControl(this._umap, this, this._container) this._umap.drop = new DropControl(this._umap, this, this._container)
this._controls.tilelayers = new U.TileLayerControl(this) this._controls.tilelayers = new U.TileLayerControl(this)
}, },
@ -104,7 +93,7 @@ const ControlsMixin = {
} }
if (this.options.noControl) return if (this.options.noControl) return
this._controls.attribution = new AttributionControl().addTo(this) this._controls.attribution = new U.AttributionControl().addTo(this)
if (this.options.miniMap) { if (this.options.miniMap) {
this.whenReady(function () { this.whenReady(function () {
if (this.selectedTilelayer) { if (this.selectedTilelayer) {

View file

@ -1,4 +1,4 @@
import { stamp } from '../../vendors/leaflet/leaflet-src.esm.js' import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { AutocompleteDatalist } from './autocomplete.js' import { AutocompleteDatalist } from './autocomplete.js'
import { MutatingForm } from './form/builder.js' import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js' import { translate } from './i18n.js'
@ -119,9 +119,10 @@ class Rule {
'options.smoothFactor', 'options.smoothFactor',
'options.dashArray', 'options.dashArray',
] ]
const container = DomUtil.create('div')
const builder = new MutatingForm(this, options) const builder = new MutatingForm(this, options)
const container = document.createElement('div') const defaultShapeProperties = DomUtil.add('div', '', container)
container.appendChild(builder.build()) defaultShapeProperties.appendChild(builder.build())
const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input) const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input)
const properties = this._umap.allProperties() const properties = this._umap.allProperties()
autocomplete.suggestions = properties autocomplete.suggestions = properties
@ -136,45 +137,43 @@ class Rule {
.map((str) => `${value}${str || ''}`) .map((str) => `${value}${str || ''}`)
} }
}) })
const backButton = Utils.loadTemplate(` this._umap.editPanel.open({ content: container, highlight: 'settings' })
<button class="flat" type="button" data-ref="add">
<i class="icon icon-16 icon-back" title="${translate('Back to list')}"></i>
</button>`)
backButton.addEventListener('click', () =>
this._umap.edit().then(() => {
this._umap.editPanel.container.querySelector('details#rules').open = true
})
)
this._umap.editPanel.open({
content: container,
highlight: 'settings',
actions: [backButton],
})
} }
renderToolbox(ul) { renderToolbox(row) {
const template = ` row.classList.toggle('off', !this.active)
<li data-id="${stamp(this)}" class="orderable"> const toggle = DomUtil.createButtonIcon(
<button class="icon icon-16 icon-eye" title="${translate('Toggle rule')}" data-ref=toggle></button> row,
<button class="icon icon-16 icon-edit show-on-edit" title="${translate('Edit')}" data-ref=edit></button> 'icon-eye',
<button class="icon icon-16 icon-delete show-on-edit" title="${translate('Delete rule')}" data-ref=remove></button> translate('Show/hide layer')
<span>${this.condition || translate('empty rule')}</span> )
<i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i> const edit = DomUtil.createButtonIcon(
</li> row,
` 'icon-edit show-on-edit',
const [li, { toggle, edit, remove }] = Utils.loadTemplateWithRefs(template) translate('Edit')
ul.appendChild(li) )
li.classList.toggle('off', !this.active) const remove = DomUtil.createButtonIcon(
edit.addEventListener('click', () => this.edit()) row,
remove.addEventListener('click', () => { 'icon-delete show-on-edit',
if (!confirm(translate('Are you sure you want to delete this rule?'))) return translate('Delete layer')
this._delete() )
this._umap.editPanel.close() DomEvent.on(edit, 'click', this.edit, this)
}) DomEvent.on(
toggle.addEventListener('click', () => { remove,
'click',
function () {
if (!confirm(translate('Are you sure you want to delete this rule?'))) return
this._delete()
this._umap.editPanel.close()
},
this
)
DomUtil.add('span', '', row, this.condition || translate('empty rule'))
DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
row.dataset.id = stamp(this)
DomEvent.on(toggle, 'click', () => {
this.active = !this.active this.active = !this.active
li.classList.toggle('off', !this.active) row.classList.toggle('off', !this.active)
this._umap.render(['rules']) this._umap.render(['rules'])
}) })
} }
@ -208,9 +207,8 @@ export default class Rules {
} }
onReorder(src, dst, initialIndex, finalIndex) { onReorder(src, dst, initialIndex, finalIndex) {
const oldRules = Utils.CopyJSON(this._umap.properties.rules || {}) const moved = this.rules.find((rule) => stamp(rule) === src.dataset.id)
const moved = this.rules.find((rule) => stamp(rule) === +src.dataset.id) const reference = this.rules.find((rule) => stamp(rule) === dst.dataset.id)
const reference = this.rules.find((rule) => stamp(rule) === +dst.dataset.id)
const movedIdx = this.rules.indexOf(moved) const movedIdx = this.rules.indexOf(moved)
let referenceIdx = this.rules.indexOf(reference) let referenceIdx = this.rules.indexOf(reference)
const minIndex = Math.min(movedIdx, referenceIdx) const minIndex = Math.min(movedIdx, referenceIdx)
@ -224,28 +222,20 @@ export default class Rules {
this.rules.splice(newIdx, 0, moved) this.rules.splice(newIdx, 0, moved)
this._umap.render(['rules']) this._umap.render(['rules'])
this.commit() this.commit()
this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
} }
edit(container) { edit(container) {
const template = ` const body = DomUtil.createFieldset(container, translate('Conditional style rules'))
<details id="rules">
<summary>${translate('Conditional style rules')}</summary>
<fieldset>
<ul data-ref=ul></ul>
<button class="umap-add" type="button" data-ref=add>${translate('Add rule')}</button>
</fieldset>
</details>
`
const [body, { ul, add }] = Utils.loadTemplateWithRefs(template)
if (this.rules.length) { if (this.rules.length) {
const ul = DomUtil.create('ul', '', body)
for (const rule of this.rules) { for (const rule of this.rules) {
rule.renderToolbox(ul) rule.renderToolbox(DomUtil.create('li', 'orderable', ul))
} }
const orderable = new Orderable(ul, this.onReorder.bind(this)) const orderable = new Orderable(ul, this.onReorder.bind(this))
} }
add.addEventListener('click', () => this.addRule())
container.appendChild(body) DomUtil.createButton('umap-add', body, translate('Add rule'), this.addRule, this)
} }
addRule() { addRule() {

View file

@ -225,14 +225,13 @@ export class SyncEngine {
} }
for (const [obj, operations] of needSave.entries()) { for (const [obj, operations] of needSave.entries()) {
const ok = await obj.save() const ok = await obj.save()
if (!ok) return false if (!ok) break
for (const operation of operations) { for (const operation of operations) {
operation.dirty = false operation.dirty = false
} }
} }
this.saved() this.saved()
this._undoManager.toggleState() this._undoManager.toggleState()
return true
} }
saved() { saved() {

View file

@ -4,8 +4,6 @@ export class Positioned {
this.anchorTop(anchor) this.anchorTop(anchor)
} else if (anchor && position === 'bottom') { } else if (anchor && position === 'bottom') {
this.anchorBottom(anchor) this.anchorBottom(anchor)
} else if (anchor && position === 'right') {
this.anchorRight(anchor)
} else { } else {
this.anchorAbsolute() this.anchorAbsolute()
} }
@ -14,7 +12,6 @@ export class Positioned {
toggleClassPosition(position) { toggleClassPosition(position) {
this.container.classList.toggle('tooltip-bottom', position === 'bottom') this.container.classList.toggle('tooltip-bottom', position === 'bottom')
this.container.classList.toggle('tooltip-top', position === 'top') this.container.classList.toggle('tooltip-top', position === 'top')
this.container.classList.toggle('tooltip-right', position === 'right')
} }
anchorTop(el) { anchorTop(el) {
@ -36,16 +33,6 @@ export class Positioned {
}) })
} }
anchorRight(el) {
this.toggleClassPosition('right')
const coords = this.getPosition(el)
console.log(coords)
this.setPosition({
left: coords.right + 11,
top: coords.top,
})
}
anchorAbsolute() { anchorAbsolute() {
const left = const left =
this.parent.offsetLeft + this.parent.offsetLeft +

View file

@ -107,7 +107,7 @@ export default class Umap {
if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema) if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema)
// Do not display in an iframe. // Do not display in an iframe.
if (this.isEmbed) { if (window.self !== window.top) {
this.properties.homeControl = false this.properties.homeControl = false
} }
@ -258,10 +258,6 @@ export default class Umap {
} }
} }
get isEmbed() {
return window.self !== window.top
}
setPropertiesFromQueryString() { setPropertiesFromQueryString() {
const asBoolean = (key) => { const asBoolean = (key) => {
const value = this.searchParams.get(key) const value = this.searchParams.get(key)
@ -695,8 +691,7 @@ export default class Umap {
if (!this.isDirty) return if (!this.isDirty) return
if (this._defaultExtent) this._setCenterAndZoom() if (this._defaultExtent) this._setCenterAndZoom()
this.backup() this.backup()
const status = await this.sync.save() await this.sync.save()
if (!status) return
// Do a blind render for now, as we are not sure what could // Do a blind render for now, as we are not sure what could
// have changed, we'll be more subtil when we'll remove the // have changed, we'll be more subtil when we'll remove the
// save action // save action
@ -1186,7 +1181,7 @@ export default class Umap {
} }
this._advancedActions(container) this._advancedActions(container)
return this.editPanel.open({ this.editPanel.open({
content: container, content: container,
className: 'dark', className: 'dark',
highlight: 'settings', highlight: 'settings',
@ -1518,17 +1513,12 @@ export default class Umap {
editDatalayers() { editDatalayers() {
if (!this.editEnabled) return if (!this.editEnabled) return
const template = ` const container = DomUtil.create('div')
<div> DomUtil.createTitle(container, translate('Manage layers'), 'icon-layers')
<h3><i class="icon icon-16 icon-layers"></i>${translate('Manage layers')}</h3> const ul = DomUtil.create('ul', '', container)
<ul data-ref=ul></ul>
</div>
`
const [container, { ul }] = Utils.loadTemplateWithRefs(template)
this.eachDataLayerReverse((datalayer) => { this.eachDataLayerReverse((datalayer) => {
const row = Utils.loadTemplate( const row = DomUtil.create('li', 'orderable', ul)
`<li class="orderable"><i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i></li>` DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
)
datalayer.renderToolbox(row) datalayer.renderToolbox(row)
const builder = new MutatingForm( const builder = new MutatingForm(
datalayer, datalayer,
@ -1539,7 +1529,6 @@ export default class Umap {
row.appendChild(form) row.appendChild(form)
row.classList.toggle('off', !datalayer.isVisible()) row.classList.toggle('off', !datalayer.isVisible())
row.dataset.id = datalayer.id row.dataset.id = datalayer.id
ul.appendChild(row)
}) })
const onReorder = (src, dst, initialIndex, finalIndex) => { const onReorder = (src, dst, initialIndex, finalIndex) => {
const movedLayer = this.datalayers[src.dataset.id] const movedLayer = this.datalayers[src.dataset.id]

View file

@ -1,3 +1,183 @@
U.HomeControl = L.Control.extend({
options: {
position: 'topleft',
},
onAdd: (map) => {
const path = map._umap.getStaticPathFor('home.svg')
const container = U.Utils.loadTemplate(
`<a href="/" class="home-button" title="${L._('Back to home')}"><img src="${path}" alt="${L._('Home logo')}" width="38px" height="38px" /></a>`
)
return container
},
})
U.EditControl = L.Control.extend({
options: {
position: 'topright',
},
onAdd: function (map) {
const container = L.DomUtil.create('div', 'edit-enable')
const enableEditing = L.DomUtil.createButton(
'',
container,
L._('Edit'),
map._umap.enableEdit,
map._umap
)
L.DomEvent.on(
enableEditing,
'mouseover',
() => {
map._umap.tooltip.open({
content: map._umap.help.displayLabel('TOGGLE_EDIT'),
anchor: enableEditing,
position: 'bottom',
delay: 750,
duration: 5000,
})
},
this
)
return container
},
})
U.MoreControls = L.Control.extend({
options: {
position: 'topleft',
},
onAdd: function () {
const container = L.DomUtil.create('div', 'umap-control-text')
const moreButton = L.DomUtil.createButton(
'umap-control-more',
container,
L._('More controls'),
this.toggle,
this
)
const lessButton = L.DomUtil.createButton(
'umap-control-less',
container,
L._('Hide controls'),
this.toggle,
this
)
return container
},
toggle: function () {
const pos = this.getPosition()
const corner = this._map._controlCorners[pos]
const className = 'umap-more-controls'
if (L.DomUtil.hasClass(corner, className)) L.DomUtil.removeClass(corner, className)
else L.DomUtil.addClass(corner, className)
},
})
U.PermanentCreditsControl = L.Control.extend({
options: {
position: 'bottomleft',
},
initialize: function (map, options) {
this.map = map
L.Control.prototype.initialize.call(this, options)
},
onAdd: function () {
this.paragraphContainer = L.DomUtil.create(
'div',
'umap-permanent-credits-container text'
)
this.setCredits()
this.setBackground()
return this.paragraphContainer
},
setCredits: function () {
this.paragraphContainer.innerHTML = U.Utils.toHTML(this.map.options.permanentCredit)
},
setBackground: function () {
if (this.map.options.permanentCreditBackground) {
this.paragraphContainer.style.backgroundColor = '#FFFFFFB0'
} else {
this.paragraphContainer.style.backgroundColor = ''
}
},
})
L.Control.Button = L.Control.extend({
initialize: function (umap, options) {
this._umap = umap
L.Control.prototype.initialize.call(this, options)
},
getClassName: function () {
return this.options.className
},
onAdd: function (map) {
const container = L.DomUtil.create('div', `${this.getClassName()} umap-control`)
const button = L.DomUtil.createButton(
'',
container,
this.options.title,
this.onClick,
this
)
L.DomEvent.on(button, 'dblclick', L.DomEvent.stopPropagation)
this.afterAdd(container)
return container
},
afterAdd: (container) => {},
})
U.DataLayersControl = L.Control.Button.extend({
options: {
position: 'topleft',
className: 'umap-control-browse',
title: L._('Open browser'),
},
afterAdd: function (container) {
U.Utils.toggleBadge(container, this._umap.browser?.hasFilters())
},
onClick: function () {
this._umap.openBrowser()
},
})
U.CaptionControl = L.Control.Button.extend({
options: {
position: 'topleft',
className: 'umap-control-caption',
title: L._('About'),
},
onClick: function () {
this._umap.openCaption()
},
})
L.Control.Embed = L.Control.Button.extend({
options: {
position: 'topleft',
title: L._('Share and download'),
className: 'leaflet-control-embed umap-control',
},
onClick: function () {
this._umap.share.open()
},
})
/* Used in view mode to define the current tilelayer */ /* Used in view mode to define the current tilelayer */
U.TileLayerControl = L.Control.IconLayers.extend({ U.TileLayerControl = L.Control.IconLayers.extend({
initialize: function (map, options) { initialize: function (map, options) {
@ -58,6 +238,128 @@ U.TileLayerControl = L.Control.IconLayers.extend({
}, },
}) })
/* Used in edit mode to define the default tilelayer */
U.TileLayerChooser = L.Control.extend({
options: {
position: 'topleft',
},
initialize: function (map, options = {}) {
this.map = map
L.Control.prototype.initialize.call(this, options)
},
onAdd: function () {
const container = L.DomUtil.create('div', 'leaflet-control-tilelayers umap-control')
const changeMapBackgroundButton = L.DomUtil.createButton(
'',
container,
L._('Change map background'),
this.openSwitcher,
this
)
L.DomEvent.on(changeMapBackgroundButton, 'dblclick', L.DomEvent.stopPropagation)
return container
},
openSwitcher: function (options = {}) {
const container = L.DomUtil.create('div', 'umap-edit-tilelayers')
L.DomUtil.createTitle(container, L._('Change tilelayers'), 'icon-tilelayer')
this._tilelayers_container = L.DomUtil.create('ul', '', container)
this.buildList(options)
const panel = options.edit ? this.map._umap.editPanel : this.map._umap.panel
panel.open({ content: container, highlight: 'tilelayers' })
},
buildList: function (options) {
this.map.eachTileLayer(function (tilelayer) {
if (
window.location.protocol === 'https:' &&
tilelayer.options.url_template.indexOf('http:') === 0
)
return
this.addTileLayerElement(tilelayer, options)
}, this)
},
addTileLayerElement: function (tilelayer, options) {
const selectedClass = this.map.hasLayer(tilelayer) ? 'selected' : ''
const el = L.DomUtil.create('li', selectedClass, this._tilelayers_container)
const img = L.DomUtil.create('img', '', el)
const name = L.DomUtil.create('div', '', el)
img.src = U.Utils.template(
tilelayer.options.url_template,
this.map.options.demoTileInfos
)
img.loading = 'lazy'
name.textContent = tilelayer.options.name
L.DomEvent.on(
el,
'click',
() => {
const oldTileLayer = this.map._umap.properties.tilelayer
this.map.selectTileLayer(tilelayer)
this.map._controls.tilelayers.setLayers()
if (options?.edit) {
this.map._umap.properties.tilelayer = tilelayer.toJSON()
this.map._umap.isDirty = true
this.map._umap.sync.update(
'properties.tilelayer',
this.map._umap.properties.tilelayer,
oldTileLayer
)
}
},
this
)
},
})
U.AttributionControl = L.Control.Attribution.extend({
options: {
prefix: '',
},
_update: function () {
// Layer is no more on the map
if (!this._map) return
L.Control.Attribution.prototype._update.call(this)
// Use our own container, so we can hide/show on small screens
const credits = this._container.innerHTML
this._container.innerHTML = ''
const container = L.DomUtil.create('div', 'attribution-container', this._container)
container.innerHTML = credits
const shortCredit = this._map._umap.getProperty('shortCredit')
const captionMenus = this._map._umap.getProperty('captionMenus')
if (shortCredit) {
L.DomUtil.element({
tagName: 'span',
parent: container,
safeHTML: `${U.Utils.toHTML(shortCredit)}`,
})
}
if (captionMenus) {
const link = L.DomUtil.add('a', '', container, `${L._('Open caption')}`)
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', () => this._map._umap.openCaption())
.on(link, 'dblclick', L.DomEvent.stop)
}
if (window.top === window.self && captionMenus) {
// We are not in iframe mode
L.DomUtil.createLink('', container, `${L._('Home')}`, '/')
}
if (captionMenus) {
L.DomUtil.createLink(
'',
container,
`${L._('Powered by uMap')}`,
'https://umap-project.org/'
)
}
L.DomUtil.createLink('attribution-toggle', this._container, '')
},
})
/* /*
* Take control over L.Control.Locate to be able to * Take control over L.Control.Locate to be able to
* call start() before adding the control (and thus the button) to the map. * call start() before adding the control (and thus the button) to the map.

View file

@ -65,6 +65,18 @@ L.DomUtil.createButton = (className, container, content, callback, context) => {
return el return el
} }
L.DomUtil.createLink = (className, container, content, url, target, title) => {
const el = L.DomUtil.add('a', className, container, content)
el.href = url
if (target) {
el.target = target
}
if (title) {
el.title = title
}
return el
}
L.DomUtil.createIcon = (parent, className, title, size = 16) => { L.DomUtil.createIcon = (parent, className, title, size = 16) => {
return L.DomUtil.element({ return L.DomUtil.element({
tagName: 'i', tagName: 'i',
@ -128,6 +140,16 @@ L.DomUtil.element = ({ tagName, parent, ...attrs }) => {
return el return el
} }
L.DomUtil.before = (target, el) => {
target.parentNode.insertBefore(el, target)
return el
}
L.DomUtil.after = (target, el) => {
target.parentNode.insertBefore(el, target.nextSibling)
return el
}
// From https://gist.github.com/Accudio/b9cb16e0e3df858cef0d31e38f1fe46f // From https://gist.github.com/Accudio/b9cb16e0e3df858cef0d31e38f1fe46f
// convert colour in range 0-255 to the modifier used within luminance calculation // convert colour in range 0-255 to the modifier used within luminance calculation
L.DomUtil.colourMod = (colour) => { L.DomUtil.colourMod = (colour) => {
@ -192,6 +214,24 @@ L.DomUtil.contrastedColor = (el, bgcolor) => {
if (bgcolor) _CACHE_CONSTRAST[bgcolor] = out if (bgcolor) _CACHE_CONSTRAST[bgcolor] = out
return out return out
} }
L.DomEvent.once = (el, types, fn, context) => {
// cf https://github.com/Leaflet/Leaflet/pull/3528#issuecomment-134551575
if (typeof types === 'object') {
for (const type in types) {
L.DomEvent.once(el, type, types[type], fn)
}
return L.DomEvent
}
const handler = L.bind(() => {
L.DomEvent.off(el, types, fn, context).off(el, types, handler, context)
}, L.DomEvent)
// add a listener that's executed once and removed after that
return L.DomEvent.on(el, types, fn, context).on(el, types, handler, context)
}
L.LatLng.prototype.isValid = function () { L.LatLng.prototype.isValid = function () {
return ( return (
Number.isFinite(this.lat) && Number.isFinite(this.lat) &&

View file

@ -96,20 +96,27 @@ html[dir="rtl"] .leaflet-tooltip-pane > * {
background-color: white; background-color: white;
min-height: initial; min-height: initial;
} }
.leaflet-control.display-on-more { .leaflet-control.display-on-more,
.umap-control-less {
display: none; display: none;
} }
.umap-control-more { .umap-control-more,
.umap-control-less {
background-image: url('./img/24-white.svg'); background-image: url('./img/24-white.svg');
background-position: -72px -402px; background-position: -72px -402px;
text-indent: -9999px;
margin-bottom: 0; margin-bottom: 0;
} }
.umap-more-controls .umap-control-more { .umap-control-less {
background-position: -108px -402px; background-position: -108px -402px;
} }
.umap-more-controls .display-on-more { .umap-more-controls .display-on-more,
.umap-more-controls .umap-control-less {
display: block; display: block;
} }
.umap-more-controls .umap-control-more {
display: none;
}
.leaflet-control-embed [type="button"] { .leaflet-control-embed [type="button"] {
background-position: 0 -180px; background-position: 0 -180px;
} }

View file

@ -465,19 +465,19 @@ def test_main_toolbox_toggle_all_layers(live_server, map, page):
expect(page.locator(".datalayer.off")).to_have_count(1) expect(page.locator(".datalayer.off")).to_have_count(1)
# Click on button # Click on button
page.locator(".umap-browser").get_by_title("Show/hide all layers").click() page.locator(".umap-browser [data-ref=toggle]").click()
# Should have hidden the two other layers # Should have hidden the two other layers
expect(page.locator(".datalayer.off")).to_have_count(3) expect(page.locator(".datalayer.off")).to_have_count(3)
expect(markers).to_have_count(0) expect(markers).to_have_count(0)
# Click again # Click again
page.locator(".umap-browser").get_by_title("Show/hide all layers").click() page.locator(".umap-browser [data-ref=toggle]").click()
# Should shown all layers # Should shown all layers
expect(page.locator(".datalayer.off")).to_have_count(0) expect(page.locator(".datalayer.off")).to_have_count(0)
expect(markers).to_have_count(3) expect(markers).to_have_count(3)
# Click again # Click again
page.locator(".umap-browser").get_by_title("Show/hide all layers").click() page.locator(".umap-browser [data-ref=toggle]").click()
# Should hidden again all layers # Should hidden again all layers
expect(page.locator(".datalayer.off")).to_have_count(3) expect(page.locator(".datalayer.off")).to_have_count(3)
expect(markers).to_have_count(0) expect(markers).to_have_count(0)

View file

@ -281,10 +281,10 @@ def test_can_deactive_rule_from_list(live_server, page, openmap):
page.get_by_role("button", name="Edit").click() page.get_by_role("button", name="Edit").click()
page.get_by_role("button", name="Map advanced properties").click() page.get_by_role("button", name="Map advanced properties").click()
page.get_by_text("Conditional style rules").click() page.get_by_text("Conditional style rules").click()
page.get_by_role("button", name="Toggle rule").click() page.get_by_role("button", name="Show/hide layer").click()
colors = getColors(markers) colors = getColors(markers)
assert colors.count("rgb(240, 248, 255)") == 0 assert colors.count("rgb(240, 248, 255)") == 0
page.get_by_role("button", name="Toggle rule").click() page.get_by_role("button", name="Show/hide layer").click()
colors = getColors(markers) colors = getColors(markers)
assert colors.count("rgb(240, 248, 255)") == 3 assert colors.count("rgb(240, 248, 255)") == 3

View file

@ -180,9 +180,9 @@ def test_sortkey_impacts_datalayerindex(map, live_server, page):
first_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(0) first_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(0)
second_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(1) second_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(1)
third_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(2) third_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(2)
assert "X Third" == first_listed_feature.text_content().strip() assert "X Third" == first_listed_feature.text_content()
assert "Y Second" == second_listed_feature.text_content().strip() assert "Y Second" == second_listed_feature.text_content()
assert "Z First" == third_listed_feature.text_content().strip() assert "Z First" == third_listed_feature.text_content()
# Change the default sortkey to be "key" # Change the default sortkey to be "key"
page.get_by_role("button", name="Edit").click() page.get_by_role("button", name="Edit").click()
@ -201,9 +201,9 @@ def test_sortkey_impacts_datalayerindex(map, live_server, page):
first_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(0) first_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(0)
second_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(1) second_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(1)
third_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(2) third_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(2)
assert "Z First" == first_listed_feature.text_content().strip() assert "Z First" == first_listed_feature.text_content()
assert "Y Second" == second_listed_feature.text_content().strip() assert "Y Second" == second_listed_feature.text_content()
assert "X Third" == third_listed_feature.text_content().strip() assert "X Third" == third_listed_feature.text_content()
def test_hover_tooltip_setting_should_be_persistent(live_server, map, page): def test_hover_tooltip_setting_should_be_persistent(live_server, map, page):

View file

@ -435,63 +435,6 @@ def test_import_geometry_collection(live_server, page, tilelayer):
expect(paths).to_have_count(2) expect(paths).to_have_count(2)
def test_import_geometry_collection_in_feature(live_server, page, tilelayer):
data = {
"type": "Feature",
"properties": {"name": "foobar"},
"geometry": {
"type": "GeometryCollection",
"geometries": [
{"type": "Point", "coordinates": [-80.6608, 35.0493]},
{
"type": "Polygon",
"coordinates": [
[
[-80.6645, 35.0449],
[-80.6634, 35.0460],
[-80.6625, 35.0455],
[-80.6638, 35.0442],
[-80.6645, 35.0449],
]
],
},
{
"type": "LineString",
"coordinates": [
[-80.66237, 35.05950],
[-80.66269, 35.05926],
[-80.66284, 35.05893],
[-80.66308, 35.05833],
[-80.66385, 35.04387],
[-80.66303, 35.04371],
],
},
],
},
}
page.goto(f"{live_server.url}/map/new/")
page.get_by_title("Open browser").click()
layers = page.locator(".umap-browser .datalayer")
markers = page.locator(".leaflet-marker-icon")
paths = page.locator("path")
expect(markers).to_have_count(0)
expect(paths).to_have_count(0)
expect(layers).to_have_count(0)
button = page.get_by_title("Import data")
expect(button).to_be_visible()
button.click()
textarea = page.locator(".umap-import textarea")
textarea.fill(json.dumps(data))
page.locator('select[name="format"]').select_option("geojson")
page.get_by_role("button", name="Import data", exact=True).click()
# A layer has been created
expect(layers).to_have_count(1)
expect(markers).to_have_count(1)
expect(paths).to_have_count(2)
# Geometries are treated as separate features.
expect(page.get_by_text("foobar")).to_have_count(3)
def test_import_multipolygon(live_server, page, tilelayer): def test_import_multipolygon(live_server, page, tilelayer):
data = { data = {
"type": "Feature", "type": "Feature",

View file

@ -63,5 +63,5 @@ def test_zoom_control(map, live_server, datalayer, page):
expect(control).to_be_visible() expect(control).to_be_visible()
page.goto(f"{live_server.url}{map.get_absolute_url()}?zoomControl=null") page.goto(f"{live_server.url}{map.get_absolute_url()}?zoomControl=null")
expect(control).to_be_hidden() expect(control).to_be_hidden()
page.locator(".umap-control-more").click() page.get_by_title("More controls").click()
expect(control).to_be_visible() expect(control).to_be_visible()