umap-project 2.8.1__py3-none-any.whl → 2.9.0b0__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.

Potentially problematic release.


This version of umap-project might be problematic. Click here for more details.

Files changed (159) hide show
  1. umap/__init__.py +1 -1
  2. umap/asgi.py +12 -7
  3. umap/context_processors.py +1 -0
  4. umap/locale/en/LC_MESSAGES/django.po +102 -59
  5. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/fr/LC_MESSAGES/django.po +105 -61
  7. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/hu/LC_MESSAGES/django.po +10 -10
  9. umap/management/commands/empty_trash.py +12 -1
  10. umap/migrations/0026_datalayer_modified_at_datalayer_share_status.py +26 -0
  11. umap/models.py +23 -3
  12. umap/settings/base.py +4 -1
  13. umap/static/umap/base.css +1 -1
  14. umap/static/umap/content.css +2 -22
  15. umap/static/umap/css/bar.css +7 -10
  16. umap/static/umap/css/form.css +28 -29
  17. umap/static/umap/css/icon.css +8 -2
  18. umap/static/umap/css/panel.css +2 -1
  19. umap/static/umap/css/tooltip.css +33 -31
  20. umap/static/umap/img/16-white.svg +2 -0
  21. umap/static/umap/img/16.svg +1 -1
  22. umap/static/umap/img/providers/bitbucket.png +0 -0
  23. umap/static/umap/img/providers/github.png +0 -0
  24. umap/static/umap/img/providers/keycloak.png +0 -0
  25. umap/static/umap/img/providers/openstreetmap-oauth2.png +0 -0
  26. umap/static/umap/img/providers/twitter-oauth2.png +0 -0
  27. umap/static/umap/img/source/16-white.svg +3 -1
  28. umap/static/umap/img/source/16.svg +1 -1
  29. umap/static/umap/js/components/alerts/alert.js +4 -1
  30. umap/static/umap/js/modules/browser.js +6 -6
  31. umap/static/umap/js/modules/caption.js +30 -7
  32. umap/static/umap/js/modules/data/features.js +21 -24
  33. umap/static/umap/js/modules/data/layer.js +71 -33
  34. umap/static/umap/js/modules/form/builder.js +241 -0
  35. umap/static/umap/js/modules/form/fields.js +1338 -0
  36. umap/static/umap/js/modules/formatter.js +5 -8
  37. umap/static/umap/js/modules/help.js +3 -1
  38. umap/static/umap/js/modules/importer.js +1 -1
  39. umap/static/umap/js/modules/permissions.js +5 -4
  40. umap/static/umap/js/modules/rendering/icon.js +5 -1
  41. umap/static/umap/js/modules/rendering/layers/classified.js +11 -7
  42. umap/static/umap/js/modules/rendering/layers/cluster.js +11 -1
  43. umap/static/umap/js/modules/rendering/map.js +0 -2
  44. umap/static/umap/js/modules/rules.js +2 -1
  45. umap/static/umap/js/modules/schema.js +5 -6
  46. umap/static/umap/js/modules/share.js +3 -3
  47. umap/static/umap/js/modules/sync/engine.js +18 -13
  48. umap/static/umap/js/modules/sync/updaters.js +8 -0
  49. umap/static/umap/js/modules/sync/websocket.js +10 -5
  50. umap/static/umap/js/modules/tableeditor.js +3 -2
  51. umap/static/umap/js/modules/ui/bar.js +17 -9
  52. umap/static/umap/js/modules/ui/base.js +7 -24
  53. umap/static/umap/js/modules/ui/tooltip.js +19 -11
  54. umap/static/umap/js/modules/umap.js +39 -27
  55. umap/static/umap/js/modules/utils.js +196 -12
  56. umap/static/umap/js/umap.controls.js +1 -13
  57. umap/static/umap/locale/br.js +21 -13
  58. umap/static/umap/locale/br.json +21 -13
  59. umap/static/umap/locale/ca.js +12 -4
  60. umap/static/umap/locale/ca.json +12 -4
  61. umap/static/umap/locale/cs_CZ.js +10 -4
  62. umap/static/umap/locale/cs_CZ.json +10 -4
  63. umap/static/umap/locale/de.js +12 -4
  64. umap/static/umap/locale/de.json +12 -4
  65. umap/static/umap/locale/el.js +12 -4
  66. umap/static/umap/locale/el.json +12 -4
  67. umap/static/umap/locale/en.js +9 -4
  68. umap/static/umap/locale/en.json +9 -4
  69. umap/static/umap/locale/es.js +20 -12
  70. umap/static/umap/locale/es.json +20 -12
  71. umap/static/umap/locale/eu.js +12 -4
  72. umap/static/umap/locale/eu.json +12 -4
  73. umap/static/umap/locale/fa_IR.js +12 -4
  74. umap/static/umap/locale/fa_IR.json +12 -4
  75. umap/static/umap/locale/fr.js +10 -5
  76. umap/static/umap/locale/fr.json +10 -5
  77. umap/static/umap/locale/gl.js +353 -345
  78. umap/static/umap/locale/gl.json +353 -345
  79. umap/static/umap/locale/hu.js +34 -26
  80. umap/static/umap/locale/hu.json +34 -26
  81. umap/static/umap/locale/it.js +100 -92
  82. umap/static/umap/locale/it.json +100 -92
  83. umap/static/umap/locale/ms.js +12 -4
  84. umap/static/umap/locale/ms.json +12 -4
  85. umap/static/umap/locale/nl.js +12 -4
  86. umap/static/umap/locale/nl.json +12 -4
  87. umap/static/umap/locale/pl.js +12 -4
  88. umap/static/umap/locale/pl.json +12 -4
  89. umap/static/umap/locale/pt.js +12 -4
  90. umap/static/umap/locale/pt.json +12 -4
  91. umap/static/umap/locale/pt_PT.js +12 -4
  92. umap/static/umap/locale/pt_PT.json +12 -4
  93. umap/static/umap/locale/th_TH.js +12 -4
  94. umap/static/umap/locale/th_TH.json +12 -4
  95. umap/static/umap/locale/zh_TW.js +10 -4
  96. umap/static/umap/locale/zh_TW.json +10 -4
  97. umap/static/umap/map.css +12 -8
  98. umap/static/umap/nav.css +2 -3
  99. umap/static/umap/unittests/utils.js +14 -0
  100. umap/static/umap/vars.css +2 -0
  101. umap/sync/__init__.py +0 -0
  102. umap/sync/app.py +181 -0
  103. umap/sync/payloads.py +49 -0
  104. umap/templates/auth/user_detail.html +4 -0
  105. umap/templates/auth/user_form.html +9 -6
  106. umap/templates/auth/user_stars.html +4 -0
  107. umap/templates/base.html +1 -1
  108. umap/templates/registration/login.html +2 -5
  109. umap/templates/umap/about.html +5 -0
  110. umap/templates/umap/about_summary.html +2 -2
  111. umap/templates/umap/components/provider.html +8 -0
  112. umap/templates/umap/js.html +0 -3
  113. umap/templates/umap/map_detail.html +1 -1
  114. umap/templates/umap/password_change.html +4 -0
  115. umap/templates/umap/password_change_done.html +4 -0
  116. umap/templates/umap/search.html +4 -0
  117. umap/templates/umap/team_confirm_delete.html +4 -0
  118. umap/templates/umap/team_detail.html +4 -0
  119. umap/templates/umap/team_form.html +4 -0
  120. umap/templates/umap/user_dashboard.html +1 -1
  121. umap/templates/umap/user_teams.html +4 -0
  122. umap/tests/base.py +3 -1
  123. umap/tests/integration/conftest.py +16 -23
  124. umap/tests/integration/test_basics.py +2 -2
  125. umap/tests/integration/test_caption.py +1 -0
  126. umap/tests/integration/test_draw_polygon.py +3 -3
  127. umap/tests/integration/test_edit_datalayer.py +1 -1
  128. umap/tests/integration/test_edit_map.py +3 -3
  129. umap/tests/integration/test_edit_polygon.py +1 -1
  130. umap/tests/integration/test_import.py +23 -1
  131. umap/tests/integration/test_optimistic_merge.py +1 -0
  132. umap/tests/integration/test_picto.py +8 -8
  133. umap/tests/integration/test_save.py +1 -0
  134. umap/tests/integration/test_star.py +13 -9
  135. umap/tests/integration/test_tableeditor.py +1 -0
  136. umap/tests/integration/test_websocket_sync.py +112 -33
  137. umap/tests/settings.py +2 -0
  138. umap/tests/test_datalayer.py +2 -3
  139. umap/tests/test_datalayer_views.py +20 -1
  140. umap/tests/test_empty_trash.py +10 -3
  141. umap/tests/test_map_views.py +11 -0
  142. umap/utils.py +24 -11
  143. umap/views.py +37 -6
  144. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/METADATA +15 -15
  145. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/RECORD +148 -147
  146. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/WHEEL +1 -1
  147. umap/management/commands/run_websocket_server.py +0 -23
  148. umap/settings/local_s3.py +0 -45
  149. umap/static/umap/bitbucket.png +0 -0
  150. umap/static/umap/github.png +0 -0
  151. umap/static/umap/js/umap.forms.js +0 -1242
  152. umap/static/umap/keycloak.png +0 -0
  153. umap/static/umap/openstreetmap.png +0 -0
  154. umap/static/umap/twitter.png +0 -0
  155. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +0 -468
  156. umap/tests/test_websocket_server.py +0 -22
  157. umap/websocket_server.py +0 -202
  158. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/entry_points.txt +0 -0
  159. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1338 @@
