diff --git a/umap/static/umap/alerts.css b/umap/static/umap/alerts.css new file mode 100644 index 000000000..b7c260e69 --- /dev/null +++ b/umap/static/umap/alerts.css @@ -0,0 +1,34 @@ +[data-alert] { + box-sizing: border-box; + min-height: 46px; + line-height: 46px; + padding-left: 10px; + width: calc(100% - 500px); + position: absolute; + left: 250px; /* Keep save/cancel button accessible. */ + right: 250px; + box-shadow: 0 1px 7px #999999; + background: none repeat scroll 0 0 rgba(20, 22, 23, 0.8); + font-weight: bold; + color: #fff; + font-size: 0.8em; + z-index: 1012; + border-radius: 2px; + visibility: visible; + top: 23px; + display: flex; + justify-content: space-between; + align-items: flex-start; +} +[data-alert][data-level="error"] { + background-color: #c60f13; +} +[data-alert] [data-close] { + color: #fff; + padding-right: 10px; + width: 100px; + line-height: 1; + margin: .5rem; + background-color: #202425; + font-size: .7rem; +} diff --git a/umap/static/umap/js/modules/alerts.js b/umap/static/umap/js/modules/alerts.js new file mode 100644 index 000000000..c1d83438e --- /dev/null +++ b/umap/static/umap/js/modules/alerts.js @@ -0,0 +1,54 @@ +export default class Alerts { + constructor() { + this.alertNode = document.querySelector('[role="alert"]') + const observer = new MutationObserver(this._callback.bind(this)) + observer.observe(this.alertNode, { childList: true }) + // On initial page load, we want to display messages from Django. + Array.from(this.alertNode.children).forEach(this._display.bind(this)) + } + + _callback(mutationList, observer) { + for (const mutation of mutationList) { + this._display( + [...mutation.addedNodes].filter((item) => item.tagName === 'P').pop() + ) + } + } + + _display(alert) { + const duration = alert.dataset?.duration || 3000 + const level = alert.dataset?.level || 'info' + const wrapper = document.createElement('div') + const alertHTML = alert.cloneNode(true).outerHTML + wrapper.innerHTML = ` +
+ ${alertHTML} + +
+ ` + const alertDiv = wrapper.firstElementChild + this.alertNode.after(alertDiv) + if (isFinite(duration)) { + setTimeout(() => { + alertDiv.remove() + }, duration) + } + } + + add(message, level = 'info', duration = 3000) { + this.alertNode.innerHTML = ` +

+ ${message} +

+ ` + } +} + +// TODISCUSS: this might be something we want somewhere else. +document.addEventListener('click', (event) => { + if (event.target.closest('[data-close]')) { + event.target.closest('[data-toclose]').remove() + } +}) diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index c7fce53f1..0d14f0cba 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,5 +1,6 @@ import * as L from '../../vendors/leaflet/leaflet-src.esm.js' import URLs from './urls.js' +import Alerts from './alerts.js' import Browser from './browser.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' // Import modules and export them to the global scope. @@ -7,4 +8,13 @@ import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './req // Copy the leaflet module, it's expected by leaflet plugins to be writeable. window.L = { ...L } -window.U = { URLs, Request, ServerRequest, RequestError, HTTPError, NOKError, Browser } +window.U = { + Alerts, + URLs, + Request, + ServerRequest, + RequestError, + HTTPError, + NOKError, + Browser, +} diff --git a/umap/static/umap/js/modules/request.js b/umap/static/umap/js/modules/request.js index b90fbf92d..356b01ccc 100644 --- a/umap/static/umap/js/modules/request.js +++ b/umap/static/umap/js/modules/request.js @@ -1,5 +1,6 @@ // Uses `L._`` from Leaflet.i18n which we cannot import as a module yet import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' +import Alert from './alerts.js' export class RequestError extends Error {} @@ -50,6 +51,7 @@ export class Request extends BaseRequest { constructor(ui) { super() this.ui = ui + this.alerts = new Alert() } async _fetch(method, uri, headers, data) { @@ -81,7 +83,7 @@ export class Request extends BaseRequest { } _onError(error) { - this.ui.alert({ content: L._('Problem in the response'), level: 'error' }) + this.alerts.add(L._('Problem in the response'), 'error') } _onNOK(error) { @@ -127,10 +129,10 @@ export class ServerRequest extends Request { try { const data = await response.json() if (data.info) { - this.ui.alert({ content: data.info, level: 'info' }) + this.alerts.add(data.info) this.ui.closePanel() } else if (data.error) { - this.ui.alert({ content: data.error, level: 'error' }) + this.alerts.add(data.error, 'error') return this._onError(new Error(data.error)) } return [data, response, null] @@ -145,10 +147,7 @@ export class ServerRequest extends Request { _onNOK(error) { if (error.status === 403) { - this.ui.alert({ - content: message || L._('Action not allowed :('), - level: 'error', - }) + this.alerts.add(message || L._('Action not allowed :('), 'error') } return [{}, error.response, error] } diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index b76dacccb..9b47f3005 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -782,8 +782,7 @@ const ControlsMixin = { if (datalayer.hasDataVisible()) found = true }) // TODO: display a results counter in the panel instead. - if (!found) - this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) + if (!found) this.alerts.add(L._('No results for these facets')) } const fields = keys.map((current) => [ @@ -1272,7 +1271,7 @@ U.Search = L.PhotonSearch.extend({ if (latlng.isValid()) { this.reverse.doReverse(latlng) } else { - this.map.ui.alert({ content: 'Invalid latitude or longitude', mode: 'error' }) + this.map.alerts.add(L._('Invalid latitude or longitude'), 'error') } return } diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index fecbba573..f5a54e83b 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -686,10 +686,7 @@ U.Marker = L.Marker.extend({ const builder = new U.FormBuilder(this, coordinatesOptions, { callback: function () { if (!this._latlng.isValid()) { - this.map.ui.alert({ - content: L._('Invalid latitude or longitude'), - level: 'error', - }) + this.map.alerts.add(L._('Invalid latitude or longitude'), 'error') builder.resetField('_latlng.lat') builder.resetField('_latlng.lng') } @@ -886,7 +883,7 @@ U.PathMixin = { items.push({ text: L._('Display measure'), callback: function () { - this.map.ui.alert({ content: this.getMeasure(), level: 'info' }) + this.map.alerts.add(this.getMeasure()) }, context: this, }) diff --git a/umap/static/umap/js/umap.importer.js b/umap/static/umap/js/umap.importer.js index 461932000..e8aa7e9e4 100644 --- a/umap/static/umap/js/umap.importer.js +++ b/umap/static/umap/js/umap.importer.js @@ -140,16 +140,15 @@ U.Importer = L.Class.extend({ this.map.processFileToImport(file, layer, type) } } else { - if (!type) - return this.map.ui.alert({ - content: L._('Please choose a format'), - level: 'error', - }) + if (!type) { + this.map.alerts.add(L._('Please choose a format'), 'error') + return + } if (this.rawInput.value && type === 'umap') { try { this.map.importRaw(this.rawInput.value, type) } catch (e) { - this.ui.alert({ content: L._('Invalid umap data'), level: 'error' }) + this.alerts.add(L._('Invalid umap data'), 'error') console.error(e) } } else { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 0366d4cb2..9331004c2 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -144,6 +144,7 @@ U.Map = L.Map.extend({ // After calling parent initialize, as we are doing initCenter our-selves if (geojson.geometry) this.options.center = this.latLng(geojson.geometry) this.urls = new U.URLs(this.options.urls) + this.alerts = new U.Alerts() this.ui = new U.UI(this._container) this.ui.on('dataloading', (e) => this.fire('dataloading', e)) @@ -393,7 +394,9 @@ U.Map = L.Map.extend({ icon: 'umap-fake-class', iconLoading: 'umap-fake-class', flyTo: this.options.easing, - onLocationError: (err) => this.ui.alert({ content: err.message }), + onLocationError: (err) => { + this.alerts.add(err.message, 'error') + }, }) this._controls.fullscreen = new L.Control.Fullscreen({ title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, @@ -677,10 +680,10 @@ U.Map = L.Map.extend({ } catch (e) { console.error(e) this.removeLayer(tilelayer) - this.ui.alert({ - content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`, - level: 'error', - }) + this.alerts.add( + `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`, + 'error' + ) // Users can put tilelayer URLs by hand, and if they add wrong {variable}, // Leaflet throw an error, and then the map is no more editable } @@ -712,10 +715,7 @@ U.Map = L.Map.extend({ } catch (e) { this.removeLayer(overlay) console.error(e) - this.ui.alert({ - content: `${L._('Error in the overlay URL')}: ${overlay._url}`, - level: 'error', - }) + this.alerts.add(`${L._('Error in the overlay URL')}: ${overlay._url}`, 'error') } }, @@ -842,10 +842,7 @@ U.Map = L.Map.extend({ if (this.options.umap_id) { // We do not want an extra message during the map creation // to avoid the double notification/alert. - this.ui.alert({ - content: L._('The zoom and center have been modified.'), - level: 'info', - }) + this.alerts.add(L._('The zoom and center have been modified.')) } }, @@ -889,12 +886,12 @@ U.Map = L.Map.extend({ processFileToImport: function (file, layer, type) { type = type || L.Util.detectFileType(file) if (!type) { - this.ui.alert({ - content: L._('Unable to detect format of file {filename}', { + this.alerts.add( + L._('Unable to detect format of file {filename}', { filename: file.name, }), - level: 'error', - }) + 'error' + ) return } if (type === 'umap') { @@ -946,10 +943,10 @@ U.Map = L.Map.extend({ self.importRaw(rawData) } catch (e) { console.error('Error importing data', e) - self.ui.alert({ - content: L._('Invalid umap data in {filename}', { filename: file.name }), - level: 'error', - }) + self.alerts.add( + L._('Invalid umap data in {filename}', { filename: file.name }), + 'error' + ) } } }, @@ -1060,10 +1057,10 @@ U.Map = L.Map.extend({ const uri = this.urls.get('map_save', { map_id: this.options.umap_id }) const [data, response, error] = await this.server.post(uri, {}, formData) if (!error) { - let duration = 3000, - alert = { content: L._('Map has been saved!'), level: 'info' } + let alertDuration = 3000 + let alertMessage = L._('Map has been saved!') if (!this.options.umap_id) { - alert.content = L._('Congratulations, your map has been created!') + alertMessage = L._('Congratulations, your map has been created!') this.options.umap_id = data.id this.permissions.setOptions(data.permissions) this.permissions.commit() @@ -1072,8 +1069,8 @@ U.Map = L.Map.extend({ data.permissions.anonymous_edit_url && this.options.urls.map_send_edit_link ) { - alert.duration = Infinity - alert.content = + alertDuration = Infinity + alertMessage = L._( 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:' ) + `
${data.permissions.anonymous_edit_url}` @@ -1108,8 +1105,9 @@ U.Map = L.Map.extend({ if (history && history.pushState) history.pushState({}, this.options.name, data.url) else window.location = data.url - alert.content = data.info || alert.content - this.once('saved', () => this.ui.alert(alert)) + this.once('saved', () => { + this.alerts.add(data.info || alertMessage, 'info', alertDuration) + }) this.ui.closePanel() this.permissions.save() } @@ -1142,11 +1140,10 @@ U.Map = L.Map.extend({ }, star: async function () { - if (!this.options.umap_id) - return this.ui.alert({ - content: L._('Please save the map first'), - level: 'error', - }) + if (!this.options.umap_id) { + this.alerts.add(L._('Please save the map first'), 'error') + return + } const url = this.urls.get('map_star', { map_id: this.options.umap_id }) const [data, response, error] = await this.server.post(url) if (!error) { @@ -1154,7 +1151,7 @@ U.Map = L.Map.extend({ let msg = data.starred ? L._('Map has been starred') : L._('Map has been unstarred') - this.ui.alert({ content: msg, level: 'info' }) + this.alerts.add(msg) this.renderControls() } }, diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 8fd86b97d..ba9dc7494 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -928,7 +928,7 @@ U.DataLayer = L.Evented.extend({ message: err[0].message, }) } - this.map.ui.alert({ content: message, level: 'error', duration: 10000 }) + this.map.alerts.add(message, 'error', 10000) console.error(err) } if (result && result.features.length) { @@ -955,7 +955,7 @@ U.DataLayer = L.Evented.extend({ const gj = JSON.parse(c) callback(gj) } catch (err) { - this.map.ui.alert({ content: `Invalid JSON file: ${err}` }) + this.map.alerts.add(L._('Invalid JSON file: {error}', { error: err }), 'error') return } } @@ -1013,12 +1013,12 @@ U.DataLayer = L.Evented.extend({ return this.geojsonToFeatures(geometry.geometries) default: - this.map.ui.alert({ - content: L._('Skipping unknown geometry.type: {type}', { + this.map.alerts.add( + L._('Skipping unknown geometry.type: {type}', { type: geometry.type || 'undefined', }), - level: 'error', - }) + 'error' + ) } if (layer) { this.addLayer(layer) @@ -1459,7 +1459,7 @@ U.DataLayer = L.Evented.extend({ if (!this.isVisible()) return const bounds = this.layer.getBounds() if (bounds.isValid()) { - const options = {maxZoom: this.getOption("zoomTo")} + const options = { maxZoom: this.getOption('zoomTo') } this.map.fitBounds(bounds, options) } }, diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index 72d601c39..161f9e57a 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -53,10 +53,7 @@ U.MapPermissions = L.Class.extend({ edit: function () { if (this.map.options.editMode !== 'advanced') return if (!this.map.options.umap_id) - return this.map.ui.alert({ - content: L._('Please save the map first'), - level: 'info', - }) + return this.map.alerts.add(L._('Please save the map first')) const container = L.DomUtil.create('div', 'permissions-panel'), fields = [], title = L.DomUtil.create('h3', '', container) @@ -135,10 +132,7 @@ U.MapPermissions = L.Class.extend({ const [data, response, error] = await this.map.server.post(this.getAttachUrl()) if (!error) { this.options.owner = this.map.options.user - this.map.ui.alert({ - content: L._('Map has been attached to your account'), - level: 'info', - }) + this.map.alerts.add(L._('Map has been attached to your account')) this.map.ui.closePanel() } }, diff --git a/umap/static/umap/js/umap.tableeditor.js b/umap/static/umap/js/umap.tableeditor.js index 19cafbeda..2c62eaea4 100644 --- a/umap/static/umap/js/umap.tableeditor.js +++ b/umap/static/umap/js/umap.tableeditor.js @@ -83,10 +83,10 @@ U.TableEditor = L.Class.extend({ validateName: function (name) { if (name.indexOf('.') !== -1) { - this.datalayer.map.ui.alert({ - content: L._('Invalide property name: {name}', { name: name }), - level: 'error', - }) + this.datalayer.map.alerts.add( + L._('Invalid property name: {name}', { name: name }), + 'error' + ) return false } return true diff --git a/umap/static/umap/test/index.html b/umap/static/umap/test/index.html index 23c9d52fd..970394c64 100644 --- a/umap/static/umap/test/index.html +++ b/umap/static/umap/test/index.html @@ -64,6 +64,7 @@ + diff --git a/umap/templates/umap/css.html b/umap/templates/umap/css.html index 186b1dfa8..c740e9e0a 100644 --- a/umap/templates/umap/css.html +++ b/umap/templates/umap/css.html @@ -24,6 +24,7 @@ href="{% static 'umap/vendors/iconlayers/iconLayers.css' %}" /> + diff --git a/umap/templates/umap/map_init.html b/umap/templates/umap/map_init.html index f3701593a..9bc809c14 100644 --- a/umap/templates/umap/map_init.html +++ b/umap/templates/umap/map_init.html @@ -1,17 +1,16 @@ {% load umap_tags %} + +
+ {% for m in messages %} + {# We have just one, but we need to loop, as for messages API #} +

{{ m }}

+ {% endfor %} +
diff --git a/umap/templates/umap/messages.html b/umap/templates/umap/messages.html index c2d192173..5cac7fb0d 100644 --- a/umap/templates/umap/messages.html +++ b/umap/templates/umap/messages.html @@ -1,5 +1,5 @@
-
+