From 5c2528900ea0168ec86c4acf0640675e1db48e7f Mon Sep 17 00:00:00 2001 From: David Larlet Date: Mon, 3 Jun 2024 10:05:27 -0400 Subject: [PATCH] chore: Use web components to display alerts + a11y roles --- umap/settings/base.py | 1 + umap/static/umap/css/alert.css | 75 -------- .../umap/js/components/alerts/alert.css | 45 +++++ .../umap/js/components/alerts/alert.html | 72 ++++++++ .../static/umap/js/components/alerts/alert.js | 169 ++++++++++++++++++ umap/static/umap/js/modules/autocomplete.js | 4 +- umap/static/umap/js/modules/global.js | 8 +- umap/static/umap/js/modules/importer.js | 8 +- umap/static/umap/js/modules/request.js | 19 +- umap/static/umap/js/modules/ui/alert.js | 82 --------- umap/static/umap/js/umap.controls.js | 2 +- umap/static/umap/js/umap.features.js | 7 +- umap/static/umap/js/umap.js | 156 +++++++--------- umap/static/umap/js/umap.layer.js | 42 ++--- umap/static/umap/js/umap.permissions.js | 13 +- umap/static/umap/js/umap.tableeditor.js | 5 +- umap/templates/umap/content.html | 3 +- umap/templates/umap/css.html | 1 - umap/templates/umap/map_init.html | 10 +- umap/templates/umap/messages.html | 20 +-- umap/views.py | 1 + 21 files changed, 400 insertions(+), 343 deletions(-) delete mode 100644 umap/static/umap/css/alert.css create mode 100644 umap/static/umap/js/components/alerts/alert.css create mode 100644 umap/static/umap/js/components/alerts/alert.html create mode 100644 umap/static/umap/js/components/alerts/alert.js delete mode 100644 umap/static/umap/js/modules/ui/alert.js diff --git a/umap/settings/base.py b/umap/settings/base.py index 7d926869..df56ccd7 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -182,6 +182,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, + "DIRS": [os.path.join(PROJECT_DIR, STATIC_ROOT)], "OPTIONS": { "context_processors": ( "django.contrib.auth.context_processors.auth", diff --git a/umap/static/umap/css/alert.css b/umap/static/umap/css/alert.css deleted file mode 100644 index 05833a55..00000000 --- a/umap/static/umap/css/alert.css +++ /dev/null @@ -1,75 +0,0 @@ -#umap-alert-container { - min-height: 46px; - line-height: 46px; - padding-left: 10px; - width: calc(100% - 500px); - position: absolute; - top: -46px; - left: 250px; /* Keep save/cancel button accessible. */ - right: 250px; - box-shadow: 0 1px 7px #999999; - visibility: hidden; - 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; -} -#umap-alert-container.error { - background-color: #c60f13; -} -.umap-alert #umap-alert-container { - visibility: visible; - top: 23px; -} -#umap-alert-container .umap-action { - margin-left: 10px; - background-color: #fff; - color: #000; - padding: 5px; - border-radius: 4px; -} -#umap-alert-container .umap-action:hover { - color: #000; -} -#umap-alert-container .error .umap-action { - background-color: #666; - color: #eee; -} -#umap-alert-container .error .umap-action:hover { - color: #fff; -} -#umap-alert-container input { - padding: 5px; - border-radius: 4px; - width: 100%; -} -#umap-alert-container .umap-close-link { - color: #fff; - float: right; - padding-right: 10px; - width: 100px; - line-height: 1; - margin: .5rem; - background-color: #202425; - font-size: .7rem; -} -#umap-alert-container .umap-close-icon { - background-position: -74px -55px; -} -#umap-alert-container .umap-alert-actions { - display: flex; - margin: 1rem; -} -#umap-alert-container .umap-alert-actions .umap-action { - margin-bottom: 0; -} - -@media all and (orientation:portrait) { - #umap-alert-container { - width: 100%; - left: 0; - right: 0; - } -} diff --git a/umap/static/umap/js/components/alerts/alert.css b/umap/static/umap/js/components/alerts/alert.css new file mode 100644 index 00000000..8715581d --- /dev/null +++ b/umap/static/umap/js/components/alerts/alert.css @@ -0,0 +1,45 @@ +[role="dialog"] { + box-sizing: border-box; + min-height: 46px; + line-height: 46px; + padding: var(--panel-gutter); + position: absolute; + 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; + margin-top: calc(var(--header-height) + var(--panel-gutter)); + display: flex; + justify-content: space-between; + align-items: flex-start; + left: 50%; + transform: translate(-50%, 0); + min-width: 80%; +} +[role="dialog"][data-level='error'] { + background-color: #c60f13; +} +[role="dialog"] > div { + margin: 0 auto; +} +[role="dialog"] [data-close] { + color: #fff; + max-width: 42px; + line-height: initial; + margin: 0; + margin-left: var(--panel-gutter); + background-color: #202425; + font-size: 0.7rem; +} +[role="dialog"] [data-close] .icon + span { + margin-left: 0; +} +#link-wrapper form { + display: flex; +} +#link-wrapper form input[type='button'] { + max-width: 100px; +} diff --git a/umap/static/umap/js/components/alerts/alert.html b/umap/static/umap/js/components/alerts/alert.html new file mode 100644 index 00000000..6380b902 --- /dev/null +++ b/umap/static/umap/js/components/alerts/alert.html @@ -0,0 +1,72 @@ +{% load i18n static %} + + + + + + + + + + diff --git a/umap/static/umap/js/components/alerts/alert.js b/umap/static/umap/js/components/alerts/alert.js new file mode 100644 index 00000000..a80d3dfb --- /dev/null +++ b/umap/static/umap/js/components/alerts/alert.js @@ -0,0 +1,169 @@ +class uMapAlert extends HTMLElement { + static info(message, duration = 5000) { + const event = new CustomEvent('umap:alert', { + bubbles: true, + cancelable: true, + detail: { message, duration }, + }) + document.dispatchEvent(event) + } + + // biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default + static error(message, duration = Infinity) { + const event = new CustomEvent('umap:alert', { + bubbles: true, + cancelable: true, + detail: { level: 'error', message, duration }, + }) + document.dispatchEvent(event) + } + + constructor() { + super() + this.container = this.querySelector('[role="dialog"]') + this.element = this.container.querySelector('[role="alert"]') + } + + _hide() { + this.setAttribute('hidden', 'hidden') + } + + _show() { + this.removeAttribute('hidden') + } + + _displayAlert(detail) { + const { level = 'info', duration = 5000, message = '' } = detail + this.container.dataset.level = level + this.container.dataset.duration = duration + this.element.textContent = message + this._show() + if (Number.isFinite(duration)) { + setTimeout(() => { + this._hide() + }, duration) + } + } + + connectedCallback() { + this.addEventListener('click', (event) => { + if (event.target.closest('[data-close]')) { + this._hide() + } + }) + document.addEventListener('umap:alert', (event) => { + this._displayAlert(event.detail) + }) + } +} + +class uMapAlertCreation extends uMapAlert { + static info( + message, + // biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default + duration = Infinity, + editLink = undefined, + sendLink = undefined + ) { + const event = new CustomEvent('umap:alert-creation', { + bubbles: true, + cancelable: true, + detail: { message, duration, editLink, sendLink }, + }) + document.dispatchEvent(event) + } + + constructor() { + super() + this.linkWrapper = this.container.querySelector('#link-wrapper') + this.formWrapper = this.container.querySelector('#form-wrapper') + } + + _displayCreationAlert(detail) { + const { + level = 'info', + duration = 5000, + message = '', + editLink = undefined, + sendLink = undefined, + } = detail + uMapAlert.prototype._displayAlert.call(this, { level, duration, message }) + this.linkWrapper.querySelector('input[type="url"]').value = editLink + const button = this.linkWrapper.querySelector('input[type="button"]') + button.addEventListener('click', (event) => { + event.preventDefault() + L.Util.copyToClipboard(editLink) + event.target.value = L._('✅ Copied!') + }) + if (sendLink) { + this.formWrapper.removeAttribute('hidden') + const form = this.formWrapper.querySelector('form') + form.addEventListener('submit', async (event) => { + event.preventDefault() + const formData = new FormData(form) + const server = new U.ServerRequest() + this._hide() + await server.post(sendLink, {}, formData) + }) + } + } + + connectedCallback() { + this.addEventListener('click', (event) => { + if (event.target.closest('[data-close]')) { + this._hide() + } + }) + document.addEventListener('umap:alert-creation', (event) => { + this._displayCreationAlert(event.detail) + }) + } +} + +class uMapAlertChoice extends uMapAlert { + static error( + message, + // biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default + duration = Infinity + ) { + const event = new CustomEvent('umap:alert-choice', { + bubbles: true, + cancelable: true, + detail: { message, duration }, + }) + document.dispatchEvent(event) + } + + constructor() { + super() + this.choiceWrapper = this.container.querySelector('#choice-wrapper') + } + + _displayChoiceAlert(detail) { + const { level = 'error', duration = 5000, message = '' } = detail + uMapAlert.prototype._displayAlert.call(this, { level, duration, message }) + const button = this.choiceWrapper.querySelector('input[type="submit"]') + button.addEventListener('click', (event) => { + event.preventDefault() + document.dispatchEvent( + new CustomEvent('umap:alert-choice-confirm', { + bubbles: true, + cancelable: true, + }) + ) + }) + } + + connectedCallback() { + this.addEventListener('click', (event) => { + if (event.target.closest('[data-close]')) { + this._hide() + } + }) + document.addEventListener('umap:alert-choice', (event) => { + this._displayChoiceAlert(event.detail) + }) + } +} + +export { uMapAlert, uMapAlertCreation, uMapAlertChoice } diff --git a/umap/static/umap/js/modules/autocomplete.js b/umap/static/umap/js/modules/autocomplete.js index 3c2949a1..4fde8b84 100644 --- a/umap/static/umap/js/modules/autocomplete.js +++ b/umap/static/umap/js/modules/autocomplete.js @@ -1,7 +1,6 @@ import { DomUtil, DomEvent, setOptions } from '../../vendors/leaflet/leaflet-src.esm.js' import { translate } from './i18n.js' import { ServerRequest } from './request.js' -import Alert from './ui/alert.js' export class BaseAutocomplete { constructor(el, options) { @@ -220,8 +219,7 @@ export class BaseAutocomplete { class BaseAjax extends BaseAutocomplete { constructor(el, options) { super(el, options) - const alert = new Alert(document.querySelector('header')) - this.server = new ServerRequest(alert) + this.server = new ServerRequest() } optionToResult(option) { return { diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index d158a40b..31081ebf 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -3,7 +3,6 @@ import Browser from './browser.js' import Facets from './facets.js' import Caption from './caption.js' import { Panel, EditPanel, FullPanel } from './ui/panel.js' -import Alert from './ui/alert.js' import Dialog from './ui/dialog.js' import Tooltip from './ui/tooltip.js' import * as Utils from './utils.js' @@ -14,6 +13,11 @@ import Orderable from './orderable.js' import Importer from './importer.js' import Help from './help.js' import { SyncEngine } from './sync/engine.js' +import { + uMapAlert as Alert, + uMapAlertCreation as AlertCreation, + uMapAlertChoice as AlertChoice, +} from '../components/alerts/alert.js' // Import modules and export them to the global scope. // For the not yet module-compatible JS out there. @@ -21,6 +25,8 @@ import { SyncEngine } from './sync/engine.js' // By alphabetic order window.U = { Alert, + AlertCreation, + AlertChoice, AjaxAutocomplete, AjaxAutocompleteMultiple, Browser, diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index ea017552..06575025 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -163,16 +163,12 @@ export default class Importer { this.map.processFileToImport(file, layer, type) } } else { - if (!type) - return this.map.alert.open({ - content: translate('Please choose a format'), - level: 'error', - }) + if (!type) return U.Alert.error(L._('Please choose a format')) if (this.rawInput.value && type === 'umap') { try { this.map.importRaw(this.rawInput.value, type) } catch (e) { - this.alert.open({ content: translate('Invalid umap data'), level: 'error' }) + U.Alert.error(L._('Invalid umap data')) console.error(e) } } else { diff --git a/umap/static/umap/js/modules/request.js b/umap/static/umap/js/modules/request.js index fe392871..af9b2cfc 100644 --- a/umap/static/umap/js/modules/request.js +++ b/umap/static/umap/js/modules/request.js @@ -1,5 +1,4 @@ -// Uses `L._`` from Leaflet.i18n which we cannot import as a module yet -import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' +import { translate } from './i18n.js' export class RequestError extends Error {} @@ -47,11 +46,6 @@ class BaseRequest { // In case of error, an alert is sent, but non 20X status are not handled // The consumer must check the response status by hand export class Request extends BaseRequest { - constructor(alert) { - super() - this.alert = alert - } - fire(name, params) { document.body.dispatchEvent(new CustomEvent(name, params)) } @@ -85,7 +79,7 @@ export class Request extends BaseRequest { } _onError(error) { - this.alert.open({ content: L._('Problem in the response'), level: 'error' }) + U.Alert.error(translate('Problem in the response')) } _onNOK(error) { @@ -131,9 +125,9 @@ export class ServerRequest extends Request { try { const data = await response.json() if (data.info) { - this.alert.open({ content: data.info, level: 'info' }) + U.Alert.info(data.info) } else if (data.error) { - this.alert.open({ content: data.error, level: 'error' }) + U.Alert.error(data.error) return this._onError(new Error(data.error)) } return [data, response, null] @@ -148,10 +142,7 @@ export class ServerRequest extends Request { _onNOK(error) { if (error.status === 403) { - this.alert.open({ - content: error.message || L._('Action not allowed :('), - level: 'error', - }) + U.Alert.error(error.message || translate('Action not allowed :(')) } return [{}, error.response, error] } diff --git a/umap/static/umap/js/modules/ui/alert.js b/umap/static/umap/js/modules/ui/alert.js deleted file mode 100644 index b02c2d82..00000000 --- a/umap/static/umap/js/modules/ui/alert.js +++ /dev/null @@ -1,82 +0,0 @@ -import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js' -import { translate } from '../i18n.js' - -const ALERTS = [] -let ALERT_ID = null - -export default class Alert { - constructor(parent) { - this.parent = parent - this.container = DomUtil.create('div', 'with-transition', this.parent) - this.container.id = 'umap-alert-container' - DomEvent.disableClickPropagation(this.container) - DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu. - DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation) - DomEvent.on(this.container, 'MozMousePixelScroll', DomEvent.stopPropagation) - } - - open(params) { - if (DomUtil.hasClass(this.parent, 'umap-alert')) ALERTS.push(params) - else this._open(params) - } - - _open(params) { - if (!params) { - if (ALERTS.length) params = ALERTS.pop() - else return - } - let timeoutID - const level_class = params.level && params.level == 'info' ? 'info' : 'error' - this.container.innerHTML = '' - DomUtil.addClass(this.parent, 'umap-alert') - DomUtil.addClass(this.container, level_class) - const close = () => { - if (timeoutID && timeoutID !== ALERT_ID) { - return - } // Another alert has been forced - this.container.innerHTML = '' - DomUtil.removeClass(this.parent, 'umap-alert') - DomUtil.removeClass(this.container, level_class) - if (timeoutID) window.clearTimeout(timeoutID) - this._open() - } - const closeButton = DomUtil.createButton( - 'umap-close-link', - this.container, - '', - close, - this - ) - DomUtil.create('i', 'umap-close-icon', closeButton) - const label = DomUtil.create('span', '', closeButton) - label.title = label.textContent = translate('Close') - DomUtil.element({ - tagName: 'div', - innerHTML: params.content, - parent: this.container, - }) - let action, el, input - const form = DomUtil.create('div', 'umap-alert-actions', this.container) - for (let action of params.actions || []) { - if (action.input) { - input = DomUtil.element({ - tagName: 'input', - parent: form, - className: 'umap-alert-input', - placeholder: action.input, - }) - } - el = DomUtil.createButton( - 'umap-action', - form, - action.label, - action.callback, - action.callbackContext - ) - DomEvent.on(el, 'click', close, this) - } - if (params.duration !== Infinity) { - ALERT_ID = timeoutID = window.setTimeout(close, params.duration || 3000) - } - } -} diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 4ae4ffbc..831aa26c 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1086,7 +1086,7 @@ U.Search = L.PhotonSearch.extend({ if (latlng.isValid()) { this.reverse.doReverse(latlng) } else { - this.map.alert.open({ content: 'Invalid latitude or longitude', mode: 'error' }) + U.Alert.error(L._('Invalid latitude or longitude')) } return } diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index a4cf8f1d..b669df4e 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -764,10 +764,7 @@ U.Marker = L.Marker.extend({ const builder = new U.FormBuilder(this, coordinatesOptions, { callback: function () { if (!this._latlng.isValid()) { - this.map.alert.open({ - content: L._('Invalid latitude or longitude'), - level: 'error', - }) + U.Alert.error(L._('Invalid latitude or longitude')) builder.resetField('_latlng.lat') builder.resetField('_latlng.lng') } @@ -966,7 +963,7 @@ U.PathMixin = { items.push({ text: L._('Display measure'), callback: function () { - this.map.alert.open({ content: this.getMeasure(), level: 'info' }) + U.Alert.info(this.getMeasure()) }, context: this, }) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index ce455434..bbfee5d1 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -13,7 +13,7 @@ L.Map.mergeOptions({ // we cannot rely on this because of the y is overriden by Leaflet // See https://github.com/Leaflet/Leaflet/pull/9201 // And let's remove this -y when this PR is merged and released. - demoTileInfos: { 's': 'a', 'z': 9, 'x': 265, 'y': 181, '-y': 181, 'r': '' }, + demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, '-y': 181, r: '' }, licences: [], licence: '', enableMarkerDraw: true, @@ -59,7 +59,6 @@ U.Map = L.Map.extend({ this.urls = new U.URLs(this.options.urls) this.panel = new U.Panel(this) - this.alert = new U.Alert(this._controlContainer) this.tooltip = new U.Tooltip(this._controlContainer) this.dialog = new U.Dialog(this._controlContainer) if (this.hasEditMode()) { @@ -68,8 +67,8 @@ U.Map = L.Map.extend({ } L.DomEvent.on(document.body, 'dataloading', (e) => this.fire('dataloading', e)) L.DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e)) - this.server = new U.ServerRequest(this.alert) - this.request = new U.Request(this.alert) + this.server = new U.ServerRequest() + this.request = new U.Request() this.initLoader() this.name = this.options.name @@ -391,7 +390,7 @@ U.Map = L.Map.extend({ icon: 'umap-fake-class', iconLoading: 'umap-fake-class', flyTo: this.options.easing, - onLocationError: (err) => this.alert.open({ content: err.message }), + onLocationError: (err) => U.Alert.error(err.message), }) this._controls.fullscreen = new L.Control.Fullscreen({ title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, @@ -680,10 +679,7 @@ U.Map = L.Map.extend({ } catch (e) { console.error(e) this.removeLayer(tilelayer) - this.alert.open({ - content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`, - level: 'error', - }) + U.Alert.error(`${L._('Error in the tilelayer URL')}: ${tilelayer._url}`) // 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 } @@ -715,10 +711,7 @@ U.Map = L.Map.extend({ } catch (e) { this.removeLayer(overlay) console.error(e) - this.alert.open({ - content: `${L._('Error in the overlay URL')}: ${overlay._url}`, - level: 'error', - }) + U.Alert.error(`${L._('Error in the overlay URL')}: ${overlay._url}`) } }, @@ -843,10 +836,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.alert.open({ - content: L._('The zoom and center have been modified.'), - level: 'info', - }) + U.Alert.info(L._('The zoom and center have been modified.')) } }, @@ -886,12 +876,11 @@ U.Map = L.Map.extend({ processFileToImport: function (file, layer, type) { type = type || U.Utils.detectFileType(file) if (!type) { - this.alert.open({ - content: L._('Unable to detect format of file {filename}', { + U.Alert.error( + L._('Unable to detect format of file {filename}', { filename: file.name, - }), - level: 'error', - }) + }) + ) return } if (type === 'umap') { @@ -947,10 +936,7 @@ U.Map = L.Map.extend({ self.importRaw(rawData) } catch (e) { console.error('Error importing data', e) - self.alert.open({ - content: L._('Invalid umap data in {filename}', { filename: file.name }), - level: 'error', - }) + U.Alert.error(L._('Invalid umap data in {filename}', { filename: file.name })) } } }, @@ -1058,57 +1044,54 @@ U.Map = L.Map.extend({ const [data, _, error] = await this.server.post(uri, {}, formData) // FIXME: login_required response will not be an error, so it will not // stop code while it should - if (!error) { - let duration = 3000, - alert = { content: L._('Map has been saved!'), level: 'info' } - if (!this.options.umap_id) { - alert.content = L._('Congratulations, your map has been created!') - this.options.umap_id = data.id - this.permissions.setOptions(data.permissions) - this.permissions.commit() - if (data.permissions && data.permissions.anonymous_edit_url) { - alert.duration = Infinity - alert.content = - 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}` + if (error) { + return + } - alert.actions = [ - { - label: L._('Copy link'), - callback: () => { - L.Util.copyToClipboard(data.permissions.anonymous_edit_url) - this.alert.open({ - content: L._('Secret edit link copied to clipboard!'), - level: 'info', - }) - }, - callbackContext: this, - }, - ] - if (this.options.urls.map_send_edit_link) { - alert.actions.push({ - label: L._('Send me the link'), - input: L._('Email'), - callback: this.sendEditLink, - callbackContext: this, - }) - } - } - } else if (!this.permissions.isDirty) { + if (!this.options.umap_id) { + this.options.umap_id = data.id + this.permissions.setOptions(data.permissions) + this.permissions.commit() + if (data?.permissions?.anonymous_edit_url) { + const send_edit_link_url = + this.options.urls.map_send_edit_link && + this.urls.get('map_send_edit_link', { + map_id: this.options.umap_id, + }) + this.once('saved', () => { + U.AlertCreation.info( + 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:' + ), + Number.Infinity, + data.permissions.anonymous_edit_url, + send_edit_link_url + ) + }) + } else { + this.once('saved', () => { + U.Alert.info(L._('Congratulations, your map has been created!')) + }) + } + } else { + if (!this.permissions.isDirty) { // Do not override local changes to permissions, // but update in case some other editors changed them in the meantime. this.permissions.setOptions(data.permissions) this.permissions.commit() } - // Update URL in case the name has changed. - 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.alert.open(alert)) - this.permissions.save() + this.once('saved', () => { + U.Alert.info(data.info || L._('Map has been saved!')) + }) } + // Update URL in case the name has changed. + if (history?.pushState) { + history.pushState({}, this.options.name, data.url) + } else { + window.location = data.url + } + this.permissions.save() }, save: function () { @@ -1126,33 +1109,20 @@ U.Map = L.Map.extend({ } }, - sendEditLink: async function () { - const input = this.alert.container.querySelector('input') - const email = input.value - - const formData = new FormData() - formData.append('email', email) - - const url = this.urls.get('map_send_edit_link', { map_id: this.options.umap_id }) - await this.server.post(url, {}, formData) - }, - star: async function () { - if (!this.options.umap_id) - return this.alert.open({ - content: L._('Please save the map first'), - level: 'error', - }) + if (!this.options.umap_id) { + return U.Alert.error(L._('Please save the map first')) + } const url = this.urls.get('map_star', { map_id: this.options.umap_id }) const [data, response, error] = await this.server.post(url) - if (!error) { - this.options.starred = data.starred - let msg = data.starred - ? L._('Map has been starred') - : L._('Map has been unstarred') - this.alert.open({ content: msg, level: 'info' }) - this.renderControls() + if (error) { + return } + this.options.starred = data.starred + U.Alert.info( + data.starred ? L._('Map has been starred') : L._('Map has been unstarred') + ) + this.renderControls() }, geometry: function () { diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 064287a7..141a448d 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -958,7 +958,7 @@ U.DataLayer = L.Evented.extend({ const doc = new DOMParser().parseFromString(x, 'text/xml') const errorNode = doc.querySelector('parsererror') if (errorNode) { - this.map.alert.open({ content: L._('Cannot parse data'), level: 'error' }) + U.Alert.error(L._('Cannot parse data')) } return doc } @@ -993,7 +993,7 @@ U.DataLayer = L.Evented.extend({ message: err[0].message, }) } - this.map.alert.open({ content: message, level: 'error', duration: 10000 }) + U.Alert.error(message, 10000) console.error(err) } if (result && result.features.length) { @@ -1020,7 +1020,7 @@ U.DataLayer = L.Evented.extend({ const gj = JSON.parse(c) callback(gj) } catch (err) { - this.map.alert.open({ content: `Invalid JSON file: ${err}` }) + U.Alert.error(`Invalid JSON file: ${err}`) return } } @@ -1121,12 +1121,11 @@ U.DataLayer = L.Evented.extend({ return this.geojsonToFeatures(geometry.geometries) default: - this.map.alert.open({ - content: L._('Skipping unknown geometry.type: {type}', { + U.Alert.error( + L._('Skipping unknown geometry.type: {type}', { type: geometry.type || 'undefined', - }), - level: 'error', - }) + }) + ) } }, @@ -1706,27 +1705,14 @@ U.DataLayer = L.Evented.extend({ const [data, response, error] = await this.map.server.post(url, headers, formData) if (error) { if (response && response.status === 412) { - const msg = L._( - 'Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.' + U.AlertChoice.error( + L._( + 'Woops! Someone else seems to have edited the data. ' + + 'You can save anyway, but this will erase the changes made by others.' + ) ) - const actions = [ - { - label: L._('Save anyway'), - callback: async () => { - // Save again, - // but do not pass the reference version this time - await this._trySave(url, {}, formData) - }, - }, - { - label: L._('Cancel'), - }, - ] - this.map.alert.open({ - content: msg, - level: 'error', - duration: 100000, - actions: actions, + document.addEventListener('umap:alert-choice-confirm', async (event) => { + await this._trySave(url, {}, formData) }) } } else { diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index 32b3a977..4fb19295 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -52,11 +52,9 @@ U.MapPermissions = L.Class.extend({ edit: function () { if (this.map.options.editMode !== 'advanced') return - if (!this.map.options.umap_id) - return this.map.alert.open({ - content: L._('Please save the map first'), - level: 'info', - }) + if (!this.map.options.umap_id) { + return U.Alert.info(L._('Please save the map first')) + } const container = L.DomUtil.create('div', 'permissions-panel') const fields = [] L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key') @@ -140,10 +138,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.alert.open({ - content: L._('Map has been attached to your account'), - level: 'info', - }) + U.Alert.info(L._('Map has been attached to your account')) this.map.editPanel.close() } }, diff --git a/umap/static/umap/js/umap.tableeditor.js b/umap/static/umap/js/umap.tableeditor.js index 36515a60..389ae61b 100644 --- a/umap/static/umap/js/umap.tableeditor.js +++ b/umap/static/umap/js/umap.tableeditor.js @@ -83,10 +83,7 @@ U.TableEditor = L.Class.extend({ validateName: function (name) { if (name.indexOf('.') !== -1) { - this.datalayer.map.alert.open({ - content: L._('Invalide property name: {name}', { name: name }), - level: 'error', - }) + U.Alert.error(L._('Invalide property name: {name}', { name: name })) return false } return true diff --git a/umap/templates/umap/content.html b/umap/templates/umap/content.html index 01e3ed43..49f035a1 100644 --- a/umap/templates/umap/content.html +++ b/umap/templates/umap/content.html @@ -38,8 +38,7 @@ {{ block.super }} diff --git a/umap/templates/umap/messages.html b/umap/templates/umap/messages.html index c2d19217..6eefee47 100644 --- a/umap/templates/umap/messages.html +++ b/umap/templates/umap/messages.html @@ -1,11 +1,9 @@ -
-
- {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} -
-
+{% load i18n %} + +{% include "umap/js/components/alerts/alert.html" %} + +{% for message in messages %} + +{% endfor %} diff --git a/umap/views.py b/umap/views.py index 9a3e2dd2..d0019851 100644 --- a/umap/views.py +++ b/umap/views.py @@ -906,6 +906,7 @@ class MapDelete(DeleteView): return HttpResponseForbidden(_("Only its owner can delete the map.")) self.object.delete() home_url = reverse("home") + messages.info(self.request, _("Map successfully deleted.")) if is_ajax(self.request): return simple_json_response(redirect=home_url) else: