From d19cc60a7a8e8d88152f7c98c1c206a9094f8972 Mon Sep 17 00:00:00 2001 From: Joachim Schleicher Date: Fri, 15 Dec 2023 10:16:55 +0100 Subject: [PATCH] refactor shareBox into separate class download() and format() functions as well as dialog itself and IFrameExporter helper moved into new file umap.share.js --- umap/static/umap/js/umap.controls.js | 239 +------------------- umap/static/umap/js/umap.js | 31 +-- umap/static/umap/js/umap.share.js | 263 ++++++++++++++++++++++ umap/static/umap/test/Map.Export.js | 6 +- umap/static/umap/test/index.html | 1 + umap/templates/umap/js.html | 1 + umap/tests/integration/test_export_map.py | 5 +- 7 files changed, 277 insertions(+), 269 deletions(-) create mode 100644 umap/static/umap/js/umap.share.js diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index bcdd0be4..2a6871c0 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -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 = `` - if (this.options.includeFullScreenLink) { - const fullUrl = this.buildUrl({ scrollWheelZoom: true }) - code += `

${L._('See full screen')}

` - } - return code - }, -}) - L.U.Editable = L.Editable.extend({ initialize: function (map, options) { L.Editable.prototype.initialize.call(this, map, options) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index c814854e..5faf7c19 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -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 ) }, diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js new file mode 100644 index 00000000..fff7bb73 --- /dev/null +++ b/umap/static/umap/js/umap.share.js @@ -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 = `` + if (this.options.includeFullScreenLink) { + const fullUrl = this.buildUrl({ scrollWheelZoom: true }) + code += `

${L._('See full screen')}

` + } + return code + }, +}) diff --git a/umap/static/umap/test/Map.Export.js b/umap/static/umap/test/Map.Export.js index b0cda485..8983c364 100644 --- a/umap/static/umap/test/Map.Export.js +++ b/umap/static/umap/test/Map.Export.js @@ -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 = diff --git a/umap/static/umap/test/index.html b/umap/static/umap/test/index.html index bc371d16..328c5262 100644 --- a/umap/static/umap/test/index.html +++ b/umap/static/umap/test/index.html @@ -43,6 +43,7 @@ + diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 131c1a02..066def43 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -45,6 +45,7 @@ + {% endcompress %} diff --git a/umap/tests/integration/test_export_map.py b/umap/tests/integration/test_export_map.py index 28b215a1..5007232c 100644 --- a/umap/tests/integration/test_export_map.py +++ b/umap/tests/integration/test_export_map.py @@ -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