Compare commits

...

16 commits

Author SHA1 Message Date
Yohan Boniface
2b4a9738a3
Merge pull request #1859 from umap-project/fix-max-height-panel-responsive
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
Test & Docs / docs (push) Has been cancelled
fix: panel condensed height should never be bigger than screen
2024-05-25 09:21:04 +02:00
Yohan Boniface
c10bc27fed fix: panel condensed height should never be bigger than screen 2024-05-24 18:39:11 +02:00
Yohan Boniface
a6a31a19a9
Merge pull request #1856 from umap-project/importer-module
chore: move importer to modules/
2024-05-23 19:47:38 +02:00
Yohan Boniface
ebf9be296d chore: move importer to modules/ 2024-05-23 19:32:07 +02:00
Yohan Boniface
109545d006 chore: prettier 2024-05-23 18:26:36 +02:00
Yohan Boniface
0c9c79195a
Merge pull request #1846 from umap-project/autocomplete-module
chore: move autocomplete to modules/
2024-05-23 15:43:25 +02:00
Yohan Boniface
1836647c00 chore: move autocomplete to modules/ 2024-05-23 15:10:46 +02:00
David Larlet
d6a20b3dda
Merge pull request #1847 from umap-project/ui-to-modules
chore: move ui to dedicated modules
2024-05-22 13:05:32 -04:00
David Larlet
ef705a862e
Merge pull request #1851 from umap-project/audio-video-tags
fix: allow audio and video tags (+attributes) in HTML
2024-05-22 12:52:16 -04:00
David Larlet
d4830f6128
Merge pull request #1852 from umap-project/1848-invert-star-icons
fix: invert star icons when map is starred or not
2024-05-22 12:51:58 -04:00
David Larlet
5f29b8b0d5
fix: invert star icons when map is starred or not
Fixes #1848
2024-05-22 11:05:44 -04:00
David Larlet
5b624167c0
fix: allow audio and video tags (+attributes) in HTML
Refs https://forum.openstreetmap.fr/t/umap-audio-video-et-panneau-lateral/2804/2
2024-05-22 10:54:24 -04:00
Yohan Boniface
776d92e7cc chore: add minimal dialog class to replace custom made help box 2024-05-22 14:00:53 +02:00
Yohan Boniface
8e446dbe70 chore: move panel.js to ui/ subfolder 2024-05-22 11:50:59 +02:00
Yohan Boniface
2ed9bc65ee chore: move tooltip to a dedicated module 2024-05-22 11:40:48 +02:00
Yohan Boniface
8ddc570e23 chore: move alert to dedicated module 2024-05-22 11:39:16 +02:00
35 changed files with 943 additions and 912 deletions

View file

@ -12,7 +12,7 @@
data-url="https://umap.openstreetmap.fr/fr/map/new/" data-url="https://umap.openstreetmap.fr/fr/map/new/"
data-alt="Panneau daide au formatage." data-alt="Panneau daide au formatage."
data-caption="Panneau daide au formatage." data-caption="Panneau daide au formatage."
data-selector=".umap-help-box" data-selector=".umap-dialog"
data-width="510" data-width="510"
data-height="326" data-height="326"
data-padding="5" data-padding="5"

View file

@ -756,146 +756,6 @@ input[type=hidden].blur + [type="button"] {
} }
/* *********** */
/* Alerts */
/* *********** */
#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%;
}
/* *********** */
/* Tooltip */
/* *********** */
#umap-tooltip-container {
line-height: 20px;
padding: 5px 10px;
width: auto;
position: absolute;
box-shadow: 0 1px 7px #999999;
display: none;
background-color: rgba(40, 40, 40, 0.8);
color: #eeeeec;
font-size: 0.8em;
border-radius: 2px;
z-index: 1011;
font-weight: normal;
max-width: 300px;
}
.umap-tooltip #umap-tooltip-container {
display: block;
}
#umap-tooltip-container.tooltip-top:after {
top: 100%;
left: calc(50% - 11px);
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: rgba(30, 30, 30, 0.8);
border-width: 11px;
margin-left: calc(-50% + 21px);
}
#umap-tooltip-container.tooltip-bottom:before {
top: -22px;
left: calc(50% - 11px);
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: rgba(30, 30, 30, 0.7);
border-width: 11px;
transform: rotate(180deg);
}
#umap-tooltip-container.tooltip.tooltip-left:after {
left: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(136, 183, 213, 0);
border-left-color: #333;
border-width: 11px;
margin-top: -10px;
}
/* *********** */
/* Close link */
/* *********** */
#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;
}
/* *********** */ /* *********** */
/* Various */ /* Various */
/* *********** */ /* *********** */
@ -913,15 +773,3 @@ input[type=hidden].blur + [type="button"] {
height: 100vh; height: 100vh;
opacity: 0.5; opacity: 0.5;
} }
/* *********** */
/* Mobile */
/* *********** */
@media all and (orientation:portrait) {
#umap-alert-container {
width: 100%;
left: 0;
right: 0;
}
}

View file

@ -0,0 +1,75 @@
#umap-alert-container {
min-height: 46px;
line-height: 46px;
padding-left: 10px;
width: calc(100% - 500px);
position: absolute;
top: -46px;
left: 250px; /* Keep save/cancel button accessible. */
right: 250px;
box-shadow: 0 1px 7px #999999;
visibility: hidden;
background: none repeat scroll 0 0 rgba(20, 22, 23, 0.8);
font-weight: bold;
color: #fff;
font-size: 0.8em;
z-index: 1012;
border-radius: 2px;
}
#umap-alert-container.error {
background-color: #c60f13;
}
.umap-alert #umap-alert-container {
visibility: visible;
top: 23px;
}
#umap-alert-container .umap-action {
margin-left: 10px;
background-color: #fff;
color: #000;
padding: 5px;
border-radius: 4px;
}
#umap-alert-container .umap-action:hover {
color: #000;
}
#umap-alert-container .error .umap-action {
background-color: #666;
color: #eee;
}
#umap-alert-container .error .umap-action:hover {
color: #fff;
}
#umap-alert-container input {
padding: 5px;
border-radius: 4px;
width: 100%;
}
#umap-alert-container .umap-close-link {
color: #fff;
float: right;
padding-right: 10px;
width: 100px;
line-height: 1;
margin: .5rem;
background-color: #202425;
font-size: .7rem;
}
#umap-alert-container .umap-close-icon {
background-position: -74px -55px;
}
#umap-alert-container .umap-alert-actions {
display: flex;
margin: 1rem;
}
#umap-alert-container .umap-alert-actions .umap-action {
margin-bottom: 0;
}
@media all and (orientation:portrait) {
#umap-alert-container {
width: 100%;
left: 0;
right: 0;
}
}

View file

@ -0,0 +1,17 @@
.umap-dialog {
z-index: 10001;
margin: auto;
margin-top: 100px;
width: 50vw;
max-width: 100vw;
max-height: 50vh;
padding: 20px;
border: 1px solid #222;
background-color: var(--background-color);
color: var(--text-color);
border-radius: 5px;
}
.umap-dialog .umap-close-link {
float: right;
width: 100px;
}

View file

@ -7,14 +7,16 @@
overflow-x: auto; overflow-x: auto;
z-index: 1010; z-index: 1010;
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-color);
opacity: 0.98; opacity: 0.98;
cursor: initial; cursor: initial;
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--color-lightGray); border: 1px solid var(--color-lightGray);
bottom: calc(var(--current-footer-height) + var(--panel-bottom));
box-sizing: border-box;
} }
.panel.dark { .panel.dark {
border: 1px solid #222; border: 1px solid #222;
color: #efefef;
} }
.panel.full { .panel.full {
width: initial; width: initial;
@ -25,16 +27,9 @@
visibility: visible; visibility: visible;
right: var(--panel-gutter); right: var(--panel-gutter);
left: var(--panel-gutter); left: var(--panel-gutter);
top: var(--header-height);
height: initial; height: initial;
max-height: initial; max-height: initial;
} }
.umap-caption-bar-enabled .panel {
bottom: calc(var(--footer-height) + var(--panel-bottom));
}
.panel {
box-sizing: border-box;
}
.panel .umap-popup-content img { .panel .umap-popup-content img {
/* See https://github.com/Leaflet/Leaflet/commit/61d746818b99d362108545c151a27f09d60960ee#commitcomment-6061847 */ /* See https://github.com/Leaflet/Leaflet/commit/61d746818b99d362108545c151a27f09d60960ee#commitcomment-6061847 */
max-width: 99% !important; max-width: 99% !important;
@ -86,13 +81,13 @@
} }
@media all and (orientation:landscape) { @media all and (orientation:landscape) {
.panel { .panel {
top: 0; top: var(--current-header-height);
margin-top: var(--panel-gutter); margin-top: var(--panel-gutter);
width: var(--panel-width); width: var(--panel-width);
max-width: calc(100% - var(--panel-gutter) * 2 - var(--control-size)) max-width: calc(100% - var(--panel-gutter) * 2 - var(--control-size))
} }
.panel.condensed { .panel.condensed {
max-height: 500px; max-height: calc(min(500px, 100% - var(--current-header-height) - var(--current-footer-height) - var(--panel-gutter) * 2));
bottom: initial; bottom: initial;
} }
.panel.right { .panel.right {
@ -109,16 +104,13 @@
right: calc(var(--panel-gutter) * 2 + var(--control-size)); right: calc(var(--panel-gutter) * 2 + var(--control-size));
visibility: visible; visibility: visible;
} }
.umap-edit-enabled .panel {
top: var(--header-height);
}
} }
@media all and (orientation:portrait) { @media all and (orientation:portrait) {
.panel { .panel {
height: 50%; height: 50%;
max-height: 400px; max-height: 400px;
width: 100%; width: 100%;
bottom: 0; bottom: var(--current-footer-height);
right: -100%; right: -100%;
} }
.panel.left { .panel.left {
@ -130,16 +122,11 @@
visibility: visible; visibility: visible;
} }
.panel.expanded { .panel.expanded {
height: 100%; height: calc(100% - var(--current-footer-height));
max-height: 100%; max-height: calc(100% - var(--current-footer-height));
} }
.umap-caption-bar-enabled .panel { .umap-caption-bar-enabled .panel {
bottom: var(--footer-height);
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
.umap-caption-bar-enabled .panel.expanded {
height: calc(100% - var(--footer-height));
max-height: calc(100% - var(--footer-height));
}
} }

View file

@ -0,0 +1,59 @@
#umap-tooltip-container {
line-height: 20px;
padding: 5px 10px;
width: auto;
position: absolute;
box-shadow: 0 1px 7px #999999;
display: none;
background-color: rgba(40, 40, 40, 0.8);
color: #eeeeec;
font-size: 0.8em;
border-radius: 2px;
z-index: 1011;
font-weight: normal;
max-width: 300px;
}
.umap-tooltip #umap-tooltip-container {
display: block;
}
#umap-tooltip-container.tooltip-top:after {
top: 100%;
left: calc(50% - 11px);
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: rgba(30, 30, 30, 0.8);
border-width: 11px;
margin-left: calc(-50% + 21px);
}
#umap-tooltip-container.tooltip-bottom:before {
top: -22px;
left: calc(50% - 11px);
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: rgba(30, 30, 30, 0.7);
border-width: 11px;
transform: rotate(180deg);
}
#umap-tooltip-container.tooltip.tooltip-left:after {
left: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(136, 183, 213, 0);
border-left-color: #333;
border-width: 11px;
margin-top: -10px;
}

