umap-project 2.4.1__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  3. umap/locale/el/LC_MESSAGES/django.po +145 -90
  4. umap/locale/en/LC_MESSAGES/django.po +13 -13
  5. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/eu/LC_MESSAGES/django.po +145 -89
  7. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/hu/LC_MESSAGES/django.po +100 -50
  9. umap/static/umap/base.css +5 -2
  10. umap/static/umap/content.css +2 -2
  11. umap/static/umap/css/contextmenu.css +11 -0
  12. umap/static/umap/css/dialog.css +25 -4
  13. umap/static/umap/css/importers.css +2 -0
  14. umap/static/umap/css/panel.css +6 -4
  15. umap/static/umap/css/slideshow.css +69 -0
  16. umap/static/umap/css/tableeditor.css +69 -0
  17. umap/static/umap/css/tooltip.css +3 -3
  18. umap/static/umap/img/16-white.svg +4 -0
  19. umap/static/umap/img/source/16-white.svg +5 -1
  20. umap/static/umap/js/components/alerts/alert.css +11 -11
  21. umap/static/umap/js/components/alerts/alert.js +1 -1
  22. umap/static/umap/js/modules/autocomplete.js +27 -5
  23. umap/static/umap/js/modules/browser.js +20 -14
  24. umap/static/umap/js/modules/caption.js +4 -4
  25. umap/static/umap/js/modules/dompurify.js +2 -3
  26. umap/static/umap/js/modules/facets.js +53 -17
  27. umap/static/umap/js/modules/formatter.js +153 -0
  28. umap/static/umap/js/modules/global.js +25 -16
  29. umap/static/umap/js/modules/help.js +26 -26
  30. umap/static/umap/js/modules/importer.js +10 -10
  31. umap/static/umap/js/modules/importers/communesfr.js +3 -1
  32. umap/static/umap/js/modules/importers/datasets.js +8 -6
  33. umap/static/umap/js/modules/importers/geodatamine.js +14 -14
  34. umap/static/umap/js/modules/importers/overpass.js +19 -15
  35. umap/static/umap/js/modules/orderable.js +2 -2
  36. umap/static/umap/js/modules/request.js +1 -1
  37. umap/static/umap/js/modules/rules.js +26 -11
  38. umap/static/umap/js/modules/schema.js +16 -12
  39. umap/static/umap/js/{umap.share.js → modules/share.js} +58 -103
  40. umap/static/umap/js/modules/slideshow.js +141 -0
  41. umap/static/umap/js/modules/sync/engine.js +3 -3
  42. umap/static/umap/js/modules/sync/updaters.js +10 -11
  43. umap/static/umap/js/modules/sync/websocket.js +1 -1
  44. umap/static/umap/js/modules/tableeditor.js +329 -0
  45. umap/static/umap/js/modules/ui/base.js +93 -0
  46. umap/static/umap/js/modules/ui/contextmenu.js +50 -0
  47. umap/static/umap/js/modules/ui/dialog.js +169 -31
  48. umap/static/umap/js/modules/ui/panel.js +7 -5
  49. umap/static/umap/js/modules/ui/tooltip.js +7 -77
  50. umap/static/umap/js/modules/urls.js +1 -2
  51. umap/static/umap/js/modules/utils.js +36 -16
  52. umap/static/umap/js/umap.controls.js +27 -29
  53. umap/static/umap/js/umap.core.js +19 -15
  54. umap/static/umap/js/umap.datalayer.permissions.js +15 -18
  55. umap/static/umap/js/umap.features.js +113 -131
  56. umap/static/umap/js/umap.forms.js +203 -228
  57. umap/static/umap/js/umap.icon.js +17 -22
  58. umap/static/umap/js/umap.js +117 -107
  59. umap/static/umap/js/umap.layer.js +374 -324
  60. umap/static/umap/js/umap.permissions.js +7 -10
  61. umap/static/umap/js/umap.popup.js +20 -20
  62. umap/static/umap/locale/am_ET.js +22 -5
  63. umap/static/umap/locale/am_ET.json +22 -5
  64. umap/static/umap/locale/ar.js +22 -5
  65. umap/static/umap/locale/ar.json +22 -5
  66. umap/static/umap/locale/ast.js +22 -5
  67. umap/static/umap/locale/ast.json +22 -5
  68. umap/static/umap/locale/bg.js +22 -5
  69. umap/static/umap/locale/bg.json +22 -5
  70. umap/static/umap/locale/br.js +22 -5
  71. umap/static/umap/locale/br.json +22 -5
  72. umap/static/umap/locale/ca.js +56 -39
  73. umap/static/umap/locale/ca.json +56 -39
  74. umap/static/umap/locale/cs_CZ.js +22 -5
  75. umap/static/umap/locale/cs_CZ.json +22 -5
  76. umap/static/umap/locale/da.js +22 -5
  77. umap/static/umap/locale/da.json +22 -5
  78. umap/static/umap/locale/de.js +22 -5
  79. umap/static/umap/locale/de.json +22 -5
  80. umap/static/umap/locale/el.js +27 -10
  81. umap/static/umap/locale/el.json +27 -10
  82. umap/static/umap/locale/en.js +22 -6
  83. umap/static/umap/locale/en.json +22 -6
  84. umap/static/umap/locale/en_US.json +22 -5
  85. umap/static/umap/locale/es.js +22 -6
  86. umap/static/umap/locale/es.json +22 -6
  87. umap/static/umap/locale/et.js +22 -5
  88. umap/static/umap/locale/et.json +22 -5
  89. umap/static/umap/locale/eu.js +167 -150
  90. umap/static/umap/locale/eu.json +167 -150
  91. umap/static/umap/locale/fa_IR.js +22 -5
  92. umap/static/umap/locale/fa_IR.json +22 -5
  93. umap/static/umap/locale/fi.js +22 -5
  94. umap/static/umap/locale/fi.json +22 -5
  95. umap/static/umap/locale/fr.js +22 -6
  96. umap/static/umap/locale/fr.json +22 -6
  97. umap/static/umap/locale/gl.js +22 -5
  98. umap/static/umap/locale/gl.json +22 -5
  99. umap/static/umap/locale/he.js +22 -5
  100. umap/static/umap/locale/he.json +22 -5
  101. umap/static/umap/locale/hr.js +22 -5
  102. umap/static/umap/locale/hr.json +22 -5
  103. umap/static/umap/locale/hu.js +89 -72
  104. umap/static/umap/locale/hu.json +89 -72
  105. umap/static/umap/locale/id.js +22 -5
  106. umap/static/umap/locale/id.json +22 -5
  107. umap/static/umap/locale/is.js +22 -5
  108. umap/static/umap/locale/is.json +22 -5
  109. umap/static/umap/locale/it.js +22 -5
  110. umap/static/umap/locale/it.json +22 -5
  111. umap/static/umap/locale/ja.js +22 -5
  112. umap/static/umap/locale/ja.json +22 -5
  113. umap/static/umap/locale/ko.js +22 -5
  114. umap/static/umap/locale/ko.json +22 -5
  115. umap/static/umap/locale/lt.js +22 -5
  116. umap/static/umap/locale/lt.json +22 -5
  117. umap/static/umap/locale/ms.js +22 -5
  118. umap/static/umap/locale/ms.json +22 -5
  119. umap/static/umap/locale/nl.js +22 -5
  120. umap/static/umap/locale/nl.json +22 -5
  121. umap/static/umap/locale/no.js +22 -5
  122. umap/static/umap/locale/no.json +22 -5
  123. umap/static/umap/locale/pl.js +22 -5
  124. umap/static/umap/locale/pl.json +22 -5
  125. umap/static/umap/locale/pl_PL.json +22 -5
  126. umap/static/umap/locale/pt.js +22 -6
  127. umap/static/umap/locale/pt.json +22 -6
  128. umap/static/umap/locale/pt_BR.js +22 -5
  129. umap/static/umap/locale/pt_BR.json +22 -5
  130. umap/static/umap/locale/pt_PT.js +22 -5
  131. umap/static/umap/locale/pt_PT.json +22 -5
  132. umap/static/umap/locale/ro.js +22 -5
  133. umap/static/umap/locale/ro.json +22 -5
  134. umap/static/umap/locale/ru.js +22 -5
  135. umap/static/umap/locale/ru.json +22 -5
  136. umap/static/umap/locale/sk_SK.js +22 -5
  137. umap/static/umap/locale/sk_SK.json +22 -5
  138. umap/static/umap/locale/sl.js +22 -5
  139. umap/static/umap/locale/sl.json +22 -5
  140. umap/static/umap/locale/sr.js +22 -5
  141. umap/static/umap/locale/sr.json +22 -5
  142. umap/static/umap/locale/sv.js +22 -5
  143. umap/static/umap/locale/sv.json +22 -5
  144. umap/static/umap/locale/th_TH.js +22 -5
  145. umap/static/umap/locale/th_TH.json +22 -5
  146. umap/static/umap/locale/tr.js +22 -5
  147. umap/static/umap/locale/tr.json +22 -5
  148. umap/static/umap/locale/uk_UA.js +22 -5
  149. umap/static/umap/locale/uk_UA.json +22 -5
  150. umap/static/umap/locale/vi.js +22 -5
  151. umap/static/umap/locale/vi.json +22 -5
  152. umap/static/umap/locale/vi_VN.json +22 -5
  153. umap/static/umap/locale/zh.js +22 -5
  154. umap/static/umap/locale/zh.json +22 -5
  155. umap/static/umap/locale/zh_CN.json +22 -5
  156. umap/static/umap/locale/zh_TW.Big5.json +22 -5
  157. umap/static/umap/locale/zh_TW.js +22 -5
  158. umap/static/umap/locale/zh_TW.json +22 -5
  159. umap/static/umap/map.css +9 -153
  160. umap/static/umap/vars.css +15 -0
  161. umap/static/umap/vendors/dompurify/purify.es.js +5 -59
  162. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  163. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +410 -428
  164. umap/static/umap/vendors/geojson-to-gpx/index.js +155 -0
  165. umap/static/umap/vendors/osmtogeojson/osmtogeojson.js +1 -2
  166. umap/static/umap/vendors/togeojson/togeojson.es.js +1109 -0
  167. umap/static/umap/vendors/togeojson/{togeojson.umd.js.map → togeojson.es.mjs.map} +1 -1
  168. umap/static/umap/vendors/tokml/tokml.es.js +895 -0
  169. umap/static/umap/vendors/tokml/tokml.es.mjs.map +1 -0
  170. umap/storage.py +6 -2
  171. umap/templates/umap/components/alerts/alert.html +3 -3
  172. umap/templates/umap/css.html +3 -0
  173. umap/templates/umap/js.html +0 -6
  174. umap/tests/fixtures/categorized_highway.geojson +1 -0
  175. umap/tests/fixtures/test_import_osm_relation.json +130 -0
  176. umap/tests/integration/conftest.py +8 -1
  177. umap/tests/integration/test_browser.py +3 -2
  178. umap/tests/integration/test_categorized_layer.py +141 -0
  179. umap/tests/integration/test_conditional_rules.py +21 -0
  180. umap/tests/integration/test_datalayer.py +9 -4
  181. umap/tests/integration/test_edit_datalayer.py +1 -0
  182. umap/tests/integration/test_edit_polygon.py +1 -1
  183. umap/tests/integration/test_export_map.py +2 -3
  184. umap/tests/integration/test_import.py +22 -0
  185. umap/tests/integration/test_map_preview.py +36 -2
  186. umap/tests/integration/test_tableeditor.py +158 -4
  187. umap/tests/integration/test_websocket_sync.py +2 -2
  188. umap/tests/test_views.py +2 -2
  189. umap/views.py +3 -2
  190. {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/METADATA +8 -8
  191. {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/RECORD +194 -184
  192. umap/static/umap/js/umap.slideshow.js +0 -165
  193. umap/static/umap/js/umap.tableeditor.js +0 -118
  194. umap/static/umap/vendors/togeojson/togeojson.umd.js +0 -2
  195. umap/static/umap/vendors/togpx/togpx.js +0 -547
  196. umap/static/umap/vendors/tokml/tokml.js +0 -343
  197. {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/WHEEL +0 -0
  198. {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/entry_points.txt +0 -0
  199. {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,329 @@
1
+ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
2
+ import { translate } from './i18n.js'
3
+ import ContextMenu from './ui/contextmenu.js'
4
+ import { WithTemplate, loadTemplate } from './utils.js'
5
+
6
+ const TEMPLATE = `
7
+ <table>
8
+ <thead>
9
+ <tr data-ref="header"></tr>
10
+ </thead>
11
+ <tbody data-ref="body">
12
+ </tbody>
13
+ </table>
14
+ `
15
+
16
+ export default class TableEditor extends WithTemplate {
17
+ constructor(datalayer) {
18
+ super()
19
+ this.datalayer = datalayer
20
+ this.map = this.datalayer.map
21
+ this.contextmenu = new ContextMenu({ className: 'dark' })
22
+ this.table = this.loadTemplate(TEMPLATE)
23
+ if (!this.datalayer.isRemoteLayer()) {
24
+ this.elements.body.addEventListener('dblclick', (event) => {
25
+ if (event.target.closest('[data-property]')) this.editCell(event.target)
26
+ })
27
+ }
28
+ this.elements.body.addEventListener('click', (event) => this.setFocus(event.target))
29
+ this.elements.body.addEventListener('keydown', (event) => this.onKeyDown(event))
30
+ this.elements.header.addEventListener('click', (event) => {
31
+ const property = event.target.dataset.property
32
+ if (property) this.openHeaderMenu(property)
33
+ })
34
+ }
35
+
36
+ openHeaderMenu(property) {
37
+ const actions = []
38
+ let filterItem
39
+ if (this.map.facets.has(property)) {
40
+ filterItem = {
41
+ label: translate('Remove filter for this column'),
42
+ action: () => {
43
+ this.map.facets.remove(property)
44
+ this.map.browser.open('filters')
45
+ },
46
+ }
47
+ } else {
48
+ filterItem = {
49
+ label: translate('Add filter for this column'),
50
+ action: () => {
51
+ this.map.facets.add(property)
52
+ this.map.browser.open('filters')
53
+ },
54
+ }
55
+ }
56
+ actions.push(filterItem)
57
+ if (!this.datalayer.isRemoteLayer()) {
58
+ actions.push({
59
+ label: translate('Rename this column'),
60
+ action: () => this.renameProperty(property),
61
+ })
62
+ actions.push({
63
+ label: translate('Delete this column'),
64
+ action: () => this.deleteProperty(property),
65
+ })
66
+ }
67
+ this.contextmenu.open([event.clientX, event.clientY], actions)
68
+ }
69
+
70
+ renderHeaders() {
71
+ this.elements.header.innerHTML = ''
72
+ const th = loadTemplate('<th><input type="checkbox" /></th>')
73
+ const checkbox = th.firstChild
74
+ this.elements.header.appendChild(th)
75
+ for (const property of this.properties) {
76
+ this.elements.header.appendChild(
77
+ loadTemplate(
78
+ `<th>${property}<button data-property="${property}" class="flat" aria-label="${translate('Advanced actions')}">…</button></th>`
79
+ )
80
+ )
81
+ }
82
+ checkbox.addEventListener('change', (event) => {
83
+ if (checkbox.checked) this.checkAll()
84
+ else this.checkAll(false)
85
+ })
86
+ }
87
+
88
+ renderBody() {
89
+ const bounds = this.map.getBounds()
90
+ const inBbox = this.map.browser.options.inBbox
91
+ let html = ''
92
+ for (const feature of Object.values(this.datalayer._layers)) {
93
+ if (feature.isFiltered()) continue
94
+ if (inBbox && !feature.isOnScreen(bounds)) continue
95
+ const tds = this.properties.map(
96
+ (prop) =>
97
+ `<td tabindex="0" data-property="${prop}">${feature.properties[prop] || ''}</td>`
98
+ )
99
+ html += `<tr data-feature="${feature.id}"><th><input type="checkbox" /></th>${tds.join('')}</tr>`
100
+ }
101
+ this.elements.body.innerHTML = html
102
+ }
103
+
104
+ resetProperties() {
105
+ this.properties = this.datalayer._propertiesIndex
106
+ if (this.properties.length === 0) {
107
+ this.properties = ['name', 'description']
108
+ }
109
+ }
110
+
111
+ validateName(name) {
112
+ if (name.includes('.')) {
113
+ U.Alert.error(translate('Name “{name}” should not contain a dot.', { name }))
114
+ return false
115
+ }
116
+ if (this.properties.includes(name)) {
117
+ U.Alert.error(translate('This name already exists: “{name}”', { name }))
118
+ return false
119
+ }
120
+ return true
121
+ }
122
+
123
+ renameProperty(property) {
124
+ this.map.dialog
125
+ .prompt(translate('Please enter the new name of this property'))
126
+ .then(({ prompt }) => {
127
+ if (!prompt || !this.validateName(prompt)) return
128
+ this.datalayer.eachLayer((feature) => {
129
+ feature.renameProperty(property, prompt)
130
+ })
131
+ this.datalayer.deindexProperty(property)
132
+ this.datalayer.indexProperty(prompt)
133
+ this.open()
134
+ })
135
+ }
136
+
137
+ deleteProperty(property) {
138
+ this.map.dialog
139
+ .confirm(
140
+ translate('Are you sure you want to delete this property on all the features?')
141
+ )
142
+ .then(() => {
143
+ this.datalayer.eachLayer((feature) => {
144
+ feature.deleteProperty(property)
145
+ })
146
+ this.datalayer.deindexProperty(property)
147
+ this.resetProperties()
148
+ this.open()
149
+ })
150
+ }
151
+
152
+ addProperty() {
153
+ this.map.dialog
154
+ .prompt(translate('Please enter the name of the property'))
155
+ .then(({ prompt }) => {
156
+ if (!prompt || !this.validateName(prompt)) return
157
+ this.datalayer.indexProperty(prompt)
158
+ this.open()
159
+ })
160
+ }
161
+
162
+ open() {
163
+ const id = 'tableeditor:edit'
164
+ this.resetProperties()
165
+ this.renderHeaders()
166
+ this.elements.body.innerHTML = ''
167
+ this.renderBody()
168
+
169
+ const actions = []
170
+ if (!this.datalayer.isRemoteLayer()) {
171
+ const addButton = loadTemplate(`
172
+ <button class="flat" type="button" data-ref="add">
173
+ <i class="icon icon-16 icon-add"></i>${translate('Add a new property')}
174
+ </button>`)
175
+ addButton.addEventListener('click', () => this.addProperty())
176
+ actions.push(addButton)
177
+
178
+ const deleteButton = loadTemplate(`
179
+ <button class="flat" type="button" data-ref="delete">
180
+ <i class="icon icon-16 icon-delete"></i>${translate('Delete selected rows')}
181
+ </button>`)
182
+ deleteButton.addEventListener('click', () => this.deleteRows())
183
+ actions.push(deleteButton)
184
+ }
185
+
186
+ const filterButton = loadTemplate(`
187
+ <button class="flat" type="button" data-ref="filters">
188
+ <i class="icon icon-16 icon-filters"></i>${translate('Filter data')}
189
+ </button>`)
190
+ filterButton.addEventListener('click', () => this.map.browser.open('filters'))
191
+ actions.push(filterButton)
192
+
193
+ this.map.fullPanel.open({
194
+ content: this.table,
195
+ className: 'umap-table-editor',
196
+ actions: actions,
197
+ })
198
+ }
199
+
200
+ editCell(cell) {
201
+ if (this.datalayer.isRemoteLayer()) return
202
+ const property = cell.dataset.property
203
+ const field = `properties.${property}`
204
+ const tr = event.target.closest('tr')
205
+ const feature = this.datalayer.getFeatureById(tr.dataset.feature)
206
+ const handler = property === 'description' ? 'Textarea' : 'Input'
207
+ const builder = new U.FormBuilder(feature, [[field, { handler }]], {
208
+ id: `umap-feature-properties_${L.stamp(feature)}`,
209
+ })
210
+ cell.innerHTML = ''
211
+ cell.appendChild(builder.build())
212
+ const input = builder.helpers[field].input
213
+ input.focus()
214
+ input.addEventListener('blur', () => {
215
+ cell.innerHTML = feature.properties[property] || ''
216
+ cell.focus()
217
+ })
218
+ input.addEventListener('keydown', (event) => {
219
+ if (event.key === 'Escape') {
220
+ builder.restoreField(field)
221
+ cell.innerHTML = feature.properties[property] || ''
222
+ cell.focus()
223
+ event.stopPropagation()
224
+ }
225
+ })
226
+ }
227
+
228
+ onKeyDown(event) {
229
+ // Only on data <td>, not inputs or anything else
230
+ if (!event.target.dataset.property) return
231
+ const key = event.key
232
+ const actions = {
233
+ Enter: () => this.editCurrent(),
234
+ ArrowRight: () => this.moveRight(),
235
+ ArrowLeft: () => this.moveLeft(),
236
+ ArrowUp: () => this.moveUp(),
237
+ ArrowDown: () => this.moveDown(),
238
+ }
239
+ if (key in actions) {
240
+ actions[key]()
241
+ event.preventDefault()
242
+ }
243
+ }
244
+
245
+ editCurrent() {
246
+ const current = this.getFocus()
247
+ if (current) {
248
+ this.editCell(current)
249
+ }
250
+ }
251
+
252
+ moveRight() {
253
+ const cell = this.getFocus()
254
+ if (cell.nextSibling) cell.nextSibling.focus()
255
+ }
256
+
257
+ moveLeft() {
258
+ const cell = this.getFocus()
259
+ if (cell.previousSibling) cell.previousSibling.focus()
260
+ }
261
+
262
+ moveDown() {
263
+ const cell = this.getFocus()
264
+ const tr = cell.closest('tr')
265
+ const property = cell.dataset.property
266
+ const nextTr = tr.nextSibling
267
+ if (nextTr) {
268
+ nextTr.querySelector(`td[data-property="${property}"`).focus()
269
+ }
270
+ }
271
+
272
+ moveUp() {
273
+ const cell = this.getFocus()
274
+ const tr = cell.closest('tr')
275
+ const property = cell.dataset.property
276
+ const previousTr = tr.previousSibling
277
+ if (previousTr) {
278
+ previousTr.querySelector(`td[data-property="${property}"`).focus()
279
+ }
280
+ }
281
+
282
+ checkAll(status = true) {
283
+ for (const checkbox of this.elements.body.querySelectorAll(
284
+ 'input[type=checkbox]'
285
+ )) {
286
+ checkbox.checked = status
287
+ }
288
+ }
289
+
290
+ getSelectedRows() {
291
+ return Array.from(
292
+ this.elements.body.querySelectorAll('input[type=checkbox]:checked')
293
+ ).map((checkbox) => checkbox.parentNode.parentNode)
294
+ }
295
+
296
+ getFocus() {
297
+ return this.elements.body.querySelector(':focus')
298
+ }
299
+
300
+ setFocus(cell) {
301
+ cell.focus({ focusVisible: true })
302
+ }
303
+
304
+ deleteRows() {
305
+ const selectedRows = this.getSelectedRows()
306
+ if (!selectedRows.length) return
307
+ this.map.dialog
308
+ .confirm(
309
+ translate('Found {count} rows. Are you sure you want to delete all?', {
310
+ count: selectedRows.length,
311
+ })
312
+ )
313
+ .then(() => {
314
+ this.datalayer.hide()
315
+ for (const row of selectedRows) {
316
+ const id = row.dataset.feature
317
+ const feature = this.datalayer.getFeatureById(id)
318
+ feature.del()
319
+ }
320
+ this.datalayer.show()
321
+ this.datalayer.fire('datachanged')
322
+ this.renderBody()
323
+ if (this.map.browser.isOpen()) {
324
+ this.map.browser.resetFilters()
325
+ this.map.browser.open('filters')
326
+ }
327
+ })
328
+ }
329
+ }
@@ -0,0 +1,93 @@
1
+ export class Positioned {
2
+ openAt({ anchor, position }) {
3
+ if (anchor && position === 'top') {
4
+ this.anchorTop(anchor)
5
+ } else if (anchor && position === 'left') {
6
+ this.anchorLeft(anchor)
7
+ } else if (anchor && position === 'bottom') {
8
+ this.anchorBottom(anchor)
9
+ } else {
10
+ this.anchorAbsolute()
11
+ }
12
+ }
13
+
14
+ anchorAbsolute() {
15
+ this.container.className = ''
16
+ const left =
17
+ this.parent.offsetLeft +
18
+ this.parent.clientWidth / 2 -
19
+ this.container.clientWidth / 2
20
+ const top = this.parent.offsetTop + 75
21
+ this.setPosition({ top: top, left: left })
22
+ }
23
+
24
+ anchorTop(el) {
25
+ this.container.className = 'tooltip-top'
26
+ const coords = this.getPosition(el)
27
+ this.setPosition({
28
+ left: coords.left - 10,
29
+ bottom: this.getDocHeight() - coords.top + 11,
30
+ })
31
+ }
32
+
33
+ anchorBottom(el) {
34
+ this.container.className = 'tooltip-bottom'
35
+ const coords = this.getPosition(el)
36
+ this.setPosition({
37
+ left: coords.left,
38
+ top: coords.bottom + 11,
39
+ })
40
+ }
41
+
42
+ anchorLeft(el) {
43
+ this.container.className = 'tooltip-left'
44
+ const coords = this.getPosition(el)
45
+ this.setPosition({
46
+ top: coords.top,
47
+ right: document.documentElement.offsetWidth - coords.left + 11,
48
+ })
49
+ }
50
+
51
+ getPosition(el) {
52
+ return el.getBoundingClientRect()
53
+ }
54
+
55
+ setPosition(coords) {
56
+ if (coords.left) this.container.style.left = `${coords.left}px`
57
+ else this.container.style.left = 'initial'
58
+ if (coords.right) this.container.style.right = `${coords.right}px`
59
+ else this.container.style.right = 'initial'
60
+ if (coords.top) this.container.style.top = `${coords.top}px`
61
+ else this.container.style.top = 'initial'
62
+ if (coords.bottom) this.container.style.bottom = `${coords.bottom}px`
63
+ else this.container.style.bottom = 'initial'
64
+ }
65
+
66
+ computePosition([x, y]) {
67
+ let left
68
+ let top
69
+ if (x < window.innerWidth / 2) {
70
+ left = x
71
+ } else {
72
+ left = x - this.container.offsetWidth
73
+ }
74
+ if (y < window.innerHeight / 2) {
75
+ top = y
76
+ } else {
77
+ top = y - this.container.offsetHeight
78
+ }
79
+ this.setPosition({ left, top })
80
+ }
81
+
82
+ getDocHeight() {
83
+ const D = document
84
+ return Math.max(
85
+ D.body.scrollHeight,
86
+ D.documentElement.scrollHeight,
87
+ D.body.offsetHeight,
88
+ D.documentElement.offsetHeight,
89
+ D.body.clientHeight,
90
+ D.documentElement.clientHeight
91
+ )
92
+ }
93
+ }
@@ -0,0 +1,50 @@
1
+ import { loadTemplate } from '../utils.js'
2
+ import { Positioned } from './base.js'
3
+
4
+ export default class ContextMenu extends Positioned {
5
+ constructor(options = {}) {
6
+ super()
7
+ this.options = options
8
+ this.container = document.createElement('ul')
9
+ this.container.className = 'umap-contextmenu'
10
+ if (options.className) {
11
+ this.container.classList.add(options.className)
12
+ }
13
+ this.container.addEventListener('focusout', (event) => {
14
+ if (!this.container.contains(event.relatedTarget)) this.close()
15
+ })
16
+ }
17
+
18
+ open([x, y], items) {
19
+ this.container.innerHTML = ''
20
+ for (const item of items) {
21
+ const li = loadTemplate(
22
+ `<li class="${item.className || ''}"><button tabindex="0" class="flat">${item.label}</button></li>`
23
+ )
24
+ li.addEventListener('click', () => {
25
+ this.close()
26
+ item.action()
27
+ })
28
+ this.container.appendChild(li)
29
+ }
30
+ document.body.appendChild(this.container)
31
+ this.computePosition([x, y])
32
+ this.container.querySelector('button').focus()
33
+ this.container.addEventListener('keydown', (event) => {
34
+ if (event.key === 'Escape') {
35
+ event.stopPropagation()
36
+ this.close()
37
+ }
38
+ })
39
+ }
40
+
41
+ close() {
42
+ try {
43
+ this.container.remove()
44
+ } catch {
45
+ // Race condition in Chrome: the focusout close has "half" removed the node
46
+ // So it's still visible in the DOM, but we calling .remove on it (or parentNode.removeChild)
47
+ // will crash.
48
+ }
49
+ }
50
+ }
@@ -1,23 +1,98 @@
1
- import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
1
  import { translate } from '../i18n.js'
2
+ import { WithTemplate } from '../utils.js'
3
3
 
4
- export default class Dialog {
5
- constructor(parent) {
6
- this.parent = parent
7
- this.className = 'umap-dialog window'
8
- this.container = DomUtil.create('dialog', this.className, this.parent)
9
- DomEvent.disableClickPropagation(this.container)
10
- DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu.
11
- DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation)
12
- DomEvent.on(this.container, 'MozMousePixelScroll', DomEvent.stopPropagation)
4
+ const TEMPLATE = `
5
+ <dialog data-ref="dialog">
6
+ <form method="dialog" data-ref="form">
7
+ <ul class="buttons">
8
+ <li><i class="icon icon-16 icon-close" data-close></i></li>
9
+ </ul>
10
+ <h3 data-ref="message" id="${Math.round(Date.now()).toString(36)}"></h3>
11
+ <fieldset data-ref="fieldset" role="document">
12
+ <div data-ref="template"></div>
13
+ </fieldset>
14
+ <menu>
15
+ <button type="button" data-ref="cancel" data-close value="cancel"></button>
16
+ <button type="submit" class="button" data-ref="accept" value="accept"></button>
17
+ </menu>
18
+ </form>
19
+ </dialog>
20
+ `
21
+
22
+ // From https://css-tricks.com/replace-javascript-dialogs-html-dialog-element/
23
+ export default class Dialog extends WithTemplate {
24
+ constructor(settings = {}) {
25
+ super()
26
+ this.settings = Object.assign(
27
+ {
28
+ accept: translate('OK'),
29
+ cancel: translate('Cancel'),
30
+ className: '',
31
+ message: '',
32
+ template: '',
33
+ },
34
+ settings
35
+ )
36
+ this.init()
13
37
  }
14
38
 
15
- get visible() {
16
- return this.container.open
39
+ collectFormData(formData) {
40
+ const object = {}
41
+ formData.forEach((value, key) => {
42
+ if (!Reflect.has(object, key)) {
43
+ object[key] = value
44
+ return
45
+ }
46
+ if (!Array.isArray(object[key])) {
47
+ object[key] = [object[key]]
48
+ }
49
+ object[key].push(value)
50
+ })
51
+ return object
17
52
  }
18
53
 
19
- close() {
20
- this.container.close()
54
+ getFocusable() {
55
+ return [
56
+ ...this.dialog.querySelectorAll(
57
+ 'button,[href],select,textarea,input:not([type="hidden"]),[tabindex]:not([tabindex="-1"])'
58
+ ),
59
+ ]
60
+ }
61
+
62
+ init() {
63
+ this.dialogSupported = typeof HTMLDialogElement === 'function'
64
+ this.dialog = this.loadTemplate(TEMPLATE)
65
+ this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'
66
+ document.body.appendChild(this.dialog)
67
+
68
+ this.focusable = []
69
+
70
+ this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
71
+ this.dialog.addEventListener('click', (event) => {
72
+ if (event.target.closest('[data-close]')) {
73
+ this.close()
74
+ }
75
+ })
76
+ if (!this.dialogSupported) {
77
+ this.elements.form.addEventListener('submit', (event) => {
78
+ event.preventDefault()
79
+ this.dialog.returnValue = 'accept'
80
+ this.close()
81
+ this.dialog.returnValue = undefined
82
+ })
83
+ }
84
+ this.dialog.addEventListener('keydown', (e) => {
85
+ if (e.key === 'Enter') {
86
+ if (!this.dialogSupported) {
87
+ e.preventDefault()
88
+ this.elements.form.requestSubmit()
89
+ }
90
+ }
91
+ if (e.key === 'Escape') {
92
+ e.stopPropagation()
93
+ this.close()
94
+ }
95
+ })
21
96
  }
22
97
 
23
98
  currentZIndex() {
@@ -28,25 +103,88 @@ export default class Dialog {
28
103
  )
29
104
  }
30
105
 
31
- open({ className, content, modal } = {}) {
32
- this.container.innerHTML = ''
106
+ open(settings = {}) {
107
+ const dialog = Object.assign({}, this.settings, settings)
108
+ this.dialog.className = 'umap-dialog window'
109
+ if (dialog.className) {
110
+ this.dialog.classList.add(...dialog.className.split(' '))
111
+ }
112
+ this.elements.accept.textContent = dialog.accept
113
+ this.elements.accept.hidden = !dialog.accept
114
+ this.elements.cancel.textContent = dialog.cancel
115
+ this.elements.cancel.hidden = !dialog.cancel
116
+ this.elements.message.textContent = dialog.message
117
+ this.elements.message.hidden = !dialog.message
118
+ this.elements.target = dialog.target || ''
119
+ this.elements.template.innerHTML = ''
120
+ if (dialog.template?.nodeType === 1) {
121
+ this.elements.template.appendChild(dialog.template)
122
+ } else {
123
+ this.elements.template.innerHTML = dialog.template || ''
124
+ }
125
+
126
+ this.focusable = this.getFocusable()
127
+ this.hasFormData = this.elements.fieldset.elements.length > 0
128
+
33
129
  const currentZIndex = this.currentZIndex()
34
- if (currentZIndex) this.container.style.zIndex = currentZIndex + 1
35
- if (modal) this.container.showModal()
36
- else this.container.show()
37
- if (className) {
38
- // Reset
39
- this.container.className = this.className
40
- this.container.classList.add(...className.split(' '))
130
+ if (currentZIndex) this.dialog.style.zIndex = currentZIndex + 1
131
+
132
+ this.toggle(true)
133
+
134
+ if (this.hasFormData) this.focusable[0].focus()
135
+ else this.elements.accept.focus()
136
+
137
+ return this.waitForUser()
138
+ }
139
+
140
+ close() {
141
+ this.toggle(false)
142
+ }
143
+
144
+ toggle(open = false) {
145
+ if (this.dialogSupported) {
146
+ if (open) this.dialog.show()
147
+ else this.dialog.close()
148
+ } else {
149
+ this.dialog.hidden = !open
150
+ if (this.elements.target && !open) {
151
+ this.elements.target.focus()
152
+ }
153
+ if (!open) {
154
+ this.dialog.dispatchEvent(new CustomEvent('close'))
155
+ }
41
156
  }
42
- const buttonsContainer = DomUtil.create('ul', 'buttons', this.container)
43
- const closeButton = DomUtil.createButtonIcon(
44
- DomUtil.create('li', '', buttonsContainer),
45
- 'icon-close',
46
- translate('Close')
157
+ }
158
+
159
+ waitForUser() {
160
+ return new Promise((resolve) => {
161
+ this.dialog.addEventListener(
162
+ 'close',
163
+ (event) => {
164
+ if (this.dialog.returnValue === 'accept') {
165
+ const value = this.hasFormData
166
+ ? this.collectFormData(new FormData(this.elements.form))
167
+ : true
168
+ resolve(value)
169
+ }
170
+ },
171
+ { once: true }
172
+ )
173
+ })
174
+ }
175
+
176
+ alert(config) {
177
+ return this.open(
178
+ Object.assign({}, config, { cancel: false, message, template: false })
47
179
  )
48
- DomEvent.on(closeButton, 'click', this.close, this)
49
- this.container.appendChild(buttonsContainer)
50
- this.container.appendChild(content)
180
+ }
181
+
182
+ confirm(message, config = {}) {
183
+ return this.open(Object.assign({}, config, { message, template: false }))
184
+ }
185
+
186
+ prompt(message, fallback = '', config = {}) {
187
+ const template = `<input type="text" name="prompt" value="${fallback}">`
188
+ return this.open(Object.assign({}, config, { message, template }))
51
189
  }
52
190
  }