1
+ import * as Utils from '../utils.js'
2
+ import { translate } from '../i18n.js'
3
+ import {
4
+ AjaxAutocomplete,
5
+ AjaxAutocompleteMultiple,
6
+ AutocompleteDatalist,
7
+ } from '../autocomplete.js'
8
+ import { SCHEMA } from '../schema.js'
9
+ import * as Icon from '../rendering/icon.js'
10
+
11
+ const Fields = {}
12
+
13
+ export default function getClass(name) {
14
+ if (typeof name === 'function') return name
15
+ if (!Fields[name]) throw Error(`Unknown class ${name}`)
16
+ return Fields[name]
17
+ }
18
+
19
+ class BaseElement {
20
+ constructor(builder, field, properties) {
21
+ this.builder = builder
22
+ this.obj = this.builder.obj
23
+ this.form = this.builder.form
24
+ this.field = field
25
+ this.setProperties(properties)
26
+ this.fieldEls = this.field.split('.')
27
+ this.name = this.builder.getName(field)
28
+ this.id = `${this.builder.properties.id || Date.now()}.${this.name}`
29
+ }
30
+
31
+ getDefaultProperties() {
32
+ return {}
33
+ }
34
+
35
+ setProperties(properties) {
36
+ this.properties = Object.assign(
37
+ this.getDefaultProperties(),
38
+ this.properties,
39
+ properties
40
+ )
41
+ }
42
+
43
+ onDefine() {}
44
+
45
+ buildTemplate() {
46
+ const template = this.builder.getTemplate(this)
47
+ const [root, elements] = Utils.loadTemplateWithRefs(template)
48
+ this.root = root
49
+ this.elements = elements
50
+ this.container = elements.container
51
+ this.form.appendChild(this.root)
52
+ }
53
+
54
+ getTemplate() {
55
+ return ''
56
+ }
57
+
58
+ build() {
59
+ if (this.properties.helpText) {
60
+ this.elements.helpText.textContent = this.properties.helpText
61
+ } else {
62
+ this.elements.helpText.hidden = true
63
+ }
64
+
65
+ if (this.elements.define) {
66
+ this.elements.define.addEventListener('click', (event) => {
67
+ event.preventDefault()
68
+ event.stopPropagation()
69
+ this.fetch()
70
+ this.onDefine()
71
+ this.root.classList.remove('undefined')
72
+ })
73
+ }
74
+ if (this.elements.undefine) {
75
+ this.elements.undefine.addEventListener('click', () => this.undefine())
76
+ }
77
+ }
78
+
79
+ clear() {
80
+ this.input.value = ''
81
+ }
82
+
83
+ get(own) {
84
+ if (!this.properties.inheritable || own) return this.builder.getter(this.field)
85
+ const path = this.field.split('.')
86
+ const key = path[path.length - 1]
87
+ return this.obj.getOption(key) || SCHEMA[key]?.default
88
+ }
89
+
90
+ toHTML() {
91
+ return this.get()
92
+ }
93
+
94
+ toJS() {
95
+ return this.value()
96
+ }
97
+
98
+ sync() {
99
+ this.set()
100
+ this.builder.fire('set', { helper: this })
101
+ }
102
+
103
+ set() {
104
+ this.builder.setter(this.field, this.toJS())
105
+ }
106
+
107
+ getLabelTemplate() {
108
+ const label = this.properties.label
109
+ const help = this.properties.helpEntries?.join() || ''
110
+ return label
111
+ ? `<label title="${label}" data-ref=label data-help="${help}">${label}</label>`
112
+ : ''
113
+ }
114
+
115
+ fetch() {}
116
+
117
+ finish() {}
118
+
119
+ undefine() {
120
+ this.root.classList.add('undefined')
121
+ this.clear()
122
+ this.sync()
123
+ }
124
+ }
125
+
126
+ Fields.Textarea = class extends BaseElement {
127
+ getTemplate() {
128
+ return `<textarea placeholder="${this.properties.placeholder || ''}" data-ref=textarea></textarea>`
129
+ }
130
+
131
+ build() {
132
+ super.build()
133
+ this.textarea = this.elements.textarea
134
+ this.fetch()
135
+ this.textarea.addEventListener(
136
+ 'input',
137
+ Utils.debounce(() => this.sync(), 300)
138
+ )
139
+ this.textarea.addEventListener('keypress', (event) => this.onKeyPress(event))
140
+ }
141
+
142
+ fetch() {
143
+ const value = this.toHTML()
144
+ this.initial = value
145
+ if (value) {
146
+ this.textarea.value = value
147
+ }
148
+ }
149
+
150
+ value() {
151
+ return this.textarea.value
152
+ }
153
+
154
+ onKeyPress(event) {
155
+ if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) {
156
+ event.stopPropagation()
157
+ event.preventDefault()
158
+ this.finish()
159
+ }
160
+ }
161
+ }
162
+
163
+ Fields.Input = class extends BaseElement {
164
+ getTemplate() {
165
+ return `<input type="${this.type()}" name="${this.name}" placeholder="${this.properties.placeholder || ''}" data-ref=input />`
166
+ }
167
+
168
+ build() {
169
+ super.build()
170
+ this.input = this.elements.input
171
+ this.input._helper = this
172
+ if (this.properties.className) {
173
+ this.input.classList.add(this.properties.className)
174
+ }
175
+ if (this.properties.min !== undefined) {
176
+ this.input.min = this.properties.min
177
+ }
178
+ if (this.properties.max !== undefined) {
179
+ this.input.max = this.properties.max
180
+ }
181
+ if (this.properties.step) {
182
+ this.input.step = this.properties.step
183
+ }
184
+ this.fetch()
185
+ this.listenForSync()
186
+ this.input.addEventListener('keydown', (event) => this.onKeyDown(event))
187
+ }
188
+
189
+ fetch() {
190
+ const value = this.toHTML() !== undefined ? this.toHTML() : null
191
+ this.initial = value
192
+ this.input.value = value
193
+ }
194
+
195
+ listenForSync() {
196
+ this.input.addEventListener(
197
+ 'input',
198
+ Utils.debounce(() => this.sync(), 300)
199
+ )
200
+ }
201
+
202
+ type() {
203
+ return this.properties.type || 'text'
204
+ }
205
+
206
+ value() {
207
+ return this.input.value || undefined
208
+ }
209
+
210
+ onKeyDown(event) {
211
+ if (event.key === 'Enter') {
212
+ event.stopPropagation()
213
+ event.preventDefault()
214
+ this.finish()
215
+ this.input.blur()
216
+ }
217
+ }
218
+ }
219
+
220
+ Fields.BlurInput = class extends Fields.Input {
221
+ listenForSync() {
222
+ this.input.addEventListener('blur', () => this.sync())
223
+ }
224
+
225
+ getTemplate() {
226
+ return `<div class="blur-container">${super.getTemplate()}<button type="button">✔</button></div>`
227
+ }
228
+
229
+ build() {
230
+ this.properties.className = 'blur'
231
+ super.build()
232
+ this.input.addEventListener('focus', () => this.fetch())
233
+ }
234
+
235
+ finish() {
236
+ this.sync()
237
+ super.finish()
238
+ }
239
+
240
+ sync() {
241
+ // Do not commit any change if user only clicked
242
+ // on the field than clicked outside
243
+ if (this.initial !== this.value()) {
244
+ super.sync()
245
+ }
246
+ }
247
+ }
248
+ const IntegerMixin = (Base) =>
249
+ class extends Base {
250
+ value() {
251
+ return !isNaN(this.input.value) && this.input.value !== ''
252
+ ? parseInt(this.input.value, 10)
253
+ : undefined
254
+ }
255
+
256
+ type() {
257
+ return 'number'
258
+ }
259
+ }
260
+
261
+ Fields.IntInput = class extends IntegerMixin(Fields.Input) {}
262
+ Fields.BlurIntInput = class extends IntegerMixin(Fields.BlurInput) {}
263
+
264
+ const FloatMixin = (Base) =>
265
+ class extends Base {
266
+ value() {
267
+ return !isNaN(this.input.value) && this.input.value !== ''
268
+ ? parseFloat(this.input.value)
269
+ : undefined
270
+ }
271
+
272
+ type() {
273
+ return 'number'
274
+ }
275
+ }
276
+
277
+ Fields.FloatInput = class extends FloatMixin(Fields.Input) {
278
+ // TODO use public class properties when in baseline
279
+ getDefaultProperties() {
280
+ return { step: 'any' }
281
+ }
282
+ }
283
+
284
+ Fields.BlurFloatInput = class extends FloatMixin(Fields.BlurInput) {
285
+ getDefaultProperties() {
286
+ return { step: 'any' }
287
+ }
288
+ }
289
+
290
+ Fields.CheckBox = class extends BaseElement {
291
+ getTemplate() {
292
+ return `<input type=checkbox name="${this.name}" data-ref=input />`
293
+ }
294
+
295
+ build() {
296
+ this.input = this.elements.input
297
+ this.input._helper = this
298
+ this.fetch()
299
+ this.input.addEventListener('change', () => this.sync())
300
+ super.build()
301
+ }
302
+
303
+ fetch() {
304
+ this.initial = this.toHTML()
305
+ this.input.checked = this.initial === true
306
+ }
307
+
308
+ value() {
309
+ return this.root.classList.contains('undefined') ? undefined : this.input.checked
310
+ }
311
+
312
+ toHTML() {
313
+ return [1, true].indexOf(this.get()) !== -1
314
+ }
315
+
316
+ clear() {
317
+ this.fetch()
318
+ }
319
+ }
320
+
321
+ Fields.Select = class extends BaseElement {
322
+ getTemplate() {
323
+ return `<select name="${this.name}" data-ref=select></select>`
324
+ }
325
+
326
+ build() {
327
+ this.select = this.elements.select
328
+ this.validValues = []
329
+ this.buildOptions()
330
+ this.select.addEventListener('change', () => this.sync())
331
+ super.build()
332
+ }
333
+
334
+ getOptions() {
335
+ return this.properties.selectOptions
336
+ }
337
+
338
+ fetch() {
339
+ this.buildOptions()
340
+ }
341
+
342
+ buildOptions() {
343
+ this.select.innerHTML = ''
344
+ for (const option of this.getOptions()) {
345
+ if (typeof option === 'string') this.buildOption(option, option)
346
+ else this.buildOption(option[0], option[1])
347
+ }
348
+ }
349
+
350
+ buildOption(value, label) {
351
+ this.validValues.push(value)
352
+ const option = Utils.loadTemplate('<option></option>')
353
+ this.select.appendChild(option)
354
+ option.value = value
355
+ option.textContent = label
356
+ if (this.toHTML() === value) {
357
+ option.selected = 'selected'
358
+ }
359
+ }
360
+
361
+ value() {
362
+ if (this.select[this.select.selectedIndex]) {
363
+ return this.select[this.select.selectedIndex].value
364
+ }
365
+ }
366
+
367
+ getDefault() {
368
+ if (this.properties.inheritable) return undefined
369
+ return this.getOptions()[0][0]
370
+ }
371
+
372
+ toJS() {
373
+ const value = this.value()
374
+ if (this.validValues.indexOf(value) !== -1) {
375
+ return value
376
+ }
377
+ return this.getDefault()
378
+ }
379
+
380
+ clear() {
381
+ this.select.value = ''
382
+ }
383
+ }
384
+
385
+ Fields.IntSelect = class extends Fields.Select {
386
+ value() {
387
+ return parseInt(super.value(), 10)
388
+ }
389
+ }
390
+
391
+ Fields.EditableText = class extends BaseElement {
392
+ getTemplate() {
393
+ return `<span contentEditable class="${this.properties.className || ''}" data-ref=input></span>`
394
+ }
395
+
396
+ buildTemplate() {
397
+ // No wrapper at all
398
+ const template = this.getTemplate()
399
+ this.input = Utils.loadTemplate(template)
400
+ this.form.appendChild(this.input)
401
+ }
402
+
403
+ build() {
404
+ this.fetch()
405
+ this.input.addEventListener('input', () => this.sync())
406
+ this.input.addEventListener('keypress', (event) => this.onKeyPress(event))
407
+ }
408
+
409
+ value() {
410
+ return this.input.textContent
411
+ }
412
+
413
+ fetch() {
414
+ this.input.textContent = this.toHTML()
415
+ }
416
+
417
+ onKeyPress(event) {
418
+ if (event.keyCode === 13) {
419
+ event.preventDefault()
420
+ this.input.blur()
421
+ }
422
+ }
423
+ }
424
+
425
+ Fields.ColorPicker = class extends Fields.Input {
426
+ getColors() {
427
+ return Utils.COLORS
428
+ }
429
+
430
+ getDefaultProperties() {
431
+ return {
432
+ placeholder: translate('Inherit'),
433
+ }
434
+ }
435
+
436
+ getTemplate() {
437
+ return `${super.getTemplate()}<div class="umap-color-picker" hidden data-ref=colors></div>`
438
+ }
439
+
440
+ build() {
441
+ super.build()
442
+ for (const color of this.getColors()) {
443
+ this.addColor(color)
444
+ }
445
+ this.spreadColor()
446
+ this.input.autocomplete = 'off'
447
+ this.input.addEventListener('focus', (event) => this.onFocus(event))
448
+ this.input.addEventListener('blur', (event) => this.onBlur(event))
449
+ this.input.addEventListener('change', () => this.sync())
450
+ }
451
+
452
+ onDefine() {
453
+ this.onFocus()
454
+ }
455
+
456
+ onFocus() {
457
+ this.showPicker()
458
+ this.spreadColor()
459
+ }
460
+
461
+ showPicker() {
462
+ this.elements.colors.hidden = false
463
+ }
464
+
465
+ closePicker() {
466
+ this.elements.colors.hidden = true
467
+ }
468
+
469
+ onBlur() {
470
+ // We must leave time for the click to be listened.
471
+ window.setTimeout(() => this.closePicker(), 100)
472
+ }
473
+
474
+ sync() {
475
+ this.spreadColor()
476
+ super.sync()
477
+ }
478
+
479
+ spreadColor() {
480
+ if (this.input.value) this.input.style.backgroundColor = this.input.value
481
+ else this.input.style.backgroundColor = 'inherit'
482
+ }
483
+
484
+ addColor(colorName) {
485
+ const span = Utils.loadTemplate('<span></span>')
486
+ this.elements.colors.appendChild(span)
487
+ span.style.backgroundColor = span.title = colorName
488
+ const updateColorInput = () => {
489
+ this.input.value = colorName
490
+ this.sync()
491
+ this.closePicker()
492
+ }
493
+ span.addEventListener('mousedown', updateColorInput)
494
+ }
495
+ }
496
+
497
+ Fields.TextColorPicker = class extends Fields.ColorPicker {
498
+ getColors() {
499
+ return [
500
+ 'Black',
501
+ 'DarkSlateGrey',
502
+ 'DimGrey',
503
+ 'SlateGrey',
504
+ 'LightSlateGrey',
505
+ 'Grey',
506
+ 'DarkGrey',
507
+ 'LightGrey',
508
+ 'White',
509
+ ]
510
+ }
511
+ }
512
+
513
+ Fields.LayerTypeChooser = class extends Fields.Select {
514
+ getOptions() {
515
+ return U.LAYER_TYPES.map((class_) => [class_.TYPE, class_.NAME])
516
+ }
517
+ }
518
+
519
+ Fields.SlideshowDelay = class extends Fields.IntSelect {
520
+ getOptions() {
521
+ const options = []
522
+ for (let i = 1; i < 30; i++) {
523
+ options.push([i * 1000, translate('{delay} seconds', { delay: i })])
524
+ }
525
+ return options
526
+ }
527
+ }
528
+
529
+ Fields.DataLayerSwitcher = class extends Fields.Select {
530
+ getOptions() {
531
+ const options = []
532
+ this.builder._umap.eachDataLayerReverse((datalayer) => {
533
+ if (
534
+ datalayer.isLoaded() &&
535
+ !datalayer.isDataReadOnly() &&
536
+ datalayer.isBrowsable()
537
+ ) {
538
+ options.push([L.stamp(datalayer), datalayer.getName()])
539
+ }
540
+ })
541
+ return options
542
+ }
543
+
544
+ toHTML() {
545
+ return L.stamp(this.obj.datalayer)
546
+ }
547
+
548
+ toJS() {
549
+ return this.builder._umap.datalayers[this.value()]
550
+ }
551
+
552
+ set() {
553
+ this.builder._umap.lastUsedDataLayer = this.toJS()
554
+ this.obj.changeDataLayer(this.toJS())
555
+ }
556
+ }
557
+
558
+ Fields.DataFormat = class extends Fields.Select {
559
+ getOptions() {
560
+ return [
561
+ [undefined, translate('Choose the data format')],
562
+ ['geojson', 'geojson'],
563
+ ['osm', 'osm'],
564
+ ['csv', 'csv'],
565
+ ['gpx', 'gpx'],
566
+ ['kml', 'kml'],
567
+ ['georss', 'georss'],
568
+ ]
569
+ }
570
+ }
571
+
572
+ Fields.LicenceChooser = class extends Fields.Select {
573
+ getOptions() {
574
+ const licences = []
575
+ const licencesList = this.builder.obj.properties.licences
576
+ let licence
577
+ for (const i in licencesList) {
578
+ licence = licencesList[i]
579
+ licences.push([i, licence.name])
580
+ }
581
+ return licences
582
+ }
583
+
584
+ toHTML() {
585
+ return this.get()?.name
586
+ }
587
+
588
+ toJS() {
589
+ return this.builder.obj.properties.licences[this.value()]
590
+ }
591
+ }
592
+
593
+ Fields.NullableBoolean = class extends Fields.Select {
594
+ getOptions() {
595
+ return [
596
+ [undefined, translate('inherit')],
597
+ [true, translate('yes')],
598
+ [false, translate('no')],
599
+ ]
600
+ }
601
+
602
+ toJS() {
603
+ let value = this.value()
604
+ switch (value) {
605
+ case 'true':
606
+ case true:
607
+ value = true
608
+ break
609
+ case 'false':
610
+ case false:
611
+ value = false
612
+ break
613
+ default:
614
+ value = undefined
615
+ }
616
+ return value
617
+ }
618
+ }
619
+
620
+ // Adds an autocomplete using all available user defined properties
621
+ Fields.PropertyInput = class extends Fields.BlurInput {
622
+ build() {
623
+ super.build()
624
+ const autocomplete = new AutocompleteDatalist(this.input)
625
+ // Will be used on Umap and DataLayer
626
+ const properties = this.builder.obj.allProperties()
627
+ autocomplete.suggestions = properties
628
+ }
629
+ }
630
+
631
+ Fields.IconUrl = class extends Fields.BlurInput {
632
+ type() {
633
+ return 'hidden'
634
+ }
635
+
636
+ getTemplate() {
637
+ return `
638
+ <div>
639
+ <div class="flat-tabs" data-ref=tabs></div>
640
+ <div class="umap-pictogram-body" data-ref=body>
641
+ ${super.getTemplate()}
642
+ </div>
643
+ <div data-ref=footer></div>
644
+ </div>
645
+ `
646
+ }
647
+
648
+ build() {
649
+ super.build()
650
+ this.tabs = this.elements.tabs
651
+ this.body = this.elements.body
652
+ this.footer = this.elements.footer
653
+ this.button = Utils.loadTemplate(
654
+ `<button type="button" class="button action-button" hidden>${translate('Change')}</button>`
655
+ )
656
+ this.button.addEventListener('click', () => this.onDefine())
657
+ this.elements.buttons.appendChild(this.button)
658
+ this.updatePreview()
659
+ }
660
+
661
+ async onDefine() {
662
+ this.footer.innerHTML = ''
663
+ const [{ pictogram_list }, response, error] = await this.builder._umap.server.get(
664
+ this.builder._umap.properties.urls.pictogram_list_json
665
+ )
666
+ if (!error) this.pictogram_list = pictogram_list
667
+ this.buildTabs()
668
+ const value = this.value()
669
+ if (Icon.RECENT.length) this.showRecentTab()
670
+ else if (!value || Utils.isPath(value)) this.showSymbolsTab()
671
+ else if (Utils.isRemoteUrl(value) || Utils.isDataImage(value)) this.showURLTab()
672
+ else this.showCharsTab()
673
+ const closeButton = Utils.loadTemplate(
674
+ `<button type="button" class="button action-button">${translate('Close')}</button>`
675
+ )
676
+ closeButton.addEventListener('click', (event) => {
677
+ this.body.innerHTML = ''
678
+ this.tabs.innerHTML = ''
679
+ this.footer.innerHTML = ''
680
+ if (this.isDefault()) this.undefine()
681
+ else this.updatePreview()
682
+ })
683
+ this.footer.appendChild(closeButton)
684
+ }
685
+
686
+ buildTabs() {
687
+ this.tabs.innerHTML = ''
688
+ // Useless div, but loadTemplate needs a root element
689
+ const [root, { recent, symbols, chars, url }] = Utils.loadTemplateWithRefs(`
690
+ <div>
691
+ <button class="flat tab-recent" data-ref=recent>${translate('Recent')}</button>
692
+ <button class="flat tab-symbols" data-ref=symbols>${translate('Symbol')}</button>
693
+ <button class="flat tab-chars" data-ref=chars>${translate('Emoji & Character')}</button>
694
+ <button class="flat tab-url" data-ref=url>${translate('URL')}</button>
695
+ </div>
696
+ `)
697
+ this.tabs.appendChild(root)
698
+ if (Icon.RECENT.length) {
699
+ recent.addEventListener('click', (event) => {
700
+ event.stopPropagation()
701
+ event.preventDefault()
702
+ this.showRecentTab()
703
+ })
704
+ } else {
705
+ recent.hidden = true
706
+ }
707
+ symbols.addEventListener('click', (event) => {
708
+ event.stopPropagation()
709
+ event.preventDefault()
710
+ this.showSymbolsTab()
711
+ })
712
+ chars.addEventListener('click', (event) => {
713
+ event.stopPropagation()
714
+ event.preventDefault()
715
+ this.showCharsTab()
716
+ })
717
+ url.addEventListener('click', (event) => {
718
+ event.stopPropagation()
719
+ event.preventDefault()
720
+ this.showURLTab()
721
+ })
722
+ }
723
+
724
+ openTab(name) {
725
+ const els = this.tabs.querySelectorAll('button')
726
+ for (const el of els) {
727
+ el.classList.remove('on')
728
+ }
729
+ const el = this.tabs.querySelector(`.tab-${name}`)
730
+ el.classList.add('on')
731
+ this.body.innerHTML = ''
732
+ }
733
+
734
+ updatePreview() {
735
+ this.elements.actions.innerHTML = ''
736
+ this.button.hidden = !this.value() || this.isDefault()
737
+ if (this.isDefault()) return
738
+ if (!Utils.hasVar(this.value())) {
739
+ // Do not try to render URL with variables
740
+ const box = Utils.loadTemplate('<div class="umap-pictogram-choice"></div>')
741
+ this.elements.actions.appendChild(box)
742
+ box.addEventListener('click', () => this.onDefine())
743
+ const icon = Icon.makeElement(this.value(), box)
744
+ }
745
+ }
746
+
747
+ addIconPreview(pictogram, parent) {
748
+ const baseClass = 'umap-pictogram-choice'
749
+ const value = pictogram.src
750
+ const search = Utils.normalize(this.searchInput.value)
751
+ const title = pictogram.attribution
752
+ ? `${pictogram.name} — © ${pictogram.attribution}`
753
+ : pictogram.name || pictogram.src
754
+ if (search && Utils.normalize(title).indexOf(search) === -1) return
755
+ const className = value === this.value() ? `${baseClass} selected` : baseClass
756
+ const container = Utils.loadTemplate(
757
+ `<div class="${className}" title="${title}"></div>`
758
+ )
759
+ parent.appendChild(container)
760
+ Icon.makeElement(value, container)
761
+ container.addEventListener('click', () => {
762
+ this.input.value = value
763
+ this.sync()
764
+ this.unselectAll(this.grid)
765
+ container.classList.add('selected')
766
+ this.updatePreview()
767
+ })
768
+ return true // Icon has been added (not filtered)
769
+ }
770
+
771
+ clear() {
772
+ this.input.value = ''
773
+ this.unselectAll(this.body)
774
+ this.sync()
775
+ this.body.innerHTML = ''
776
+ this.updatePreview()
777
+ }
778
+
779
+ addCategory(items, name) {
780
+ const [parent, { grid }] = Utils.loadTemplateWithRefs(`
781
+ <div class="umap-pictogram-category">
782
+ <h6 hidden=${!name}>${name}</h6>
783
+ <div class="umap-pictogram-grid" data-ref=grid></div>
784
+ </div>
785
+ `)
786
+ let hasIcons = false
787
+ for (const item of items) {
788
+ hasIcons = this.addIconPreview(item, grid) || hasIcons
789
+ }
790
+ if (hasIcons) this.grid.appendChild(parent)
791
+ }
792
+
793
+ buildSymbolsList() {
794
+ this.grid.innerHTML = ''
795
+ const categories = {}
796
+ let category
797
+ for (const props of this.pictogram_list) {
798
+ category = props.category || translate('Generic')
799
+ categories[category] = categories[category] || []
800
+ categories[category].push(props)
801
+ }
802
+ const sorted = Object.entries(categories).toSorted(([a], [b]) =>
803
+ Utils.naturalSort(a, b, U.lang)
804
+ )
805
+ for (const [name, items] of sorted) {
806
+ this.addCategory(items, name)
807
+ }
808
+ }
809
+
810
+ buildRecentList() {
811
+ this.grid.innerHTML = ''
812
+ const items = U.Icon.RECENT.map((src) => ({
813
+ src,
814
+ }))
815
+ this.addCategory(items)
816
+ }
817
+
818
+ isDefault() {
819
+ return !this.value() || this.value() === SCHEMA.iconUrl.default
820
+ }
821
+
822
+ addGrid(onSearch) {
823
+ this.searchInput = Utils.loadTemplate(
824
+ `<input type="search" placeholder="${translate('Search')}" />`
825
+ )
826
+ this.grid = Utils.loadTemplate('<div></div>')
827
+ this.body.appendChild(this.searchInput)
828
+ this.body.appendChild(this.grid)
829
+ this.searchInput.addEventListener('input', onSearch)
830
+ }
831
+
832
+ showRecentTab() {
833
+ if (!Icon.RECENT.length) return
834
+ this.openTab('recent')
835
+ this.addGrid(() => this.buildRecentList())
836
+ this.buildRecentList()
837
+ }
838
+
839
+ showSymbolsTab() {
840
+ this.openTab('symbols')
841
+ this.addGrid(() => this.buildSymbolsList())
842
+ this.buildSymbolsList()
843
+ }
844
+
845
+ showCharsTab() {
846
+ this.openTab('chars')
847
+ const value = !Icon.isImg(this.value()) ? this.value() : null
848
+ const input = this.buildInput(this.body, value)
849
+ input.placeholder = translate('Type char or paste emoji')
850
+ input.type = 'text'
851
+ }
852
+
853
+ showURLTab() {
854
+ this.openTab('url')
855
+ const value =
856
+ Utils.isRemoteUrl(this.value()) || Utils.isDataImage(this.value())
857
+ ? this.value()
858
+ : null
859
+ const input = this.buildInput(this.body, value)
860
+ input.placeholder = translate('Add image URL')
861
+ input.type = 'url'
862
+ }
863
+
864
+ buildInput(parent, value) {
865
+ const [element, { input }] = Utils.loadTemplateWithRefs(
866
+ '<div class="blur-container"><input class="blur" data-ref="input" /><button type="button">✔</button></div>'
867
+ )
868
+ parent.appendChild(element)
869
+ if (value) input.value = value
870
+ input.addEventListener('blur', () => {
871
+ // Do not clear this.input when focus-blur
872
+ // empty input
873
+ if (input.value === value) return
874
+ this.input.value = input.value
875
+ this.sync()
876
+ })
877
+ return input
878
+ }
879
+
880
+ unselectAll(container) {
881
+ for (const el of container.querySelectorAll('div.selected')) {
882
+ el.classList.remove('selected')
883
+ }
884
+ }
885
+ }
886
+
887
+ Fields.Url = class extends Fields.Input {
888
+ type() {
889
+ return 'url'
890
+ }
891
+ }
892
+
893
+ Fields.Switch = class extends Fields.CheckBox {
894
+ getTemplate() {
895
+ const label = this.properties.label
896
+ return `${super.getTemplate()}<label title="${label}" for="${this.id}" data-ref=customLabel>${label}</label>`
897
+ }
898
+
899
+ build() {
900
+ super.build()
901
+ // We have it in our template
902
+ if (!this.properties.inheritable) {
903
+ // We already have the label near the switch,
904
+ // only show the default label in inheritable mode
905
+ // as the switch itself may be hidden (until "defined")
906
+ if (this.elements.label) {
907
+ this.elements.label.hidden = true
908
+ this.elements.label.innerHTML = ''
909
+ this.elements.label.title = ''
910
+ }
911
+ }
912
+ this.container.classList.add('with-switch')
913
+ this.input.classList.add('switch')
914
+ this.input.id = this.id
915
+ }
916
+ }
917
+
918
+ Fields.FacetSearchBase = class extends BaseElement {
919
+ buildLabel() {}
920
+ }
921
+
922
+ Fields.FacetSearchChoices = class extends Fields.FacetSearchBase {
923
+ getTemplate() {
924
+ return `
925
+ <fieldset class="umap-facet">
926
+ <legend data-ref=label>${Utils.escapeHTML(this.properties.label)}</legend>
927
+ <ul data-ref=ul></ul>
928
+ </fieldset>
929
+ `
930
+ }
931
+
932
+ build() {
933
+ this.type = this.properties.criteria.type
934
+
935
+ const choices = this.properties.criteria.choices
936
+ choices.sort()
937
+ choices.forEach((value) => this.buildLi(value))
938
+ super.build()
939
+ }
940
+
941
+ buildLi(value) {
942
+ const name = `${this.type}_${this.name}`
943
+ const [li, { input, label }] = Utils.loadTemplateWithRefs(`
944
+ <li>
945
+ <label>
946
+ <input type="${this.type}" name="${name}" data-ref=input />
947
+ <span data-ref=label></span>
948
+ </label>
949
+ </li>
950
+ `)
951
+ label.textContent = value
952
+ input.checked = this.get().choices.includes(value)
953
+ input.dataset.value = value
954
+ input.addEventListener('change', () => this.sync())
955
+ this.elements.ul.appendChild(li)
956
+ }
957
+
958
+ toJS() {
959
+ return {
960
+ type: this.type,
961
+ choices: [...this.elements.ul.querySelectorAll('input:checked')].map(
962
+ (i) => i.dataset.value
963
+ ),
964
+ }
965
+ }
966
+ }
967
+
968
+ Fields.MinMaxBase = class extends Fields.FacetSearchBase {
969
+ getInputType(type) {
970
+ return type
971
+ }
972
+
973
+ getLabels() {
974
+ return [translate('Min'), translate('Max')]
975
+ }
976
+
977
+ prepareForHTML(value) {
978
+ return value.valueOf()
979
+ }
980
+
981
+ getTemplate() {
982
+ const [minLabel, maxLabel] = this.getLabels()
983
+ const { min, max, type } = this.properties.criteria
984
+ this.type = type
985
+ const inputType = this.getInputType(this.type)
986
+ const minHTML = this.prepareForHTML(min)
987
+ const maxHTML = this.prepareForHTML(max)
988
+ return `
989
+ <fieldset class="umap-facet">
990
+ <legend>${Utils.escapeHTML(this.properties.label)}</legend>
991
+ <label>${minLabel}<input min="${minHTML}" max="${maxHTML}" step=any type="${inputType}" data-ref=minInput /></label>
992
+ <label>${maxLabel}<input min="${minHTML}" max="${maxHTML}" step=any type="${inputType}" data-ref=maxInput /></label>
993
+ </fieldset>
994
+ `
995
+ }
996
+
997
+ build() {
998
+ this.minInput = this.elements.minInput
999
+ this.maxInput = this.elements.maxInput
1000
+ const { min, max, type } = this.properties.criteria
1001
+ const { min: modifiedMin, max: modifiedMax } = this.get()
1002
+
1003
+ const currentMin = modifiedMin !== undefined ? modifiedMin : min
1004
+ const currentMax = modifiedMax !== undefined ? modifiedMax : max
1005
+ if (min != null) {
1006
+ // The value stored using setAttribute is not modified by
1007
+ // user input, and will be used as initial value when calling
1008
+ // form.reset(), and can also be retrieve later on by using
1009
+ // getAttributing, to compare with current value and know
1010
+ // if this value has been modified by the user
1011
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset
1012
+ this.minInput.setAttribute('value', this.prepareForHTML(min))
1013
+ this.minInput.value = this.prepareForHTML(currentMin)
1014
+ }
1015
+
1016
+ if (max != null) {
1017
+ // Cf comment above about setAttribute vs value
1018
+ this.maxInput.setAttribute('value', this.prepareForHTML(max))
1019
+ this.maxInput.value = this.prepareForHTML(currentMax)
1020
+ }
1021
+ this.toggleStatus()
1022
+
1023
+ this.minInput.addEventListener('change', () => this.sync())
1024
+ this.maxInput.addEventListener('change', () => this.sync())
1025
+ super.build()
1026
+ }
1027
+
1028
+ toggleStatus() {
1029
+ this.minInput.dataset.modified = this.isMinModified()
1030
+ this.maxInput.dataset.modified = this.isMaxModified()
1031
+ }
1032
+
1033
+ sync() {
1034
+ super.sync()
1035
+ this.toggleStatus()
1036
+ }
1037
+
1038
+ isMinModified() {
1039
+ const default_ = this.minInput.getAttribute('value')
1040
+ const current = this.minInput.value
1041
+ return current !== default_
1042
+ }
1043
+
1044
+ isMaxModified() {
1045
+ const default_ = this.maxInput.getAttribute('value')
1046
+ const current = this.maxInput.value
1047
+ return current !== default_
1048
+ }
1049
+
1050
+ toJS() {
1051
+ const opts = {
1052
+ type: this.type,
1053
+ }
1054
+ if (this.minInput.value !== '' && this.isMinModified()) {
1055
+ opts.min = this.prepareForJS(this.minInput.value)
1056
+ }
1057
+ if (this.maxInput.value !== '' && this.isMaxModified()) {
1058
+ opts.max = this.prepareForJS(this.maxInput.value)
1059
+ }
1060
+ return opts
1061
+ }
1062
+ }
1063
+
1064
+ Fields.FacetSearchNumber = class extends Fields.MinMaxBase {
1065
+ prepareForJS(value) {
1066
+ return new Number(value)
1067
+ }
1068
+ }
1069
+
1070
+ Fields.FacetSearchDate = class extends Fields.MinMaxBase {
1071
+ prepareForJS(value) {
1072
+ return new Date(value)
1073
+ }
1074
+
1075
+ toLocaleDateTime(dt) {
1076
+ return new Date(dt.valueOf() - dt.getTimezoneOffset() * 60000)
1077
+ }
1078
+
1079
+ prepareForHTML(value) {
1080
+ // Value must be in local time
1081
+ if (Number.isNaN(value)) return
1082
+ return this.toLocaleDateTime(value).toISOString().substr(0, 10)
1083
+ }
1084
+
1085
+ getLabels() {
1086
+ return [translate('From'), translate('Until')]
1087
+ }
1088
+ }
1089
+
1090
+ Fields.FacetSearchDateTime = class extends Fields.FacetSearchDate {
1091
+ getInputType(type) {
1092
+ return 'datetime-local'
1093
+ }
1094
+
1095
+ prepareForHTML(value) {
1096
+ // Value must be in local time
1097
+ if (Number.isNaN(value)) return
1098
+ return this.toLocaleDateTime(value).toISOString().slice(0, -1)
1099
+ }
1100
+ }
1101
+
1102
+ Fields.MultiChoice = class extends BaseElement {
1103
+ getDefault() {
1104
+ return 'null'
1105
+ }
1106
+ // TODO: use public property when it's in our baseline
1107
+ getClassName() {
1108
+ return 'umap-multiplechoice'
1109
+ }
1110
+
1111
+ clear() {
1112
+ const checked = this.container.querySelector('input[type="radio"]:checked')
1113
+ if (checked) checked.checked = false
1114
+ }
1115
+
1116
+ fetch() {
1117
+ this.initial = this.toHTML()
1118
+ let value = this.initial
1119
+ if (!this.container.querySelector(`input[type="radio"][value="${value}"]`)) {
1120
+ value =
1121
+ this.properties.default !== undefined ? this.properties.default : this.default
1122
+ }
1123
+ const choices = this.getChoices().map(([value, label]) => `${value}`)
1124
+ if (choices.includes(`${value}`)) {
1125
+ this.container.querySelector(`input[type="radio"][value="${value}"]`).checked =
1126
+ true
1127
+ }
1128
+ }
1129
+
1130
+ value() {
1131
+ const checked = this.container.querySelector('input[type="radio"]:checked')
1132
+ if (checked) return checked.value
1133
+ }
1134
+
1135
+ getChoices() {
1136
+ return this.properties.choices || this.choices
1137
+ }
1138
+
1139
+ getTemplate() {
1140
+ return `<div class="${this.getClassName()} by${this.getChoices().length}" data-ref=wrapper></div>`
1141
+ }
1142
+
1143
+ build() {
1144
+ const choices = this.getChoices()
1145
+ for (const [i, [value, label]] of choices.entries()) {
1146
+ this.addChoice(value, label, i)
1147
+ }
1148
+ this.fetch()
1149
+ super.build()
1150
+ }
1151
+
1152
+ addChoice(value, label, counter) {
1153
+ const id = `${Date.now()}.${this.name}.${counter}`
1154
+ const input = Utils.loadTemplate(
1155
+ `<input type="radio" name="${this.name}" id="${id}" value="${value}" />`
1156
+ )
1157
+ this.elements.wrapper.appendChild(input)
1158
+ this.elements.wrapper.appendChild(
1159
+ Utils.loadTemplate(`<label for="${id}">${label}</label>`)
1160
+ )
1161
+ input.addEventListener('change', () => this.sync())
1162
+ }
1163
+ }
1164
+
1165
+ Fields.TernaryChoices = class extends Fields.MultiChoice {
1166
+ getDefault() {
1167
+ return 'null'
1168
+ }
1169
+
1170
+ toJS() {
1171
+ let value = this.value()
1172
+ switch (value) {
1173
+ case 'true':
1174
+ case true:
1175
+ value = true
1176
+ break
1177
+ case 'false':
1178
+ case false:
1179
+ value = false
1180
+ break
1181
+ case 'null':
1182
+ case null:
1183
+ value = null
1184
+ break
1185
+ default:
1186
+ value = undefined
1187
+ }
1188
+ return value
1189
+ }
1190
+ }
1191
+
1192
+ Fields.NullableChoices = class extends Fields.TernaryChoices {
1193
+ getChoices() {
1194
+ return (
1195
+ this.properties.choices || [
1196
+ [true, translate('always')],
1197
+ [false, translate('never')],
1198
+ ['null', translate('hidden')],
1199
+ ]
1200
+ )
1201
+ }
1202
+ }
1203
+
1204
+ Fields.DataLayersControl = class extends Fields.TernaryChoices {
1205
+ getChoices() {
1206
+ return [
1207
+ [true, translate('collapsed')],
1208
+ ['expanded', translate('expanded')],
1209
+ [false, translate('never')],
1210
+ ['null', translate('hidden')],
1211
+ ]
1212
+ }
1213
+
1214
+ toJS() {
1215
+ let value = this.value()
1216
+ if (value !== 'expanded') value = super.toJS()
1217
+ return value
1218
+ }
1219
+ }
1220
+
1221
+ Fields.Range = class extends Fields.FloatInput {
1222
+ type() {
1223
+ return 'range'
1224
+ }
1225
+
1226
+ value() {
1227
+ return this.root.classList.contains('undefined') ? undefined : super.value()
1228
+ }
1229
+
1230
+ build() {
1231
+ super.build()
1232
+ let options = ''
1233
+ const step = this.properties.step || 1
1234
+ const digits = step < 1 ? 1 : 0
1235
+ const id = `range-${this.properties.label || this.name}`
1236
+ for (
1237
+ let i = this.properties.min;
1238
+ i <= this.properties.max;
1239
+ i += this.properties.step
1240
+ ) {
1241
+ const ii = i.toFixed(digits)
1242
+ options += `<option value="${ii}" label="${ii}"></option>`
1243
+ }
1244
+ const datalist = Utils.loadTemplate(
1245
+ `<datalist class="umap-field-datalist" id="${id}">${options}</datalist>`
1246
+ )
1247
+ this.container.appendChild(datalist)
1248
+ this.input.setAttribute('list', id)
1249
+ }
1250
+ }
1251
+
1252
+ Fields.ManageOwner = class extends BaseElement {
1253
+ build() {
1254
+ const options = {
1255
+ className: 'edit-owner',
1256
+ on_select: L.bind(this.onSelect, this),
1257
+ placeholder: translate("Type new owner's username"),
1258
+ }
1259
+ this.autocomplete = new AjaxAutocomplete(this.container, options)
1260
+ const owner = this.toHTML()
1261
+ if (owner) {
1262
+ this.autocomplete.displaySelected({
1263
+ item: { value: owner.id, label: owner.name },
1264
+ })
1265
+ }
1266
+ }
1267
+
1268
+ value() {
1269
+ return this._value
1270
+ }
1271
+
1272
+ onSelect(choice) {
1273
+ this._value = {
1274
+ id: choice.item.value,
1275
+ name: choice.item.label,
1276
+ url: choice.item.url,
1277
+ }
1278
+ this.set()
1279
+ }
1280
+ }
1281
+
1282
+ Fields.ManageEditors = class extends BaseElement {
1283
+ build() {
1284
+ const options = {
1285
+ className: 'edit-editors',
1286
+ on_select: L.bind(this.onSelect, this),
1287
+ on_unselect: L.bind(this.onUnselect, this),
1288
+ placeholder: translate("Type editor's username"),
1289
+ }
1290
+ this.autocomplete = new AjaxAutocompleteMultiple(this.container, options)
1291
+ this._values = this.toHTML()
1292
+ if (this._values)
1293
+ for (let i = 0; i < this._values.length; i++)
1294
+ this.autocomplete.displaySelected({
1295
+ item: { value: this._values[i].id, label: this._values[i].name },
1296
+ })
1297
+ }
1298
+
1299
+ value() {
1300
+ return this._values
1301
+ }
1302
+
1303
+ onSelect(choice) {
1304
+ this._values.push({
1305
+ id: choice.item.value,
1306
+ name: choice.item.label,
1307
+ url: choice.item.url,
1308
+ })
1309
+ this.set()
1310
+ }
1311
+
1312
+ onUnselect(choice) {
1313
+ const index = this._values.findIndex((item) => item.id === choice.item.value)
1314
+ if (index !== -1) {
1315
+ this._values.splice(index, 1)
1316
+ this.set()
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ Fields.ManageTeam = class extends Fields.IntSelect {
1322
+ getOptions() {
1323
+ return [[null, translate('None')]].concat(
1324
+ this.properties.teams.map((team) => [team.id, team.name])
1325
+ )
1326
+ }
1327
+
1328
+ toHTML() {
1329
+ return this.get()?.id
1330
+ }
1331
+
1332
+ toJS() {
1333
+ const value = this.value()
1334
+ for (const team of this.properties.teams) {
1335
+ if (team.id === value) return team
1336
+ }
1337
+ }
1338
+ }