From 931ac7442d3fff6bd9876020fffe2fc5a9a6348b Mon Sep 17 00:00:00 2001 From: Joachim Schleicher Date: Sun, 10 Dec 2023 20:03:14 +0100 Subject: [PATCH 1/5] refactor share dialog for better usability * ordering is now short URL - download - backup - embed ordered by easier use cases for everyone to complex iframe code for web experts * show available formats as buttons instead of hiding them in a dropdown * add explaining labels * change the dark options fieldset to light gray --- umap/static/umap/base.css | 27 ++++-- umap/static/umap/img/16.svg | 4 +- umap/static/umap/img/source/16.svg | 27 +++++- umap/static/umap/js/umap.controls.js | 119 ++++++++++++++++----------- 4 files changed, 116 insertions(+), 61 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 78b7c342..b07dd8f1 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -162,14 +162,16 @@ textarea { padding: 7px; } select { + border: 1px solid #222; width: 100%; height: 28px; line-height: 28px; - color: #efefef; - border: 1px solid #222; - background-color: #393F3F; margin-top: 5px; } +.dark select { + color: #efefef; + background-color: #393F3F; +} select[multiple="multiple"] { height: auto; } @@ -225,7 +227,8 @@ button.flat, font-size: 10px; border-radius: 0 2px; } -.content .helptext { +.content .helptext, +#umap-ui-container .help-text { background-color: #eee; color: #000; } @@ -294,16 +297,19 @@ input[type="file"] + .error { text-align: left; display: block; cursor: pointer; - background-color: #232729; + background-color: #eee; height: 30px; line-height: 30px; - color: #fff; margin: 0; font-family: fira_sans; font-weight: normal; font-size: 1.2em; padding: 0 5px; } +.dark .fieldset.toggle .legend { + background-color: #232729; + color: #fff; +} .fieldset.toggle .legend:before { background-repeat: no-repeat; text-indent: 24px; @@ -311,11 +317,14 @@ input[type="file"] + .error { width: 24px; line-height: 24px; display: inline-block; - background-image: url('./img/16-white.svg'); + background-image: url('./img/16.svg'); vertical-align: bottom; content: " "; background-position: -144px -76px; } +.dark .fieldset.toggle .legend:before { + background-image: url('./img/16-white.svg'); +} .fieldset.toggle.on .legend:before { background-position: -144px -51px; } @@ -401,6 +410,7 @@ input.switch:checked ~ label:after { .umap-multiplechoice.by5 { grid-template-columns: 1fr 1fr 1fr; } +.button-bar.by4, .umap-multiplechoice.by4 { grid-template-columns: 1fr 1fr 1fr 1fr; } @@ -448,7 +458,8 @@ input.switch:checked ~ label:after { } .umap-field-iconUrl .action-button, .inheritable .define, -.inheritable .undefine { +.inheritable .undefine, +.copy-button { float: right; width: initial; min-height: 18px; diff --git a/umap/static/umap/img/16.svg b/umap/static/umap/img/16.svg index a7f38acd..3ed21c10 100644 --- a/umap/static/umap/img/16.svg +++ b/umap/static/umap/img/16.svg @@ -1,4 +1,4 @@ - + @@ -168,5 +168,7 @@ + + diff --git a/umap/static/umap/img/source/16.svg b/umap/static/umap/img/source/16.svg index f63086c6..ed2042bf 100644 --- a/umap/static/umap/img/source/16.svg +++ b/umap/static/umap/img/source/16.svg @@ -2,7 +2,7 @@ + @@ -852,5 +859,17 @@ id="path2378" style="stroke-width:0.264583" /> + + diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 157f32bf..bcdd0be4 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1086,12 +1086,77 @@ L.U.Map.include({ 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) - let option exportUrl.type = 'text' const UIFields = [ ['dimensions.width', { handler: 'Input', label: L._('width') }], @@ -1132,54 +1197,12 @@ L.U.Map.include({ const builder = new L.U.FormBuilder(iframeExporter, UIFields, { callback: buildIframeCode, }) - const iframeOptions = L.DomUtil.createFieldset(container, L._('Export options')) + const iframeOptions = L.DomUtil.createFieldset( + container, + L._('Embed and link options') + ) iframeOptions.appendChild(builder.build()) - if (this.options.shortUrl) { - L.DomUtil.create('hr', '', container) - L.DomUtil.add('h4', '', container, L._('Short URL')) - 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._('Backup data')) - 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 data'), - 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) - L.DomUtil.add('h4', '', container, L._('Download data')) - const typeInput = L.DomUtil.create('select', '', container) - typeInput.name = 'format' - const exportCaveat = L.DomUtil.add( - 'small', - 'help-text', - container, - L._('Only visible features will be downloaded.') - ) - for (const key in this.EXPORT_TYPES) { - if (this.EXPORT_TYPES.hasOwnProperty(key)) { - option = L.DomUtil.create('option', '', typeInput) - option.value = key - option.textContent = this.EXPORT_TYPES[key].name || key - if (this.EXPORT_TYPES[key].selected) option.selected = true - } - } - L.DomUtil.createButton( - 'button', - container, - L._('Download data'), - () => this.download(typeInput.value), - this - ) + this.ui.openPanel({ data: { html: container } }) }, }) From d19cc60a7a8e8d88152f7c98c1c206a9094f8972 Mon Sep 17 00:00:00 2001 From: Joachim Schleicher Date: Fri, 15 Dec 2023 10:16:55 +0100 Subject: [PATCH 2/5] 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 From aec4330df6ec77295ab1db45bd5f5d038a26b92e Mon Sep 17 00:00:00 2001 From: Joachim Schleicher Date: Tue, 19 Dec 2023 16:40:31 +0100 Subject: [PATCH 3/5] add download icons file icon CC0 from https://www.iconfinder.com/icons/9110902/file_download_icon arrow down drawn in inkscape --- umap/static/umap/img/16.svg | 8 ++++++ umap/static/umap/img/source/16.svg | 43 +++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/umap/static/umap/img/16.svg b/umap/static/umap/img/16.svg index 3ed21c10..1db810b6 100644 --- a/umap/static/umap/img/16.svg +++ b/umap/static/umap/img/16.svg @@ -1,5 +1,8 @@ + + + @@ -170,5 +173,10 @@ + + + + + diff --git a/umap/static/umap/img/source/16.svg b/umap/static/umap/img/source/16.svg index ed2042bf..99554023 100644 --- a/umap/static/umap/img/source/16.svg +++ b/umap/static/umap/img/source/16.svg @@ -20,6 +20,26 @@ xmlns:dc="http://purl.org/dc/elements/1.1/"> + + + + + + + + From 3f6c705d2e85c7271a8c592a833e1242d22e7cab Mon Sep 17 00:00:00 2001 From: Joachim Schleicher Date: Tue, 19 Dec 2023 17:27:27 +0100 Subject: [PATCH 4/5] re-style download panel according to suggestion by @Aurelie-Jallut in https://github.com/umap-project/umap/pull/1454#issuecomment-1858062371 --- umap/static/umap/js/umap.controls.js | 2 +- umap/static/umap/js/umap.share.js | 64 ++++++++++++++-------------- umap/static/umap/map.css | 33 ++++++++++++++ 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 2a6871c0..0af058ce 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -409,7 +409,7 @@ L.Control.Embed = L.Control.extend({ const shareButton = L.DomUtil.createButton( '', container, - L._('Share, download and embed this map'), + L._('Share and download'), map.share.open, map.share ) diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js index fff7bb73..7a397a49 100644 --- a/umap/static/umap/js/umap.share.js +++ b/umap/static/umap/js/umap.share.js @@ -46,61 +46,59 @@ L.U.Share = L.Class.extend({ 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') + this.title.textContent = L._('Share and download') + + L.DomUtil.createButton( + 'button copy-button', + this.container, + L._('copy'), + () => navigator.clipboard.writeText(this.mapUrl.value), + this + ) + const mapUrlLabel = L.DomUtil.add('label', '', this.container, L._('Link to view the map')) + this.mapUrl = L.DomUtil.create('input', 'umap-share-url', mapUrlLabel) + this.mapUrl.type = 'text' + this.mapUrl.readOnly = true + this.mapUrl.value = window.location.protocol + L.Util.getBaseUrl() + if (this.map.options.shortUrl) { L.DomUtil.createButton( 'button copy-button', this.container, L._('copy'), - () => navigator.clipboard.writeText(this.map.options.shortUrl), + () => navigator.clipboard.writeText(this.shortUrl.value), 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) + shortUrlLabel.textContent = L._('Short link') + this.shortUrl = L.DomUtil.create('input', 'umap-share-url', shortUrlLabel) + this.shortUrl.type = 'text' + this.shortUrl.readOnly = true + this.shortUrl.value = this.map.options.shortUrl } - 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 + L.DomUtil.create('hr', '', this.container) + + L.DomUtil.add('h4', '', this.container, L._('Download')) + L.DomUtil.add('small', 'label', this.container, L._('Only visible layers')) for (const key in this.EXPORT_TYPES) { if (this.EXPORT_TYPES.hasOwnProperty(key)) { L.DomUtil.createButton( - 'button', - typeInput, - this.EXPORT_TYPES[key].name || key, + 'download-file', + this.container, + this.EXPORT_TYPES[key].name || key + ' data', () => 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') + L.DomUtil.create('div', 'vspace', this.container) + L.DomUtil.add('small', 'label', this.container, L._('All data and settings 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', + 'download-backup', this.container, L._('Download full backup'), downloadUrl diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index ccd5ed48..5095a73b 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -239,6 +239,39 @@ ul.photon-autocomplete { +/* ***************************** */ +/* Share and download panel */ +/* ***************************** */ +.download-file { + height: 1.2em; + min-height: 1.2em; + padding: 0; + text-align: left; + vertical-align: bottom; +} +.download-file:before, +.download-backup:before { + height: 24px; + width: 24px; + background-repeat: no-repeat; + background-image: url('./img/16.svg'); + background-size: auto auto; + background-position: -4px -145px; + content: " "; + vertical-align: bottom; + display: inline-block; +} +.download-backup:before { + background-position: -27px -144px; +} +.leaflet-container .download-backup { + color: black; + display: block; +} +.vspace { + height: 32px; +} + /* *********** */ /* Draw */ /* *********** */ From dbf0179297c77a15784b2d6ac152663f750dba4a Mon Sep 17 00:00:00 2001 From: Joachim Schleicher Date: Thu, 28 Dec 2023 14:26:15 +0100 Subject: [PATCH 5/5] cleanup sharebox changes * "data" moved to helptext * drop css introduced in previous draft * prettier.js --- umap/static/umap/base.css | 4 +--- umap/static/umap/js/umap.controls.js | 1 - umap/static/umap/js/umap.share.js | 20 +++++++++++++++----- umap/tests/integration/test_export_map.py | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index b07dd8f1..13039fbd 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -227,8 +227,7 @@ button.flat, font-size: 10px; border-radius: 0 2px; } -.content .helptext, -#umap-ui-container .help-text { +.content .helptext { background-color: #eee; color: #000; } @@ -410,7 +409,6 @@ input.switch:checked ~ label:after { .umap-multiplechoice.by5 { grid-template-columns: 1fr 1fr 1fr; } -.button-bar.by4, .umap-multiplechoice.by4 { grid-template-columns: 1fr 1fr 1fr 1fr; } diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 0af058ce..01968107 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1069,7 +1069,6 @@ L.U.TileLayerControl = L.Control.IconLayers.extend({ L.Control.IconLayers.prototype.setLayers.call(this, layers.slice(0, maxShown)) if (this.map.selected_tilelayer) this.setActiveLayer(this.map.selected_tilelayer) }, - }) /* Used in edit mode to define the default tilelayer */ diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js index 7a397a49..75b1e5ee 100644 --- a/umap/static/umap/js/umap.share.js +++ b/umap/static/umap/js/umap.share.js @@ -55,7 +55,12 @@ L.U.Share = L.Class.extend({ () => navigator.clipboard.writeText(this.mapUrl.value), this ) - const mapUrlLabel = L.DomUtil.add('label', '', this.container, L._('Link to view the map')) + const mapUrlLabel = L.DomUtil.add( + 'label', + '', + this.container, + L._('Link to view the map') + ) this.mapUrl = L.DomUtil.create('input', 'umap-share-url', mapUrlLabel) this.mapUrl.type = 'text' this.mapUrl.readOnly = true @@ -80,27 +85,32 @@ L.U.Share = L.Class.extend({ L.DomUtil.create('hr', '', this.container) L.DomUtil.add('h4', '', this.container, L._('Download')) - L.DomUtil.add('small', 'label', this.container, L._('Only visible layers')) + L.DomUtil.add('small', 'label', this.container, L._("Only visible layers' data")) for (const key in this.EXPORT_TYPES) { if (this.EXPORT_TYPES.hasOwnProperty(key)) { L.DomUtil.createButton( 'download-file', this.container, - this.EXPORT_TYPES[key].name || key + ' data', + this.EXPORT_TYPES[key].name || key, () => this.download(key), this ) } } L.DomUtil.create('div', 'vspace', this.container) - L.DomUtil.add('small', 'label', this.container, L._('All data and settings of the map')) + L.DomUtil.add( + 'small', + 'label', + this.container, + L._('All data and settings 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( 'download-backup', this.container, - L._('Download full backup'), + L._('full backup'), downloadUrl ) let name = this.map.options.name || 'data' diff --git a/umap/tests/integration/test_export_map.py b/umap/tests/integration/test_export_map.py index 5007232c..e1735e9b 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 backup") + link = page.get_by_role("link", name="full backup") expect(link).to_be_visible() with page.expect_download() as download_info: link.click()