diff --git a/umap/static/umap/css/panel.css b/umap/static/umap/css/panel.css
index be99ffa5..19c7240a 100644
--- a/umap/static/umap/css/panel.css
+++ b/umap/static/umap/css/panel.css
@@ -27,7 +27,8 @@
.panel.full.on {
visibility: visible;
right: calc(var(--panel-gutter) * 2 + var(--control-size));
- left: var(--panel-gutter);
+/* left: var(--panel-gutter);*/
+ left: calc(var(--panel-gutter) * 3 + var(--control-size) + var(--panel-width));
height: initial;
max-height: initial;
}
diff --git a/umap/static/umap/css/tableeditor.css b/umap/static/umap/css/tableeditor.css
new file mode 100644
index 00000000..90f955dd
--- /dev/null
+++ b/umap/static/umap/css/tableeditor.css
@@ -0,0 +1,75 @@
+.umap-table-editor {
+ width: 100%;
+ overflow-x: auto;
+}
+.umap-table-editor table {
+ white-space: nowrap;
+ table-layout: fixed;
+ border-collapse: collapse;
+ border-bottom: 1px solid black;
+ border-top: 1px solid black;
+}
+.umap-table-editor thead {
+ text-align: center;
+ height: 48px;
+ line-height: 48px;
+ background-color: #2c3133;
+}
+.umap-table-editor thead tr {
+ border-bottom: 3px solid var(--color-accent);
+}
+.umap-table-editor thead th {
+ border-left: 1px solid #0b0c0c;
+}
+.umap-table-editor .tbody tr input {
+ margin: 0;
+ border-right: none;
+ display: inline;
+}
+.umap-table-editor thead i {
+ display: none;
+ width: 50%;
+ cursor: pointer;
+ padding: 10px 0;
+ height: 24px;
+ line-height: 24px;
+}
+.umap-table-editor thead i:before {
+ width: 40px;
+}
+.umap-table-editor thead th:hover i {
+ display: inline-block;
+}
+.umap-table-editor thead th i:hover {
+ background-color: #33393b;
+}
+.umap-table-editor thead th:hover span {
+ display: none;
+}
+.umap-table-editor td {
+ overflow: hidden;
+}
+.umap-table-editor td {
+ border: 1px solid #222;
+}
+.umap-table-editor td:focus {
+ outline-color: var(--color-accent);
+}
+.umap-table-editor th, .umap-table-editor td {
+ padding: 10px;
+ vertical-align: top;
+}
+.umap-table-editor tr:nth-child(even) {
+ background-color: var(--color-mediumGray);
+}
+.umap-table-editor tr {
+ border-left: 1px solid black;
+ border-right: 1px solid black;
+}
+.umap-table-editor .formbox,
+.umap-table-editor input {
+ margin: 0;
+}
+.umap-table-editor input {
+ border-radius: initial;
+}
diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js
index a5ef3c36..fc4a1af4 100644
--- a/umap/static/umap/js/modules/browser.js
+++ b/umap/static/umap/js/modules/browser.js
@@ -107,6 +107,7 @@ export default class Browser {
this.map.eachBrowsableDataLayer((datalayer) => {
datalayer.resetLayer(true)
this.updateDatalayer(datalayer)
+ if (this.map.fullPanel.isOpen()) datalayer.tableEdit()
})
this.toggleBadge()
}
@@ -149,7 +150,7 @@ export default class Browser {
DomEvent.disableClickPropagation(container)
DomUtil.createTitle(container, translate('Data browser'), 'icon-layers')
- const formContainer = DomUtil.createFieldset(container, L._('Filters'), {
+ this.formContainer = DomUtil.createFieldset(container, L._('Filters'), {
on: this.mode === 'filters',
className: 'filters',
icon: 'icon-filters',
@@ -169,7 +170,7 @@ export default class Browser {
callback: () => this.onFormChange(),
})
let filtersBuilder
- formContainer.appendChild(builder.build())
+ this.formContainer.appendChild(builder.build())
DomEvent.on(builder.form, 'reset', () => {
window.setTimeout(builder.syncAll.bind(builder))
})
@@ -181,12 +182,11 @@ export default class Browser {
DomEvent.on(filtersBuilder.form, 'reset', () => {
window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder))
})
- formContainer.appendChild(filtersBuilder.build())
+ this.formContainer.appendChild(filtersBuilder.build())
}
- const reset = DomUtil.createButton('flat', formContainer, '', () => {
- builder.form.reset()
- if (filtersBuilder) filtersBuilder.form.reset()
- })
+ const reset = DomUtil.createButton('flat', this.formContainer, '', () =>
+ this.resetFilters()
+ )
DomUtil.createIcon(reset, 'icon-restore')
DomUtil.element({
tagName: 'span',
@@ -202,6 +202,12 @@ export default class Browser {
this.update()
}
+ resetFilters() {
+ for (const form of this.formContainer?.querySelectorAll('form') || []) {
+ form.reset()
+ }
+ }
+
static backButton(map) {
const button = DomUtil.createButtonIcon(
DomUtil.create('li', '', undefined),
diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js
index af96433e..492a625d 100644
--- a/umap/static/umap/js/modules/global.js
+++ b/umap/static/umap/js/modules/global.js
@@ -19,6 +19,7 @@ import Slideshow from './slideshow.js'
import { SyncEngine } from './sync/engine.js'
import Dialog from './ui/dialog.js'
import { EditPanel, FullPanel, Panel } from './ui/panel.js'
+import TableEditor from './tableeditor.js'
import Tooltip from './ui/tooltip.js'
import URLs from './urls.js'
import * as Utils from './utils.js'
@@ -55,6 +56,7 @@ window.U = {
Share,
Slideshow,
SyncEngine,
+ TableEditor,
Tooltip,
URLs,
Utils,
diff --git a/umap/static/umap/js/modules/tableeditor.js b/umap/static/umap/js/modules/tableeditor.js
new file mode 100644
index 00000000..df17ad3e
--- /dev/null
+++ b/umap/static/umap/js/modules/tableeditor.js
@@ -0,0 +1,232 @@
+import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
+import { translate } from './i18n.js'
+
+export default class TableEditor {
+ constructor(datalayer) {
+ this.datalayer = datalayer
+ this.table = DomUtil.create('table')
+ this.thead = DomUtil.create('thead', '', this.table)
+ this.header = DomUtil.create('tr', '', this.thead)
+ this.body = DomUtil.create('tbody', '', this.table)
+ this.resetProperties()
+ this.body.addEventListener('dblclick', (event) => {
+ if (event.target.closest('[data-property]')) this.editCell(event.target)
+ })
+ this.body.addEventListener('click', (event) => this.setFocus(event.target))
+ this.body.addEventListener('keydown', (event) => this.onKeyDown(event))
+ }
+
+ renderHeaders() {
+ this.header.innerHTML = '
| '
+ for (let i = 0; i < this.properties.length; i++) {
+ this.renderHeader(this.properties[i])
+ }
+ const checkbox = this.header.querySelector('input[type=checkbox]')
+ checkbox.addEventListener('change', (event) => {
+ if (checkbox.checked) this.checkAll()
+ else this.checkAll(false)
+ })
+ }
+
+ renderHeader(property) {
+ const container = DomUtil.create('th', '', this.header)
+ const title = DomUtil.add('span', '', container, property)
+ const del = DomUtil.create('i', 'umap-delete', container)
+ const rename = DomUtil.create('i', 'umap-edit', container)
+ del.title = translate('Delete this property on all the features')
+ rename.title = translate('Rename this property on all the features')
+ DomEvent.on(del, 'click', () => this.deleteProperty(property))
+ DomEvent.on(rename, 'click', () => this.renameProperty(property))
+ }
+
+ renderBody() {
+ const bounds = this.datalayer.map.getBounds()
+ const inBbox = this.datalayer.map.browser.options.inBbox
+ let html = ''
+ for (const feature of Object.values(this.datalayer._layers)) {
+ if (feature.isFiltered()) continue
+ if (inBbox && !feature.isOnScreen(bounds)) continue
+ html += ` | ${this.properties.map((prop) => `${feature.properties[prop] || ''} | `).join('')}
`
+ }
+ // this.datalayer.eachLayer(this.renderRow, this)
+ // const builder = new U.FormBuilder(feature, this.field_properties, {
+ // id: `umap-feature-properties_${L.stamp(feature)}`,
+ // className: 'trow',
+ // callback: feature.resetTooltip,
+ // })
+ // this.body.appendChild(builder.build())
+ this.body.innerHTML = html
+ }
+
+ compileProperties() {
+ this.resetProperties()
+ if (this.properties.length === 0) this.properties = ['name']
+ // description is a forced textarea, don't edit it in a text input, or you lose cariage returns
+ if (this.properties.indexOf('description') !== -1)
+ this.properties.splice(this.properties.indexOf('description'), 1)
+ this.properties.sort()
+ this.field_properties = []
+ for (let i = 0; i < this.properties.length; i++) {
+ this.field_properties.push([
+ `properties.${this.properties[i]}`,
+ { wrapper: 'td' },
+ ])
+ }
+ }
+
+ resetProperties() {
+ this.properties = this.datalayer._propertiesIndex
+ }
+
+ validateName(name) {
+ if (name.includes('.') !== -1) {
+ U.Alert.error(translate('Invalide property name: {name}', { name: name }))
+ return false
+ }
+ return true
+ }
+
+ renameProperty(property) {
+ this.datalayer.map.dialog
+ .prompt(translate('Please enter the new name of this property'))
+ .then(({ prompt }) => {
+ if (!prompt || !this.validateName(prompt)) return
+ this.datalayer.eachLayer((feature) => {
+ feature.renameProperty(property, prompt)
+ })
+ this.datalayer.deindexProperty(property)
+ this.datalayer.indexProperty(prompt)
+ this.open()
+ })
+ }
+
+ deleteProperty(property) {
+ this.datalayer.map.dialog
+ .confirm(
+ translate('Are you sure you want to delete this property on all the features?')
+ )
+ .then(() => {
+ this.datalayer.eachLayer((feature) => {
+ feature.deleteProperty(property)
+ })
+ this.datalayer.deindexProperty(property)
+ this.resetProperties()
+ this.open()
+ })
+ }
+
+ addProperty() {
+ this.datalayer.map.dialog
+ .prompt(translate('Please enter the name of the property'))
+ .then(({ prompt }) => {
+ if (!prompt || !this.validateName(prompt)) return
+ this.datalayer.indexProperty(prompt)
+ this.edit()
+ })
+ }
+
+ open() {
+ const id = 'tableeditor:edit'
+ this.compileProperties()
+ this.renderHeaders()
+ this.body.innerHTML = ''
+ this.renderBody()
+ const addButton = DomUtil.createButton(
+ 'flat',
+ undefined,
+ translate('Add a new property')
+ )
+ const iconElement = DomUtil.createIcon(addButton, 'icon-add')
+ addButton.insertBefore(iconElement, addButton.firstChild)
+ DomEvent.on(addButton, 'click', this.addProperty, this)
+
+ const template = document.createElement('template')
+ template.innerHTML = `
+ `
+ const deleteButton = template.content.firstElementChild
+ deleteButton.addEventListener('click', () => this.deleteRows())
+
+ this.datalayer.map.fullPanel.open({
+ content: this.table,
+ className: 'umap-table-editor',
+ actions: [addButton, deleteButton],
+ })
+ }
+
+ editCell(cell) {
+ const property = cell.dataset.property
+ const field = `properties.${property}`
+ const feature = this.datalayer.getFeatureById(
+ event.target.parentNode.dataset.feature
+ )
+ const builder = new U.FormBuilder(feature, [field], {
+ id: `umap-feature-properties_${L.stamp(feature)}`,
+ className: 'trow',
+ callback: feature.resetTooltip,
+ })
+ cell.innerHTML = ''
+ cell.appendChild(builder.build())
+ const input = builder.helpers[field].input
+ input.focus()
+ input.addEventListener('blur', () => {
+ cell.innerHTML = feature.properties[property] || ''
+ })
+ }
+
+ onKeyDown(event) {
+ const key = event.key
+ if (key === 'Enter') {
+ const current = this.getFocus()
+ if (current) {
+ this.editCell(current)
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ }
+ }
+
+ checkAll(status = true) {
+ for (const checkbox of this.body.querySelectorAll('input[type=checkbox]')) {
+ checkbox.checked = status
+ }
+ }
+
+ getSelectedRows() {
+ return Array.from(this.body.querySelectorAll('input[type=checkbox]:checked')).map(
+ (checkbox) => checkbox.parentNode.parentNode
+ )
+ }
+
+ getFocus() {
+ return this.body.querySelector(':focus')
+ }
+
+ setFocus(cell) {
+ cell.focus({ focusVisible: true })
+ }
+
+ deleteRows() {
+ const selectedRows = this.getSelectedRows()
+ if (!selectedRows.length) return
+ this.datalayer.map.dialog
+ .confirm(
+ translate('Found {count} rows. Are you sure you want to delete all?', {
+ count: selectedRows.length,
+ })
+ )
+ .then(() => {
+ this.datalayer.hide()
+ for (const row of selectedRows) {
+ const id = row.dataset.feature
+ const feature = this.datalayer.getFeatureById(id)
+ feature.del()
+ }
+ this.datalayer.show()
+ this.datalayer.fire('datachanged')
+ this.renderBody()
+ this.datalayer.map.browser.resetFilters()
+ })
+ }
+}
diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js
index 0eee5b9e..96dda219 100644
--- a/umap/static/umap/js/umap.layer.js
+++ b/umap/static/umap/js/umap.layer.js
@@ -1032,7 +1032,7 @@ U.DataLayer = L.Evented.extend({
this._index.splice(this._index.indexOf(id), 1)
delete this._layers[id]
delete this.map.features_index[feature.getSlug()]
- if (this.hasDataLoaded()) this.fire('datachanged')
+ if (this.hasDataLoaded() && this.isVisible()) this.fire('datachanged')
},
indexProperties: function (feature) {
@@ -1802,7 +1802,7 @@ U.DataLayer = L.Evented.extend({
tableEdit: function () {
if (this.isRemoteLayer() || !this.isVisible()) return
const editor = new U.TableEditor(this)
- editor.edit()
+ editor.open()
},
getFilterKeys: function () {
diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css
index a3df4b8b..4df013c0 100644
--- a/umap/static/umap/map.css
+++ b/umap/static/umap/map.css
@@ -908,64 +908,6 @@ a.umap-control-caption,
padding-left: 31px;
}
-/* ********************************* */
-/* Table Editor */
-/* ********************************* */
-
-.umap-table-editor .table {
- display: table;
- width: 100%;
- white-space: nowrap;
- table-layout: fixed;
-}
-.umap-table-editor .tbody {
- display: table-row-group;
-}
-.umap-table-editor .thead,
-.umap-table-editor .trow {
- display: table-row;
-}
-.umap-table-editor .tcell {
- display: table-cell;
- width: 200px;
-}
-.umap-table-editor .thead {
- text-align: center;
- height: 48px;
- line-height: 48px;
- background-color: #2c3133;
-}
-.umap-table-editor .thead .tcell {
- border-left: 1px solid #0b0c0c;
-}
-.umap-table-editor .tbody .trow input {
- margin: 0;
- border-right: none;
- display: inline;
-}
-.umap-table-editor .tbody .trow + .trow input {
- border-top: none;
-}
-.umap-table-editor .thead i {
- display: none;
- width: 50%;
- cursor: pointer;
- padding: 10px 0;
- height: 24px;
- line-height: 24px;
-}
-.umap-table-editor .thead i:before {
- width: 40px;
-}
-.umap-table-editor .thead .tcell:hover i {
- display: inline-block;
-}
-.umap-table-editor .thead .tcell i:hover {
- background-color: #33393b;
-}
-.umap-table-editor .thead .tcell:hover span {
- display: none;
-}
/* ********************************* */
/* Tilelayer switcher */
diff --git a/umap/templates/umap/css.html b/umap/templates/umap/css.html
index 1fccc8cc..92307fca 100644
--- a/umap/templates/umap/css.html
+++ b/umap/templates/umap/css.html
@@ -34,4 +34,5 @@
+
diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html
index 9659b547..9ae5857b 100644
--- a/umap/templates/umap/js.html
+++ b/umap/templates/umap/js.html
@@ -51,6 +51,5 @@
-