Compare commits

..

43 commits

Author SHA1 Message Date
Yohan Boniface
5259cab027 chore: remove saving.js import added by mistake during rebase 2025-03-28 18:00:14 +01:00
Yohan Boniface
7ede27bf0f chore: top edit bar responsiveness
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
5807cfbbcd wip: move undo/redo buttons to the left
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
e41ad4e069 wip: allow to sync version restore
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
c933df585c wip: add test to make sure saving also save remote dirty datalayers 2025-03-28 18:00:14 +01:00
Yohan Boniface
50f2c08ecb wip: do not call document during JS unittests 2025-03-28 18:00:14 +01:00
Yohan Boniface
d61e045903 wip: allow to sync/undo filter added/removed from table editor 2025-03-28 18:00:14 +01:00
Yohan Boniface
6b2038e83e wip: permissions does not inherit from ServerStored anymore 2025-03-28 18:00:14 +01:00
Yohan Boniface
0389e9a185 wip: allow to undo/sync rules
When editing Rule(s), we are not editing the map data itself, but a
sort of proxy objects. This was done mainly because map.properties.rules
is an array of object, and at this time Leaflet.FormBuilder did not know
how to edit an array (something like properties.rules.0.condition).
Now that we integrated FormBuilder, it still does not know how to do this
but we could teach it, or find another way (real Proxy or use reference
to the original object in the Rule).
2025-03-28 18:00:14 +01:00
Yohan Boniface
a2e864ad73 wip: uncreated map should always appear as dirty 2025-03-28 18:00:14 +01:00
Yohan Boniface
d0fb85d552 wip: DataLayer does not inherit anymore from ServerStored 2025-03-28 18:00:14 +01:00
Yohan Boniface
fa83764c8b wip: allow DataLayer.clear to be sync and undone 2025-03-28 18:00:14 +01:00
Yohan Boniface
d438a007e4 wip: uMap does not inherit anymore from ServerStored
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
be52e7ca2f wip: remove not effective code
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
172de0e2d0 wip: add permissions related fields in schema
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
77da6425c2 wip: allow to mark an operation as not undoable
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
093ed061c1 wip: tests pass
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
5cb7cb2738 fixup: make sure to toggle remote client state at save too
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
dfdfae0080 wip: derive the dirty status from the undoManager
This should pave the way for removing the SaveManager.

Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
4fd066387d Update the tests and remove cancel edits
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2025-03-28 18:00:14 +01:00
fa3ba46ca8 Add integration test for batch undo/redo 2025-03-28 18:00:14 +01:00
Yohan Boniface
cb46a5f875 Batch operations
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 18:00:14 +01:00
Yohan Boniface
cc2625bfac wip: undo redo
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2025-03-28 18:00:14 +01:00
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 307 additions and 205 deletions

View file

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

View file

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

View file

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

View file

