@@ -210,5 +210,9 @@
+
+
+
+
diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js
index 6677b5f6..d595bab7 100644
--- a/umap/static/umap/js/modules/facets.js
+++ b/umap/static/umap/js/modules/facets.js
@@ -12,8 +12,8 @@ export default class Facets {
const properties = {}
let selected
- names.forEach((name) => {
- const type = defined[name].type
+ for (const name of names) {
+ const type = defined.get(name).type
properties[name] = { type: type }
selected = this.selected[name] || {}
selected.type = type
@@ -22,13 +22,13 @@ export default class Facets {
selected.choices = selected.choices || []
}
this.selected[name] = selected
- })
+ }
this.map.eachBrowsableDataLayer((datalayer) => {
datalayer.eachFeature((feature) => {
- names.forEach((name) => {
+ for (const name of names) {
let value = feature.properties[name]
- const type = defined[name].type
+ const type = defined.get(name).type
const parser = this.getParser(type)
value = parser(value)
switch (type) {
@@ -56,7 +56,7 @@ export default class Facets {
properties[name].choices.push(value)
}
}
- })
+ }
})
})
return properties
@@ -73,7 +73,7 @@ export default class Facets {
build() {
const defined = this.getDefined()
- const names = Object.keys(defined)
+ const names = [...defined.keys()]
const facetProperties = this.compute(names, defined)
const fields = names.map((name) => {
@@ -90,7 +90,7 @@ export default class Facets {
handler = 'FacetSearchDateTime'
break
}
- const label = defined[name].label
+ const label = defined.get(name).label
return [
`selected.${name}`,
{
@@ -107,12 +107,14 @@ export default class Facets {
getDefined() {
const defaultType = 'checkbox'
const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime']
+ const defined = new Map()
+ if (!this.map.options.facetKey) return defined
return (this.map.options.facetKey || '').split(',').reduce((acc, curr) => {
let [name, label, type] = curr.split('|')
type = allowedTypes.includes(type) ? type : defaultType
- acc[name] = { label: label || name, type: type }
+ acc.set(name, { label: label || name, type: type })
return acc
- }, {})
+ }, defined)
}
getParser(type) {
@@ -127,4 +129,32 @@ export default class Facets {
return (v) => String(v || '')
}
}
+
+ dumps(parsed) {
+ const dumped = []
+ for (const [property, { label, type }] of parsed) {
+ dumped.push([property, label, type].filter(Boolean).join('|'))
+ }
+ return dumped.join(',')
+ }
+
+ has(property) {
+ return this.getDefined().has(property)
+ }
+
+ add(property, label, type) {
+ const defined = this.getDefined()
+ if (!defined.has(property)) {
+ defined.set(property, { label, type })
+ this.map.options.facetKey = this.dumps(defined)
+ this.map.isDirty = true
+ }
+ }
+
+ remove(property) {
+ const defined = this.getDefined()
+ defined.delete(property)
+ this.map.options.facetKey = this.dumps(defined)
+ this.map.isDirty = true
+ }
}
diff --git a/umap/static/umap/js/modules/tableeditor.js b/umap/static/umap/js/modules/tableeditor.js
index df17ad3e..90212c84 100644
--- a/umap/static/umap/js/modules/tableeditor.js
+++ b/umap/static/umap/js/modules/tableeditor.js
@@ -1,61 +1,104 @@
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'
-export default class TableEditor {
+const TEMPLATE = `
+
+`
+
+export default class TableEditor extends WithTemplate {
constructor(datalayer) {
+ super()
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.map = this.datalayer.map
+ this.contextmenu = new ContextMenu({ className: 'dark' })
+ this.table = this.loadTemplate(TEMPLATE)
this.resetProperties()
- this.body.addEventListener('dblclick', (event) => {
+ this.elements.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))
+ this.elements.body.addEventListener('click', (event) => this.setFocus(event.target))
+ this.elements.body.addEventListener('keydown', (event) => this.onKeyDown(event))
+ this.elements.header.addEventListener('click', (event) => {
+ const property = event.target.dataset.property
+ if (property) this.openHeaderMenu(property)
+ })
+ }
+
+ openHeaderMenu(property) {
+ let filterItem
+ if (this.map.facets.has(property)) {
+ filterItem = {
+ label: translate('Remove filter for this property'),
+ action: () => {
+ this.map.facets.remove(property)
+ this.map.browser.open('filters')
+ },
+ }
+ } else {
+ filterItem = {
+ label: translate('Add filter for this property'),
+ action: () => {
+ this.map.facets.add(property)
+ this.map.browser.open('filters')
+ },
+ }
+ }
+ this.contextmenu.open(
+ [event.clientX, event.clientY],
+ [
+ {
+ label: translate('Delete this property on all the features'),
+ action: () => this.deleteProperty(property),
+ },
+ {
+ label: translate('Rename this property on all the features'),
+ action: () => this.renameProperty(property),
+ },
+ filterItem,
+ ]
+ )
}
renderHeaders() {
- this.header.innerHTML = ' | '
- for (let i = 0; i < this.properties.length; i++) {
- this.renderHeader(this.properties[i])
+ this.elements.header.innerHTML = ''
+ const th = loadTemplate(' | ')
+ const checkbox = th.firstChild
+ this.elements.header.appendChild(th)
+ for (const property of this.properties) {
+ this.elements.header.appendChild(
+ loadTemplate(
+ `${property} | `
+ )
+ )
}
- 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
+ const bounds = this.map.getBounds()
+ const inBbox = this.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('')}
`
+ const tds = this.properties.map(
+ (prop) =>
+ `${feature.properties[prop] || ''} | `
+ )
+ html += ` | ${tds.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
+ this.elements.body.innerHTML = html
}
compileProperties() {
@@ -87,7 +130,7 @@ export default class TableEditor {
}
renameProperty(property) {
- this.datalayer.map.dialog
+ this.map.dialog
.prompt(translate('Please enter the new name of this property'))
.then(({ prompt }) => {
if (!prompt || !this.validateName(prompt)) return
@@ -101,7 +144,7 @@ export default class TableEditor {
}
deleteProperty(property) {
- this.datalayer.map.dialog
+ this.map.dialog
.confirm(
translate('Are you sure you want to delete this property on all the features?')
)
@@ -116,7 +159,7 @@ export default class TableEditor {
}
addProperty() {
- this.datalayer.map.dialog
+ this.map.dialog
.prompt(translate('Please enter the name of the property'))
.then(({ prompt }) => {
if (!prompt || !this.validateName(prompt)) return
@@ -129,29 +172,31 @@ export default class TableEditor {
const id = 'tableeditor:edit'
this.compileProperties()
this.renderHeaders()
- this.body.innerHTML = ''
+ this.elements.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 addButton = loadTemplate(`
+ `)
+ addButton.addEventListener('click', () => this.addProperty())
+
+ const deleteButton = loadTemplate(`
`
- const deleteButton = template.content.firstElementChild
+ `)
deleteButton.addEventListener('click', () => this.deleteRows())
- this.datalayer.map.fullPanel.open({
+ const filterButton = loadTemplate(`
+ `)
+ filterButton.addEventListener('click', () => this.map.browser.open('filters'))
+
+ this.map.fullPanel.open({
content: this.table,
className: 'umap-table-editor',
- actions: [addButton, deleteButton],
+ actions: [addButton, deleteButton, filterButton],
})
}
@@ -188,19 +233,21 @@ export default class TableEditor {
}
checkAll(status = true) {
- for (const checkbox of this.body.querySelectorAll('input[type=checkbox]')) {
+ for (const checkbox of this.elements.body.querySelectorAll(
+ 'input[type=checkbox]'
+ )) {
checkbox.checked = status
}
}
getSelectedRows() {
- return Array.from(this.body.querySelectorAll('input[type=checkbox]:checked')).map(
- (checkbox) => checkbox.parentNode.parentNode
- )
+ return Array.from(
+ this.elements.body.querySelectorAll('input[type=checkbox]:checked')
+ ).map((checkbox) => checkbox.parentNode.parentNode)
}
getFocus() {
- return this.body.querySelector(':focus')
+ return this.elements.body.querySelector(':focus')
}
setFocus(cell) {
@@ -210,7 +257,7 @@ export default class TableEditor {
deleteRows() {
const selectedRows = this.getSelectedRows()
if (!selectedRows.length) return
- this.datalayer.map.dialog
+ this.map.dialog
.confirm(
translate('Found {count} rows. Are you sure you want to delete all?', {
count: selectedRows.length,
@@ -226,7 +273,8 @@ export default class TableEditor {
this.datalayer.show()
this.datalayer.fire('datachanged')
this.renderBody()
- this.datalayer.map.browser.resetFilters()
+ this.map.browser.resetFilters()
+ this.map.browser.open('filters')
})
}
}
diff --git a/umap/static/umap/js/modules/ui/base.js b/umap/static/umap/js/modules/ui/base.js
new file mode 100644
index 00000000..4f0c176c
--- /dev/null
+++ b/umap/static/umap/js/modules/ui/base.js
@@ -0,0 +1,77 @@
+export class Positioned {
+ openAt({ anchor, position }) {
+ if (anchor && position === 'top') {
+ this.anchorTop(anchor)
+ } else if (anchor && position === 'left') {
+ this.anchorLeft(anchor)
+ } else if (anchor && position === 'bottom') {
+ this.anchorBottom(anchor)
+ } else {
+ this.anchorAbsolute()
+ }
+ }
+
+ anchorAbsolute() {
+ this.container.className = ''
+ const left =
+ this.parent.offsetLeft +
+ this.parent.clientWidth / 2 -
+ this.container.clientWidth / 2
+ const top = this.parent.offsetTop + 75
+ this.setPosition({ top: top, left: left })
+ }
+
+ anchorTop(el) {
+ this.container.className = 'tooltip-top'
+ const coords = this.getPosition(el)
+ this.setPosition({
+ left: coords.left - 10,
+ bottom: this.getDocHeight() - coords.top + 11,
+ })
+ }
+
+ anchorBottom(el) {
+ this.container.className = 'tooltip-bottom'
+ const coords = this.getPosition(el)
+ this.setPosition({
+ left: coords.left,
+ top: coords.bottom + 11,
+ })
+ }
+
+ anchorLeft(el) {
+ this.container.className = 'tooltip-left'
+ const coords = this.getPosition(el)
+ this.setPosition({
+ top: coords.top,
+ right: document.documentElement.offsetWidth - coords.left + 11,
+ })
+ }
+
+ getPosition(el) {
+ return el.getBoundingClientRect()
+ }
+
+ setPosition(coords) {
+ if (coords.left) this.container.style.left = `${coords.left}px`
+ else this.container.style.left = 'initial'
+ if (coords.right) this.container.style.right = `${coords.right}px`
+ else this.container.style.right = 'initial'
+ if (coords.top) this.container.style.top = `${coords.top}px`
+ else this.container.style.top = 'initial'
+ if (coords.bottom) this.container.style.bottom = `${coords.bottom}px`
+ else this.container.style.bottom = 'initial'
+ }
+
+ getDocHeight() {
+ const D = document
+ return Math.max(
+ D.body.scrollHeight,
+ D.documentElement.scrollHeight,
+ D.body.offsetHeight,
+ D.documentElement.offsetHeight,
+ D.body.clientHeight,
+ D.documentElement.clientHeight
+ )
+ }
+}
diff --git a/umap/static/umap/js/modules/ui/contextmenu.js b/umap/static/umap/js/modules/ui/contextmenu.js
new file mode 100644
index 00000000..d47a0249
--- /dev/null
+++ b/umap/static/umap/js/modules/ui/contextmenu.js
@@ -0,0 +1,43 @@
+import { loadTemplate } from '../utils.js'
+
+export default class ContextMenu {
+ constructor(options = {}) {
+ this.options = options
+ this.container = document.createElement('ul')
+ this.container.className = 'umap-contextmenu'
+ if (options.className) {
+ this.container.classList.add(options.className)
+ }
+ this.container.addEventListener('focusout', (event) => {
+ if (!this.container.contains(event.relatedTarget)) this.close()
+ })
+ }
+
+ open([x, y], items) {
+ this.container.innerHTML = ''
+ for (const item of items) {
+ const li = loadTemplate(
+ ``
+ )
+ li.addEventListener('click', () => {
+ this.close()
+ item.action()
+ })
+ this.container.appendChild(li)
+ }
+ document.body.appendChild(this.container)
+ this.container.style.top = `${y}px`
+ this.container.style.left = `${x}px`
+ this.container.querySelector('button').focus()
+ this.container.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape') {
+ event.stopPropagation()
+ this.close()
+ }
+ })
+ }
+
+ close() {
+ this.container.parentNode.removeChild(this.container)
+ }
+}
diff --git a/umap/static/umap/js/modules/ui/panel.js b/umap/static/umap/js/modules/ui/panel.js
index f84a92b3..6fa77e60 100644
--- a/umap/static/umap/js/modules/ui/panel.js
+++ b/umap/static/umap/js/modules/ui/panel.js
@@ -9,7 +9,7 @@ export class Panel {
// This will be set once according to the panel configurated at load
// or by using panels as popups
this.mode = null
- this.classname = 'left'
+ this.className = 'left'
DomEvent.disableClickPropagation(this.container)
DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu.
DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation)
@@ -25,9 +25,10 @@ export class Panel {
}
open({ content, className, actions = [] } = {}) {
- this.container.className = `with-transition panel window ${this.classname} ${
+ this.container.className = `with-transition panel window ${this.className} ${
this.mode || ''
}`
+ document.body.classList.add(`panel-${this.className.split(' ')[0]}-on`)
this.container.innerHTML = ''
const actionsContainer = DomUtil.create('ul', 'buttons', this.container)
const body = DomUtil.create('div', 'body', this.container)
@@ -69,6 +70,7 @@ export class Panel {
}
close() {
+ document.body.classList.remove(`panel-${this.className.split(' ')[0]}-on`)
if (DomUtil.hasClass(this.container, 'on')) {
DomUtil.removeClass(this.container, 'on')
this.map.invalidateSize({ pan: false })
@@ -80,14 +82,14 @@ export class Panel {
export class EditPanel extends Panel {
constructor(map) {
super(map)
- this.classname = 'right dark'
+ this.className = 'right dark'
}
}
export class FullPanel extends Panel {
constructor(map) {
super(map)
- this.classname = 'full dark'
+ this.className = 'full dark'
this.mode = 'expanded'
}
}
diff --git a/umap/static/umap/js/modules/ui/tooltip.js b/umap/static/umap/js/modules/ui/tooltip.js
index feed1b5e..88a859f9 100644
--- a/umap/static/umap/js/modules/ui/tooltip.js
+++ b/umap/static/umap/js/modules/ui/tooltip.js
@@ -1,8 +1,10 @@
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
+import { Positioned } from './base.js'
-export default class Tooltip {
+export default class Tooltip extends Positioned {
constructor(parent) {
+ super()
this.parent = parent
this.container = DomUtil.create('div', 'with-transition', this.parent)
this.container.id = 'umap-tooltip-container'
@@ -13,16 +15,8 @@ export default class Tooltip {
}
open(opts) {
- function showIt() {
- if (opts.anchor && opts.position === 'top') {
- this.anchorTop(opts.anchor)
- } else if (opts.anchor && opts.position === 'left') {
- this.anchorLeft(opts.anchor)
- } else if (opts.anchor && opts.position === 'bottom') {
- this.anchorBottom(opts.anchor)
- } else {
- this.anchorAbsolute()
- }
+ const showIt = () => {
+ this.openAt(opts)
L.DomUtil.addClass(this.parent, 'umap-tooltip')
this.container.innerHTML = U.Utils.escapeHTML(opts.content)
}
@@ -39,43 +33,6 @@ export default class Tooltip {
}
}
- anchorAbsolute() {
- this.container.className = ''
- const left =
- this.parent.offsetLeft +
- this.parent.clientWidth / 2 -
- this.container.clientWidth / 2
- const top = this.parent.offsetTop + 75
- this.setPosition({ top: top, left: left })
- }
-
- anchorTop(el) {
- this.container.className = 'tooltip-top'
- const coords = this.getPosition(el)
- this.setPosition({
- left: coords.left - 10,
- bottom: this.getDocHeight() - coords.top + 11,
- })
- }
-
- anchorBottom(el) {
- this.container.className = 'tooltip-bottom'
- const coords = this.getPosition(el)
- this.setPosition({
- left: coords.left,
- top: coords.bottom + 11,
- })
- }
-
- anchorLeft(el) {
- this.container.className = 'tooltip-left'
- const coords = this.getPosition(el)
- this.setPosition({
- top: coords.top,
- right: document.documentElement.offsetWidth - coords.left + 11,
- })
- }
-
close(id) {
// Clear timetout even if a new tooltip has been added
// in the meantime. Eg. after a mouseout from the anchor.
@@ -86,31 +43,4 @@ export default class Tooltip {
this.setPosition({})
L.DomUtil.removeClass(this.parent, 'umap-tooltip')
}
-
- getPosition(el) {
- return el.getBoundingClientRect()
- }
-
- setPosition(coords) {
- if (coords.left) this.container.style.left = `${coords.left}px`
- else this.container.style.left = 'initial'
- if (coords.right) this.container.style.right = `${coords.right}px`
- else this.container.style.right = 'initial'
- if (coords.top) this.container.style.top = `${coords.top}px`
- else this.container.style.top = 'initial'
- if (coords.bottom) this.container.style.bottom = `${coords.bottom}px`
- else this.container.style.bottom = 'initial'
- }
-
- getDocHeight() {
- const D = document
- return Math.max(
- D.body.scrollHeight,
- D.documentElement.scrollHeight,
- D.body.offsetHeight,
- D.documentElement.offsetHeight,
- D.body.clientHeight,
- D.documentElement.clientHeight
- )
- }
}
diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js
index c1427fd0..9b7b11f1 100644
--- a/umap/static/umap/js/modules/utils.js
+++ b/umap/static/umap/js/modules/utils.js
@@ -378,11 +378,18 @@ export function toggleBadge(element, value) {
else delete element.dataset.badge
}
+export function loadTemplate(html) {
+ const template = document.createElement('template')
+ template.innerHTML = html
+ .split('\n')
+ .map((line) => line.trim())
+ .join('')
+ return template.content.firstElementChild
+}
+
export class WithTemplate {
loadTemplate(html) {
- const template = document.createElement('template')
- template.innerHTML = html.split('\n').map((line) => line.trim()).join('')
- this.element = template.content.firstElementChild
+ this.element = loadTemplate(html)
this.elements = {}
for (const element of this.element.querySelectorAll('[data-ref]')) {
this.elements[element.dataset.ref] = element
diff --git a/umap/static/umap/vars.css b/umap/static/umap/vars.css
index 9e6a88b0..b8d3dc25 100644
--- a/umap/static/umap/vars.css
+++ b/umap/static/umap/vars.css
@@ -44,9 +44,13 @@
--zindex-toolbar: 480;
--zindex-autocomplete: 470;
--zindex-dialog: 460;
+ --zindex-contextmenu: 455;
--zindex-icon-active: 450;
+ --zindex-tooltip: 445;
--zindex-panels: 440;
--zindex-dragover: 410;
+
+ --block-shadow: 0 1px 7px var(--color-mediumGray);
}
.dark {
--background-color: var(--color-darkGray);
diff --git a/umap/templates/umap/css.html b/umap/templates/umap/css.html
index 92307fca..f488b93e 100644
--- a/umap/templates/umap/css.html
+++ b/umap/templates/umap/css.html
@@ -29,6 +29,7 @@
+