From 186025e0f066703f27bf8b85cf87c66cdeb516a3 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 1 Jul 2024 18:54:06 +0200 Subject: [PATCH 1/4] chore: add custom prompt --- umap/static/umap/css/dialog.css | 2 + umap/static/umap/js/modules/global.js | 3 +- umap/static/umap/js/modules/help.js | 9 +++-- umap/static/umap/js/modules/importer.js | 4 +- umap/static/umap/js/modules/ui/dialog.js | 47 ++++++++++++++++++++++-- umap/static/umap/js/umap.js | 9 ++--- umap/static/umap/js/umap.tableeditor.js | 21 +++++++---- 7 files changed, 71 insertions(+), 24 deletions(-) diff --git a/umap/static/umap/css/dialog.css b/umap/static/umap/css/dialog.css index 9fb2c67e..43072f27 100644 --- a/umap/static/umap/css/dialog.css +++ b/umap/static/umap/css/dialog.css @@ -11,6 +11,8 @@ color: var(--text-color); border-radius: 5px; overflow-y: auto; + height: fit-content; + max-height: 90vh; } .umap-dialog .umap-close-link { float: right; diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index c4eebe03..366bed6b 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -14,7 +14,7 @@ import { HTTPError, NOKError, Request, RequestError, ServerRequest } from './req import Rules from './rules.js' import { SCHEMA } from './schema.js' import { SyncEngine } from './sync/engine.js' -import Dialog from './ui/dialog.js' +import { Dialog, Prompt } from './ui/dialog.js' import { EditPanel, FullPanel, Panel } from './ui/panel.js' import Tooltip from './ui/tooltip.js' import URLs from './urls.js' @@ -42,6 +42,7 @@ window.U = { NOKError, Orderable, Panel, + Prompt, Request, RequestError, Rules, diff --git a/umap/static/umap/js/modules/help.js b/umap/static/umap/js/modules/help.js index fe0a8460..0d50e9cd 100644 --- a/umap/static/umap/js/modules/help.js +++ b/umap/static/umap/js/modules/help.js @@ -165,6 +165,7 @@ const ENTRIES = { export default class Help { constructor(map) { this.map = map + this.dialog = new U.Dialog() this.isMacOS = /mac/i.test( // eslint-disable-next-line compat/compat -- Fallback available. navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform @@ -207,7 +208,7 @@ export default class Help { }) } } - this.map.dialog.open({ content: container, className: 'dark' }) + this.dialog.open({ content: container, className: 'dark' }) } button(container, entries, classname) { @@ -241,10 +242,10 @@ export default class Help { const actionsContainer = DomUtil.create('ul', 'umap-edit-actions', container) const addAction = (action) => { const actionContainer = DomUtil.add('li', '', actionsContainer) - DomUtil.add('i', action.options.className, actionContainer), - DomUtil.add('span', '', actionContainer, action.options.tooltip) + DomUtil.add('i', action.options.className, actionContainer) + DomUtil.add('span', '', actionContainer, action.options.tooltip) DomEvent.on(actionContainer, 'click', action.addHooks, action) - DomEvent.on(actionContainer, 'click', this.map.dialog.close, this.map.dialog) + DomEvent.on(actionContainer, 'click', this.dialog.close, this.map.dialog) } title.textContent = translate('Where do we go from here?') for (const id in this.map.helpMenuActions) { diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index ba227fd9..4ab13f18 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -2,7 +2,7 @@ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' import { uMapAlert as Alert } from '../components/alerts/alert.js' import { translate } from './i18n.js' import { SCHEMA } from './schema.js' -import Dialog from './ui/dialog.js' +import { Dialog } from './ui/dialog.js' import * as Utils from './utils.js' const TEMPLATE = ` @@ -53,7 +53,7 @@ export default class Importer { this.TYPES = ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap'] this.IMPORTERS = [] this.loadImporters() - this.dialog = new Dialog(this.map._controlContainer) + this.dialog = new Dialog() } loadImporters() { diff --git a/umap/static/umap/js/modules/ui/dialog.js b/umap/static/umap/js/modules/ui/dialog.js index 9cfea1b1..ebc12cb0 100644 --- a/umap/static/umap/js/modules/ui/dialog.js +++ b/umap/static/umap/js/modules/ui/dialog.js @@ -1,11 +1,10 @@ import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js' import { translate } from '../i18n.js' -export default class Dialog { - constructor(parent) { - this.parent = parent +export class Dialog { + constructor() { this.className = 'umap-dialog window' - this.container = DomUtil.create('dialog', this.className, this.parent) + this.container = DomUtil.create('dialog', this.className, document.body) 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) @@ -48,5 +47,45 @@ export default class Dialog { DomEvent.on(closeButton, 'click', this.close, this) this.container.appendChild(buttonsContainer) this.container.appendChild(content) + DomEvent.once(this.container, 'keydown', (e) => { + DomEvent.stop(e) + if (e.key === 'Escape') this.close() + }) + } +} + +const PROMPT = ` +
+

+ + +
+` + +export class Prompt extends Dialog { + get input() { + return this.container.querySelector('input[name="prompt"]') + } + + get title() { + return this.container.querySelector('h3') + } + + get form() { + return this.container.querySelector('form') + } + + open({ className, title } = {}) { + const content = DomUtil.element({ tagName: 'div', safeHTML: PROMPT }) + super.open({ className, content }) + this.title.textContent = title + const promise = new Promise((resolve, reject) => { + DomEvent.on(this.form, 'submit', (e) => { + DomEvent.stop(e) + resolve(this.input.value) + this.close() + }) + }) + return promise } } diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index c1aaa9f7..c5907661 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -57,7 +57,6 @@ U.Map = L.Map.extend({ this.panel = new U.Panel(this) this.tooltip = new U.Tooltip(this._controlContainer) - this.dialog = new U.Dialog(this._controlContainer) if (this.hasEditMode()) { this.editPanel = new U.EditPanel(this) this.fullPanel = new U.FullPanel(this) @@ -539,18 +538,16 @@ U.Map = L.Map.extend({ initShortcuts: function () { const globalShortcuts = function (e) { if (e.key === 'Escape') { - if (this.dialog.visible) { - this.dialog.close() - } else if (this.importer.dialog.visible) { + if (this.importer.dialog.visible) { this.importer.dialog.close() } else if (this.editEnabled && this.editTools.drawing()) { this.editTools.stopDrawing() } else if (this.measureTools.enabled()) { this.measureTools.stopDrawing() - } else if (this.editPanel?.isOpen()) { - this.editPanel?.close() } else if (this.fullPanel?.isOpen()) { this.fullPanel?.close() + } else if (this.editPanel?.isOpen()) { + this.editPanel?.close() } else if (this.panel.isOpen()) { this.panel.close() } diff --git a/umap/static/umap/js/umap.tableeditor.js b/umap/static/umap/js/umap.tableeditor.js index 9fc2e1e3..861a2b96 100644 --- a/umap/static/umap/js/umap.tableeditor.js +++ b/umap/static/umap/js/umap.tableeditor.js @@ -89,6 +89,19 @@ U.TableEditor = L.Class.extend({ return true }, + addProperty: function () { + new U.Prompt() + .open({ + className: 'dark', + title: L._('Please enter the name of the property'), + }) + .then((newName) => { + if (!newName || !this.validateName(newName)) return + this.datalayer.indexProperty(newName) + this.edit() + }) + }, + edit: function () { const id = 'tableeditor:edit' this.compileProperties() @@ -102,13 +115,7 @@ U.TableEditor = L.Class.extend({ ) const iconElement = L.DomUtil.createIcon(addButton, 'icon-add') addButton.insertBefore(iconElement, addButton.firstChild) - const addProperty = function () { - const newName = prompt(L._('Please enter the name of the property')) - if (!newName || !this.validateName(newName)) return - this.datalayer.indexProperty(newName) - this.edit() - } - L.DomEvent.on(addButton, 'click', addProperty, this) + L.DomEvent.on(addButton, 'click', this.addProperty, this) this.datalayer.map.fullPanel.open({ content: this.table, className: 'umap-table-editor', From 91badcdb5e9838c0b4dc99cf8443486d51c19cc9 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 2 Jul 2024 18:11:22 +0200 Subject: [PATCH 2/4] wip: rework dialog class --- umap/static/umap/css/dialog.css | 27 ++- umap/static/umap/css/window.css | 1 + .../umap/js/components/alerts/alert.css | 20 +- umap/static/umap/js/modules/autocomplete.js | 1 + umap/static/umap/js/modules/global.js | 3 +- umap/static/umap/js/modules/help.js | 4 +- umap/static/umap/js/modules/importer.js | 8 +- .../umap/js/modules/importers/communesfr.js | 4 +- .../umap/js/modules/importers/datasets.js | 14 +- .../umap/js/modules/importers/geodatamine.js | 20 +- .../umap/js/modules/importers/overpass.js | 27 ++- umap/static/umap/js/modules/ui/dialog.js | 226 ++++++++++++------ umap/static/umap/js/umap.features.js | 20 +- umap/static/umap/js/umap.js | 10 +- umap/static/umap/js/umap.tableeditor.js | 73 +++--- .../umap/components/alerts/alert.html | 6 +- umap/tests/integration/test_browser.py | 5 +- umap/tests/integration/test_edit_datalayer.py | 1 + umap/tests/integration/test_edit_polygon.py | 2 +- umap/tests/integration/test_tableeditor.py | 4 +- 20 files changed, 296 insertions(+), 180 deletions(-) diff --git a/umap/static/umap/css/dialog.css b/umap/static/umap/css/dialog.css index 43072f27..d186d7e3 100644 --- a/umap/static/umap/css/dialog.css +++ b/umap/static/umap/css/dialog.css @@ -14,7 +14,28 @@ height: fit-content; max-height: 90vh; } -.umap-dialog .umap-close-link { - float: right; - width: 100px; +:where([data-component="no-dialog"]:not([hidden])) { + display: block; + inset-inline-start: 50%; + position: fixed; + transform: translateX(-50%); +} +:where([data-component*="dialog"] menu) { + display: flex; + gap: calc(var(--gutter) / 2); + justify-content: flex-end; + margin: 0; + padding: 0; +} +:where([data-component*="dialog"] [data-ref="fieldset"]) { + border: 0; + margin: unset; + padding: unset; +} +/* hack for Firefox */ +@-moz-document url-prefix() { + [data-component="no-dialog"]:not([hidden]) { + inset-inline-start: 0; + transform: none; + } } diff --git a/umap/static/umap/css/window.css b/umap/static/umap/css/window.css index a4768fbd..e45e9056 100644 --- a/umap/static/umap/css/window.css +++ b/umap/static/umap/css/window.css @@ -10,6 +10,7 @@ position: sticky; top: 0; height: var(--panel-header-height); + float: right; } .window .buttons li { cursor: pointer; diff --git a/umap/static/umap/js/components/alerts/alert.css b/umap/static/umap/js/components/alerts/alert.css index 737ee71a..af445e6f 100644 --- a/umap/static/umap/js/components/alerts/alert.css +++ b/umap/static/umap/js/components/alerts/alert.css @@ -1,4 +1,4 @@ -[role="dialog"] { +.umap-alert[role="dialog"] { box-sizing: border-box; min-height: 46px; line-height: 46px; @@ -20,36 +20,36 @@ width: max-content; z-index: var(--zindex-alert); } -[role="dialog"] > div { +.umap-alert[role="dialog"] > div { margin: 0 auto; min-width: 60%; background-size: 20px; background-position: 0 15px; padding-left: 28px; } -[role="dialog"][data-level="info"] > div { +.umap-alert[role="dialog"][data-level="info"] > div { background-image: url('../../../img/alert-icon-info.svg'); background-repeat: no-repeat; } -[role="dialog"][data-level="success"] > div { +.umap-alert[role="dialog"][data-level="success"] > div { background-image: url('../../../img/alert-icon-success.svg'); background-repeat: no-repeat; } -[role="dialog"][data-level="error"] > div { +.umap-alert[role="dialog"][data-level="error"] > div { background-image: url('../../../img/alert-icon-error.svg'); background-repeat: no-repeat; } -[role="dialog"][data-level="error"] { +.umap-alert[role="dialog"][data-level="error"] { background-color: var(--color-darkRed); } -[role="dialog"] a { +.umap-alert[role="dialog"] a { text-decoration: underline; } -[role="dialog"] label { +.umap-alert[role="dialog"] label { font-size: .8rem; font-weight: normal; } -[role="dialog"] a[target="_blank"] { +.umap-alert[role="dialog"] a[target="_blank"] { background: url('../../../img/icon-external-link.svg') no-repeat right center; padding-right: 14px; background-size: 12px; @@ -127,7 +127,7 @@ h3[role="alert"] + p { #link-wrapper { margin-bottom: 1rem; } -[role="dialog"] #conflict-wrapper a[target="_blank"] { +.umap-alert[role="dialog"] #conflict-wrapper a[target="_blank"] { background-position-y: 16px; } diff --git a/umap/static/umap/js/modules/autocomplete.js b/umap/static/umap/js/modules/autocomplete.js index 8e4bfae2..54f7697a 100644 --- a/umap/static/umap/js/modules/autocomplete.js +++ b/umap/static/umap/js/modules/autocomplete.js @@ -45,6 +45,7 @@ export class BaseAutocomplete { placeholder: this.options.placeholder, autocomplete: 'off', className: this.options.className, + name: this.options.name || 'autocomplete' }) DomEvent.on(this.input, 'keydown', this.onKeyDown, this) DomEvent.on(this.input, 'keyup', this.onKeyUp, this) diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 366bed6b..c4eebe03 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -14,7 +14,7 @@ import { HTTPError, NOKError, Request, RequestError, ServerRequest } from './req import Rules from './rules.js' import { SCHEMA } from './schema.js' import { SyncEngine } from './sync/engine.js' -import { Dialog, Prompt } from './ui/dialog.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' @@ -42,7 +42,6 @@ window.U = { NOKError, Orderable, Panel, - Prompt, Request, RequestError, Rules, diff --git a/umap/static/umap/js/modules/help.js b/umap/static/umap/js/modules/help.js index 0d50e9cd..aafbc42e 100644 --- a/umap/static/umap/js/modules/help.js +++ b/umap/static/umap/js/modules/help.js @@ -208,7 +208,7 @@ export default class Help { }) } } - this.dialog.open({ content: container, className: 'dark' }) + this.dialog.open({ template: container, className: 'dark', cancel: false, accept: false }) } button(container, entries, classname) { @@ -245,7 +245,7 @@ export default class Help { DomUtil.add('i', action.options.className, actionContainer) DomUtil.add('span', '', actionContainer, action.options.tooltip) DomEvent.on(actionContainer, 'click', action.addHooks, action) - DomEvent.on(actionContainer, 'click', this.dialog.close, this.map.dialog) + DomEvent.on(actionContainer, 'click', this.dialog.close, this.dialog) } title.textContent = translate('Where do we go from here?') for (const id in this.map.helpMenuActions) { diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index 4ab13f18..65069c09 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -2,7 +2,7 @@ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' import { uMapAlert as Alert } from '../components/alerts/alert.js' import { translate } from './i18n.js' import { SCHEMA } from './schema.js' -import { Dialog } from './ui/dialog.js' +import Dialog from './ui/dialog.js' import * as Utils from './utils.js' const TEMPLATE = ` @@ -114,7 +114,7 @@ export default class Importer { } get action() { - return this.qs('[name=action]:checked').value + return this.qs('[name=action]:checked')?.value } get layerId() { @@ -234,7 +234,7 @@ export default class Importer { } submit() { - let hasErrors = false + let hasErrors if (this.format === 'umap') { hasErrors = !this.full() } else if (!this.url) { @@ -242,7 +242,7 @@ export default class Importer { } else if (this.action) { hasErrors = !this[this.action]() } - if (!hasErrors) { + if (hasErrors === false) { Alert.info(translate('Data successfully imported!')) } } diff --git a/umap/static/umap/js/modules/importers/communesfr.js b/umap/static/umap/js/modules/importers/communesfr.js index 6a7348cc..6b4c0f15 100644 --- a/umap/static/umap/js/modules/importers/communesfr.js +++ b/umap/static/umap/js/modules/importers/communesfr.js @@ -37,8 +37,10 @@ export class Importer { this.autocomplete = new Autocomplete(container, options) importer.dialog.open({ - content: container, + template: container, className: `${this.id} importer dark`, + cancel: false, + accept: false, }) } } diff --git a/umap/static/umap/js/modules/importers/datasets.js b/umap/static/umap/js/modules/importers/datasets.js index a376f696..eb2c4049 100644 --- a/umap/static/umap/js/modules/importers/datasets.js +++ b/umap/static/umap/js/modules/importers/datasets.js @@ -30,13 +30,15 @@ export class Importer { importer.format = select.options[select.selectedIndex].dataset.format importer.layerName = select.options[select.selectedIndex].textContent } - importer.dialog.close() } - L.DomUtil.createButton('', container, translate('Choose this dataset'), confirm) - importer.dialog.open({ - content: container, - className: `${this.id} importer dark`, - }) + importer.dialog + .open({ + template: container, + className: `${this.id} importer dark`, + accept: translate('Choose this dataset'), + cancel: false, + }) + .then(confirm) } } diff --git a/umap/static/umap/js/modules/importers/geodatamine.js b/umap/static/umap/js/modules/importers/geodatamine.js index cfbef595..816fcbcb 100644 --- a/umap/static/umap/js/modules/importers/geodatamine.js +++ b/umap/static/umap/js/modules/importers/geodatamine.js @@ -25,7 +25,6 @@ const TEMPLATE = ` - ` class Autocomplete extends SingleMixin(BaseAjax) { @@ -66,7 +65,6 @@ export class Importer { } else { console.error(response) } - const asPoint = container.querySelector('[name=aspoint]') this.autocomplete = new Autocomplete(container.querySelector('#boundary'), { placeholder: translate('Search admin boundary'), url: `${this.baseUrl}/boundaries/search?text={q}`, @@ -75,21 +73,23 @@ export class Importer { boundaryName = choice.item.label }, }) - const confirm = () => { + const confirm = (form) => { if (!boundary || !select.value) { Alert.error(translate('Please choose a theme and a boundary first.')) return } - importer.url = `${this.baseUrl}/data/${select.value}/${boundary}?format=geojson&aspoint=${asPoint.checked}` + importer.url = `${this.baseUrl}/data/${form.theme}/${boundary}?format=geojson&aspoint=${Boolean(form.aspoint)}` importer.format = 'geojson' importer.layerName = `${boundaryName} — ${select.options[select.selectedIndex].textContent}` - importer.dialog.close() } - DomEvent.on(container.querySelector('button'), 'click', confirm) - importer.dialog.open({ - content: container, - className: `${this.id} importer dark`, - }) + importer.dialog + .open({ + template: container, + className: `${this.id} importer dark`, + accept: translate('Choose this data'), + cancel: false, + }) + .then(confirm) } } diff --git a/umap/static/umap/js/modules/importers/overpass.js b/umap/static/umap/js/modules/importers/overpass.js index 8bd7f058..4e9c5190 100644 --- a/umap/static/umap/js/modules/importers/overpass.js +++ b/umap/static/umap/js/modules/importers/overpass.js @@ -11,7 +11,7 @@ const TEMPLATE = `