View file

@ -0,0 +1,309 @@
import { DomUtil, DomEvent, setOptions } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import { ServerRequest } from './request.js'
import Alert from './ui/alert.js'
export class BaseAutocomplete {
constructor(el, options) {
this.el = el
this.options = {
placeholder: translate('Start typing...'),
emptyMessage: translate('No result'),
allowFree: true,
minChar: 2,
maxResults: 5,
}
this.cache = ''
this.results = []
this._current = null
setOptions(this, options)
this.createInput()
this.createContainer()
this.selectedContainer = this.initSelectedContainer()
}
get current() {
return this._current
}
set current(index) {
if (typeof index === 'object') {
index = this.resultToIndex(index)
}
this._current = index
}
createInput() {
this.input = DomUtil.element({
tagName: 'input',
type: 'text',
parent: this.el,
placeholder: this.options.placeholder,
autocomplete: 'off',
className: this.options.className,
})
DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
DomEvent.on(this.input, 'keyup', this.onKeyUp, this)
DomEvent.on(this.input, 'blur', this.onBlur, this)
}
createContainer() {
this.container = DomUtil.element({
tagName: 'ul',
parent: document.body,
className: 'umap-autocomplete',
})
}
resizeContainer() {
const l = this.getLeft(this.input)
const t = this.getTop(this.input) + this.input.offsetHeight
this.container.style.left = `${l}px`
this.container.style.top = `${t}px`
const width = this.options.width ? this.options.width : this.input.offsetWidth - 2
this.container.style.width = `${width}px`
}
onKeyDown(e) {
switch (e.key) {
case 'Tab':
if (this.current !== null) this.setChoice()
DomEvent.stop(e)
break
case 'Enter':
DomEvent.stop(e)
this.setChoice()
break
case 'Escape':
DomEvent.stop(e)
this.hide()
break
case 'ArrowDown':
if (this.results.length > 0) {
if (this.current !== null && this.current < this.results.length - 1) {
// what if one result?
this.current++
this.highlight()
} else if (this.current === null) {
this.current = 0
this.highlight()
}
}
break
case 'ArrowUp':
if (this.current !== null) {
DomEvent.stop(e)
}
if (this.results.length > 0) {
if (this.current > 0) {
this.current--
this.highlight()
} else if (this.current === 0) {
this.current = null
this.highlight()
}
}
break
}
}
onKeyUp(e) {
const special = [
'Tab',
'Enter',
'ArrowLeft',
'ArrowRight',
'ArrowDown',
'ArrowUp',
'Meta',
'Shift',
'Alt',
'Control',
]
if (!special.includes(e.key)) {
this.search()
}
}
onBlur() {
setTimeout(() => this.hide(), 100)
}
clear() {
this.results = []
this.current = null
this.cache = ''
this.container.innerHTML = ''
}
hide() {
this.clear()
this.container.style.display = 'none'
this.input.value = ''
}
setChoice(choice) {
choice = choice || this.results[this.current]
if (choice) {
this.input.value = choice.item.label
this.options.on_select(choice)
this.displaySelected(choice)
this.hide()
if (this.options.callback) {
this.options.callback.bind(this)(choice)
}
}
}
createResult(item) {
const el = DomUtil.element({
tagName: 'li',
parent: this.container,
textContent: item.label,
})
const result = {
item: item,
el: el,
}
DomEvent.on(el, 'mouseover', () => {
this.current = result
this.highlight()
})
DomEvent.on(el, 'mousedown', () => this.setChoice())
return result
}
resultToIndex(result) {
return this.results.findIndex((item) => item.item.value === result.item.value)
}
handleResults(data) {
this.clear()
this.container.style.display = 'block'
this.resizeContainer()
data.forEach((item) => {
this.results.push(this.createResult(item))
})
this.current = 0
this.highlight()
//TODO manage no results
}
highlight() {
this.results.forEach((result, index) => {
if (index === this.current) DomUtil.addClass(result.el, 'on')
else DomUtil.removeClass(result.el, 'on')
})
}
getLeft(el) {
let tmp = el.offsetLeft
el = el.offsetParent
while (el) {
tmp += el.offsetLeft
el = el.offsetParent
}
return tmp
}
getTop(el) {
let tmp = el.offsetTop
el = el.offsetParent
while (el) {
tmp += el.offsetTop
el = el.offsetParent
}
return tmp
}
}
class BaseAjax extends BaseAutocomplete {
constructor(el, options) {
super(el, options)
const alert = new Alert(document.querySelector('header'))
this.server = new ServerRequest(alert)
}
optionToResult(option) {
return {
value: option.value,
label: option.innerHTML,
}
}
async search() {
let val = this.input.value
if (val.length < this.options.minChar) {
this.clear()
return
}
if (val === this.cache) return
else this.cache = val
val = val.toLowerCase()
const [{ data }, response] = await this.server.get(
`/agnocomplete/AutocompleteUser/?q=${encodeURIComponent(val)}`
)
this.handleResults(data)
}
}
export class AjaxAutocompleteMultiple extends BaseAjax {
initSelectedContainer() {
return DomUtil.after(
this.input,
DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' })
)
}
displaySelected(result) {
const result_el = DomUtil.element({
tagName: 'li',
parent: this.selectedContainer,
})
result_el.textContent = result.item.label
const close = DomUtil.element({
tagName: 'span',
parent: result_el,
className: 'close',
textContent: '×',
})
DomEvent.on(close, 'click', () => {
this.selectedContainer.removeChild(result_el)
this.options.on_unselect(result)
})
this.hide()
}
}
export class AjaxAutocomplete extends BaseAjax {
initSelectedContainer() {
return DomUtil.after(
this.input,
DomUtil.element({ tagName: 'div', className: 'umap-singleresult' })
)
}
displaySelected(result) {
const result_el = DomUtil.element({
tagName: 'div',
parent: this.selectedContainer,
})
result_el.textContent = result.item.label
const close = DomUtil.element({
tagName: 'span',
parent: result_el,
className: 'close',
textContent: '×',
})
this.input.style.display = 'none'
DomEvent.on(
close,
'click',
function () {
this.selectedContainer.innerHTML = ''
this.input.style.display = 'block'
},
this
)
this.hide()
}
}

View file

@ -2,11 +2,16 @@ import URLs from './urls.js'
import Browser from './browser.js' 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 './panel.js' import { Panel, EditPanel, FullPanel } from './ui/panel.js'
import Alert from './ui/alert.js'
import Dialog from './ui/dialog.js'
import Tooltip from './ui/tooltip.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
import { SCHEMA } from './schema.js' import { SCHEMA } from './schema.js'
import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js'
import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js'
import Orderable from './orderable.js' import Orderable from './orderable.js'
import Importer from './importer.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,10 +26,16 @@ window.U = {
Browser, Browser,
Facets, Facets,
Panel, Panel,
Alert,
Dialog,
Tooltip,
EditPanel, EditPanel,
FullPanel, FullPanel,
Utils, Utils,
SCHEMA, SCHEMA,
Importer,
Orderable, Orderable,
Caption, Caption,
AjaxAutocomplete,
AjaxAutocompleteMultiple,
} }

View file

