diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js
index 7b2e25b5..54010818 100644
--- a/umap/static/umap/js/modules/data/features.js
+++ b/umap/static/umap/js/modules/data/features.js
@@ -10,6 +10,7 @@ import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from '../rendering/ui.js'
+import loadPopup from '../rendering/popup.js'
class Feature {
constructor(datalayer, geojson = {}, id = null) {
@@ -291,7 +292,7 @@ class Feature {
getPopupClass() {
const old = this.getOption('popupTemplate') // Retrocompat.
- return U.Popup[this.getOption('popupShape') || old] || U.Popup
+ return loadPopup(this.getOption('popupShape') || old)
}
attachPopup() {
diff --git a/umap/static/umap/js/modules/rendering/popup.js b/umap/static/umap/js/modules/rendering/popup.js
new file mode 100644
index 00000000..dc1e460a
--- /dev/null
+++ b/umap/static/umap/js/modules/rendering/popup.js
@@ -0,0 +1,99 @@
+import {
+ DomEvent,
+ DomUtil,
+ Path,
+ Popup as BasePopup,
+} from '../../../vendors/leaflet/leaflet-src.esm.js'
+import loadTemplate from './template.js'
+import Browser from '../browser.js'
+
+export default function loadPopup(name) {
+ switch (name) {
+ case 'Large':
+ return PopupLarge
+ case 'Panel':
+ return Panel
+ default:
+ return Popup
+ }
+}
+
+const Popup = BasePopup.extend({
+ initialize: function (feature) {
+ this.feature = feature
+ this.container = DomUtil.create('div', 'umap-popup')
+ this.format()
+ BasePopup.prototype.initialize.call(this, {}, feature)
+ this.setContent(this.container)
+ },
+
+ format: function () {
+ const name = this.feature.getOption('popupTemplate')
+ this.content = loadTemplate(name, this.feature, this.container)
+ const elements = this.container.querySelectorAll('img,iframe')
+ for (const element of elements) {
+ this.onElementLoaded(element)
+ }
+ if (!elements.length && this.container.textContent.replace('\n', '') === '') {
+ this.container.innerHTML = ''
+ DomUtil.add('h3', '', this.container, this.feature.getDisplayName())
+ }
+ },
+
+ onElementLoaded: function (el) {
+ DomEvent.on(el, 'load', () => {
+ this._updateLayout()
+ this._updatePosition()
+ this._adjustPan()
+ })
+ },
+})
+
+const PopupLarge = Popup.extend({
+ options: {
+ maxWidth: 500,
+ className: 'umap-popup-large',
+ },
+})
+
+const Panel = Popup.extend({
+ options: {
+ zoomAnimation: false,
+ },
+
+ onAdd: function (map) {
+ map.panel.setDefaultMode('expanded')
+ map.panel.open({
+ content: this._content,
+ actions: [Browser.backButton(map)],
+ })
+
+ // fire events as in base class Popup.js:onAdd
+ map.fire('popupopen', { popup: this })
+ if (this._source) {
+ this._source.fire('popupopen', { popup: this }, true)
+ if (!(this._source instanceof Path)) {
+ this._source.on('preclick', DomEvent.stopPropagation)
+ }
+ }
+ },
+
+ onRemove: function (map) {
+ map.panel.close()
+
+ // fire events as in base class Popup.js:onRemove
+ map.fire('popupclose', { popup: this })
+ if (this._source) {
+ this._source.fire('popupclose', { popup: this }, true)
+ if (!(this._source instanceof Path)) {
+ this._source.off('preclick', DomEvent.stopPropagation)
+ }
+ }
+ },
+
+ update: () => {},
+ _updateLayout: () => {},
+ _updatePosition: () => {},
+ _adjustPan: () => {},
+ _animateZoom: () => {},
+})
diff --git a/umap/static/umap/js/modules/rendering/template.js b/umap/static/umap/js/modules/rendering/template.js
new file mode 100644
index 00000000..cb032dc1
--- /dev/null
+++ b/umap/static/umap/js/modules/rendering/template.js
@@ -0,0 +1,214 @@
+import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
+import { translate, getLocale } from '../i18n.js'
+import * as Utils from '../utils.js'
+
+export default function loadTemplate(name, feature, container) {
+ let klass = PopupTemplate
+ switch (name) {
+ case 'GeoRSSLink':
+ klass = GeoRSSLink
+ break
+ case 'GeoRSSImage':
+ klass = GeoRSSImage
+ break
+ case 'Table':
+ klass = Table
+ break
+ case 'OSM':
+ klass = OSM
+ break
+ }
+ const content = new klass()
+ return content.render(feature, container)
+}
+
+class PopupTemplate {
+ renderTitle(feature, container) {}
+
+ renderBody(feature, container) {
+ const template = feature.getOption('popupContentTemplate')
+ const target = feature.getOption('outlinkTarget')
+ const properties = feature.extendedProperties()
+ // Resolve properties inside description
+ properties.description = Utils.greedyTemplate(
+ feature.properties.description || '',
+ properties
+ )
+ let content = Utils.greedyTemplate(template, properties)
+ content = Utils.toHTML(content, { target: target })
+ return Utils.loadTemplate(`
`)
+ }
+
+ renderFooter(feature, container) {
+ if (feature.hasPopupFooter()) {
+ const template = `
+ `
+ const [footer, { previous, zoom, next }] = Utils.loadTemplateWithRefs(template)
+ const nextFeature = feature.getNext()
+ const previousFeature = feature.getPrevious()
+ // Fixme: remove me when this is merged and released
+ // https://github.com/Leaflet/Leaflet/pull/9052
+ DomEvent.disableClickPropagation(footer)
+ if (nextFeature) {
+ next.title = translate('Go to «{feature}»', {
+ feature: nextFeature.properties.name || translate('next'),
+ })
+ DomEvent.on(next, 'click', () => {
+ nextFeature.zoomTo({ callback: nextFeature.view })
+ })
+ }
+ if (previousFeature) {
+ previous.title = translate('Go to «{feature}»', {
+ feature: previousFeature.properties.name || translate('previous'),
+ })
+ DomEvent.on(previous, 'click', () => {
+ previousFeature.zoomTo({ callback: previousFeature.view })
+ })
+ }
+ DomEvent.on(zoom, 'click', () => feature.zoomTo())
+ container.appendChild(footer)
+ }
+ }
+
+ render(feature, container) {
+ const title = this.renderTitle(feature, container)
+ if (title) container.appendChild(title)
+ const body = this.renderBody(feature, container)
+ if (body) DomUtil.add('div', 'umap-popup-content', container, body)
+ this.renderFooter(feature, container)
+ }
+}
+export const TitleMixin = (Base) =>
+ class extends Base {
+ renderTitle(feature, container) {
+ const title = feature.getDisplayName()
+ if (title) {
+ return Utils.loadTemplate(``)
+ }
+ }
+ }
+
+class Table extends TitleMixin(PopupTemplate) {
+ getValue(feature, key) {
+ // TODO, manage links (url, mailto, wikipedia...)
+ const value = Utils.escapeHTML(feature.properties[key]).trim()
+ if (value.indexOf('http') === 0) {
+ return `${value}`
+ }
+ return value
+ }
+
+ makeRow(feature, key) {
+ return Utils.loadTemplate(
+ `${key} | ${this.getValue(feature, key)} |
`
+ )
+ }
+
+ renderBody(feature, container) {
+ const table = document.createElement('table')
+
+ for (const key in feature.properties) {
+ if (typeof feature.properties[key] === 'object' || key === 'name') continue
+ table.appendChild(this.makeRow(feature, key))
+ }
+ return table
+ }
+}
+
+class GeoRSSImage extends TitleMixin(PopupTemplate) {
+ renderBody(feature, container) {
+ const body = DomUtil.create('a')
+ body.href = feature.properties.link
+ body.target = '_blank'
+ if (feature.properties.img) {
+ const img = DomUtil.create('img', '', body)
+ img.src = feature.properties.img
+ // Sadly, we are unable to override this from JS the clean way
+ // See https://github.com/Leaflet/Leaflet/commit/61d746818b99d362108545c151a27f09d60960ee#commitcomment-6061847
+ img.style.maxWidth = '500px'
+ img.style.maxHeight = '500px'
+ }
+ return body
+ }
+}
+
+class GeoRSSLink extends TitleMixin(PopupTemplate) {
+ renderBody(feature, container) {
+ const title = this.renderTitle(feature, container)
+ return Utils.loadTemplate(
+ `${title}`
+ )
+ }
+}
+
+class OSM extends TitleMixin(PopupTemplate) {
+ getName(feature) {
+ const props = feature.properties
+ const locale = getLocale()
+ if (locale && props[`name:${locale}`]) return props[`name:${locale}`]
+ return props.name
+ }
+
+ renderBody(feature, container) {
+ const props = feature.properties
+ const body = document.createElement('div')
+ const title = DomUtil.add('h3', 'popup-title', container)
+ const color = feature.getPreviewColor()
+ title.style.backgroundColor = color
+ const iconUrl = feature.getDynamicOption('iconUrl')
+ const icon = U.Icon.makeIconElement(iconUrl, title)
+ DomUtil.addClass(icon, 'icon')
+ U.Icon.setIconContrast(icon, title, iconUrl, color)
+ if (DomUtil.contrastedColor(title, color)) title.style.color = 'white'
+ DomUtil.add('span', '', title, this.getName(feature))
+ const street = props['addr:street']
+ if (street) {
+ const row = DomUtil.add('address', 'address', body)
+ const number = props['addr:housenumber']
+ if (number) {
+ // Poor way to deal with international forms of writting addresses
+ DomUtil.add('span', '', row, `${translate('No.')}: ${number}`)
+ DomUtil.add('span', '', row, `${translate('Street')}: ${street}`)
+ } else {
+ DomUtil.add('span', '', row, street)
+ }
+ }
+ if (props.website) {
+ body.appendChild(
+ Utils.loadTemplate(``)
+ )
+ }
+ const phone = props.phone || props['contact:phone']
+ if (phone) {
+ body.appendChild(
+ Utils.loadTemplate(``)
+ )
+ }
+ if (props.mobile) {
+ body.appendChild(
+ Utils.loadTemplate(
+ ``
+ )
+ )
+ }
+ const email = props.email || props['contact:email']
+ if (email) {
+ body.appendChild(
+ Utils.loadTemplate(``)
+ )
+ }
+ const id = props['@id'] || props.id
+ if (id) {
+ body.appendChild(
+ Utils.loadTemplate(
+ ``
+ )
+ )
+ }
+ return body
+ }
+}
diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js
index b43128df..1f4640a0 100644
--- a/umap/static/umap/js/modules/utils.js
+++ b/umap/static/umap/js/modules/utils.js
@@ -392,13 +392,20 @@ export function loadTemplate(html) {
return template.content.firstElementChild
}
+export function loadTemplateWithRefs(html) {
+ const element = loadTemplate(html)
+ const elements = {}
+ for (const node of element.querySelectorAll('[data-ref]')) {
+ elements[node.dataset.ref] = node
+ }
+ return [element, elements]
+}
+
export class WithTemplate {
loadTemplate(html) {
- this.element = loadTemplate(html)
- this.elements = {}
- for (const element of this.element.querySelectorAll('[data-ref]')) {
- this.elements[element.dataset.ref] = element
- }
+ const [element, elements] = loadTemplateWithRefs(html)
+ this.element = element
+ this.elements = elements
return this.element
}
}
diff --git a/umap/static/umap/js/umap.popup.js b/umap/static/umap/js/umap.popup.js
deleted file mode 100644
index 6c4d48e1..00000000
--- a/umap/static/umap/js/umap.popup.js
+++ /dev/null
@@ -1,341 +0,0 @@
-/* Shapes */
-
-U.Popup = L.Popup.extend({
- options: {
- parseTemplate: true,
- },
-
- initialize: function (feature) {
- this.feature = feature
- this.container = L.DomUtil.create('div', 'umap-popup')
- this.format()
- L.Popup.prototype.initialize.call(this, {}, feature)
- this.setContent(this.container)
- },
-
- format: function () {
- const mode = this.feature.getOption('popupTemplate') || 'Default'
- const klass = U.PopupTemplate[mode] || U.PopupTemplate.Default
- this.content = new klass(this.feature, this.container)
- this.content.render()
- const els = this.container.querySelectorAll('img,iframe')
- for (let i = 0; i < els.length; i++) {
- this.onElementLoaded(els[i])
- }
- if (!els.length && this.container.textContent.replace('\n', '') === '') {
- this.container.innerHTML = ''
- L.DomUtil.add('h3', '', this.container, this.feature.getDisplayName())
- }
- },
-
- onElementLoaded: function (el) {
- L.DomEvent.on(
- el,
- 'load',
- function () {
- this._updateLayout()
- this._updatePosition()
- this._adjustPan()
- },
- this
- )
- },
-})
-
-U.Popup.Large = U.Popup.extend({
- options: {
- maxWidth: 500,
- className: 'umap-popup-large',
- },
-})
-
-U.Popup.Panel = U.Popup.extend({
- options: {
- zoomAnimation: false,
- },
-
- onAdd: function (map) {
- map.panel.setDefaultMode('expanded')
- map.panel.open({
- content: this._content,
- actions: [U.Browser.backButton(map)],
- })
-
- // fire events as in base class Popup.js:onAdd
- map.fire('popupopen', { popup: this })
- if (this._source) {
- this._source.fire('popupopen', { popup: this }, true)
- if (!(this._source instanceof L.Path)) {
- this._source.on('preclick', L.DomEvent.stopPropagation)
- }
- }
- },
-
- onRemove: function (map) {
- map.panel.close()
-
- // fire events as in base class Popup.js:onRemove
- map.fire('popupclose', { popup: this })
- if (this._source) {
- this._source.fire('popupclose', { popup: this }, true)
- if (!(this._source instanceof L.Path)) {
- this._source.off('preclick', L.DomEvent.stopPropagation)
- }
- }
- },
-
- update: () => {},
- _updateLayout: () => {},
- _updatePosition: () => {},
- _adjustPan: () => {},
- _animateZoom: () => {},
-})
-U.Popup.SimplePanel = U.Popup.Panel // Retrocompat.
-
-/* Content templates */
-
-U.PopupTemplate = {}
-
-U.PopupTemplate.Default = L.Class.extend({
- initialize: function (feature, container) {
- this.feature = feature
- this.container = container
- },
-
- renderTitle: () => {},
-
- renderBody: function () {
- const template = this.feature.getOption('popupContentTemplate')
- const target = this.feature.getOption('outlinkTarget')
- const container = L.DomUtil.create('div', 'umap-popup-container text')
- let content = ''
- let properties
- let center
- properties = this.feature.extendedProperties()
- // Resolve properties inside description
- properties.description = U.Utils.greedyTemplate(
- this.feature.properties.description || '',
- properties
- )
- content = U.Utils.greedyTemplate(template, properties)
- content = U.Utils.toHTML(content, { target: target })
- container.innerHTML = content
- return container
- },
-
- renderFooter: function () {
- if (this.feature.hasPopupFooter()) {
- const footer = L.DomUtil.create('ul', 'umap-popup-footer', this.container)
- const previousLi = L.DomUtil.create('li', 'previous', footer)
- const zoomLi = L.DomUtil.create('li', 'zoom', footer)
- const nextLi = L.DomUtil.create('li', 'next', footer)
- const next = this.feature.getNext()
- const prev = this.feature.getPrevious()
- // Fixme: remove me when this is merged and released
- // https://github.com/Leaflet/Leaflet/pull/9052
- L.DomEvent.disableClickPropagation(footer)
- if (next)
- nextLi.title = L._('Go to «{feature}»', {
- feature: next.properties.name || L._('next'),
- })
- if (prev)
- previousLi.title = L._('Go to «{feature}»', {
- feature: prev.properties.name || L._('previous'),
- })
- zoomLi.title = L._('Zoom to this feature')
- L.DomEvent.on(nextLi, 'click', () => {
- if (next) next.zoomTo({ callback: next.view })
- })
- L.DomEvent.on(previousLi, 'click', () => {
- if (prev) prev.zoomTo({ callback: prev.view })
- })
- L.DomEvent.on(
- zoomLi,
- 'click',
- function () {
- this.zoomTo()
- },
- this.feature
- )
- }
- },
-
- render: function () {
- const title = this.renderTitle()
- if (title) this.container.appendChild(title)
- const body = this.renderBody()
- if (body) L.DomUtil.add('div', 'umap-popup-content', this.container, body)
- this.renderFooter()
- },
-})
-
-U.PopupTemplate.BaseWithTitle = U.PopupTemplate.Default.extend({
- renderTitle: function () {
- let title
- if (this.feature.getDisplayName()) {
- title = L.DomUtil.create('h3', 'popup-title')
- title.textContent = this.feature.getDisplayName()
- }
- return title
- },
-})
-
-U.PopupTemplate.Table = U.PopupTemplate.BaseWithTitle.extend({
- formatRow: (key, value) => {
- if (value.indexOf('http') === 0) {
- value = `${value}`
- }
- return value
- },
-
- addRow: function (container, key, value) {
- const tr = L.DomUtil.create('tr', '', container)
- L.DomUtil.add('th', '', tr, key)
- L.DomUtil.element({
- tagName: 'td',
- parent: tr,
- innerHTML: this.formatRow(key, value),
- })
- },
-
- renderBody: function () {
- const table = L.DomUtil.create('table')
-
- for (const key in this.feature.properties) {
- if (typeof this.feature.properties[key] === 'object' || key === 'name') continue
- // TODO, manage links (url, mailto, wikipedia...)
- this.addRow(table, key, U.Utils.escapeHTML(this.feature.properties[key]).trim())
- }
- return table
- },
-})
-
-U.PopupTemplate.GeoRSSImage = U.PopupTemplate.BaseWithTitle.extend({
- options: {
- minWidth: 300,
- maxWidth: 500,
- className: 'umap-popup-large umap-georss-image',
- },
-
- renderBody: function () {
- const container = L.DomUtil.create('a')
- container.href = this.feature.properties.link
- container.target = '_blank'
- if (this.feature.properties.img) {
- const img = L.DomUtil.create('img', '', container)
- img.src = this.feature.properties.img
- // Sadly, we are unable to override this from JS the clean way
- // See https://github.com/Leaflet/Leaflet/commit/61d746818b99d362108545c151a27f09d60960ee#commitcomment-6061847
- img.style.maxWidth = `${this.options.maxWidth}px`
- img.style.maxHeight = `${this.options.maxWidth}px`
- this.onElementLoaded(img)
- }
- return container
- },
-})
-
-U.PopupTemplate.GeoRSSLink = U.PopupTemplate.Default.extend({
- options: {
- className: 'umap-georss-link',
- },
-
- renderBody: function () {
- const title = this.renderTitle(this)
- const a = L.DomUtil.add('a')
- a.href = this.feature.properties.link
- a.target = '_blank'
- a.appendChild(title)
- return a
- },
-})
-
-U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
- options: {
- className: 'umap-openstreetmap',
- },
-
- getName: function () {
- const props = this.feature.properties
- const locale = L.getLocale()
- if (locale && props[`name:${locale}`]) return props[`name:${locale}`]
- return props.name
- },
-
- renderBody: function () {
- const props = this.feature.properties
- const container = L.DomUtil.add('div')
- const title = L.DomUtil.add('h3', 'popup-title', container)
- const color = this.feature.getPreviewColor()
- title.style.backgroundColor = color
- const iconUrl = this.feature.getDynamicOption('iconUrl')
- const icon = U.Icon.makeIconElement(iconUrl, title)
- L.DomUtil.addClass(icon, 'icon')
- U.Icon.setIconContrast(icon, title, iconUrl, color)
- if (L.DomUtil.contrastedColor(title, color)) title.style.color = 'white'
- L.DomUtil.add('span', '', title, this.getName())
- const street = props['addr:street']
- if (street) {
- const row = L.DomUtil.add('address', 'address', container)
- const number = props['addr:housenumber']
- if (number) {
- // Poor way to deal with international forms of writting addresses
- L.DomUtil.add('span', '', row, `${L._('No.')}: ${number}`)
- L.DomUtil.add('span', '', row, `${L._('Street')}: ${street}`)
- } else {
- L.DomUtil.add('span', '', row, street)
- }
- }
- if (props.website) {
- L.DomUtil.element({
- tagName: 'a',
- parent: container,
- href: props.website,
- textContent: props.website,
- })
- }
- const phone = props.phone || props['contact:phone']
- if (phone) {
- L.DomUtil.add(
- 'div',
- '',
- container,
- L.DomUtil.element({ tagName: 'a', href: `tel:${phone}`, textContent: phone })
- )
- }
- if (props.mobile) {
- L.DomUtil.add(
- 'div',
- '',
- container,
- L.DomUtil.element({
- tagName: 'a',
- href: `tel:${props.mobile}`,
- textContent: props.mobile,
- })
- )
- }
- const email = props.email || props['contact:email']
- if (email) {
- L.DomUtil.add(
- 'div',
- '',
- container,
- L.DomUtil.element('a', { href: `mailto:${email}`, textContent: email })
- )
- }
- const id = props['@id'] || props.id
- if (id) {
- L.DomUtil.add(
- 'div',
- 'osm-link',
- container,
- L.DomUtil.element({
- tagName: 'a',
- href: `https://www.openstreetmap.org/${id}`,
- textContent: L._('See on OpenStreetMap'),
- })
- )
- }
- return container
- },
-})
diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css
index 4df013c0..697d09db 100644
--- a/umap/static/umap/map.css
+++ b/umap/static/umap/map.css
@@ -1017,10 +1017,10 @@ a.umap-control-caption,
.umap-popup-footer li.zoom:before {
background-position: -5px -101px;
}
-.umap-popup-footer li.previous:before {
+.umap-popup-footer li[rel="prev"]:before {
background-position: -28px -77px;
}
-.umap-popup-footer li.next:before {
+.umap-popup-footer li[rel="next"]:before {
background-position: -5px -77px;
}
.umap-popup a:hover {