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

Potentially problematic release.


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

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