From fcf22195cbc3876a827ea824d43e23850609e4b9 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 28 Feb 2024 11:56:32 +0100 Subject: [PATCH] chore: move defaultOptions from forms to schema --- umap/static/umap/js/modules/global.js | 2 +- umap/static/umap/js/modules/schema.js | 416 ++++++++++++++++++++++---- umap/static/umap/js/umap.forms.js | 337 +++------------------ umap/static/umap/js/umap.js | 44 +-- 4 files changed, 404 insertions(+), 395 deletions(-) diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 90bd180f..25ee3505 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,7 +1,7 @@ import URLs from './urls.js' import Browser from './browser.js' import * as Utils from './utils.js' -import SCHEMA from './schema.js' +import {SCHEMA} from './schema.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' // Import modules and export them to the global scope. diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index db3075ca..39b26864 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -1,62 +1,360 @@ import { translate } from './i18n.js' -const SCHEMA = { - zoom: { type: undefined }, - scrollWheelZoom: { type: Boolean }, - scaleControl: { type: Boolean }, - moreControl: { type: Boolean }, - miniMap: { type: Boolean }, - displayPopupFooter: { type: undefined }, - onLoadPanel: { type: String }, - defaultView: { type: String }, - name: { type: String, label: translate('name') }, - description: { type: String }, - licence: { type: undefined }, - tilelayer: { type: undefined }, - overlay: { type: undefined }, - limitBounds: { type: undefined }, - color: { type: String }, - iconClass: { type: String }, - iconUrl: { type: String }, - smoothFactor: { type: undefined }, - iconOpacity: { type: undefined }, - opacity: { type: undefined }, - weight: { type: undefined }, - fill: { type: undefined }, - fillColor: { type: undefined }, - fillOpacity: { type: undefined }, - dashArray: { type: undefined }, - popupShape: { type: String }, - popupTemplate: { type: String }, - popupContentTemplate: { type: String }, - zoomTo: { type: Number }, - captionBar: { type: Boolean }, - captionMenus: { type: Boolean }, - slideshow: { type: undefined }, - sortKey: { type: undefined }, - labelKey: { type: String }, - filterKey: { type: undefined }, - facetKey: { type: undefined }, - slugKey: { type: undefined }, - showLabel: { type: 'NullableBoolean' }, - labelDirection: { type: undefined }, - labelInteractive: { type: undefined }, - outlinkTarget: { type: undefined }, - shortCredit: { type: undefined }, - longCredit: { type: undefined }, - permanentCredit: { type: undefined }, - permanentCreditBackground: { type: undefined }, - zoomControl: { type: 'NullableBoolean' }, - datalayersControl: { type: 'NullableBoolean' }, - searchControl: { type: 'NullableBoolean' }, - locateControl: { type: 'NullableBoolean' }, - fullscreenControl: { type: 'NullableBoolean' }, - editinosmControl: { type: 'NullableBoolean' }, - embedControl: { type: 'NullableBoolean' }, - measureControl: { type: 'NullableBoolean' }, - tilelayersControl: { type: 'NullableBoolean' }, - starControl: { type: 'NullableBoolean' }, - easing: { type: undefined }, +export const SCHEMA = { + zoom: { + type: Number, + }, + scrollWheelZoom: { + type: Boolean, + label: translate('Allow scroll wheel zoom?'), + }, + scaleControl: { + type: Boolean, + label: translate('Do you want to display the scale control?'), + }, + moreControl: { + type: Boolean, + label: translate('Do you want to display the «more» control?'), + }, + miniMap: { + type: Boolean, + label: translate('Do you want to display a minimap?'), + }, + displayPopupFooter: { + type: Boolean, + label: translate('Do you want to display popup footer?'), + }, + onLoadPanel: { + 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', + }, + defaultView: { + type: String, + label: translate('Default view'), + choices: [ + ['center', translate('Saved center and zoom')], + ['data', translate('Fit all data')], + ['latest', translate('Latest feature')], + ['locate', translate('User location')], + ], + default: 'center', + }, + name: { + type: String, + label: translate('name'), + }, + description: { + label: translate('description'), + type: 'Text', + helpEntries: 'textFormatting', + }, + licence: { + type: String, + label: translate('licence'), + }, + tilelayer: { + type: Object, + }, + overlay: { + type: Object, + }, + limitBounds: { + type: Object, + }, + color: { + type: String, + handler: 'ColorPicker', + label: translate('color'), + helpEntries: 'colorValue', + inheritable: true, + }, + iconClass: { + type: String, + label: translate('Icon shape'), + inheritable: true, + choices: [ + ['Default', translate('Default')], + ['Circle', translate('Circle')], + ['Drop', translate('Drop')], + ['Ball', translate('Ball')], + ], + }, + iconUrl: { + type: String, + handler: 'IconUrl', + label: translate('Icon symbol'), + inheritable: true, + helpText: 'formatIconSymbol', + }, + smoothFactor: { + type: Number, + min: 0, + max: 10, + step: 0.5, + label: translate('Simplify'), + helpEntries: 'smoothFactor', + inheritable: true, + }, + iconOpacity: { + type: Number, + min: 0.1, + max: 1, + step: 0.1, + label: translate('icon opacity'), + inheritable: true, + }, + opacity: { + type: Number, + min: 0.1, + max: 1, + step: 0.1, + label: translate('opacity'), + inheritable: true, + }, + weight: { + type: Number, + min: 1, + max: 20, + step: 1, + label: translate('weight'), + inheritable: true, + }, + fill: { + type: Boolean, + label: translate('fill'), + helpEntries: 'fill', + inheritable: 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, + }, + 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, + }, + 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?'), + }, + captionMenus: { + type: Boolean, + label: translate('Do you want to display caption menus?'), + }, + 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: { + type: String, + label: translate('Label direction'), + inheritable: true, + choices: [ + ['auto', translate('Automatic')], + ['left', translate('On the left')], + ['right', translate('On the right')], + ['top', translate('On the top')], + ['bottom', translate('On the bottom')], + ], + }, + labelInteractive: { + type: Boolean, + label: translate('Labels are clickable'), + inheritable: true, + }, + outlinkTarget: { + type: String, + label: translate('Open link in…'), + inheritable: true, + default: 'blank', + choices: [ + ['blank', translate('new window')], + ['self', translate('iframe')], + ['parent', translate('parent window')], + ], + }, + shortCredit: { + type: String, + label: translate('Short credits'), + helpEntries: ['shortCredit', 'textFormatting'], + }, + longCredit: { + type: 'Text', + label: translate('Long credits'), + helpEntries: ['longCredit', 'textFormatting'], + }, + permanentCredit: { + type: 'Text', + label: translate('Permanent credits'), + helpEntries: ['permanentCredit', 'textFormatting'], + }, + permanentCreditBackground: { + type: Boolean, + label: translate('Permanent credits background'), + }, + zoomControl: { + type: Boolean, + nullable: true, + label: translate('Display the zoom control'), + }, + datalayersControl: { + type: Boolean, + nullable: true, + handler: 'DataLayersControl', + label: translate('Display the data layers control'), + }, + searchControl: { + type: Boolean, + nullable: true, + label: translate('Display the search control'), + }, + locateControl: { + type: Boolean, + nullable: true, + label: translate('Display the locate control'), + }, + fullscreenControl: { + type: Boolean, + nullable: true, + label: translate('Display the fullscreen control'), + }, + editinosmControl: { + type: Boolean, + nullable: true, + label: translate('Display the control to open OpenStreetMap editor'), + }, + embedControl: { + type: Boolean, + nullable: true, + label: translate('Display the embed control'), + }, + measureControl: { + type: Boolean, + nullable: true, + label: translate('Display the measure control'), + }, + tilelayersControl: { + type: Boolean, + nullable: true, + label: translate('Display the tile layers control'), + }, + starControl: { + type: Boolean, + nullable: true, + label: translate('Display the star map button'), + }, + easing: { + type: Boolean, + }, + interactive: { + type: Boolean, + label: translate('Allow interactions'), + helpEntries: 'interactive', + inheritable: true, + }, + fromZoom: { + type: Number, + label: translate('From zoom'), + helpText: translate('Optional.'), + }, + toZoom: { + type: Number, + label: translate('To zoom'), + helpText: translate('Optional.'), + }, + stroke: { + type: Boolean, + label: translate('stroke'), + helpEntries: 'stroke', + inheritable: true, + }, + outlink: { + label: translate('Link to…'), + helpEntries: 'outlink', + placeholder: 'http://...', + inheritable: true, + }, } - -export default { SCHEMA } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 7834b499..958329d3 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -340,15 +340,6 @@ L.FormBuilder.TextColorPicker = L.FormBuilder.ColorPicker.extend({ ], }) -L.FormBuilder.IconClassSwitcher = L.FormBuilder.Select.extend({ - selectOptions: [ - ['Default', L._('Default')], - ['Circle', L._('Circle')], - ['Drop', L._('Drop')], - ['Ball', L._('Ball')], - ], -}) - L.FormBuilder.ProxyTTLSelect = L.FormBuilder.Select.extend({ selectOptions: [ [undefined, L._('No cache')], @@ -358,24 +349,6 @@ L.FormBuilder.ProxyTTLSelect = L.FormBuilder.Select.extend({ ], }) -L.FormBuilder.PopupShape = L.FormBuilder.Select.extend({ - selectOptions: [ - ['Default', L._('Popup')], - ['Large', L._('Popup (large)')], - ['Panel', L._('Side panel')], - ], -}) - -L.FormBuilder.PopupContent = L.FormBuilder.Select.extend({ - selectOptions: [ - ['Default', L._('Default')], - ['Table', L._('Table')], - ['GeoRSSImage', L._('GeoRSS (title + image)')], - ['GeoRSSLink', L._('GeoRSS (only link)')], - ['OSM', L._('OpenStreetMap')], - ], -}) - L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({ getOptions: function () { const layer_classes = [ @@ -427,24 +400,6 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({ }, }) -L.FormBuilder.DefaultView = L.FormBuilder.Select.extend({ - selectOptions: [ - ['center', L._('Saved center and zoom')], - ['data', L._('Fit all data')], - ['latest', L._('Latest feature')], - ['locate', L._('User location')], - ], -}) - -L.FormBuilder.OnLoadPanel = L.FormBuilder.Select.extend({ - selectOptions: [ - ['none', L._('None')], - ['caption', L._('Caption')], - ['databrowser', L._('Data browser')], - ['facet', L._('Facet search')], - ], -}) - L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({ selectOptions: [ [undefined, L._('Choose the data format')], @@ -457,16 +412,6 @@ L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({ ], }) -L.FormBuilder.LabelDirection = L.FormBuilder.Select.extend({ - selectOptions: [ - ['auto', L._('Automatic')], - ['left', L._('On the left')], - ['right', L._('On the right')], - ['top', L._('On the top')], - ['bottom', L._('On the bottom')], - ], -}) - L.FormBuilder.LicenceChooser = L.FormBuilder.Select.extend({ getOptions: function () { const licences = [] @@ -905,7 +850,7 @@ L.FormBuilder.TernaryChoices = L.FormBuilder.MultiChoice.extend({ }, }) -L.FormBuilder.ControlChoice = L.FormBuilder.TernaryChoices.extend({ +L.FormBuilder.NullableChoices = L.FormBuilder.TernaryChoices.extend({ choices: [ [true, L._('always')], [false, L._('never')], @@ -913,17 +858,7 @@ L.FormBuilder.ControlChoice = L.FormBuilder.TernaryChoices.extend({ ], }) -L.FormBuilder.LabelChoice = L.FormBuilder.TernaryChoices.extend({ - default: false, - - choices: [ - [true, L._('always')], - [false, L._('never')], - ['null', L._('on hover')], - ], -}) - -L.FormBuilder.DataLayersControl = L.FormBuilder.ControlChoice.extend({ +L.FormBuilder.DataLayersControl = L.FormBuilder.TernaryChoices.extend({ choices: [ [true, L._('collapsed')], ['expanded', L._('expanded')], @@ -934,21 +869,11 @@ L.FormBuilder.DataLayersControl = L.FormBuilder.ControlChoice.extend({ toJS: function () { let value = this.value() if (value !== 'expanded') - value = L.FormBuilder.ControlChoice.prototype.toJS.call(this) + value = L.FormBuilder.TernaryChoices.prototype.toJS.call(this) return value }, }) -L.FormBuilder.OutlinkTarget = L.FormBuilder.MultiChoice.extend({ - default: 'blank', - - choices: [ - ['blank', L._('new window')], - ['self', L._('iframe')], - ['parent', L._('parent window')], - ], -}) - L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({ type: function () { return 'range' @@ -1053,232 +978,44 @@ U.FormBuilder = L.FormBuilder.extend({ }, defaultOptions: { - name: { label: L._('name') }, - description: { - label: L._('description'), - handler: 'Textarea', - helpEntries: 'textFormatting', - }, - color: { - handler: 'ColorPicker', - label: L._('color'), - helpEntries: 'colorValue', - inheritable: true, - }, - iconOpacity: { - handler: 'Range', - min: 0.1, - max: 1, - step: 0.1, - label: L._('icon opacity'), - inheritable: true, - }, - opacity: { - handler: 'Range', - min: 0.1, - max: 1, - step: 0.1, - label: L._('opacity'), - inheritable: true, - }, - stroke: { - handler: 'Switch', - label: L._('stroke'), - helpEntries: 'stroke', - inheritable: true, - }, - weight: { - handler: 'Range', - min: 1, - max: 20, - step: 1, - label: L._('weight'), - inheritable: true, - }, - fill: { - handler: 'Switch', - label: L._('fill'), - helpEntries: 'fill', - inheritable: true, - }, - fillColor: { - handler: 'ColorPicker', - label: L._('fill color'), - helpEntries: 'fillColor', - inheritable: true, - }, - fillOpacity: { - handler: 'Range', - min: 0.1, - max: 1, - step: 0.1, - label: L._('fill opacity'), - inheritable: true, - }, - smoothFactor: { - handler: 'Range', - min: 0, - max: 10, - step: 0.5, - label: L._('Simplify'), - helpEntries: 'smoothFactor', - inheritable: true, - }, - dashArray: { - label: L._('dash array'), - helpEntries: 'dashArray', - inheritable: true, - }, - iconClass: { - handler: 'IconClassSwitcher', - label: L._('Icon shape'), - inheritable: true, - }, - iconUrl: { - handler: 'IconUrl', - label: L._('Icon symbol'), - inheritable: true, - helpText: U.Help.formatIconSymbol, - }, - popupShape: { handler: 'PopupShape', label: L._('Popup shape'), inheritable: true }, - popupTemplate: { - handler: 'PopupContent', - label: L._('Popup content style'), - inheritable: true, - }, - popupContentTemplate: { - label: L._('Popup content template'), - handler: 'Textarea', - helpEntries: ['dynamicProperties', 'textFormatting'], - placeholder: '# {name}', - inheritable: true, - }, - datalayer: { - handler: 'DataLayerSwitcher', - label: L._('Choose the layer of the feature'), - }, - moreControl: { - handler: 'Switch', - label: L._('Do you want to display the «more» control?'), - }, - scrollWheelZoom: { handler: 'Switch', label: L._('Allow scroll wheel zoom?') }, - miniMap: { handler: 'Switch', label: L._('Do you want to display a minimap?') }, - scaleControl: { - handler: 'Switch', - label: L._('Do you want to display the scale control?'), - }, - onLoadPanel: { - handler: 'OnLoadPanel', - label: L._('Do you want to display a panel on load?'), - }, - defaultView: { - handler: 'DefaultView', - label: L._('Default view'), - }, - displayPopupFooter: { - handler: 'Switch', - label: L._('Do you want to display popup footer?'), - }, - captionBar: { - handler: 'Switch', - label: L._('Do you want to display a caption bar?'), - }, - captionMenus: { - handler: 'Switch', - label: L._('Do you want to display caption menus?'), - }, - zoomTo: { - handler: 'IntInput', - placeholder: L._('Inherit'), - helpEntries: 'zoomTo', - label: L._('Default zoom level'), - inheritable: true, - }, - showLabel: { - handler: 'LabelChoice', - label: L._('Display label'), - inheritable: true, - }, - labelDirection: { - handler: 'LabelDirection', - label: L._('Label direction'), - inheritable: true, - }, - labelInteractive: { - handler: 'Switch', - label: L._('Labels are clickable'), - inheritable: true, - }, - outlink: { - label: L._('Link to…'), - helpEntries: 'outlink', - placeholder: 'http://...', - inheritable: true, - }, - outlinkTarget: { - handler: 'OutlinkTarget', - label: L._('Open link in…'), - inheritable: true, - }, - labelKey: { - helpEntries: 'labelKey', - placeholder: L._('Default: name'), - label: L._('Label key'), - inheritable: true, - }, - zoomControl: { handler: 'ControlChoice', label: L._('Display the zoom control') }, - searchControl: { - handler: 'ControlChoice', - label: L._('Display the search control'), - }, - fullscreenControl: { - handler: 'ControlChoice', - label: L._('Display the fullscreen control'), - }, - embedControl: { handler: 'ControlChoice', label: L._('Display the embed control') }, - locateControl: { - handler: 'ControlChoice', - label: L._('Display the locate control'), - }, - measureControl: { - handler: 'ControlChoice', - label: L._('Display the measure control'), - }, - tilelayersControl: { - handler: 'ControlChoice', - label: L._('Display the tile layers control'), - }, - editinosmControl: { - handler: 'ControlChoice', - label: L._('Display the control to open OpenStreetMap editor'), - }, - datalayersControl: { - handler: 'DataLayersControl', - label: L._('Display the data layers control'), - }, - starControl: { - handler: 'ControlChoice', - label: L._('Display the star map button'), - }, - fromZoom: { - handler: 'IntInput', - label: L._('From zoom'), - helpText: L._('Optional.'), - }, - toZoom: { handler: 'IntInput', label: L._('To zoom'), helpText: L._('Optional.') }, - interactive: { - handler: 'Switch', - label: L._('Allow interactions'), - helpEntries: 'interactive', - inheritable: true, - }, + color: { handler: 'ColorPicker' }, + fillColor: { handler: 'ColorPicker' }, + iconUrl: { handler: 'IconUrl' }, + datalayer: { handler: 'DataLayerSwitcher' }, + datalayersControl: { handler: 'DataLayersControl' }, + licence: { handler: 'LicenceChooser' }, + }, + + computeDefaultOptions: function () { + for (let [key, schema] of Object.entries(U.SCHEMA)) { + schema = L.Util.extend({}, schema, this.defaultOptions[key]) + if (!schema.handler) { + 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) { + if (schema.choices.length <= 5) { + schema.handler = 'MultiChoice' + } else { + schema.handler = 'Select' + schema.selectOptions = schema.choices + } + } + } + // FormBuilder use this key for the input type itself + delete schema.type + this.defaultOptions[key] = schema + } }, initialize: function (obj, fields, options) { this.map = obj.map || obj.getMap() - for (const [key, schema] of Object.entries(U.SCHEMA)) { - this.defaultOptions[key] = this.defaultOptions[key] || schema - } + this.computeDefaultOptions() L.FormBuilder.prototype.initialize.call(this, obj, fields, options) this.on('finish', this.finish) }, diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index c503c418..2fe72afb 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -271,13 +271,11 @@ U.Map = L.Map.extend({ // FIXME retrocompat L.Util.setBooleanFromQueryString(options, 'displayDataBrowserOnLoad') L.Util.setBooleanFromQueryString(options, 'displayCaptionOnLoad') - for (const [key, { type }] of Object.entries(U.SCHEMA)) { - switch (type) { + for (const [key, schema] of Object.entries(U.SCHEMA)) { + switch (schema.type) { case Boolean: - L.Util.setBooleanFromQueryString(options, key) - break - case 'NullableBoolean': - L.Util.setNullableBooleanFromQueryString(options, key) + if (schema.nullable) L.Util.setNullableBooleanFromQueryString(options, key) + else L.Util.setBooleanFromQueryString(options, key) break case Number: L.Util.setNumberFromQueryString(options, key) @@ -1497,35 +1495,11 @@ U.Map = L.Map.extend({ _editCredits: function (container) { const credits = L.DomUtil.createFieldset(container, L._('Credits')) const creditsFields = [ - ['options.licence', { handler: 'LicenceChooser', label: L._('licence') }], - [ - 'options.shortCredit', - { - handler: 'Input', - label: L._('Short credits'), - helpEntries: ['shortCredit', 'textFormatting'], - }, - ], - [ - 'options.longCredit', - { - handler: 'Textarea', - label: L._('Long credits'), - helpEntries: ['longCredit', 'textFormatting'], - }, - ], - [ - 'options.permanentCredit', - { - handler: 'Textarea', - label: L._('Permanent credits'), - helpEntries: ['permanentCredit', 'textFormatting'], - }, - ], - [ - 'options.permanentCreditBackground', - { handler: 'Switch', label: L._('Permanent credits background') }, - ], + 'options.licence', + 'options.shortCredit', + 'options.longCredit', + 'options.permanentCredit', + 'options.permanentCreditBackground', ] const creditsBuilder = new U.FormBuilder(this, creditsFields, { callback: this.renderControls,