@ -1,104 +1,107 @@
U.Importer = L.Class.extend({ import { DomUtil, DomEvent } from '../../vendors/leaflet/leaflet-src.esm.js'
TYPES: ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap'], import { translate } from './i18n.js'
initialize: function (map) {
export default class Importer {
constructor(map) {
this.map = map this.map = map
this.presets = map.options.importPresets this.presets = map.options.importPresets
}, this.TYPES = ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap']
}
build: function () { build() {
this.container = L.DomUtil.create('div', 'umap-upload') this.container = DomUtil.create('div', 'umap-upload')
this.title = L.DomUtil.createTitle( this.title = DomUtil.createTitle(
this.container, this.container,
L._('Import data'), translate('Import data'),
'icon-upload' 'icon-upload'
) )
this.presetBox = L.DomUtil.create('div', 'formbox', this.container) this.presetBox = DomUtil.create('div', 'formbox', this.container)
this.presetSelect = L.DomUtil.create('select', '', this.presetBox) this.presetSelect = DomUtil.create('select', '', this.presetBox)
this.fileBox = L.DomUtil.create('div', 'formbox', this.container) this.fileBox = DomUtil.create('div', 'formbox', this.container)
this.fileInput = L.DomUtil.element({ this.fileInput = DomUtil.element({
tagName: 'input', tagName: 'input',
type: 'file', type: 'file',
parent: this.fileBox, parent: this.fileBox,
multiple: 'multiple', multiple: 'multiple',
autofocus: true, autofocus: true,
}) })
this.urlInput = L.DomUtil.element({ this.urlInput = DomUtil.element({
tagName: 'input', tagName: 'input',
type: 'text', type: 'text',
parent: this.container, parent: this.container,
placeholder: L._('Provide an URL here'), placeholder: translate('Provide an URL here'),
}) })
this.rawInput = L.DomUtil.element({ this.rawInput = DomUtil.element({
tagName: 'textarea', tagName: 'textarea',
parent: this.container, parent: this.container,
placeholder: L._('Paste your data here'), placeholder: translate('Paste your data here'),
}) })
this.typeLabel = L.DomUtil.add( this.typeLabel = DomUtil.add(
'label', 'label',
'', '',
this.container, this.container,
L._('Choose the format of the data to import') translate('Choose the format of the data to import')
) )
this.layerLabel = L.DomUtil.add( this.layerLabel = DomUtil.add(
'label', 'label',
'', '',
this.container, this.container,
L._('Choose the layer to import in') translate('Choose the layer to import in')
) )
this.clearLabel = L.DomUtil.element({ this.clearLabel = DomUtil.element({
tagName: 'label', tagName: 'label',
parent: this.container, parent: this.container,
textContent: L._('Replace layer content'), textContent: translate('Replace layer content'),
for: 'datalayer-clear-check', for: 'datalayer-clear-check',
}) })
this.submitInput = L.DomUtil.element({ this.submitInput = DomUtil.element({
tagName: 'input', tagName: 'input',
type: 'button', type: 'button',
parent: this.container, parent: this.container,
value: L._('Import'), value: translate('Import'),
className: 'button', className: 'button',
}) })
this.map.help.button(this.typeLabel, 'importFormats') this.map.help.button(this.typeLabel, 'importFormats')
this.typeInput = L.DomUtil.element({ this.typeInput = DomUtil.element({
tagName: 'select', tagName: 'select',
name: 'format', name: 'format',
parent: this.typeLabel, parent: this.typeLabel,
}) })
this.layerInput = L.DomUtil.element({ this.layerInput = DomUtil.element({
tagName: 'select', tagName: 'select',
name: 'datalayer', name: 'datalayer',
parent: this.layerLabel, parent: this.layerLabel,
}) })
this.clearFlag = L.DomUtil.element({ this.clearFlag = DomUtil.element({
tagName: 'input', tagName: 'input',
type: 'checkbox', type: 'checkbox',
name: 'clear', name: 'clear',
id: 'datalayer-clear-check', id: 'datalayer-clear-check',
parent: this.clearLabel, parent: this.clearLabel,
}) })
L.DomUtil.element({ DomUtil.element({
tagName: 'option', tagName: 'option',
value: '', value: '',
textContent: L._('Choose the data format'), textContent: translate('Choose the data format'),
parent: this.typeInput, parent: this.typeInput,
}) })
for (let i = 0; i < this.TYPES.length; i++) { for (const type of this.TYPES) {
option = L.DomUtil.create('option', '', this.typeInput) const option = DomUtil.create('option', '', this.typeInput)
option.value = option.textContent = this.TYPES[i] option.value = option.textContent = type
} }
if (this.presets.length) { if (this.presets.length) {
const noPreset = L.DomUtil.create('option', '', this.presetSelect) const noPreset = DomUtil.create('option', '', this.presetSelect)
noPreset.value = noPreset.textContent = L._('Choose a preset') noPreset.value = noPreset.textContent = translate('Choose a preset')
for (let j = 0; j < this.presets.length; j++) { for (const preset of this.presets) {
option = L.DomUtil.create('option', '', presetSelect) option = DomUtil.create('option', '', presetSelect)
option.value = this.presets[j].url option.value = preset.url
option.textContent = this.presets[j].label option.textContent = preset.label
} }
} else { } else {
this.presetBox.style.display = 'none' this.presetBox.style.display = 'none'
} }
L.DomEvent.on(this.submitInput, 'click', this.submit, this) DomEvent.on(this.submitInput, 'click', this.submit, this)
L.DomEvent.on( DomEvent.on(
this.fileInput, this.fileInput,
'change', 'change',
(e) => { (e) => {
@ -116,9 +119,9 @@ U.Importer = L.Class.extend({
}, },
this this
) )
}, }
open: function () { open() {
if (!this.container) this.build() if (!this.container) this.build()
const onLoad = this.map.editPanel.open({ content: this.container }) const onLoad = this.map.editPanel.open({ content: this.container })
onLoad.then(() => { onLoad.then(() => {
@ -128,25 +131,25 @@ U.Importer = L.Class.extend({
this.map.eachDataLayerReverse((datalayer) => { this.map.eachDataLayerReverse((datalayer) => {
if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) { if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) {
const id = L.stamp(datalayer) const id = L.stamp(datalayer)
option = L.DomUtil.add('option', '', this.layerInput, datalayer.options.name) option = DomUtil.add('option', '', this.layerInput, datalayer.options.name)
option.value = id option.value = id
} }
}) })
L.DomUtil.element({ DomUtil.element({
tagName: 'option', tagName: 'option',
value: '', value: '',
textContent: L._('Import in a new layer'), textContent: translate('Import in a new layer'),
parent: this.layerInput, parent: this.layerInput,
}) })
}) })
}, }
openFiles: function () { openFiles() {
this.open() this.open()
this.fileInput.showPicker() this.fileInput.showPicker()
}, }
submit: function () { submit() {
let type = this.typeInput.value let type = this.typeInput.value
const layerId = this.layerInput[this.layerInput.selectedIndex].value const layerId = this.layerInput[this.layerInput.selectedIndex].value
let layer let layer
@ -161,15 +164,15 @@ U.Importer = L.Class.extend({
} }
} else { } else {
if (!type) if (!type)
return this.map.ui.alert({ return this.map.alert.open({
content: L._('Please choose a format'), content: translate('Please choose a format'),
level: 'error', 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.ui.alert({ content: L._('Invalid umap data'), level: 'error' }) this.alert.open({ content: translate('Invalid umap data'), level: 'error' })
console.error(e) console.error(e)
} }
} else { } else {
@ -183,5 +186,5 @@ U.Importer = L.Class.extend({
) )
} }
} }
}, }
}) }

View file