@ -84,6 +84,11 @@ hgroup {
hgroup > :not(:first-child):last-child { hgroup > :not(:first-child):last-child {
font-weight: normal; font-weight: normal;
} }
hgroup p,
hgroup button {
margin: 0;
}
/* /*
* List * List
@ -158,10 +163,23 @@ dt {
} }
.grid-container { .grid-container {
display: grid; 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-container.by4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); --grid-column-count: 4;
--grid-item--min-width: 60px;
} }
.grid-container > * { .grid-container > * {
text-align: center; text-align: center;

View file

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

View file

@ -111,8 +111,6 @@
.umap-right-edit-toolbox { .umap-right-edit-toolbox {
display: flex; display: flex;
column-gap: 10px; column-gap: 10px;
}
.umap-right-edit-toolbox {
align-items: center; align-items: center;
} }
@ -131,17 +129,20 @@
text-indent: -9999px; text-indent: -9999px;
} }
.umap-main-edit-toolbox .map-name { .umap-main-edit-toolbox .map-name {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold; font-weight: bold;
text-align: start; text-align: start;
} }
.truncate {
display: inline-flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.umap-main-edit-toolbox .username {
max-width: 100px;
}
.umap-main-edit-toolbox .share-status { .umap-main-edit-toolbox .share-status {
font-style: italic; font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
} }
.map-name:after { .map-name:after {
content: '\00a0'; content: '\00a0';
@ -159,11 +160,13 @@
.umap-main-edit-toolbox h3 { .umap-main-edit-toolbox h3 {
display: inline; display: inline;
} }
.umap-caption-bar button { .umap-caption-bar .umap-map-author {
margin-inline-start: 10px; margin-inline-end: 10px;
} }
.umap-caption-bar button + button:before { .umap-caption-bar > button + button:after,
.umap-caption-bar > button + button:before {
content: '|'; content: '|';
padding-inline-start: 10px;
padding-inline-end: 10px; padding-inline-end: 10px;
} }
.umap-main-edit-toolbox .umap-user:hover { .umap-main-edit-toolbox .umap-user:hover {
@ -192,7 +195,14 @@
z-index: var(--zindex-panels); z-index: var(--zindex-panels);
} }
.umap-caption-bar-enabled .umap-caption-bar { .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 { .umap-caption-bar-enabled {
--current-footer-height: var(--footer-height); --current-footer-height: var(--footer-height);
@ -229,3 +239,14 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
@media all and (max-width: 980px) {
.umap-main-edit-toolbox button span {
display: none;
}
}
@media all and (max-width: 770px) {
.umap-main-edit-toolbox .umap-help-link,
.umap-main-edit-toolbox .share-status {
display: none !important;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,22 +1,22 @@
import { import {
DomUtil,
DomEvent, DomEvent,
stamp, DomUtil,
GeoJSON, GeoJSON,
LineUtil, LineUtil,
stamp,
} from '../../../vendors/leaflet/leaflet-src.esm.js' } 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 { 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 { import {
LeafletMarker, LeafletMarker,
LeafletPolyline,
LeafletPolygon, LeafletPolygon,
LeafletPolyline,
MaskPolygon, MaskPolygon,
} from '../rendering/ui.js' } from '../rendering/ui.js'
import loadPopup from '../rendering/popup.js' import { SCHEMA } from '../schema.js'
import { MutatingForm } from '../form/builder.js' import * as Utils from '../utils.js'
class Feature { class Feature {
constructor(umap, datalayer, geojson = {}, id = null) { constructor(umap, datalayer, geojson = {}, id = null) {

View file

@ -1,25 +1,25 @@
// FIXME: this module should not depend on Leaflet // FIXME: this module should not depend on Leaflet
import { import {
DomUtil,
DomEvent, DomEvent,
stamp, DomUtil,
GeoJSON, GeoJSON,
stamp,
} from '../../../vendors/leaflet/leaflet-src.esm.js' } 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 { import {
uMapAlert as Alert, uMapAlert as Alert,
uMapAlertConflict as AlertConflict, uMapAlertConflict as AlertConflict,
} from '../../components/alerts/alert.js' } from '../../components/alerts/alert.js'
import { MutatingForm } from '../form/builder.js'
import { translate } from '../i18n.js' import { translate } from '../i18n.js'
import { DataLayerPermissions } from '../permissions.js' import { DataLayerPermissions } from '../permissions.js'
import { Point, LineString, Polygon } from './features.js' import { Default as DefaultLayer } from '../rendering/layers/base.js'
import TableEditor from '../tableeditor.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 * as Schema from '../schema.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 = [ export const LAYER_TYPES = [
DefaultLayer, DefaultLayer,
@ -946,12 +946,18 @@ export class DataLayer {
this.propagateHide() this.propagateHide()
} }
toggle() { toggle(force) {
// From now on, do not try to how/hidedataChanged // From now on, do not try to how/hidedataChanged
// automatically this layer. // automatically this layer.
let display = force
this._forcedVisibility = true 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() else this.hide()
this._umap.bottomBar.redraw()
} }
zoomTo() { zoomTo() {
@ -1239,7 +1245,7 @@ export class DataLayer {
this this
) )
} }
DomEvent.on(toggle, 'click', this.toggle, this) DomEvent.on(toggle, 'click', () => this.toggle())
DomEvent.on(zoomTo, 'click', this.zoomTo, this) DomEvent.on(zoomTo, 'click', this.zoomTo, this)
container.classList.add(this.getHidableClass()) container.classList.add(this.getHidableClass())
container.classList.toggle('off', !this.isVisible()) 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 { 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 { export class Form extends Utils.WithEvents {
constructor(obj, fields, properties) { constructor(obj, fields, properties) {

View file

@ -1,12 +1,12 @@
import * as Utils from '../utils.js'
import { translate } from '../i18n.js'
import { import {
AjaxAutocomplete, AjaxAutocomplete,
AjaxAutocompleteMultiple, AjaxAutocompleteMultiple,
AutocompleteDatalist, AutocompleteDatalist,
} from '../autocomplete.js' } from '../autocomplete.js'
import { SCHEMA } from '../schema.js' import { translate } from '../i18n.js'
import * as Icon from '../rendering/icon.js' import * as Icon from '../rendering/icon.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
const Fields = {} const Fields = {}
@ -254,8 +254,8 @@ Fields.BlurInput = class extends Fields.Input {
const IntegerMixin = (Base) => const IntegerMixin = (Base) =>
class extends Base { class extends Base {
value() { value() {
return !isNaN(this.input.value) && this.input.value !== '' return !Number.isNaN(this.input.value) && this.input.value !== ''
? parseInt(this.input.value, 10) ? Number.parseInt(this.input.value, 10)
: undefined : undefined
} }
@ -270,8 +270,8 @@ Fields.BlurIntInput = class extends IntegerMixin(Fields.BlurInput) {}
const FloatMixin = (Base) => const FloatMixin = (Base) =>
class extends Base { class extends Base {
value() { value() {
return !isNaN(this.input.value) && this.input.value !== '' return !Number.isNaN(this.input.value) && this.input.value !== ''
? parseFloat(this.input.value) ? Number.parseFloat(this.input.value)
: undefined : undefined
} }
@ -390,7 +390,7 @@ Fields.Select = class extends BaseElement {
Fields.IntSelect = class extends Fields.Select { Fields.IntSelect = class extends Fields.Select {
value() { 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) */ /* Uses globals for: csv2geojson, osmtogeojson (not available as ESM) */
import { translate } from './i18n.js' import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
export const EXPORT_FORMATS = { export const EXPORT_FORMATS = {
geojson: { geojson: {

View file

@ -4,14 +4,14 @@ import {
AjaxAutocompleteMultiple, AjaxAutocompleteMultiple,
AutocompleteDatalist, AutocompleteDatalist,
} from './autocomplete.js' } from './autocomplete.js'
import { LineString, Point, Polygon } from './data/features.js'
import { LAYER_TYPES } from './data/layer.js'
import Help from './help.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 { ServerRequest } from './request.js'
import { SCHEMA } from './schema.js' import { SCHEMA } from './schema.js'
import * as Utils from './utils.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. // 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.

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ export class AutocompleteCommunes extends SingleMixin(BaseAjax) {
let options = { q: encodeURIComponent(value) } let options = { q: encodeURIComponent(value) }
const re = /^(0[1-9]|[1-9][ABab\d])\d{3}$/gm const re = /^(0[1-9]|[1-9][ABab\d])\d{3}$/gm
if (re.test(value)) { 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) } options = { code: encodeURIComponent(value) }
} }
return Util.template(url, options) return Util.template(url, options)

View file

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

View file

@ -1,11 +1,11 @@
import { import {
DivIcon,
DomEvent, DomEvent,
DomUtil, DomUtil,
DivIcon,
Icon, Icon,
} from '../../../vendors/leaflet/leaflet-src.esm.js' } from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js' import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
export function getClass(name) { export function getClass(name) {
switch (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 { translate } from '../../i18n.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js' import * as Utils from '../../utils.js'
import { CircleMarker } from '../ui.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, // Layer where each feature color is relative to the others,
// so we need all features before behing able to set one // 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 // WARNING must be loaded dynamically, or at least after leaflet.markercluster
// Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM // Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
import { translate } from '../../i18n.js' import { translate } from '../../i18n.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js' import * as Utils from '../../utils.js'
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { Cluster as ClusterIcon } from '../icon.js' import { Cluster as ClusterIcon } from '../icon.js'
import { LayerMixin } from './base.js'
const MarkerCluster = L.MarkerCluster.extend({ const MarkerCluster = L.MarkerCluster.extend({
// Custom class so we can call computeTextColor // Custom class so we can call computeTextColor

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,18 @@
// Goes here all code related to Leaflet, DOM and user interactions. // Goes here all code related to Leaflet, DOM and user interactions.
import { import {
Marker,
Polyline,
Polygon,
CircleMarker as BaseCircleMarker, CircleMarker as BaseCircleMarker,
DomEvent,
DomUtil, DomUtil,
LineUtil,
latLng,
LatLng, LatLng,
LatLngBounds, LatLngBounds,
DomEvent, LineUtil,
Marker,
Polygon,
Polyline,
latLng,
} from '../../../vendors/leaflet/leaflet-src.esm.js' } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js' import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { translate } from '../i18n.js'
import * as Utils from '../utils.js' import * as Utils from '../utils.js'
import * as Icon from './icon.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 { 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 { AutocompleteDatalist } from './autocomplete.js'
import Orderable from './orderable.js'
import { MutatingForm } from './form/builder.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] const EMPTY_VALUES = ['', undefined, null]

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,24 @@
import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js' import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { LineString, Point, Polygon } from '../data/features.js'
import { translate } from '../i18n.js' import { translate } from '../i18n.js'
import { WithTemplate } from '../utils.js' import { WithTemplate } from '../utils.js'
import ContextMenu from './contextmenu.js'
import * as Utils from '../utils.js' import * as Utils from '../utils.js'
import { Point, LineString, Polygon } from '../data/features.js' import ContextMenu from './contextmenu.js'
const TOP_BAR_TEMPLATE = ` const TOP_BAR_TEMPLATE = `
<div class="umap-main-edit-toolbox with-transition dark"> <div class="umap-main-edit-toolbox with-transition dark">
<div class="umap-left-edit-toolbox" data-ref="left"> <div class="umap-left-edit-toolbox" data-ref="left">
<div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div> <div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div>
<button class="map-name flat" type="button" data-ref="name"></button> <button class="map-name flat truncate" type="button" data-ref="name"></button>
<button class="share-status flat" type="button" data-ref="share"></button> <button class="share-status flat truncate" type="button" data-ref="share"></button>
<button class="edit-undo round" type="button" data-ref="undo" disabled>
<i class="icon icon-16 icon-undo"></i>
<span>${translate('Undo')}</span>
</button>
<button class="edit-redo round" type="button" data-ref="redo" disabled>
<i class="icon icon-16 icon-redo"></i>
<span>${translate('Redo')}</span>
</button>
</div> </div>
<div class="umap-right-edit-toolbox" data-ref="right"> <div class="umap-right-edit-toolbox" data-ref="right">
<button class="connected-peers round" type="button" data-ref="peers"> <button class="connected-peers round" type="button" data-ref="peers">
@ -19,20 +27,12 @@ const TOP_BAR_TEMPLATE = `
</button> </button>
<button class="umap-user flat" type="button" data-ref="user"> <button class="umap-user flat" type="button" data-ref="user">
<i class="icon icon-16 icon-profile"></i> <i class="icon icon-16 icon-profile"></i>
<span class="username" data-ref="username"></span> <span class="username truncate" data-ref="username"></span>
</button> </button>
<button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button> <button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
<button class="edit-undo round" type="button" data-ref="undo" disabled>
<i class="icon icon-16 icon-undo"></i>
<span class="">${translate('Undo')}</span>
</button>
<button class="edit-redo round" type="button" data-ref="redo" disabled>
<i class="icon icon-16 icon-redo"></i>
<span class="">${translate('Redo')}</span>
</button>
<button class="edit-disable round disabled-on-dirty" type="button" data-ref="view"> <button class="edit-disable round disabled-on-dirty" type="button" data-ref="view">
<i class="icon icon-16 icon-eye"></i> <i class="icon icon-16 icon-eye"></i>
<span class="">${translate('View')}</span> <span>${translate('View')}</span>
</button> </button>
<button class="edit-save button round enabled-on-dirty" type="button" data-ref="save"> <button class="edit-save button round enabled-on-dirty" type="button" data-ref="save">
<i class="icon icon-16 icon-save"></i> <i class="icon icon-16 icon-save"></i>
@ -173,6 +173,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-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('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> <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> </div>
` `
@ -195,6 +196,14 @@ export class BottomBar extends WithTemplate {
this._umap.openBrowser('filters') this._umap.openBrowser('filters')
) )
this._slideshow.renderToolbox(this.element) 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() this.redraw()
} }
@ -207,6 +216,27 @@ export class BottomBar extends WithTemplate {
this.elements.caption.hidden = !showMenus this.elements.caption.hidden = !showMenus
this.elements.browse.hidden = !showMenus this.elements.browse.hidden = !showMenus
this.elements.filter.hidden = !showMenus || !this._umap.properties.facetKey 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 { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js' import { translate } from '../i18n.js'
import { Positioned } from './base.js'
import * as Utils from '../utils.js' import * as Utils from '../utils.js'
import { Positioned } from './base.js'
export default class Tooltip extends Positioned { export default class Tooltip extends Positioned {
constructor(parent) { constructor(parent) {

View file

@ -2,36 +2,37 @@ import {
DomUtil, DomUtil,
Util as LeafletUtil, Util as LeafletUtil,
latLngBounds, latLngBounds,
stamp,
} from '../../vendors/leaflet/leaflet-src.esm.js' } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate, setLocale, getLocale } from './i18n.js' import {
import * as Utils from './utils.js' uMapAlert as Alert,
import { SyncEngine } from './sync/engine.js' uMapAlertCreation as AlertCreation,
import { LeafletMap } from './rendering/map.js' } from '../components/alerts/alert.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 Browser from './browser.js' import Browser from './browser.js'
import Caption from './caption.js' import Caption from './caption.js'
import Importer from './importer.js' import { DataLayer } from './data/layer.js'
import Rules from './rules.js' import Facets from './facets.js'
import Share from './share.js'
import {
uMapAlertCreation as AlertCreation,
uMapAlert as Alert,
} from '../components/alerts/alert.js'
import Orderable from './orderable.js'
import { MutatingForm } from './form/builder.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 { 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 { export default class Umap {
constructor(element, geojson) { constructor(element, geojson) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -180,7 +180,7 @@ def test_can_restore_version(live_server, openmap, page, datalayer):
page.get_by_role("button", name="Manage layers").click() page.get_by_role("button", name="Manage layers").click()
page.locator(".panel.right").get_by_title("Edit", exact=True).click() page.locator(".panel.right").get_by_title("Edit", exact=True).click()
page.get_by_text("Versions").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() page.get_by_role("button", name="OK").click()
expect(marker).to_have_class(re.compile(".*umap-ball-icon.*")) 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( expect(page.locator(".umap-main-edit-toolbox .map-name")).to_have_text(
"Imported map" "Imported map"
) )
expect(page.get_by_text("Tunnels")).to_be_visible() expect(page.locator(".panel.left").get_by_text("Tunnels")).to_be_visible()
expect(page.get_by_text("Cities")).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(".leaflet-control-minimap")).to_be_visible()
expect( expect(
page.locator('img[src="https://tile.openstreetmap.fr/hot/6/32/21.png"]') 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") peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
peerA.get_by_role("button", name="Open browser").click() peerA.get_by_role("button", name="Open browser").click()
expect(peerA.get_by_text("datalayer 1")).to_be_visible() expect(peerA.locator(".panel").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 2")).to_be_visible()
peerB.get_by_role("button", name="Open browser").click() peerB.get_by_role("button", name="Open browser").click()
expect(peerB.get_by_text("datalayer 1")).to_be_visible() expect(peerB.locator(".panel").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 2")).to_be_visible()
# Delete "datalayer 2" in peerA # Delete "datalayer 2" in peerA
peerA.locator(".datalayer").get_by_role("button", name="Delete layer").first.click() peerA.locator(".datalayer").get_by_role("button", name="Delete layer").first.click()
peerA.get_by_role("button", name="OK").click() peerA.get_by_role("button", name="OK").click()
expect(peerA.get_by_text("datalayer 2")).to_be_hidden() expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.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 # Save delete to the server
with peerA.expect_response(re.compile(".*/datalayer/delete/.*")): with peerA.expect_response(re.compile(".*/datalayer/delete/.*")):
peerA.get_by_role("button", name="Save").click() peerA.get_by_role("button", name="Save").click()
expect(peerA.get_by_text("datalayer 2")).to_be_hidden() expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.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") @pytest.mark.xdist_group(name="websockets")