refactor: Separate ui rendering from data updates

`U.SCHEMA` now contains an `impacts` key, which makes it possible to
specify what part of the UI is impacted by data changes.

A new `render` method has been added on `U.Map` and `U.DataLayer`, which
is used to rerender the proper parts of the UI depending on the passed
properties.

`U.FormBuilder` calls this `render()` method (if present), during form
changes.
This commit is contained in:
Alexis Métaireau 2024-03-12 09:39:22 +01:00
parent 99a0007ce0
commit 55cc7a098f
5 changed files with 416 additions and 330 deletions

View file

@ -1,45 +1,52 @@
import { translate } from './i18n.js'
// Possible impacts
// ['ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', 'background']
export const SCHEMA = {
zoom: {
type: Number,
},
scrollWheelZoom: {
browsable: {
type: Boolean,
label: translate('Allow scroll wheel zoom?'),
impacts: ['ui'],
},
scaleControl: {
captionBar: {
impacts: ['ui'],
type: Boolean,
label: translate('Do you want to display the scale control?'),
label: translate('Do you want to display a caption bar?'),
default: false,
impacts: [],
},
captionMenus: {
impacts: ['ui'],
type: Boolean,
label: translate('Do you want to display caption menus?'),
default: true,
},
moreControl: {
type: Boolean,
label: translate('Do you want to display the «more» control?'),
default: true,
},
miniMap: {
type: Boolean,
label: translate('Do you want to display a minimap?'),
default: false,
},
displayPopupFooter: {
type: Boolean,
label: translate('Do you want to display popup footer?'),
default: false,
},
onLoadPanel: {
color: {
impacts: ['data'],
type: String,
label: translate('Do you want to display a panel on load?'),
choices: [
['none', translate('None')],
['caption', translate('Caption')],
['databrowser', translate('Data browser')],
['facet', translate('Facet search')],
],
default: 'none',
handler: 'ColorPicker',
label: translate('color'),
helpEntries: 'colorValue',
inheritable: true,
default: 'DarkBlue',
},
dashArray: {
impacts: ['data'],
type: String,
label: translate('dash array'),
helpEntries: 'dashArray',
inheritable: true,
},
datalayersControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
handler: 'DataLayersControl',
label: translate('Display the data layers control'),
default: true,
},
defaultView: {
impacts: [], // no need to update the ui, only useful when loading the map
type: String,
label: translate('Default view'),
choices: [
@ -50,37 +57,80 @@ export const SCHEMA = {
],
default: 'center',
},
name: {
type: String,
label: translate('name'),
},
description: {
label: translate('description'),
impacts: ['ui'],
type: 'Text',
helpEntries: 'textFormatting',
},
licence: {
type: String,
label: translate('licence'),
displayOnLoad: {
type: Boolean,
impacts: [],
},
tilelayer: {
type: Object,
displayPopupFooter: {
type: Boolean,
impacts: ['ui'],
label: translate('Do you want to display popup footer?'),
default: false,
},
overlay: {
type: Object,
easing: { impacts: [], type: Boolean, default: false },
editinosmControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the control to open OpenStreetMap editor'),
default: null,
},
limitBounds: {
type: Object,
embedControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the embed control'),
default: true,
},
color: {
facetKey: { impacts: ['ui'], type: String },
fill: {
impacts: ['data'],
type: Boolean,
label: translate('fill'),
helpEntries: 'fill',
inheritable: true,
default: true,
},
fillColor: {
impacts: ['data'],
type: String,
handler: 'ColorPicker',
label: translate('color'),
helpEntries: 'colorValue',
label: translate('fill color'),
helpEntries: 'fillColor',
inheritable: true,
default: 'DarkBlue',
},
fillOpacity: {
impacts: ['data'],
type: Number,
min: 0.1,
max: 1,
step: 0.1,
label: translate('fill opacity'),
inheritable: true,
default: 0.3,
},
filterKey: { impacts: [], type: String },
fromZoom: {
impacts: [], // not needed
type: Number,
label: translate('From zoom'),
helpText: translate('Optional.'),
},
fullscreenControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the fullscreen control'),
default: true,
},
iconClass: {
impacts: ['data'],
type: String,
label: translate('Icon shape'),
inheritable: true,
@ -92,26 +142,8 @@ export const SCHEMA = {
],
default: 'Default',
},
iconUrl: {
type: String,
handler: 'IconUrl',
label: translate('Icon symbol'),
inheritable: true,
// helpText: translate(
// 'Symbol can be either a unicode character or an URL. You can use feature properties as variables: ex.: with "http://myserver.org/images/{name}.png", the {name} variable will be replaced by the "name" value of each marker.'
// ),
},
smoothFactor: {
type: Number,
min: 0,
max: 10,
step: 0.5,
label: translate('Simplify'),
helpEntries: 'smoothFactor',
inheritable: true,
default: 1.0,
},
iconOpacity: {
impacts: ['data'],
type: Number,
min: 0.1,
max: 1,
@ -120,132 +152,28 @@ export const SCHEMA = {
inheritable: true,
default: 1,
},
opacity: {
type: Number,
min: 0.1,
max: 1,
step: 0.1,
label: translate('opacity'),
iconUrl: {
impacts: ['data'],
type: String,
handler: 'IconUrl',
label: translate('Icon symbol'),
inheritable: true,
default: 0.5,
},
weight: {
type: Number,
min: 1,
max: 20,
step: 1,
label: translate('weight'),
inheritable: true,
default: 3,
},
fill: {
inCaption: {
type: Boolean,
label: translate('fill'),
helpEntries: 'fill',
impacts: ['ui'],
},
interactive: {
impacts: ['data'],
type: Boolean,
label: translate('Allow interactions'),
helpEntries: 'interactive',
inheritable: true,
default: true,
},
fillColor: {
type: String,
handler: 'ColorPicker',
label: translate('fill color'),
helpEntries: 'fillColor',
inheritable: true,
},
fillOpacity: {
type: Number,
min: 0.1,
max: 1,
step: 0.1,
label: translate('fill opacity'),
inheritable: true,
default: 0.3,
},
dashArray: {
type: String,
label: translate('dash array'),
helpEntries: 'dashArray',
inheritable: true,
},
popupShape: {
type: String,
label: translate('Popup shape'),
inheritable: true,
choices: [
['Default', translate('Popup')],
['Large', translate('Popup (large)')],
['Panel', translate('Side panel')],
],
default: 'Default',
},
popupTemplate: {
type: String,
label: translate('Popup content style'),
inheritable: true,
choices: [
['Default', translate('Default')],
['Table', translate('Table')],
['GeoRSSImage', translate('GeoRSS (title + image)')],
['GeoRSSLink', translate('GeoRSS (only link)')],
['OSM', translate('OpenStreetMap')],
],
default: 'Default',
},
popupContentTemplate: {
type: 'Text',
label: translate('Popup content template'),
helpEntries: ['dynamicProperties', 'textFormatting'],
placeholder: '# {name}',
inheritable: true,
default: '# {name}\n{description}',
},
zoomTo: {
type: Number,
placeholder: translate('Inherit'),
helpEntries: 'zoomTo',
label: translate('Default zoom level'),
inheritable: true,
},
captionBar: {
type: Boolean,
label: translate('Do you want to display a caption bar?'),
default: false,
},
captionMenus: {
type: Boolean,
label: translate('Do you want to display caption menus?'),
default: true,
},
slideshow: {
type: Object,
},
sortKey: {
type: String,
},
labelKey: {
type: String,
helpEntries: 'labelKey',
placeholder: translate('Default: name'),
label: translate('Label key'),
inheritable: true,
},
filterKey: {
type: String,
},
facetKey: {
type: String,
},
slugKey: {
type: String,
},
showLabel: {
type: Boolean,
nullable: true,
label: translate('Display label'),
inheritable: true,
default: false,
},
labelDirection: {
impacts: ['data'],
type: String,
label: translate('Label direction'),
inheritable: true,
@ -259,11 +187,87 @@ export const SCHEMA = {
default: 'auto',
},
labelInteractive: {
impacts: ['data'],
type: Boolean,
label: translate('Labels are clickable'),
inheritable: true,
},
labelKey: {
impacts: ['data'],
type: String,
helpEntries: 'labelKey',
placeholder: translate('Default: name'),
label: translate('Label key'),
inheritable: true,
},
licence: { impacts: ['ui'], type: String, label: translate('licence') },
limitBounds: { impacts: ['limit-bounds'], type: Object },
locateControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the locate control'),
},
longCredit: {
impacts: ['ui'],
type: 'Text',
label: translate('Long credits'),
helpEntries: ['longCredit', 'textFormatting'],
},
measureControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the measure control'),
},
miniMap: {
impacts: ['ui'],
type: Boolean,
label: translate('Do you want to display a minimap?'),
default: false,
},
moreControl: {
impacts: ['ui'],
type: Boolean,
label: translate('Do you want to display the «more» control?'),
default: true,
},
name: {
impacts: ['ui', 'data'],
type: String,
label: translate('name'),
},
onLoadPanel: {
impacts: [], // This is what happens during the map instantiation
type: String,
label: translate('Do you want to display a panel on load?'),
choices: [
['none', translate('None')],
['caption', translate('Caption')],
['databrowser', translate('Data browser')],
['facet', translate('Facet search')],
],
default: 'none',
},
opacity: {
impacts: ['data'],
type: Number,
min: 0.1,
max: 1,
step: 0.1,
label: translate('opacity'),
inheritable: true,
default: 0.5,
},
outlink: {
type: String,
label: translate('Link to…'),
helpEntries: 'outlink',
placeholder: 'http://...',
inheritable: true,
},
outlinkTarget: {
impacts: [],
type: String,
label: translate('Open link in…'),
inheritable: true,
@ -274,115 +278,162 @@ export const SCHEMA = {
['parent', translate('parent window')],
],
},
shortCredit: {
type: String,
label: translate('Short credits'),
helpEntries: ['shortCredit', 'textFormatting'],
},
longCredit: {
type: 'Text',
label: translate('Long credits'),
helpEntries: ['longCredit', 'textFormatting'],
},
overlay: { impacts: ['background'], type: Object },
permanentCredit: {
impacts: ['ui'],
type: 'Text',
label: translate('Permanent credits'),
helpEntries: ['permanentCredit', 'textFormatting'],
},
permanentCreditBackground: {
impacts: ['ui'],
type: Boolean,
label: translate('Permanent credits background'),
default: true,
},
zoomControl: {
popupContentTemplate: {
impacts: [], // not needed
type: 'Text',
label: translate('Popup content template'),
helpEntries: ['dynamicProperties', 'textFormatting'],
placeholder: '# {name}',
inheritable: true,
default: '# {name}\n{description}',
},
popupShape: {
impacts: [], // not needed
type: String,
label: translate('Popup shape'),
inheritable: true,
choices: [
['Default', translate('Popup')],
['Large', translate('Popup (large)')],
['Panel', translate('Side panel')],
],
default: 'Default',
},
popupTemplate: {
impacts: [], // not needed
type: String,
label: translate('Popup content style'),
inheritable: true,
choices: [
['Default', translate('Default')],
['Table', translate('Table')],
['GeoRSSImage', translate('GeoRSS (title + image)')],
['GeoRSSLink', translate('GeoRSS (only link)')],
['OSM', translate('OpenStreetMap')],
],
default: 'Default',
},
remoteData: {
type: Object,
impacts: ['remote-data'],
},
scaleControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the zoom control'),
label: translate('Do you want to display the scale control?'),
default: true,
},
datalayersControl: {
scrollWheelZoom: {
impacts: ['ui'],
type: Boolean,
nullable: true,
handler: 'DataLayersControl',
label: translate('Display the data layers control'),
default: true,
label: translate('Allow scroll wheel zoom?'),
},
searchControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the search control'),
default: true,
},
locateControl: {
shortCredit: {
impacts: ['ui'],
type: String,
label: translate('Short credits'),
helpEntries: ['shortCredit', 'textFormatting'],
},
showLabel: {
impacts: ['data'],
type: Boolean,
nullable: true,
label: translate('Display the locate control'),
label: translate('Display label'),
inheritable: true,
default: false,
},
fullscreenControl: {
type: Boolean,
nullable: true,
label: translate('Display the fullscreen control'),
default: true,
},
editinosmControl: {
type: Boolean,
nullable: true,
label: translate('Display the control to open OpenStreetMap editor'),
default: null,
},
embedControl: {
type: Boolean,
nullable: true,
label: translate('Display the embed control'),
default: true,
},
measureControl: {
type: Boolean,
nullable: true,
label: translate('Display the measure control'),
},
tilelayersControl: {
type: Boolean,
nullable: true,
label: translate('Display the tile layers control'),
slideshow: { impacts: ['ui'], type: Object },
slugKey: { impacts: [], type: String },
smoothFactor: {
impacts: ['data'],
type: Number,
min: 0,
max: 10,
step: 0.5,
label: translate('Simplify'),
helpEntries: 'smoothFactor',
inheritable: true,
default: 1.0,
},
sortKey: { impacts: ['data', 'datalayer-index'], type: String },
starControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the star map button'),
},
easing: {
type: Boolean,
default: false,
},
interactive: {
type: Boolean,
label: translate('Allow interactions'),
helpEntries: 'interactive',
inheritable: true,
default: true,
},
fromZoom: {
type: Number,
label: translate('From zoom'),
helpText: translate('Optional.'),
},
toZoom: {
type: Number,
label: translate('To zoom'),
helpText: translate('Optional.'),
},
stroke: {
impacts: ['data'],
type: Boolean,
label: translate('stroke'),
helpEntries: 'stroke',
inheritable: true,
default: true,
},
outlink: {
label: translate('Link to…'),
helpEntries: 'outlink',
placeholder: 'http://...',
tilelayer: { impacts: ['background'], type: Object },
tilelayersControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the tile layers control'),
},
toZoom: {
type: Number,
impacts: [], // not needed
label: translate('To zoom'),
helpText: translate('Optional.'),
},
type: {
type: 'String',
impacts: ['data'],
},
weight: {
impacts: ['data'],
type: Number,
min: 1,
max: 20,
step: 1,
label: translate('weight'),
inheritable: true,
default: 3,
},
zoom: {
impacts: [], // default zoom, doesn't need to be updated
type: Number,
},
zoomControl: {
impacts: ['ui'],
type: Boolean,
nullable: true,
label: translate('Display the zoom control'),
default: true,
},
zoomTo: {
impacts: [], // not need to update the view
type: Number,
placeholder: translate('Inherit'),
helpEntries: 'zoomTo',
label: translate('Default zoom level'),
inheritable: true,
},
}

View file

@ -25,6 +25,31 @@ export function checkId(string) {
return /^[A-Za-z0-9]{5}$/.test(string)
}
/**
* Compute the impacts for a given list of fields.
*
* Return a set containing the impacts.
*
* @param {fields} list[fields]
* @returns Set[string]
*/
export function getImpactsFromSchema(fields) {
return fields
.map((field) => {
// remove the option prefix for fields
// And only keep the first part in case of a subfield
// (e.g "options.limitBounds.foobar" will just return "limitBounds")
return field.replace('options.', '').split('.')[0]
})
.reduce((acc, field) => {
// retrieve the "impacts" field from the schema
// and merge them together using sets
const impacts = U.SCHEMA[field]?.impacts || []
impacts.forEach((impact) => acc.add(impact))
return acc
}, new Set())
}
/**
* Import DOM purify, and initialize it.
*
@ -303,5 +328,5 @@ export function template(str, data) {
value = value(data)
}
return value
})
}
}

View file

@ -1032,7 +1032,10 @@ U.FormBuilder = L.FormBuilder.extend({
setter: function (field, value) {
L.FormBuilder.prototype.setter.call(this, field, value)
if (this.options.makeDirty !== false) this.obj.isDirty = true
if (this.options.makeDirty !== false) {
this.obj.isDirty = true
if ('render' in this.obj) this.obj.render([field], this)
}
},
finish: function () {

View file

@ -69,6 +69,7 @@ U.Map = L.Map.extend({
this.options.zoomControl = zoomControl !== undefined ? zoomControl : true
this.options.fullscreenControl =
fullscreenControl !== undefined ? fullscreenControl : true
this.datalayersFromQueryString = L.Util.queryString('datalayers')
if (this.datalayersFromQueryString) {
this.datalayersFromQueryString = this.datalayersFromQueryString
@ -242,6 +243,42 @@ U.Map = L.Map.extend({
this.on('click contextmenu.show', this.closeInplaceToolbar)
},
render: function (fields) {
let impacts = U.Utils.getImpactsFromSchema(fields)
for (let impact of impacts) {
switch (impact) {
case 'ui':
this.initCaptionBar()
this.renderEditToolbar()
this.renderControls()
break
case 'data':
this.redrawVisibleDataLayers()
break
case 'datalayer-index':
this.reindexDataLayers()
break
case 'background':
this.initTileLayers()
break
case 'bounds':
this.handleLimitBounds()
break
}
}
},
reindexDataLayers: function () {
this.eachDataLayer((datalayer) => datalayer.reindex())
},
redrawVisibleDataLayers: function () {
this.eachVisibleDataLayer((datalayer) => {
datalayer.redraw()
})
},
setOptionsFromQueryString: function (options) {
// This is not an editable option
L.Util.setFromQueryString(options, 'editMode')
@ -269,6 +306,8 @@ U.Map = L.Map.extend({
}
},
// Merge the given schema with the default one
// Missing keys inside the schema are merged with the default ones.
overrideSchema: function (schema) {
for (const [key, extra] of Object.entries(schema)) {
U.SCHEMA[key] = L.extend({}, U.SCHEMA[key], extra)
@ -1128,13 +1167,7 @@ U.Map = L.Map.extend({
'options.captionBar',
'options.captionMenus',
])
builder = new U.FormBuilder(this, UIFields, {
callback: function () {
this.renderControls()
this.initCaptionBar()
},
callbackContext: this,
})
builder = new U.FormBuilder(this, UIFields)
const controlsOptions = L.DomUtil.createFieldset(
container,
L._('User interface options')
@ -1157,14 +1190,7 @@ U.Map = L.Map.extend({
'options.dashArray',
]
builder = new U.FormBuilder(this, shapeOptions, {
callback: function (e) {
if (this._controls.miniMap) this.renderControls()
this.eachVisibleDataLayer((datalayer) => {
datalayer.redraw()
})
},
})
builder = new U.FormBuilder(this, shapeOptions)
const defaultShapeProperties = L.DomUtil.createFieldset(
container,
L._('Default shape properties')
@ -1217,14 +1243,7 @@ U.Map = L.Map.extend({
],
]
builder = new U.FormBuilder(this, optionsFields, {
callback: function (e) {
this.initCaptionBar()
if (e.helper.field === 'options.sortKey') {
this.eachDataLayer((datalayer) => datalayer.reindex())
}
},
})
builder = new U.FormBuilder(this, optionsFields)
const defaultProperties = L.DomUtil.createFieldset(
container,
L._('Default properties')
@ -1242,20 +1261,7 @@ U.Map = L.Map.extend({
'options.labelInteractive',
'options.outlinkTarget',
]
builder = new U.FormBuilder(this, popupFields, {
callback: function (e) {
if (
e.helper.field === 'options.popupTemplate' ||
e.helper.field === 'options.popupContentTemplate' ||
e.helper.field === 'options.popupShape' ||
e.helper.field === 'options.outlinkTarget'
)
return
this.eachVisibleDataLayer((datalayer) => {
datalayer.redraw()
})
},
})
builder = new U.FormBuilder(this, popupFields)
const popupFieldset = L.DomUtil.createFieldset(
container,
L._('Default interaction options')
@ -1309,10 +1315,7 @@ U.Map = L.Map.extend({
container,
L._('Custom background')
)
builder = new U.FormBuilder(this, tilelayerFields, {
callback: this.initTileLayers,
callbackContext: this,
})
builder = new U.FormBuilder(this, tilelayerFields)
customTilelayer.appendChild(builder.build())
},
@ -1360,10 +1363,7 @@ U.Map = L.Map.extend({
['options.overlay.tms', { handler: 'Switch', label: L._('TMS format') }],
]
const overlay = L.DomUtil.createFieldset(container, L._('Custom overlay'))
builder = new U.FormBuilder(this, overlayFields, {
callback: this.initTileLayers,
callbackContext: this,
})
builder = new U.FormBuilder(this, overlayFields)
overlay.appendChild(builder.build())
},
@ -1390,10 +1390,7 @@ U.Map = L.Map.extend({
{ handler: 'BlurFloatInput', placeholder: L._('max East') },
],
]
const boundsBuilder = new U.FormBuilder(this, boundsFields, {
callback: this.handleLimitBounds,
callbackContext: this,
})
const boundsBuilder = new U.FormBuilder(this, boundsFields)
limitBounds.appendChild(boundsBuilder.build())
const boundsButtons = L.DomUtil.create('div', 'button-bar half', limitBounds)
L.DomUtil.createButton(
@ -1454,7 +1451,6 @@ U.Map = L.Map.extend({
]
const slideshowHandler = function () {
this.slideshow.setOptions(this.options.slideshow)
this.renderControls()
}
const slideshowBuilder = new U.FormBuilder(this, slideshowFields, {
callback: slideshowHandler,
@ -1472,10 +1468,7 @@ U.Map = L.Map.extend({
'options.permanentCredit',
'options.permanentCreditBackground',
]
const creditsBuilder = new U.FormBuilder(this, creditsFields, {
callback: this.renderControls,
callbackContext: this,
})
const creditsBuilder = new U.FormBuilder(this, creditsFields)
credits.appendChild(creditsBuilder.build())
},

View file

@ -254,16 +254,18 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
},
onEdit: function (field, builder) {
// Only compute the breaks if we're dealing with choropleth
if (!field.startsWith('options.choropleth')) return
// If user touches the breaks, then force manual mode
if (field === 'options.choropleth.breaks') {
this.datalayer.options.choropleth.mode = 'manual'
builder.helpers['options.choropleth.mode'].fetch()
if (builder) builder.helpers['options.choropleth.mode'].fetch()
}
this.computeBreaks()
// If user changes the mode or the number of classes,
// then update the breaks input value
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
builder.helpers['options.choropleth.breaks'].fetch()
if (builder) builder.helpers['options.choropleth.breaks'].fetch()
}
},
@ -594,6 +596,31 @@ U.DataLayer = L.Evented.extend({
if (this.autoLoaded()) this.map.on('zoomend', this.onZoomEnd, this)
},
render: function (fields, builder) {
let impacts = U.Utils.getImpactsFromSchema(fields)
for (let impact of impacts) {
switch (impact) {
case 'ui':
this.map.updateDatalayersControl()
break
case 'data':
if (fields.includes('options.type')) {
this.resetLayer()
}
this.hide()
fields.forEach((field) => {
this.layer.onEdit(field, builder)
})
this.show()
break
case 'remote-data':
this.fetchRemoteData()
break
}
}
},
onMoveEnd: function (e) {
if (this.isRemoteLayer() && this.showAtZoom()) this.fetchRemoteData()
},
@ -1189,29 +1216,18 @@ U.DataLayer = L.Evented.extend({
const title = L.DomUtil.add('h3', '', container, L._('Layer properties'))
let builder = new U.FormBuilder(this, metadataFields, {
callback: function (e) {
this.map.updateDatalayersControl()
if (e.helper.field === 'options.type') {
this.resetLayer()
this.edit()
}
},
})
container.appendChild(builder.build())
const redrawCallback = function (e) {
const field = e.helper.field,
builder = e.helper.builder
this.hide()
this.layer.onEdit(field, builder)
this.show()
}
const layerOptions = this.layer.getEditableOptions()
if (layerOptions.length) {
builder = new U.FormBuilder(this, layerOptions, {
id: 'datalayer-layer-properties',
callback: redrawCallback,
})
const layerProperties = L.DomUtil.createFieldset(
container,
@ -1235,7 +1251,6 @@ U.DataLayer = L.Evented.extend({
builder = new U.FormBuilder(this, shapeOptions, {
id: 'datalayer-advanced-properties',
callback: redrawCallback,
})
const shapeProperties = L.DomUtil.createFieldset(container, L._('Shape properties'))
shapeProperties.appendChild(builder.build())
@ -1251,7 +1266,6 @@ U.DataLayer = L.Evented.extend({
builder = new U.FormBuilder(this, optionsFields, {
id: 'datalayer-advanced-properties',
callback: redrawCallback,
})
const advancedProperties = L.DomUtil.createFieldset(
container,
@ -1269,7 +1283,7 @@ U.DataLayer = L.Evented.extend({
'options.outlinkTarget',
'options.interactive',
]
builder = new U.FormBuilder(this, popupFields, { callback: redrawCallback })
builder = new U.FormBuilder(this, popupFields)
const popupFieldset = L.DomUtil.createFieldset(
container,
L._('Interaction options')