Merge pull request #1782 from umap-project/fix-caption
fix: ensure tilelayer attribution with smart text is displayed as HMTL
This commit is contained in:
commit
713214e258
12 changed files with 255 additions and 156 deletions
|
@ -70,7 +70,6 @@ export default function getPurify() {
|
||||||
export function escapeHTML(s) {
|
export function escapeHTML(s) {
|
||||||
s = s ? s.toString() : ''
|
s = s ? s.toString() : ''
|
||||||
s = getPurify().sanitize(s, {
|
s = getPurify().sanitize(s, {
|
||||||
USE_PROFILES: { html: true },
|
|
||||||
ADD_TAGS: ['iframe'],
|
ADD_TAGS: ['iframe'],
|
||||||
ALLOWED_TAGS: [
|
ALLOWED_TAGS: [
|
||||||
'h3',
|
'h3',
|
||||||
|
@ -86,9 +85,10 @@ export function escapeHTML(s) {
|
||||||
'iframe',
|
'iframe',
|
||||||
'img',
|
'img',
|
||||||
'br',
|
'br',
|
||||||
|
'span',
|
||||||
],
|
],
|
||||||
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'],
|
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'],
|
||||||
ALLOWED_ATTR: ['href', 'src', 'width', 'height'],
|
ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'style'],
|
||||||
// Added: `geo:` URL scheme as defined in RFC5870:
|
// Added: `geo:` URL scheme as defined in RFC5870:
|
||||||
// https://www.rfc-editor.org/rfc/rfc5870.html
|
// https://www.rfc-editor.org/rfc/rfc5870.html
|
||||||
// The base RegExp comes from:
|
// The base RegExp comes from:
|
||||||
|
|
|
@ -35,27 +35,25 @@ U.AutoComplete = L.Class.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
createInput: function () {
|
createInput: function () {
|
||||||
this.input = L.DomUtil.element(
|
this.input = L.DomUtil.element({
|
||||||
'input',
|
tagName: 'input',
|
||||||
{
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
parent: this.el,
|
||||||
placeholder: this.options.placeholder,
|
placeholder: this.options.placeholder,
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
className: this.options.className,
|
className: this.options.className,
|
||||||
},
|
})
|
||||||
this.el
|
|
||||||
)
|
|
||||||
L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
|
L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
|
||||||
L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this)
|
L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this)
|
||||||
L.DomEvent.on(this.input, 'blur', this.onBlur, this)
|
L.DomEvent.on(this.input, 'blur', this.onBlur, this)
|
||||||
},
|
},
|
||||||
|
|
||||||
createContainer: function () {
|
createContainer: function () {
|
||||||
this.container = L.DomUtil.element(
|
this.container = L.DomUtil.element({
|
||||||
'ul',
|
tagName: 'ul',
|
||||||
{ className: 'umap-autocomplete' },
|
parent: document.body,
|
||||||
document.body
|
className: 'umap-autocomplete',
|
||||||
)
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
resizeContainer: function () {
|
resizeContainer: function () {
|
||||||
|
@ -174,8 +172,11 @@ U.AutoComplete = L.Class.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
createResult: function (item) {
|
createResult: function (item) {
|
||||||
const el = L.DomUtil.element('li', {}, this.container)
|
const el = L.DomUtil.element({
|
||||||
el.textContent = item.label
|
tagName: 'li',
|
||||||
|
parent: this.container,
|
||||||
|
textContent: item.label,
|
||||||
|
})
|
||||||
const result = {
|
const result = {
|
||||||
item: item,
|
item: item,
|
||||||
el: el,
|
el: el,
|
||||||
|
@ -276,15 +277,22 @@ U.AutoComplete.Ajax.SelectMultiple = U.AutoComplete.Ajax.extend({
|
||||||
initSelectedContainer: function () {
|
initSelectedContainer: function () {
|
||||||
return L.DomUtil.after(
|
return L.DomUtil.after(
|
||||||
this.input,
|
this.input,
|
||||||
L.DomUtil.element('ul', { className: 'umap-multiresult' })
|
L.DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' })
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
displaySelected: function (result) {
|
displaySelected: function (result) {
|
||||||
const result_el = L.DomUtil.element('li', {}, this.selected_container)
|
const result_el = L.DomUtil.element({
|
||||||
|
tagName: 'li',
|
||||||
|
parent: this.selected_container,
|
||||||
|
})
|
||||||
result_el.textContent = result.item.label
|
result_el.textContent = result.item.label
|
||||||
const close = L.DomUtil.element('span', { className: 'close' }, result_el)
|
const close = L.DomUtil.element({
|
||||||
close.textContent = '×'
|
tagName: 'span',
|
||||||
|
parent: result_el,
|
||||||
|
className: 'close',
|
||||||
|
textContent: '×',
|
||||||
|
})
|
||||||
L.DomEvent.on(
|
L.DomEvent.on(
|
||||||
close,
|
close,
|
||||||
'click',
|
'click',
|
||||||
|
@ -302,15 +310,22 @@ U.AutoComplete.Ajax.Select = U.AutoComplete.Ajax.extend({
|
||||||
initSelectedContainer: function () {
|
initSelectedContainer: function () {
|
||||||
return L.DomUtil.after(
|
return L.DomUtil.after(
|
||||||
this.input,
|
this.input,
|
||||||
L.DomUtil.element('div', { className: 'umap-singleresult' })
|
L.DomUtil.element({ tagName: 'div', className: 'umap-singleresult' })
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
displaySelected: function (result) {
|
displaySelected: function (result) {
|
||||||
const result_el = L.DomUtil.element('div', {}, this.selected_container)
|
const result_el = L.DomUtil.element({
|
||||||
|
tagName: 'div',
|
||||||
|
parent: this.selected_container,
|
||||||
|
})
|
||||||
result_el.textContent = result.item.label
|
result_el.textContent = result.item.label
|
||||||
const close = L.DomUtil.element('span', { className: 'close' }, result_el)
|
const close = L.DomUtil.element({
|
||||||
close.textContent = '×'
|
tagName: 'span',
|
||||||
|
parent: result_el,
|
||||||
|
className: 'close',
|
||||||
|
textContent: '×',
|
||||||
|
})
|
||||||
this.input.style.display = 'none'
|
this.input.style.display = 'none'
|
||||||
L.DomEvent.on(
|
L.DomEvent.on(
|
||||||
close,
|
close,
|
||||||
|
|
|
@ -669,20 +669,27 @@ const ControlsMixin = {
|
||||||
L.DomUtil.createTitle(container, this.options.name, 'icon-caption')
|
L.DomUtil.createTitle(container, this.options.name, 'icon-caption')
|
||||||
this.permissions.addOwnerLink('h5', container)
|
this.permissions.addOwnerLink('h5', container)
|
||||||
if (this.options.description) {
|
if (this.options.description) {
|
||||||
const description = L.DomUtil.create('div', 'umap-map-description', container)
|
const description = L.DomUtil.element({
|
||||||
description.innerHTML = U.Utils.toHTML(this.options.description)
|
tagName: 'div',
|
||||||
|
className: 'umap-map-description',
|
||||||
|
safeHTML: U.Utils.toHTML(this.options.description),
|
||||||
|
parent: container,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container)
|
const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container)
|
||||||
this.eachVisibleDataLayer((datalayer) => {
|
this.eachVisibleDataLayer((datalayer) => {
|
||||||
if (!datalayer.options.inCaption) return
|
if (!datalayer.options.inCaption) return
|
||||||
const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer),
|
const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer),
|
||||||
legend = L.DomUtil.create('span', '', p),
|
legend = L.DomUtil.create('span', '', p),
|
||||||
headline = L.DomUtil.create('strong', '', p),
|
headline = L.DomUtil.create('strong', '', p)
|
||||||
description = L.DomUtil.create('span', '', p)
|
|
||||||
datalayer.onceLoaded(function () {
|
datalayer.onceLoaded(function () {
|
||||||
datalayer.renderLegend(legend)
|
datalayer.renderLegend(legend)
|
||||||
if (datalayer.options.description) {
|
if (datalayer.options.description) {
|
||||||
description.innerHTML = U.Utils.toHTML(datalayer.options.description)
|
L.DomUtil.element({
|
||||||
|
tagName: 'span',
|
||||||
|
parent: p,
|
||||||
|
safeHTML: U.Utils.toHTML(datalayer.options.description),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
datalayer.renderToolbox(headline)
|
datalayer.renderToolbox(headline)
|
||||||
|
@ -692,12 +699,11 @@ const ControlsMixin = {
|
||||||
credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits'))
|
credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits'))
|
||||||
title = L.DomUtil.add('h5', '', credits, L._('User content credits'))
|
title = L.DomUtil.add('h5', '', credits, L._('User content credits'))
|
||||||
if (this.options.shortCredit || this.options.longCredit) {
|
if (this.options.shortCredit || this.options.longCredit) {
|
||||||
L.DomUtil.add(
|
L.DomUtil.element({
|
||||||
'p',
|
tagName: 'p',
|
||||||
'',
|
parent: credits,
|
||||||
credits,
|
safeHTML: U.Utils.toHTML(this.options.longCredit || this.options.shortCredit),
|
||||||
U.Utils.toHTML(this.options.longCredit || this.options.shortCredit)
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (this.options.licence) {
|
if (this.options.licence) {
|
||||||
const licence = L.DomUtil.add(
|
const licence = L.DomUtil.add(
|
||||||
|
@ -718,21 +724,26 @@ const ControlsMixin = {
|
||||||
L.DomUtil.create('hr', '', credits)
|
L.DomUtil.create('hr', '', credits)
|
||||||
title = L.DomUtil.create('h5', '', credits)
|
title = L.DomUtil.create('h5', '', credits)
|
||||||
title.textContent = L._('Map background credits')
|
title.textContent = L._('Map background credits')
|
||||||
const tilelayerCredit = L.DomUtil.create('p', '', credits),
|
const tilelayerCredit = L.DomUtil.create('p', '', credits)
|
||||||
name = L.DomUtil.create('strong', '', tilelayerCredit),
|
L.DomUtil.element({
|
||||||
attribution = L.DomUtil.create('span', '', tilelayerCredit)
|
tagName: 'strong',
|
||||||
name.textContent = `${this.selected_tilelayer.options.name} `
|
parent: tilelayerCredit,
|
||||||
attribution.innerHTML = this.selected_tilelayer.getAttribution()
|
textContent: `${this.selected_tilelayer.options.name} `,
|
||||||
|
})
|
||||||
|
L.DomUtil.element({
|
||||||
|
tagName: 'span',
|
||||||
|
parent: tilelayerCredit,
|
||||||
|
safeHTML: this.selected_tilelayer.getAttribution(),
|
||||||
|
})
|
||||||
L.DomUtil.create('hr', '', credits)
|
L.DomUtil.create('hr', '', credits)
|
||||||
const umapCredit = L.DomUtil.create('p', '', credits),
|
const urls = {
|
||||||
urls = {
|
|
||||||
leaflet: 'http://leafletjs.com',
|
leaflet: 'http://leafletjs.com',
|
||||||
django: 'https://www.djangoproject.com',
|
django: 'https://www.djangoproject.com',
|
||||||
umap: 'http://wiki.openstreetmap.org/wiki/UMap',
|
umap: 'http://wiki.openstreetmap.org/wiki/UMap',
|
||||||
changelog: 'https://umap-project.readthedocs.io/en/master/changelog/',
|
changelog: 'https://umap-project.readthedocs.io/en/master/changelog/',
|
||||||
version: this.options.umap_version,
|
version: this.options.umap_version,
|
||||||
}
|
}
|
||||||
umapCredit.innerHTML = L._(
|
const creditHTML = L._(
|
||||||
`
|
`
|
||||||
Powered by <a href="{leaflet}">Leaflet</a> and
|
Powered by <a href="{leaflet}">Leaflet</a> and
|
||||||
<a href="{django}">Django</a>,
|
<a href="{django}">Django</a>,
|
||||||
|
@ -741,6 +752,7 @@ const ControlsMixin = {
|
||||||
`,
|
`,
|
||||||
urls
|
urls
|
||||||
)
|
)
|
||||||
|
L.DomUtil.element({ tagName: 'p', innerHTML: creditHTML, parent: credits })
|
||||||
this.panel.open({ content: container })
|
this.panel.open({ content: container })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1046,16 +1058,16 @@ U.AttributionControl = L.Control.Attribution.extend({
|
||||||
// Use our own container, so we can hide/show on small screens
|
// Use our own container, so we can hide/show on small screens
|
||||||
const credits = this._container.innerHTML
|
const credits = this._container.innerHTML
|
||||||
this._container.innerHTML = ''
|
this._container.innerHTML = ''
|
||||||
const container = L.DomUtil.add(
|
const container = L.DomUtil.create('div', 'attribution-container', this._container)
|
||||||
'div',
|
container.innerHTML = credits
|
||||||
'attribution-container',
|
|
||||||
this._container,
|
|
||||||
credits
|
|
||||||
)
|
|
||||||
const shortCredit = this._map.getOption('shortCredit'),
|
const shortCredit = this._map.getOption('shortCredit'),
|
||||||
captionMenus = this._map.getOption('captionMenus')
|
captionMenus = this._map.getOption('captionMenus')
|
||||||
if (shortCredit) {
|
if (shortCredit) {
|
||||||
L.DomUtil.add('span', '', container, ` — ${U.Utils.toHTML(shortCredit)}`)
|
L.DomUtil.element({
|
||||||
|
tagName: 'span',
|
||||||
|
parent: container,
|
||||||
|
safeHTML: ` — ${U.Utils.toHTML(shortCredit)}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (captionMenus) {
|
if (captionMenus) {
|
||||||
const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`)
|
const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`)
|
||||||
|
|
|
@ -118,19 +118,21 @@ L.DomUtil.createLink = (className, container, content, url, target, title) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
L.DomUtil.createIcon = (parent, className, title, size = 16) => {
|
L.DomUtil.createIcon = (parent, className, title, size = 16) => {
|
||||||
return L.DomUtil.element(
|
return L.DomUtil.element({
|
||||||
'i',
|
tagName: 'i',
|
||||||
{ className: `icon icon-${size} ${className}`, title: title || '' },
|
parent: parent,
|
||||||
parent
|
className: `icon icon-${size} ${className}`,
|
||||||
)
|
title: title || '',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
L.DomUtil.createButtonIcon = (parent, className, title, size = 16) => {
|
L.DomUtil.createButtonIcon = (parent, className, title, size = 16) => {
|
||||||
return L.DomUtil.element(
|
return L.DomUtil.element({
|
||||||
'button',
|
tagName: 'button',
|
||||||
{ className: `icon icon-${size} ${className}`, title: title || '' },
|
parent: parent,
|
||||||
parent
|
className: `icon icon-${size} ${className}`,
|
||||||
)
|
title: title || '',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
L.DomUtil.createTitle = (parent, text, className, tag = 'h3') => {
|
L.DomUtil.createTitle = (parent, text, className, tag = 'h3') => {
|
||||||
|
@ -163,8 +165,13 @@ L.DomUtil.classIf = (el, className, bool) => {
|
||||||
else L.DomUtil.removeClass(el, className)
|
else L.DomUtil.removeClass(el, className)
|
||||||
}
|
}
|
||||||
|
|
||||||
L.DomUtil.element = (what, attrs, parent) => {
|
L.DomUtil.element = ({ tagName, parent, ...attrs }) => {
|
||||||
const el = document.createElement(what)
|
const el = document.createElement(tagName)
|
||||||
|
if (attrs.innerHTML) {
|
||||||
|
attrs.innerHTML = U.Utils.escapeHTML(attrs.innerHTML)
|
||||||
|
} else if (attrs.safeHTML) {
|
||||||
|
attrs.innerHTML = attrs.safeHTML
|
||||||
|
}
|
||||||
for (const attr in attrs) {
|
for (const attr in attrs) {
|
||||||
el[attr] = attrs[attr]
|
el[attr] = attrs[attr]
|
||||||
}
|
}
|
||||||
|
|
|
@ -757,12 +757,15 @@ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
buildLabel: function () {
|
buildLabel: function () {
|
||||||
this.label = L.DomUtil.element('legend', {textContent: this.options.label})
|
this.label = L.DomUtil.element({
|
||||||
|
tagName: 'legend',
|
||||||
|
textContent: this.options.label,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
buildLi: function (value) {
|
buildLi: function (value) {
|
||||||
const property_li = L.DomUtil.create('li', '', this.ul)
|
const property_li = L.DomUtil.create('li', '', this.ul)
|
||||||
const label = L.DomUtil.add('label', '', property_li)
|
const label = L.DomUtil.create('label', '', property_li)
|
||||||
const input = L.DomUtil.create('input', '', label)
|
const input = L.DomUtil.create('input', '', label)
|
||||||
L.DomUtil.add('span', '', label, value)
|
L.DomUtil.add('span', '', label, value)
|
||||||
|
|
||||||
|
@ -807,7 +810,7 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
|
||||||
const [minLabel, maxLabel] = this.getLabels()
|
const [minLabel, maxLabel] = this.getLabels()
|
||||||
|
|
||||||
this.minLabel = L.DomUtil.create('label', '', this.container)
|
this.minLabel = L.DomUtil.create('label', '', this.container)
|
||||||
this.minLabel.innerHTML = minLabel
|
this.minLabel.textContent = minLabel
|
||||||
|
|
||||||
this.minInput = L.DomUtil.create('input', '', this.minLabel)
|
this.minInput = L.DomUtil.create('input', '', this.minLabel)
|
||||||
this.minInput.type = this.inputType
|
this.minInput.type = this.inputType
|
||||||
|
@ -817,9 +820,8 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
|
||||||
this.minInput.dataset.value = min
|
this.minInput.dataset.value = min
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.maxLabel = L.DomUtil.create('label', '', this.container)
|
this.maxLabel = L.DomUtil.create('label', '', this.container)
|
||||||
this.maxLabel.innerHTML = maxLabel
|
this.maxLabel.textContent = maxLabel
|
||||||
|
|
||||||
this.maxInput = L.DomUtil.create('input', '', this.maxLabel)
|
this.maxInput = L.DomUtil.create('input', '', this.maxLabel)
|
||||||
this.maxInput.type = this.inputType
|
this.maxInput.type = this.inputType
|
||||||
|
@ -834,7 +836,10 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
buildLabel: function () {
|
buildLabel: function () {
|
||||||
this.label = L.DomUtil.element('legend', {textContent: this.options.label})
|
this.label = L.DomUtil.element({
|
||||||
|
tagName: 'legend',
|
||||||
|
textContent: this.options.label,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
toJS: function () {
|
toJS: function () {
|
||||||
|
@ -974,22 +979,23 @@ L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
buildHelpText: function () {
|
buildHelpText: function () {
|
||||||
const datalist = L.DomUtil.create(
|
|
||||||
'datalist',
|
|
||||||
'umap-field-datalist',
|
|
||||||
this.getHelpTextParent()
|
|
||||||
)
|
|
||||||
datalist.id = `range-${this.options.label || this.name}`
|
|
||||||
this.input.setAttribute('list', datalist.id)
|
|
||||||
let options = ''
|
let options = ''
|
||||||
const step = this.options.step || 1,
|
const step = this.options.step || 1
|
||||||
digits = step < 1 ? 1 : 0
|
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) {
|
for (let i = this.options.min; i <= this.options.max; i += this.options.step) {
|
||||||
options += `<option value="${i.toFixed(digits)}" label="${i.toFixed(
|
options += `<option value="${i.toFixed(digits)}" label="${i.toFixed(
|
||||||
digits
|
digits
|
||||||
)}"></option>`
|
)}"></option>`
|
||||||
}
|
}
|
||||||
datalist.innerHTML = 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.Input.prototype.buildHelpText.call(this)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,21 +15,24 @@ U.Importer = L.Class.extend({
|
||||||
this.presetBox = L.DomUtil.create('div', 'formbox', this.container)
|
this.presetBox = L.DomUtil.create('div', 'formbox', this.container)
|
||||||
this.presetSelect = L.DomUtil.create('select', '', this.presetBox)
|
this.presetSelect = L.DomUtil.create('select', '', this.presetBox)
|
||||||
this.fileBox = L.DomUtil.create('div', 'formbox', this.container)
|
this.fileBox = L.DomUtil.create('div', 'formbox', this.container)
|
||||||
this.fileInput = L.DomUtil.element(
|
this.fileInput = L.DomUtil.element({
|
||||||
'input',
|
tagName: 'input',
|
||||||
{ type: 'file', multiple: 'multiple', autofocus: true },
|
type: 'file',
|
||||||
this.fileBox
|
parent: this.fileBox,
|
||||||
)
|
multiple: 'multiple',
|
||||||
this.urlInput = L.DomUtil.element(
|
autofocus: true,
|
||||||
'input',
|
})
|
||||||
{ type: 'text', placeholder: L._('Provide an URL here') },
|
this.urlInput = L.DomUtil.element({
|
||||||
this.container
|
tagName: 'input',
|
||||||
)
|
type: 'text',
|
||||||
this.rawInput = L.DomUtil.element(
|
parent: this.container,
|
||||||
'textarea',
|
placeholder: L._('Provide an URL here'),
|
||||||
{ placeholder: L._('Paste your data here') },
|
})
|
||||||
this.container
|
this.rawInput = L.DomUtil.element({
|
||||||
)
|
tagName: 'textarea',
|
||||||
|
parent: this.container,
|
||||||
|
placeholder: L._('Paste your data here'),
|
||||||
|
})
|
||||||
this.typeLabel = L.DomUtil.add(
|
this.typeLabel = L.DomUtil.add(
|
||||||
'label',
|
'label',
|
||||||
'',
|
'',
|
||||||
|
@ -42,33 +45,43 @@ U.Importer = L.Class.extend({
|
||||||
this.container,
|
this.container,
|
||||||
L._('Choose the layer to import in')
|
L._('Choose the layer to import in')
|
||||||
)
|
)
|
||||||
this.clearLabel = L.DomUtil.element(
|
this.clearLabel = L.DomUtil.element({
|
||||||
'label',
|
tagName: 'label',
|
||||||
{ textContent: L._('Replace layer content'), for: 'datalayer-clear-check' },
|
parent: this.container,
|
||||||
this.container
|
textContent: L._('Replace layer content'),
|
||||||
)
|
for: 'datalayer-clear-check',
|
||||||
this.submitInput = L.DomUtil.element(
|
})
|
||||||
'input',
|
this.submitInput = L.DomUtil.element({
|
||||||
{ type: 'button', value: L._('Import'), className: 'button' },
|
tagName: 'input',
|
||||||
this.container
|
type: 'button',
|
||||||
)
|
parent: this.container,
|
||||||
|
value: L._('Import'),
|
||||||
|
className: 'button',
|
||||||
|
})
|
||||||
this.map.help.button(this.typeLabel, 'importFormats')
|
this.map.help.button(this.typeLabel, 'importFormats')
|
||||||
this.typeInput = L.DomUtil.element('select', { name: 'format' }, this.typeLabel)
|
this.typeInput = L.DomUtil.element({
|
||||||
this.layerInput = L.DomUtil.element(
|
tagName: 'select',
|
||||||
'select',
|
name: 'format',
|
||||||
{ name: 'datalayer' },
|
parent: this.typeLabel,
|
||||||
this.layerLabel
|
})
|
||||||
)
|
this.layerInput = L.DomUtil.element({
|
||||||
this.clearFlag = L.DomUtil.element(
|
tagName: 'select',
|
||||||
'input',
|
name: 'datalayer',
|
||||||
{ type: 'checkbox', name: 'clear', id: 'datalayer-clear-check' },
|
parent: this.layerLabel,
|
||||||
this.clearLabel
|
})
|
||||||
)
|
this.clearFlag = L.DomUtil.element({
|
||||||
L.DomUtil.element(
|
tagName: 'input',
|
||||||
'option',
|
type: 'checkbox',
|
||||||
{ value: '', textContent: L._('Choose the data format') },
|
name: 'clear',
|
||||||
this.typeInput
|
id: 'datalayer-clear-check',
|
||||||
)
|
parent: this.clearLabel,
|
||||||
|
})
|
||||||
|
L.DomUtil.element({
|
||||||
|
tagName: 'option',
|
||||||
|
value: '',
|
||||||
|
textContent: L._('Choose the data format'),
|
||||||
|
parent: this.typeInput,
|
||||||
|
})
|
||||||
for (let i = 0; i < this.TYPES.length; i++) {
|
for (let i = 0; i < this.TYPES.length; i++) {
|
||||||
option = L.DomUtil.create('option', '', this.typeInput)
|
option = L.DomUtil.create('option', '', this.typeInput)
|
||||||
option.value = option.textContent = this.TYPES[i]
|
option.value = option.textContent = this.TYPES[i]
|
||||||
|
@ -119,11 +132,12 @@ U.Importer = L.Class.extend({
|
||||||
option.value = id
|
option.value = id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
L.DomUtil.element(
|
L.DomUtil.element({
|
||||||
'option',
|
tagName: 'option',
|
||||||
{ value: '', textContent: L._('Import in a new layer') },
|
value: '',
|
||||||
this.layerInput
|
textContent: L._('Import in a new layer'),
|
||||||
)
|
parent: this.layerInput,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -62,9 +62,15 @@ U.MapPermissions = L.Class.extend({
|
||||||
L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key')
|
L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key')
|
||||||
if (this.isAnonymousMap()) {
|
if (this.isAnonymousMap()) {
|
||||||
if (this.options.anonymous_edit_url) {
|
if (this.options.anonymous_edit_url) {
|
||||||
const helpText = `${L._('Secret edit link:')}<br>${this.options.anonymous_edit_url
|
const helpText = `${L._('Secret edit link:')}<br>${
|
||||||
|
this.options.anonymous_edit_url
|
||||||
}`
|
}`
|
||||||
L.DomUtil.add('p', 'help-text', container, helpText)
|
L.DomUtil.element({
|
||||||
|
tagName: 'p',
|
||||||
|
className: 'help-text',
|
||||||
|
innerHTML: helpText,
|
||||||
|
parent: container,
|
||||||
|
})
|
||||||
fields.push([
|
fields.push([
|
||||||
'options.edit_status',
|
'options.edit_status',
|
||||||
{
|
{
|
||||||
|
|
|
@ -190,7 +190,11 @@ U.PopupTemplate.Table = U.PopupTemplate.BaseWithTitle.extend({
|
||||||
addRow: function (container, key, value) {
|
addRow: function (container, key, value) {
|
||||||
const tr = L.DomUtil.create('tr', '', container)
|
const tr = L.DomUtil.create('tr', '', container)
|
||||||
L.DomUtil.add('th', '', tr, key)
|
L.DomUtil.add('th', '', tr, key)
|
||||||
L.DomUtil.add('td', '', tr, this.formatRow(key, value))
|
L.DomUtil.element({
|
||||||
|
tagName: 'td',
|
||||||
|
parent: tr,
|
||||||
|
innerHTML: this.formatRow(key, value),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
renderBody: function () {
|
renderBody: function () {
|
||||||
|
@ -281,11 +285,12 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (props.website) {
|
if (props.website) {
|
||||||
L.DomUtil.element(
|
L.DomUtil.element({
|
||||||
'a',
|
tagName: 'a',
|
||||||
{ href: props.website, textContent: props.website },
|
parent: container,
|
||||||
container
|
href: props.website,
|
||||||
)
|
textContent: props.website,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const phone = props.phone || props['contact:phone']
|
const phone = props.phone || props['contact:phone']
|
||||||
if (phone) {
|
if (phone) {
|
||||||
|
@ -293,7 +298,7 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
|
||||||
'div',
|
'div',
|
||||||
'',
|
'',
|
||||||
container,
|
container,
|
||||||
L.DomUtil.element('a', { href: `tel:${phone}`, textContent: phone })
|
L.DomUtil.element({ tagName: 'a', href: `tel:${phone}`, textContent: phone })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (props.mobile) {
|
if (props.mobile) {
|
||||||
|
@ -301,7 +306,8 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
|
||||||
'div',
|
'div',
|
||||||
'',
|
'',
|
||||||
container,
|
container,
|
||||||
L.DomUtil.element('a', {
|
L.DomUtil.element({
|
||||||
|
tagName: 'a',
|
||||||
href: `tel:${props.mobile}`,
|
href: `tel:${props.mobile}`,
|
||||||
textContent: props.mobile,
|
textContent: props.mobile,
|
||||||
})
|
})
|
||||||
|
@ -322,7 +328,8 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
|
||||||
'div',
|
'div',
|
||||||
'osm-link',
|
'osm-link',
|
||||||
container,
|
container,
|
||||||
L.DomUtil.element('a', {
|
L.DomUtil.element({
|
||||||
|
tagName: 'a',
|
||||||
href: `https://www.openstreetmap.org/${id}`,
|
href: `https://www.openstreetmap.org/${id}`,
|
||||||
textContent: L._('See on OpenStreetMap'),
|
textContent: L._('See on OpenStreetMap'),
|
||||||
})
|
})
|
||||||
|
|
|
@ -143,7 +143,7 @@ U.Share = L.Class.extend({
|
||||||
}
|
}
|
||||||
const iframeExporter = new U.IframeExporter(this.map)
|
const iframeExporter = new U.IframeExporter(this.map)
|
||||||
const buildIframeCode = () => {
|
const buildIframeCode = () => {
|
||||||
iframe.innerHTML = iframeExporter.build()
|
iframe.textContent = iframeExporter.build()
|
||||||
exportUrl.value = window.location.protocol + iframeExporter.buildUrl()
|
exportUrl.value = window.location.protocol + iframeExporter.buildUrl()
|
||||||
}
|
}
|
||||||
buildIframeCode()
|
buildIframeCode()
|
||||||
|
|
|
@ -51,21 +51,22 @@ U.UI = L.Evented.extend({
|
||||||
close,
|
close,
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
L.DomUtil.add('i', 'umap-close-icon', closeButton)
|
L.DomUtil.create('i', 'umap-close-icon', closeButton)
|
||||||
const label = L.DomUtil.create('span', '', closeButton)
|
const label = L.DomUtil.create('span', '', closeButton)
|
||||||
label.title = label.textContent = L._('Close')
|
label.title = label.textContent = L._('Close')
|
||||||
L.DomUtil.add('div', '', this._alert, e.content)
|
L.DomUtil.element({ tagName: 'div', innerHTML: e.content, parent: this._alert })
|
||||||
if (e.actions) {
|
if (e.actions) {
|
||||||
let action, el, input
|
let action, el, input
|
||||||
const form = L.DomUtil.add('div', 'umap-alert-actions', this._alert)
|
const form = L.DomUtil.create('div', 'umap-alert-actions', this._alert)
|
||||||
for (let i = 0; i < e.actions.length; i++) {
|
for (let i = 0; i < e.actions.length; i++) {
|
||||||
action = e.actions[i]
|
action = e.actions[i]
|
||||||
if (action.input) {
|
if (action.input) {
|
||||||
input = L.DomUtil.element(
|
input = L.DomUtil.element({
|
||||||
'input',
|
tagName: 'input',
|
||||||
{ className: 'umap-alert-input', placeholder: action.input },
|
parent: form,
|
||||||
form
|
className: 'umap-alert-input',
|
||||||
)
|
placeholder: action.input,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
el = L.DomUtil.createButton(
|
el = L.DomUtil.createButton(
|
||||||
'umap-action',
|
'umap-action',
|
||||||
|
@ -97,7 +98,7 @@ U.UI = L.Evented.extend({
|
||||||
this.anchorTooltipAbsolute()
|
this.anchorTooltipAbsolute()
|
||||||
}
|
}
|
||||||
L.DomUtil.addClass(this.parent, 'umap-tooltip')
|
L.DomUtil.addClass(this.parent, 'umap-tooltip')
|
||||||
this._tooltip.innerHTML = opts.content
|
this._tooltip.innerHTML = U.Utils.escapeHTML(opts.content)
|
||||||
}
|
}
|
||||||
this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0)
|
this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0)
|
||||||
const id = this.TOOLTIP_ID
|
const id = this.TOOLTIP_ID
|
||||||
|
|
|
@ -45,3 +45,24 @@ def test_create_map_with_cursor(page, live_server, tilelayer):
|
||||||
"z-index: 200;"
|
"z-index: 200;"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_put_script_tag_in_datalayer_name_or_description(
|
||||||
|
openmap, live_server, page, tilelayer
|
||||||
|
):
|
||||||
|
page.goto(f"{live_server.url}{openmap.get_absolute_url()}")
|
||||||
|
page.get_by_role("button", name="Edit").click()
|
||||||
|
page.get_by_role("link", name="Manage layers").click()
|
||||||
|
page.get_by_role("button", name="Add a layer").click()
|
||||||
|
page.locator('input[name="name"]').click()
|
||||||
|
page.locator('input[name="name"]').fill('<script>alert("attack")</script>')
|
||||||
|
page.locator(".umap-field-description textarea").click()
|
||||||
|
page.locator(".umap-field-description textarea").fill(
|
||||||
|
'<p>before <script>alert("attack")</script> after</p>'
|
||||||
|
)
|
||||||
|
page.get_by_role("button", name="Save").click()
|
||||||
|
page.get_by_role("button", name="About").click()
|
||||||
|
# Title should contain raw HTML (we are using textContent)
|
||||||
|
expect(page.get_by_text('<script>alert("attack")</script>')).to_be_visible()
|
||||||
|
# Description should contain escaped HTML
|
||||||
|
expect(page.get_by_text("before after")).to_be_visible()
|
||||||
|
|
|
@ -112,3 +112,13 @@ def test_map_should_display_custom_tilelayer(map, live_server, tilelayers, page)
|
||||||
iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer")
|
iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer")
|
||||||
# The second of the list should be the current
|
# The second of the list should be the current
|
||||||
expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern)
|
expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern)
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_have_smart_text_in_attribution(tilelayer, map, live_server, page):
|
||||||
|
map.settings["properties"]["tilelayer"]["attribution"] = (
|
||||||
|
"© [[http://www.openstreetmap.org/copyright|OpenStreetMap]] contributors"
|
||||||
|
)
|
||||||
|
map.save()
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
expect(page.get_by_text("© OpenStreetMap contributors")).to_be_visible()
|
||||||
|
expect(page.get_by_role("link", name="OpenStreetMap")).to_be_visible()
|
||||||
|
|
Loading…
Reference in a new issue