mirror of
https://github.com/umap-project/umap.git
synced 2025-04-30 20:12:37 +02:00
Merge pull request #1876 from umap-project/dom-driven-alerts-2
Use web components to display alerts + a11y roles
This commit is contained in:
commit
7660153b3b
40 changed files with 666 additions and 438 deletions
|
@ -89,9 +89,7 @@ texte est affiché en haut de la carte, comme celui ci-dessous :
|
||||||
data-alt="Message d’alerte contenant le lien d’édition."
|
data-alt="Message d’alerte 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 d’alerte contenant le lien d’édition.</shot-scraper>
|
>Message d’alerte 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 n’est associée à aucun compte, et **uMap**
|
carte que vous avez créée n’est 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 n’est alors pas
|
comment créer son catalogue de cartes en utilisant un compte, il n’est 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 |
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -300,7 +300,7 @@ ul.umap-autocomplete {
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages .error {
|
.messages .error {
|
||||||
background-color: #c60f13;
|
background-color: var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
|
35
umap/static/umap/css/window.css
Normal file
35
umap/static/umap/css/window.css
Normal 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;
|
||||||
|
}
|
122
umap/static/umap/js/components/alerts/alert.css
Normal file
122
umap/static/umap/js/components/alerts/alert.css
Normal 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;
|
||||||
|
}
|
88
umap/static/umap/js/components/alerts/alert.html
Normal file
88
umap/static/umap/js/components/alerts/alert.html
Normal 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>
|
160
umap/static/umap/js/components/alerts/alert.js
Normal file
160
umap/static/umap/js/components/alerts/alert.js
Normal 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 }
|
54
umap/static/umap/js/components/base.js
Normal file
54
umap/static/umap/js/components/base.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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' %}" />
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue