Merge pull request #1876 from umap-project/dom-driven-alerts-2

Use web components to display alerts + a11y roles
This commit is contained in:
David Larlet 2024-06-13 15:06:25 -04:00 committed by GitHub
commit 7660153b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 666 additions and 438 deletions

View file

@ -89,9 +89,7 @@ texte est affiché en haut de la carte, comme celui ci-dessous :
data-alt="Message dalerte contenant le lien dédition." data-alt="Message dalerte contenant le lien dédition."
data-width="790" data-width="790"
data-height="226" data-height="226"
data-selector="#umap-alert-container" data-selector='umap-alert-creation [role="dialog"]'
data-wait-for="document.querySelector('#umap-alert-container .umap-alert-actions')"
data-javascript="document.querySelector('button.leaflet-control-edit-save').click()"
>Message dalerte contenant le lien dédition.</shot-scraper> >Message dalerte contenant le lien dédition.</shot-scraper>
Ce texte explique que vous venez de créer une carte **anonyme** et vous Ce texte explique que vous venez de créer une carte **anonyme** et vous
@ -99,7 +97,9 @@ donne un lien (une URL) pour pouvoir modifier la carte. En effet la
carte que vous avez créée nest associée à aucun compte, et **uMap** carte que vous avez créée nest associée à aucun compte, et **uMap**
considère que seules les personnes ayant ce *lien secret* peuvent la considère que seules les personnes ayant ce *lien secret* peuvent la
modifier. Vous devez donc conserver ce lien si vous souhaitez pouvoir modifier. Vous devez donc conserver ce lien si vous souhaitez pouvoir
modifier la carte. Nous verrons dans [le prochain tutoriel](3-create-account.md) modifier la carte ou saisir votre adresse de courriel pour le recevoir.
Nous verrons dans [le prochain tutoriel](3-create-account.md)
comment créer son catalogue de cartes en utilisant un compte, il nest alors pas comment créer son catalogue de cartes en utilisant un compte, il nest alors pas
nécessaire de conserver de lien secret. nécessaire de conserver de lien secret.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -182,6 +182,7 @@ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True, "APP_DIRS": True,
"DIRS": [os.path.join(PROJECT_DIR, STATIC_ROOT)],
"OPTIONS": { "OPTIONS": {
"context_processors": ( "context_processors": (
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",

View file

@ -266,6 +266,11 @@ button.flat,
min-height: inherit; min-height: inherit;
width: initial; width: initial;
display: initial; display: initial;
line-height: inherit;
}
button.flat:hover,
[type="button"].flat:hover,
.dark [type="button"].flat:hover {
text-decoration: underline; text-decoration: underline;
} }
.help-text, .helptext { .help-text, .helptext {

View file

@ -300,7 +300,7 @@ ul.umap-autocomplete {
} }
.messages .error { .messages .error {
background-color: #c60f13; background-color: var(--color-red);
} }

View file

@ -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;
}
}

View file

@ -10,6 +10,7 @@
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
border-radius: 5px; border-radius: 5px;
overflow-y: auto;
} }
.umap-dialog .umap-close-link { .umap-dialog .umap-close-link {
float: right; float: right;

View file

@ -25,7 +25,7 @@
} }
.panel.full.on { .panel.full.on {
visibility: visible; visibility: visible;
right: var(--panel-gutter); right: calc(var(--panel-gutter) * 2 + var(--control-size));
left: var(--panel-gutter); left: var(--panel-gutter);
height: initial; height: initial;
max-height: initial; max-height: initial;
@ -41,41 +41,6 @@
height: calc(100% - var(--panel-header-height)); /* Minus size of toolbox */ height: calc(100% - var(--panel-header-height)); /* Minus size of toolbox */
padding: var(--panel-gutter); padding: var(--panel-gutter);
} }
.panel .toolbox {
padding: 5px 10px;
overflow: hidden;
display: flex;
flex-direction: row-reverse;
font-size: 10px;
justify-content: flex-start;
gap: 5px;
line-height: 2.2em;
background-color: #fff;
position: sticky;
top: 0;
height: var(--panel-header-height);
}
.panel.dark .toolbox {
background-color: var(--color-darkGray);
}
.panel .toolbox li {
cursor: pointer;
display: inline;
padding: 0 2px;
border: 1px solid #b6b6b3;
border-radius: 2px;
}
.panel.dark .toolbox
.panel.dark .toolbox li {
color: #d3dfeb;
border: 1px solid #202425;
}
.panel .toolbox li:hover {
background-color: #d4d4d2;
}
.panel.dark .toolbox li:hover {
background-color: #353c3e;
}
.panel h3 { .panel h3 {
line-height: 120%; line-height: 120%;
} }

View file

@ -0,0 +1,35 @@
.window .buttons {
padding: 5px 10px;
display: flex;
flex-direction: row-reverse;
font-size: 10px;
justify-content: flex-start;
gap: 5px;
line-height: 2.2em;
background-color: inherit;
position: sticky;
top: 0;
height: var(--panel-header-height);
}
.window .buttons li {
cursor: pointer;
display: inline;
padding: 0 2px;
border: 1px solid #b6b6b3;
border-radius: 2px;
}
.window.dark .buttons
.window.dark .buttons li {
color: #d3dfeb;
border: 1px solid #202425;
}
.window.dark[data-level="error"] .buttons li:hover,
.window.dark[data-level="error"] .buttons li button:hover {
background-color: darkred;
}
.window .buttons li:hover {
background-color: #d4d4d2;
}
.window.dark .buttons li:hover {
background-color: #353c3e;
}

View file

