diff --git a/umap/models.py b/umap/models.py index aa60b334..c61401ff 100644 --- a/umap/models.py +++ b/umap/models.py @@ -10,6 +10,7 @@ from django.core.files.base import File from django.core.signing import Signer from django.template.defaultfilters import slugify from django.urls import reverse +from django.utils.functional import classproperty from django.utils.translation import gettext_lazy as _ from .managers import PublicManager @@ -218,7 +219,7 @@ class Map(NamedModel): "umap_id": self.pk, "onLoadPanel": "none", "captionBar": False, - "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, + "schema": self.extra_schema, "slideshow": {}, } ) @@ -329,6 +330,14 @@ class Map(NamedModel): datalayer.clone(map_inst=new) return new + @classproperty + def extra_schema(self): + return { + "iconUrl": { + "default": "%sumap/img/marker.svg" % settings.STATIC_URL, + } + } + class Pictogram(NamedModel): """ diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index c3370acf..80c7f358 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -30,7 +30,7 @@ export default class Browser { title.textContent = feature.getDisplayName() || '—' const bgcolor = feature.getDynamicOption('color') colorBox.style.backgroundColor = bgcolor - if (symbol && symbol !== this.map.options.default_iconUrl) { + if (symbol && symbol !== U.SCHEMA.iconUrl.default) { const icon = U.Icon.makeIconElement(symbol, colorBox) U.Icon.setIconContrast(icon, colorBox, symbol, bgcolor) } @@ -131,7 +131,7 @@ export default class Browser { 'h3', 'umap-browse-title', container, - this.map.options.name + this.map.getOption('name') ) const formContainer = DomUtil.create('div', '', container) diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 2d579187..25ee3505 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,14 +1,12 @@ -import * as L from '../../vendors/leaflet/leaflet-src.esm.js' import URLs from './urls.js' import Browser from './browser.js' import * as Utils from './utils.js' +import {SCHEMA} from './schema.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' // Import modules and export them to the global scope. // For the not yet module-compatible JS out there. -// Copy the leaflet module, it's expected by leaflet plugins to be writeable. -window.L = { ...L } window.U = { URLs, Request, @@ -18,4 +16,5 @@ window.U = { NOKError, Browser, Utils, + SCHEMA, } diff --git a/umap/static/umap/js/modules/i18n.js b/umap/static/umap/js/modules/i18n.js new file mode 100644 index 00000000..02c90997 --- /dev/null +++ b/umap/static/umap/js/modules/i18n.js @@ -0,0 +1,35 @@ +// Comes from https://github.com/Leaflet/Leaflet/pull/9281 +import { Util } from '../../vendors/leaflet/leaflet-src.esm.js' + +export const locales = {} + +// @property locale: String +// The current locale code, that will be used when translating strings. +export let locale = null + +// @function registerLocale(code: String, locale?: Object): String +// Define localized strings for a given locale, defined by `code`. +export function registerLocale(code, locale) { + locales[code] = Util.extend({}, locales[code], locale) +} +// @function setLocale(code: String): undefined +// Define or change the locale code to be used when translating strings. +export function setLocale(code) { + locale = code +} +// @function translate(string: String, data?: Object): String +// Actually try to translate the `string`, with optionnal variable passed in `data`. +export function translate(string, data = {}) { + if (locale && locales[locale] && locales[locale][string] !== undefined) { + string = locales[locale][string] + } + try { + // Do not fail if some data is missing + // a bad translation should not break the app + string = Util.template(string, data) + } catch (err) { + console.error(err) + } + + return string +} diff --git a/umap/static/umap/js/modules/leaflet-configure.js b/umap/static/umap/js/modules/leaflet-configure.js new file mode 100644 index 00000000..b7b502ff --- /dev/null +++ b/umap/static/umap/js/modules/leaflet-configure.js @@ -0,0 +1,7 @@ +import * as L from '../../vendors/leaflet/leaflet-src.esm.js' +// Comes from https://github.com/Leaflet/Leaflet/pull/9281 +// TODELETE once it's merged! +import * as i18n from './i18n.js' + +window.L = { ...L, ...i18n } +window.L._ = i18n.translate diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js new file mode 100644 index 00000000..d34f8ffc --- /dev/null +++ b/umap/static/umap/js/modules/schema.js @@ -0,0 +1,388 @@ +import { translate } from './i18n.js' + +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?'), + 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: { + 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, + default: 'DarkBlue', + }, + iconClass: { + type: String, + label: translate('Icon shape'), + inheritable: true, + choices: [ + ['Default', translate('Default')], + ['Circle', translate('Circle')], + ['Drop', translate('Drop')], + ['Ball', translate('Ball')], + ], + 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: { + type: Number, + min: 0.1, + max: 1, + step: 0.1, + label: translate('icon opacity'), + inheritable: true, + default: 1, + }, + opacity: { + type: Number, + min: 0.1, + max: 1, + step: 0.1, + label: translate('opacity'), + inheritable: true, + default: 0.5, + }, + weight: { + type: Number, + min: 1, + max: 20, + step: 1, + label: translate('weight'), + inheritable: true, + default: 3, + }, + fill: { + type: Boolean, + label: translate('fill'), + helpEntries: 'fill', + 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: { + 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')], + ], + default: 'auto', + }, + 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'), + default: true, + }, + zoomControl: { + type: Boolean, + nullable: true, + label: translate('Display the zoom control'), + default: true, + }, + datalayersControl: { + type: Boolean, + nullable: true, + handler: 'DataLayersControl', + label: translate('Display the data layers control'), + default: true, + }, + searchControl: { + type: Boolean, + nullable: true, + label: translate('Display the search control'), + default: true, + }, + locateControl: { + type: Boolean, + nullable: true, + label: translate('Display the locate control'), + }, + 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'), + }, + starControl: { + 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: { + type: Boolean, + label: translate('stroke'), + helpEntries: 'stroke', + inheritable: true, + default: true, + }, + outlink: { + label: translate('Link to…'), + helpEntries: 'outlink', + placeholder: 'http://...', + inheritable: true, + }, +} diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index b76daccc..cfbd78bf 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1184,25 +1184,22 @@ U.AttributionControl = L.Control.Attribution.extend({ this._container, credits ) - if (this._map.options.shortCredit) { - L.DomUtil.add( - 'span', - '', - container, - ` — ${L.Util.toHTML(this._map.options.shortCredit)}` - ) + const shortCredit = this._map.getOption('shortCredit'), + captionMenus = this._map.getOption('captionMenus') + if (shortCredit) { + L.DomUtil.add('span', '', container, ` — ${L.Util.toHTML(shortCredit)}`) } - if (this._map.options.captionMenus) { + if (captionMenus) { const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`) L.DomEvent.on(link, 'click', L.DomEvent.stop) .on(link, 'click', this._map.displayCaption, this._map) .on(link, 'dblclick', L.DomEvent.stop) } - if (window.top === window.self && this._map.options.captionMenus) { + if (window.top === window.self && captionMenus) { // We are not in iframe mode L.DomUtil.createLink('', container, ` — ${L._('Home')}`, '/') } - if (this._map.options.captionMenus) { + if (captionMenus) { L.DomUtil.createLink( '', container, diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 1dcda4b2..a7cabbc3 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -415,7 +415,10 @@ L.DomUtil.TextColorFromBackgroundColor = (el, bgcolor) => { L.DomUtil.contrastWCAG21 = (rgb) => { const [r, g, b] = rgb // luminance of inputted colour - const lum = 0.2126 * L.DomUtil.colourMod(r) + 0.7152 * L.DomUtil.colourMod(g) + 0.0722 * L.DomUtil.colourMod(b) + const lum = + 0.2126 * L.DomUtil.colourMod(r) + + 0.7152 * L.DomUtil.colourMod(g) + + 0.0722 * L.DomUtil.colourMod(b) // white has a luminance of 1 const whiteLum = 1 const contrast = (whiteLum + 0.05) / (lum + 0.05) @@ -729,9 +732,6 @@ U.Help = L.Class.extend({ formatURL: `${L._( 'Supported variables that will be dynamically replaced' )}: {bbox}, {lat}, {lng}, {zoom}, {east}, {north}..., {left}, {top}..., locale, lang`, - formatIconSymbol: L._( - '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.' - ), colorValue: L._('Must be a valid CSS value (eg.: DarkBlue or #123456)'), smoothFactor: L._( 'How much to simplify the polyline on each zoom level (more = better performance and smoother look, less = more accurate)' @@ -741,7 +741,7 @@ U.Help = L.Class.extend({ ), zoomTo: L._('Zoom level for automatic zooms'), labelKey: L._( - 'The name of the property to use as feature label (eg.: "nom"). You can also use properties inside brackets to use more than one or mix with static content (eg.: "{name} in {place}")' + 'The name of the property to use as feature label (eg.: "nom"). You can also use properties inside brackets to use more than one or mix with static content (eg.: "{name} in {place}")' ), stroke: L._('Whether to display or not polygons paths.'), fill: L._('Whether to fill polygons with color.'), diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 57caecce..94958370 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -53,7 +53,7 @@ U.FeatureMixin = { }, getSlug: function () { - return this.properties[this.map.options.slugKey || 'name'] || '' + return this.properties[this.map.getOption('slugKey') || 'name'] || '' }, getPermalink: function () { @@ -103,11 +103,15 @@ U.FeatureMixin = { L._('Feature properties') ) - let builder = new U.FormBuilder(this, ['datalayer'], { - callback: function () { - this.edit(e) - }, // removeLayer step will close the edit panel, let's reopen it - }) + let builder = new U.FormBuilder( + this, + [['datalayer', { handler: 'DataLayerSwitcher' }]], + { + callback: function () { + this.edit(e) + }, // removeLayer step will close the edit panel, let's reopen it + } + ) container.appendChild(builder.build()) const properties = [] @@ -209,7 +213,7 @@ U.FeatureMixin = { if (L.Browser.ielt9) return false if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) return false - return this.map.options.displayPopupFooter + return this.map.getOption('displayPopupFooter') }, getPopupClass: function () { @@ -309,7 +313,7 @@ U.FeatureMixin = { zoomTo: function (e) { e = e || {} - const easing = e.easing !== undefined ? e.easing : this.map.options.easing + const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing') if (easing) { this.map.flyTo(this.getCenter(), this.getBestZoom()) } else { @@ -975,7 +979,7 @@ U.PathMixin = { zoomTo: function (e) { // Use bounds instead of centroid for paths. e = e || {} - const easing = e.easing !== undefined ? e.easing : this.map.options.easing + const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing') if (easing) { this.map.flyToBounds(this.getBounds(), this.getBestZoom()) } else { diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 2d28a026..5031e152 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 = [] @@ -708,7 +653,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, isDefault: function () { - return !this.value() || this.value() === U.DEFAULT_ICON_URL + return !this.value() || this.value() === U.SCHEMA.iconUrl.default }, addGrid: function (onSearch) { @@ -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' @@ -1052,230 +977,55 @@ U.FormBuilder = L.FormBuilder.extend({ className: 'umap-form', }, - 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, - }, + computeDefaultOptions: function () { + for (let [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, [value, 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 'datalayersControl': + schema.handler = 'DataLayersControl' + break + case 'licence': + schema.handler = 'LicenceChooser' + break + } + } + // 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() + this.computeDefaultOptions() L.FormBuilder.prototype.initialize.call(this, obj, fields, options) this.on('finish', this.finish) }, diff --git a/umap/static/umap/js/umap.icon.js b/umap/static/umap/js/umap.icon.js index 5a0189b3..7eae6575 100644 --- a/umap/static/umap/js/umap.icon.js +++ b/umap/static/umap/js/umap.icon.js @@ -19,7 +19,7 @@ U.Icon = L.DivIcon.extend({ _setRecent: function (url) { if (L.Util.hasVar(url)) return - if (url === U.DEFAULT_ICON_URL) return + if (url === U.SCHEMA.iconUrl.default) return if (U.Icon.RECENT.indexOf(url) === -1) { U.Icon.RECENT.push(url) } @@ -236,7 +236,7 @@ U.Icon.setIconContrast = function (icon, parent, src, bgcolor) { if (L.DomUtil.contrastedColor(parent, bgcolor)) { // Decide whether to switch svg to white or not, but do it // only for internal SVG, as invert could do weird things - if (L.Util.isPath(src) && src.endsWith('.svg') && src !== U.DEFAULT_ICON_URL) { + if (L.Util.isPath(src) && src.endsWith('.svg') && src !== U.SCHEMA.iconUrl.default) { // Must be called after icon container is added to the DOM // An image icon.style.filter = 'invert(1)' diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index bc73784b..b4df88cf 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -2,33 +2,12 @@ L.Map.mergeOptions({ overlay: null, datalayers: [], hash: true, - default_color: 'DarkBlue', - default_smoothFactor: 1.0, - default_opacity: 0.5, - default_fillOpacity: 0.3, - default_stroke: true, - default_fill: true, - default_weight: 3, - default_iconOpacity: 1, - default_iconClass: 'Default', - default_popupContentTemplate: '# {name}\n{description}', - default_interactive: true, - default_labelDirection: 'auto', maxZoomLimit: 24, attributionControl: false, editMode: 'advanced', - embedControl: true, - zoomControl: true, - datalayersControl: true, - searchControl: true, - editInOSMControl: false, - editInOSMControlOptions: false, - scaleControl: true, noControl: false, // Do not render any control. - miniMap: false, name: '', description: '', - displayPopupFooter: false, // When a TileLayer is in TMS mode, it needs -y instead of y. // This is usually handled by the TileLayer instance itself, but // we cannot rely on this because of the y is overriden by Leaflet @@ -44,77 +23,14 @@ L.Map.mergeOptions({ importPresets: [ // {url: 'http://localhost:8019/en/datalayer/1502/', label: 'Simplified World Countries', format: 'geojson'} ], - moreControl: true, - captionBar: false, - captionMenus: true, slideshow: {}, clickable: true, - easing: false, permissions: {}, - permanentCreditBackground: true, featuresHaveOwner: false, }) U.Map = L.Map.extend({ includes: [ControlsMixin], - editableOptions: { - zoom: undefined, - scrollWheelZoom: Boolean, - scaleControl: Boolean, - moreControl: Boolean, - miniMap: Boolean, - displayPopupFooter: undefined, - onLoadPanel: String, - defaultView: String, - name: String, - description: String, - licence: undefined, - tilelayer: undefined, - overlay: undefined, - limitBounds: undefined, - color: String, - iconClass: String, - iconUrl: String, - smoothFactor: undefined, - iconOpacity: undefined, - opacity: undefined, - weight: undefined, - fill: undefined, - fillColor: undefined, - fillOpacity: undefined, - dashArray: undefined, - popupShape: String, - popupTemplate: String, - popupContentTemplate: String, - zoomTo: Number, - captionBar: Boolean, - captionMenus: Boolean, - slideshow: undefined, - sortKey: undefined, - labelKey: String, - filterKey: undefined, - facetKey: undefined, - slugKey: undefined, - showLabel: 'NullableBoolean', - labelDirection: undefined, - labelInteractive: undefined, - outlinkTarget: undefined, - shortCredit: undefined, - longCredit: undefined, - permanentCredit: undefined, - permanentCreditBackground: undefined, - zoomControl: 'NullableBoolean', - datalayersControl: 'NullableBoolean', - searchControl: 'NullableBoolean', - locateControl: 'NullableBoolean', - fullscreenControl: 'NullableBoolean', - editinosmControl: 'NullableBoolean', - embedControl: 'NullableBoolean', - measureControl: 'NullableBoolean', - tilelayersControl: 'NullableBoolean', - starControl: 'NullableBoolean', - easing: undefined, - }, initialize: function (el, geojson) { // Locale name (pt_PT, en_US…) @@ -133,7 +49,8 @@ U.Map = L.Map.extend({ geojson.properties.fullscreenControl = false L.Map.prototype.initialize.call(this, el, geojson.properties) - U.DEFAULT_ICON_URL = this.options.default_iconUrl + + if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema) // After calling parent initialize, as we are doing initCenter our-selves if (geojson.geometry) this.options.center = this.latLng(geojson.geometry) @@ -329,13 +246,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(this.editableOptions)) { - 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) @@ -352,6 +267,12 @@ U.Map = L.Map.extend({ } }, + overrideSchema: function (schema) { + for (const [key, extra] of Object.entries(schema)) { + U.SCHEMA[key] = L.extend({}, U.SCHEMA[key], extra) + } + }, + initControls: function () { this.helpMenuActions = {} this._controls = {} @@ -393,7 +314,7 @@ U.Map = L.Map.extend({ title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, }) this._controls.search = new U.SearchControl() - this._controls.embed = new L.Control.Embed(this, this.options.embedOptions) + this._controls.embed = new L.Control.Embed(this) this._controls.tilelayersChooser = new U.TileLayerChooser(this) if (this.options.user) this._controls.star = new U.StarControl(this) this._controls.editinosm = new L.Control.EditInOSM({ @@ -459,7 +380,7 @@ U.Map = L.Map.extend({ let name, status, control for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) { name = this.HIDDABLE_CONTROLS[i] - status = this.options[`${name}Control`] + status = this.getOption(`${name}Control`) if (status === false) continue control = this._controls[name] if (!control) continue @@ -468,9 +389,9 @@ U.Map = L.Map.extend({ L.DomUtil.addClass(control._container, 'display-on-more') else L.DomUtil.removeClass(control._container, 'display-on-more') } - if (this.options.permanentCredit) this._controls.permanentCredit.addTo(this) - if (this.options.moreControl) this._controls.more.addTo(this) - if (this.options.scaleControl) this._controls.scale.addTo(this) + if (this.getOption('permanentCredit')) this._controls.permanentCredit.addTo(this) + if (this.getOption('moreControl')) this._controls.more.addTo(this) + if (this.getOption('scaleControl')) this._controls.scale.addTo(this) }, initDataLayers: async function (datalayers) { @@ -820,7 +741,7 @@ U.Map = L.Map.extend({ }, getDefaultOption: function (option) { - return this.options[`default_${option}`] + return U.SCHEMA[option] && U.SCHEMA[option].default }, getOption: function (option) { @@ -904,7 +825,7 @@ U.Map = L.Map.extend({ let mustReindex = false - for (const option of Object.keys(this.editableOptions)) { + for (const option of Object.keys(U.SCHEMA)) { if (typeof importedData.properties[option] !== 'undefined') { this.options[option] = importedData.properties[option] if (option === 'sortKey') mustReindex = true @@ -1033,7 +954,7 @@ U.Map = L.Map.extend({ exportOptions: function () { const properties = {} - for (const option of Object.keys(this.editableOptions)) { + for (const option of Object.keys(U.SCHEMA)) { if (typeof this.options[option] !== 'undefined') { properties[option] = this.options[option] } @@ -1555,35 +1476,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, @@ -1691,7 +1588,7 @@ U.Map = L.Map.extend({ name = L.DomUtil.create('h3', '', container) L.DomEvent.disableClickPropagation(container) this.permissions.addOwnerLink('span', container) - if (this.options.captionMenus) { + if (this.getOption('captionMenus')) { L.DomUtil.createButton( 'umap-about-link flat', container, diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js index 167f73d1..ad7c7b14 100644 --- a/umap/static/umap/js/umap.share.js +++ b/umap/static/umap/js/umap.share.js @@ -215,7 +215,7 @@ U.IframeExporter = L.Evented.extend({ this.map = map this.baseUrl = L.Util.getBaseUrl() // Use map default, not generic default - this.queryString.onLoadPanel = this.map.options.onLoadPanel + this.queryString.onLoadPanel = this.map.getOption('onLoadPanel') }, getMap: function () { diff --git a/umap/static/umap/test/index.html b/umap/static/umap/test/index.html index 9d6da7d4..e01794a3 100644 --- a/umap/static/umap/test/index.html +++ b/umap/static/umap/test/index.html @@ -8,7 +8,6 @@ - diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 95e4a675..86ea502e 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -2,11 +2,16 @@ + +{% if locale %} + {% with "umap/locale/"|add:locale|add:".js" as path %} + + {% endwith %} +{% endif %} - -{% if locale %} - {% with "umap/locale/"|add:locale|add:".js" as path %} - - {% endwith %} -{% endif %} + diff --git a/umap/tests/integration/test_owned_map.py b/umap/tests/integration/test_owned_map.py index f6b84584..6316940b 100644 --- a/umap/tests/integration/test_owned_map.py +++ b/umap/tests/integration/test_owned_map.py @@ -210,7 +210,8 @@ def test_can_change_owner(map, live_server, login, user): close = page.locator(".umap-field-owner .close") close.click() input = page.locator("input.edit-owner") - input.type(user.username) + with page.expect_response(re.compile(r".*/agnocomplete/.*")): + input.type(user.username) input.press("Tab") save = page.get_by_role("button", name="Save") expect(save).to_be_visible() diff --git a/umap/views.py b/umap/views.py index be6a900a..05187268 100644 --- a/umap/views.py +++ b/umap/views.py @@ -495,7 +495,7 @@ class MapDetailMixin: "urls": _urls_for_js(), "tilelayers": TileLayer.get_list(), "editMode": self.edit_mode, - "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, # noqa + "schema": Map.extra_schema, "umap_id": self.get_umap_id(), "starred": self.is_starred(), "licences": dict((l.name, l.json) for l in Licence.objects.all()),