From 34f7d8ebaf415b4552ccbd029dc7b5dc2aac8fcc Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 6 Jan 2025 20:10:20 +0100 Subject: [PATCH] chore: remove Leaflet dependency from form modules --- umap/static/umap/js/modules/data/features.js | 13 +- umap/static/umap/js/modules/data/layer.js | 20 +- umap/static/umap/js/modules/form/builder.js | 29 +- umap/static/umap/js/modules/form/fields.js | 468 ++++++++++--------- umap/static/umap/js/modules/permissions.js | 9 +- umap/static/umap/js/modules/rules.js | 3 +- umap/static/umap/js/modules/share.js | 3 +- umap/static/umap/js/modules/tableeditor.js | 3 +- umap/static/umap/js/modules/umap.js | 26 +- umap/tests/integration/test_edit_map.py | 4 +- 10 files changed, 291 insertions(+), 287 deletions(-) diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index 0035f5302..707398041 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -16,6 +16,7 @@ import { MaskPolygon, } from '../rendering/ui.js' import loadPopup from '../rendering/popup.js' +import { MutatingForm } from '../form/builder.js' class Feature { constructor(umap, datalayer, geojson = {}, id = null) { @@ -225,7 +226,7 @@ class Feature { `icon-${this.getClassName()}` ) - let builder = new U.FormBuilder( + let builder = new MutatingForm( this, [['datalayer', { handler: 'DataLayerSwitcher' }]], { @@ -254,7 +255,7 @@ class Feature { labelKeyFound = U.DEFAULT_LABEL_KEY } properties.unshift([`properties.${labelKeyFound}`, { label: labelKeyFound }]) - builder = new U.FormBuilder(this, properties, { + builder = new MutatingForm(this, properties, { id: 'umap-feature-properties', }) container.appendChild(builder.build()) @@ -285,7 +286,7 @@ class Feature { appendEditFieldsets(container) { const optionsFields = this.getShapeOptions() - let builder = new U.FormBuilder(this, optionsFields, { + let builder = new MutatingForm(this, optionsFields, { id: 'umap-feature-shape-properties', }) const shapeProperties = DomUtil.createFieldset( @@ -295,7 +296,7 @@ class Feature { shapeProperties.appendChild(builder.build()) const advancedOptions = this.getAdvancedOptions() - builder = new U.FormBuilder(this, advancedOptions, { + builder = new MutatingForm(this, advancedOptions, { id: 'umap-feature-advanced-properties', }) const advancedProperties = DomUtil.createFieldset( @@ -305,7 +306,7 @@ class Feature { advancedProperties.appendChild(builder.build()) const interactionOptions = this.getInteractionOptions() - builder = new U.FormBuilder(this, interactionOptions) + builder = new MutatingForm(this, interactionOptions) const popupFieldset = DomUtil.createFieldset( container, translate('Interaction options') @@ -733,7 +734,7 @@ export class Point extends Feature { ['ui._latlng.lat', { handler: 'FloatInput', label: translate('Latitude') }], ['ui._latlng.lng', { handler: 'FloatInput', label: translate('Longitude') }], ] - const builder = new U.FormBuilder(this, coordinatesOptions, { + const builder = new MutatingForm(this, coordinatesOptions, { callback: () => { if (!this.ui._latlng.isValid()) { Alert.error(translate('Invalid latitude or longitude')) diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index a6f77a671..bd36bd951 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -1,5 +1,3 @@ -// Uses U.FormBuilder not available as ESM - // FIXME: this module should not depend on Leaflet import { DomUtil, @@ -22,6 +20,7 @@ import { Point, LineString, Polygon } from './features.js' import TableEditor from '../tableeditor.js' import { ServerStored } from '../saving.js' import * as Schema from '../schema.js' +import { MutatingForm } from '../form/builder.js' export const LAYER_TYPES = [ DefaultLayer, @@ -668,10 +667,11 @@ export class DataLayer extends ServerStored { ], ] DomUtil.createTitle(container, translate('Layer properties'), 'icon-layers') - let builder = new U.FormBuilder(this, metadataFields, { - callback(e) { + let builder = new MutatingForm(this, metadataFields, { + callback: (helper) => { + console.log(helper) this._umap.onDataLayersChanged() - if (e.helper.field === 'options.type') { + if (helper.field === 'options.type') { this.edit() } }, @@ -681,7 +681,7 @@ export class DataLayer extends ServerStored { const layerOptions = this.layer.getEditableOptions() if (layerOptions.length) { - builder = new U.FormBuilder(this, layerOptions, { + builder = new MutatingForm(this, layerOptions, { id: 'datalayer-layer-properties', }) const layerProperties = DomUtil.createFieldset( @@ -704,7 +704,7 @@ export class DataLayer extends ServerStored { 'options.fillOpacity', ] - builder = new U.FormBuilder(this, shapeOptions, { + builder = new MutatingForm(this, shapeOptions, { id: 'datalayer-advanced-properties', }) const shapeProperties = DomUtil.createFieldset( @@ -721,7 +721,7 @@ export class DataLayer extends ServerStored { 'options.toZoom', ] - builder = new U.FormBuilder(this, optionsFields, { + builder = new MutatingForm(this, optionsFields, { id: 'datalayer-advanced-properties', }) const advancedProperties = DomUtil.createFieldset( @@ -740,7 +740,7 @@ export class DataLayer extends ServerStored { 'options.outlinkTarget', 'options.interactive', ] - builder = new U.FormBuilder(this, popupFields) + builder = new MutatingForm(this, popupFields) const popupFieldset = DomUtil.createFieldset( container, translate('Interaction options') @@ -796,7 +796,7 @@ export class DataLayer extends ServerStored { container, translate('Remote data') ) - builder = new U.FormBuilder(this, remoteDataFields) + builder = new MutatingForm(this, remoteDataFields) remoteDataContainer.appendChild(builder.build()) DomUtil.createButton( 'button umap-verify', diff --git a/umap/static/umap/js/modules/form/builder.js b/umap/static/umap/js/modules/form/builder.js index 86a7ac843..cb6d0446c 100644 --- a/umap/static/umap/js/modules/form/builder.js +++ b/umap/static/umap/js/modules/form/builder.js @@ -13,7 +13,7 @@ export class Form { this.form.id = this.properties.id } if (this.properties.className) { - this.form.classList.add(this.properties.className) + this.form.classList.add(...this.properties.className.split(' ')) } } @@ -108,14 +108,16 @@ export class Form { } } - onPostSync() { + onPostSync(helper) { if (this.properties.callback) { - this.properties.callback(this.obj) + this.properties.callback(helper) } } + + finish() {} } -export class DataForm extends Form { +export class MutatingForm extends Form { constructor(obj, fields, properties) { super(obj, fields, properties) this._umap = obj._umap || properties.umap @@ -188,22 +190,7 @@ export class DataForm extends Form { } } - getter(field) { - const path = field.split('.') - let value = this.obj - let sub - for (sub of path) { - try { - value = value[sub] - } catch { - console.log(field) - } - } - if (value === undefined) value = SCHEMA[sub]?.default - return value - } - - finish(event) { - event.helper?.input?.blur() + finish(helper) { + helper.input?.blur() } } diff --git a/umap/static/umap/js/modules/form/fields.js b/umap/static/umap/js/modules/form/fields.js index 9a4fe8676..3c183a751 100644 --- a/umap/static/umap/js/modules/form/fields.js +++ b/umap/static/umap/js/modules/form/fields.js @@ -5,6 +5,8 @@ import { AjaxAutocompleteMultiple, AutocompleteDatalist, } from '../autocomplete.js' +import { SCHEMA } from '../schema.js' +import * as Icon from '../rendering/icon.js' const Fields = {} @@ -35,8 +37,8 @@ class BaseElement { getParentNode() { const classNames = ['formbox'] if (this.properties.inheritable) { - classNames.push(inheritable) - if (this.get(true)) classNames.push('undefined') + classNames.push('inheritable') + if (this.get(true) === undefined) classNames.push('undefined') } classNames.push(`umap-field-${this.name}`) const [wrapper, { header, define, undefine, quickContainer, container }] = @@ -55,8 +57,8 @@ class BaseElement { this.form.appendChild(this.wrapper) if (this.properties.inheritable) { define.addEventListener('click', (event) => { - e.preventDefault() - e.stopPropagation() + event.preventDefault() + event.stopPropagation() this.fetch() this.onDefine() this.wrapper.classList.remove('undefined') @@ -80,7 +82,7 @@ class BaseElement { if (!this.properties.inheritable || own) return this.builder.getter(this.field) const path = this.field.split('.') const key = path[path.length - 1] - return this.obj.getOption(key) + return this.obj.getOption(key) || SCHEMA[key]?.default } toHTML() { @@ -110,13 +112,16 @@ class BaseElement { buildLabel() { if (this.properties.label) { - this.label = L.DomUtil.create('label', '', this.getLabelParent()) - this.label.textContent = this.label.title = this.properties.label + const label = this.properties.label + this.label = Utils.loadTemplate(``) + const parent = this.getLabelParent() + parent.appendChild(this.label) if (this.properties.helpEntries) { this.builder._umap.help.button(this.label, this.properties.helpEntries) } else if (this.properties.helpTooltip) { - const info = L.DomUtil.create('i', 'info', this.label) - L.DomEvent.on(info, 'mouseover', () => { + const info = Utils.loadTemplate('') + this.label.appendChild(info) + info.addEventListener('mouseover', () => { this.builder._umap.tooltip.open({ anchor: info, content: this.properties.helpTooltip, @@ -129,22 +134,23 @@ class BaseElement { buildHelpText() { if (this.properties.helpText) { - const container = L.DomUtil.create('small', 'help-text', this.getHelpTextParent()) - container.innerHTML = this.properties.helpText + const container = Utils.loadTemplate( + `${Utils.escapeHTML(this.properties.helpText)}` + ) + const parent = this.getHelpTextParent() + parent.appendChild(container) } } fetch() {} - finish() { - this.fireAndForward('finish') - } + finish() {} onPostSync() { if (this.properties.callback) { - this.properties.callback(this.obj) + this.properties.callback(this) } - this.builder.onPostSync() + this.builder.onPostSync(this) } undefine() { @@ -156,16 +162,15 @@ class BaseElement { Fields.Textarea = class extends BaseElement { build() { - this.input = L.DomUtil.create( - 'textarea', - this.properties.className || '', - this.parentNode - ) - if (this.properties.placeholder) + this.input = Utils.loadTemplate('') + if (this.properties.className) this.input.classList.add(this.properties.className) + if (this.properties.placeholder) { this.input.placeholder = this.properties.placeholder + } + this.parentNode.appendChild(this.input) this.fetch() - L.DomEvent.on(this.input, 'input', this.sync, this) - L.DomEvent.on(this.input, 'keypress', this.onKeyPress, this) + this.input.addEventListener('input', () => this.sync()) + this.input.addEventListener('keypress', (event) => this.onKeyPress(event)) } fetch() { @@ -180,9 +185,10 @@ Fields.Textarea = class extends BaseElement { return this.input.value } - onKeyPress(e) { - if (e.key === 'Enter' && (e.shiftKey || e.ctrlKey)) { - L.DomEvent.stop(e) + onKeyPress(event) { + if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) { + event.stopPropagation() + event.preventDefault() this.finish() } } @@ -190,14 +196,14 @@ Fields.Textarea = class extends BaseElement { Fields.Input = class extends BaseElement { build() { - this.input = L.DomUtil.create( - 'input', - this.properties.className || '', - this.parentNode - ) + this.input = Utils.loadTemplate('') + this.parentNode.appendChild(this.input) this.input.type = this.type() this.input.name = this.name this.input._helper = this + if (this.properties.className) { + this.input.classList.add(this.properties.className) + } if (this.properties.placeholder) { this.input.placeholder = this.properties.placeholder } @@ -211,8 +217,8 @@ Fields.Input = class extends BaseElement { this.input.step = this.properties.step } this.fetch() - L.DomEvent.on(this.input, this.getSyncEvent(), this.sync, this) - L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this) + this.input.addEventListener(this.getSyncEvent(), () => this.sync()) + this.input.addEventListener('keydown', (event) => this.onKeyDown(event)) } fetch() { @@ -233,10 +239,12 @@ Fields.Input = class extends BaseElement { return this.input.value || undefined } - onKeyDown(e) { - if (e.key === 'Enter') { - L.DomEvent.stop(e) + onKeyDown(event) { + if (event.key === 'Enter') { + event.stopPropagation() + event.preventDefault() this.finish() + this.input.blur() } } } @@ -249,8 +257,8 @@ Fields.BlurInput = class extends Fields.Input { build() { this.properties.className = 'blur' super.build() - const button = L.DomUtil.create('span', 'button blur-button') - L.DomUtil.after(this.input, button) + const button = Utils.loadTemplate('') + this.input.parentNode.insertBefore(button, this.input.nextSibling) this.input.addEventListener('focus', () => this.fetch()) } @@ -312,7 +320,11 @@ Fields.CheckBox = class extends BaseElement { build() { const container = Utils.loadTemplate('
') this.parentNode.appendChild(container) - this.input = L.DomUtil.create('input', this.properties.className || '', container) + this.input = Utils.loadTemplate('') + container.appendChild(this.input) + if (this.properties.className) { + this.input.classList.add(this.properties.className) + } this.input.type = 'checkbox' this.input.name = this.name this.input._helper = this @@ -340,11 +352,11 @@ Fields.CheckBox = class extends BaseElement { Fields.Select = class extends BaseElement { build() { - this.select = L.DomUtil.create('select', '', this.parentNode) - this.select.name = this.name + this.select = Utils.loadTemplate(``) + this.parentNode.appendChild(this.select) this.validValues = [] this.buildOptions() - L.DomEvent.on(this.select, 'change', this.sync, this) + this.select.addEventListener('change', () => this.sync()) } getOptions() { @@ -365,7 +377,8 @@ Fields.Select = class extends BaseElement { buildOption(value, label) { this.validValues.push(value) - const option = L.DomUtil.create('option', '', this.select) + const option = Utils.loadTemplate('') + this.select.appendChild(option) option.value = value option.innerHTML = label if (this.toHTML() === value) { @@ -374,8 +387,9 @@ Fields.Select = class extends BaseElement { } value() { - if (this.select[this.select.selectedIndex]) + if (this.select[this.select.selectedIndex]) { return this.select[this.select.selectedIndex].value + } } getDefault() { @@ -431,15 +445,14 @@ Fields.NullableBoolean = class extends Fields.Select { Fields.EditableText = class extends BaseElement { build() { - this.input = L.DomUtil.create( - 'span', - this.properties.className || '', - this.parentNode + this.input = Utils.loadTemplate( + `` ) + this.parentNode.appendChild(this.input) this.input.contentEditable = true this.fetch() - L.DomEvent.on(this.input, 'input', this.sync, this) - L.DomEvent.on(this.input, 'keypress', this.onKeyPress, this) + this.input.addEventListener('input', () => this.sync()) + this.input.addEventListener('keypress', (event) => this.onKeyPress(event)) } getParentNode() { @@ -475,21 +488,21 @@ Fields.ColorPicker = class extends Fields.Input { build() { super.build() this.input.placeholder = this.properties.placeholder || translate('Inherit') - this.container = L.DomUtil.create( - 'div', - 'umap-color-picker', - this.extendedContainer - ) + this.container = Utils.loadTemplate('
') + this.extendedContainer.appendChild(this.container) this.container.style.display = 'none' - for (const idx in this.colors) { - this.addColor(this.colors[idx]) + for (const color of this.getColors()) { + this.addColor(color) } this.spreadColor() this.input.autocomplete = 'off' - L.DomEvent.on(this.input, 'focus', this.onFocus, this) - L.DomEvent.on(this.input, 'blur', this.onBlur, this) - L.DomEvent.on(this.input, 'change', this.sync, this) - this.on('define', this.onFocus) + this.input.addEventListener('focus', (event) => this.onFocus(event)) + this.input.addEventListener('blur', (event) => this.onBlur(event)) + this.input.addEventListener('change', () => this.sync()) + } + + onDefine() { + this.onFocus() } onFocus() { @@ -516,14 +529,15 @@ Fields.ColorPicker = class extends Fields.Input { } addColor(colorName) { - const span = L.DomUtil.create('span', '', this.container) + const span = Utils.loadTemplate('') + this.container.appendChild(span) span.style.backgroundColor = span.title = colorName - const updateColorInput = function () { + const updateColorInput = () => { this.input.value = colorName this.sync() this.container.style.display = 'none' } - L.DomEvent.on(span, 'mousedown', updateColorInput, this) + span.addEventListener('mousedown', updateColorInput) } } @@ -668,12 +682,20 @@ Fields.IconUrl = class extends Fields.BlurInput { build() { super.build() - this.buttons = L.DomUtil.create('div', '', this.parentNode) - this.tabs = L.DomUtil.create('div', 'flat-tabs', this.parentNode) - this.body = L.DomUtil.create('div', 'umap-pictogram-body', this.parentNode) - this.footer = L.DomUtil.create('div', '', this.parentNode) + const [container, { buttons, tabs, body, footer }] = Utils.loadTemplateWithRefs(` +
+
+
+
+
+
+ `) + this.parentNode.appendChild(container) + this.buttons = buttons + this.tabs = tabs + this.body = body + this.footer = footer this.updatePreview() - this.on('define', this.onDefine) } async onDefine() { @@ -689,72 +711,64 @@ Fields.IconUrl = class extends Fields.BlurInput { else if (!value || Utils.isPath(value)) this.showSymbolsTab() else if (Utils.isRemoteUrl(value) || Utils.isDataImage(value)) this.showURLTab() else this.showCharsTab() - const closeButton = L.DomUtil.createButton( - 'button action-button', - this.footer, - translate('Close'), - function (e) { - this.body.innerHTML = '' - this.tabs.innerHTML = '' - this.footer.innerHTML = '' - if (this.isDefault()) this.undefine(e) - else this.updatePreview() - }, - this + const closeButton = Utils.loadTemplate( + `` ) + closeButton.addEventListener('click', () => { + this.body.innerHTML = '' + this.tabs.innerHTML = '' + this.footer.innerHTML = '' + if (this.isDefault()) this.undefine() + else this.updatePreview() + }) + this.footer.appendChild(closeButton) } buildTabs() { this.tabs.innerHTML = '' - if (U.Icon.RECENT.length) { - const recent = L.DomUtil.add( - 'button', - 'flat tab-recent', - this.tabs, - translate('Recent') - ) - L.DomEvent.on(recent, 'click', L.DomEvent.stop).on( - recent, - 'click', - this.showRecentTab, - this - ) + // Useless div, but loadTemplate needs a root element + const [root, { recent, symbols, chars, url }] = Utils.loadTemplateWithRefs(` +
+ + + + +
+ `) + this.tabs.appendChild(root) + if (Icon.RECENT.length) { + recent.addEventListener('click', (event) => { + event.stopPropagation() + event.preventDefault() + this.showRecentTab() + }) + } else { + recent.hidden = true } - const symbol = L.DomUtil.add( - 'button', - 'flat tab-symbols', - this.tabs, - translate('Symbol') - ) - const char = L.DomUtil.add( - 'button', - 'flat tab-chars', - this.tabs, - translate('Emoji & Character') - ) - url = L.DomUtil.add('button', 'flat tab-url', this.tabs, translate('URL')) - L.DomEvent.on(symbol, 'click', L.DomEvent.stop).on( - symbol, - 'click', - this.showSymbolsTab, - this - ) - L.DomEvent.on(char, 'click', L.DomEvent.stop).on( - char, - 'click', - this.showCharsTab, - this - ) - L.DomEvent.on(url, 'click', L.DomEvent.stop).on(url, 'click', this.showURLTab, this) + symbols.addEventListener('click', (event) => { + event.stopPropagation() + event.preventDefault() + this.showSymbolsTab() + }) + chars.addEventListener('click', (event) => { + event.stopPropagation() + event.preventDefault() + this.showCharsTab() + }) + url.addEventListener('click', (event) => { + event.stopPropagation() + event.preventDefault() + this.showURLTab() + }) } openTab(name) { const els = this.tabs.querySelectorAll('button') for (const el of els) { - L.DomUtil.removeClass(el, 'on') + el.classList.remove('on') } const el = this.tabs.querySelector(`.tab-${name}`) - L.DomUtil.addClass(el, 'on') + el.classList.add('on') this.body.innerHTML = '' } @@ -763,17 +777,17 @@ Fields.IconUrl = class extends Fields.BlurInput { if (this.isDefault()) return if (!Utils.hasVar(this.value())) { // Do not try to render URL with variables - const box = L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons) - L.DomEvent.on(box, 'click', this.onDefine, this) - const icon = U.Icon.makeElement(this.value(), box) + const box = Utils.loadTemplate('
') + this.buttons.appendChild(box) + box.addEventListener('click', () => this.onDefine()) + const icon = Icon.makeElement(this.value(), box) } - this.button = L.DomUtil.createButton( - 'button action-button', - this.buttons, - this.value() ? translate('Change') : translate('Add'), - this.onDefine, - this + const text = this.value() ? translate('Change') : translate('Add') + const button = Utils.loadTemplate( + `` ) + button.addEventListener('click', () => this.onDefine()) + this.buttons.appendChild(button) } addIconPreview(pictogram, parent) { @@ -785,20 +799,17 @@ Fields.IconUrl = class extends Fields.BlurInput { : pictogram.name || pictogram.src if (search && Utils.normalize(title).indexOf(search) === -1) return const className = value === this.value() ? `${baseClass} selected` : baseClass - const container = L.DomUtil.create('div', className, parent) - U.Icon.makeElement(value, container) - container.title = title - L.DomEvent.on( - container, - 'click', - function (e) { - this.input.value = value - this.sync() - this.unselectAll(this.grid) - L.DomUtil.addClass(container, 'selected') - }, - this + const container = Utils.loadTemplate( + `
` ) + parent.appendChild(container) + Icon.makeElement(value, container) + container.addEventListener('click', () => { + this.input.value = value + this.sync() + this.unselectAll(this.grid) + container.classList.add('selected') + }) return true // Icon has been added (not filtered) } @@ -811,14 +822,17 @@ Fields.IconUrl = class extends Fields.BlurInput { } addCategory(items, name) { - const parent = L.DomUtil.create('div', 'umap-pictogram-category') - if (name) L.DomUtil.add('h6', '', parent, name) - const grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent) - let status = false + const [parent, { grid }] = Utils.loadTemplateWithRefs(` +
+ +
+
+ `) + let hasIcons = false for (const item of items) { - status = this.addIconPreview(item, grid) || status + hasIcons = this.addIconPreview(item, grid) || hasIcons } - if (status) this.grid.appendChild(parent) + if (hasIcons) this.grid.appendChild(parent) } buildSymbolsList() { @@ -847,33 +861,35 @@ Fields.IconUrl = class extends Fields.BlurInput { } isDefault() { - return !this.value() || this.value() === U.SCHEMA.iconUrl.default + return !this.value() || this.value() === SCHEMA.iconUrl.default } addGrid(onSearch) { - this.searchInput = L.DomUtil.create('input', '', this.body) - this.searchInput.type = 'search' - this.searchInput.placeholder = translate('Search') - this.grid = L.DomUtil.create('div', '', this.body) - L.DomEvent.on(this.searchInput, 'input', onSearch, this) + this.searchInput = Utils.loadTemplate( + `` + ) + this.grid = Utils.loadTemplate('
') + this.body.appendChild(this.searchInput) + this.body.appendChild(this.grid) + this.searchInput.addEventListener('input', onSearch) } showRecentTab() { - if (!U.Icon.RECENT.length) return + if (!Icon.RECENT.length) return this.openTab('recent') - this.addGrid(this.buildRecentList) + this.addGrid(() => this.buildRecentList()) this.buildRecentList() } showSymbolsTab() { this.openTab('symbols') - this.addGrid(this.buildSymbolsList) + this.addGrid(() => this.buildSymbolsList()) this.buildSymbolsList() } showCharsTab() { this.openTab('chars') - const value = !U.Icon.isImg(this.value()) ? this.value() : null + const value = !Icon.isImg(this.value()) ? this.value() : null const input = this.buildInput(this.body, value) input.placeholder = translate('Type char or paste emoji') input.type = 'text' @@ -891,10 +907,12 @@ Fields.IconUrl = class extends Fields.BlurInput { } buildInput(parent, value) { - const input = L.DomUtil.create('input', 'blur', parent) - const button = L.DomUtil.create('span', 'button blur-button', parent) + const input = Utils.loadTemplate('') + const button = Utils.loadTemplate('') + parent.appendChild(input) + parent.appendChild(button) if (value) input.value = value - L.DomEvent.on(input, 'blur', () => { + input.addEventListener('blur', () => { // Do not clear this.input when focus-blur // empty input if (input.value === value) return @@ -926,33 +944,34 @@ Fields.Switch = class extends Fields.CheckBox { build() { super.build() - console.log(this) if (this.properties.inheritable) { this.label = Utils.loadTemplate('') } this.input.parentNode.appendChild(this.label) - L.DomUtil.addClass(this.input.parentNode, 'with-switch') + this.input.parentNode.classList.add('with-switch') const id = `${this.builder.properties.id || Date.now()}.${this.name}` this.label.setAttribute('for', id) - L.DomUtil.addClass(this.input, 'switch') + this.input.classList.add('switch') this.input.id = id } } Fields.FacetSearchBase = class extends BaseElement { - buildLabel() { - this.label = L.DomUtil.element({ - tagName: 'legend', - textContent: this.properties.label, - }) - } + buildLabel() {} } Fields.FacetSearchChoices = class extends Fields.FacetSearchBase { build() { - this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) - this.container.appendChild(this.label) - this.ul = L.DomUtil.create('ul', '', this.container) + const [container, { ul, label }] = Utils.loadTemplateWithRefs(` +
+ ${Utils.escapeHTML(this.properties.label)} + +
+ `) + this.container = container + this.ul = ul + this.label = label + this.parentNode.appendChild(this.container) this.type = this.properties.criteria.type const choices = this.properties.criteria.choices @@ -961,17 +980,20 @@ Fields.FacetSearchChoices = class extends Fields.FacetSearchBase { } buildLi(value) { - const property_li = L.DomUtil.create('li', '', this.ul) - const label = L.DomUtil.create('label', '', property_li) - const input = L.DomUtil.create('input', '', label) - L.DomUtil.add('span', '', label, value) - - input.type = this.type - input.name = `${this.type}_${this.name}` + const name = `${this.type}_${this.name}` + const [li, { input, label }] = Utils.loadTemplateWithRefs(` +
  • + +
  • + `) + label.textContent = value input.checked = this.get().choices.includes(value) input.dataset.value = value - - L.DomEvent.on(input, 'change', (e) => this.sync()) + input.addEventListener('change', () => this.sync()) + this.ul.appendChild(li) } toJS() { @@ -998,26 +1020,27 @@ Fields.MinMaxBase = class extends Fields.FacetSearchBase { } build() { - this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) - this.container.appendChild(this.label) + const [minLabel, maxLabel] = this.getLabels() const { min, max, type } = this.properties.criteria const { min: modifiedMin, max: modifiedMax } = this.get() const currentMin = modifiedMin !== undefined ? modifiedMin : min const currentMax = modifiedMax !== undefined ? modifiedMax : max this.type = type - this.inputType = this.getInputType(this.type) - - const [minLabel, maxLabel] = this.getLabels() - - this.minLabel = L.DomUtil.create('label', '', this.container) - this.minLabel.textContent = minLabel - - this.minInput = L.DomUtil.create('input', '', this.minLabel) - this.minInput.type = this.inputType - this.minInput.step = 'any' - this.minInput.min = this.prepareForHTML(min) - this.minInput.max = this.prepareForHTML(max) + const inputType = this.getInputType(this.type) + const minHTML = this.prepareForHTML(min) + const maxHTML = this.prepareForHTML(max) + const [container, { minInput, maxInput }] = Utils.loadTemplateWithRefs(` +
    + ${Utils.escapeHTML(this.properties.label)} + + +
    + `) + this.container = container + this.minInput = minInput + this.maxInput = maxInput + this.parentNode.appendChild(this.container) if (min != null) { // The value stored using setAttribute is not modified by // user input, and will be used as initial value when calling @@ -1029,14 +1052,6 @@ Fields.MinMaxBase = class extends Fields.FacetSearchBase { this.minInput.value = this.prepareForHTML(currentMin) } - this.maxLabel = L.DomUtil.create('label', '', this.container) - this.maxLabel.textContent = maxLabel - - this.maxInput = L.DomUtil.create('input', '', this.maxLabel) - this.maxInput.type = this.inputType - this.maxInput.step = 'any' - this.maxInput.min = this.prepareForHTML(min) - this.maxInput.max = this.prepareForHTML(max) if (max != null) { // Cf comment above about setAttribute vs value this.maxInput.setAttribute('value', this.prepareForHTML(max)) @@ -1044,8 +1059,8 @@ Fields.MinMaxBase = class extends Fields.FacetSearchBase { } this.toggleStatus() - L.DomEvent.on(this.minInput, 'change', () => this.sync()) - L.DomEvent.on(this.maxInput, 'change', () => this.sync()) + this.minInput.addEventListener('change', () => this.sync()) + this.maxInput.addEventListener('change', () => this.sync()) } toggleStatus() { @@ -1126,6 +1141,7 @@ Fields.MultiChoice = class extends BaseElement { getDefault() { return 'null' } + // TODO: use public property when it's in our baseline getClassName() { return 'umap-multiplechoice' } @@ -1160,11 +1176,10 @@ Fields.MultiChoice = class extends BaseElement { build() { const choices = this.getChoices() - this.container = L.DomUtil.create( - 'div', - `${this.className} by${choices.length}`, - this.parentNode + this.container = Utils.loadTemplate( + `
    ` ) + this.parentNode.appendChild(this.container) for (const [i, [value, label]] of choices.entries()) { this.addChoice(value, label, i) } @@ -1172,15 +1187,15 @@ Fields.MultiChoice = class extends BaseElement { } addChoice(value, label, counter) { - const input = L.DomUtil.create('input', '', this.container) - label = L.DomUtil.add('label', '', this.container, label) - input.type = 'radio' - input.name = this.name - input.value = value const id = `${Date.now()}.${this.name}.${counter}` - label.setAttribute('for', id) - input.id = id - L.DomEvent.on(input, 'change', this.sync, this) + const input = Utils.loadTemplate( + `` + ) + this.container.appendChild(input) + this.container.appendChild( + Utils.loadTemplate(``) + ) + input.addEventListener('change', () => this.sync()) } } @@ -1261,13 +1276,10 @@ Fields.Range = class extends Fields.FloatInput { digits )}">` } - const datalist = L.DomUtil.element({ - tagName: 'datalist', - parent: this.getHelpTextParent(), - className: 'umap-field-datalist', - safeHTML: options, - id: id, - }) + const parent = this.getHelpTextParent() + const datalist = Utils.loadTemplate( + `${options}` + ) this.input.setAttribute('list', id) super.buildHelpText() } diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index b2c7650fe..3ece0029c 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -3,6 +3,7 @@ import { translate } from './i18n.js' import { uMapAlert as Alert } from '../components/alerts/alert.js' import { ServerStored } from './saving.js' import * as Utils from './utils.js' +import { MutatingForm } from './form/builder.js' // Dedicated object so we can deal with a separate dirty status, and thus // call the endpoint only when needed, saving one call at each save. @@ -58,7 +59,7 @@ export class MapPermissions extends ServerStored { selectOptions: this._umap.properties.share_statuses, }, ]) - const builder = new U.FormBuilder(this, fields) + const builder = new MutatingForm(this, fields) const form = builder.build() container.appendChild(form) @@ -133,7 +134,7 @@ export class MapPermissions extends ServerStored { { handler: 'ManageEditors', label: translate("Map's editors") }, ]) - const builder = new U.FormBuilder(this, topFields) + const builder = new MutatingForm(this, topFields) const form = builder.build() container.appendChild(form) if (collaboratorsFields.length) { @@ -141,7 +142,7 @@ export class MapPermissions extends ServerStored { `
    ${translate('Manage collaborators')}
    ` ) container.appendChild(fieldset) - const builder = new U.FormBuilder(this, collaboratorsFields) + const builder = new MutatingForm(this, collaboratorsFields) const form = builder.build() container.appendChild(form) } @@ -269,7 +270,7 @@ export class DataLayerPermissions extends ServerStored { }, ], ] - const builder = new U.FormBuilder(this, fields, { + const builder = new MutatingForm(this, fields, { className: 'umap-form datalayer-permissions', }) const form = builder.build() diff --git a/umap/static/umap/js/modules/rules.js b/umap/static/umap/js/modules/rules.js index f8c6dbb4f..2e79ece17 100644 --- a/umap/static/umap/js/modules/rules.js +++ b/umap/static/umap/js/modules/rules.js @@ -3,6 +3,7 @@ import { translate } from './i18n.js' import * as Utils from './utils.js' import { AutocompleteDatalist } from './autocomplete.js' import Orderable from './orderable.js' +import { MutatingForm } from './form/builder.js' const EMPTY_VALUES = ['', undefined, null] @@ -129,7 +130,7 @@ class Rule { 'options.dashArray', ] const container = DomUtil.create('div') - const builder = new U.FormBuilder(this, options) + const builder = new MutatingForm(this, options) const defaultShapeProperties = DomUtil.add('div', '', container) defaultShapeProperties.appendChild(builder.build()) const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input) diff --git a/umap/static/umap/js/modules/share.js b/umap/static/umap/js/modules/share.js index 919656795..6a0a49cee 100644 --- a/umap/static/umap/js/modules/share.js +++ b/umap/static/umap/js/modules/share.js @@ -2,6 +2,7 @@ import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' import { EXPORT_FORMATS } from './formatter.js' import { translate } from './i18n.js' import * as Utils from './utils.js' +import { MutatingForm } from './form/builder.js' export default class Share { constructor(umap) { @@ -125,7 +126,7 @@ export default class Share { exportUrl.value = window.location.protocol + iframeExporter.buildUrl() } buildIframeCode() - const builder = new U.FormBuilder(iframeExporter, UIFields, { + const builder = new MutatingForm(iframeExporter, UIFields, { callback: buildIframeCode, }) const iframeOptions = DomUtil.createFieldset( diff --git a/umap/static/umap/js/modules/tableeditor.js b/umap/static/umap/js/modules/tableeditor.js index 0cabf37ae..255f26cbf 100644 --- a/umap/static/umap/js/modules/tableeditor.js +++ b/umap/static/umap/js/modules/tableeditor.js @@ -2,6 +2,7 @@ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' import { translate } from './i18n.js' import ContextMenu from './ui/contextmenu.js' import { WithTemplate, loadTemplate } from './utils.js' +import { MutatingForm } from './form/builder.js' const TEMPLATE = ` @@ -205,7 +206,7 @@ export default class TableEditor extends WithTemplate { const tr = event.target.closest('tr') const feature = this.datalayer.getFeatureById(tr.dataset.feature) const handler = property === 'description' ? 'Textarea' : 'Input' - const builder = new U.FormBuilder(feature, [[field, { handler }]], { + const builder = new MutatingForm(feature, [[field, { handler }]], { id: `umap-feature-properties_${L.stamp(feature)}`, }) cell.innerHTML = '' diff --git a/umap/static/umap/js/modules/umap.js b/umap/static/umap/js/modules/umap.js index 543a5a94c..aff442282 100644 --- a/umap/static/umap/js/modules/umap.js +++ b/umap/static/umap/js/modules/umap.js @@ -34,7 +34,7 @@ import { uMapAlert as Alert, } from '../components/alerts/alert.js' import Orderable from './orderable.js' -import { DataForm } from './form/builder.js' +import { MutatingForm } from './form/builder.js' export default class Umap extends ServerStored { constructor(element, geojson) { @@ -735,7 +735,7 @@ export default class Umap extends ServerStored { const metadataFields = ['properties.name', 'properties.description'] DomUtil.createTitle(container, translate('Edit map details'), 'icon-caption') - const builder = new DataForm(this, metadataFields, { + const builder = new MutatingForm(this, metadataFields, { className: 'map-metadata', umap: this, }) @@ -750,7 +750,7 @@ export default class Umap extends ServerStored { 'properties.permanentCredit', 'properties.permanentCreditBackground', ] - const creditsBuilder = new DataForm(this, creditsFields, { umap: this }) + const creditsBuilder = new MutatingForm(this, creditsFields, { umap: this }) credits.appendChild(creditsBuilder.build()) this.editPanel.open({ content: container }) } @@ -771,7 +771,7 @@ export default class Umap extends ServerStored { 'properties.captionBar', 'properties.captionMenus', ]) - const builder = new DataForm(this, UIFields, { umap: this }) + const builder = new MutatingForm(this, UIFields, { umap: this }) const controlsOptions = DomUtil.createFieldset( container, translate('User interface options') @@ -794,7 +794,7 @@ export default class Umap extends ServerStored { 'properties.dashArray', ] - const builder = new DataForm(this, shapeOptions, { umap: this }) + const builder = new MutatingForm(this, shapeOptions, { umap: this }) const defaultShapeProperties = DomUtil.createFieldset( container, translate('Default shape properties') @@ -813,7 +813,7 @@ export default class Umap extends ServerStored { 'properties.slugKey', ] - const builder = new DataForm(this, optionsFields, { umap: this }) + const builder = new MutatingForm(this, optionsFields, { umap: this }) const defaultProperties = DomUtil.createFieldset( container, translate('Default properties') @@ -831,7 +831,7 @@ export default class Umap extends ServerStored { 'properties.labelInteractive', 'properties.outlinkTarget', ] - const builder = new DataForm(this, popupFields, { umap: this }) + const builder = new MutatingForm(this, popupFields, { umap: this }) const popupFieldset = DomUtil.createFieldset( container, translate('Default interaction options') @@ -888,7 +888,7 @@ export default class Umap extends ServerStored { container, translate('Custom background') ) - const builder = new DataForm(this, tilelayerFields, { umap: this }) + const builder = new MutatingForm(this, tilelayerFields, { umap: this }) customTilelayer.appendChild(builder.build()) } @@ -936,7 +936,7 @@ export default class Umap extends ServerStored { ['properties.overlay.tms', { handler: 'Switch', label: translate('TMS format') }], ] const overlay = DomUtil.createFieldset(container, translate('Custom overlay')) - const builder = new DataForm(this, overlayFields, { umap: this }) + const builder = new MutatingForm(this, overlayFields, { umap: this }) overlay.appendChild(builder.build()) } @@ -963,7 +963,7 @@ export default class Umap extends ServerStored { { handler: 'BlurFloatInput', placeholder: translate('max East') }, ], ] - const boundsBuilder = new DataForm(this, boundsFields, { umap: this }) + const boundsBuilder = new MutatingForm(this, boundsFields, { umap: this }) limitBounds.appendChild(boundsBuilder.build()) const boundsButtons = DomUtil.create('div', 'button-bar half', limitBounds) DomUtil.createButton( @@ -1028,7 +1028,7 @@ export default class Umap extends ServerStored { { handler: 'Switch', label: translate('Autostart when map is loaded') }, ], ] - const slideshowBuilder = new DataForm(this, slideshowFields, { + const slideshowBuilder = new MutatingForm(this, slideshowFields, { callback: () => { this.slideshow.load() // FIXME when we refactor formbuilder: this callback is called in a 'postsync' @@ -1043,7 +1043,7 @@ export default class Umap extends ServerStored { _editSync(container) { const sync = DomUtil.createFieldset(container, translate('Real-time collaboration')) - const builder = new DataForm(this, ['properties.syncEnabled'], { + const builder = new MutatingForm(this, ['properties.syncEnabled'], { umap: this, }) sync.appendChild(builder.build()) @@ -1462,7 +1462,7 @@ export default class Umap extends ServerStored { const row = DomUtil.create('li', 'orderable', ul) DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder')) datalayer.renderToolbox(row) - const builder = new DataForm( + const builder = new MutatingForm( datalayer, [['options.name', { handler: 'EditableText' }]], { className: 'umap-form-inline' } diff --git a/umap/tests/integration/test_edit_map.py b/umap/tests/integration/test_edit_map.py index 6328d6997..5dc65ad93 100644 --- a/umap/tests/integration/test_edit_map.py +++ b/umap/tests/integration/test_edit_map.py @@ -60,8 +60,8 @@ def test_zoomcontrol_impacts_ui(live_server, page, tilelayer): # Hide them page.get_by_text("User interface options").click() hide_zoom_controls = ( - page.locator("div") - .filter(has_text=re.compile(r"^Display the zoom control")) + page.locator(".panel") + .filter(has_text=re.compile("Display the zoom control")) .locator("label") .nth(2) )