@ -47,14 +47,18 @@ 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(ui) { constructor(alert) {
super() super()
this.ui = ui this.alert = alert
}
fire(name, params) {
document.body.dispatchEvent(new CustomEvent(name, params))
} }
async _fetch(method, uri, headers, data) { async _fetch(method, uri, headers, data) {
const id = Math.random() const id = Math.random()
this.ui.fire('dataloading', { id: id }) this.fire('dataloading', { id: id })
try { try {
const response = await BaseRequest.prototype._fetch.call( const response = await BaseRequest.prototype._fetch.call(
this, this,
@ -68,7 +72,7 @@ export class Request extends BaseRequest {
if (error instanceof NOKError) return this._onNOK(error) if (error instanceof NOKError) return this._onNOK(error)
return this._onError(error) return this._onError(error)
} finally { } finally {
this.ui.fire('dataload', { id: id }) this.fire('dataload', { id: id })
} }
} }
@ -81,7 +85,7 @@ export class Request extends BaseRequest {
} }
_onError(error) { _onError(error) {
this.ui.alert({ content: L._('Problem in the response'), level: 'error' }) this.alert.open({ content: L._('Problem in the response'), level: 'error' })
} }
_onNOK(error) { _onNOK(error) {
@ -127,9 +131,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.ui.alert({ content: data.info, level: 'info' }) this.alert.open({ content: data.info, level: 'info' })
} else if (data.error) { } else if (data.error) {
this.ui.alert({ content: data.error, level: 'error' }) this.alert.open({ content: data.error, level: 'error' })
return this._onError(new Error(data.error)) return this._onError(new Error(data.error))
} }
return [data, response, null] return [data, response, null]
@ -144,7 +148,7 @@ export class ServerRequest extends Request {
_onNOK(error) { _onNOK(error) {
if (error.status === 403) { if (error.status === 403) {
this.ui.alert({ this.alert.open({
content: error.message || L._('Action not allowed :('), content: error.message || L._('Action not allowed :('),
level: 'error', level: 'error',
}) })

View file

@ -0,0 +1,82 @@
import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
const ALERTS = []
let ALERT_ID = null
export default class Alert {
constructor(parent) {
this.parent = parent
this.container = DomUtil.create('div', 'with-transition', this.parent)
this.container.id = 'umap-alert-container'
DomEvent.disableClickPropagation(this.container)
DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu.
DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation)
DomEvent.on(this.container, 'MozMousePixelScroll', DomEvent.stopPropagation)
}
open(params) {
if (DomUtil.hasClass(this.parent, 'umap-alert')) ALERTS.push(params)
else this._open(params)
}
_open(params) {
if (!params) {
if (ALERTS.length) params = ALERTS.pop()
else return
}
let timeoutID
const level_class = params.level && params.level == 'info' ? 'info' : 'error'
this.container.innerHTML = ''
DomUtil.addClass(this.parent, 'umap-alert')
DomUtil.addClass(this.container, level_class)
const close = () => {
if (timeoutID && timeoutID !== ALERT_ID) {
return
} // Another alert has been forced
this.container.innerHTML = ''
DomUtil.removeClass(this.parent, 'umap-alert')
DomUtil.removeClass(this.container, level_class)
if (timeoutID) window.clearTimeout(timeoutID)
this._open()
}
const closeButton = DomUtil.createButton(
'umap-close-link',
this.container,
'',
close,
this
)
DomUtil.create('i', 'umap-close-icon', closeButton)
const label = DomUtil.create('span', '', closeButton)
label.title = label.textContent = translate('Close')
DomUtil.element({
tagName: 'div',
innerHTML: params.content,
parent: this.container,
})
let action, el, input
const form = DomUtil.create('div', 'umap-alert-actions', this.container)
for (let action of params.actions || []) {
if (action.input) {
input = DomUtil.element({
tagName: 'input',
parent: form,
className: 'umap-alert-input',
placeholder: action.input,
})
}
el = DomUtil.createButton(
'umap-action',
form,
action.label,
action.callback,
action.callbackContext
)
DomEvent.on(el, 'click', close, this)
}
if (params.duration !== Infinity) {
ALERT_ID = timeoutID = window.setTimeout(close, params.duration || 3000)
}
}
}

View file

@ -0,0 +1,40 @@
import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
export default class Dialog {
constructor(parent) {
this.parent = parent
this.container = DomUtil.create('dialog', 'umap-dialog', this.parent)
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)
}
get visible() {
return this.container.open
}
close() {
this.container.close()
}
open({ className, content, modal } = {}) {
this.container.innerHTML = ''
if (modal) this.container.showModal()
else this.container.show()
if (className) {
this.container.classList.add(className)
}
const closeButton = DomUtil.createButton(
'umap-close-link',
this.container,
'',
() => this.container.close()
)
DomUtil.createIcon(closeButton, 'icon-close')
const label = DomUtil.create('span', '', closeButton)
label.title = label.textContent = translate('Close')
this.container.appendChild(content)
}
}

View file

@ -1,5 +1,5 @@
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'
export class Panel { export class Panel {
constructor(map) { constructor(map) {

View file

@ -0,0 +1,116 @@
import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
export default class Tooltip {
constructor(parent) {
this.parent = parent
this.container = DomUtil.create('div', 'with-transition', this.parent)
this.container.id = 'umap-tooltip-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(opts) {
function showIt() {
if (opts.anchor && opts.position === 'top') {
this.anchorTop(opts.anchor)
} else if (opts.anchor && opts.position === 'left') {
this.anchorLeft(opts.anchor)
} else if (opts.anchor && opts.position === 'bottom') {
this.anchorBottom(opts.anchor)
} else {
this.anchorAbsolute()
}
L.DomUtil.addClass(this.parent, 'umap-tooltip')
this.container.innerHTML = U.Utils.escapeHTML(opts.content)
}
this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0)
const id = this.TOOLTIP_ID
const closeIt = () => {
this.close(id)
}
if (opts.anchor) {
L.DomEvent.once(opts.anchor, 'mouseout', closeIt)
}
if (opts.duration !== Infinity) {
window.setTimeout(closeIt, opts.duration || 3000)
}
}
anchorAbsolute() {
this.container.className = ''
const left =
this.parent.offsetLeft +
this.parent.clientWidth / 2 -
this.container.clientWidth / 2,
top = this.parent.offsetTop + 75
this.setPosition({ top: top, left: left })
}
anchorTop(el) {
this.container.className = 'tooltip-top'
const coords = this.getPosition(el)
this.setPosition({
left: coords.left - 10,
bottom: this.getDocHeight() - coords.top + 11,
})
}
anchorBottom(el) {
this.container.className = 'tooltip-bottom'
const coords = this.getPosition(el)
this.setPosition({
left: coords.left,
top: coords.bottom + 11,
})
}
anchorLeft(el) {
this.container.className = 'tooltip-left'
const coords = this.getPosition(el)
this.setPosition({
top: coords.top,
right: document.documentElement.offsetWidth - coords.left + 11,
})
}
close(id) {
// Clear timetout even if a new tooltip has been added
// in the meantime. Eg. after a mouseout from the anchor.
window.clearTimeout(id)
if (id && id !== this.TOOLTIP_ID) return
this.container.className = ''
this.container.innerHTML = ''
this.setPosition({})
L.DomUtil.removeClass(this.parent, 'umap-tooltip')
}
getPosition(el) {
return el.getBoundingClientRect()
}
setPosition(coords) {
if (coords.left) this.container.style.left = `${coords.left}px`
else this.container.style.left = 'initial'
if (coords.right) this.container.style.right = `${coords.right}px`
else this.container.style.right = 'initial'
if (coords.top) this.container.style.top = `${coords.top}px`
else this.container.style.top = 'initial'
if (coords.bottom) this.container.style.bottom = `${coords.bottom}px`
else this.container.style.bottom = 'initial'
}
getDocHeight() {
const D = document
return Math.max(
D.body.scrollHeight,
D.documentElement.scrollHeight,
D.body.offsetHeight,
D.documentElement.offsetHeight,
D.body.clientHeight,
D.documentElement.clientHeight
)
}
}

View file

@ -84,11 +84,21 @@ export function escapeHTML(s) {
'div', 'div',
'iframe', 'iframe',
'img', 'img',
'audio',
'video',
'source',
'br', 'br',
'span', 'span',
], ],
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'], ADD_ATTR: [
ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'style', 'dir', 'title'], 'target',
'allow',
'allowfullscreen',
'frameborder',
'scrolling',
'controls',
],
ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'style', 'dir', 'title', 'type'],
// Added: `geo:` URL scheme as defined in RFC5870: // Added: `geo:` URL scheme as defined in RFC5870:
// https://www.rfc-editor.org/rfc/rfc5870.html // https://www.rfc-editor.org/rfc/rfc5870.html
// The base RegExp comes from: // The base RegExp comes from:

View file

@ -1,341 +0,0 @@
U.AutoComplete = L.Class.extend({
options: {
placeholder: 'Start typing...',
emptyMessage: 'No result',
allowFree: true,
minChar: 2,
maxResults: 5,
},
CACHE: '',
RESULTS: [],
initialize: function (el, options) {
this.el = el
const ui = new U.UI(document.querySelector('header'))
this.server = new U.ServerRequest(ui)
L.setOptions(this, options)
let CURRENT = null
try {
Object.defineProperty(this, 'CURRENT', {
get: function () {
return CURRENT
},
set: function (index) {
if (typeof index === 'object') {
index = this.resultToIndex(index)
}
CURRENT = index
},
})
} catch (e) {
// Hello IE8
}
return this
},
createInput: function () {
this.input = L.DomUtil.element({
tagName: 'input',
type: 'text',
parent: this.el,
placeholder: this.options.placeholder,
autocomplete: 'off',
className: this.options.className,
})
L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this)
L.DomEvent.on(this.input, 'blur', this.onBlur, this)
},
createContainer: function () {
this.container = L.DomUtil.element({
tagName: 'ul',
parent: document.body,
className: 'umap-autocomplete',
})
},
resizeContainer: function () {
const l = this.getLeft(this.input)
const t = this.getTop(this.input) + this.input.offsetHeight
this.container.style.left = `${l}px`
this.container.style.top = `${t}px`
const width = this.options.width ? this.options.width : this.input.offsetWidth - 2
this.container.style.width = `${width}px`
},
onKeyDown: function (e) {
switch (e.keyCode) {
case U.Keys.TAB:
if (this.CURRENT !== null) this.setChoice()
L.DomEvent.stop(e)
break
case U.Keys.ENTER:
L.DomEvent.stop(e)
this.setChoice()
break
case U.Keys.ESC:
L.DomEvent.stop(e)
this.hide()
break
case U.Keys.DOWN:
if (this.RESULTS.length > 0) {
if (this.CURRENT !== null && this.CURRENT < this.RESULTS.length - 1) {
// what if one result?
this.CURRENT++
this.highlight()
} else if (this.CURRENT === null) {
this.CURRENT = 0
this.highlight()
}
}
break
case U.Keys.UP:
if (this.CURRENT !== null) {
L.DomEvent.stop(e)
}
if (this.RESULTS.length > 0) {
if (this.CURRENT > 0) {
this.CURRENT--
this.highlight()
} else if (this.CURRENT === 0) {
this.CURRENT = null
this.highlight()
}
}
break
}
},
onKeyUp: function (e) {
const special = [
U.Keys.TAB,
U.Keys.ENTER,
U.Keys.LEFT,
U.Keys.RIGHT,
U.Keys.DOWN,
U.Keys.UP,
U.Keys.APPLE,
U.Keys.SHIFT,
U.Keys.ALT,
U.Keys.CTRL,
]
if (special.indexOf(e.keyCode) === -1) {
this.search()
}
},
onBlur: function () {
setTimeout(() => this.hide(), 100)
},
clear: function () {
this.RESULTS = []
this.CURRENT = null
this.CACHE = ''
this.container.innerHTML = ''
},
hide: function () {
this.clear()
this.container.style.display = 'none'
this.input.value = ''
},
setChoice: function (choice) {
choice = choice || this.RESULTS[this.CURRENT]
if (choice) {
this.input.value = choice.item.label
this.options.on_select(choice)
this.displaySelected(choice)
this.hide()
if (this.options.callback) {
L.Util.bind(this.options.callback, this)(choice)
}
}
},
search: async function () {
let val = this.input.value
if (val.length < this.options.minChar) {
this.clear()
return
}
if (`${val}` === `${this.CACHE}`) return
else this.CACHE = val
val = val.toLowerCase()
const [{ data }, response] = await this.server.get(
`/agnocomplete/AutocompleteUser/?q=${encodeURIComponent(val)}`
)
this.handleResults(data)
},
createResult: function (item) {
const el = L.DomUtil.element({
tagName: 'li',
parent: this.container,
textContent: item.label,
})
const result = {
item: item,
el: el,
}
L.DomEvent.on(
el,
'mouseover',
function () {
this.CURRENT = result
this.highlight()
},
this
)
L.DomEvent.on(
el,
'mousedown',
function () {
this.setChoice()
},
this
)
return result
},
resultToIndex: function (result) {
let out = null
this.forEach(this.RESULTS, (item, index) => {
if (item.item.value == result.item.value) {
out = index
return
}
})
return out
},
handleResults: function (data) {
this.clear()
this.container.style.display = 'block'
this.resizeContainer()
this.forEach(data, (item) => {
this.RESULTS.push(this.createResult(item))
})
this.CURRENT = 0
this.highlight()
//TODO manage no results
},
highlight: function () {
this.forEach(this.RESULTS, (result, index) => {
if (index === this.CURRENT) L.DomUtil.addClass(result.el, 'on')
else L.DomUtil.removeClass(result.el, 'on')
})
},
getLeft: function (el) {
let tmp = el.offsetLeft
el = el.offsetParent
while (el) {
tmp += el.offsetLeft
el = el.offsetParent
}
return tmp
},
getTop: function (el) {
let tmp = el.offsetTop
el = el.offsetParent
while (el) {
tmp += el.offsetTop
el = el.offsetParent
}
return tmp
},
forEach: function (els, callback) {
Array.prototype.forEach.call(els, callback)
},
})
U.AutoComplete.Ajax = U.AutoComplete.extend({
initialize: function (el, options) {
U.AutoComplete.prototype.initialize.call(this, el, options)
if (!this.el) return this
this.createInput()
this.createContainer()
this.selected_container = this.initSelectedContainer()
},
optionToResult: function (option) {
return {
value: option.value,
label: option.innerHTML,
}
},
})
U.AutoComplete.Ajax.SelectMultiple = U.AutoComplete.Ajax.extend({
initSelectedContainer: function () {
return L.DomUtil.after(
this.input,
L.DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' })
)
},
displaySelected: function (result) {
const result_el = L.DomUtil.element({
tagName: 'li',
parent: this.selected_container,
})
result_el.textContent = result.item.label
const close = L.DomUtil.element({
tagName: 'span',
parent: result_el,
className: 'close',
textContent: '×',
})
L.DomEvent.on(
close,
'click',
function () {
this.selected_container.removeChild(result_el)
this.options.on_unselect(result)
},
this
)
this.hide()
},
})
U.AutoComplete.Ajax.Select = U.AutoComplete.Ajax.extend({
initSelectedContainer: function () {
return L.DomUtil.after(
this.input,
L.DomUtil.element({ tagName: 'div', className: 'umap-singleresult' })
)
},
displaySelected: function (result) {
const result_el = L.DomUtil.element({
tagName: 'div',
parent: this.selected_container,
})
result_el.textContent = result.item.label
const close = L.DomUtil.element({
tagName: 'span',
parent: result_el,
className: 'close',
textContent: '×',
})
this.input.style.display = 'none'
L.DomEvent.on(
close,
'click',
function () {
this.selected_container.innerHTML = ''
this.input.style.display = 'block'
},
this
)
this.hide()
},
})

View file

@ -394,7 +394,7 @@ U.EditControl = L.Control.extend({
enableEditing, enableEditing,
'mouseover', 'mouseover',
function () { function () {
map.ui.tooltip({ map.tooltip.open({
content: map.help.displayLabel('TOGGLE_EDIT'), content: map.help.displayLabel('TOGGLE_EDIT'),
anchor: enableEditing, anchor: enableEditing,
position: 'bottom', position: 'bottom',
@ -693,7 +693,7 @@ const ControlsMixin = {
nameButton, nameButton,
'mouseover', 'mouseover',
function () { function () {
this.ui.tooltip({ this.tooltip.open({
content: L._('Edit the title of the map'), content: L._('Edit the title of the map'),
anchor: nameButton, anchor: nameButton,
position: 'bottom', position: 'bottom',
@ -714,7 +714,7 @@ const ControlsMixin = {
shareStatusButton, shareStatusButton,
'mouseover', 'mouseover',
function () { function () {
this.ui.tooltip({ this.tooltip.open({
content: L._('Update who can see and edit the map'), content: L._('Update who can see and edit the map'),
anchor: shareStatusButton, anchor: shareStatusButton,
position: 'bottom', position: 'bottom',
@ -763,7 +763,7 @@ const ControlsMixin = {
controlEditCancel, controlEditCancel,
'mouseover', 'mouseover',
function () { function () {
this.ui.tooltip({ this.tooltip.open({
content: this.help.displayLabel('CANCEL'), content: this.help.displayLabel('CANCEL'),
anchor: controlEditCancel, anchor: controlEditCancel,
position: 'bottom', position: 'bottom',
@ -784,7 +784,7 @@ const ControlsMixin = {
controlEditDisable, controlEditDisable,
'mouseover', 'mouseover',
function () { function () {
this.ui.tooltip({ this.tooltip.open({
content: this.help.displayLabel('PREVIEW'), content: this.help.displayLabel('PREVIEW'),
anchor: controlEditDisable, anchor: controlEditDisable,
position: 'bottom', position: 'bottom',
@ -805,7 +805,7 @@ const ControlsMixin = {
controlEditSave, controlEditSave,
'mouseover', 'mouseover',
function () { function () {
this.ui.tooltip({ this.tooltip.open({
content: this.help.displayLabel('SAVE'), content: this.help.displayLabel('SAVE'),
anchor: controlEditSave, anchor: controlEditSave,
position: 'bottom', position: 'bottom',
@ -1048,7 +1048,6 @@ U.Locate = L.Control.Locate.extend({
if (!this._container || !this._container.parentNode) return if (!this._container || !this._container.parentNode) return
return L.Control.Locate.prototype.remove.call(this) return L.Control.Locate.prototype.remove.call(this)
}, },
}) })
U.Search = L.PhotonSearch.extend({ U.Search = L.PhotonSearch.extend({
@ -1087,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.ui.alert({ content: 'Invalid latitude or longitude', mode: 'error' }) this.map.alert.open({ content: 'Invalid latitude or longitude', mode: 'error' })
} }
return return
} }
@ -1250,7 +1249,7 @@ U.Editable = L.Editable.extend({
L.Editable.prototype.initialize.call(this, map, options) L.Editable.prototype.initialize.call(this, map, options)
this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip) this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip)
this.on('editable:drawing:end', (e) => { this.on('editable:drawing:end', (e) => {
this.closeTooltip() this.map.tooltip.close()
// Leaflet.Editable will delete the drawn shape if invalid // Leaflet.Editable will delete the drawn shape if invalid
// (eg. line has only one drawn point) // (eg. line has only one drawn point)
// So let's check if the layer has no more shape // So let's check if the layer has no more shape
@ -1314,7 +1313,7 @@ U.Editable = L.Editable.extend({
drawingTooltip: function (e) { drawingTooltip: function (e) {
if (e.layer instanceof L.Marker && e.type == 'editable:drawing:start') { if (e.layer instanceof L.Marker && e.type == 'editable:drawing:start') {
this.map.ui.tooltip({ content: L._('Click to add a marker') }) this.map.tooltip.open({ content: L._('Click to add a marker') })
} }
if (!(e.layer instanceof L.Polyline)) { if (!(e.layer instanceof L.Polyline)) {
// only continue with Polylines and Polygons // only continue with Polylines and Polygons
@ -1357,7 +1356,7 @@ U.Editable = L.Editable.extend({
} }
} }
if (content) { if (content) {
this.map.ui.tooltip({ content: content }) this.map.tooltip.open({ content: content })
} }
}, },

View file

@ -346,22 +346,6 @@ U.Help = L.Class.extend({
initialize: function (map) { initialize: function (map) {
this.map = map this.map = map
this.box = L.DomUtil.create(
'div',
'umap-help-box with-transition dark',
document.body
)
const closeButton = L.DomUtil.createButton(
'umap-close-link',
this.box,
'',
this.hide,
this
)
L.DomUtil.add('i', 'umap-close-icon', closeButton)
const label = L.DomUtil.create('span', '', closeButton)
label.title = label.textContent = L._('Close')
this.content = L.DomUtil.create('div', 'umap-help-content', this.box)
this.isMacOS = /mac/i.test( this.isMacOS = /mac/i.test(
// eslint-disable-next-line compat/compat -- Fallback available. // eslint-disable-next-line compat/compat -- Fallback available.
navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform
@ -377,20 +361,12 @@ U.Help = L.Class.extend({
}, },
show: function () { show: function () {
this.content.innerHTML = '' const container = L.DomUtil.add('div')
for (let i = 0, name; i < arguments.length; i++) { for (let i = 0, name; i < arguments.length; i++) {
name = arguments[i] name = arguments[i]
L.DomUtil.add('div', 'umap-help-entry', this.content, this.resolve(name)) L.DomUtil.add('div', 'umap-help-entry', container, this.resolve(name))
} }
L.DomUtil.addClass(document.body, 'umap-help-on') this.map.dialog.open({ content: container, className: 'dark' })
},
hide: function () {
L.DomUtil.removeClass(document.body, 'umap-help-on')
},
visible: function () {
return L.DomUtil.hasClass(document.body, 'umap-help-on')
}, },
resolve: function (name) { resolve: function (name) {
@ -424,16 +400,15 @@ U.Help = L.Class.extend({
}, },
edit: function () { edit: function () {
const container = L.DomUtil.create('div', ''), const container = L.DomUtil.create('div', '')
self = this, const title = L.DomUtil.create('h3', '', container)
title = L.DomUtil.create('h3', '', container), const actionsContainer = L.DomUtil.create('ul', 'umap-edit-actions', container)
actionsContainer = L.DomUtil.create('ul', 'umap-edit-actions', container)
const addAction = (action) => { const addAction = (action) => {
const actionContainer = L.DomUtil.add('li', '', actionsContainer) const actionContainer = L.DomUtil.add('li', '', actionsContainer)
L.DomUtil.add('i', action.options.className, actionContainer), L.DomUtil.add('i', action.options.className, actionContainer),
L.DomUtil.add('span', '', actionContainer, action.options.tooltip) L.DomUtil.add('span', '', actionContainer, action.options.tooltip)
L.DomEvent.on(actionContainer, 'click', action.addHooks, action) L.DomEvent.on(actionContainer, 'click', action.addHooks, action)
L.DomEvent.on(actionContainer, 'click', self.hide, self) L.DomEvent.on(actionContainer, 'click', this.map.dialog.close, this.map.dialog)
} }
title.textContent = L._('Where do we go from here?') title.textContent = L._('Where do we go from here?')
for (const id in this.map.helpMenuActions) { for (const id in this.map.helpMenuActions) {

View file

@ -726,7 +726,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.ui.alert({ this.map.alert.open({
content: L._('Invalid latitude or longitude'), content: L._('Invalid latitude or longitude'),
level: 'error', level: 'error',
}) })
@ -878,9 +878,9 @@ U.PathMixin = {
_onMouseOver: function () { _onMouseOver: function () {
if (this.map.measureTools && this.map.measureTools.enabled()) { if (this.map.measureTools && this.map.measureTools.enabled()) {
this.map.ui.tooltip({ content: this.getMeasure(), anchor: this }) this.map.tooltip.open({ content: this.getMeasure(), anchor: this })
} else if (this.map.editEnabled && !this.map.editedFeature) { } else if (this.map.editEnabled && !this.map.editedFeature) {
this.map.ui.tooltip({ content: L._('Click to edit'), anchor: this }) this.map.tooltip.open({ content: L._('Click to edit'), anchor: this })
} }
}, },
@ -928,7 +928,7 @@ U.PathMixin = {
items.push({ items.push({
text: L._('Display measure'), text: L._('Display measure'),
callback: function () { callback: function () {
this.map.ui.alert({ content: this.getMeasure(), level: 'info' }) this.map.alert.open({ content: this.getMeasure(), level: 'info' })
}, },
context: this, context: this,
}) })

View file

@ -78,7 +78,7 @@ L.FormBuilder.Element.include({
info, info,
'mouseover', 'mouseover',
function () { function () {
this.builder.map.ui.tooltip({ this.builder.map.tooltip.open({
anchor: info, anchor: info,
content: this.options.helpTooltip, content: this.options.helpTooltip,
position: 'top', position: 'top',
@ -1067,7 +1067,7 @@ L.FormBuilder.ManageOwner = L.FormBuilder.Element.extend({
className: 'edit-owner', className: 'edit-owner',
on_select: L.bind(this.onSelect, this), on_select: L.bind(this.onSelect, this),
} }
this.autocomplete = new U.AutoComplete.Ajax.Select(this.parentNode, options) this.autocomplete = new U.AjaxAutocomplete(this.parentNode, options)
const owner = this.toHTML() const owner = this.toHTML()
if (owner) if (owner)
this.autocomplete.displaySelected({ this.autocomplete.displaySelected({
@ -1096,7 +1096,7 @@ L.FormBuilder.ManageEditors = L.FormBuilder.Element.extend({
on_select: L.bind(this.onSelect, this), on_select: L.bind(this.onSelect, this),
on_unselect: L.bind(this.onUnselect, this), on_unselect: L.bind(this.onUnselect, this),
} }
this.autocomplete = new U.AutoComplete.Ajax.SelectMultiple(this.parentNode, options) this.autocomplete = new U.AjaxAutocompleteMultiple(this.parentNode, options)
this._values = this.toHTML() this._values = this.toHTML()
if (this._values) if (this._values)
for (let i = 0; i < this._values.length; i++) for (let i = 0; i < this._values.length; i++)

View file

@ -57,15 +57,17 @@ 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.dialog = new U.Dialog(this._controlContainer)
if (this.hasEditMode()) { if (this.hasEditMode()) {
this.editPanel = new U.EditPanel(this) this.editPanel = new U.EditPanel(this)
this.fullPanel = new U.FullPanel(this) this.fullPanel = new U.FullPanel(this)
} }
this.ui = new U.UI(this._container) L.DomEvent.on(document.body, 'dataloading', (e) => this.fire('dataloading', e))
this.ui.on('dataloading', (e) => this.fire('dataloading', e)) L.DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e))
this.ui.on('dataload', (e) => this.fire('dataload', e)) this.server = new U.ServerRequest(this.alert)
this.server = new U.ServerRequest(this.ui) this.request = new U.Request(this.alert)
this.request = new U.Request(this.ui)
this.initLoader() this.initLoader()
this.name = this.options.name this.name = this.options.name
@ -359,7 +361,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.ui.alert({ content: err.message }), onLocationError: (err) => this.alert.open({ content: 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') },
@ -392,7 +394,9 @@ U.Map = L.Map.extend({
}, },
renderControls: function () { renderControls: function () {
const hasSlideshow = Boolean(this.options.slideshow && this.options.slideshow.active) const hasSlideshow = Boolean(
this.options.slideshow && this.options.slideshow.active
)
const barEnabled = this.options.captionBar || hasSlideshow const barEnabled = this.options.captionBar || hasSlideshow
document.body.classList.toggle('umap-caption-bar-enabled', barEnabled) document.body.classList.toggle('umap-caption-bar-enabled', barEnabled)
document.body.classList.toggle('umap-slideshow-enabled', hasSlideshow) document.body.classList.toggle('umap-slideshow-enabled', hasSlideshow)
@ -522,8 +526,8 @@ U.Map = L.Map.extend({
L.DomEvent.stop(e) L.DomEvent.stop(e)
this.search() this.search()
} else if (e.keyCode === U.Keys.ESC) { } else if (e.keyCode === U.Keys.ESC) {
if (this.help.visible()) { if (this.dialog.visible) {
this.help.hide() this.dialog.close()
} else { } else {
this.panel.close() this.panel.close()
this.editPanel?.close() this.editPanel?.close()
@ -641,7 +645,7 @@ U.Map = L.Map.extend({
} catch (e) { } catch (e) {
console.error(e) console.error(e)
this.removeLayer(tilelayer) this.removeLayer(tilelayer)
this.ui.alert({ this.alert.open({
content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`, content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`,
level: 'error', level: 'error',
}) })
@ -676,7 +680,7 @@ U.Map = L.Map.extend({
} catch (e) { } catch (e) {
this.removeLayer(overlay) this.removeLayer(overlay)
console.error(e) console.error(e)
this.ui.alert({ this.alert.open({
content: `${L._('Error in the overlay URL')}: ${overlay._url}`, content: `${L._('Error in the overlay URL')}: ${overlay._url}`,
level: 'error', level: 'error',
}) })
@ -799,7 +803,7 @@ U.Map = L.Map.extend({
if (this.options.umap_id) { if (this.options.umap_id) {
// We do not want an extra message during the map creation // We do not want an extra message during the map creation
// to avoid the double notification/alert. // to avoid the double notification/alert.
this.ui.alert({ this.alert.open({
content: L._('The zoom and center have been modified.'), content: L._('The zoom and center have been modified.'),
level: 'info', level: 'info',
}) })
@ -842,7 +846,7 @@ 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.ui.alert({ this.alert.open({
content: L._('Unable to detect format of file {filename}', { content: L._('Unable to detect format of file {filename}', {
filename: file.name, filename: file.name,
}), }),
@ -899,7 +903,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.ui.alert({ self.alert.open({
content: L._('Invalid umap data in {filename}', { filename: file.name }), content: L._('Invalid umap data in {filename}', { filename: file.name }),
level: 'error', level: 'error',
}) })
@ -1030,7 +1034,7 @@ U.Map = L.Map.extend({
label: L._('Copy link'), label: L._('Copy link'),
callback: () => { callback: () => {
L.Util.copyToClipboard(data.permissions.anonymous_edit_url) L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
this.ui.alert({ this.alert.open({
content: L._('Secret edit link copied to clipboard!'), content: L._('Secret edit link copied to clipboard!'),
level: 'info', level: 'info',
}) })
@ -1058,7 +1062,7 @@ U.Map = L.Map.extend({
history.pushState({}, this.options.name, data.url) history.pushState({}, this.options.name, data.url)
else window.location = data.url else window.location = data.url
alert.content = data.info || alert.content alert.content = data.info || alert.content
this.once('saved', () => this.ui.alert(alert)) this.once('saved', () => this.alert.open(alert))
this.permissions.save() this.permissions.save()
} }
}, },
@ -1079,7 +1083,7 @@ U.Map = L.Map.extend({
}, },
sendEditLink: async function () { sendEditLink: async function () {
const input = this.ui._alert.querySelector('input') const input = this.alert.container.querySelector('input')
const email = input.value const email = input.value
const formData = new FormData() const formData = new FormData()
@ -1091,7 +1095,7 @@ U.Map = L.Map.extend({
star: async function () { star: async function () {
if (!this.options.umap_id) if (!this.options.umap_id)
return this.ui.alert({ return this.alert.open({
content: L._('Please save the map first'), content: L._('Please save the map first'),
level: 'error', level: 'error',
}) })
@ -1102,7 +1106,7 @@ U.Map = L.Map.extend({
let msg = data.starred let msg = data.starred
? L._('Map has been starred') ? L._('Map has been starred')
: L._('Map has been unstarred') : L._('Map has been unstarred')
this.ui.alert({ content: msg, level: 'info' }) this.alert.open({ content: msg, level: 'info' })
this.renderControls() this.renderControls()
} }
}, },

View file

@ -965,7 +965,7 @@ U.DataLayer = L.Evented.extend({
message: err[0].message, message: err[0].message,
}) })
} }
this.map.ui.alert({ content: message, level: 'error', duration: 10000 }) this.map.alert.open({ content: message, level: 'error', duration: 10000 })
console.error(err) console.error(err)
} }
if (result && result.features.length) { if (result && result.features.length) {
@ -992,7 +992,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.ui.alert({ content: `Invalid JSON file: ${err}` }) this.map.alert.open({ content: `Invalid JSON file: ${err}` })
return return
} }
} }
@ -1050,7 +1050,7 @@ U.DataLayer = L.Evented.extend({
return this.geojsonToFeatures(geometry.geometries) return this.geojsonToFeatures(geometry.geometries)
default: default:
this.map.ui.alert({ this.map.alert.open({
content: L._('Skipping unknown geometry.type: {type}', { content: L._('Skipping unknown geometry.type: {type}', {
type: geometry.type || 'undefined', type: geometry.type || 'undefined',
}), }),
@ -1641,7 +1641,7 @@ U.DataLayer = L.Evented.extend({
label: L._('Cancel'), label: L._('Cancel'),
}, },
] ]
this.map.ui.alert({ this.map.alert.open({
content: msg, content: msg,
level: 'error', level: 'error',
duration: 100000, duration: 100000,

View file

@ -53,7 +53,7 @@ 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.ui.alert({ return this.map.alert.open({
content: L._('Please save the map first'), content: L._('Please save the map first'),
level: 'info', level: 'info',
}) })
@ -139,7 +139,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.ui.alert({ this.map.alert.open({
content: L._('Map has been attached to your account'), content: L._('Map has been attached to your account'),
level: 'info', level: 'info',
}) })

View file

@ -83,7 +83,7 @@ U.TableEditor = L.Class.extend({
validateName: function (name) { validateName: function (name) {
if (name.indexOf('.') !== -1) { if (name.indexOf('.') !== -1) {
this.datalayer.map.ui.alert({ this.datalayer.map.alert.open({
content: L._('Invalide property name: {name}', { name: name }), content: L._('Invalide property name: {name}', { name: name }),
level: 'error', level: 'error',
}) })

View file

@ -1,190 +0,0 @@
/*
* Modals
*/
U.UI = L.Evented.extend({
ALERTS: Array(),
ALERT_ID: null,
TOOLTIP_ID: null,
initialize: function (parent) {
this.parent = parent
this.container = L.DomUtil.create('div', 'leaflet-ui-container', this.parent)
L.DomEvent.disableClickPropagation(this.container)
L.DomEvent.on(this.container, 'contextmenu', L.DomEvent.stopPropagation) // Do not activate our custom context menu.
L.DomEvent.on(this.container, 'wheel', L.DomEvent.stopPropagation)
L.DomEvent.on(this.container, 'MozMousePixelScroll', L.DomEvent.stopPropagation)
this._alert = L.DomUtil.create('div', 'with-transition', this.container)
this._alert.id = 'umap-alert-container'
this._tooltip = L.DomUtil.create('div', '', this.container)
this._tooltip.id = 'umap-tooltip-container'
},
alert: function (e) {
if (L.DomUtil.hasClass(this.parent, 'umap-alert')) this.ALERTS.push(e)
else this.popAlert(e)
},
popAlert: function (e) {
if (!e) {
if (this.ALERTS.length) e = this.ALERTS.pop()
else return
}
let timeoutID
const level_class = e.level && e.level == 'info' ? 'info' : 'error'
this._alert.innerHTML = ''
L.DomUtil.addClass(this.parent, 'umap-alert')
L.DomUtil.addClass(this._alert, level_class)
const close = () => {
if (timeoutID && timeoutID !== this.ALERT_ID) {
return
} // Another alert has been forced
this._alert.innerHTML = ''
L.DomUtil.removeClass(this.parent, 'umap-alert')
L.DomUtil.removeClass(this._alert, level_class)
if (timeoutID) window.clearTimeout(timeoutID)
this.popAlert()
}
const closeButton = L.DomUtil.createButton(
'umap-close-link',
this._alert,
'',
close,
this
)
L.DomUtil.create('i', 'umap-close-icon', closeButton)
const label = L.DomUtil.create('span', '', closeButton)
label.title = label.textContent = L._('Close')
L.DomUtil.element({ tagName: 'div', innerHTML: e.content, parent: this._alert })
if (e.actions) {
let action, el, input
const form = L.DomUtil.create('div', 'umap-alert-actions', this._alert)
for (let i = 0; i < e.actions.length; i++) {
action = e.actions[i]
if (action.input) {
input = L.DomUtil.element({
tagName: 'input',
parent: form,
className: 'umap-alert-input',
placeholder: action.input,
})
}
el = L.DomUtil.createButton(
'umap-action',
form,
action.label,
action.callback,
action.callbackContext || this.map
)
L.DomEvent.on(el, 'click', close, this)
}
}
if (e.duration !== Infinity) {
this.ALERT_ID = timeoutID = window.setTimeout(
L.bind(close, this),
e.duration || 3000
)
}
},
tooltip: function (opts) {
function showIt() {
if (opts.anchor && opts.position === 'top') {
this.anchorTooltipTop(opts.anchor)
} else if (opts.anchor && opts.position === 'left') {
this.anchorTooltipLeft(opts.anchor)
} else if (opts.anchor && opts.position === 'bottom') {
this.anchorTooltipBottom(opts.anchor)
} else {
this.anchorTooltipAbsolute()
}
L.DomUtil.addClass(this.parent, 'umap-tooltip')
this._tooltip.innerHTML = U.Utils.escapeHTML(opts.content)
}
this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0)
const id = this.TOOLTIP_ID
function closeIt() {
this.closeTooltip(id)
}
if (opts.anchor) {
L.DomEvent.once(opts.anchor, 'mouseout', closeIt, this)
}
if (opts.duration !== Infinity) {
window.setTimeout(L.bind(closeIt, this), opts.duration || 3000)
}
},
anchorTooltipAbsolute: function () {
this._tooltip.className = ''
const left =
this.parent.offsetLeft +
this.parent.clientWidth / 2 -
this._tooltip.clientWidth / 2,
top = this.parent.offsetTop + 75
this.setTooltipPosition({ top: top, left: left })
},
anchorTooltipTop: function (el) {
this._tooltip.className = 'tooltip-top'
const coords = this.getPosition(el)
this.setTooltipPosition({
left: coords.left - 10,
bottom: this.getDocHeight() - coords.top + 11,
})
},
anchorTooltipBottom: function (el) {
this._tooltip.className = 'tooltip-bottom'
const coords = this.getPosition(el)
this.setTooltipPosition({
left: coords.left,
top: coords.bottom + 11,
})
},
anchorTooltipLeft: function (el) {
this._tooltip.className = 'tooltip-left'
const coords = this.getPosition(el)
this.setTooltipPosition({
top: coords.top,
right: document.documentElement.offsetWidth - coords.left + 11,
})
},
closeTooltip: function (id) {
// Clear timetout even if a new tooltip has been added
// in the meantime. Eg. after a mouseout from the anchor.
window.clearTimeout(id)
if (id && id !== this.TOOLTIP_ID) return
this._tooltip.className = ''
this._tooltip.innerHTML = ''
this.setTooltipPosition({})
L.DomUtil.removeClass(this.parent, 'umap-tooltip')
},
getPosition: function (el) {
return el.getBoundingClientRect()
},
setTooltipPosition: function (coords) {
if (coords.left) this._tooltip.style.left = `${coords.left}px`
else this._tooltip.style.left = 'initial'
if (coords.right) this._tooltip.style.right = `${coords.right}px`
else this._tooltip.style.right = 'initial'
if (coords.top) this._tooltip.style.top = `${coords.top}px`
else this._tooltip.style.top = 'initial'
if (coords.bottom) this._tooltip.style.bottom = `${coords.bottom}px`
else this._tooltip.style.bottom = 'initial'
},
getDocHeight: function () {
const D = document
return Math.max(
D.body.scrollHeight,
D.documentElement.scrollHeight,
D.body.offsetHeight,
D.documentElement.offsetHeight,
D.body.clientHeight,
D.documentElement.clientHeight
)
},
})

View file

@ -17,6 +17,24 @@
} }
/* *********** */
/* Structure */
/* *********** */
.umap-edit-enabled {
--current-header-height: var(--header-height);
}
.umap-caption-bar-enabled {
--current-footer-height: var(--footer-height);
}
.leaflet-top {
top: var(--current-header-height);
}
.leaflet-bottom {
bottom: var(--current-footer-height);
}
/* *********** */ /* *********** */
/* Controls */ /* Controls */
/* *********** */ /* *********** */
@ -99,10 +117,10 @@
box-shadow: 0 0 4px 0 black inset; box-shadow: 0 0 4px 0 black inset;
} }
.leaflet-control-star [type="button"] { .leaflet-control-star [type="button"] {
background-position: -108px -144px; background-position: -144px -144px;
} }
.leaflet-control-star.starred [type="button"] { .leaflet-control-star.starred [type="button"] {
background-position: -144px -144px; background-position: -108px -144px;
} }
.leaflet-control-search [type="button"] { .leaflet-control-search [type="button"] {
background-position: -36px -108px; background-position: -36px -108px;
@ -390,24 +408,6 @@ ul.photon-autocomplete {
/* ********************************* */ /* ********************************* */
/* Help Lightbox */ /* Help Lightbox */
/* ********************************* */ /* ********************************* */
.umap-help-box {
z-index: 10001;
position: absolute;
margin: 0 calc(50% - 500px/2);
width: 500px;
max-width: 100vw;
padding: 40px 20px;
border: 1px solid #222;
background-color: var(--color-darkGray);
color: #efefef;
font-size: 0.8em;
visibility: hidden;
top: -100%;
}
.umap-help-box .umap-close-link {
float: right;
width: 100px;
}
.umap-help-button { .umap-help-button {
display: inline-block; display: inline-block;
width: 16px; width: 16px;
@ -426,10 +426,6 @@ ul.photon-autocomplete {
.dark .umap-help-button { .dark .umap-help-button {
background-image: url('./img/16-white.svg'); background-image: url('./img/16-white.svg');
} }
.umap-help-on .umap-help-box {
visibility: visible;
top: 100px;
}
.umap-help-entry + .umap-help-entry { .umap-help-entry + .umap-help-entry {
margin-top: 10px; margin-top: 10px;
border-top: 1px solid #aaa; border-top: 1px solid #aaa;
@ -639,9 +635,6 @@ ul.photon-autocomplete {
.umap-edit-enabled .umap-main-edit-toolbox { .umap-edit-enabled .umap-main-edit-toolbox {
top: 0; top: 0;
} }
.umap-edit-enabled .umap-caption-bar {
display: none;
}
.umap-caption-bar h3, .umap-caption-bar h3,
.umap-main-edit-toolbox h3 { .umap-main-edit-toolbox h3 {
display: inline; display: inline;
@ -664,12 +657,9 @@ ul.photon-autocomplete {
padding-left: 20px; padding-left: 20px;
display: inline-block; /* Prevents underline on hover. */ display: inline-block; /* Prevents underline on hover. */
} }
.umap-edit-enabled .leaflet-top {
top: 46px;
}
.umap-caption-bar-enabled .umap-caption-bar { .umap-caption-bar-enabled .umap-caption-bar {
display: block; display: block;
height: 46px; height: var(--header-height);
background-color: #fff; background-color: #fff;
width: 100%; width: 100%;
position: absolute; position: absolute;
@ -678,15 +668,12 @@ ul.photon-autocomplete {
right: 0; right: 0;
padding: 0 0 0 5px; padding: 0 0 0 5px;
text-align: left; text-align: left;
line-height: 46px; line-height: 100%;
cursor: auto; cursor: auto;
border-top: 1px solid var(--color-lightGray); border-top: 1px solid var(--color-lightGray);
opacity: 0.93; opacity: 0.93;
z-index: 1000; z-index: 1000;
} }
.umap-caption-bar-enabled .leaflet-bottom {
bottom: 46px;
}
.umap-help { .umap-help {
font-style: italic; font-style: italic;
} }

View file

@ -192,6 +192,24 @@ describe('Utils', function () {
) )
}) })
it('should not escape video tag with dedicated attributes', function () {
assert.equal(
Utils.escapeHTML(
'<video width="100%" height="281" controls><source type="video/mp4" src="movie.mp4"></video>'
),
'<video controls="" height="281" width="100%"><source src="movie.mp4" type="video/mp4"></video>'
)
})
it('should not escape audio tag with dedicated attributes', function () {
assert.equal(
Utils.escapeHTML(
'<audio controls><source type="audio/ogg" src="horse.ogg"></audio>'
),
'<audio controls=""><source src="horse.ogg" type="audio/ogg"></audio>'
)
})
it('should not fail with int value', function () { it('should not fail with int value', function () {
assert.equal(Utils.escapeHTML(25), '25') assert.equal(Utils.escapeHTML(25), '25')
}) })
@ -461,8 +479,7 @@ describe('Utils', function () {
}) })
describe('#normalize()', function () { describe('#normalize()', function () {
it('should remove accents', it('should remove accents', function () {
function () {
// French é // French é
assert.equal(Utils.normalize('aéroport'), 'aeroport') assert.equal(Utils.normalize('aéroport'), 'aeroport')
// American é // American é
@ -530,17 +547,17 @@ describe('Utils', function () {
}) })
}) })
describe("#copyJSON", function () { describe('#copyJSON', function () {
it('should actually copy the JSON', function () { it('should actually copy the JSON', function () {
let originalJSON = { "some": "json" } let originalJSON = { some: 'json' }
let returned = Utils.CopyJSON(originalJSON) let returned = Utils.CopyJSON(originalJSON)
// Change the original JSON // Change the original JSON
originalJSON["anotherKey"] = "value" originalJSON['anotherKey'] = 'value'
// ensure the two aren't the same object // ensure the two aren't the same object
assert.notEqual(returned, originalJSON) assert.notEqual(returned, originalJSON)
assert.deepEqual(returned, { "some": "json" }) assert.deepEqual(returned, { some: 'json' })
}) })
}) })
@ -599,19 +616,34 @@ describe('Utils', function () {
}) })
describe('parseNaiveDate', () => { describe('parseNaiveDate', () => {
it('should parse a date', () => { it('should parse a date', () => {
assert.equal(Utils.parseNaiveDate("2024/03/04").toISOString(), "2024-03-04T00:00:00.000Z") assert.equal(
Utils.parseNaiveDate('2024/03/04').toISOString(),
'2024-03-04T00:00:00.000Z'
)
}) })
it('should parse a datetime', () => { it('should parse a datetime', () => {
assert.equal(Utils.parseNaiveDate("2024/03/04 12:13:14").toISOString(), "2024-03-04T00:00:00.000Z") assert.equal(
Utils.parseNaiveDate('2024/03/04 12:13:14').toISOString(),
'2024-03-04T00:00:00.000Z'
)
}) })
it('should parse an iso datetime', () => { it('should parse an iso datetime', () => {
assert.equal(Utils.parseNaiveDate("2024-03-04T00:00:00.000Z").toISOString(), "2024-03-04T00:00:00.000Z") assert.equal(
Utils.parseNaiveDate('2024-03-04T00:00:00.000Z').toISOString(),
'2024-03-04T00:00:00.000Z'
)
}) })
it('should parse a GMT time', () => { it('should parse a GMT time', () => {
assert.equal(Utils.parseNaiveDate("04 Mar 2024 00:12:00 GMT").toISOString(), "2024-03-04T00:00:00.000Z") assert.equal(
Utils.parseNaiveDate('04 Mar 2024 00:12:00 GMT').toISOString(),
'2024-03-04T00:00:00.000Z'
)
}) })
it('should parse a GMT time with explicit timezone', () => { it('should parse a GMT time with explicit timezone', () => {
assert.equal(Utils.parseNaiveDate("Thu, 04 Mar 2024 00:00:00 GMT+0300").toISOString(), "2024-03-03T00:00:00.000Z") assert.equal(
Utils.parseNaiveDate('Thu, 04 Mar 2024 00:00:00 GMT+0300').toISOString(),
'2024-03-03T00:00:00.000Z'
)
}) })
}) })
}) })

View file

@ -11,6 +11,7 @@
--background-color: var(--color-light); --background-color: var(--color-light);
--color-accent: var(--color-brightCyan); --color-accent: var(--color-brightCyan);
--text-color: black;
/* Buttons. */ /* Buttons. */
--button-primary-background: var(--color-waterMint); --button-primary-background: var(--color-waterMint);
@ -24,9 +25,12 @@
--panel-header-height: 36px; --panel-header-height: 36px;
--panel-width: 400px; --panel-width: 400px;
--header-height: 46px; --header-height: 46px;
--current-header-height: 0px;
--footer-height: 46px; --footer-height: 46px;
--current-footer-height: 0px;
--control-size: 36px; --control-size: 36px;
} }
.dark { .dark {
--background-color: var(--color-darkGray); --background-color: var(--color-darkGray);
--text-color: #efefef;
} }

View file

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

View file

@ -29,4 +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/tooltip.css' %}" />
<link rel="stylesheet" href="{% static 'umap/css/dialog.css' %}" />
<link rel="stylesheet" href="{% static 'umap/theme.css' %}" /> <link rel="stylesheet" href="{% static 'umap/theme.css' %}" />

View file

@ -46,7 +46,6 @@
<script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}" <script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}"
defer></script> defer></script>
<script src="{% static 'umap/js/umap.core.js' %}" defer></script> <script src="{% static 'umap/js/umap.core.js' %}" defer></script>
<script src="{% static 'umap/js/umap.autocomplete.js' %}" defer></script>
<script src="{% static 'umap/js/umap.popup.js' %}" defer></script> <script src="{% static 'umap/js/umap.popup.js' %}" defer></script>
<script src="{% static 'umap/js/umap.forms.js' %}" defer></script> <script src="{% static 'umap/js/umap.forms.js' %}" defer></script>
<script src="{% static 'umap/js/umap.icon.js' %}" defer></script> <script src="{% static 'umap/js/umap.icon.js' %}" defer></script>
@ -57,8 +56,6 @@
<script src="{% static 'umap/js/umap.controls.js' %}" defer></script> <script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
<script src="{% static 'umap/js/umap.slideshow.js' %}" defer></script> <script src="{% static 'umap/js/umap.slideshow.js' %}" defer></script>
<script src="{% static 'umap/js/umap.tableeditor.js' %}" defer></script> <script src="{% static 'umap/js/umap.tableeditor.js' %}" defer></script>
<script src="{% static 'umap/js/umap.importer.js' %}" defer></script>
<script src="{% static 'umap/js/umap.share.js' %}" defer></script> <script src="{% static 'umap/js/umap.share.js' %}" defer></script>
<script src="{% static 'umap/js/umap.js' %}" defer></script> <script src="{% static 'umap/js/umap.js' %}" defer></script>
<script src="{% static 'umap/js/umap.ui.js' %}" defer></script>
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script> <script src="{% static 'umap/js/components/fragment.js' %}" defer></script>

View file

@ -6,7 +6,7 @@
U.MAP = new U.Map("map", {{ map_settings|notag|safe }}) U.MAP = new U.Map("map", {{ map_settings|notag|safe }})
{% for m in messages %} {% for m in messages %}
{# We have just one, but we need to loop, as for messages API #} {# We have just one, but we need to loop, as for messages API #}
U.MAP.ui.alert({ U.MAP.alert.open({
content: "{{ m }}", content: "{{ m }}",
level: "{{ m.tags }}", level: "{{ m.tags }}",
duration: 100000 duration: 100000

View file

@ -164,7 +164,7 @@ def test_alert_message_after_create(
page.goto(f"{live_server.url}/en/map/new") page.goto(f"{live_server.url}/en/map/new")
save = page.get_by_role("button", name="Save") save = page.get_by_role("button", name="Save")
expect(save).to_be_visible() expect(save).to_be_visible()
alert = page.locator(".umap-alert") alert = page.locator("#umap-alert-container")
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,7 +194,7 @@ 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") alert = page.locator("#umap-alert-container")
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.get_by_placeholder("Email").fill("foo@bar.com")
@ -214,7 +214,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") alert = page.locator("#umap-alert-container")
expect(alert).to_be_visible() expect(alert).to_be_visible()
expect( expect(
alert.get_by_text( alert.get_by_text(

View file

@ -28,7 +28,7 @@ def test_owner_can_delete_map_after_confirmation(map, live_server, login):
def test_dashboard_map_preview(map, live_server, datalayer, login): def test_dashboard_map_preview(map, live_server, datalayer, login):
page = login(map.owner) page = login(map.owner)
page.goto(f"{live_server.url}/en/me") page.goto(f"{live_server.url}/en/me")
dialog = page.locator("dialog") dialog = page.get_by_role("dialog")
expect(dialog).to_be_hidden() expect(dialog).to_be_hidden()
button = page.get_by_role("button", name="Open preview") button = page.get_by_role("button", name="Open preview")
expect(button).to_be_visible() expect(button).to_be_visible()

View file

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