Merge pull request #2034 from umap-project/popup-to-modules

chore: move popups to modules
This commit is contained in:
Yohan Boniface 2024-08-02 17:15:26 +02:00 committed by GitHub
commit 9f5361f2c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 329 additions and 349 deletions

View file

@ -10,6 +10,7 @@ import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js' import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js' import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from '../rendering/ui.js' import { LeafletMarker, LeafletPolyline, LeafletPolygon } from '../rendering/ui.js'
import loadPopup from '../rendering/popup.js'
class Feature { class Feature {
constructor(datalayer, geojson = {}, id = null) { constructor(datalayer, geojson = {}, id = null) {
@ -291,7 +292,7 @@ class Feature {
getPopupClass() { getPopupClass() {
const old = this.getOption('popupTemplate') // Retrocompat. const old = this.getOption('popupTemplate') // Retrocompat.
return U.Popup[this.getOption('popupShape') || old] || U.Popup return loadPopup(this.getOption('popupShape') || old)
} }
attachPopup() { attachPopup() {

View file

@ -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: () => {},
})

View file

@ -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(`<div class="umap-popup-container text">${content}</div>`)
}
renderFooter(feature, container) {
if (feature.hasPopupFooter()) {
const template = `
<ul class="umap-popup-footer">
<li rel="prev" data-ref="previous"></li>
<li class="zoom" data-ref="zoom" title="${translate('Zoom to this feature')}"></li>
<li rel="next" data-ref="next"></li>
</ul>`
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(`<h3 class="popup-title">${title}</h3>`)
}
}
}
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 `<a href="${value}" target="_blank">${value}</a>`
}
return value
}
makeRow(feature, key) {
return Utils.loadTemplate(
`<tr><th>${key}</th><td>${this.getValue(feature, key)}</td></tr>`
)
}
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(
`<a href="${feature.properties.link}" target="_blank">${title}</a>`
)
}
}
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(`<div><a href="${props.website}">${props.website}</a></div>`)
)
}
const phone = props.phone || props['contact:phone']
if (phone) {
body.appendChild(
Utils.loadTemplate(`<div><a href="tel:${phone}">${phone}</a></div>`)
)
}
if (props.mobile) {
body.appendChild(
Utils.loadTemplate(
`<div><a href="tel:${props.mobile}">${props.mobile}</a></div>`
)
)
}
const email = props.email || props['contact:email']
if (email) {
body.appendChild(
Utils.loadTemplate(`<div><a href="mailto:${email}">${email}</a></div>`)
)
}
const id = props['@id'] || props.id
if (id) {
body.appendChild(
Utils.loadTemplate(
`<div class="osm-link"><a href="https://www.openstreetmap.org/${id}">${translate('See on OpenStreetMap')}</a></div>`
)
)
}
return body
}
}

View file

@ -392,13 +392,20 @@ export function loadTemplate(html) {
return template.content.firstElementChild 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 { export class WithTemplate {
loadTemplate(html) { loadTemplate(html) {
this.element = loadTemplate(html) const [element, elements] = loadTemplateWithRefs(html)
this.elements = {} this.element = element
for (const element of this.element.querySelectorAll('[data-ref]')) { this.elements = elements
this.elements[element.dataset.ref] = element
}
return this.element return this.element
} }
} }

View file

@ -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 = `<a href="${value}" target="_blank">${value}</a>`
}
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
},
})

View file

@ -1017,10 +1017,10 @@ a.umap-control-caption,
.umap-popup-footer li.zoom:before { .umap-popup-footer li.zoom:before {
background-position: -5px -101px; background-position: -5px -101px;
} }
.umap-popup-footer li.previous:before { .umap-popup-footer li[rel="prev"]:before {
background-position: -28px -77px; background-position: -28px -77px;
} }
.umap-popup-footer li.next:before { .umap-popup-footer li[rel="next"]:before {
background-position: -5px -77px; background-position: -5px -77px;
} }
.umap-popup a:hover { .umap-popup a:hover {