diff --git a/umap/static/umap/css/icon.css b/umap/static/umap/css/icon.css
index fb22329f..ad4e4cfb 100644
--- a/umap/static/umap/css/icon.css
+++ b/umap/static/umap/css/icon.css
@@ -118,6 +118,10 @@
.icon-upload {
background-position: -144px -97px;
}
+.icon-download {
+ transform: rotate(180deg);
+ background-position: -146px -95px;
+}
.icon-zoom {
background-position: -1px -49px;
}
diff --git a/umap/static/umap/js/modules/tableeditor.js b/umap/static/umap/js/modules/tableeditor.js
index 622dee21..f5db47b0 100644
--- a/umap/static/umap/js/modules/tableeditor.js
+++ b/umap/static/umap/js/modules/tableeditor.js
@@ -1,7 +1,7 @@
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import ContextMenu from './ui/contextmenu.js'
-import { WithTemplate, loadTemplate } from './utils.js'
+import { WithTemplate, loadTemplate, downloadBlob } from './utils.js'
const TEMPLATE = `
@@ -201,6 +201,13 @@ export default class TableEditor extends WithTemplate {
filterButton.addEventListener('click', () => this.map.browser.open('filters'))
actions.push(filterButton)
+ const downloadButton = loadTemplate(`
+ `)
+ downloadButton.addEventListener('click', () => this.exportAsCSV())
+ actions.push(downloadButton)
+
this.map.fullPanel.open({
content: this.table,
className: 'umap-table-editor',
@@ -295,7 +302,7 @@ export default class TableEditor extends WithTemplate {
getSelectedRows() {
return Array.from(
this.elements.body.querySelectorAll('input[type=checkbox]:checked')
- ).map((checkbox) => checkbox.parentNode.parentNode)
+ ).map((checkbox) => checkbox.closest('tr'))
}
getFocus() {
@@ -331,4 +338,32 @@ export default class TableEditor extends WithTemplate {
}
})
}
+
+ _rowToCSV(row) {
+ console.log(row)
+ return row
+ .map((content) => content.replaceAll('"', '""')) // escape double quotes
+ .map((content) => `"${content}"`) // quote it
+ .join(',') // comma-separated
+ }
+
+ exportAsCSV() {
+ const headers = this._rowToCSV(
+ Array.from(this.elements.header.querySelectorAll('th'))
+ .slice(1) // Remove initial select-all checkbox column
+ .map((header) => header.textContent)
+ .map((header) => header.slice(0, -1)) // Remove trailing `…`
+ )
+ const rows = Array.from(this.elements.body.querySelectorAll('tr'))
+ .map((line) =>
+ Array.from(line.querySelectorAll('td')).map((cell) => cell.textContent)
+ )
+ .map(this._rowToCSV)
+ const csv = [headers, ...rows].join('\r\n')
+ downloadBlob(
+ csv,
+ `umap-export-${this.datalayer.umap_id}-${this.map.options.umap_id}.csv`,
+ 'text/csv;charset=utf-8;'
+ )
+ }
}
diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js
index 9b7b11f1..121c24c9 100644
--- a/umap/static/umap/js/modules/utils.js
+++ b/umap/static/umap/js/modules/utils.js
@@ -397,3 +397,12 @@ export class WithTemplate {
return this.element
}
}
+
+export function downloadBlob(content, filename, contentType) {
+ const blob = new Blob([content], { type: contentType })
+ const url = URL.createObjectURL(blob)
+ const tmp = document.createElement('a')
+ tmp.href = url
+ tmp.setAttribute('download', filename)
+ tmp.click()
+}