Compare commits

...

20 commits

Author SHA1 Message Date
Yohan Boniface
8292608365 chore: fix HLC comparison
Some checks failed
Test & Docs / tests (postgresql, 3.10) (push) Has been cancelled
Test & Docs / tests (postgresql, 3.12) (push) Has been cancelled
Test & Docs / lint (push) Has been cancelled
This change was made by biome, but it breaks unittests, not
sure why but let's revert for now.
2025-03-28 17:59:35 +01:00
Yohan Boniface
ae8cbf39ad
feat: display maps list as a grid now (#2590)
![image](https://github.com/user-attachments/assets/db9eafd1-b4ea-48a1-a359-3e55d0011f4b)
2025-03-28 16:15:51 +01:00
Yohan Boniface
7c5d821ec8
feat: layers selector in bottom bar (#2579) 2025-03-28 16:15:20 +01:00
Yohan Boniface
4e9e828c8f feat: display maps list as a grid now
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 16:05:11 +01:00
Yohan Boniface
79d60d0995 fix: update datalayers switcher when datalayer visibility changes
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 15:56:28 +01:00
Yohan Boniface
75457b6d5c
fix: do not fail when trying to read metadata of a missing geojson (#2592)
Some geojson have been removed by mistake time ago (cf #1003), when
someone tries to load a map referencing them, it was until recently just
showing an error message, but since recently we try to get the metadata
from it, and this will crash.
2025-03-28 15:28:13 +01:00
Yohan Boniface
8538db278d
feat: add titles in the text formatting dialog (#2584)
![image](https://github.com/user-attachments/assets/c90549b6-6081-4b1d-8b90-49c1df42ded2)

fix #813
2025-03-28 15:27:30 +01:00
Yohan Boniface
ba9e8ffe9b
chore: apply Biome check (#2588)
This is mostly imports ordering but there are a couple of subtleties 🐉 

There are still 9 errors before we can automatize the check with the CI!
2025-03-28 15:26:52 +01:00
Yohan Boniface
dcf5f1a763
fix: make sure umap.properties.slideshow is defined (#2583) 2025-03-28 15:25:19 +01:00
Yohan Boniface
41264e740f fix: do not fail when trying to read metadata of a missing geojson
Some geojson have been removed by mistake time ago (cf #1003), when someone
tries to load a map referencing them, it was until recently just
showing an error message, but since recently we try to get the
metadata from it, and this will crash.
2025-03-28 12:58:07 +01:00
Yohan Boniface
953b37a181 fixup: fix tests 2025-03-27 13:14:20 +01:00
Yohan Boniface
9eaf33c118 wip: only show layer selector if there are at least two layers 2025-03-27 13:11:24 +01:00
David Larlet
f2cde6af4e fixup: positionning of caption bar elements 2025-03-27 13:11:24 +01:00
Yohan Boniface
a4abecbd2c fixup: only show datalayers with inCaption=true in switcher 2025-03-27 13:11:24 +01:00
Yohan Boniface
254a2018f5 chore: use toggle to switch visibility in datalayer switcher 2025-03-27 13:11:24 +01:00
Yohan Boniface
3df52e002d wip 2025-03-27 13:11:24 +01:00
David Larlet
82208d618a
chore: apply Biome check 2025-03-26 14:37:56 -04:00
Yohan Boniface
e993aa7dbc chore: bump eslint ecmaVersion from 2020 to 2021 2025-03-26 11:30:08 +01:00
Yohan Boniface
ecca66ccc2 feat: add titles in the text formatting dialog
fix #813
2025-03-19 08:14:33 +01:00
Yohan Boniface
360ca100ba fix: make sure umap.properties.slideshow is defined 2025-03-18 07:35:10 +01:00
50 changed files with 277 additions and 188 deletions

View file

@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 2020,
"ecmaVersion": 2021,
"sourceType": "module"
}
}

View file

@ -1,11 +1,7 @@
{
"files": {
"include": [
"umap/static/umap/js/**"
],
"ignore": [
"umap/static/umap/vendors/**"
]
"include": ["umap/static/umap/js/**"],
"ignore": ["umap/static/umap/vendors/**"]
},
"formatter": {
"enabled": true,
@ -22,7 +18,11 @@
"rules": {
"style": {
"useBlockStatements": "off",
"noShoutyConstants": "warn"
"noShoutyConstants": "warn",
"noParameterAssign": "off"
},
"complexity": {
"noForEach": "off"
},
"performance": {
"noDelete": "off"

View file

@ -526,7 +526,10 @@ class DataLayer(NamedModel):
metadata = self.settings
if not metadata:
# Fallback to file for old datalayers.
try:
data = json.loads(self.geojson.read().decode())
except FileNotFoundError:
data = {}
metadata = data.get("_umap_options")
if not metadata:
metadata = {

View file

@ -84,6 +84,11 @@ hgroup {
hgroup > :not(:first-child):last-child {
font-weight: normal;
}
hgroup p,
hgroup button {
margin: 0;
}
/*
* List
@ -158,10 +163,23 @@ dt {
}
.grid-container {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
--grid-layout-gap: calc(var(--gutter) * 2);
--grid-column-count: 3;
--grid-item--min-width: 300px;
/**
* Calculated values.
*/
--gap-count: calc(var(--grid-column-count) - 1);
--total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap));
--grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count));
grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item--min-width), var(--grid-item--max-width)), 1fr));
grid-gap: var(--grid-layout-gap);
}
.grid-container.by4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
--grid-column-count: 4;
--grid-item--min-width: 60px;
}
.grid-container > * {
text-align: center;

View file

@ -56,9 +56,9 @@ body.login header {
.map_fragment {
width: 100%;
}
.map_list .map_fragment,
.map_fragment,
.demo_map .map_fragment {
height: 210px;
height: var(--map-fragment-height);
}
.map_list .legend {
padding-top: 7px;
@ -164,6 +164,9 @@ h2.tabs a:hover {
color: #efefef;
text-decoration: underline;
}
.more_button {
min-height: var(--map-fragment-height);
}
/* **************************** */

View file

@ -163,11 +163,13 @@
.umap-main-edit-toolbox h3 {
display: inline;
}
.umap-caption-bar button {
margin-inline-start: 10px;
.umap-caption-bar .umap-map-author {
margin-inline-end: 10px;
}
.umap-caption-bar button + button:before {
.umap-caption-bar > button + button:after,
.umap-caption-bar > button + button:before {
content: '|';
padding-inline-start: 10px;
padding-inline-end: 10px;
}
.umap-main-edit-toolbox .umap-user:hover {
@ -196,7 +198,14 @@
z-index: var(--zindex-panels);
}
.umap-caption-bar-enabled .umap-caption-bar {
display: block;
display: flex;
align-items: baseline;
}
.umap-caption-bar select {
margin-top: 0;
line-height: initial;
height: initial;
width: auto;
}
.umap-caption-bar-enabled {
--current-footer-height: var(--footer-height);

View file

@ -14,6 +14,9 @@
height: fit-content;
max-height: 90vh;
}
.umap-dialog ul + h4 {
margin-top: var(--box-margin);
}
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
position: relative;

View file

@ -140,7 +140,6 @@ class uMapAlertConflict extends uMapAlert {
}
onAlertConflict(event) {
// biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default
const {
level = 'info',
duration = Number.POSITIVE_INFINITY,

View file

@ -1,10 +1,10 @@
import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { Form } from './form/builder.js'
import { EXPORT_FORMATS } from './formatter.js'
import { translate } from './i18n.js'
import * as Icon from './rendering/icon.js'
import * as Utils from './utils.js'
import { EXPORT_FORMATS } from './formatter.js'
import ContextMenu from './ui/contextmenu.js'
import { Form } from './form/builder.js'
import * as Utils from './utils.js'
export default class Browser {
constructor(umap, leafletMap) {

View file

@ -1,6 +1,6 @@
import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
const TEMPLATE = `
<div class="umap-caption">

View file

@ -1,22 +1,22 @@
import {
DomUtil,
DomEvent,
stamp,
DomUtil,
GeoJSON,
LineUtil,
stamp,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { MutatingForm } from '../form/builder.js'
import { translate } from '../i18n.js'
import loadPopup from '../rendering/popup.js'
import {
LeafletMarker,
LeafletPolyline,
LeafletPolygon,
LeafletPolyline,
MaskPolygon,
} from '../rendering/ui.js'
import loadPopup from '../rendering/popup.js'
import { MutatingForm } from '../form/builder.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
class Feature {
constructor(umap, datalayer, geojson = {}, id = null) {

View file

@ -1,26 +1,26 @@
// FIXME: this module should not depend on Leaflet
import {
DomUtil,
DomEvent,
stamp,
DomUtil,
GeoJSON,
stamp,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { Default as DefaultLayer } from '../rendering/layers/base.js'
import { Cluster } from '../rendering/layers/cluster.js'
import { Heat } from '../rendering/layers/heat.js'
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
import {
uMapAlert as Alert,
uMapAlertConflict as AlertConflict,
} from '../../components/alerts/alert.js'
import { MutatingForm } from '../form/builder.js'
import { translate } from '../i18n.js'
import { DataLayerPermissions } from '../permissions.js'
import { Point, LineString, Polygon } from './features.js'
import TableEditor from '../tableeditor.js'
import { Default as DefaultLayer } from '../rendering/layers/base.js'
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
import { Cluster } from '../rendering/layers/cluster.js'
import { Heat } from '../rendering/layers/heat.js'
import { ServerStored } from '../saving.js'
import * as Schema from '../schema.js'
import { MutatingForm } from '../form/builder.js'
import TableEditor from '../tableeditor.js'
import * as Utils from '../utils.js'
import { LineString, Point, Polygon } from './features.js'
export const LAYER_TYPES = [
DefaultLayer,
@ -966,12 +966,18 @@ export class DataLayer extends ServerStored {
this.propagateHide()
}
toggle() {
toggle(force) {
// From now on, do not try to how/hidedataChanged
// automatically this layer.
let display = force
this._forcedVisibility = true
if (!this.isVisible()) this.show()
if (force === undefined) {
if (!this.isVisible()) display = true
else display = false
}
if (display) this.show()
else this.hide()
this._umap.bottomBar.redraw()
}
zoomTo() {
@ -1258,7 +1264,7 @@ export class DataLayer extends ServerStored {
this
)
}
DomEvent.on(toggle, 'click', this.toggle, this)
DomEvent.on(toggle, 'click', () => this.toggle())
DomEvent.on(zoomTo, 'click', this.zoomTo, this)
container.classList.add(this.getHidableClass())
container.classList.toggle('off', !this.isVisible())

View file

@ -1,7 +1,7 @@
import getClass from './fields.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
import getClass from './fields.js'
export class Form extends Utils.WithEvents {
constructor(obj, fields, properties) {

View file

@ -1,12 +1,12 @@
import * as Utils from '../utils.js'
import { translate } from '../i18n.js'
import {
AjaxAutocomplete,
AjaxAutocompleteMultiple,
AutocompleteDatalist,
} from '../autocomplete.js'
import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js'
import * as Icon from '../rendering/icon.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
const Fields = {}
@ -254,8 +254,8 @@ Fields.BlurInput = class extends Fields.Input {
const IntegerMixin = (Base) =>
class extends Base {
value() {
return !isNaN(this.input.value) && this.input.value !== ''
? parseInt(this.input.value, 10)
return !Number.isNaN(this.input.value) && this.input.value !== ''
? Number.parseInt(this.input.value, 10)
: undefined
}
@ -270,8 +270,8 @@ Fields.BlurIntInput = class extends IntegerMixin(Fields.BlurInput) {}
const FloatMixin = (Base) =>
class extends Base {
value() {
return !isNaN(this.input.value) && this.input.value !== ''
? parseFloat(this.input.value)
return !Number.isNaN(this.input.value) && this.input.value !== ''
? Number.parseFloat(this.input.value)
: undefined
}
@ -390,7 +390,7 @@ Fields.Select = class extends BaseElement {
Fields.IntSelect = class extends Fields.Select {
value() {
return parseInt(super.value(), 10)
return Number.parseInt(super.value(), 10)
}
}

View file

@ -1,6 +1,6 @@
import { uMapAlert as Alert } from '../components/alerts/alert.js'
/* Uses globals for: csv2geojson, osmtogeojson (not available as ESM) */
import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
export const EXPORT_FORMATS = {
geojson: {

View file

@ -4,14 +4,14 @@ import {
AjaxAutocompleteMultiple,
AutocompleteDatalist,
} from './autocomplete.js'
import { LineString, Point, Polygon } from './data/features.js'
import { LAYER_TYPES } from './data/layer.js'
import Help from './help.js'
import * as Icon from './rendering/icon.js'
import { LeafletMarker, LeafletPolygon, LeafletPolyline } from './rendering/ui.js'
import { ServerRequest } from './request.js'
import { SCHEMA } from './schema.js'
import * as Utils from './utils.js'
import * as Icon from './rendering/icon.js'
import { LAYER_TYPES } from './data/layer.js'
import { Point, LineString, Polygon } from './data/features.js'
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from './rendering/ui.js'
// Import modules and export them to the global scope.
// For the not yet module-compatible JS out there.

View file

@ -1,7 +1,7 @@
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import Dialog from './ui/dialog.js'
import * as Utils from './utils.js'
const SHORTCUTS = {
DRAW_MARKER: {
@ -135,14 +135,23 @@ const ENTRIES = {
<li>${translate('# one hash for main heading')}</li>
<li>${translate('## two hashes for second heading')}</li>
<li>${translate('### three hashes for third heading')}</li>
<li>${translate('--- for a horizontal rule')}</li>
</ul>
<h4>${translate('Links')}</h4>
<ul>
<li>${translate('Simple link: [[https://example.com]]')}</li>
<li>${translate('Link with text: [[https://example.com|text of the link]]')}</li>
</ul>
<h4>${translate('Images')}</h4>
<ul>
<li>${translate('Image: {{https://image.url.com}}')}</li>
<li>${translate('Image with custom width (in px): {{https://image.url.com|width}}')}</li>
</ul>
<h4>${translate('Iframes')}</h4>
<ul>
<li>${translate('Iframe: {{{https://iframe.url.com}}}')}</li>
<li>${translate('Iframe with custom height (in px): {{{https://iframe.url.com|height}}}')}</li>
<li>${translate('Iframe with custom height and width (in px): {{{https://iframe.url.com|height*width}}}')}</li>
<li>${translate('--- for a horizontal rule')}</li>
</ul>
</div>
`,

View file

@ -1,9 +1,9 @@
import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { BaseAjax, SingleMixin } from '../autocomplete.js'
import { translate } from '../i18n.js'
import * as Utils from '../utils.js'
import { AutocompleteCommunes } from './communesfr.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
const TEMPLATE = `
<div>

View file

@ -1,9 +1,9 @@
import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { BaseAjax, SingleMixin } from '../autocomplete.js'
import { translate } from '../i18n.js'
import * as Util from '../utils.js'
import { AutocompleteCommunes } from './communesfr.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
const TEMPLATE = `
<h3>Cadastre</h3>

View file

@ -15,7 +15,7 @@ export class AutocompleteCommunes extends SingleMixin(BaseAjax) {
let options = { q: encodeURIComponent(value) }
const re = /^(0[1-9]|[1-9][ABab\d])\d{3}$/gm
if (re.test(value)) {
url = "https://geo.api.gouv.fr/communes?code={code}&limit=5"
url = 'https://geo.api.gouv.fr/communes?code={code}&limit=5'
options = { code: encodeURIComponent(value) }
}
return Util.template(url, options)

View file

@ -1,9 +1,9 @@
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
import { ServerStored } from './saving.js'
import * as Utils from './utils.js'
import { MutatingForm } from './form/builder.js'
// Dedicated object so we can deal with a separate dirty status, and thus
// call the endpoint only when needed, saving one call at each save.

View file

@ -1,11 +1,11 @@
import {
DivIcon,
DomEvent,
DomUtil,
DivIcon,
Icon,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
export function getClass(name) {
switch (name) {

View file

@ -1,9 +1,9 @@
import { FeatureGroup, DomUtil } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import colorbrewer from '../../../../vendors/colorbrewer/colorbrewer.js'
import { DomUtil, FeatureGroup } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../../i18n.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js'
import { CircleMarker } from '../ui.js'
import colorbrewer from '../../../../vendors/colorbrewer/colorbrewer.js'
import { LayerMixin } from './base.js'
// Layer where each feature color is relative to the others,
// so we need all features before behing able to set one

View file

@ -1,10 +1,10 @@
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
// WARNING must be loaded dynamically, or at least after leaflet.markercluster
// Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
import { translate } from '../../i18n.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js'
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { Cluster as ClusterIcon } from '../icon.js'
import { LayerMixin } from './base.js'
const MarkerCluster = L.MarkerCluster.extend({
// Custom class so we can call computeTextColor

View file

@ -1,14 +1,14 @@
// Uses global L.HeatLayer, not exposed as ESM
import {
Marker,
LatLng,
latLngBounds,
Bounds,
LatLng,
Marker,
latLngBounds,
point,
} from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js'
import { translate } from '../../i18n.js'
import * as Utils from '../../utils.js'
import { LayerMixin } from './base.js'
export const Heat = L.HeatLayer.extend({
statics: {

View file

@ -1,18 +1,18 @@
// Goes here all code related to Leaflet, DOM and user interactions.
import {
Map as BaseMap,
DomUtil,
DomEvent,
latLngBounds,
latLng,
Control,
DomEvent,
DomUtil,
latLng,
latLngBounds,
setOptions,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import DropControl from '../drop.js'
import { translate } from '../i18n.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'
import DropControl from '../drop.js'
// Those options are not saved on the server, so they can live here
// instead of in umap.properties

View file

@ -1,11 +1,11 @@
import {
Popup as BasePopup,
DomEvent,
DomUtil,
Path,
Popup as BasePopup,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import loadTemplate from './template.js'
import Browser from '../browser.js'
import loadTemplate from './template.js'
export default function loadPopup(name) {
switch (name) {

View file

@ -1,8 +1,8 @@
import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate, getLocale } from '../i18n.js'
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { getLocale, translate } from '../i18n.js'
import { Request } from '../request.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'
import { Request } from '../request.js'
export default async function loadTemplate(name, feature, container) {
let klass = PopupTemplate

View file

@ -1,18 +1,18 @@
// Goes here all code related to Leaflet, DOM and user interactions.
import {
Marker,
Polyline,
Polygon,
CircleMarker as BaseCircleMarker,
DomEvent,
DomUtil,
LineUtil,
latLng,
LatLng,
LatLngBounds,
DomEvent,
LineUtil,
Marker,
Polygon,
Polyline,
latLng,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { translate } from '../i18n.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'

View file

@ -1,9 +1,9 @@
import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import { AutocompleteDatalist } from './autocomplete.js'
import Orderable from './orderable.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
import Orderable from './orderable.js'
import * as Utils from './utils.js'
const EMPTY_VALUES = ['', undefined, null]

View file

@ -1,8 +1,8 @@
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { MutatingForm } from './form/builder.js'
import { EXPORT_FORMATS } from './formatter.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import { MutatingForm } from './form/builder.js'
export default class Share {
constructor(umap) {

View file

@ -18,6 +18,7 @@ export default class Slideshow extends WithTemplate {
this._umap = umap
this._id = null
this.CLASSNAME = 'umap-slideshow-active'
this._umap.properties.slideshow ??= {}
this.load()
this._current = null

View file

@ -1,8 +1,8 @@
import * as SaveManager from '../saving.js'
import * as Utils from '../utils.js'
import { HybridLogicalClock } from './hlc.js'
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
import { WebSocketTransport } from './websocket.js'
import * as SaveManager from '../saving.js'
// Start reconnecting after 2 seconds, then double the delay each time
// maxing out at 32 seconds.

View file

@ -1,8 +1,8 @@
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
import ContextMenu from './ui/contextmenu.js'
import { WithTemplate, loadTemplate } from './utils.js'
import { MutatingForm } from './form/builder.js'
const TEMPLATE = `
<table>

View file

@ -1,9 +1,9 @@
import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { LineString, Point, Polygon } from '../data/features.js'
import { translate } from '../i18n.js'
import { WithTemplate } from '../utils.js'
import ContextMenu from './contextmenu.js'
import * as Utils from '../utils.js'
import { Point, LineString, Polygon } from '../data/features.js'
import ContextMenu from './contextmenu.js'
const TOP_BAR_TEMPLATE = `
<div class="umap-main-edit-toolbox with-transition dark">
@ -167,6 +167,7 @@ const BOTTOM_BAR_TEMPLATE = `
<button class="umap-about-link flat" type="button" title="${translate('Open caption')}" data-ref="caption">${translate('Open caption')}</button>
<button class="umap-open-browser-link flat" type="button" title="${translate('Browse data')}" data-ref="browse">${translate('Browse data')}</button>
<button class="umap-open-browser-link flat" type="button" title="${translate('Filter data')}" data-ref="filter">${translate('Filter data')}</button>
<select data-ref=layers></select>
</div>
`
@ -189,6 +190,14 @@ export class BottomBar extends WithTemplate {
this._umap.openBrowser('filters')
)
this._slideshow.renderToolbox(this.element)
this.elements.layers.addEventListener('change', () => {
const select = this.elements.layers
const selected = select.options[select.selectedIndex].value
if (!selected) return
this._umap.eachDataLayer((datalayer) => {
datalayer.toggle(datalayer.id === selected)
})
})
this.redraw()
}
@ -201,6 +210,27 @@ export class BottomBar extends WithTemplate {
this.elements.caption.hidden = !showMenus
this.elements.browse.hidden = !showMenus
this.elements.filter.hidden = !showMenus || !this._umap.properties.facetKey
this.buildDataLayerSwitcher()
}
buildDataLayerSwitcher() {
this.elements.layers.innerHTML = ''
const datalayers = this._umap.datalayersIndex.filter((d) => d.options.inCaption)
if (datalayers.length < 2) {
this.elements.layers.hidden = true
} else {
this.elements.layers.appendChild(Utils.loadTemplate(`<option value=""></option>`))
this.elements.layers.hidden = false
const visible = datalayers.filter((datalayer) => datalayer.isVisible())
for (const datalayer of datalayers) {
const selected = visible.length === 1 && datalayer.isVisible() ? 'selected' : ''
this.elements.layers.appendChild(
Utils.loadTemplate(
`<option value="${datalayer.id}" ${selected}>${datalayer.getName()}</option>`
)
)
}
}
}
}

View file

@ -1,7 +1,7 @@
import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
import { Positioned } from './base.js'
import * as Utils from '../utils.js'
import { Positioned } from './base.js'
export default class Tooltip extends Positioned {
constructor(parent) {

View file

@ -1,40 +1,40 @@
import {
DomUtil,
Util as LeafletUtil,
stamp,
latLngBounds,
stamp,
} from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate, setLocale, getLocale } from './i18n.js'
import * as Utils from './utils.js'
import { ServerStored } from './saving.js'
import * as SAVEMANAGER from './saving.js'
import { SyncEngine } from './sync/engine.js'
import { LeafletMap } from './rendering/map.js'
import URLs from './urls.js'
import { Panel, EditPanel, FullPanel } from './ui/panel.js'
import Dialog from './ui/dialog.js'
import { BottomBar, TopBar, EditBar } from './ui/bar.js'
import Tooltip from './ui/tooltip.js'
import ContextMenu from './ui/contextmenu.js'
import { Request, ServerRequest } from './request.js'
import Help from './help.js'
import { Formatter } from './formatter.js'
import Slideshow from './slideshow.js'
import { MapPermissions } from './permissions.js'
import { SCHEMA } from './schema.js'
import { DataLayer } from './data/layer.js'
import Facets from './facets.js'
import {
uMapAlert as Alert,
uMapAlertCreation as AlertCreation,
} from '../components/alerts/alert.js'
import Browser from './browser.js'
import Caption from './caption.js'
import Importer from './importer.js'
import Rules from './rules.js'
import Share from './share.js'
import {
uMapAlertCreation as AlertCreation,
uMapAlert as Alert,
} from '../components/alerts/alert.js'
import Orderable from './orderable.js'
import { DataLayer } from './data/layer.js'
import Facets from './facets.js'
import { MutatingForm } from './form/builder.js'
import { Formatter } from './formatter.js'
import Help from './help.js'
import { getLocale, setLocale, translate } from './i18n.js'
import Importer from './importer.js'
import Orderable from './orderable.js'
import { MapPermissions } from './permissions.js'
import { LeafletMap } from './rendering/map.js'
import { Request, ServerRequest } from './request.js'
import Rules from './rules.js'
import { ServerStored } from './saving.js'
import * as SAVEMANAGER from './saving.js'
import { SCHEMA } from './schema.js'
import Share from './share.js'
import Slideshow from './slideshow.js'
import { SyncEngine } from './sync/engine.js'
import { BottomBar, EditBar, TopBar } from './ui/bar.js'
import ContextMenu from './ui/contextmenu.js'
import Dialog from './ui/dialog.js'
import { EditPanel, FullPanel, Panel } from './ui/panel.js'
import Tooltip from './ui/tooltip.js'
import URLs from './urls.js'
import * as Utils from './utils.js'
export default class Umap extends ServerStored {
constructor(element, geojson) {

View file

@ -368,10 +368,13 @@ export function isDataImage(value) {
* characters and no diacritics.
*/
export function normalize(s) {
return (s || '')
return (
(s || '')
.toLowerCase()
.normalize('NFD')
// biome-ignore lint/suspicious/noMisleadingCharacterClass: <explanation>
.replace(/[\u0300-\u036f]/g, '')
)
}
// Vendorized from leaflet.utils

View file

@ -660,10 +660,6 @@ a.umap-control-caption,
.umap-caption .header i.icon {
flex-shrink: 0;
}
.umap-caption hgroup p,
.umap-caption hgroup button {
margin: 0;
}
.umap-browser .main-toolbox {
padding-left: 4px; /* Align with toolbox below */
border-top: 1px solid var(--color-mediumGray);

View file

@ -45,6 +45,7 @@
--box-margin: 14px;
--text-margin: 7px;
--dialog-width: 40vw;
--map-fragment-height: 210px;
/* z-indexes (leaflet CSS sets the map at 400 by default) */
--zindex-alert: 500;

View file

@ -13,7 +13,7 @@
</h2>
</div>
<div class="wrapper">
<div class="map_list row">
<div class="row grid-container">
{% if maps %}
{% include "umap/map_list.html" %}
{% else %}

View file

@ -13,7 +13,7 @@
</h2>
</div>
<div class="wrapper">
<div class="map_list row">
<div class="grid-container row">
{% if maps %}
{% include "umap/map_list.html" %}
{% else %}

View file

@ -43,22 +43,27 @@
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', event => {
const server = new U.ServerRequest()
const getMore = async function (e) {
L.DomEvent.stop(e)
const [{html}, response, error] = await server.get(this.href)
const getMore = async function (link) {
const container = link.parentNode
container.removeChild(link)
const [{html}, response, error] = await server.get(link.href)
if (!error) {
const container = this.parentNode
container.innerHTML = html
const template = document.createElement('template')
template.innerHTML = html
container.appendChild(template.content)
listenForMore()
}
}
const listenForMore = () => {
const more = document.querySelector('.more_button')
if (more) {
L.DomEvent.on(more, 'click', getMore, more)
L.DomEvent.on(more, 'click', (e) => {
L.DomEvent.stop(e)
getMore(more)
})
}
}
}
const more = document.querySelector('.more_button')
if (more) {
L.DomEvent.on(more, 'click', getMore, more)
}
listenForMore()
})
</script>
{% endblock bottom_js %}

View file

@ -20,12 +20,14 @@
{% endif %}
<div class="wrapper">
{% if maps %}
<div class="row">
<h2 class="section">
{% blocktrans %}Get inspired, browse maps{% endblocktrans %}
</h2>
<div class="map_list row">
<div class="grid-container">
{% include "umap/map_list.html" %}
</div>
</div>
{% endif %}
</div>
{% endblock maincontent %}

View file

@ -1,22 +1,19 @@
{% load umap_tags i18n %}
{% for map_inst in maps %}
<hr />
<div class="col wide">
<div>
{% map_fragment map_inst prefix=prefix page=request.GET.p %}
<div class="legend">
<a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
<hgroup>
<h3><a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a></h3>
{% with author=map_inst.get_author %}
{% if author %}
<em>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></em>
<p>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></p>
{% endif %}
{% endwith %}
</div>
</hgroup>
</div>
{% endfor %}
{% if maps.has_next %}
<div class="col wide">
<a href="?{% paginate_querystring maps.next_page_number %}"
class="button more_button neutral">{% trans "More" %}</a>
</div>
{% endif %}

View file

@ -12,7 +12,7 @@
{% block maincontent %}
{% include "umap/search_bar.html" %}
<div class="wrapper">
<div class="map_list row">
<div class="row">
{% if request.GET.q %}
{% if maps %}
<h2>
@ -22,7 +22,9 @@
{{ count }} maps found:
{% endblocktranslate %}
</h2>
<div class="grid-container">
{% include "umap/map_list.html" with prefix="search_map" %}
</div>
{% else %}
<h2>
{% trans "No map found." %}
@ -32,7 +34,9 @@
<h2>
{% trans "Latest created maps" %}
</h2>
<div class="grid-container">
{% include "umap/map_list.html" with prefix="search_map" %}
</div>
{% endif %}
</div>
</div>

View file

@ -18,7 +18,7 @@
{% endif %}
</div>
</div>
<div class="map_list row">
<div class="grid-container row">
{% if maps %}
{% include "umap/map_list.html" %}
{% else %}

View file

@ -182,7 +182,7 @@ def test_can_restore_version(live_server, openmap, page, datalayer):
page.get_by_role("button", name="Manage layers").click()
page.locator(".panel.right").get_by_title("Edit", exact=True).click()
page.get_by_text("Versions").click()
page.get_by_role("button", name="Restore this version").last.click()
page.get_by_title("Restore this version").last.click()
page.get_by_role("button", name="OK").click()
expect(marker).to_have_class(re.compile(".*umap-ball-icon.*"))

View file

@ -86,8 +86,8 @@ def test_umap_import_from_textarea(live_server, tilelayer, page, settings):
expect(page.locator(".umap-main-edit-toolbox .map-name")).to_have_text(
"Imported map"
)
expect(page.get_by_text("Tunnels")).to_be_visible()
expect(page.get_by_text("Cities")).to_be_visible()
expect(page.locator(".panel.left").get_by_text("Tunnels")).to_be_visible()
expect(page.locator(".panel.left").get_by_text("Cities")).to_be_visible()
expect(page.locator(".leaflet-control-minimap")).to_be_visible()
expect(
page.locator('img[src="https://tile.openstreetmap.fr/hot/6/32/21.png"]')

View file

@ -477,23 +477,23 @@ def test_should_sync_datalayers_delete(new_page, asgi_live_server, tilelayer):
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
peerA.get_by_role("button", name="Open browser").click()
expect(peerA.get_by_text("datalayer 1")).to_be_visible()
expect(peerA.get_by_text("datalayer 2")).to_be_visible()
expect(peerA.locator(".panel").get_by_text("datalayer 1")).to_be_visible()
expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_visible()
peerB.get_by_role("button", name="Open browser").click()
expect(peerB.get_by_text("datalayer 1")).to_be_visible()
expect(peerB.get_by_text("datalayer 2")).to_be_visible()
expect(peerB.locator(".panel").get_by_text("datalayer 1")).to_be_visible()
expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_visible()
# Delete "datalayer 2" in peerA
peerA.locator(".datalayer").get_by_role("button", name="Delete layer").first.click()
peerA.get_by_role("button", name="OK").click()
expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
# Save delete to the server
with peerA.expect_response(re.compile(".*/datalayer/delete/.*")):
peerA.get_by_role("button", name="Save").click()
expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
@pytest.mark.xdist_group(name="websockets")