@ -0,0 +1,122 @@
[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 var(--color-darkGray);
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: var(--color-red);
}
[role="dialog"] a {
text-decoration: underline;
}
[role="dialog"] > div {
margin: 0 auto;
}
[role="group"] {
display: inline-flex;
position: relative;
width: 100%;
border-radius: var(--border-radius);
vertical-align: middle;
margin: 0;
padding: 0;
border: none;
}
[role="group"] input:not([type="checkbox"], [type="radio"]):not(:last-child),
[role="group"] > :not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
[role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child),
[role="group"] > :not(:first-child) {
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
width: 33%;
}
[role="group"] input[type="submit"] {
background: var(--color-darkGray);
color: var(--color-light);
border: 1px solid var(--color-light);
line-height: initial;
}
[role="group"] input:not([type="submit"]) {
background: var(--color-light);
color: var(--color-darkGray);
border: 1px solid var(--color-light);
}
[role="group"] input[type="button"] {
background: var(--color-darkGray);
color: var(--color-light);
border: none;
line-height: initial;
}
[role="group"] input[type="button"]:hover {
text-decoration: underline;
border: none;
}
@media only screen and (max-width:770px) {
[role="group"] {
display: flex;
flex-direction: column;
}
[role="group"] input:not([type="checkbox"], [type="radio"]):not(:last-child),
[role="group"] > :not(:last-child) {
border-radius: var(--border-radius);
}
[role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child),
[role="group"] > :not(:first-child) {
border-radius: var(--border-radius);
}
[role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child),
[role="group"] > :not(:first-child) {
width: 100%;
}
}
#link-wrapper {
margin-bottom: 1rem;
}
#conflict-wrapper form {
display: flex;
justify-content: space-around;
}
#conflict-wrapper form [type="submit"] {
width: initial;
background: inherit;
color: var(--color-light);
border: 1px solid var(--color-red);
font-weight: bold;
}
#conflict-wrapper form [type="submit"]:hover {
width: initial;
background: darkred;
color: var(--color-light);
border: 1px solid var(--color-light);
}
@media only screen and (max-width:770px) {
#conflict-wrapper form {
flex-direction: column;
text-align: center;
}
}
umap-alert-choice a {
color: var(--color-light);
text-decoration: underline;
}

View file

@ -0,0 +1,88 @@
{% load i18n static %}
<style type="text/css">
@import "{% static 'umap/js/components/alerts/alert.css' %}";
</style>
<template id="umap-alert-template">
<div role="dialog" class="dark window">
<div>
<p role="alert"></p>
</div>
<ul class="buttons">
<li>
<button class="icon icon-16 icon-close" aria-label="{% translate "Close" %}" data-close></button>
</li>
</ul>
</div>
</template>
<umap-alert></umap-alert>
<template id="umap-alert-creation-template">
<div role="dialog" class="dark window">
<div>
<h3>{% translate "Save the edit link" %}</h3>
<p role="alert"></p>
<div id="link-wrapper">
<form>
<fieldset role="group">
<input type="url" name="url">
<input type="button" value="{% translate "Copy link" %}">
</fieldset>
</form>
</div>
<div id="form-wrapper" hidden>
<p>{% translate "You can also receive that URL by email:" %}</p>
<form>
<fieldset role="group">
<input type="email" name="email" placeholder="{% translate "Email" %}" required>
<input type="submit" value="{% translate "Send me the link" %}" class="umap-action">
</fieldset>
</form>
</div>
<p><em>{% translate "Pro-tip: to easily find back your maps," %} <a href="{% url "login" %}" target="_blank">{% translate "create an account" %}</a></em></p>
</div>
<ul class="buttons">
<li>
<button class="icon icon-16 icon-close" aria-label="{% translate "Close" %}" data-close></button>
</li>
</ul>
</div>
</template>
<umap-alert-creation></umap-alert-creation>
<template id="umap-alert-conflict-template">
<div role="dialog" class="dark window">
<div>
<p role="alert"></p>
<div id="conflict-wrapper">
<form>
<a href="#" onclick="document.url" target="_blank">{% translate "See their edits in another tab" %}</a>
<input id="your-changes" type="submit" value="{% translate "Keep your changes and loose theirs" %}">
<input id="their-changes" type="submit" value="{% translate "Keep their changes and loose yours" %}">
</form>
</div>
</div>
<ul class="buttons">
<li>
<button class="icon icon-16 icon-close" aria-label="{% translate "Close" %}" data-close></button>
</li>
</ul>
</div>
</template>
<umap-alert-conflict></umap-alert-conflict>
<script type="module">
import { register } from '{% static 'umap/js/components/base.js' %}'
import {
uMapAlert,
uMapAlertCreation,
uMapAlertConflict
} from '{% static 'umap/js/components/alerts/alert.js' %}'
register(uMapAlert, 'umap-alert')
register(uMapAlertCreation, 'umap-alert-creation')
register(uMapAlertConflict, 'umap-alert-conflict')
</script>

View file

