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 @@ -