diff --git a/package.json b/package.json
index b20caee12..adf552b6e 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,6 @@
     "leaflet": "1.9.4",
     "leaflet-editable": "^1.3.0",
     "leaflet-editinosm": "0.2.3",
-    "leaflet-formbuilder": "0.2.10",
     "leaflet-fullscreen": "1.0.2",
     "leaflet-hash": "0.2.1",
     "leaflet-i18n": "0.3.5",
diff --git a/scripts/vendorsjs.sh b/scripts/vendorsjs.sh
index ca722a2f6..7508460c5 100755
--- a/scripts/vendorsjs.sh
+++ b/scripts/vendorsjs.sh
@@ -17,7 +17,6 @@ mkdir -p umap/static/umap/vendors/markercluster/ && cp -r node_modules/leaflet.m
 mkdir -p umap/static/umap/vendors/heat/ && cp -r node_modules/leaflet.heat/dist/leaflet-heat.js umap/static/umap/vendors/heat/
 mkdir -p umap/static/umap/vendors/fullscreen/ && cp -r node_modules/leaflet-fullscreen/dist/** umap/static/umap/vendors/fullscreen/
 mkdir -p umap/static/umap/vendors/toolbar/ && cp -r node_modules/leaflet-toolbar/dist/leaflet.toolbar.* umap/static/umap/vendors/toolbar/
-mkdir -p umap/static/umap/vendors/formbuilder/ && cp -r node_modules/leaflet-formbuilder/Leaflet.FormBuilder.js umap/static/umap/vendors/formbuilder/
 mkdir -p umap/static/umap/vendors/measurable/ && cp -r node_modules/leaflet-measurable/Leaflet.Measurable.* umap/static/umap/vendors/measurable/
 mkdir -p umap/static/umap/vendors/photon/ && cp -r node_modules/leaflet.photon/leaflet.photon.js umap/static/umap/vendors/photon/
 mkdir -p umap/static/umap/vendors/csv2geojson/ && cp -r node_modules/csv2geojson/csv2geojson.js umap/static/umap/vendors/csv2geojson/
diff --git a/umap/static/umap/css/form.css b/umap/static/umap/css/form.css
index 10f12f2c4..e4ad44272 100644
--- a/umap/static/umap/css/form.css
+++ b/umap/static/umap/css/form.css
@@ -1,3 +1,4 @@
+.umap-form-inline .formbox,
 .umap-form-inline {
     display: inline;
 }
@@ -381,16 +382,19 @@ input.switch:checked ~ label:after {
     box-shadow: inset 0 0 6px 0px #2c3233;
     color: var(--color-darkGray);
 }
-.inheritable .header,
-.inheritable {
-    clear: both;
-    overflow: hidden;
+.inheritable .header .buttons {
+    padding: 0;
 }
 .inheritable .header {
     margin-bottom: 5px;
+    display: flex;
+    align-items: center;
+    align-content: center;
+    justify-content: space-between;
 }
 .inheritable .header label {
     padding-top: 6px;
+    width: initial;
 }
 .inheritable + .inheritable {
     border-top: 1px solid #222;
@@ -400,22 +404,11 @@ input.switch:checked ~ label:after {
 .umap-field-iconUrl .action-button,
 .inheritable .define,
 .inheritable .undefine {
-    float: inline-end;
     width: initial;
     min-height: 18px;
     line-height: 18px;
     margin-bottom: 0;
 }
-.inheritable .quick-actions {
-    float: inline-end;
-}
-.inheritable .quick-actions .formbox {
-    margin-bottom: 0;
-}
-.inheritable .quick-actions input {
-    width: 100px;
-    margin-inline-end: 5px;
-}
 .inheritable .define,
 .inheritable.undefined .undefine,
 .inheritable.undefined .show-on-defined {
@@ -493,12 +486,15 @@ i.info {
     padding: 0 5px;
 }
 .flat-tabs {
-    display: flex;
+    display: none;
     justify-content: space-around;
     font-size: 1.2em;
     margin-bottom: 20px;
     border-bottom: 1px solid #bebebe;
 }
+.flat-tabs:has(.flat) {
+    display: flex;
+}
 .flat-tabs button {
     padding: 10px;
     text-decoration: none;
@@ -534,7 +530,7 @@ i.info {
     background-color: #999;
     text-align: center;
     margin-bottom: 5px;
-    display: block;
+    display: inline-block;
     color: black;
     font-weight: bold;
 }
@@ -559,7 +555,6 @@ i.info {
     clear: both;
     margin-bottom: 20px;
     overflow: hidden;
-    display: none;
 }
 .umap-color-picker span {
     width: 20px;
diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js
index 3faef4cc8..4c2f9d602 100644
--- a/umap/static/umap/js/modules/browser.js
+++ b/umap/static/umap/js/modules/browser.js
@@ -4,6 +4,7 @@ import * as Icon from './rendering/icon.js'
 import * as Utils from './utils.js'
 import { EXPORT_FORMATS } from './formatter.js'
 import ContextMenu from './ui/contextmenu.js'
+import { Form } from './form/builder.js'
 
 export default class Browser {
   constructor(umap, leafletMap) {
@@ -179,9 +180,8 @@ export default class Browser {
       ],
       ['options.inBbox', { handler: 'Switch', label: translate('Current map view') }],
     ]
-    const builder = new L.FormBuilder(this, fields, {
-      callback: () => this.onFormChange(),
-    })
+    const builder = new Form(this, fields)
+    builder.on('set', () => this.onFormChange())
     let filtersBuilder
     this.formContainer.appendChild(builder.build())
     DomEvent.on(builder.form, 'reset', () => {
@@ -189,9 +189,8 @@ export default class Browser {
     })
     if (this._umap.properties.facetKey) {
       fields = this._umap.facets.build()
-      filtersBuilder = new L.FormBuilder(this._umap.facets, fields, {
-        callback: () => this.onFormChange(),
-      })
+      filtersBuilder = new Form(this._umap.facets, fields)
+      filtersBuilder.on('set', () => this.onFormChange())
       DomEvent.on(filtersBuilder.form, 'reset', () => {
         window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder))
       })
diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js
index 0035f5302..18ce98f95 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,15 +226,11 @@ class Feature {
       `icon-${this.getClassName()}`
     )
 
-    let builder = new U.FormBuilder(
-      this,
-      [['datalayer', { handler: 'DataLayerSwitcher' }]],
-      {
-        callback() {
-          this.edit(event)
-        }, // removeLayer step will close the edit panel, let's reopen it
-      }
-    )
+    let builder = new MutatingForm(this, [
+      ['datalayer', { handler: 'DataLayerSwitcher' }],
+    ])
+    // removeLayer step will close the edit panel, let's reopen it
+    builder.on('set', () => this.edit(event))
     container.appendChild(builder.build())
 
     const properties = []
@@ -254,7 +251,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 +282,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 +292,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 +302,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,16 +730,15 @@ 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, {
-      callback: () => {
-        if (!this.ui._latlng.isValid()) {
-          Alert.error(translate('Invalid latitude or longitude'))
-          builder.restoreField('ui._latlng.lat')
-          builder.restoreField('ui._latlng.lng')
-        }
-        this.pullGeometry()
-        this.zoomTo({ easing: false })
-      },
+    const builder = new MutatingForm(this, coordinatesOptions)
+    builder.on('set', () => {
+      if (!this.ui._latlng.isValid()) {
+        Alert.error(translate('Invalid latitude or longitude'))
+        builder.restoreField('ui._latlng.lat')
+        builder.restoreField('ui._latlng.lng')
+      }
+      this.pullGeometry()
+      this.zoomTo({ easing: false })
     })
     const fieldset = DomUtil.createFieldset(container, translate('Coordinates'))
     fieldset.appendChild(builder.build())
diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js
index d1be28e9f..bb0f18f93 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,
@@ -659,7 +658,7 @@ export class DataLayer extends ServerStored {
         {
           label: translate('Data is browsable'),
           handler: 'Switch',
-          helpEntries: 'browsable',
+          helpEntries: ['browsable'],
         },
       ],
       [
@@ -671,20 +670,19 @@ export class DataLayer extends ServerStored {
       ],
     ]
     DomUtil.createTitle(container, translate('Layer properties'), 'icon-layers')
-    let builder = new U.FormBuilder(this, metadataFields, {
-      callback(e) {
-        this._umap.onDataLayersChanged()
-        if (e.helper.field === 'options.type') {
-          this.edit()
-        }
-      },
+    let builder = new MutatingForm(this, metadataFields)
+    builder.on('set', ({ detail }) => {
+      this._umap.onDataLayersChanged()
+      if (detail.helper.field === 'options.type') {
+        this.edit()
+      }
     })
     container.appendChild(builder.build())
 
     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(
@@ -707,7 +705,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(
@@ -724,7 +722,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(
@@ -743,7 +741,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')
@@ -799,7 +797,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
new file mode 100644
index 000000000..ea78a10f6
--- /dev/null
+++ b/umap/static/umap/js/modules/form/builder.js
@@ -0,0 +1,241 @@
+import getClass from './fields.js'
+import * as Utils from '../utils.js'
+import { SCHEMA } from '../schema.js'
+import { translate } from '../i18n.js'
+
+export class Form extends Utils.WithEvents {
+  constructor(obj, fields, properties) {
+    super()
+    this.setProperties(properties)
+    this.defaultProperties = {}
+    this.obj = obj
+    this.form = Utils.loadTemplate('<form></form>')
+    this.setFields(fields)
+    if (this.properties.id) {
+      this.form.id = this.properties.id
+    }
+    if (this.properties.className) {
+      this.form.classList.add(...this.properties.className.split(' '))
+    }
+  }
+
+  setProperties(properties) {
+    this.properties = Object.assign({}, this.properties, properties)
+  }
+
+  setFields(fields) {
+    this.fields = fields || []
+    this.helpers = {}
+  }
+
+  build() {
+    this.form.innerHTML = ''
+    for (const definition of this.fields) {
+      this.buildField(this.makeField(definition))
+    }
+    return this.form
+  }
+
+  buildField(field) {
+    field.buildTemplate()
+    field.build()
+  }
+
+  makeField(field) {
+    // field can be either a string like "option.name" or a full definition array,
+    // like ['properties.tilelayer.tms', {handler: 'CheckBox', helpText: 'TMS format'}]
+    let properties
+    if (Array.isArray(field)) {
+      properties = field[1] || {}
+      field = field[0]
+    } else {
+      properties = this.defaultProperties[this.getName(field)] || {}
+    }
+    const class_ = getClass(properties.handler || 'Input')
+    this.helpers[field] = new class_(this, field, properties)
+    return this.helpers[field]
+  }
+
+  getter(field) {
+    const path = field.split('.')
+    let value = this.obj
+    for (const sub of path) {
+      try {
+        value = value[sub]
+      } catch {
+        console.log(field)
+      }
+    }
+    return value
+  }
+
+  setter(field, value) {
+    const path = field.split('.')
+    let obj = this.obj
+    let what
+    for (let i = 0, l = path.length; i < l; i++) {
+      what = path[i]
+      if (what === path[l - 1]) {
+        if (typeof value === 'undefined') {
+          delete obj[what]
+        } else {
+          obj[what] = value
+        }
+      } else {
+        obj = obj[what]
+      }
+    }
+  }
+
+  restoreField(field) {
+    const initial = this.helpers[field].initial
+    this.setter(field, initial)
+  }
+
+  getName(field) {
+    const fieldEls = field.split('.')
+    return fieldEls[fieldEls.length - 1]
+  }
+
+  fetchAll() {
+    for (const helper of Object.values(this.helpers)) {
+      helper.fetch()
+    }
+  }
+
+  syncAll() {
+    for (const helper of Object.values(this.helpers)) {
+      helper.sync()
+    }
+  }
+
+  onPostSync(helper) {
+    if (this.properties.callback) {
+      this.properties.callback(helper)
+    }
+  }
+
+  finish() {}
+
+  getTemplate(helper) {
+    return `
+      <div class="formbox" data-ref=container>
+        ${helper.getTemplate()}
+        <small class="help-text" data-ref=helpText></small>
+      </div>`
+  }
+}
+
+export class MutatingForm extends Form {
+  constructor(obj, fields, properties) {
+    super(obj, fields, properties)
+    this._umap = obj._umap || properties.umap
+    this.computeDefaultProperties()
+    // this.on('finish', this.finish)
+  }
+
+  computeDefaultProperties() {
+    const customHandlers = {
+      sortKey: 'PropertyInput',
+      easing: 'Switch',
+      facetKey: 'PropertyInput',
+      slugKey: 'PropertyInput',
+      labelKey: 'PropertyInput',
+    }
+    for (const [key, schema] of Object.entries(SCHEMA)) {
+      if (schema.type === Boolean) {
+        if (schema.nullable) schema.handler = 'NullableChoices'
+        else schema.handler = 'Switch'
+      } else if (schema.type === 'Text') {
+        schema.handler = 'Textarea'
+      } else if (schema.type === Number) {
+        if (schema.step) schema.handler = 'Range'
+        else schema.handler = 'IntInput'
+      } else if (schema.choices) {
+        const text_length = schema.choices.reduce(
+          (acc, [_, label]) => acc + label.length,
+          0
+        )
+        // Try to be smart and use MultiChoice only
+        // for choices where labels are shorts…
+        if (text_length < 40) {
+          schema.handler = 'MultiChoice'
+        } else {
+          schema.handler = 'Select'
+          schema.selectOptions = schema.choices
+        }
+      } else {
+        switch (key) {
+          case 'color':
+          case 'fillColor':
+            schema.handler = 'ColorPicker'
+            break
+          case 'iconUrl':
+            schema.handler = 'IconUrl'
+            break
+          case 'licence':
+            schema.handler = 'LicenceChooser'
+            break
+        }
+      }
+
+      if (customHandlers[key]) {
+        schema.handler = customHandlers[key]
+      }
+      // Input uses this key for its type attribute
+      delete schema.type
+      this.defaultProperties[key] = schema
+    }
+  }
+
+  setter(field, value) {
+    super.setter(field, value)
+    this.obj.isDirty = true
+    if ('render' in this.obj) {
+      this.obj.render([field], this)
+    }
+    if ('sync' in this.obj) {
+      this.obj.sync.update(field, value)
+    }
+  }
+
+  getTemplate(helper) {
+    let template
+    if (helper.properties.inheritable) {
+      const extraClassName = helper.get(true) === undefined ? ' undefined' : ''
+      template = `
+        <div class="umap-field-${helper.name} formbox inheritable${extraClassName}">
+          <div class="header" data-ref=header>
+            ${helper.getLabelTemplate()}
+            <span class="actions show-on-defined" data-ref=actions></span>
+            <span class="buttons" data-ref=buttons>
+              <button type="button" class="button undefine" data-ref=undefine>${translate('clear')}</button>
+              <button type="button" class="button define" data-ref=define>${translate('define')}</button>
+            </span>
+          </div>
+          <div class="show-on-defined" data-ref=container>
+            ${helper.getTemplate()}
+            <small class="help-text" data-ref=helpText></small>
+          </div>
+        </div>`
+    } else {
+      template = `
+      <div class="formbox umap-field-${helper.name}" data-ref=container>
+        ${helper.getLabelTemplate()}
+        ${helper.getTemplate()}
+        <small class="help-text" data-ref=helpText></small>
+      </div>`
+    }
+    return template
+  }
+
+  build() {
+    super.build()
+    this._umap.help.parse(this.form)
+    return this.form
+  }
+
+  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
new file mode 100644
index 000000000..1ff8b5b0a
--- /dev/null
+++ b/umap/static/umap/js/modules/form/fields.js
@@ -0,0 +1,1330 @@
+import * as Utils from '../utils.js'
+import { translate } from '../i18n.js'
+import {
+  AjaxAutocomplete,
+  AjaxAutocompleteMultiple,
+  AutocompleteDatalist,
+} from '../autocomplete.js'
+import { SCHEMA } from '../schema.js'
+import * as Icon from '../rendering/icon.js'
+
+const Fields = {}
+
+export default function getClass(name) {
+  if (typeof name === 'function') return name
+  if (!Fields[name]) throw Error(`Unknown class ${name}`)
+  return Fields[name]
+}
+
+class BaseElement {
+  constructor(builder, field, properties) {
+    this.builder = builder
+    this.obj = this.builder.obj
+    this.form = this.builder.form
+    this.field = field
+    this.setProperties(properties)
+    this.fieldEls = this.field.split('.')
+    this.name = this.builder.getName(field)
+    this.id = `${this.builder.properties.id || Date.now()}.${this.name}`
+  }
+
+  getDefaultProperties() {
+    return {}
+  }
+
+  setProperties(properties) {
+    this.properties = Object.assign(
+      this.getDefaultProperties(),
+      this.properties,
+      properties
+    )
+  }
+
+  onDefine() {}
+
+  buildTemplate() {
+    const template = this.builder.getTemplate(this)
+    const [root, elements] = Utils.loadTemplateWithRefs(template)
+    this.root = root
+    this.elements = elements
+    this.container = elements.container
+    this.form.appendChild(this.root)
+  }
+
+  getTemplate() {
+    return ''
+  }
+
+  build() {
+    if (this.properties.helpText) {
+      this.elements.helpText.textContent = this.properties.helpText
+    } else {
+      this.elements.helpText.hidden = true
+    }
+
+    if (this.elements.define) {
+      this.elements.define.addEventListener('click', (event) => {
+        event.preventDefault()
+        event.stopPropagation()
+        this.fetch()
+        this.onDefine()
+        this.root.classList.remove('undefined')
+      })
+    }
+    if (this.elements.undefine) {
+      this.elements.undefine.addEventListener('click', () => this.undefine())
+    }
+  }
+
+  clear() {
+    this.input.value = ''
+  }
+
+  get(own) {
+    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) || SCHEMA[key]?.default
+  }
+
+  toHTML() {
+    return this.get()
+  }
+
+  toJS() {
+    return this.value()
+  }
+
+  sync() {
+    this.set()
+    this.builder.fire('set', { helper: this })
+  }
+
+  set() {
+    this.builder.setter(this.field, this.toJS())
+  }
+
+  getLabelTemplate() {
+    const label = this.properties.label
+    const help = this.properties.helpEntries?.join() || ''
+    return label
+      ? `<label title="${label}" data-ref=label data-help="${help}">${label}</label>`
+      : ''
+  }
+
+  fetch() {}
+
+  finish() {}
+
+  undefine() {
+    this.root.classList.add('undefined')
+    this.clear()
+    this.sync()
+  }
+}
+
+Fields.Textarea = class extends BaseElement {
+  getTemplate() {
+    return `<textarea placeholder="${this.properties.placeholder || ''}" data-ref=textarea></textarea>`
+  }
+
+  build() {
+    super.build()
+    this.textarea = this.elements.textarea
+    this.fetch()
+    this.textarea.addEventListener('input', () => this.sync())
+    this.textarea.addEventListener('keypress', (event) => this.onKeyPress(event))
+  }
+
+  fetch() {
+    const value = this.toHTML()
+    this.initial = value
+    if (value) {
+      this.textarea.value = value
+    }
+  }
+
+  value() {
+    return this.textarea.value
+  }
+
+  onKeyPress(event) {
+    if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) {
+      event.stopPropagation()
+      event.preventDefault()
+      this.finish()
+    }
+  }
+}
+
+Fields.Input = class extends BaseElement {
+  getTemplate() {
+    return `<input type="${this.type()}" name="${this.name}" placeholder="${this.properties.placeholder || ''}" data-ref=input />`
+  }
+
+  build() {
+    super.build()
+    this.input = this.elements.input
+    this.input._helper = this
+    if (this.properties.className) {
+      this.input.classList.add(this.properties.className)
+    }
+    if (this.properties.min !== undefined) {
+      this.input.min = this.properties.min
+    }
+    if (this.properties.max !== undefined) {
+      this.input.max = this.properties.max
+    }
+    if (this.properties.step) {
+      this.input.step = this.properties.step
+    }
+    this.fetch()
+    this.input.addEventListener(this.getSyncEvent(), () => this.sync())
+    this.input.addEventListener('keydown', (event) => this.onKeyDown(event))
+  }
+
+  fetch() {
+    const value = this.toHTML() !== undefined ? this.toHTML() : null
+    this.initial = value
+    this.input.value = value
+  }
+
+  getSyncEvent() {
+    return 'input'
+  }
+
+  type() {
+    return this.properties.type || 'text'
+  }
+
+  value() {
+    return this.input.value || undefined
+  }
+
+  onKeyDown(event) {
+    if (event.key === 'Enter') {
+      event.stopPropagation()
+      event.preventDefault()
+      this.finish()
+      this.input.blur()
+    }
+  }
+}
+
+Fields.BlurInput = class extends Fields.Input {
+  getSyncEvent() {
+    return 'blur'
+  }
+
+  getTemplate() {
+    return `${super.getTemplate()}<span class="button blur-button"></span>`
+  }
+
+  build() {
+    this.properties.className = 'blur'
+    super.build()
+    this.input.addEventListener('focus', () => this.fetch())
+  }
+
+  finish() {
+    this.sync()
+    super.finish()
+  }
+
+  sync() {
+    // Do not commit any change if user only clicked
+    // on the field than clicked outside
+    if (this.initial !== this.value()) {
+      super.sync()
+    }
+  }
+}
+const IntegerMixin = (Base) =>
+  class extends Base {
+    value() {
+      return !isNaN(this.input.value) && this.input.value !== ''
+        ? parseInt(this.input.value, 10)
+        : undefined
+    }
+
+    type() {
+      return 'number'
+    }
+  }
+
+Fields.IntInput = class extends IntegerMixin(Fields.Input) {}
+Fields.BlurIntInput = class extends IntegerMixin(Fields.BlurInput) {}
+
+const FloatMixin = (Base) =>
+  class extends Base {
+    value() {
+      return !isNaN(this.input.value) && this.input.value !== ''
+        ? parseFloat(this.input.value)
+        : undefined
+    }
+
+    type() {
+      return 'number'
+    }
+  }
+
+Fields.FloatInput = class extends FloatMixin(Fields.Input) {
+  // TODO use public class properties when in baseline
+  getDefaultProperties() {
+    return { step: 'any' }
+  }
+}
+
+Fields.BlurFloatInput = class extends FloatMixin(Fields.BlurInput) {
+  getDefaultProperties() {
+    return { step: 'any' }
+  }
+}
+
+Fields.CheckBox = class extends BaseElement {
+  getTemplate() {
+    return `<input type=checkbox name="${this.name}" data-ref=input />`
+  }
+
+  build() {
+    this.input = this.elements.input
+    this.input._helper = this
+    this.fetch()
+    this.input.addEventListener('change', () => this.sync())
+    super.build()
+  }
+
+  fetch() {
+    this.initial = this.toHTML()
+    this.input.checked = this.initial === true
+  }
+
+  value() {
+    return this.root.classList.contains('undefined') ? undefined : this.input.checked
+  }
+
+  toHTML() {
+    return [1, true].indexOf(this.get()) !== -1
+  }
+
+  clear() {
+    this.fetch()
+  }
+}
+
+Fields.Select = class extends BaseElement {
+  getTemplate() {
+    return `<select name="${this.name}" data-ref=select></select>`
+  }
+
+  build() {
+    this.select = this.elements.select
+    this.validValues = []
+    this.buildOptions()
+    this.select.addEventListener('change', () => this.sync())
+    super.build()
+  }
+
+  getOptions() {
+    return this.properties.selectOptions
+  }
+
+  fetch() {
+    this.buildOptions()
+  }
+
+  buildOptions() {
+    this.select.innerHTML = ''
+    for (const option of this.getOptions()) {
+      if (typeof option === 'string') this.buildOption(option, option)
+      else this.buildOption(option[0], option[1])
+    }
+  }
+
+  buildOption(value, label) {
+    this.validValues.push(value)
+    const option = Utils.loadTemplate('<option></option>')
+    this.select.appendChild(option)
+    option.value = value
+    option.textContent = label
+    if (this.toHTML() === value) {
+      option.selected = 'selected'
+    }
+  }
+
+  value() {
+    if (this.select[this.select.selectedIndex]) {
+      return this.select[this.select.selectedIndex].value
+    }
+  }
+
+  getDefault() {
+    if (this.properties.inheritable) return undefined
+    return this.getOptions()[0][0]
+  }
+
+  toJS() {
+    const value = this.value()
+    if (this.validValues.indexOf(value) !== -1) {
+      return value
+    }
+    return this.getDefault()
+  }
+
+  clear() {
+    this.select.value = ''
+  }
+}
+
+Fields.IntSelect = class extends Fields.Select {
+  value() {
+    return parseInt(super.value(), 10)
+  }
+}
+
+Fields.EditableText = class extends BaseElement {
+  getTemplate() {
+    return `<span contentEditable class="${this.properties.className || ''}" data-ref=input></span>`
+  }
+
+  buildTemplate() {
+    // No wrapper at all
+    const template = this.getTemplate()
+    this.input = Utils.loadTemplate(template)
+    this.form.appendChild(this.input)
+  }
+
+  build() {
+    this.fetch()
+    this.input.addEventListener('input', () => this.sync())
+    this.input.addEventListener('keypress', (event) => this.onKeyPress(event))
+  }
+
+  value() {
+    return this.input.textContent
+  }
+
+  fetch() {
+    this.input.textContent = this.toHTML()
+  }
+
+  onKeyPress(event) {
+    if (event.keyCode === 13) {
+      event.preventDefault()
+      this.input.blur()
+    }
+  }
+}
+
+Fields.ColorPicker = class extends Fields.Input {
+  getColors() {
+    return Utils.COLORS
+  }
+
+  getDefaultProperties() {
+    return {
+      placeholder: translate('Inherit'),
+    }
+  }
+
+  getTemplate() {
+    return `${super.getTemplate()}<div class="umap-color-picker" hidden data-ref=colors></div>`
+  }
+
+  build() {
+    super.build()
+    for (const color of this.getColors()) {
+      this.addColor(color)
+    }
+    this.spreadColor()
+    this.input.autocomplete = 'off'
+    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() {
+    this.showPicker()
+    this.spreadColor()
+  }
+
+  showPicker() {
+    this.elements.colors.hidden = false
+  }
+
+  closePicker() {
+    this.elements.colors.hidden = true
+  }
+
+  onBlur() {
+    // We must leave time for the click to be listened.
+    window.setTimeout(() => this.closePicker(), 100)
+  }
+
+  sync() {
+    this.spreadColor()
+    super.sync()
+  }
+
+  spreadColor() {
+    if (this.input.value) this.input.style.backgroundColor = this.input.value
+    else this.input.style.backgroundColor = 'inherit'
+  }
+
+  addColor(colorName) {
+    const span = Utils.loadTemplate('<span></span>')
+    this.elements.colors.appendChild(span)
+    span.style.backgroundColor = span.title = colorName
+    const updateColorInput = () => {
+      this.input.value = colorName
+      this.sync()
+      this.closePicker()
+    }
+    span.addEventListener('mousedown', updateColorInput)
+  }
+}
+
+Fields.TextColorPicker = class extends Fields.ColorPicker {
+  getColors() {
+    return [
+      'Black',
+      'DarkSlateGrey',
+      'DimGrey',
+      'SlateGrey',
+      'LightSlateGrey',
+      'Grey',
+      'DarkGrey',
+      'LightGrey',
+      'White',
+    ]
+  }
+}
+
+Fields.LayerTypeChooser = class extends Fields.Select {
+  getOptions() {
+    return U.LAYER_TYPES.map((class_) => [class_.TYPE, class_.NAME])
+  }
+}
+
+Fields.SlideshowDelay = class extends Fields.IntSelect {
+  getOptions() {
+    const options = []
+    for (let i = 1; i < 30; i++) {
+      options.push([i * 1000, translate('{delay} seconds', { delay: i })])
+    }
+    return options
+  }
+}
+
+Fields.DataLayerSwitcher = class extends Fields.Select {
+  getOptions() {
+    const options = []
+    this.builder._umap.eachDataLayerReverse((datalayer) => {
+      if (
+        datalayer.isLoaded() &&
+        !datalayer.isDataReadOnly() &&
+        datalayer.isBrowsable()
+      ) {
+        options.push([L.stamp(datalayer), datalayer.getName()])
+      }
+    })
+    return options
+  }
+
+  toHTML() {
+    return L.stamp(this.obj.datalayer)
+  }
+
+  toJS() {
+    return this.builder._umap.datalayers[this.value()]
+  }
+
+  set() {
+    this.builder._umap.lastUsedDataLayer = this.toJS()
+    this.obj.changeDataLayer(this.toJS())
+  }
+}
+
+Fields.DataFormat = class extends Fields.Select {
+  getOptions() {
+    return [
+      [undefined, translate('Choose the data format')],
+      ['geojson', 'geojson'],
+      ['osm', 'osm'],
+      ['csv', 'csv'],
+      ['gpx', 'gpx'],
+      ['kml', 'kml'],
+      ['georss', 'georss'],
+    ]
+  }
+}
+
+Fields.LicenceChooser = class extends Fields.Select {
+  getOptions() {
+    const licences = []
+    const licencesList = this.builder.obj.properties.licences
+    let licence
+    for (const i in licencesList) {
+      licence = licencesList[i]
+      licences.push([i, licence.name])
+    }
+    return licences
+  }
+
+  toHTML() {
+    return this.get()?.name
+  }
+
+  toJS() {
+    return this.builder.obj.properties.licences[this.value()]
+  }
+}
+
+Fields.NullableBoolean = class extends Fields.Select {
+  getOptions() {
+    return [
+      [undefined, translate('inherit')],
+      [true, translate('yes')],
+      [false, translate('no')],
+    ]
+  }
+
+  toJS() {
+    let value = this.value()
+    switch (value) {
+      case 'true':
+      case true:
+        value = true
+        break
+      case 'false':
+      case false:
+        value = false
+        break
+      default:
+        value = undefined
+    }
+    return value
+  }
+}
+
+// Adds an autocomplete using all available user defined properties
+Fields.PropertyInput = class extends Fields.BlurInput {
+  build() {
+    super.build()
+    const autocomplete = new AutocompleteDatalist(this.input)
+    // Will be used on Umap and DataLayer
+    const properties = this.builder.obj.allProperties()
+    autocomplete.suggestions = properties
+  }
+}
+
+Fields.IconUrl = class extends Fields.BlurInput {
+  type() {
+    return 'hidden'
+  }
+
+  getTemplate() {
+    return `
+      <div>
+        <div class="flat-tabs" data-ref=tabs></div>
+        <div class="umap-pictogram-body" data-ref=body>
+        ${super.getTemplate()}
+        </div>
+        <div data-ref=footer></div>
+      </div>
+    `
+  }
+
+  build() {
+    super.build()
+    this.tabs = this.elements.tabs
+    this.body = this.elements.body
+    this.footer = this.elements.footer
+    this.button = Utils.loadTemplate(
+      `<button type="button" class="button action-button" hidden>${translate('Change')}</button>`
+    )
+    this.button.addEventListener('click', () => this.onDefine())
+    this.elements.buttons.appendChild(this.button)
+    this.updatePreview()
+  }
+
+  async onDefine() {
+    this.footer.innerHTML = ''
+    const [{ pictogram_list }, response, error] = await this.builder._umap.server.get(
+      this.builder._umap.properties.urls.pictogram_list_json
+    )
+    if (!error) this.pictogram_list = pictogram_list
+    this.buildTabs()
+    const value = this.value()
+    if (Icon.RECENT.length) this.showRecentTab()
+    else if (!value || Utils.isPath(value)) this.showSymbolsTab()
+    else if (Utils.isRemoteUrl(value) || Utils.isDataImage(value)) this.showURLTab()
+    else this.showCharsTab()
+    const closeButton = Utils.loadTemplate(
+      `<button type="button" class="button action-button">${translate('Close')}</button>`
+    )
+    closeButton.addEventListener('click', (event) => {
+      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 = ''
+    // Useless div, but loadTemplate needs a root element
+    const [root, { recent, symbols, chars, url }] = Utils.loadTemplateWithRefs(`
+      <div>
+        <button class="flat tab-recent" data-ref=recent>${translate('Recent')}</button>
+        <button class="flat tab-symbols" data-ref=symbols>${translate('Symbol')}</button>
+        <button class="flat tab-chars" data-ref=chars>${translate('Emoji & Character')}</button>
+        <button class="flat tab-url" data-ref=url>${translate('URL')}</button>
+      </div>
+    `)
+    this.tabs.appendChild(root)
+    if (Icon.RECENT.length) {
+      recent.addEventListener('click', (event) => {
+        event.stopPropagation()
+        event.preventDefault()
+        this.showRecentTab()
+      })
+    } else {
+      recent.hidden = true
+    }
+    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) {
+      el.classList.remove('on')
+    }
+    const el = this.tabs.querySelector(`.tab-${name}`)
+    el.classList.add('on')
+    this.body.innerHTML = ''
+  }
+
+  updatePreview() {
+    this.elements.actions.innerHTML = ''
+    this.button.hidden = !this.value() || this.isDefault()
+    if (this.isDefault()) return
+    if (!Utils.hasVar(this.value())) {
+      // Do not try to render URL with variables
+      const box = Utils.loadTemplate('<div class="umap-pictogram-choice"></div>')
+      this.elements.actions.appendChild(box)
+      box.addEventListener('click', () => this.onDefine())
+      const icon = Icon.makeElement(this.value(), box)
+    }
+  }
+
+  addIconPreview(pictogram, parent) {
+    const baseClass = 'umap-pictogram-choice'
+    const value = pictogram.src
+    const search = Utils.normalize(this.searchInput.value)
+    const title = pictogram.attribution
+      ? `${pictogram.name} — © ${pictogram.attribution}`
+      : pictogram.name || pictogram.src
+    if (search && Utils.normalize(title).indexOf(search) === -1) return
+    const className = value === this.value() ? `${baseClass} selected` : baseClass
+    const container = Utils.loadTemplate(
+      `<div class="${className}" title="${title}"></div>`
+    )
+    parent.appendChild(container)
+    Icon.makeElement(value, container)
+    container.addEventListener('click', () => {
+      this.input.value = value
+      this.sync()
+      this.unselectAll(this.grid)
+      container.classList.add('selected')
+      this.updatePreview()
+    })
+    return true // Icon has been added (not filtered)
+  }
+
+  clear() {
+    this.input.value = ''
+    this.unselectAll(this.body)
+    this.sync()
+    this.body.innerHTML = ''
+    this.updatePreview()
+  }
+
+  addCategory(items, name) {
+    const [parent, { grid }] = Utils.loadTemplateWithRefs(`
+      <div class="umap-pictogram-category">
+        <h6 hidden=${!name}>${name}</h6>
+        <div class="umap-pictogram-grid" data-ref=grid></div>
+      </div>
+    `)
+    let hasIcons = false
+    for (const item of items) {
+      hasIcons = this.addIconPreview(item, grid) || hasIcons
+    }
+    if (hasIcons) this.grid.appendChild(parent)
+  }
+
+  buildSymbolsList() {
+    this.grid.innerHTML = ''
+    const categories = {}
+    let category
+    for (const props of this.pictogram_list) {
+      category = props.category || translate('Generic')
+      categories[category] = categories[category] || []
+      categories[category].push(props)
+    }
+    const sorted = Object.entries(categories).toSorted(([a], [b]) =>
+      Utils.naturalSort(a, b, U.lang)
+    )
+    for (const [name, items] of sorted) {
+      this.addCategory(items, name)
+    }
+  }
+
+  buildRecentList() {
+    this.grid.innerHTML = ''
+    const items = U.Icon.RECENT.map((src) => ({
+      src,
+    }))
+    this.addCategory(items)
+  }
+
+  isDefault() {
+    return !this.value() || this.value() === SCHEMA.iconUrl.default
+  }
+
+  addGrid(onSearch) {
+    this.searchInput = Utils.loadTemplate(
+      `<input type="search" placeholder="${translate('Search')}" />`
+    )
+    this.grid = Utils.loadTemplate('<div></div>')
+    this.body.appendChild(this.searchInput)
+    this.body.appendChild(this.grid)
+    this.searchInput.addEventListener('input', onSearch)
+  }
+
+  showRecentTab() {
+    if (!Icon.RECENT.length) return
+    this.openTab('recent')
+    this.addGrid(() => this.buildRecentList())
+    this.buildRecentList()
+  }
+
+  showSymbolsTab() {
+    this.openTab('symbols')
+    this.addGrid(() => this.buildSymbolsList())
+    this.buildSymbolsList()
+  }
+
+  showCharsTab() {
+    this.openTab('chars')
+    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'
+  }
+
+  showURLTab() {
+    this.openTab('url')
+    const value =
+      Utils.isRemoteUrl(this.value()) || Utils.isDataImage(this.value())
+        ? this.value()
+        : null
+    const input = this.buildInput(this.body, value)
+    input.placeholder = translate('Add image URL')
+    input.type = 'url'
+  }
+
+  buildInput(parent, value) {
+    const input = Utils.loadTemplate('<input class="blur" />')
+    const button = Utils.loadTemplate('<span class="button blur-button"></span>')
+    parent.appendChild(input)
+    parent.appendChild(button)
+    if (value) input.value = value
+    input.addEventListener('blur', () => {
+      // Do not clear this.input when focus-blur
+      // empty input
+      if (input.value === value) return
+      this.input.value = input.value
+      this.sync()
+    })
+    return input
+  }
+
+  unselectAll(container) {
+    for (const el of container.querySelectorAll('div.selected')) {
+      el.classList.remove('selected')
+    }
+  }
+}
+
+Fields.Url = class extends Fields.Input {
+  type() {
+    return 'url'
+  }
+}
+
+Fields.Switch = class extends Fields.CheckBox {
+  getTemplate() {
+    const label = this.properties.label
+    return `${super.getTemplate()}<label title="${label}" for="${this.id}" data-ref=customLabel>${label}</label>`
+  }
+
+  build() {
+    super.build()
+    // We have it in our template
+    if (!this.properties.inheritable) {
+      // We already have the label near the switch,
+      // only show the default label in inheritable mode
+      // as the switch itself may be hidden (until "defined")
+      if (this.elements.label) {
+        this.elements.label.hidden = true
+        this.elements.label.innerHTML = ''
+        this.elements.label.title = ''
+      }
+    }
+    this.container.classList.add('with-switch')
+    this.input.classList.add('switch')
+    this.input.id = this.id
+  }
+}
+
+Fields.FacetSearchBase = class extends BaseElement {
+  buildLabel() {}
+}
+
+Fields.FacetSearchChoices = class extends Fields.FacetSearchBase {
+  getTemplate() {
+    return `
+      <fieldset class="umap-facet">
+        <legend data-ref=label>${Utils.escapeHTML(this.properties.label)}</legend>
+        <ul data-ref=ul></ul>
+      </fieldset>
+      `
+  }
+
+  build() {
+    this.type = this.properties.criteria.type
+
+    const choices = this.properties.criteria.choices
+    choices.sort()
+    choices.forEach((value) => this.buildLi(value))
+    super.build()
+  }
+
+  buildLi(value) {
+    const name = `${this.type}_${this.name}`
+    const [li, { input, label }] = Utils.loadTemplateWithRefs(`
+      <li>
+        <label>
+          <input type="${this.type}" name="${name}" data-ref=input />
+          <span data-ref=label></span>
+        </label>
+      </li>
+    `)
+    label.textContent = value
+    input.checked = this.get().choices.includes(value)
+    input.dataset.value = value
+    input.addEventListener('change', () => this.sync())
+    this.elements.ul.appendChild(li)
+  }
+
+  toJS() {
+    return {
+      type: this.type,
+      choices: [...this.elements.ul.querySelectorAll('input:checked')].map(
+        (i) => i.dataset.value
+      ),
+    }
+  }
+}
+
+Fields.MinMaxBase = class extends Fields.FacetSearchBase {
+  getInputType(type) {
+    return type
+  }
+
+  getLabels() {
+    return [translate('Min'), translate('Max')]
+  }
+
+  prepareForHTML(value) {
+    return value.valueOf()
+  }
+
+  getTemplate() {
+    const [minLabel, maxLabel] = this.getLabels()
+    const { min, max, type } = this.properties.criteria
+    this.type = type
+    const inputType = this.getInputType(this.type)
+    const minHTML = this.prepareForHTML(min)
+    const maxHTML = this.prepareForHTML(max)
+    return `
+      <fieldset class="umap-facet">
+        <legend>${Utils.escapeHTML(this.properties.label)}</legend>
+        <label>${minLabel}<input min="${minHTML}" max="${maxHTML}" step=any type="${inputType}" data-ref=minInput /></label>
+        <label>${maxLabel}<input min="${minHTML}" max="${maxHTML}" step=any type="${inputType}" data-ref=maxInput /></label>
+      </fieldset>
+    `
+  }
+
+  build() {
+    this.minInput = this.elements.minInput
+    this.maxInput = this.elements.maxInput
+    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
+    if (min != null) {
+      // The value stored using setAttribute is not modified by
+      // user input, and will be used as initial value when calling
+      // form.reset(), and can also be retrieve later on by using
+      // getAttributing, to compare with current value and know
+      // if this value has been modified by the user
+      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset
+      this.minInput.setAttribute('value', this.prepareForHTML(min))
+      this.minInput.value = this.prepareForHTML(currentMin)
+    }
+
+    if (max != null) {
+      // Cf comment above about setAttribute vs value
+      this.maxInput.setAttribute('value', this.prepareForHTML(max))
+      this.maxInput.value = this.prepareForHTML(currentMax)
+    }
+    this.toggleStatus()
+
+    this.minInput.addEventListener('change', () => this.sync())
+    this.maxInput.addEventListener('change', () => this.sync())
+    super.build()
+  }
+
+  toggleStatus() {
+    this.minInput.dataset.modified = this.isMinModified()
+    this.maxInput.dataset.modified = this.isMaxModified()
+  }
+
+  sync() {
+    super.sync()
+    this.toggleStatus()
+  }
+
+  isMinModified() {
+    const default_ = this.minInput.getAttribute('value')
+    const current = this.minInput.value
+    return current !== default_
+  }
+
+  isMaxModified() {
+    const default_ = this.maxInput.getAttribute('value')
+    const current = this.maxInput.value
+    return current !== default_
+  }
+
+  toJS() {
+    const opts = {
+      type: this.type,
+    }
+    if (this.minInput.value !== '' && this.isMinModified()) {
+      opts.min = this.prepareForJS(this.minInput.value)
+    }
+    if (this.maxInput.value !== '' && this.isMaxModified()) {
+      opts.max = this.prepareForJS(this.maxInput.value)
+    }
+    return opts
+  }
+}
+
+Fields.FacetSearchNumber = class extends Fields.MinMaxBase {
+  prepareForJS(value) {
+    return new Number(value)
+  }
+}
+
+Fields.FacetSearchDate = class extends Fields.MinMaxBase {
+  prepareForJS(value) {
+    return new Date(value)
+  }
+
+  toLocaleDateTime(dt) {
+    return new Date(dt.valueOf() - dt.getTimezoneOffset() * 60000)
+  }
+
+  prepareForHTML(value) {
+    // Value must be in local time
+    if (Number.isNaN(value)) return
+    return this.toLocaleDateTime(value).toISOString().substr(0, 10)
+  }
+
+  getLabels() {
+    return [translate('From'), translate('Until')]
+  }
+}
+
+Fields.FacetSearchDateTime = class extends Fields.FacetSearchDate {
+  getInputType(type) {
+    return 'datetime-local'
+  }
+
+  prepareForHTML(value) {
+    // Value must be in local time
+    if (Number.isNaN(value)) return
+    return this.toLocaleDateTime(value).toISOString().slice(0, -1)
+  }
+}
+
+Fields.MultiChoice = class extends BaseElement {
+  getDefault() {
+    return 'null'
+  }
+  // TODO: use public property when it's in our baseline
+  getClassName() {
+    return 'umap-multiplechoice'
+  }
+
+  clear() {
+    const checked = this.container.querySelector('input[type="radio"]:checked')
+    if (checked) checked.checked = false
+  }
+
+  fetch() {
+    this.initial = this.toHTML()
+    let value = this.initial
+    if (!this.container.querySelector(`input[type="radio"][value="${value}"]`)) {
+      value =
+        this.properties.default !== undefined ? this.properties.default : this.default
+    }
+    const choices = this.getChoices().map(([value, label]) => `${value}`)
+    if (choices.includes(`${value}`)) {
+      this.container.querySelector(`input[type="radio"][value="${value}"]`).checked =
+        true
+    }
+  }
+
+  value() {
+    const checked = this.container.querySelector('input[type="radio"]:checked')
+    if (checked) return checked.value
+  }
+
+  getChoices() {
+    return this.properties.choices || this.choices
+  }
+
+  getTemplate() {
+    return `<div class="${this.getClassName()} by${this.getChoices().length}" data-ref=wrapper></div>`
+  }
+
+  build() {
+    const choices = this.getChoices()
+    for (const [i, [value, label]] of choices.entries()) {
+      this.addChoice(value, label, i)
+    }
+    this.fetch()
+    super.build()
+  }
+
+  addChoice(value, label, counter) {
+    const id = `${Date.now()}.${this.name}.${counter}`
+    const input = Utils.loadTemplate(
+      `<input type="radio" name="${this.name}" id="${id}" value="${value}" />`
+    )
+    this.elements.wrapper.appendChild(input)
+    this.elements.wrapper.appendChild(
+      Utils.loadTemplate(`<label for="${id}">${label}</label>`)
+    )
+    input.addEventListener('change', () => this.sync())
+  }
+}
+
+Fields.TernaryChoices = class extends Fields.MultiChoice {
+  getDefault() {
+    return 'null'
+  }
+
+  toJS() {
+    let value = this.value()
+    switch (value) {
+      case 'true':
+      case true:
+        value = true
+        break
+      case 'false':
+      case false:
+        value = false
+        break
+      case 'null':
+      case null:
+        value = null
+        break
+      default:
+        value = undefined
+    }
+    return value
+  }
+}
+
+Fields.NullableChoices = class extends Fields.TernaryChoices {
+  getChoices() {
+    return [
+      [true, translate('always')],
+      [false, translate('never')],
+      ['null', translate('hidden')],
+    ]
+  }
+}
+
+Fields.DataLayersControl = class extends Fields.TernaryChoices {
+  getChoices() {
+    return [
+      [true, translate('collapsed')],
+      ['expanded', translate('expanded')],
+      [false, translate('never')],
+      ['null', translate('hidden')],
+    ]
+  }
+
+  toJS() {
+    let value = this.value()
+    if (value !== 'expanded') value = super.toJS()
+    return value
+  }
+}
+
+Fields.Range = class extends Fields.FloatInput {
+  type() {
+    return 'range'
+  }
+
+  value() {
+    return this.root.classList.contains('undefined') ? undefined : super.value()
+  }
+
+  build() {
+    super.build()
+    let options = ''
+    const step = this.properties.step || 1
+    const digits = step < 1 ? 1 : 0
+    const id = `range-${this.properties.label || this.name}`
+    for (
+      let i = this.properties.min;
+      i <= this.properties.max;
+      i += this.properties.step
+    ) {
+      const ii = i.toFixed(digits)
+      options += `<option value="${ii}" label="${ii}"></option>`
+    }
+    const datalist = Utils.loadTemplate(
+      `<datalist class="umap-field-datalist" id="${id}">${options}</datalist>`
+    )
+    this.container.appendChild(datalist)
+    this.input.setAttribute('list', id)
+  }
+}
+
+Fields.ManageOwner = class extends BaseElement {
+  build() {
+    const options = {
+      className: 'edit-owner',
+      on_select: L.bind(this.onSelect, this),
+      placeholder: translate("Type new owner's username"),
+    }
+    this.autocomplete = new AjaxAutocomplete(this.container, options)
+    const owner = this.toHTML()
+    if (owner) {
+      this.autocomplete.displaySelected({
+        item: { value: owner.id, label: owner.name },
+      })
+    }
+  }
+
+  value() {
+    return this._value
+  }
+
+  onSelect(choice) {
+    this._value = {
+      id: choice.item.value,
+      name: choice.item.label,
+      url: choice.item.url,
+    }
+    this.set()
+  }
+}
+
+Fields.ManageEditors = class extends BaseElement {
+  build() {
+    const options = {
+      className: 'edit-editors',
+      on_select: L.bind(this.onSelect, this),
+      on_unselect: L.bind(this.onUnselect, this),
+      placeholder: translate("Type editor's username"),
+    }
+    this.autocomplete = new AjaxAutocompleteMultiple(this.container, options)
+    this._values = this.toHTML()
+    if (this._values)
+      for (let i = 0; i < this._values.length; i++)
+        this.autocomplete.displaySelected({
+          item: { value: this._values[i].id, label: this._values[i].name },
+        })
+  }
+
+  value() {
+    return this._values
+  }
+
+  onSelect(choice) {
+    this._values.push({
+      id: choice.item.value,
+      name: choice.item.label,
+      url: choice.item.url,
+    })
+    this.set()
+  }
+
+  onUnselect(choice) {
+    const index = this._values.findIndex((item) => item.id === choice.item.value)
+    if (index !== -1) {
+      this._values.splice(index, 1)
+      this.set()
+    }
+  }
+}
+
+Fields.ManageTeam = class extends Fields.IntSelect {
+  getOptions() {
+    return [[null, translate('None')]].concat(
+      this.properties.teams.map((team) => [team.id, team.name])
+    )
+  }
+
+  toHTML() {
+    return this.get()?.id
+  }
+
+  toJS() {
+    const value = this.value()
+    for (const team of this.properties.teams) {
+      if (team.id === value) return team
+    }
+  }
+}
diff --git a/umap/static/umap/js/modules/help.js b/umap/static/umap/js/modules/help.js
index ddb67ad3d..84390c502 100644
--- a/umap/static/umap/js/modules/help.js
+++ b/umap/static/umap/js/modules/help.js
@@ -228,7 +228,9 @@ export default class Help {
 
   parse(container) {
     for (const element of container.querySelectorAll('[data-help]')) {
-      this.button(element, element.dataset.help.split(','))
+      if (element.dataset.help) {
+        this.button(element, element.dataset.help.split(','))
+      }
     }
   }
 
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 {
         `<fieldset class="separator"><legend>${translate('Manage collaborators')}</legend></fieldset>`
       )
       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..ccf936366 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,9 +126,8 @@ export default class Share {
       exportUrl.value = window.location.protocol + iframeExporter.buildUrl()
     }
     buildIframeCode()
-    const builder = new U.FormBuilder(iframeExporter, UIFields, {
-      callback: buildIframeCode,
-    })
+    const builder = new MutatingForm(iframeExporter, UIFields)
+    builder.on('set', buildIframeCode)
     const iframeOptions = DomUtil.createFieldset(
       this.container,
       translate('Embed and link options')
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 = `
   <table>
@@ -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 57ffc46b1..ab4d94fe6 100644
--- a/umap/static/umap/js/modules/umap.js
+++ b/umap/static/umap/js/modules/umap.js
@@ -34,6 +34,7 @@ import {
   uMapAlert as Alert,
 } from '../components/alerts/alert.js'
 import Orderable from './orderable.js'
+import { MutatingForm } from './form/builder.js'
 
 export default class Umap extends ServerStored {
   constructor(element, geojson) {
@@ -734,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 U.FormBuilder(this, metadataFields, {
+    const builder = new MutatingForm(this, metadataFields, {
       className: 'map-metadata',
       umap: this,
     })
@@ -749,7 +750,7 @@ export default class Umap extends ServerStored {
       'properties.permanentCredit',
       'properties.permanentCreditBackground',
     ]
-    const creditsBuilder = new U.FormBuilder(this, creditsFields, { umap: this })
+    const creditsBuilder = new MutatingForm(this, creditsFields, { umap: this })
     credits.appendChild(creditsBuilder.build())
     this.editPanel.open({ content: container })
   }
@@ -770,7 +771,7 @@ export default class Umap extends ServerStored {
       'properties.captionBar',
       'properties.captionMenus',
     ])
-    const builder = new U.FormBuilder(this, UIFields, { umap: this })
+    const builder = new MutatingForm(this, UIFields, { umap: this })
     const controlsOptions = DomUtil.createFieldset(
       container,
       translate('User interface options')
@@ -793,7 +794,7 @@ export default class Umap extends ServerStored {
       'properties.dashArray',
     ]
 
-    const builder = new U.FormBuilder(this, shapeOptions, { umap: this })
+    const builder = new MutatingForm(this, shapeOptions, { umap: this })
     const defaultShapeProperties = DomUtil.createFieldset(
       container,
       translate('Default shape properties')
@@ -812,7 +813,7 @@ export default class Umap extends ServerStored {
       'properties.slugKey',
     ]
 
-    const builder = new U.FormBuilder(this, optionsFields, { umap: this })
+    const builder = new MutatingForm(this, optionsFields, { umap: this })
     const defaultProperties = DomUtil.createFieldset(
       container,
       translate('Default properties')
@@ -830,7 +831,7 @@ export default class Umap extends ServerStored {
       'properties.labelInteractive',
       'properties.outlinkTarget',
     ]
-    const builder = new U.FormBuilder(this, popupFields, { umap: this })
+    const builder = new MutatingForm(this, popupFields, { umap: this })
     const popupFieldset = DomUtil.createFieldset(
       container,
       translate('Default interaction options')
@@ -887,7 +888,7 @@ export default class Umap extends ServerStored {
       container,
       translate('Custom background')
     )
-    const builder = new U.FormBuilder(this, tilelayerFields, { umap: this })
+    const builder = new MutatingForm(this, tilelayerFields, { umap: this })
     customTilelayer.appendChild(builder.build())
   }
 
@@ -935,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 U.FormBuilder(this, overlayFields, { umap: this })
+    const builder = new MutatingForm(this, overlayFields, { umap: this })
     overlay.appendChild(builder.build())
   }
 
@@ -962,7 +963,7 @@ export default class Umap extends ServerStored {
         { handler: 'BlurFloatInput', placeholder: translate('max East') },
       ],
     ]
-    const boundsBuilder = new U.FormBuilder(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(
@@ -1027,14 +1028,7 @@ export default class Umap extends ServerStored {
         { handler: 'Switch', label: translate('Autostart when map is loaded') },
       ],
     ]
-    const slideshowBuilder = new U.FormBuilder(this, slideshowFields, {
-      callback: () => {
-        this.slideshow.load()
-        // FIXME when we refactor formbuilder: this callback is called in a 'postsync'
-        // event, which comes after the call of `setter` method, which will call the
-        // map.render method, which should do this redraw.
-        this.bottomBar.redraw()
-      },
+    const slideshowBuilder = new MutatingForm(this, slideshowFields, {
       umap: this,
     })
     slideshow.appendChild(slideshowBuilder.build())
@@ -1042,7 +1036,9 @@ export default class Umap extends ServerStored {
 
   _editSync(container) {
     const sync = DomUtil.createFieldset(container, translate('Real-time collaboration'))
-    const builder = new U.FormBuilder(this, ['properties.syncEnabled'], { umap: this })
+    const builder = new MutatingForm(this, ['properties.syncEnabled'], {
+      umap: this,
+    })
     sync.appendChild(builder.build())
   }
 
@@ -1348,6 +1344,10 @@ export default class Umap extends ServerStored {
         }
         this.topBar.redraw()
       },
+      'properties.slideshow.active': () => {
+        this.slideshow.load()
+        this.bottomBar.redraw()
+      },
       numberOfConnectedPeers: () => {
         Utils.eachElement('.connected-peers span', (el) => {
           if (this.sync.websocketConnected) {
@@ -1459,7 +1459,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 U.FormBuilder(
+      const builder = new MutatingForm(
         datalayer,
         [['options.name', { handler: 'EditableText' }]],
         { className: 'umap-form-inline' }
diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js
index 2f70edf45..b36bc8402 100644
--- a/umap/static/umap/js/modules/utils.js
+++ b/umap/static/umap/js/modules/utils.js
@@ -416,9 +416,11 @@ export function loadTemplate(html) {
 }
 
 export function loadTemplateWithRefs(html) {
-  const element = loadTemplate(html)
+  const template = document.createElement('template')
+  template.innerHTML = html
+  const element = template.content.firstElementChild
   const elements = {}
-  for (const node of element.querySelectorAll('[data-ref]')) {
+  for (const node of template.content.querySelectorAll('[data-ref]')) {
     elements[node.dataset.ref] = node
   }
   return [element, elements]
@@ -446,3 +448,169 @@ export function eachElement(selector, callback) {
     callback(el)
   }
 }
+
+export class WithEvents {
+  constructor() {
+    this._target = new EventTarget()
+  }
+
+  on(eventType, callback) {
+    if (typeof callback !== 'function') return
+    this._target.addEventListener(eventType, callback)
+  }
+
+  fire(eventType, detail) {
+    const event = new CustomEvent(eventType, { detail })
+    this._target.dispatchEvent(event)
+  }
+}
+
+export const COLORS = [
+  'Black',
+  'Navy',
+  'DarkBlue',
+  'MediumBlue',
+  'Blue',
+  'DarkGreen',
+  'Green',
+  'Teal',
+  'DarkCyan',
+  'DeepSkyBlue',
+  'DarkTurquoise',
+  'MediumSpringGreen',
+  'Lime',
+  'SpringGreen',
+  'Aqua',
+  'Cyan',
+  'MidnightBlue',
+  'DodgerBlue',
+  'LightSeaGreen',
+  'ForestGreen',
+  'SeaGreen',
+  'DarkSlateGray',
+  'DarkSlateGrey',
+  'LimeGreen',
+  'MediumSeaGreen',
+  'Turquoise',
+  'RoyalBlue',
+  'SteelBlue',
+  'DarkSlateBlue',
+  'MediumTurquoise',
+  'Indigo',
+  'DarkOliveGreen',
+  'CadetBlue',
+  'CornflowerBlue',
+  'MediumAquaMarine',
+  'DimGray',
+  'DimGrey',
+  'SlateBlue',
+  'OliveDrab',
+  'SlateGray',
+  'SlateGrey',
+  'LightSlateGray',
+  'LightSlateGrey',
+  'MediumSlateBlue',
+  'LawnGreen',
+  'Chartreuse',
+  'Aquamarine',
+  'Maroon',
+  'Purple',
+  'Olive',
+  'Gray',
+  'Grey',
+  'SkyBlue',
+  'LightSkyBlue',
+  'BlueViolet',
+  'DarkRed',
+  'DarkMagenta',
+  'SaddleBrown',
+  'DarkSeaGreen',
+  'LightGreen',
+  'MediumPurple',
+  'DarkViolet',
+  'PaleGreen',
+  'DarkOrchid',
+  'YellowGreen',
+  'Sienna',
+  'Brown',
+  'DarkGray',
+  'DarkGrey',
+  'LightBlue',
+  'GreenYellow',
+  'PaleTurquoise',
+  'LightSteelBlue',
+  'PowderBlue',
+  'FireBrick',
+  'DarkGoldenRod',
+  'MediumOrchid',
+  'RosyBrown',
+  'DarkKhaki',
+  'Silver',
+  'MediumVioletRed',
+  'IndianRed',
+  'Peru',
+  'Chocolate',
+  'Tan',
+  'LightGray',
+  'LightGrey',
+  'Thistle',
+  'Orchid',
+  'GoldenRod',
+  'PaleVioletRed',
+  'Crimson',
+  'Gainsboro',
+  'Plum',
+  'BurlyWood',
+  'LightCyan',
+  'Lavender',
+  'DarkSalmon',
+  'Violet',
+  'PaleGoldenRod',
+  'LightCoral',
+  'Khaki',
+  'AliceBlue',
+  'HoneyDew',
+  'Azure',
+  'SandyBrown',
+  'Wheat',
+  'Beige',
+  'WhiteSmoke',
+  'MintCream',
+  'GhostWhite',
+  'Salmon',
+  'AntiqueWhite',
+  'Linen',
+  'LightGoldenRodYellow',
+  'OldLace',
+  'Red',
+  'Fuchsia',
+  'Magenta',
+  'DeepPink',
+  'OrangeRed',
+  'Tomato',
+  'HotPink',
+  'Coral',
+  'DarkOrange',
+  'LightSalmon',
+  'Orange',
+  'LightPink',
+  'Pink',
+  'Gold',
+  'PeachPuff',
+  'NavajoWhite',
+  'Moccasin',
+  'Bisque',
+  'MistyRose',
+  'BlanchedAlmond',
+  'PapayaWhip',
+  'LavenderBlush',
+  'SeaShell',
+  'Cornsilk',
+  'LemonChiffon',
+  'FloralWhite',
+  'Snow',
+  'Yellow',
+  'LightYellow',
+  'Ivory',
+  'White',
+]
diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js
deleted file mode 100644
index dc90d168b..000000000
--- a/umap/static/umap/js/umap.forms.js
+++ /dev/null
@@ -1,1242 +0,0 @@
-U.COLORS = [
-  'Black',
-  'Navy',
-  'DarkBlue',
-  'MediumBlue',
-  'Blue',
-  'DarkGreen',
-  'Green',
-  'Teal',
-  'DarkCyan',
-  'DeepSkyBlue',
-  'DarkTurquoise',
-  'MediumSpringGreen',
-  'Lime',
-  'SpringGreen',
-  'Aqua',
-  'Cyan',
-  'MidnightBlue',
-  'DodgerBlue',
-  'LightSeaGreen',
-  'ForestGreen',
-  'SeaGreen',
-  'DarkSlateGray',
-  'DarkSlateGrey',
-  'LimeGreen',
-  'MediumSeaGreen',
-  'Turquoise',
-  'RoyalBlue',
-  'SteelBlue',
-  'DarkSlateBlue',
-  'MediumTurquoise',
-  'Indigo',
-  'DarkOliveGreen',
-  'CadetBlue',
-  'CornflowerBlue',
-  'MediumAquaMarine',
-  'DimGray',
-  'DimGrey',
-  'SlateBlue',
-  'OliveDrab',
-  'SlateGray',
-  'SlateGrey',
-  'LightSlateGray',
-  'LightSlateGrey',
-  'MediumSlateBlue',
-  'LawnGreen',
-  'Chartreuse',
-  'Aquamarine',
-  'Maroon',
-  'Purple',
-  'Olive',
-  'Gray',
-  'Grey',
-  'SkyBlue',
-  'LightSkyBlue',
-  'BlueViolet',
-  'DarkRed',
-  'DarkMagenta',
-  'SaddleBrown',
-  'DarkSeaGreen',
-  'LightGreen',
-  'MediumPurple',
-  'DarkViolet',
-  'PaleGreen',
-  'DarkOrchid',
-  'YellowGreen',
-  'Sienna',
-  'Brown',
-  'DarkGray',
-  'DarkGrey',
-  'LightBlue',
-  'GreenYellow',
-  'PaleTurquoise',
-  'LightSteelBlue',
-  'PowderBlue',
-  'FireBrick',
-  'DarkGoldenRod',
-  'MediumOrchid',
-  'RosyBrown',
-  'DarkKhaki',
-  'Silver',
-  'MediumVioletRed',
-  'IndianRed',
-  'Peru',
-  'Chocolate',
-  'Tan',
-  'LightGray',
-  'LightGrey',
-  'Thistle',
-  'Orchid',
-  'GoldenRod',
-  'PaleVioletRed',
-  'Crimson',
-  'Gainsboro',
-  'Plum',
-  'BurlyWood',
-  'LightCyan',
-  'Lavender',
-  'DarkSalmon',
-  'Violet',
-  'PaleGoldenRod',
-  'LightCoral',
-  'Khaki',
-  'AliceBlue',
-  'HoneyDew',
-  'Azure',
-  'SandyBrown',
-  'Wheat',
-  'Beige',
-  'WhiteSmoke',
-  'MintCream',
-  'GhostWhite',
-  'Salmon',
-  'AntiqueWhite',
-  'Linen',
-  'LightGoldenRodYellow',
-  'OldLace',
-  'Red',
-  'Fuchsia',
-  'Magenta',
-  'DeepPink',
-  'OrangeRed',
-  'Tomato',
-  'HotPink',
-  'Coral',
-  'DarkOrange',
-  'LightSalmon',
-  'Orange',
-  'LightPink',
-  'Pink',
-  'Gold',
-  'PeachPuff',
-  'NavajoWhite',
-  'Moccasin',
-  'Bisque',
-  'MistyRose',
-  'BlanchedAlmond',
-  'PapayaWhip',
-  'LavenderBlush',
-  'SeaShell',
-  'Cornsilk',
-  'LemonChiffon',
-  'FloralWhite',
-  'Snow',
-  'Yellow',
-  'LightYellow',
-  'Ivory',
-  'White',
-]
-
-L.FormBuilder.Element.include({
-  undefine: function () {
-    L.DomUtil.addClass(this.wrapper, 'undefined')
-    this.clear()
-    this.sync()
-  },
-
-  getParentNode: function () {
-    if (this.options.wrapper) {
-      return L.DomUtil.create(
-        this.options.wrapper,
-        this.options.wrapperClass || '',
-        this.form
-      )
-    }
-    let className = 'formbox'
-    if (this.options.inheritable) {
-      className +=
-        this.get(true) === undefined ? ' inheritable undefined' : ' inheritable '
-    }
-    className += ` umap-field-${this.name}`
-    this.wrapper = L.DomUtil.create('div', className, this.form)
-    this.header = L.DomUtil.create('div', 'header', this.wrapper)
-    if (this.options.inheritable) {
-      const undefine = L.DomUtil.add('a', 'button undefine', this.header, L._('clear'))
-      const define = L.DomUtil.add('a', 'button define', this.header, L._('define'))
-      L.DomEvent.on(
-        define,
-        'click',
-        function (e) {
-          L.DomEvent.stop(e)
-          this.fetch()
-          this.fire('define')
-          L.DomUtil.removeClass(this.wrapper, 'undefined')
-        },
-        this
-      )
-      L.DomEvent.on(undefine, 'click', L.DomEvent.stop).on(
-        undefine,
-        'click',
-        this.undefine,
-        this
-      )
-    }
-    this.quickContainer = L.DomUtil.create(
-      'span',
-      'quick-actions show-on-defined',
-      this.header
-    )
-    this.extendedContainer = L.DomUtil.create('div', 'show-on-defined', this.wrapper)
-    return this.extendedContainer
-  },
-
-  getLabelParent: function () {
-    return this.header
-  },
-
-  clear: function () {
-    this.input.value = ''
-  },
-
-  get: function (own) {
-    if (!this.options.inheritable || own) return this.builder.getter(this.field)
-    const path = this.field.split('.')
-    const key = path[path.length - 1]
-    return this.obj.getOption(key)
-  },
-
-  buildLabel: function () {
-    if (this.options.label) {
-      this.label = L.DomUtil.create('label', '', this.getLabelParent())
-      this.label.textContent = this.label.title = this.options.label
-      if (this.options.helpEntries) {
-        this.builder._umap.help.button(this.label, this.options.helpEntries)
-      } else if (this.options.helpTooltip) {
-        const info = L.DomUtil.create('i', 'info', this.label)
-        L.DomEvent.on(info, 'mouseover', () => {
-          this.builder._umap.tooltip.open({
-            anchor: info,
-            content: this.options.helpTooltip,
-            position: 'top',
-          })
-        })
-      }
-    }
-  },
-})
-
-L.FormBuilder.Select.include({
-  clear: function () {
-    this.select.value = ''
-  },
-
-  getDefault: function () {
-    if (this.options.inheritable) return undefined
-    return this.getOptions()[0][0]
-  },
-})
-
-L.FormBuilder.CheckBox.include({
-  value: function () {
-    return L.DomUtil.hasClass(this.wrapper, 'undefined')
-      ? undefined
-      : this.input.checked
-  },
-
-  clear: function () {
-    this.fetch()
-  },
-})
-
-L.FormBuilder.EditableText = L.FormBuilder.Element.extend({
-  build: function () {
-    this.input = L.DomUtil.create('span', this.options.className || '', this.parentNode)
-    this.input.contentEditable = true
-    this.fetch()
-    L.DomEvent.on(this.input, 'input', this.sync, this)
-    L.DomEvent.on(this.input, 'keypress', this.onKeyPress, this)
-  },
-
-  getParentNode: function () {
-    return this.form
-  },
-
-  value: function () {
-    return this.input.textContent
-  },
-
-  fetch: function () {
-    this.input.textContent = this.toHTML()
-  },
-
-  onKeyPress: function (event) {
-    if (event.keyCode === 13) {
-      event.preventDefault()
-      this.input.blur()
-    }
-  },
-})
-
-L.FormBuilder.ColorPicker = L.FormBuilder.Input.extend({
-  colors: U.COLORS,
-  getParentNode: function () {
-    L.FormBuilder.CheckBox.prototype.getParentNode.call(this)
-    return this.quickContainer
-  },
-
-  build: function () {
-    L.FormBuilder.Input.prototype.build.call(this)
-    this.input.placeholder = this.options.placeholder || L._('Inherit')
-    this.container = L.DomUtil.create(
-      'div',
-      'umap-color-picker',
-      this.extendedContainer
-    )
-    this.container.style.display = 'none'
-    for (const idx in this.colors) {
-      this.addColor(this.colors[idx])
-    }
-    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)
-  },
-
-  onFocus: function () {
-    this.container.style.display = 'block'
-    this.spreadColor()
-  },
-
-  onBlur: function () {
-    const closePicker = () => {
-      this.container.style.display = 'none'
-    }
-    // We must leave time for the click to be listened.
-    window.setTimeout(closePicker, 100)
-  },
-
-  sync: function () {
-    this.spreadColor()
-    L.FormBuilder.Input.prototype.sync.call(this)
-  },
-
-  spreadColor: function () {
-    if (this.input.value) this.input.style.backgroundColor = this.input.value
-    else this.input.style.backgroundColor = 'inherit'
-  },
-
-  addColor: function (colorName) {
-    const span = L.DomUtil.create('span', '', this.container)
-    span.style.backgroundColor = span.title = colorName
-    const updateColorInput = function () {
-      this.input.value = colorName
-      this.sync()
-      this.container.style.display = 'none'
-    }
-    L.DomEvent.on(span, 'mousedown', updateColorInput, this)
-  },
-})
-
-L.FormBuilder.TextColorPicker = L.FormBuilder.ColorPicker.extend({
-  colors: [
-    'Black',
-    'DarkSlateGrey',
-    'DimGrey',
-    'SlateGrey',
-    'LightSlateGrey',
-    'Grey',
-    'DarkGrey',
-    'LightGrey',
-    'White',
-  ],
-})
-
-L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
-  getOptions: () => {
-    return U.LAYER_TYPES.map((class_) => [class_.TYPE, class_.NAME])
-  },
-})
-
-L.FormBuilder.SlideshowDelay = L.FormBuilder.IntSelect.extend({
-  getOptions: () => {
-    const options = []
-    for (let i = 1; i < 30; i++) {
-      options.push([i * 1000, L._('{delay} seconds', { delay: i })])
-    }
-    return options
-  },
-})
-
-L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
-  getOptions: function () {
-    const options = []
-    this.builder._umap.eachDataLayerReverse((datalayer) => {
-      if (
-        datalayer.isLoaded() &&
-        !datalayer.isDataReadOnly() &&
-        datalayer.isBrowsable()
-      ) {
-        options.push([L.stamp(datalayer), datalayer.getName()])
-      }
-    })
-    return options
-  },
-
-  toHTML: function () {
-    return L.stamp(this.obj.datalayer)
-  },
-
-  toJS: function () {
-    return this.builder._umap.datalayers[this.value()]
-  },
-
-  set: function () {
-    this.builder._umap.lastUsedDataLayer = this.toJS()
-    this.obj.changeDataLayer(this.toJS())
-  },
-})
-
-L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({
-  selectOptions: [
-    [undefined, L._('Choose the data format')],
-    ['geojson', 'geojson'],
-    ['osm', 'osm'],
-    ['csv', 'csv'],
-    ['gpx', 'gpx'],
-    ['kml', 'kml'],
-    ['georss', 'georss'],
-  ],
-})
-
-L.FormBuilder.LicenceChooser = L.FormBuilder.Select.extend({
-  getOptions: function () {
-    const licences = []
-    const licencesList = this.builder.obj.properties.licences
-    let licence
-    for (const i in licencesList) {
-      licence = licencesList[i]
-      licences.push([i, licence.name])
-    }
-    return licences
-  },
-
-  toHTML: function () {
-    return this.get()?.name
-  },
-
-  toJS: function () {
-    return this.builder.obj.properties.licences[this.value()]
-  },
-})
-
-L.FormBuilder.NullableBoolean = L.FormBuilder.Select.extend({
-  selectOptions: [
-    [undefined, L._('inherit')],
-    [true, L._('yes')],
-    [false, L._('no')],
-  ],
-
-  toJS: function () {
-    let value = this.value()
-    switch (value) {
-      case 'true':
-      case true:
-        value = true
-        break
-      case 'false':
-      case false:
-        value = false
-        break
-      default:
-        value = undefined
-    }
-    return value
-  },
-})
-
-L.FormBuilder.BlurInput.include({
-  build: function () {
-    this.options.className = 'blur'
-    L.FormBuilder.Input.prototype.build.call(this)
-    const button = L.DomUtil.create('span', 'button blur-button')
-    L.DomUtil.after(this.input, button)
-    L.DomEvent.on(this.input, 'focus', this.fetch, this)
-  },
-})
-
-// Adds an autocomplete using all available user defined properties
-L.FormBuilder.PropertyInput = L.FormBuilder.BlurInput.extend({
-  build: function () {
-    L.FormBuilder.BlurInput.prototype.build.call(this)
-    const autocomplete = new U.AutocompleteDatalist(this.input)
-    // Will be used on Umap and DataLayer
-    const properties = this.builder.obj.allProperties()
-    autocomplete.suggestions = properties
-  },
-})
-
-L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
-  type: () => 'hidden',
-
-  build: function () {
-    L.FormBuilder.BlurInput.prototype.build.call(this)
-    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)
-    this.updatePreview()
-    this.on('define', this.onDefine)
-  },
-
-  onDefine: async function () {
-    this.buttons.innerHTML = ''
-    this.footer.innerHTML = ''
-    const [{ pictogram_list }, response, error] = await this.builder._umap.server.get(
-      this.builder._umap.properties.urls.pictogram_list_json
-    )
-    if (!error) this.pictogram_list = pictogram_list
-    this.buildTabs()
-    const value = this.value()
-    if (U.Icon.RECENT.length) this.showRecentTab()
-    else if (!value || U.Utils.isPath(value)) this.showSymbolsTab()
-    else if (U.Utils.isRemoteUrl(value) || U.Utils.isDataImage(value)) this.showURLTab()
-    else this.showCharsTab()
-    const closeButton = L.DomUtil.createButton(
-      'button action-button',
-      this.footer,
-      L._('Close'),
-      function (e) {
-        this.body.innerHTML = ''
-        this.tabs.innerHTML = ''
-        this.footer.innerHTML = ''
-        if (this.isDefault()) this.undefine(e)
-        else this.updatePreview()
-      },
-      this
-    )
-  },
-
-  buildTabs: function () {
-    this.tabs.innerHTML = ''
-    if (U.Icon.RECENT.length) {
-      const recent = L.DomUtil.add(
-        'button',
-        'flat tab-recent',
-        this.tabs,
-        L._('Recent')
-      )
-      L.DomEvent.on(recent, 'click', L.DomEvent.stop).on(
-        recent,
-        'click',
-        this.showRecentTab,
-        this
-      )
-    }
-    const symbol = L.DomUtil.add('button', 'flat tab-symbols', this.tabs, L._('Symbol'))
-    const char = L.DomUtil.add(
-      'button',
-      'flat tab-chars',
-      this.tabs,
-      L._('Emoji & Character')
-    )
-    url = L.DomUtil.add('button', 'flat tab-url', this.tabs, L._('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)
-  },
-
-  openTab: function (name) {
-    const els = this.tabs.querySelectorAll('button')
-    for (const el of els) {
-      L.DomUtil.removeClass(el, 'on')
-    }
-    const el = this.tabs.querySelector(`.tab-${name}`)
-    L.DomUtil.addClass(el, 'on')
-    this.body.innerHTML = ''
-  },
-
-  updatePreview: function () {
-    this.buttons.innerHTML = ''
-    if (this.isDefault()) return
-    if (!U.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)
-    }
-    this.button = L.DomUtil.createButton(
-      'button action-button',
-      this.buttons,
-      this.value() ? L._('Change') : L._('Add'),
-      this.onDefine,
-      this
-    )
-  },
-
-  addIconPreview: function (pictogram, parent) {
-    const baseClass = 'umap-pictogram-choice'
-    const value = pictogram.src
-    const search = U.Utils.normalize(this.searchInput.value)
-    const title = pictogram.attribution
-      ? `${pictogram.name} — © ${pictogram.attribution}`
-      : pictogram.name || pictogram.src
-    if (search && U.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
-    )
-    return true // Icon has been added (not filtered)
-  },
-
-  clear: function () {
-    this.input.value = ''
-    this.unselectAll(this.body)
-    this.sync()
-    this.body.innerHTML = ''
-    this.updatePreview()
-  },
-
-  addCategory: function (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
-    for (const item of items) {
-      status = this.addIconPreview(item, grid) || status
-    }
-    if (status) this.grid.appendChild(parent)
-  },
-
-  buildSymbolsList: function () {
-    this.grid.innerHTML = ''
-    const categories = {}
-    let category
-    for (const props of this.pictogram_list) {
-      category = props.category || L._('Generic')
-      categories[category] = categories[category] || []
-      categories[category].push(props)
-    }
-    const sorted = Object.entries(categories).toSorted(([a], [b]) =>
-      U.Utils.naturalSort(a, b, U.lang)
-    )
-    for (const [name, items] of sorted) {
-      this.addCategory(items, name)
-    }
-  },
-
-  buildRecentList: function () {
-    this.grid.innerHTML = ''
-    const items = U.Icon.RECENT.map((src) => ({
-      src,
-    }))
-    this.addCategory(items)
-  },
-
-  isDefault: function () {
-    return !this.value() || this.value() === U.SCHEMA.iconUrl.default
-  },
-
-  addGrid: function (onSearch) {
-    this.searchInput = L.DomUtil.create('input', '', this.body)
-    this.searchInput.type = 'search'
-    this.searchInput.placeholder = L._('Search')
-    this.grid = L.DomUtil.create('div', '', this.body)
-    L.DomEvent.on(this.searchInput, 'input', onSearch, this)
-  },
-
-  showRecentTab: function () {
-    if (!U.Icon.RECENT.length) return
-    this.openTab('recent')
-    this.addGrid(this.buildRecentList)
-    this.buildRecentList()
-  },
-
-  showSymbolsTab: function () {
-    this.openTab('symbols')
-    this.addGrid(this.buildSymbolsList)
-    this.buildSymbolsList()
-  },
-
-  showCharsTab: function () {
-    this.openTab('chars')
-    const value = !U.Icon.isImg(this.value()) ? this.value() : null
-    const input = this.buildInput(this.body, value)
-    input.placeholder = L._('Type char or paste emoji')
-    input.type = 'text'
-  },
-
-  showURLTab: function () {
-    this.openTab('url')
-    const value =
-      U.Utils.isRemoteUrl(this.value()) || U.Utils.isDataImage(this.value())
-        ? this.value()
-        : null
-    const input = this.buildInput(this.body, value)
-    input.placeholder = L._('Add image URL')
-    input.type = 'url'
-  },
-
-  buildInput: function (parent, value) {
-    const input = L.DomUtil.create('input', 'blur', parent)
-    const button = L.DomUtil.create('span', 'button blur-button', parent)
-    if (value) input.value = value
-    L.DomEvent.on(input, 'blur', () => {
-      // Do not clear this.input when focus-blur
-      // empty input
-      if (input.value === value) return
-      this.input.value = input.value
-      this.sync()
-    })
-    return input
-  },
-
-  unselectAll: (container) => {
-    const els = container.querySelectorAll('div.selected')
-    for (const el in els) {
-      if (els.hasOwnProperty(el)) L.DomUtil.removeClass(els[el], 'selected')
-    }
-  },
-})
-
-L.FormBuilder.Url = L.FormBuilder.Input.extend({
-  type: () => 'url',
-})
-
-L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({
-  getParentNode: function () {
-    L.FormBuilder.CheckBox.prototype.getParentNode.call(this)
-    if (this.options.inheritable) return this.quickContainer
-    return this.extendedContainer
-  },
-
-  build: function () {
-    L.FormBuilder.CheckBox.prototype.build.apply(this)
-    if (this.options.inheritable)
-      this.label = L.DomUtil.create('label', '', this.input.parentNode)
-    else this.input.parentNode.appendChild(this.label)
-    L.DomUtil.addClass(this.input.parentNode, 'with-switch')
-    const id = `${this.builder.options.id || Date.now()}.${this.name}`
-    this.label.setAttribute('for', id)
-    L.DomUtil.addClass(this.input, 'switch')
-    this.input.id = id
-  },
-})
-
-L.FormBuilder.FacetSearchBase = L.FormBuilder.Element.extend({
-  buildLabel: function () {
-    this.label = L.DomUtil.element({
-      tagName: 'legend',
-      textContent: this.options.label,
-    })
-  },
-})
-L.FormBuilder.FacetSearchChoices = L.FormBuilder.FacetSearchBase.extend({
-  build: function () {
-    this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode)
-    this.container.appendChild(this.label)
-    this.ul = L.DomUtil.create('ul', '', this.container)
-    this.type = this.options.criteria.type
-
-    const choices = this.options.criteria.choices
-    choices.sort()
-    choices.forEach((value) => this.buildLi(value))
-  },
-
-  buildLi: function (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}`
-    input.checked = this.get().choices.includes(value)
-    input.dataset.value = value
-
-    L.DomEvent.on(input, 'change', (e) => this.sync())
-  },
-
-  toJS: function () {
-    return {
-      type: this.type,
-      choices: [...this.ul.querySelectorAll('input:checked')].map(
-        (i) => i.dataset.value
-      ),
-    }
-  },
-})
-
-L.FormBuilder.MinMaxBase = L.FormBuilder.FacetSearchBase.extend({
-  getInputType: (type) => type,
-
-  getLabels: () => [L._('Min'), L._('Max')],
-
-  prepareForHTML: (value) => value.valueOf(),
-
-  build: function () {
-    this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode)
-    this.container.appendChild(this.label)
-    const { min, max, type } = this.options.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)
-    if (min != null) {
-      // The value stored using setAttribute is not modified by
-      // user input, and will be used as initial value when calling
-      // form.reset(), and can also be retrieve later on by using
-      // getAttributing, to compare with current value and know
-      // if this value has been modified by the user
-      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset
-      this.minInput.setAttribute('value', this.prepareForHTML(min))
-      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))
-      this.maxInput.value = this.prepareForHTML(currentMax)
-    }
-    this.toggleStatus()
-
-    L.DomEvent.on(this.minInput, 'change', () => this.sync())
-    L.DomEvent.on(this.maxInput, 'change', () => this.sync())
-  },
-
-  toggleStatus: function () {
-    this.minInput.dataset.modified = this.isMinModified()
-    this.maxInput.dataset.modified = this.isMaxModified()
-  },
-
-  sync: function () {
-    L.FormBuilder.Element.prototype.sync.call(this)
-    this.toggleStatus()
-  },
-
-  isMinModified: function () {
-    const default_ = this.minInput.getAttribute('value')
-    const current = this.minInput.value
-    return current !== default_
-  },
-
-  isMaxModified: function () {
-    const default_ = this.maxInput.getAttribute('value')
-    const current = this.maxInput.value
-    return current !== default_
-  },
-
-  toJS: function () {
-    const opts = {
-      type: this.type,
-    }
-    if (this.minInput.value !== '' && this.isMinModified()) {
-      opts.min = this.prepareForJS(this.minInput.value)
-    }
-    if (this.maxInput.value !== '' && this.isMaxModified()) {
-      opts.max = this.prepareForJS(this.maxInput.value)
-    }
-    return opts
-  },
-})
-
-L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({
-  prepareForJS: (value) => new Number(value),
-})
-
-L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({
-  prepareForJS: (value) => new Date(value),
-
-  toLocaleDateTime: (dt) => new Date(dt.valueOf() - dt.getTimezoneOffset() * 60000),
-
-  prepareForHTML: function (value) {
-    // Value must be in local time
-    if (Number.isNaN(value)) return
-    return this.toLocaleDateTime(value).toISOString().substr(0, 10)
-  },
-
-  getLabels: () => [L._('From'), L._('Until')],
-})
-
-L.FormBuilder.FacetSearchDateTime = L.FormBuilder.FacetSearchDate.extend({
-  getInputType: (type) => 'datetime-local',
-
-  prepareForHTML: function (value) {
-    // Value must be in local time
-    if (Number.isNaN(value)) return
-    return this.toLocaleDateTime(value).toISOString().slice(0, -1)
-  },
-})
-
-L.FormBuilder.MultiChoice = L.FormBuilder.Element.extend({
-  default: 'null',
-  className: 'umap-multiplechoice',
-
-  clear: function () {
-    const checked = this.container.querySelector('input[type="radio"]:checked')
-    if (checked) checked.checked = false
-  },
-
-  fetch: function () {
-    this.initial = this.toHTML()
-    let value = this.initial
-    if (!this.container.querySelector(`input[type="radio"][value="${value}"]`)) {
-      value = this.options.default !== undefined ? this.options.default : this.default
-    }
-    const choices = this.getChoices().map(([value, label]) => `${value}`)
-    if (choices.includes(`${value}`)) {
-      this.container.querySelector(`input[type="radio"][value="${value}"]`).checked =
-        true
-    }
-  },
-
-  value: function () {
-    const checked = this.container.querySelector('input[type="radio"]:checked')
-    if (checked) return checked.value
-  },
-
-  getChoices: function () {
-    return this.options.choices || this.choices
-  },
-
-  build: function () {
-    const choices = this.getChoices()
-    this.container = L.DomUtil.create(
-      'div',
-      `${this.className} by${choices.length}`,
-      this.parentNode
-    )
-    for (const [i, [value, label]] of choices.entries()) {
-      this.addChoice(value, label, i)
-    }
-    this.fetch()
-  },
-
-  addChoice: function (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)
-  },
-})
-
-L.FormBuilder.TernaryChoices = L.FormBuilder.MultiChoice.extend({
-  default: 'null',
-
-  toJS: function () {
-    let value = this.value()
-    switch (value) {
-      case 'true':
-      case true:
-        value = true
-        break
-      case 'false':
-      case false:
-        value = false
-        break
-      case 'null':
-      case null:
-        value = null
-        break
-      default:
-        value = undefined
-    }
-    return value
-  },
-})
-
-L.FormBuilder.NullableChoices = L.FormBuilder.TernaryChoices.extend({
-  choices: [
-    [true, L._('always')],
-    [false, L._('never')],
-    ['null', L._('hidden')],
-  ],
-})
-
-L.FormBuilder.DataLayersControl = L.FormBuilder.TernaryChoices.extend({
-  choices: [
-    [true, L._('collapsed')],
-    ['expanded', L._('expanded')],
-    [false, L._('never')],
-    ['null', L._('hidden')],
-  ],
-
-  toJS: function () {
-    let value = this.value()
-    if (value !== 'expanded')
-      value = L.FormBuilder.TernaryChoices.prototype.toJS.call(this)
-    return value
-  },
-})
-
-L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({
-  type: () => 'range',
-
-  value: function () {
-    return L.DomUtil.hasClass(this.wrapper, 'undefined')
-      ? undefined
-      : L.FormBuilder.FloatInput.prototype.value.call(this)
-  },
-
-  buildHelpText: function () {
-    let options = ''
-    const step = this.options.step || 1
-    const digits = step < 1 ? 1 : 0
-    const id = `range-${this.options.label || this.name}`
-    for (let i = this.options.min; i <= this.options.max; i += this.options.step) {
-      options += `<option value="${i.toFixed(digits)}" label="${i.toFixed(
-        digits
-      )}"></option>`
-    }
-    const datalist = L.DomUtil.element({
-      tagName: 'datalist',
-      parent: this.getHelpTextParent(),
-      className: 'umap-field-datalist',
-      safeHTML: options,
-      id: id,
-    })
-    this.input.setAttribute('list', id)
-    L.FormBuilder.Input.prototype.buildHelpText.call(this)
-  },
-})
-
-L.FormBuilder.ManageOwner = L.FormBuilder.Element.extend({
-  build: function () {
-    const options = {
-      className: 'edit-owner',
-      on_select: L.bind(this.onSelect, this),
-      placeholder: L._("Type new owner's username"),
-    }
-    this.autocomplete = new U.AjaxAutocomplete(this.parentNode, options)
-    const owner = this.toHTML()
-    if (owner)
-      this.autocomplete.displaySelected({
-        item: { value: owner.id, label: owner.name },
-      })
-  },
-
-  value: function () {
-    return this._value
-  },
-
-  onSelect: function (choice) {
-    this._value = {
-      id: choice.item.value,
-      name: choice.item.label,
-      url: choice.item.url,
-    }
-    this.set()
-  },
-})
-
-L.FormBuilder.ManageEditors = L.FormBuilder.Element.extend({
-  build: function () {
-    const options = {
-      className: 'edit-editors',
-      on_select: L.bind(this.onSelect, this),
-      on_unselect: L.bind(this.onUnselect, this),
-      placeholder: L._("Type editor's username"),
-    }
-    this.autocomplete = new U.AjaxAutocompleteMultiple(this.parentNode, options)
-    this._values = this.toHTML()
-    if (this._values)
-      for (let i = 0; i < this._values.length; i++)
-        this.autocomplete.displaySelected({
-          item: { value: this._values[i].id, label: this._values[i].name },
-        })
-  },
-
-  value: function () {
-    return this._values
-  },
-
-  onSelect: function (choice) {
-    this._values.push({
-      id: choice.item.value,
-      name: choice.item.label,
-      url: choice.item.url,
-    })
-    this.set()
-  },
-
-  onUnselect: function (choice) {
-    const index = this._values.findIndex((item) => item.id === choice.item.value)
-    if (index !== -1) {
-      this._values.splice(index, 1)
-      this.set()
-    }
-  },
-})
-
-L.FormBuilder.ManageTeam = L.FormBuilder.IntSelect.extend({
-  getOptions: function () {
-    return [[null, L._('None')]].concat(
-      this.options.teams.map((team) => [team.id, team.name])
-    )
-  },
-  toHTML: function () {
-    return this.get()?.id
-  },
-  toJS: function () {
-    const value = this.value()
-    for (const team of this.options.teams) {
-      if (team.id === value) return team
-    }
-  },
-})
-
-U.FormBuilder = L.FormBuilder.extend({
-  options: {
-    className: 'umap-form',
-  },
-
-  customHandlers: {
-    sortKey: 'PropertyInput',
-    easing: 'Switch',
-    facetKey: 'PropertyInput',
-    slugKey: 'PropertyInput',
-    labelKey: 'PropertyInput',
-  },
-
-  computeDefaultOptions: function () {
-    for (const [key, schema] of Object.entries(U.SCHEMA)) {
-      if (schema.type === Boolean) {
-        if (schema.nullable) schema.handler = 'NullableChoices'
-        else schema.handler = 'Switch'
-      } else if (schema.type === 'Text') {
-        schema.handler = 'Textarea'
-      } else if (schema.type === Number) {
-        if (schema.step) schema.handler = 'Range'
-        else schema.handler = 'IntInput'
-      } else if (schema.choices) {
-        const text_length = schema.choices.reduce(
-          (acc, [_, label]) => acc + label.length,
-          0
-        )
-        // Try to be smart and use MultiChoice only
-        // for choices where labels are shorts…
-        if (text_length < 40) {
-          schema.handler = 'MultiChoice'
-        } else {
-          schema.handler = 'Select'
-          schema.selectOptions = schema.choices
-        }
-      } else {
-        switch (key) {
-          case 'color':
-          case 'fillColor':
-            schema.handler = 'ColorPicker'
-            break
-          case 'iconUrl':
-            schema.handler = 'IconUrl'
-            break
-          case 'licence':
-            schema.handler = 'LicenceChooser'
-            break
-        }
-      }
-      if (this.customHandlers[key]) {
-        schema.handler = this.customHandlers[key]
-      }
-      // FormBuilder use this key for the input type itself
-      delete schema.type
-      this.defaultOptions[key] = schema
-    }
-  },
-
-  initialize: function (obj, fields, options = {}) {
-    this._umap = obj._umap || options.umap
-    this.computeDefaultOptions()
-    L.FormBuilder.prototype.initialize.call(this, obj, fields, options)
-    this.on('finish', this.finish)
-  },
-
-  setter: function (field, value) {
-    L.FormBuilder.prototype.setter.call(this, field, value)
-    this.obj.isDirty = true
-    if ('render' in this.obj) {
-      this.obj.render([field], this)
-    }
-    if ('sync' in this.obj) {
-      this.obj.sync.update(field, value)
-    }
-  },
-
-  getter: function (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) values = U.SCHEMA[sub]?.default
-    return value
-  },
-
-  finish: (event) => {
-    event.helper?.input?.blur()
-  },
-})
diff --git a/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js b/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js
deleted file mode 100644
index 6f814904e..000000000
--- a/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js
+++ /dev/null
@@ -1,468 +0,0 @@
-L.FormBuilder = L.Evented.extend({
-  options: {
-    className: 'leaflet-form',
-  },
-
-  defaultOptions: {
-    // Eg.:
-    // name: {label: L._('name')},
-    // description: {label: L._('description'), handler: 'Textarea'},
-    // opacity: {label: L._('opacity'), helpText: L._('Opacity, from 0.1 to 1.0 (opaque).')},
-  },
-
-  initialize: function (obj, fields, options) {
-    L.setOptions(this, options)
-    this.obj = obj
-    this.form = L.DomUtil.create('form', this.options.className)
-    this.setFields(fields)
-    if (this.options.id) {
-      this.form.id = this.options.id
-    }
-    if (this.options.className) {
-      L.DomUtil.addClass(this.form, this.options.className)
-    }
-  },
-
-  setFields: function (fields) {
-    this.fields = fields || []
-    this.helpers = {}
-  },
-
-  build: function () {
-    this.form.innerHTML = ''
-    for (const idx in this.fields) {
-      this.buildField(this.fields[idx])
-    }
-    this.on('postsync', this.onPostSync)
-    return this.form
-  },
-
-  buildField: function (field) {
-    // field can be either a string like "option.name" or a full definition array,
-    // like ['options.tilelayer.tms', {handler: 'CheckBox', helpText: 'TMS format'}]
-    let type
-    let helper
-    let options
-    if (Array.isArray(field)) {
-      options = field[1] || {}
-      field = field[0]
-    } else {
-      options = this.defaultOptions[this.getName(field)] || {}
-    }
-    type = options.handler || 'Input'
-    if (typeof type === 'string' && L.FormBuilder[type]) {
-      helper = new L.FormBuilder[type](this, field, options)
-    } else {
-      helper = new type(this, field, options)
-    }
-    this.helpers[field] = helper
-    return helper
-  },
-
-  getter: function (field) {
-    const path = field.split('.')
-    let value = this.obj
-    for (const sub of path) {
-      try {
-        value = value[sub]
-      } catch {
-        console.log(field)
-      }
-    }
-    return value
-  },
-
-  setter: function (field, value) {
-    const path = field.split('.')
-    let obj = this.obj
-    let what
-    for (let i = 0, l = path.length; i < l; i++) {
-      what = path[i]
-      if (what === path[l - 1]) {
-        if (typeof value === 'undefined') {
-          delete obj[what]
-        } else {
-          obj[what] = value
-        }
-      } else {
-        obj = obj[what]
-      }
-    }
-  },
-
-  restoreField: function (field) {
-    const initial = this.helpers[field].initial
-    this.setter(field, initial)
-  },
-
-  getName: (field) => {
-    const fieldEls = field.split('.')
-    return fieldEls[fieldEls.length - 1]
-  },
-
-  fetchAll: function () {
-    for (const helper of Object.values(this.helpers)) {
-      helper.fetch()
-    }
-  },
-
-  syncAll: function () {
-    for (const helper of Object.values(this.helpers)) {
-      helper.sync()
-    }
-  },
-
-  onPostSync: function (e) {
-    if (e.helper.options.callback) {
-      e.helper.options.callback.call(e.helper.options.callbackContext || this.obj, e)
-    }
-    if (this.options.callback) {
-      this.options.callback.call(this.options.callbackContext || this.obj, e)
-    }
-  },
-})
-
-L.FormBuilder.Element = L.Evented.extend({
-  initialize: function (builder, field, options) {
-    this.builder = builder
-    this.obj = this.builder.obj
-    this.form = this.builder.form
-    this.field = field
-    L.setOptions(this, options)
-    this.fieldEls = this.field.split('.')
-    this.name = this.builder.getName(field)
-    this.parentNode = this.getParentNode()
-    this.buildLabel()
-    this.build()
-    this.buildHelpText()
-    this.fireAndForward('helper:init')
-  },
-
-  fireAndForward: function (type, e = {}) {
-    e.helper = this
-    this.fire(type, e)
-    this.builder.fire(type, e)
-    if (this.obj.fire) this.obj.fire(type, e)
-  },
-
-  getParentNode: function () {
-    return this.options.wrapper
-      ? L.DomUtil.create(
-          this.options.wrapper,
-          this.options.wrapperClass || '',
-          this.form
-        )
-      : this.form
-  },
-
-  get: function () {
-    return this.builder.getter(this.field)
-  },
-
-  toHTML: function () {
-    return this.get()
-  },
-
-  toJS: function () {
-    return this.value()
-  },
-
-  sync: function () {
-    this.fireAndForward('presync')
-    this.set()
-    this.fireAndForward('postsync')
-  },
-
-  set: function () {
-    this.builder.setter(this.field, this.toJS())
-  },
-
-  getLabelParent: function () {
-    return this.parentNode
-  },
-
-  getHelpTextParent: function () {
-    return this.parentNode
-  },
-
-  buildLabel: function () {
-    if (this.options.label) {
-      this.label = L.DomUtil.create('label', '', this.getLabelParent())
-      this.label.innerHTML = this.options.label
-    }
-  },
-
-  buildHelpText: function () {
-    if (this.options.helpText) {
-      const container = L.DomUtil.create('small', 'help-text', this.getHelpTextParent())
-      container.innerHTML = this.options.helpText
-    }
-  },
-
-  fetch: () => {},
-
-  finish: function () {
-    this.fireAndForward('finish')
-  },
-})
-
-L.FormBuilder.Textarea = L.FormBuilder.Element.extend({
-  build: function () {
-    this.input = L.DomUtil.create(
-      'textarea',
-      this.options.className || '',
-      this.parentNode
-    )
-    if (this.options.placeholder) this.input.placeholder = this.options.placeholder
-    this.fetch()
-    L.DomEvent.on(this.input, 'input', this.sync, this)
-    L.DomEvent.on(this.input, 'keypress', this.onKeyPress, this)
-  },
-
-  fetch: function () {
-    const value = this.toHTML()
-    this.initial = value
-    if (value) {
-      this.input.value = value
-    }
-  },
-
-  value: function () {
-    return this.input.value
-  },
-
-  onKeyPress: function (e) {
-    if (e.key === 'Enter' && (e.shiftKey || e.ctrlKey)) {
-      L.DomEvent.stop(e)
-      this.finish()
-    }
-  },
-})
-
-L.FormBuilder.Input = L.FormBuilder.Element.extend({
-  build: function () {
-    this.input = L.DomUtil.create(
-      'input',
-      this.options.className || '',
-      this.parentNode
-    )
-    this.input.type = this.type()
-    this.input.name = this.name
-    this.input._helper = this
-    if (this.options.placeholder) {
-      this.input.placeholder = this.options.placeholder
-    }
-    if (this.options.min !== undefined) {
-      this.input.min = this.options.min
-    }
-    if (this.options.max !== undefined) {
-      this.input.max = this.options.max
-    }
-    if (this.options.step) {
-      this.input.step = this.options.step
-    }
-    this.fetch()
-    L.DomEvent.on(this.input, this.getSyncEvent(), this.sync, this)
-    L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
-  },
-
-  fetch: function () {
-    const value = this.toHTML() !== undefined ? this.toHTML() : null
-    this.initial = value
-    this.input.value = value
-  },
-
-  getSyncEvent: () => 'input',
-
-  type: function () {
-    return this.options.type || 'text'
-  },
-
-  value: function () {
-    return this.input.value || undefined
-  },
-
-  onKeyDown: function (e) {
-    if (e.key === 'Enter') {
-      L.DomEvent.stop(e)
-      this.finish()
-    }
-  },
-})
-
-L.FormBuilder.BlurInput = L.FormBuilder.Input.extend({
-  getSyncEvent: () => 'blur',
-
-  build: function () {
-    L.FormBuilder.Input.prototype.build.call(this)
-    L.DomEvent.on(this.input, 'focus', this.fetch, this)
-  },
-
-  finish: function () {
-    this.sync()
-    L.FormBuilder.Input.prototype.finish.call(this)
-  },
-
-  sync: function () {
-    // Do not commit any change if user only clicked
-    // on the field than clicked outside
-    if (this.initial !== this.value()) {
-      L.FormBuilder.Input.prototype.sync.call(this)
-    }
-  },
-})
-
-L.FormBuilder.IntegerMixin = {
-  value: function () {
-    return !isNaN(this.input.value) && this.input.value !== ''
-      ? parseInt(this.input.value, 10)
-      : undefined
-  },
-
-  type: () => 'number',
-}
-
-L.FormBuilder.IntInput = L.FormBuilder.Input.extend({
-  includes: [L.FormBuilder.IntegerMixin],
-})
-
-L.FormBuilder.BlurIntInput = L.FormBuilder.BlurInput.extend({
-  includes: [L.FormBuilder.IntegerMixin],
-})
-
-L.FormBuilder.FloatMixin = {
-  value: function () {
-    return !isNaN(this.input.value) && this.input.value !== ''
-      ? parseFloat(this.input.value)
-      : undefined
-  },
-
-  type: () => 'number',
-}
-
-L.FormBuilder.FloatInput = L.FormBuilder.Input.extend({
-  options: {
-    step: 'any',
-  },
-
-  includes: [L.FormBuilder.FloatMixin],
-})
-
-L.FormBuilder.BlurFloatInput = L.FormBuilder.BlurInput.extend({
-  options: {
-    step: 'any',
-  },
-
-  includes: [L.FormBuilder.FloatMixin],
-})
-
-L.FormBuilder.CheckBox = L.FormBuilder.Element.extend({
-  build: function () {
-    const container = L.DomUtil.create('div', 'checkbox-wrapper', this.parentNode)
-    this.input = L.DomUtil.create('input', this.options.className || '', container)
-    this.input.type = 'checkbox'
-    this.input.name = this.name
-    this.input._helper = this
-    this.fetch()
-    L.DomEvent.on(this.input, 'change', this.sync, this)
-  },
-
-  fetch: function () {
-    this.initial = this.toHTML()
-    this.input.checked = this.initial === true
-  },
-
-  value: function () {
-    return this.input.checked
-  },
-
-  toHTML: function () {
-    return [1, true].indexOf(this.get()) !== -1
-  },
-})
-
-L.FormBuilder.Select = L.FormBuilder.Element.extend({
-  selectOptions: [['value', 'label']],
-
-  build: function () {
-    this.select = L.DomUtil.create('select', '', this.parentNode)
-    this.select.name = this.name
-    this.validValues = []
-    this.buildOptions()
-    L.DomEvent.on(this.select, 'change', this.sync, this)
-  },
-
-  getOptions: function () {
-    return this.options.selectOptions || this.selectOptions
-  },
-
-  fetch: function () {
-    this.buildOptions()
-  },
-
-  buildOptions: function () {
-    this.select.innerHTML = ''
-    for (const option of this.getOptions()) {
-      if (typeof option === 'string') this.buildOption(option, option)
-      else this.buildOption(option[0], option[1])
-    }
-  },
-
-  buildOption: function (value, label) {
-    this.validValues.push(value)
-    const option = L.DomUtil.create('option', '', this.select)
-    option.value = value
-    option.innerHTML = label
-    if (this.toHTML() === value) {
-      option.selected = 'selected'
-    }
-  },
-
-  value: function () {
-    if (this.select[this.select.selectedIndex])
-      return this.select[this.select.selectedIndex].value
-  },
-
-  getDefault: function () {
-    return this.getOptions()[0][0]
-  },
-
-  toJS: function () {
-    const value = this.value()
-    if (this.validValues.indexOf(value) !== -1) {
-      return value
-    }
-    return this.getDefault()
-  },
-})
-
-L.FormBuilder.IntSelect = L.FormBuilder.Select.extend({
-  value: function () {
-    return parseInt(L.FormBuilder.Select.prototype.value.apply(this), 10)
-  },
-})
-
-L.FormBuilder.NullableBoolean = L.FormBuilder.Select.extend({
-  selectOptions: [
-    [undefined, 'inherit'],
-    [true, 'yes'],
-    [false, 'no'],
-  ],
-
-  toJS: function () {
-    let value = this.value()
-    switch (value) {
-      case 'true':
-      case true:
-        value = true
-        break
-      case 'false':
-      case false:
-        value = false
-        break
-      default:
-        value = undefined
-    }
-    return value
-  },
-})
diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html
index f6aca61ef..974739315 100644
--- a/umap/templates/umap/js.html
+++ b/umap/templates/umap/js.html
@@ -30,8 +30,6 @@
 <script src="{% static 'umap/vendors/fullscreen/Leaflet.fullscreen.min.js' %}"
         defer></script>
 <script src="{% static 'umap/vendors/toolbar/leaflet.toolbar.js' %}" defer></script>
-<script src="{% static 'umap/vendors/formbuilder/Leaflet.FormBuilder.js' %}"
-        defer></script>
 <script src="{% static 'umap/vendors/measurable/Leaflet.Measurable.js' %}"
         defer></script>
 <script src="{% static 'umap/vendors/iconlayers/iconLayers.js' %}" defer></script>
@@ -40,7 +38,6 @@
 <script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}"
         defer></script>
 <script src="{% static 'umap/js/umap.core.js' %}" defer></script>
-<script src="{% static 'umap/js/umap.forms.js' %}" defer></script>
 <script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
 <script type="module" src="{% static 'umap/js/components/fragment.js' %}" defer></script>
 {% endautoescape %}
diff --git a/umap/tests/integration/test_edit_datalayer.py b/umap/tests/integration/test_edit_datalayer.py
index f9a9fa076..a14c87640 100644
--- a/umap/tests/integration/test_edit_datalayer.py
+++ b/umap/tests/integration/test_edit_datalayer.py
@@ -103,7 +103,7 @@ def test_can_change_icon_class(live_server, openmap, page):
     expect(page.locator(".umap-circle-icon")).to_be_hidden()
     page.locator(".panel.right").get_by_title("Edit", exact=True).click()
     page.get_by_text("Shape properties").click()
-    page.locator(".umap-field-iconClass a.define").click()
+    page.locator(".umap-field-iconClass button.define").click()
     page.get_by_text("Circle", exact=True).click()
     expect(page.locator(".umap-circle-icon")).to_be_visible()
     expect(page.locator(".umap-div-icon")).to_be_hidden()
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)
     )
diff --git a/umap/tests/integration/test_edit_polygon.py b/umap/tests/integration/test_edit_polygon.py
index 5f60087bd..ec1ce7cc5 100644
--- a/umap/tests/integration/test_edit_polygon.py
+++ b/umap/tests/integration/test_edit_polygon.py
@@ -101,7 +101,7 @@ def test_can_remove_stroke(live_server, openmap, page, bootstrap):
     page.get_by_role("link", name="Toggle edit mode").click()
     page.get_by_text("Shape properties").click()
     page.locator(".umap-field-stroke .define").first.click()
-    page.locator(".umap-field-stroke label").first.click()
+    page.locator(".umap-field-stroke .show-on-defined label").first.click()
     expect(page.locator(".leaflet-overlay-pane path[stroke='DarkBlue']")).to_have_count(
         0
     )
diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py
index d4b38954c..f561a4595 100644
--- a/umap/tests/integration/test_picto.py
+++ b/umap/tests/integration/test_picto.py
@@ -57,7 +57,7 @@ def test_can_change_picto_at_map_level(openmap, live_server, page, pictos):
     define.click()
     # No picto defined yet, so recent should not be visible
     expect(page.get_by_text("Recent")).to_be_hidden()
-    symbols = page.locator(".umap-pictogram-choice")
+    symbols = page.locator(".umap-pictogram-body .umap-pictogram-choice")
     expect(symbols).to_have_count(2)
     search = page.locator(".umap-pictogram-body input")
     search.type("star")
@@ -90,8 +90,8 @@ def test_can_change_picto_at_datalayer_level(openmap, live_server, page, pictos)
     expect(define).to_be_visible()
     expect(undefine).to_be_hidden()
     define.click()
-    # Map has an icon defined, so it shold open on Recent tab
-    symbols = page.locator(".umap-pictogram-choice")
+    # Map has an icon defined, so it should open on Recent tab
+    symbols = page.locator(".umap-pictogram-body .umap-pictogram-choice")
     expect(page.get_by_text("Recent")).to_be_visible()
     expect(symbols).to_have_count(1)
     symbol_tab = page.get_by_role("button", name="Symbol")
@@ -128,8 +128,8 @@ def test_can_change_picto_at_marker_level(openmap, live_server, page, pictos):
     expect(define).to_be_visible()
     expect(undefine).to_be_hidden()
     define.click()
-    # Map has an icon defined, so it shold open on Recent tab
-    symbols = page.locator(".umap-pictogram-choice")
+    # Map has an icon defined, so it shuold open on Recent tab
+    symbols = page.locator(".umap-pictogram-body .umap-pictogram-choice")
     expect(page.get_by_text("Recent")).to_be_visible()
     expect(symbols).to_have_count(1)
     symbol_tab = page.get_by_role("button", name="Symbol")
@@ -180,7 +180,7 @@ def test_can_use_remote_url_as_picto(openmap, live_server, page, pictos):
     expect(modify).to_be_visible()
     modify.click()
     # Should be on Recent tab
-    symbols = page.locator(".umap-pictogram-choice")
+    symbols = page.locator(".umap-pictogram-body .umap-pictogram-choice")
     expect(page.get_by_text("Recent")).to_be_visible()
     expect(symbols).to_have_count(1)
 
@@ -215,10 +215,10 @@ def test_can_use_char_as_picto(openmap, live_server, page, pictos):
     close.click()
     edit_settings.click()
     shape_settings.click()
-    preview = page.locator(".umap-pictogram-choice")
+    preview = page.locator(".header .umap-pictogram-choice")
     expect(preview).to_be_visible()
     preview.click()
     # Should be on URL tab
-    symbols = page.locator(".umap-pictogram-choice")
+    symbols = page.locator(".umap-pictogram-body .umap-pictogram-choice")
     expect(page.get_by_text("Recent")).to_be_visible()
     expect(symbols).to_have_count(1)
diff --git a/umap/tests/integration/test_websocket_sync.py b/umap/tests/integration/test_websocket_sync.py
index f4e3d7f80..96946ce8c 100644
--- a/umap/tests/integration/test_websocket_sync.py
+++ b/umap/tests/integration/test_websocket_sync.py
@@ -187,9 +187,11 @@ def test_websocket_connection_can_sync_map_properties(
     # Zoom control is synced
     peerB.get_by_role("link", name="Map advanced properties").click()
     peerB.locator("summary").filter(has_text="User interface options").click()
-    peerB.locator("div").filter(
-        has_text=re.compile(r"^Display the zoom control")
-    ).locator("label").nth(2).click()
+    switch = peerB.locator("div.formbox").filter(
+        has_text=re.compile("Display the zoom control")
+    )
+    expect(switch).to_be_visible()
+    switch.get_by_text("Never").click()
 
     expect(peerA.locator(".leaflet-control-zoom")).to_be_hidden()
 
@@ -278,7 +280,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
     peerB.locator("path").nth(1).drag_to(b_map_el, target_position={"x": 400, "y": 400})
     peerB.locator("path").nth(1).click()
     peerB.locator("summary").filter(has_text="Shape properties").click()
-    peerB.locator(".header > a:nth-child(2)").first.click()
+    peerB.locator(".umap-field-color button.define").first.click()
     peerB.get_by_title("Orchid", exact=True).first.click()
     peerB.locator("#map").press("Escape")
     peerB.get_by_role("button", name="Save").click()