refactor shareBox into separate class

download() and format() functions as well as dialog itself and
IFrameExporter helper moved into new file umap.share.js
This commit is contained in:
Joachim Schleicher 2023-12-15 10:16:55 +01:00
parent 931ac7442d
commit d19cc60a7a
7 changed files with 277 additions and 269 deletions

View file

@ -410,8 +410,8 @@ L.Control.Embed = L.Control.extend({
'',
container,
L._('Share, download and embed this map'),
map.renderShareBox,
map
map.share.open,
map.share
)
L.DomEvent.on(shareButton, 'dblclick', L.DomEvent.stopPropagation)
return container
@ -894,46 +894,6 @@ L.U.Map.include({
this.ui.openPanel({ data: { html: container }, actions: actions })
},
EXPORT_TYPES: {
geojson: {
formatter: function (map) {
return JSON.stringify(map.toGeoJSON(), null, 2)
},
ext: '.geojson',
filetype: 'application/json',
},
gpx: {
formatter: function (map) {
return togpx(map.toGeoJSON())
},
ext: '.gpx',
filetype: 'application/gpx+xml',
},
kml: {
formatter: function (map) {
return tokml(map.toGeoJSON())
},
ext: '.kml',
filetype: 'application/vnd.google-earth.kml+xml',
},
csv: {
formatter: function (map) {
const table = []
map.eachFeature((feature) => {
const row = feature.toGeoJSON()['properties'],
center = feature.getCenter()
delete row['_umap_options']
row['Latitude'] = center.lat
row['Longitude'] = center.lng
table.push(row)
})
return csv2geojson.dsv.csvFormat(table)
},
ext: '.csv',
filetype: 'text/csv',
},
},
renderEditToolbar: function () {
const container = L.DomUtil.create(
'div',
@ -1081,130 +1041,6 @@ L.U.Map.include({
this
)
},
renderShareBox: function () {
const container = L.DomUtil.create('div', 'umap-share')
const title = L.DomUtil.create('h3', '', container)
title.textContent = L._('Share, download and embed this map')
if (this.options.shortUrl) {
L.DomUtil.createButton(
'button copy-button',
container,
L._('copy'),
() => navigator.clipboard.writeText(this.options.shortUrl),
this
)
L.DomUtil.add('h4', '', container, L._('Short URL'))
const shortUrlLabel = L.DomUtil.create('label', '', container)
shortUrlLabel.textContent = L._('Share this link to view the map')
const shortUrl = L.DomUtil.create('input', 'umap-short-url', container)
shortUrl.type = 'text'
shortUrl.value = this.options.shortUrl
L.DomUtil.create('hr', '', container)
}
L.DomUtil.add('h4', '', container, L._('Download data'))
const downloadLabel = L.DomUtil.create('label', '', container)
downloadLabel.textContent = L._('Choose the format of the data to export')
const exportCaveat = L.DomUtil.add(
'small',
'help-text',
container,
L._('Only visible features will be downloaded.')
)
console.log(this.EXPORT_TYPES)
const typeInput = L.DomUtil.create(
'div',
`button-bar by${Object.keys(this.EXPORT_TYPES).length}`,
container
)
let option
for (const key in this.EXPORT_TYPES) {
if (this.EXPORT_TYPES.hasOwnProperty(key)) {
L.DomUtil.createButton(
'button',
typeInput,
this.EXPORT_TYPES[key].name || key,
() => this.download(key),
this
)
}
}
L.DomUtil.create('hr', '', container)
L.DomUtil.add('h4', '', container, L._('Backup data'))
const backupLabel = L.DomUtil.create('label', '', container)
backupLabel.textContent = L._('Download all data and settings of the map')
const downloadUrl = L.Util.template(this.options.urls.map_download, {
map_id: this.options.umap_id,
})
const link = L.DomUtil.createLink(
'button',
container,
L._('Download full backup'),
downloadUrl
)
let name = this.options.name || 'data'
name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase()
link.setAttribute('download', `${name}.umap`)
L.DomUtil.create('hr', '', container)
const embedTitle = L.DomUtil.add('h4', '', container, L._('Embed the map'))
const iframe = L.DomUtil.create('textarea', 'umap-share-iframe', container)
const urlTitle = L.DomUtil.add('h4', '', container, L._('Direct link'))
const shortUrlLabel = L.DomUtil.create('label', '', container)
shortUrlLabel.textContent = L._(
'Share this link to open a customized map view'
)
const exportUrl = L.DomUtil.create('input', 'umap-share-url', container)
exportUrl.type = 'text'
const UIFields = [
['dimensions.width', { handler: 'Input', label: L._('width') }],
['dimensions.height', { handler: 'Input', label: L._('height') }],
[
'options.includeFullScreenLink',
{ handler: 'Switch', label: L._('Include full screen link?') },
],
[
'options.currentView',
{ handler: 'Switch', label: L._('Current view instead of default map view?') },
],
[
'options.keepCurrentDatalayers',
{ handler: 'Switch', label: L._('Keep current visible layers') },
],
[
'options.viewCurrentFeature',
{ handler: 'Switch', label: L._('Open current feature on load') },
],
'queryString.moreControl',
'queryString.scrollWheelZoom',
'queryString.miniMap',
'queryString.scaleControl',
'queryString.onLoadPanel',
'queryString.captionBar',
'queryString.captionMenus',
]
for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) {
UIFields.push(`queryString.${this.HIDDABLE_CONTROLS[i]}Control`)
}
const iframeExporter = new L.U.IframeExporter(this)
const buildIframeCode = () => {
iframe.innerHTML = iframeExporter.build()
exportUrl.value = window.location.protocol + iframeExporter.buildUrl()
}
buildIframeCode()
const builder = new L.U.FormBuilder(iframeExporter, UIFields, {
callback: buildIframeCode,
})
const iframeOptions = L.DomUtil.createFieldset(
container,
L._('Embed and link options')
)
iframeOptions.appendChild(builder.build())
this.ui.openPanel({ data: { html: container } })
},
})
/* Used in view mode to define the current tilelayer */
@ -1527,77 +1363,6 @@ L.U.ContextMenu = L.Map.ContextMenu.extend({
},
})
L.U.IframeExporter = L.Evented.extend({
options: {
includeFullScreenLink: true,
currentView: false,
keepCurrentDatalayers: false,
viewCurrentFeature: false,
},
queryString: {
scaleControl: false,
miniMap: false,
scrollWheelZoom: false,
zoomControl: true,
editMode: 'disabled',
moreControl: true,
searchControl: null,
tilelayersControl: null,
embedControl: null,
datalayersControl: true,
onLoadPanel: 'none',
captionBar: false,
captionMenus: true,
},
dimensions: {
width: '100%',
height: '300px',
},
initialize: function (map) {
this.map = map
this.baseUrl = L.Util.getBaseUrl()
// Use map default, not generic default
this.queryString.onLoadPanel = this.map.options.onLoadPanel
},
getMap: function () {
return this.map
},
buildUrl: function (options) {
const datalayers = []
if (this.options.viewCurrentFeature && this.map.currentFeature) {
this.queryString.feature = this.map.currentFeature.getSlug()
}
if (this.options.keepCurrentDatalayers) {
this.map.eachDataLayer((datalayer) => {
if (datalayer.isVisible() && datalayer.umap_id) {
datalayers.push(datalayer.umap_id)
}
})
this.queryString.datalayers = datalayers.join(',')
} else {
delete this.queryString.datalayers
}
const currentView = this.options.currentView ? window.location.hash : ''
const queryString = L.extend({}, this.queryString, options)
return `${this.baseUrl}?${L.Util.buildQueryString(queryString)}${currentView}`
},
build: function () {
const iframeUrl = this.buildUrl()
let code = `<iframe width="${this.dimensions.width}" height="${this.dimensions.height}" frameborder="0" allowfullscreen allow="geolocation" src="${iframeUrl}"></iframe>`
if (this.options.includeFullScreenLink) {
const fullUrl = this.buildUrl({ scrollWheelZoom: true })
code += `<p><a href="${fullUrl}">${L._('See full screen')}</a></p>`
}
return code
},
})
L.U.Editable = L.Editable.extend({
initialize: function (map, options) {
L.Editable.prototype.initialize.call(this, map, options)

View file

@ -254,7 +254,7 @@ L.U.Map.include({
}
this.initShortcuts()
this.onceDataLoaded(function () {
if (L.Util.queryString('share')) this.renderShareBox()
if (L.Util.queryString('share')) this.share.open()
else if (this.options.onLoadPanel === 'databrowser') this.openBrowser()
else if (this.options.onLoadPanel === 'caption') this.displayCaption()
else if (
@ -347,6 +347,7 @@ L.U.Map.include({
this.browser = new L.U.Browser(this)
this.importer = new L.U.Importer(this)
this.drop = new L.U.DropControl(this)
this.share = new L.U.Share(this)
this._controls.tilelayers = new L.U.TileLayerControl(this)
this._controls.tilelayers.setLayers()
@ -846,28 +847,6 @@ L.U.Map.include({
})
},
format: function (mode) {
const type = this.EXPORT_TYPES[mode]
const content = type.formatter(this)
let name = this.options.name || 'data'
name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase()
const filename = name + type.ext
return { content, filetype: type.filetype, filename }
},
download: function (mode) {
const { content, filetype, filename } = this.format(mode)
const blob = new Blob([content], { type: filetype })
window.URL = window.URL || window.webkitURL
const el = document.createElement('a')
el.download = filename
el.href = window.URL.createObjectURL(blob)
el.style.display = 'none'
document.body.appendChild(el)
el.click()
document.body.removeChild(el)
},
processFileToImport: function (file, layer, type) {
type = type || L.Util.detectFileType(file)
if (!type) {
@ -1699,9 +1678,9 @@ L.U.Map.include({
L.DomUtil.createButton(
'button umap-download',
advancedButtons,
L._('Open download panel'),
this.renderShareBox,
this
L._('Open share & download panel'),
this.share.open,
this.share
)
},

View file

@ -0,0 +1,263 @@
L.U.Share = L.Class.extend({
EXPORT_TYPES: {
geojson: {
formatter: function (map) {
return JSON.stringify(map.toGeoJSON(), null, 2)
},
ext: '.geojson',
filetype: 'application/json',
},
gpx: {
formatter: function (map) {
return togpx(map.toGeoJSON())
},
ext: '.gpx',
filetype: 'application/gpx+xml',
},
kml: {
formatter: function (map) {
return tokml(map.toGeoJSON())
},
ext: '.kml',
filetype: 'application/vnd.google-earth.kml+xml',
},
csv: {
formatter: function (map) {
const table = []
map.eachFeature((feature) => {
const row = feature.toGeoJSON()['properties'],
center = feature.getCenter()
delete row['_umap_options']
row['Latitude'] = center.lat
row['Longitude'] = center.lng
table.push(row)
})
return csv2geojson.dsv.csvFormat(table)
},
ext: '.csv',
filetype: 'text/csv',
},
},
initialize: function (map) {
this.map = map
},
build: function () {
this.container = L.DomUtil.create('div', 'umap-share')
this.title = L.DomUtil.create('h3', '', this.container)
this.title.textContent = L._('Share, download and embed this map')
if (this.map.options.shortUrl) {
L.DomUtil.createButton(
'button copy-button',
this.container,
L._('copy'),
() => navigator.clipboard.writeText(this.map.options.shortUrl),
this
)
L.DomUtil.add('h4', '', this.container, L._('Short URL'))
const shortUrlLabel = L.DomUtil.create('label', '', this.container)
shortUrlLabel.textContent = L._('Share this link to view the map')
const shortUrl = L.DomUtil.create('input', 'umap-short-url', this.container)
shortUrl.type = 'text'
shortUrl.value = this.map.options.shortUrl
L.DomUtil.create('hr', '', this.container)
}
L.DomUtil.add('h4', '', this.container, L._('Download data'))
const downloadLabel = L.DomUtil.create('label', '', this.container)
downloadLabel.textContent = L._('Choose the format of the data to export')
const exportCaveat = L.DomUtil.add(
'small',
'help-text',
this.container,
L._('Only visible features will be downloaded.')
)
console.log(this.EXPORT_TYPES)
const typeInput = L.DomUtil.create(
'div',
`button-bar by${Object.keys(this.EXPORT_TYPES).length}`,
this.container
)
let option
for (const key in this.EXPORT_TYPES) {
if (this.EXPORT_TYPES.hasOwnProperty(key)) {
L.DomUtil.createButton(
'button',
typeInput,
this.EXPORT_TYPES[key].name || key,
() => this.download(key),
this
)
}
}
L.DomUtil.create('hr', '', this.container)
L.DomUtil.add('h4', '', this.container, L._('Backup data'))
const backupLabel = L.DomUtil.create('label', '', this.container)
backupLabel.textContent = L._('Download all data and properties of the map')
const downloadUrl = L.Util.template(this.map.options.urls.map_download, {
map_id: this.map.options.umap_id,
})
const link = L.DomUtil.createLink(
'button',
this.container,
L._('Download full backup'),
downloadUrl
)
let name = this.map.options.name || 'data'
name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase()
link.setAttribute('download', `${name}.umap`)
L.DomUtil.create('hr', '', this.container)
const embedTitle = L.DomUtil.add('h4', '', this.container, L._('Embed the map'))
const iframe = L.DomUtil.create('textarea', 'umap-share-iframe', this.container)
const urlTitle = L.DomUtil.add('h4', '', this.container, L._('Direct link'))
const shortUrlLabel = L.DomUtil.create('label', '', this.container)
shortUrlLabel.textContent = L._('Share this link to open a customized map view')
const exportUrl = L.DomUtil.create('input', 'umap-share-url', this.container)
exportUrl.type = 'text'
const UIFields = [
['dimensions.width', { handler: 'Input', label: L._('width') }],
['dimensions.height', { handler: 'Input', label: L._('height') }],
[
'options.includeFullScreenLink',
{ handler: 'Switch', label: L._('Include full screen link?') },
],
[
'options.currentView',
{ handler: 'Switch', label: L._('Current view instead of default map view?') },
],
[
'options.keepCurrentDatalayers',
{ handler: 'Switch', label: L._('Keep current visible layers') },
],
[
'options.viewCurrentFeature',
{ handler: 'Switch', label: L._('Open current feature on load') },
],
'queryString.moreControl',
'queryString.scrollWheelZoom',
'queryString.miniMap',
'queryString.scaleControl',
'queryString.onLoadPanel',
'queryString.captionBar',
'queryString.captionMenus',
]
for (let i = 0; i < this.map.HIDDABLE_CONTROLS.length; i++) {
UIFields.push(`queryString.${this.map.HIDDABLE_CONTROLS[i]}Control`)
}
const iframeExporter = new L.U.IframeExporter(this.map)
const buildIframeCode = () => {
iframe.innerHTML = iframeExporter.build()
exportUrl.value = window.location.protocol + iframeExporter.buildUrl()
}
buildIframeCode()
const builder = new L.U.FormBuilder(iframeExporter, UIFields, {
callback: buildIframeCode,
})
const iframeOptions = L.DomUtil.createFieldset(
this.container,
L._('Embed and link options')
)
iframeOptions.appendChild(builder.build())
},
open: function () {
if (!this.container) this.build()
this.map.ui.openPanel({ data: { html: this.container } })
},
format: function (mode) {
const type = this.EXPORT_TYPES[mode]
const content = type.formatter(this.map)
let name = this.map.options.name || 'data'
name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase()
const filename = name + type.ext
return { content, filetype: type.filetype, filename }
},
download: function (mode) {
const { content, filetype, filename } = this.format(mode)
const blob = new Blob([content], { type: filetype })
window.URL = window.URL || window.webkitURL
const el = document.createElement('a')
el.download = filename
el.href = window.URL.createObjectURL(blob)
el.style.display = 'none'
document.body.appendChild(el)
el.click()
document.body.removeChild(el)
},
})
L.U.IframeExporter = L.Evented.extend({
options: {
includeFullScreenLink: true,
currentView: false,
keepCurrentDatalayers: false,
viewCurrentFeature: false,
},
queryString: {
scaleControl: false,
miniMap: false,
scrollWheelZoom: false,
zoomControl: true,
editMode: 'disabled',
moreControl: true,
searchControl: null,
tilelayersControl: null,
embedControl: null,
datalayersControl: true,
onLoadPanel: 'none',
captionBar: false,
captionMenus: true,
},
dimensions: {
width: '100%',
height: '300px',
},
initialize: function (map) {
this.map = map
this.baseUrl = L.Util.getBaseUrl()
// Use map default, not generic default
this.queryString.onLoadPanel = this.map.options.onLoadPanel
},
getMap: function () {
return this.map
},
buildUrl: function (options) {
const datalayers = []
if (this.options.viewCurrentFeature && this.map.currentFeature) {
this.queryString.feature = this.map.currentFeature.getSlug()
}
if (this.options.keepCurrentDatalayers) {
this.map.eachDataLayer((datalayer) => {
if (datalayer.isVisible() && datalayer.umap_id) {
datalayers.push(datalayer.umap_id)
}
})
this.queryString.datalayers = datalayers.join(',')
} else {
delete this.queryString.datalayers
}
const currentView = this.options.currentView ? window.location.hash : ''
const queryString = L.extend({}, this.queryString, options)
return `${this.baseUrl}?${L.Util.buildQueryString(queryString)}${currentView}`
},
build: function () {
const iframeUrl = this.buildUrl()
let code = `<iframe width="${this.dimensions.width}" height="${this.dimensions.height}" frameborder="0" allowfullscreen allow="geolocation" src="${iframeUrl}"></iframe>`
if (this.options.includeFullScreenLink) {
const fullUrl = this.buildUrl({ scrollWheelZoom: true })
code += `<p><a href="${fullUrl}">${L._('See full screen')}</a></p>`
}
return code
},
})

View file

@ -20,7 +20,7 @@ describe('L.U.Map.Export', function () {
describe('#formatters()', function () {
it('should export to geojson', function () {
const { content, filetype, filename } = this.map.format('geojson')
const { content, filetype, filename } = this.map.share.format('geojson')
assert.equal(filetype, 'application/json')
assert.equal(filename, 'name_of_the_map.geojson')
assert.deepEqual(JSON.parse(content), {
@ -86,7 +86,7 @@ describe('L.U.Map.Export', function () {
})
it('should export to gpx', function () {
const { content, filetype, filename } = this.map.format('gpx')
const { content, filetype, filename } = this.map.share.format('gpx')
assert.equal(filetype, 'application/gpx+xml')
assert.equal(filename, 'name_of_the_map.gpx')
const expected =
@ -95,7 +95,7 @@ describe('L.U.Map.Export', function () {
})
it('should export to kml', function () {
const { content, filetype, filename } = this.map.format('kml')
const { content, filetype, filename } = this.map.share.format('kml')
assert.equal(filetype, 'application/vnd.google-earth.kml+xml')
assert.equal(filename, 'name_of_the_map.kml')
const expected =

View file

@ -43,6 +43,7 @@
<script src="../js/umap.datalayer.permissions.js"></script>
<script src="../js/umap.browser.js"></script>
<script src="../js/umap.importer.js"></script>
<script src="../js/umap.share.js"></script>
<script src="../js/umap.js"></script>
<script src="../js/umap.ui.js"></script>
<link rel="stylesheet" href="../vendors/leaflet/leaflet.css" />

View file

@ -45,6 +45,7 @@
<script src="{{ STATIC_URL }}umap/js/umap.tableeditor.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.browser.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.importer.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.share.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.js"></script>
<script src="{{ STATIC_URL }}umap/js/umap.ui.js"></script>
{% endcompress %}

View file

@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
def test_umap_export(map, live_server, datalayer, page):
page.goto(f"{live_server.url}{map.get_absolute_url()}?share")
link = page.get_by_role("link", name="Download full data")
link = page.get_by_role("link", name="Download full backup")
expect(link).to_be_visible()
with page.expect_download() as download_info:
link.click()
@ -73,9 +73,8 @@ def test_umap_export(map, live_server, datalayer, page):
def test_csv_export(map, live_server, datalayer, page):
page.goto(f"{live_server.url}{map.get_absolute_url()}?share")
button = page.get_by_role("button", name="Download data")
button = page.get_by_role("button", name="csv")
expect(button).to_be_visible()
page.locator('select[name="format"]').select_option("csv")
with page.expect_download() as download_info:
button.click()
download = download_info.value