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 a6f77a671..c53d91632 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,
@@ -656,7 +655,7 @@ export class DataLayer extends ServerStored {
{
label: translate('Data is browsable'),
handler: 'Switch',
- helpEntries: 'browsable',
+ helpEntries: ['browsable'],
},
],
[
@@ -668,20 +667,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(
@@ -704,7 +702,7 @@ export class DataLayer extends ServerStored {
'options.fillOpacity',
]
- builder = new U.FormBuilder(this, shapeOptions, {
+ builder = new MutatingForm(this, shapeOptions, {
id: 'datalayer-advanced-properties',
})
const shapeProperties = DomUtil.createFieldset(
@@ -721,7 +719,7 @@ export class DataLayer extends ServerStored {
'options.toZoom',
]
- builder = new U.FormBuilder(this, optionsFields, {
+ builder = new MutatingForm(this, optionsFields, {
id: 'datalayer-advanced-properties',
})
const advancedProperties = DomUtil.createFieldset(
@@ -740,7 +738,7 @@ export class DataLayer extends ServerStored {
'options.outlinkTarget',
'options.interactive',
]
- builder = new U.FormBuilder(this, popupFields)
+ builder = new MutatingForm(this, popupFields)
const popupFieldset = DomUtil.createFieldset(
container,
translate('Interaction options')
@@ -796,7 +794,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('
')
+ 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 `
+
+ ${helper.getTemplate()}
+
+
`
+ }
+}
+
+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 = `
+ `
+ } else {
+ template = `
+
+ ${helper.getLabelTemplate()}
+ ${helper.getTemplate()}
+
+
`
+ }
+ 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
+ ? ``
+ : ''
+ }
+
+ fetch() {}
+
+ finish() {}
+
+ undefine() {
+ this.root.classList.add('undefined')
+ this.clear()
+ this.sync()
+ }
+}
+
+Fields.Textarea = class extends BaseElement {
+ getTemplate() {
+ return ``
+ }
+
+ 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 ``
+ }
+
+ 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()}`
+ }
+
+ 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 ``
+ }
+
+ 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 ``
+ }
+
+ 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('')
+ 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 ``
+ }
+
+ 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()}`
+ }
+
+ 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('')
+ 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 `
+
+
+
+ ${super.getTemplate()}
+
+
+
+ `
+ }
+
+ build() {
+ super.build()
+ this.tabs = this.elements.tabs
+ this.body = this.elements.body
+ this.footer = this.elements.footer
+ this.button = Utils.loadTemplate(
+ ``
+ )
+ 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(
+ ``
+ )
+ 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(`
+
+
+
+
+
+
+ `)
+ 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('')
+ 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(
+ ``
+ )
+ 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(`
+
+ `)
+ 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(
+ ``
+ )
+ this.grid = Utils.loadTemplate('')
+ 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('')
+ const button = Utils.loadTemplate('')
+ 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()}`
+ }
+
+ 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 `
+
+ `
+ }
+
+ 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(`
+
+
+
+ `)
+ 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 `
+
+ `
+ }
+
+ 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 ``
+ }
+
+ 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(
+ ``
+ )
+ this.elements.wrapper.appendChild(input)
+ this.elements.wrapper.appendChild(
+ Utils.loadTemplate(``)
+ )
+ 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 += ``
+ }
+ const datalist = Utils.loadTemplate(
+ ``
+ )
+ 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 {
``
)
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 = `
@@ -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 += ``
- }
- 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 @@
-
@@ -40,7 +38,6 @@
-
{% 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 c5e56e893..53c063d6c 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()