From a2ca3a143615fde971a5f908afae02b0487d62a4 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 30 Jul 2024 16:59:44 +0200 Subject: [PATCH] chore: move popups to modules --- umap/static/umap/js/modules/data/features.js | 3 +- .../static/umap/js/modules/rendering/popup.js | 99 +++++ .../umap/js/modules/rendering/template.js | 214 +++++++++++ umap/static/umap/js/modules/utils.js | 17 +- umap/static/umap/js/umap.popup.js | 341 ------------------ umap/static/umap/map.css | 4 +- 6 files changed, 329 insertions(+), 349 deletions(-) create mode 100644 umap/static/umap/js/modules/rendering/popup.js create mode 100644 umap/static/umap/js/modules/rendering/template.js delete mode 100644 umap/static/umap/js/umap.popup.js 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(`
${content}
`) + } + + 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(`
${props.website}
`) + ) + } + const phone = props.phone || props['contact:phone'] + if (phone) { + body.appendChild( + Utils.loadTemplate(`
${phone}
`) + ) + } + if (props.mobile) { + body.appendChild( + Utils.loadTemplate( + `
${props.mobile}
` + ) + ) + } + const email = props.email || props['contact:email'] + if (email) { + body.appendChild( + Utils.loadTemplate(`
${email}
`) + ) + } + 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 {