@ -0,0 +1,160 @@
import { translate } from '../../modules/i18n.js'
import { ServerRequest } from '../../modules/request.js'
import { uMapElement } from '../base.js'
class uMapAlert extends uMapElement {
static get observedAttributes() {
return ['open']
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'open':
newValue === 'open' ? this._show() : this._hide()
break
}
}
static info(message, duration = 5000) {
uMapAlert.emit('alert', { message, duration })
}
// biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default
static error(message, duration = Infinity) {
uMapAlert.emit('alert', { level: 'error', message, duration })
}
constructor() {
super()
this._hide()
this.container = this.querySelector('[role="dialog"]')
this.element = this.container.querySelector('[role="alert"]')
}
_hide() {
this.setAttribute('hidden', 'hidden')
this.removeAttribute('open')
}
_show() {
this.removeAttribute('hidden')
}
_handleClose() {
this.addEventListener('click', (event) => {
if (event.target.closest('[data-close]')) {
this._hide()
}
})
}
onAlert(event) {
const { level = 'info', duration = 5000, message = '' } = event.detail
this.container.dataset.level = level
this.container.dataset.duration = duration
this.element.textContent = message
this.setAttribute('open', 'open')
if (Number.isFinite(duration)) {
setTimeout(() => {
this._hide()
}, duration)
}
}
connectedCallback() {
this._handleClose()
this.listen('alert')
}
}
class uMapAlertCreation extends uMapAlert {
static info(
message,
// biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default
duration = Infinity,
editLink = undefined,
sendLink = undefined
) {
uMapAlertCreation.emit('alertCreation', { message, duration, editLink, sendLink })
}
constructor() {
super()
this.linkWrapper = this.container.querySelector('#link-wrapper')
this.formWrapper = this.container.querySelector('#form-wrapper')
}
onAlertCreation(event) {
const {
level = 'info',
duration = 5000,
message = '',
editLink = undefined,
sendLink = undefined,
} = event.detail
uMapAlert.prototype.onAlert.call(this, { detail: { 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 = translate('✅ 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 ServerRequest()
this.removeAttribute('open')
await server.post(sendLink, {}, formData)
})
}
}
connectedCallback() {
this._handleClose()
this.listen('alertCreation')
}
}
class uMapAlertConflict extends uMapAlert {
static error(
message,
// biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default
duration = Infinity
) {
uMapAlertConflict.emit('alertConflict', { level: 'error', message, duration })
}
constructor() {
super()
this.conflictWrapper = this.container.querySelector('#conflict-wrapper')
}
onAlertConflict(event) {
const { level = 'info', duration = 5000, message = '' } = event.detail
uMapAlert.prototype.onAlert.call(this, { detail: { level, duration, message } })
const form = this.conflictWrapper.querySelector('form')
form.addEventListener('submit', (event) => {
event.preventDefault()
switch (event.submitter.id) {
case 'your-changes':
uMapAlertConflict.emit('alertConflictOverride')
break
case 'their-changes':
window.location.reload()
break
}
this.removeAttribute('open')
})
}
connectedCallback() {
this._handleClose()
this.listen('alertConflict')
}
}
export { uMapAlert, uMapAlertCreation, uMapAlertConflict }

View file

@ -0,0 +1,54 @@
const EVENT_PREFIX = 'umap'
export class uMapElement extends HTMLElement {
static emit(type, detail = {}) {
const event = new CustomEvent(`${EVENT_PREFIX}:${type}`, {
bubbles: true,
cancelable: true,
detail: detail,
})
return document.dispatchEvent(event)
}
/**
* Retrieves a clone of the content template either using the `template`
* attribute or an id mathing the name of the component:
*
* `umap-alert` component => `umap-alert-template` template id lookup.
*/
get template() {
return document
.getElementById(this.getAttribute('template') || `${this.localName}-template`)
.content.cloneNode(true)
}
constructor() {
super()
this.append(this.template)
}
/**
* Special method which allows to easily listen to events
* and have automated event to component method binding.
*
* For instance listening to `alert` will then call `onAlert`.
*/
handleEvent(event) {
event.preventDefault()
// From `umap:alert` to `alert`.
const eventName = event.type.replace(`${EVENT_PREFIX}:`, '')
// From `alert` event type to `onAlert` call against that class.
this[`on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`](event)
}
listen(eventName) {
// Using `this` as a listener will call `handleEvent` under the hood.
document.addEventListener(`${EVENT_PREFIX}:${eventName}`, this)
}
}
export function register(klass, name) {
if ('customElements' in globalThis && !customElements.get(name)) {
customElements.define(name, klass)
}
}

View file

@ -1,7 +1,6 @@
import { DomUtil, DomEvent, setOptions } from '../../vendors/leaflet/leaflet-src.esm.js' import { DomUtil, DomEvent, setOptions } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js' import { translate } from './i18n.js'
import { ServerRequest } from './request.js' import { ServerRequest } from './request.js'
import Alert from './ui/alert.js'
export class BaseAutocomplete { export class BaseAutocomplete {
constructor(el, options) { constructor(el, options) {
@ -220,8 +219,7 @@ export class BaseAutocomplete {
class BaseAjax extends BaseAutocomplete { class BaseAjax extends BaseAutocomplete {
constructor(el, options) { constructor(el, options) {
super(el, options) super(el, options)
const alert = new Alert(document.querySelector('header')) this.server = new ServerRequest()
this.server = new ServerRequest(alert)
} }
optionToResult(option) { optionToResult(option) {
return { return {

View file

@ -3,7 +3,6 @@ import Browser from './browser.js'
import Facets from './facets.js' import Facets from './facets.js'
import Caption from './caption.js' import Caption from './caption.js'
import { Panel, EditPanel, FullPanel } from './ui/panel.js' import { Panel, EditPanel, FullPanel } from './ui/panel.js'
import Alert from './ui/alert.js'
import Dialog from './ui/dialog.js' import Dialog from './ui/dialog.js'
import Tooltip from './ui/tooltip.js' import Tooltip from './ui/tooltip.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
@ -14,6 +13,11 @@ import Orderable from './orderable.js'
import Importer from './importer.js' import Importer from './importer.js'
import Help from './help.js' import Help from './help.js'
import { SyncEngine } from './sync/engine.js' import { SyncEngine } from './sync/engine.js'
import {
uMapAlert as Alert,
uMapAlertCreation as AlertCreation,
uMapAlertConflict as AlertConflict,
} from '../components/alerts/alert.js'
// Import modules and export them to the global scope. // Import modules and export them to the global scope.
// For the not yet module-compatible JS out there. // For the not yet module-compatible JS out there.
@ -21,6 +25,8 @@ import { SyncEngine } from './sync/engine.js'
// By alphabetic order // By alphabetic order
window.U = { window.U = {
Alert, Alert,
AlertCreation,
AlertConflict,
AjaxAutocomplete, AjaxAutocomplete,
AjaxAutocompleteMultiple, AjaxAutocompleteMultiple,
Browser, Browser,

View file

@ -1,5 +1,6 @@
import { DomUtil, DomEvent } from '../../vendors/leaflet/leaflet-src.esm.js' import { DomUtil, DomEvent } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js' import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
export default class Importer { export default class Importer {
constructor(map) { constructor(map) {
@ -163,16 +164,14 @@ export default class Importer {
this.map.processFileToImport(file, layer, type) this.map.processFileToImport(file, layer, type)
} }
} else { } else {
if (!type) if (!type) {
return this.map.alert.open({ return Alert.error(L._('Please choose a format'))
content: translate('Please choose a format'), }
level: 'error',
})
if (this.rawInput.value && type === 'umap') { if (this.rawInput.value && type === 'umap') {
try { try {
this.map.importRaw(this.rawInput.value, type) this.map.importRaw(this.rawInput.value, type)
} catch (e) { } catch (e) {
this.alert.open({ content: translate('Invalid umap data'), level: 'error' }) Alert.error(L._('Invalid umap data'))
console.error(e) console.error(e)
} }
} else { } else {

View file

@ -1,5 +1,5 @@
// Uses `L._`` from Leaflet.i18n which we cannot import as a module yet import { translate } from './i18n.js'
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' import { uMapAlert as Alert } from '../components/alerts/alert.js'
export class RequestError extends Error {} export class RequestError extends Error {}
@ -47,11 +47,6 @@ class BaseRequest {
// In case of error, an alert is sent, but non 20X status are not handled // In case of error, an alert is sent, but non 20X status are not handled
// The consumer must check the response status by hand // The consumer must check the response status by hand
export class Request extends BaseRequest { export class Request extends BaseRequest {
constructor(alert) {
super()
this.alert = alert
}
fire(name, params) { fire(name, params) {
document.body.dispatchEvent(new CustomEvent(name, params)) document.body.dispatchEvent(new CustomEvent(name, params))
} }
@ -85,7 +80,7 @@ export class Request extends BaseRequest {
} }
_onError(error) { _onError(error) {
this.alert.open({ content: L._('Problem in the response'), level: 'error' }) Alert.error(translate('Problem in the response'))
} }
_onNOK(error) { _onNOK(error) {
@ -131,9 +126,9 @@ export class ServerRequest extends Request {
try { try {
const data = await response.json() const data = await response.json()
if (data.info) { if (data.info) {
this.alert.open({ content: data.info, level: 'info' }) Alert.info(data.info)
} else if (data.error) { } else if (data.error) {
this.alert.open({ content: data.error, level: 'error' }) Alert.error(data.error)
return this._onError(new Error(data.error)) return this._onError(new Error(data.error))
} }
return [data, response, null] return [data, response, null]
@ -148,10 +143,7 @@ export class ServerRequest extends Request {
_onNOK(error) { _onNOK(error) {
if (error.status === 403) { if (error.status === 403) {
this.alert.open({ Alert.error(error.message || translate('Action not allowed :('))
content: error.message || L._('Action not allowed :('),
level: 'error',
})
} }
return [{}, error.response, error] return [{}, error.response, error]
} }

View file

@ -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)
}
}
}

View file

@ -4,7 +4,7 @@ import { translate } from '../i18n.js'
export default class Dialog { export default class Dialog {
constructor(parent) { constructor(parent) {
this.parent = parent this.parent = parent
this.container = DomUtil.create('dialog', 'umap-dialog', this.parent) this.container = DomUtil.create('dialog', 'umap-dialog window', this.parent)
DomEvent.disableClickPropagation(this.container) DomEvent.disableClickPropagation(this.container)
DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu. 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, 'wheel', DomEvent.stopPropagation)
@ -26,15 +26,14 @@ export default class Dialog {
if (className) { if (className) {
this.container.classList.add(className) this.container.classList.add(className)
} }
const closeButton = DomUtil.createButton( const buttonsContainer = DomUtil.create('ul', 'buttons', this.container)
'umap-close-link', const closeButton = DomUtil.createButtonIcon(
this.container, DomUtil.create('li', '', buttonsContainer),
'', 'icon-close',
() => this.container.close() translate('Close')
) )
DomUtil.createIcon(closeButton, 'icon-close') DomEvent.on(closeButton, 'click', this.close, this)
const label = DomUtil.create('span', '', closeButton) this.container.appendChild(buttonsContainer)
label.title = label.textContent = translate('Close')
this.container.appendChild(content) this.container.appendChild(content)
} }
} }

View file

@ -25,27 +25,34 @@ export class Panel {
} }
open({ content, className, actions = [] } = {}) { open({ content, className, actions = [] } = {}) {
this.container.className = `with-transition panel ${this.classname} ${this.mode || ''}` this.container.className = `with-transition panel window ${this.classname} ${
this.mode || ''
}`
this.container.innerHTML = '' this.container.innerHTML = ''
const actionsContainer = DomUtil.create('ul', 'toolbox', this.container) const actionsContainer = DomUtil.create('ul', 'buttons', this.container)
const body = DomUtil.create('div', 'body', this.container) const body = DomUtil.create('div', 'body', this.container)
body.appendChild(content) body.appendChild(content)
const closeLink = DomUtil.create('li', 'umap-close-link', actionsContainer) const closeButton = DomUtil.createButtonIcon(
DomUtil.add('i', 'icon icon-16 icon-close', closeLink) DomUtil.create('li', '', actionsContainer),
closeLink.title = translate('Close') 'icon-close',
const resizeLink = DomUtil.create('li', 'umap-resize-link', actionsContainer) translate('Close')
DomUtil.add('i', 'icon icon-16 icon-resize', resizeLink) )
resizeLink.title = translate('Toggle size') const resizeButton = DomUtil.createButtonIcon(
for (let action of actions) { DomUtil.create('li', '', actionsContainer),
actionsContainer.appendChild(action) 'icon-resize',
translate('Toggle size')
)
for (const action of actions) {
const element = DomUtil.element({ tagName: 'li', parent: actionsContainer })
element.appendChild(action)
} }
if (className) DomUtil.addClass(body, className) if (className) DomUtil.addClass(body, className)
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
DomUtil.addClass(this.container, 'on') DomUtil.addClass(this.container, 'on')
resolve() resolve()
}) })
DomEvent.on(closeLink, 'click', this.close, this) DomEvent.on(closeButton, 'click', this.close, this)
DomEvent.on(resizeLink, 'click', this.resize, this) DomEvent.on(resizeButton, 'click', this.resize, this)
return promise return promise
} }

View file

@ -84,7 +84,7 @@ U.UpdateExtentAction = U.BaseAction.extend({
}, },
addHooks: function () { addHooks: function () {
this.map.updateExtent() this.map.setCenterAndZoom()
}, },
}) })
@ -1086,7 +1086,7 @@ U.Search = L.PhotonSearch.extend({
if (latlng.isValid()) { if (latlng.isValid()) {
this.reverse.doReverse(latlng) this.reverse.doReverse(latlng)
} else { } else {
this.map.alert.open({ content: 'Invalid latitude or longitude', mode: 'error' }) U.Alert.error(L._('Invalid latitude or longitude'))
} }
return return
} }

View file

@ -764,10 +764,7 @@ U.Marker = L.Marker.extend({
const builder = new U.FormBuilder(this, coordinatesOptions, { const builder = new U.FormBuilder(this, coordinatesOptions, {
callback: function () { callback: function () {
if (!this._latlng.isValid()) { if (!this._latlng.isValid()) {
this.map.alert.open({ U.Alert.error(L._('Invalid latitude or longitude'))
content: L._('Invalid latitude or longitude'),
level: 'error',
})
builder.resetField('_latlng.lat') builder.resetField('_latlng.lat')
builder.resetField('_latlng.lng') builder.resetField('_latlng.lng')
} }
@ -966,7 +963,7 @@ U.PathMixin = {
items.push({ items.push({
text: L._('Display measure'), text: L._('Display measure'),
callback: function () { callback: function () {
this.map.alert.open({ content: this.getMeasure(), level: 'info' }) U.Alert.info(this.getMeasure())
}, },
context: this, context: this,
}) })

View file

@ -13,7 +13,7 @@ L.Map.mergeOptions({
// we cannot rely on this because of the y is overriden by Leaflet // we cannot rely on this because of the y is overriden by Leaflet
// See https://github.com/Leaflet/Leaflet/pull/9201 // See https://github.com/Leaflet/Leaflet/pull/9201
// And let's remove this -y when this PR is merged and released. // 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: [], licences: [],
licence: '', licence: '',
enableMarkerDraw: true, enableMarkerDraw: true,
@ -59,7 +59,6 @@ U.Map = L.Map.extend({
this.urls = new U.URLs(this.options.urls) this.urls = new U.URLs(this.options.urls)
this.panel = new U.Panel(this) this.panel = new U.Panel(this)
this.alert = new U.Alert(this._controlContainer)
this.tooltip = new U.Tooltip(this._controlContainer) this.tooltip = new U.Tooltip(this._controlContainer)
this.dialog = new U.Dialog(this._controlContainer) this.dialog = new U.Dialog(this._controlContainer)
if (this.hasEditMode()) { 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, 'dataloading', (e) => this.fire('dataloading', e))
L.DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e)) L.DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e))
this.server = new U.ServerRequest(this.alert) this.server = new U.ServerRequest()
this.request = new U.Request(this.alert) this.request = new U.Request()
this.initLoader() this.initLoader()
this.name = this.options.name this.name = this.options.name
@ -391,7 +390,7 @@ U.Map = L.Map.extend({
icon: 'umap-fake-class', icon: 'umap-fake-class',
iconLoading: 'umap-fake-class', iconLoading: 'umap-fake-class',
flyTo: this.options.easing, 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({ this._controls.fullscreen = new L.Control.Fullscreen({
title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') },
@ -680,10 +679,7 @@ U.Map = L.Map.extend({
} catch (e) { } catch (e) {
console.error(e) console.error(e)
this.removeLayer(tilelayer) this.removeLayer(tilelayer)
this.alert.open({ U.Alert.error(`${L._('Error in the tilelayer URL')}: ${tilelayer._url}`)
content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`,
level: 'error',
})
// Users can put tilelayer URLs by hand, and if they add wrong {variable}, // 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 // Leaflet throw an error, and then the map is no more editable
} }
@ -715,10 +711,7 @@ U.Map = L.Map.extend({
} catch (e) { } catch (e) {
this.removeLayer(overlay) this.removeLayer(overlay)
console.error(e) console.error(e)
this.alert.open({ U.Alert.error(`${L._('Error in the overlay URL')}: ${overlay._url}`)
content: `${L._('Error in the overlay URL')}: ${overlay._url}`,
level: 'error',
})
} }
}, },
@ -835,19 +828,16 @@ U.Map = L.Map.extend({
return this.getDefaultOption(option) return this.getDefaultOption(option)
}, },
updateExtent: function () { setCenterAndZoom: function () {
this._setCenterAndZoom()
U.Alert.info(L._('The zoom and center have been modified.'))
},
_setCenterAndZoom: function () {
this.options.center = this.getCenter() this.options.center = this.getCenter()
this.options.zoom = this.getZoom() this.options.zoom = this.getZoom()
this.isDirty = true this.isDirty = true
this._default_extent = false this._default_extent = false
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',
})
}
}, },
updateTileLayers: function () { updateTileLayers: function () {
@ -886,12 +876,11 @@ U.Map = L.Map.extend({
processFileToImport: function (file, layer, type) { processFileToImport: function (file, layer, type) {
type = type || U.Utils.detectFileType(file) type = type || U.Utils.detectFileType(file)
if (!type) { if (!type) {
this.alert.open({ U.Alert.error(
content: L._('Unable to detect format of file {filename}', { L._('Unable to detect format of file {filename}', {
filename: file.name, filename: file.name,
}), })
level: 'error', )
})
return return
} }
if (type === 'umap') { if (type === 'umap') {
@ -947,10 +936,7 @@ U.Map = L.Map.extend({
self.importRaw(rawData) self.importRaw(rawData)
} catch (e) { } catch (e) {
console.error('Error importing data', e) console.error('Error importing data', e)
self.alert.open({ U.Alert.error(L._('Invalid umap data in {filename}', { filename: file.name }))
content: L._('Invalid umap data in {filename}', { filename: file.name }),
level: 'error',
})
} }
} }
}, },
@ -1058,62 +1044,59 @@ U.Map = L.Map.extend({
const [data, _, error] = await this.server.post(uri, {}, formData) const [data, _, error] = await this.server.post(uri, {}, formData)
// FIXME: login_required response will not be an error, so it will not // FIXME: login_required response will not be an error, so it will not
// stop code while it should // stop code while it should
if (!error) { if (error) {
let duration = 3000, return
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:'
) + `<br>${data.permissions.anonymous_edit_url}`
alert.actions = [ if (!this.options.umap_id) {
{ this.options.umap_id = data.id
label: L._('Copy link'), this.permissions.setOptions(data.permissions)
callback: () => { this.permissions.commit()
L.Util.copyToClipboard(data.permissions.anonymous_edit_url) if (data?.permissions?.anonymous_edit_url) {
this.alert.open({ const send_edit_link_url =
content: L._('Secret edit link copied to clipboard!'), this.options.urls.map_send_edit_link &&
level: 'info', this.urls.get('map_send_edit_link', {
}) map_id: this.options.umap_id,
}, })
callbackContext: this, this.once('saved', () => {
}, U.AlertCreation.info(
] L._(
if (this.options.urls.map_send_edit_link) { 'Your map has been created! As you are not logged in, ' +
alert.actions.push({ 'here is your secret link to edit the map, please keep it safe:'
label: L._('Send me the link'), ),
input: L._('Email'), Number.Infinity,
callback: this.sendEditLink, data.permissions.anonymous_edit_url,
callbackContext: this, send_edit_link_url
}) )
} })
} } else {
} else if (!this.permissions.isDirty) { 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, // Do not override local changes to permissions,
// but update in case some other editors changed them in the meantime. // but update in case some other editors changed them in the meantime.
this.permissions.setOptions(data.permissions) this.permissions.setOptions(data.permissions)
this.permissions.commit() this.permissions.commit()
} }
// Update URL in case the name has changed. this.once('saved', () => {
if (history && history.pushState) U.Alert.info(data.info || L._('Map has been saved!'))
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()
} }
// 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 () { save: function () {
if (!this.isDirty) return if (!this.isDirty) return
if (this._default_extent) this.updateExtent() if (this._default_extent) this._setCenterAndZoom()
this.backup() this.backup()
this.once('saved', () => { this.once('saved', () => {
this.isDirty = false this.isDirty = false
@ -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 () { star: async function () {
if (!this.options.umap_id) if (!this.options.umap_id) {
return this.alert.open({ return U.Alert.error(L._('Please save the map first'))
content: L._('Please save the map first'), }
level: 'error',
})
const url = this.urls.get('map_star', { map_id: this.options.umap_id }) const url = this.urls.get('map_star', { map_id: this.options.umap_id })
const [data, response, error] = await this.server.post(url) const [data, response, error] = await this.server.post(url)
if (!error) { if (error) {
this.options.starred = data.starred return
let msg = data.starred
? L._('Map has been starred')
: L._('Map has been unstarred')
this.alert.open({ content: msg, level: 'info' })
this.renderControls()
} }
this.options.starred = data.starred
U.Alert.info(
data.starred ? L._('Map has been starred') : L._('Map has been unstarred')
)
this.renderControls()
}, },
geometry: function () { geometry: function () {

View file

@ -958,7 +958,7 @@ U.DataLayer = L.Evented.extend({
const doc = new DOMParser().parseFromString(x, 'text/xml') const doc = new DOMParser().parseFromString(x, 'text/xml')
const errorNode = doc.querySelector('parsererror') const errorNode = doc.querySelector('parsererror')
if (errorNode) { if (errorNode) {
this.map.alert.open({ content: L._('Cannot parse data'), level: 'error' }) U.Alert.error(L._('Cannot parse data'))
} }
return doc return doc
} }
@ -993,7 +993,7 @@ U.DataLayer = L.Evented.extend({
message: err[0].message, message: err[0].message,
}) })
} }
this.map.alert.open({ content: message, level: 'error', duration: 10000 }) U.Alert.error(message, 10000)
console.error(err) console.error(err)
} }
if (result && result.features.length) { if (result && result.features.length) {
@ -1020,7 +1020,7 @@ U.DataLayer = L.Evented.extend({
const gj = JSON.parse(c) const gj = JSON.parse(c)
callback(gj) callback(gj)
} catch (err) { } catch (err) {
this.map.alert.open({ content: `Invalid JSON file: ${err}` }) U.Alert.error(`Invalid JSON file: ${err}`)
return return
} }
} }
@ -1121,12 +1121,11 @@ U.DataLayer = L.Evented.extend({
return this.geojsonToFeatures(geometry.geometries) return this.geojsonToFeatures(geometry.geometries)
default: default:
this.map.alert.open({ U.Alert.error(
content: L._('Skipping unknown geometry.type: {type}', { L._('Skipping unknown geometry.type: {type}', {
type: geometry.type || 'undefined', type: geometry.type || 'undefined',
}), })
level: 'error', )
})
} }
}, },
@ -1467,17 +1466,19 @@ U.DataLayer = L.Evented.extend({
'_blank' '_blank'
) )
} }
const button = L.DomUtil.create('li', '') const backButton = L.DomUtil.createButtonIcon(
L.DomUtil.create('i', 'icon icon-16 icon-back', button) undefined,
button.title = L._('Back to layers') 'icon-back',
L._('Back to layers')
)
// Fixme: remove me when this is merged and released // Fixme: remove me when this is merged and released
// https://github.com/Leaflet/Leaflet/pull/9052 // https://github.com/Leaflet/Leaflet/pull/9052
L.DomEvent.disableClickPropagation(button) L.DomEvent.disableClickPropagation(backButton)
L.DomEvent.on(button, 'click', this.map.editDatalayers, this.map) L.DomEvent.on(backButton, 'click', this.map.editDatalayers, this.map)
this.map.editPanel.open({ this.map.editPanel.open({
content: container, content: container,
actions: [button], actions: [backButton],
}) })
}, },
@ -1706,27 +1707,14 @@ U.DataLayer = L.Evented.extend({
const [data, response, error] = await this.map.server.post(url, headers, formData) const [data, response, error] = await this.map.server.post(url, headers, formData)
if (error) { if (error) {
if (response && response.status === 412) { if (response && response.status === 412) {
const msg = L._( U.AlertConflict.error(
'Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.' L._(
'Whoops! Other contributor(s) changed some of the same map elements as you. ' +
'This situation is tricky, you have to choose carefully which version is pertinent.'
)
) )
const actions = [ document.addEventListener('umap:alertConflictOverride', async (event) => {
{ await this._trySave(url, {}, formData)
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,
}) })
} }
} else { } else {

View file

@ -52,11 +52,9 @@ U.MapPermissions = L.Class.extend({
edit: function () { edit: function () {
if (this.map.options.editMode !== 'advanced') return if (this.map.options.editMode !== 'advanced') return
if (!this.map.options.umap_id) if (!this.map.options.umap_id) {
return this.map.alert.open({ return U.Alert.info(L._('Please save the map first'))
content: L._('Please save the map first'), }
level: 'info',
})
const container = L.DomUtil.create('div', 'permissions-panel') const container = L.DomUtil.create('div', 'permissions-panel')
const fields = [] const fields = []
L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key') 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()) const [data, response, error] = await this.map.server.post(this.getAttachUrl())
if (!error) { if (!error) {
this.options.owner = this.map.options.user this.options.owner = this.map.options.user
this.map.alert.open({ U.Alert.info(L._('Map has been attached to your account'))
content: L._('Map has been attached to your account'),
level: 'info',
})
this.map.editPanel.close() this.map.editPanel.close()
} }
}, },

View file

@ -83,10 +83,7 @@ U.TableEditor = L.Class.extend({
validateName: function (name) { validateName: function (name) {
if (name.indexOf('.') !== -1) { if (name.indexOf('.') !== -1) {
this.datalayer.map.alert.open({ U.Alert.error(L._('Invalide property name: {name}', { name: name }))
content: L._('Invalide property name: {name}', { name: name }),
level: 'error',
})
return false return false
} }
return true return true
@ -98,10 +95,13 @@ U.TableEditor = L.Class.extend({
this.renderHeaders() this.renderHeaders()
this.body.innerHTML = '' this.body.innerHTML = ''
this.datalayer.eachLayer(this.renderRow, this) this.datalayer.eachLayer(this.renderRow, this)
const addButton = L.DomUtil.create('li', 'add-property') const addButton = L.DomUtil.createButton(
L.DomUtil.createIcon(addButton, 'icon-add') 'flat',
const label = L.DomUtil.create('span', '', addButton) undefined,
label.textContent = label.title = L._('Add a new property') L._('Add a new property')
)
const iconElement = L.DomUtil.createIcon(addButton, 'icon-add')
addButton.insertBefore(iconElement, addButton.firstChild)
const addProperty = function () { const addProperty = function () {
const newName = prompt(L._('Please enter the name of the property')) const newName = prompt(L._('Please enter the name of the property'))
if (!newName || !this.validateName(newName)) return if (!newName || !this.validateName(newName)) return

View file

@ -659,14 +659,14 @@ ul.photon-autocomplete {
} }
.umap-caption-bar-enabled .umap-caption-bar { .umap-caption-bar-enabled .umap-caption-bar {
display: block; display: block;
height: var(--header-height); height: var(--footer-height);
background-color: #fff; background-color: #fff;
width: 100%; width: 100%;
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
padding: 0 0 0 5px; padding: var(--gutter);
text-align: left; text-align: left;
line-height: 100%; line-height: 100%;
cursor: auto; cursor: auto;

View file

@ -8,6 +8,7 @@
--color-limeGreen: #b9f5d2; --color-limeGreen: #b9f5d2;
--color-brightCyan: #46ece6; --color-brightCyan: #46ece6;
--color-lightCyan: #d4fbf9; --color-lightCyan: #d4fbf9;
--color-red: #c60f13;
--background-color: var(--color-light); --background-color: var(--color-light);
--color-accent: var(--color-brightCyan); --color-accent: var(--color-brightCyan);
@ -20,15 +21,17 @@
--button-neutral-color: var(--color-darkGray); --button-neutral-color: var(--color-darkGray);
/* Sizes and spaces */ /* Sizes and spaces */
--gutter: 8px;
--panel-gutter: 10px; --panel-gutter: 10px;
--panel-bottom: 40px; --panel-bottom: 40px;
--panel-header-height: 36px; --panel-header-height: 36px;
--panel-width: 400px; --panel-width: 400px;
--header-height: 46px; --header-height: 46px;
--current-header-height: 0px; --current-header-height: 0px;
--footer-height: 46px; --footer-height: 28px;
--current-footer-height: 0px; --current-footer-height: 0px;
--control-size: 36px; --control-size: 36px;
--border-radius: 4px;
} }
.dark { .dark {
--background-color: var(--color-darkGray); --background-color: var(--color-darkGray);

View file

@ -38,8 +38,7 @@
{{ block.super }} {{ block.super }}
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener('DOMContentLoaded', event => { window.addEventListener('DOMContentLoaded', event => {
const alert = new U.Alert(document.querySelector('header')) const server = new U.ServerRequest()
const server = new U.ServerRequest(alert)
const getMore = async function (e) { const getMore = async function (e) {
L.DomEvent.stop(e) L.DomEvent.stop(e)
const [{html}, response, error] = await server.get(this.href) const [{html}, response, error] = await server.get(this.href)

View file

@ -29,7 +29,7 @@
<link rel="stylesheet" href="{% static 'umap/nav.css' %}" /> <link rel="stylesheet" href="{% static 'umap/nav.css' %}" />
<link rel="stylesheet" href="{% static 'umap/map.css' %}" /> <link rel="stylesheet" href="{% static 'umap/map.css' %}" />
<link rel="stylesheet" href="{% static 'umap/css/panel.css' %}" /> <link rel="stylesheet" href="{% static 'umap/css/panel.css' %}" />
<link rel="stylesheet" href="{% static 'umap/css/alert.css' %}" /> <link rel="stylesheet" href="{% static 'umap/css/window.css' %}" />
<link rel="stylesheet" href="{% static 'umap/css/tooltip.css' %}" /> <link rel="stylesheet" href="{% static 'umap/css/tooltip.css' %}" />
<link rel="stylesheet" href="{% static 'umap/css/dialog.css' %}" /> <link rel="stylesheet" href="{% static 'umap/css/dialog.css' %}" />
<link rel="stylesheet" href="{% static 'umap/theme.css' %}" /> <link rel="stylesheet" href="{% static 'umap/theme.css' %}" />

View file

@ -1,17 +1,11 @@
{% load umap_tags %} {% load umap_tags %}
{% include "umap/messages.html" %}
<div id="map"></div> <div id="map"></div>
<!-- djlint:off --> <!-- djlint:off -->
<script defer type="text/javascript"> <script defer type="text/javascript">
window.addEventListener('DOMContentLoaded', (event) => { window.addEventListener('DOMContentLoaded', (event) => {
U.MAP = new U.Map("map", {{ map_settings|notag|safe }}) U.MAP = new U.Map("map", {{ map_settings|notag|safe }})
{% for m in messages %}
{# We have just one, but we need to loop, as for messages API #}
U.MAP.alert.open({
content: "{{ m }}",
level: "{{ m.tags }}",
duration: 100000
})
{% endfor %}
}) })
</script> </script>
<!-- djlint:on --> <!-- djlint:on -->

View file

@ -1,11 +1,9 @@
<div class="wrapper"> {% load i18n %}
<div class="row">
{% if messages %} {% include "umap/js/components/alerts/alert.html" %}
<ul class="messages">
{% for message in messages %} {% for message in messages %}
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}>{{ message }}</li> <script type="module" defer>
{% endfor %} U.Alert.info("{{ message }}")
</ul> </script>
{% endif %} {% endfor %}
</div>
</div>

View file

@ -127,6 +127,8 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
data.setdefault("_umap_options", {}) data.setdefault("_umap_options", {})
kwargs["settings"]["name"] = kwargs["name"] kwargs["settings"]["name"] = kwargs["name"]
data["_umap_options"]["name"] = kwargs["name"] data["_umap_options"]["name"] = kwargs["name"]
data.setdefault("type", "FeatureCollection")
data.setdefault("features", [])
kwargs["geojson"] = ContentFile(json.dumps(data), "foo.json") kwargs["geojson"] = ContentFile(json.dumps(data), "foo.json")
return kwargs return kwargs

View file

@ -7,6 +7,11 @@ import pytest
from playwright.sync_api import expect from playwright.sync_api import expect
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {**browser_context_args, "locale": "en-GB", "timezone_id": "Europe/Paris"}
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def set_timeout(context): def set_timeout(context):
timeout = int(os.environ.get("PLAYWRIGHT_TIMEOUT", 7500)) timeout = int(os.environ.get("PLAYWRIGHT_TIMEOUT", 7500))

View file

@ -164,7 +164,7 @@ def test_alert_message_after_create(
page.goto(f"{live_server.url}/en/map/new") page.goto(f"{live_server.url}/en/map/new")
save = page.get_by_role("button", name="Save") save = page.get_by_role("button", name="Save")
expect(save).to_be_visible() expect(save).to_be_visible()
alert = page.locator("#umap-alert-container") alert = page.locator('umap-alert-creation div[role="dialog"]')
expect(alert).to_be_hidden() expect(alert).to_be_hidden()
with page.expect_response(re.compile(r".*/map/create/")): with page.expect_response(re.compile(r".*/map/create/")):
save.click() save.click()
@ -194,14 +194,15 @@ def test_alert_message_after_create(
def test_email_sending_error_are_catched(tilelayer, page, live_server): def test_email_sending_error_are_catched(tilelayer, page, live_server):
page.goto(f"{live_server.url}/en/map/new") page.goto(f"{live_server.url}/en/map/new")
alert = page.locator("#umap-alert-container") alert_creation = page.locator('umap-alert-creation div[role="dialog"]')
with page.expect_response(re.compile(r".*/map/create/")): with page.expect_response(re.compile(r".*/map/create/")):
page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Save").click()
alert.get_by_placeholder("Email").fill("foo@bar.com") alert_creation.get_by_placeholder("Email").fill("foo@bar.com")
with patch("umap.views.send_mail", side_effect=SMTPException) as patched: with patch("umap.views.send_mail", side_effect=SMTPException) as patched:
with page.expect_response(re.compile("/en/map/.*/send-edit-link/")): with page.expect_response(re.compile("/en/map/.*/send-edit-link/")):
alert.get_by_role("button", name="Send me the link").click() alert_creation.get_by_role("button", name="Send me the link").click()
assert patched.called assert patched.called
alert = page.locator('umap-alert div[role="dialog"]')
expect(alert.get_by_text("Can't send email to foo@bar.com")).to_be_visible() expect(alert.get_by_text("Can't send email to foo@bar.com")).to_be_visible()
@ -214,7 +215,7 @@ def test_alert_message_after_create_show_link_even_without_mail(
page.goto(f"{live_server.url}/en/map/new") page.goto(f"{live_server.url}/en/map/new")
with page.expect_response(re.compile(r".*/map/create/")): with page.expect_response(re.compile(r".*/map/create/")):
page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Save").click()
alert = page.locator("#umap-alert-container") alert = page.locator('umap-alert-creation div[role="dialog"]')
expect(alert).to_be_visible() expect(alert).to_be_visible()
expect( expect(
alert.get_by_text( alert.get_by_text(

View file

@ -228,7 +228,7 @@ def test_facets_search_are_persistent_when_closing_panel(live_server, page, map)
DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA1)
DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA2)
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670")
panel = page.locator(".umap-browser") panel = page.locator(".panel.left")
# Facet values # Facet values
odd = page.get_by_label("odd") odd = page.get_by_label("odd")
@ -266,7 +266,7 @@ def test_facets_search_are_persistent_when_closing_panel(live_server, page, map)
# Close panel # Close panel
expect(panel.locator("summary")).to_have_attribute("data-badge", " ") expect(panel.locator("summary")).to_have_attribute("data-badge", " ")
expect(page.locator(".umap-control-browse")).to_have_attribute("data-badge", " ") expect(page.locator(".umap-control-browse")).to_have_attribute("data-badge", " ")
page.get_by_role("listitem", name="Close").click() panel.get_by_role("button", name="Close").click()
page.get_by_role("button", name="See layers").click() page.get_by_role("button", name="See layers").click()
expect(panel.get_by_label("Min")).to_have_value("13") expect(panel.get_by_label("Min")).to_have_value("13")
expect(panel.get_by_label("Min")).to_have_attribute("data-modified", "true") expect(panel.get_by_label("Min")).to_have_attribute("data-modified", "true")

View file

@ -448,4 +448,4 @@ def test_import_csv_without_valid_latlon_headers(tilelayer, live_server, page):
# FIXME do not create a layer # FIXME do not create a layer
expect(layers).to_have_count(1) expect(layers).to_have_count(1)
expect(markers).to_have_count(0) expect(markers).to_have_count(0)
expect(page.locator("#umap-alert-container")).to_be_visible() expect(page.locator('umap-alert div[data-level="error"]')).to_be_visible()

View file

@ -288,9 +288,9 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
saved = DataLayer.objects.last() saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text()) data = json.loads(Path(saved.geojson.path).read_text())
assert data["features"][0]["properties"]["name"] == "new name" assert data["features"][0]["properties"]["name"] == "new name"
expect(page_two.get_by_text("Woops! Someone else seems to")).to_be_visible() expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")): with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
page_two.get_by_role("button", name="Save anyway").click() page_two.get_by_text("Keep your changes and loose theirs").click()
saved = DataLayer.objects.last() saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text()) data = json.loads(Path(saved.geojson.path).read_text())
assert data["features"][0]["properties"]["name"] == "custom name" assert data["features"][0]["properties"]["name"] == "custom name"

View file

@ -171,7 +171,7 @@ def test_can_use_remote_url_as_picto(openmap, live_server, page, pictos):
input_el.blur() input_el.blur()
expect(marker).to_have_attribute("src", "https://foo.bar/img.jpg") expect(marker).to_have_attribute("src", "https://foo.bar/img.jpg")
# Now close and reopen the form, it should still be the URL tab # Now close and reopen the form, it should still be the URL tab
close = page.locator(".panel.right.on .toolbox").get_by_title("Close") close = page.locator(".panel.right.on .buttons").get_by_title("Close")
expect(close).to_be_visible() expect(close).to_be_visible()
close.click() close.click()
edit_settings.click() edit_settings.click()
@ -210,7 +210,7 @@ def test_can_use_char_as_picto(openmap, live_server, page, pictos):
expect(marker).to_have_count(1) expect(marker).to_have_count(1)
expect(marker).to_have_text("") expect(marker).to_have_text("")
# Now close and reopen the form, it should still be the URL tab # Now close and reopen the form, it should still be the URL tab
close = page.locator(".panel.right.on .toolbox").get_by_title("Close") close = page.locator(".panel.right.on .buttons").get_by_title("Close")
expect(close).to_be_visible() expect(close).to_be_visible()
close.click() close.click()
edit_settings.click() edit_settings.click()

View file

@ -906,6 +906,7 @@ class MapDelete(DeleteView):
return HttpResponseForbidden(_("Only its owner can delete the map.")) return HttpResponseForbidden(_("Only its owner can delete the map."))
self.object.delete() self.object.delete()
home_url = reverse("home") home_url = reverse("home")
messages.info(self.request, _("Map successfully deleted."))
if is_ajax(self.request): if is_ajax(self.request):
return simple_json_response(redirect=home_url) return simple_json_response(redirect=home_url)
else: else:
@ -1109,7 +1110,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
reference = json.loads(f.read()) reference = json.loads(f.read())
break break
else: else:
# If the document is not found, we can't merge. # If the reference document is not found, we can't merge.
return None return None
# New data received in the request. # New data received in the request.
incoming = json.loads(self.request.FILES["geojson"].read()) incoming = json.loads(self.request.FILES["geojson"].read())