@defra/forms-engine-plugin 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.public/javascripts/application.min.js +1 -1
  2. package/.public/javascripts/application.min.js.map +1 -1
  3. package/.public/javascripts/shared.min.js +1 -1
  4. package/.public/javascripts/shared.min.js.map +1 -1
  5. package/.public/javascripts/vendor/accessible-autocomplete.min.js.map +1 -1
  6. package/.public/stylesheets/application.min.css +1 -1
  7. package/.public/stylesheets/application.min.css.map +1 -1
  8. package/.server/client/javascripts/file-upload.js +13 -8
  9. package/.server/client/javascripts/file-upload.js.map +1 -1
  10. package/.server/client/javascripts/geospatial-map.d.ts +189 -0
  11. package/.server/client/javascripts/geospatial-map.js +1068 -0
  12. package/.server/client/javascripts/geospatial-map.js.map +1 -0
  13. package/.server/client/javascripts/location-map.d.ts +6 -91
  14. package/.server/client/javascripts/location-map.js +78 -385
  15. package/.server/client/javascripts/location-map.js.map +1 -1
  16. package/.server/client/javascripts/map.d.ts +199 -0
  17. package/.server/client/javascripts/map.js +384 -0
  18. package/.server/client/javascripts/map.js.map +1 -0
  19. package/.server/client/javascripts/shared.d.ts +3 -1
  20. package/.server/client/javascripts/shared.js +3 -1
  21. package/.server/client/javascripts/shared.js.map +1 -1
  22. package/.server/client/stylesheets/shared.scss +7 -0
  23. package/.server/server/plugins/engine/components/ComponentBase.d.ts +1 -0
  24. package/.server/server/plugins/engine/components/ComponentBase.js +2 -0
  25. package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
  26. package/.server/server/plugins/engine/components/FileUploadField.d.ts +3 -2
  27. package/.server/server/plugins/engine/components/FileUploadField.js +11 -3
  28. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  29. package/.server/server/plugins/engine/components/FormComponent.d.ts +9 -1
  30. package/.server/server/plugins/engine/components/FormComponent.js +22 -0
  31. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  32. package/.server/server/plugins/engine/components/GeospatialField.d.ts +77 -0
  33. package/.server/server/plugins/engine/components/GeospatialField.js +102 -0
  34. package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -0
  35. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.d.ts +3 -0
  36. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js +63 -0
  37. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js.map +1 -0
  38. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  39. package/.server/server/plugins/engine/components/helpers/components.js +7 -0
  40. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  41. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +6 -0
  42. package/.server/server/plugins/engine/components/helpers/geospatial.js +71 -0
  43. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -0
  44. package/.server/server/plugins/engine/components/helpers/geospatial.test.js +42 -0
  45. package/.server/server/plugins/engine/components/helpers/geospatial.test.js.map +1 -0
  46. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  47. package/.server/server/plugins/engine/components/index.js +1 -0
  48. package/.server/server/plugins/engine/components/index.js.map +1 -1
  49. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +11 -0
  50. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +65 -28
  51. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  52. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +1 -0
  53. package/.server/server/plugins/engine/pageControllers/PageController.js +2 -0
  54. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  55. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +13 -1
  56. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/validationOptions.js +2 -1
  58. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  59. package/.server/server/plugins/engine/types.d.ts +63 -2
  60. package/.server/server/plugins/engine/types.js +33 -0
  61. package/.server/server/plugins/engine/types.js.map +1 -1
  62. package/.server/server/plugins/engine/views/components/geospatialfield.html +7 -0
  63. package/.server/server/plugins/nunjucks/context.test.js.map +1 -1
  64. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  65. package/.server/server/routes/types.js.map +1 -1
  66. package/.server/server/services/cacheService.js +3 -0
  67. package/.server/server/services/cacheService.js.map +1 -1
  68. package/package.json +9 -5
  69. package/src/client/javascripts/file-upload.js +12 -8
  70. package/src/client/javascripts/geospatial-map.js +1023 -0
  71. package/src/client/javascripts/location-map.js +94 -390
  72. package/src/client/javascripts/map.js +389 -0
  73. package/src/client/javascripts/shared.js +3 -1
  74. package/src/client/stylesheets/shared.scss +7 -0
  75. package/src/server/plugins/engine/components/ComponentBase.ts +2 -0
  76. package/src/server/plugins/engine/components/FileUploadField.test.ts +11 -8
  77. package/src/server/plugins/engine/components/FileUploadField.ts +14 -5
  78. package/src/server/plugins/engine/components/FormComponent.ts +29 -0
  79. package/src/server/plugins/engine/components/GeospatialField.test.ts +380 -0
  80. package/src/server/plugins/engine/components/GeospatialField.ts +145 -0
  81. package/src/server/plugins/engine/components/helpers/__stubs__/geospatial.ts +85 -0
  82. package/src/server/plugins/engine/components/helpers/components.test.ts +44 -0
  83. package/src/server/plugins/engine/components/helpers/components.ts +10 -0
  84. package/src/server/plugins/engine/components/helpers/geospatial.test.js +55 -0
  85. package/src/server/plugins/engine/components/helpers/geospatial.ts +93 -0
  86. package/src/server/plugins/engine/components/index.ts +1 -0
  87. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +109 -5
  88. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +69 -21
  89. package/src/server/plugins/engine/pageControllers/PageController.ts +2 -0
  90. package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +74 -0
  91. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +17 -1
  92. package/src/server/plugins/engine/pageControllers/validationOptions.ts +3 -1
  93. package/src/server/plugins/engine/types.ts +77 -4
  94. package/src/server/plugins/engine/views/components/geospatialfield.html +7 -0
  95. package/src/server/plugins/nunjucks/context.test.js +2 -3
  96. package/src/server/routes/types.ts +4 -2
  97. package/src/server/services/cacheService.ts +2 -0
@@ -0,0 +1,1068 @@
1
+ import { bbox } from '@turf/bbox';
2
+ import { EVENTS, createMap, defaultConfig, getCentroidGridRef, getCoordinateGridRef } from "./map.js";
3
+ const helpPanelConfig = {
4
+ showLabel: true,
5
+ label: 'How to use this map',
6
+ mobile: {
7
+ slot: 'bottom',
8
+ open: true,
9
+ dismissible: true,
10
+ modal: false
11
+ },
12
+ tablet: {
13
+ slot: 'bottom',
14
+ open: true,
15
+ dismissible: true,
16
+ modal: false
17
+ },
18
+ desktop: {
19
+ slot: 'bottom',
20
+ open: true,
21
+ dismissible: true,
22
+ modal: false
23
+ },
24
+ html: '<p class="govuk-body-s govuk-!-margin-bottom-2">You can add points, shapes or lines to the map.</p><ul class="govuk-list govuk-list--number govuk-body-s"><li>Search for a county, place or postcode</li><li>Use the + and - icons to zoom in and out</li><li>Add a point or click \'Done\' when you have finished drawing a line or shape</li><li>Give the location a name</li></ul>'
25
+ };
26
+ const lineFeatureProperties = {
27
+ stroke: 'rgba(0, 11, 112, 1)',
28
+ fill: 'rgba(0, 11, 112, 0.2)',
29
+ strokeWidth: 2
30
+ };
31
+ const polygonFeatureProperties = {
32
+ stroke: 'rgba(0,112,60,1)',
33
+ fill: 'rgba(0,112,60,0.2)',
34
+ strokeWidth: 2
35
+ };
36
+
37
+ /**
38
+ * @type {Record<'Point' | 'LineString' | 'Polygon', string>}
39
+ */
40
+ const typeDescriptions = {
41
+ Point: 'Point',
42
+ LineString: 'Line',
43
+ Polygon: 'Shape'
44
+ };
45
+ const POINT_SVG = '<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" /><circle cx="12" cy="10" r="3" />';
46
+ const POLYGON_SVG = '<path d="M19.5 7v10M4.5 7v10M7 19.5h10M7 4.5h10"/><path d="M22 18v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm0-15v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zM7 18v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zM7 3v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1z"/>';
47
+ const LINE_SVG = '<path d="M5.706 16.294L16.294 5.706"/><path d="M21 2v3c0 .549-.451 1-1 1h-3c-.549 0-1-.451-1-1V2c0-.549.451-1 1-1h3c.549 0 1 .451 1 1zM6 17v3c0 .549-.451 1-1 1H2c-.549 0-1-.451-1-1v-3c0-.549.451-1 1-1h3c.549 0 1 .451 1 1z"/>';
48
+
49
+ /**
50
+ * Extract and parses the GeoJSON from the textarea
51
+ * @param {HTMLTextAreaElement} geospatialInput - the textarea containing the geojson
52
+ */
53
+ export function getGeoJSON(geospatialInput) {
54
+ const value = geospatialInput.value.trim();
55
+ const hasValue = !!value;
56
+
57
+ /** @type {FeatureCollection} */
58
+ const features = hasValue ? JSON.parse(value) : [];
59
+
60
+ /** @type {GeoJSON} */
61
+ const geojson = {
62
+ type: 'FeatureCollection',
63
+ features
64
+ };
65
+ return geojson;
66
+ }
67
+
68
+ /**
69
+ * Gets the bounding box covering a feature collection
70
+ * @param {GeoJSON} geojson - the geojson
71
+ */
72
+ export function getBoundingBox(geojson) {
73
+ return bbox(geojson);
74
+ }
75
+
76
+ /**
77
+ * Processes a geospatial field to add map capability
78
+ * @param {MapsEnvironmentConfig} config - the geospatial field element
79
+ * @param {Element} geospatial - the geospatial field element
80
+ * @param {number} index - the 0-based index
81
+ */
82
+ export function processGeospatial(config, geospatial, index) {
83
+ // @ts-expect-error - Defra namespace currently comes from UMD support files
84
+ const defra = window.defra;
85
+ if (!(geospatial instanceof HTMLDivElement)) {
86
+ return;
87
+ }
88
+ const geospatialInput = geospatial.querySelector('.govuk-textarea');
89
+ if (!(geospatialInput instanceof HTMLTextAreaElement)) {
90
+ return;
91
+ }
92
+ const {
93
+ listEl,
94
+ mapId
95
+ } = createContainers(geospatialInput, index);
96
+ const geojson = getGeoJSON(geospatialInput);
97
+ const bounds = geojson.features.length ? getBoundingBox(geojson) : undefined;
98
+ const drawPlugin = defra.drawMLPlugin();
99
+ const initConfig = {
100
+ ...defaultConfig,
101
+ bounds,
102
+ plugins: [drawPlugin]
103
+ };
104
+ const {
105
+ map,
106
+ interactPlugin
107
+ } = createMap(mapId, initConfig, config);
108
+ const featuresManager = getFeaturesManager(geojson);
109
+ const activeFeatureManager = getActiveFeatureManager();
110
+ const uiManager = getUIManager(geojson, map, mapId, listEl, geospatialInput);
111
+
112
+ /**
113
+ * @type {Context}
114
+ */
115
+ const context = {
116
+ map,
117
+ featuresManager,
118
+ activeFeatureManager,
119
+ uiManager,
120
+ interactPlugin,
121
+ drawPlugin
122
+ };
123
+ addEventListeners(context);
124
+ }
125
+
126
+ /**
127
+ * Adds a feature to the map
128
+ * @param {Feature} feature - the geojson feature
129
+ * @param {any} drawPlugin - the map draw plugin
130
+ * @param {InteractiveMap} map - the interactive map
131
+ */
132
+ export function addFeatureToMap(feature, drawPlugin, map) {
133
+ switch (feature.geometry.type) {
134
+ case 'Polygon':
135
+ drawPlugin.addFeature({
136
+ ...feature,
137
+ ...polygonFeatureProperties
138
+ });
139
+ break;
140
+ case 'LineString':
141
+ drawPlugin.addFeature({
142
+ ...feature,
143
+ ...lineFeatureProperties
144
+ });
145
+ break;
146
+ case 'Point':
147
+ map.addMarker(feature.id, feature.geometry.coordinates);
148
+ break;
149
+ default:
150
+ break;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Returns HTML summary list for the features
156
+ * @param {FeatureCollection} features - the features
157
+ * @param {string} mapId - the ID of the map
158
+ * @param {boolean} [readonly] - render the list in readonly mode
159
+ */
160
+ export function createFeaturesHTML(features, mapId, readonly = false) {
161
+ return `<dl class="govuk-summary-list">
162
+ ${features.map((feature, index) => createFeatureHTML(feature, index, mapId, readonly)).join('\n')}
163
+ </dl>`;
164
+ }
165
+
166
+ /**
167
+ * Focus feature
168
+ * @param {Feature} feature - the feature
169
+ * @param {MapLibreMap} mapProvider - the feature id
170
+ */
171
+ export function focusFeature(feature, mapProvider) {
172
+ mapProvider.fitBounds(bbox(feature));
173
+ }
174
+
175
+ /**
176
+ * Returns HTML summary row for an single feature
177
+ * @param {Feature} feature - the geo feature
178
+ * @param {number} index - the feature index
179
+ * @param {string} mapId - the ID of the map
180
+ * @param {boolean} readonly - render the list item in readonly mode
181
+ */
182
+ function createFeatureHTML(feature, index, mapId, readonly) {
183
+ const flattened = feature.geometry.coordinates.flat(2);
184
+ const points = [];
185
+ for (let i = 0; i < flattened.length; i += 2) {
186
+ points.push(flattened.slice(i, i + 2).join(', '));
187
+ }
188
+ const coordinates = points.map(p => `<li>${p}</li>`).join('');
189
+ const description = readonly ? `<p class="govuk-body govuk-!-margin-bottom-0">${feature.properties.description}</p>` : `<input class="govuk-input govuk-!-width-two-thirds" type="text" id="description_${index}" value="${feature.properties.description}" data-id="${feature.id}">`;
190
+
191
+ // Change action link
192
+ const changeAction = () => `<li class="govuk-summary-list__actions-list-item">
193
+ <a class="govuk-link govuk-link--no-visited-state" href="#${mapId}" data-action="edit" data-id="${feature.id}"
194
+ data-type="${feature.geometry.type}">Update<span class="govuk-visually-hidden"> location</span></a>
195
+ </li>`;
196
+
197
+ // Delete action link
198
+ const deleteAction = () => `<li class="govuk-summary-list__actions-list-item">
199
+ <a class="govuk-link govuk-link--no-visited-state" href="#" data-action="delete" data-id="${feature.id}"
200
+ data-type="${feature.geometry.type}">Delete<span class="govuk-visually-hidden"> location</span></a>
201
+ </li>`;
202
+
203
+ // Focus action link
204
+ const focusAction = () => `<li class="govuk-summary-list__actions-list-item">
205
+ <a class="govuk-link govuk-link--no-visited-state" href="#${mapId}" data-action="focus" data-id="${feature.id}">Show<span class="govuk-visually-hidden"> location</span></a>
206
+ </li>`;
207
+ const links = readonly ? focusAction() : `${changeAction()}${deleteAction()}`;
208
+ const actions = `<ul class="govuk-summary-list__actions-list">${links}</ul>`;
209
+ return `<div class="govuk-summary-list__row govuk-summary-list__row--no-border">
210
+ <dt class="govuk-summary-list__key">
211
+ <div class="govuk-form-group">
212
+ <label class="govuk-label govuk-label--s" ${readonly ? '' : `for="description_${index}"`}>Location ${index + 1} description</label>
213
+ ${description}
214
+ </div>
215
+ </dt>
216
+ <dd class="govuk-summary-list__actions">
217
+ ${actions}
218
+ </dd>
219
+ </div>
220
+ <div class="govuk-summary-list__row">
221
+ <details class="govuk-details govuk-!-margin-bottom-2">
222
+ <summary class="govuk-details__summary">
223
+ <span class="govuk-details__summary-text">Coordinates</span>
224
+ </summary>
225
+ <div class="govuk-details__text">
226
+ <dl class="govuk-summary-list">
227
+ <div class="govuk-summary-list__row">
228
+ <dt class="govuk-summary-list__key">Type</dt>
229
+ <dd class="govuk-summary-list__value">${typeDescriptions[feature.geometry.type]}</dd>
230
+ </div>
231
+ <div class="govuk-summary-list__row">
232
+ <dt class="govuk-summary-list__key">Center grid reference</dt>
233
+ <dd class="govuk-summary-list__value">${feature.properties.centroidGridReference}</dd>
234
+ </div>
235
+ <div class="govuk-summary-list__row">
236
+ <dt class="govuk-summary-list__key">First point grid reference</dt>
237
+ <dd class="govuk-summary-list__value">${feature.properties.coordinateGridReference}</dd>
238
+ </div>
239
+ <div class="govuk-summary-list__row">
240
+ <dt class="govuk-summary-list__key">Detailed coordinates</dt>
241
+ <dd class="govuk-summary-list__value">
242
+ <ol class="govuk-list govuk-list--number">${coordinates}</ol>
243
+ </dd>
244
+ </div>
245
+ </dl>
246
+ </div>
247
+ </details>
248
+ </div>`;
249
+ }
250
+
251
+ /**
252
+ * Generate a random id
253
+ */
254
+ function generateID() {
255
+ return window.crypto.randomUUID();
256
+ }
257
+
258
+ /**
259
+ * Factory closure to track the active feature id
260
+ * @returns {ActiveFeatureManager}
261
+ */
262
+ function getActiveFeatureManager() {
263
+ /** @type {string | undefined} */
264
+ let activeFeature;
265
+
266
+ /**
267
+ * Returns the active feature id
268
+ * @type {GetActiveFeature}
269
+ */
270
+ function getActiveFeature() {
271
+ return activeFeature;
272
+ }
273
+
274
+ /**
275
+ * Sets the active feature id
276
+ * @type {SetActiveFeature}
277
+ */
278
+ function setActiveFeature(id) {
279
+ activeFeature = id;
280
+ }
281
+
282
+ /**
283
+ * Resets the active feature id
284
+ * @type {ResetActiveFeature}
285
+ */
286
+ function resetActiveFeature() {
287
+ activeFeature = undefined;
288
+ }
289
+ return {
290
+ getActiveFeature,
291
+ setActiveFeature,
292
+ resetActiveFeature
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Reduce coordinate precision to 7 dps
298
+ * @param {Feature} feature
299
+ */
300
+ function prepareGeometry(feature) {
301
+ const {
302
+ geometry
303
+ } = feature;
304
+ const maxPrecision = 7;
305
+
306
+ /**
307
+ * @param {Coordinates} coordinates
308
+ */
309
+ function formatPrecision(coordinates) {
310
+ coordinates[0] = +coordinates[0].toFixed(maxPrecision);
311
+ coordinates[1] = +coordinates[1].toFixed(maxPrecision);
312
+ }
313
+ if (geometry.type === 'Point') {
314
+ formatPrecision(geometry.coordinates);
315
+ } else if (geometry.type === 'LineString') {
316
+ geometry.coordinates.forEach(formatPrecision);
317
+ } else {
318
+ geometry.coordinates.flat().forEach(formatPrecision);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Factory closure to return a features manager
324
+ * @param {GeoJSON} geojson
325
+ * @returns {FeaturesManager}
326
+ */
327
+ function getFeaturesManager(geojson) {
328
+ /**
329
+ * Get a feature from the geojson by id
330
+ * @type {GetFeatures}
331
+ */
332
+ function getFeatures() {
333
+ return geojson.features;
334
+ }
335
+
336
+ /**
337
+ * Get a feature from the geojson by id
338
+ * @type {GetFeature}
339
+ */
340
+ function getFeature(id) {
341
+ return geojson.features.find(f => f.id === id);
342
+ }
343
+
344
+ /**
345
+ * Add a feature to the geojson
346
+ * @type {AddFeature}
347
+ */
348
+ function addFeature(feature) {
349
+ feature.properties.coordinateGridReference = getCoordinateGridRef(feature);
350
+ feature.properties.centroidGridReference = getCentroidGridRef(feature);
351
+ prepareGeometry(feature);
352
+ geojson.features.push(feature);
353
+ }
354
+
355
+ /**
356
+ * Updates a feature in the geojson
357
+ * @type {UpdateFeature}
358
+ */
359
+ function updateFeature(id, geometry) {
360
+ const feature = getFeature(id);
361
+
362
+ // Ensure the feature exists in the geojson
363
+ if (feature) {
364
+ feature.properties.coordinateGridReference = getCoordinateGridRef(feature);
365
+ feature.properties.centroidGridReference = getCentroidGridRef(feature);
366
+ feature.geometry = geometry;
367
+ prepareGeometry(feature);
368
+ }
369
+ return feature;
370
+ }
371
+
372
+ /**
373
+ * Removes a feature from the geojson
374
+ * @type {RemoveFeature}
375
+ */
376
+ function removeFeature(id) {
377
+ const idx = geojson.features.findIndex(f => f.id === id);
378
+ return idx > -1 ? geojson.features.splice(idx, 1) : undefined;
379
+ }
380
+ return {
381
+ getFeatures,
382
+ getFeature,
383
+ addFeature,
384
+ updateFeature,
385
+ removeFeature
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Factory to render features into the list and hidden textarea
391
+ * @param {GeoJSON} geojson - the geojson of features
392
+ * @param {string} mapId - the ID of the map
393
+ * @param {HTMLDivElement} listEl - where to render the feature list
394
+ * @param {Function} renderValue - function that renders the features JSON into the hidden textarea
395
+ * @returns {RenderList}
396
+ */
397
+ function getListRenderer(geojson, mapId, listEl, renderValue) {
398
+ return function renderList() {
399
+ const html = createFeaturesHTML(geojson.features, mapId);
400
+ listEl.innerHTML = html;
401
+ renderValue();
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Factory to render features JSON into the hidden textarea
407
+ * @param {GeoJSON} geojson - the features
408
+ * @param {HTMLTextAreaElement} geospatialInput - the geospatial textarea
409
+ * @returns {RenderValue}
410
+ */
411
+ function getValueRenderer(geojson, geospatialInput) {
412
+ return function renderValue() {
413
+ geospatialInput.value = JSON.stringify(geojson.features, null, 2);
414
+ };
415
+ }
416
+
417
+ /**
418
+ * Factory closure to manage the UI
419
+ * @param {GeoJSON} geojson - the features
420
+ * @param {InteractiveMap} map - the map
421
+ * @param {string} mapId - the ID of the map
422
+ * @param {HTMLDivElement} listEl - where to render the feature list
423
+ * @param {HTMLTextAreaElement} geospatialInput - the geospatial textarea
424
+ */
425
+ function getUIManager(geojson, map, mapId, listEl, geospatialInput) {
426
+ /**
427
+ * Toggle the hidden state of the action buttons
428
+ * @type {ToggleActionButtons}
429
+ */
430
+ function toggleActionButtons(hidden) {
431
+ map.toggleButtonState('btnAddPoint', 'hidden', hidden);
432
+ map.toggleButtonState('btnAddPolygon', 'hidden', hidden);
433
+ map.toggleButtonState('btnAddLine', 'hidden', hidden);
434
+ }
435
+
436
+ /**
437
+ * Set focus to the last description input
438
+ * @type {FocusDescriptionInput}
439
+ */
440
+ function focusDescriptionInput() {
441
+ const inputs = listEl.querySelectorAll('input');
442
+ if (inputs.length) {
443
+ const lastInput = /** @type {HTMLInputElement} */inputs.item(inputs.length - 1);
444
+ lastInput.focus();
445
+ lastInput.select();
446
+ }
447
+ }
448
+ const renderValue = getValueRenderer(geojson, geospatialInput);
449
+ const renderList = getListRenderer(geojson, mapId, listEl, renderValue);
450
+
451
+ /** @type {UIManager} */
452
+ return {
453
+ renderList,
454
+ renderValue,
455
+ listEl,
456
+ toggleActionButtons,
457
+ focusDescriptionInput
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Setup the UI event listeners
463
+ * @param {Context} context - the context
464
+ */
465
+ function addEventListeners(context) {
466
+ const {
467
+ map
468
+ } = context;
469
+ map.on(EVENTS.mapReady, onMapReadyFactory(context));
470
+ }
471
+
472
+ /**
473
+ * Create the map and list containers and adds them to the DOM
474
+ * @param {HTMLTextAreaElement} geospatialInput - the textarea containing the geojson
475
+ * @param {number} index - the 0 based index
476
+ */
477
+ function createContainers(geospatialInput, index) {
478
+ const mapEl = document.createElement('div');
479
+ const mapId = `geospatialmap_${index}`;
480
+ mapEl.setAttribute('id', mapId);
481
+ mapEl.setAttribute('class', 'map-container');
482
+ const listEl = document.createElement('div');
483
+ const listId = `${mapId}_list`;
484
+ listEl.setAttribute('id', listId);
485
+ geospatialInput.after(mapEl);
486
+ mapEl.after(listEl);
487
+ geospatialInput.classList.add('js-hidden');
488
+ return {
489
+ mapEl,
490
+ listEl,
491
+ mapId
492
+ };
493
+ }
494
+
495
+ /**
496
+ * Callback factory function which fires when the map is ready
497
+ * @param {Context} context - the UI context
498
+ */
499
+ function onMapReadyFactory(context) {
500
+ const {
501
+ map,
502
+ activeFeatureManager,
503
+ uiManager,
504
+ interactPlugin,
505
+ drawPlugin
506
+ } = context;
507
+ const {
508
+ toggleActionButtons
509
+ } = uiManager;
510
+ const {
511
+ resetActiveFeature
512
+ } = activeFeatureManager;
513
+
514
+ /**
515
+ * Callback function which fires when the map is ready
516
+ * @param {object} e - the event
517
+ * @param {MapLibreMap} e.map - the map provider instance
518
+ */
519
+ return function onMapReady(e) {
520
+ // Add info panel
521
+ map.addPanel('info', helpPanelConfig);
522
+ map.addButton('btnAddPoint', {
523
+ variant: 'tertiary',
524
+ label: 'Add point',
525
+ iconSvgContent: POINT_SVG,
526
+ onClick: () => {
527
+ resetActiveFeature();
528
+ toggleActionButtons(true);
529
+ interactPlugin.enable();
530
+ },
531
+ mobile: {
532
+ slot: 'actions'
533
+ },
534
+ tablet: {
535
+ slot: 'actions'
536
+ },
537
+ desktop: {
538
+ slot: 'actions'
539
+ }
540
+ });
541
+ map.addButton('btnAddPolygon', {
542
+ variant: 'tertiary',
543
+ label: 'Add shape',
544
+ iconSvgContent: POLYGON_SVG,
545
+ onClick: () => {
546
+ resetActiveFeature();
547
+ toggleActionButtons(true);
548
+ drawPlugin.newPolygon(generateID(), polygonFeatureProperties);
549
+ },
550
+ mobile: {
551
+ slot: 'actions'
552
+ },
553
+ tablet: {
554
+ slot: 'actions'
555
+ },
556
+ desktop: {
557
+ slot: 'actions'
558
+ }
559
+ });
560
+ map.addButton('btnAddLine', {
561
+ variant: 'tertiary',
562
+ label: 'Add line',
563
+ iconSvgContent: LINE_SVG,
564
+ onClick: () => {
565
+ resetActiveFeature();
566
+ toggleActionButtons(true);
567
+ drawPlugin.newLine(generateID(), lineFeatureProperties);
568
+ },
569
+ mobile: {
570
+ slot: 'actions'
571
+ },
572
+ tablet: {
573
+ slot: 'actions'
574
+ },
575
+ desktop: {
576
+ slot: 'actions'
577
+ }
578
+ });
579
+
580
+ // Set the map provider on the context
581
+ context.mapProvider = e.map;
582
+ map.on(EVENTS.drawReady, onDrawReadyFactory(context));
583
+ map.on(EVENTS.drawCreated, onDrawCreatedFactory(context));
584
+ map.on(EVENTS.drawEdited, onDrawEditedFactory(context));
585
+ map.on(EVENTS.drawCancelled, onDrawCancelledFactory(context));
586
+ map.on(EVENTS.interactMarkerChange, onInteractMarkerChangedFactory(context));
587
+ const {
588
+ listEl
589
+ } = uiManager;
590
+ listEl.addEventListener('click', onListElClickFactory(context), false);
591
+ listEl.addEventListener('change', onListElChangeFactory(context), false);
592
+ };
593
+ }
594
+
595
+ /**
596
+ * Callback factory function which fires when the map draw plugin is ready
597
+ * @param {Context} context - the UI context
598
+ */
599
+ function onDrawReadyFactory(context) {
600
+ const {
601
+ featuresManager,
602
+ uiManager,
603
+ drawPlugin,
604
+ map
605
+ } = context;
606
+ const {
607
+ renderList
608
+ } = uiManager;
609
+ const {
610
+ getFeatures
611
+ } = featuresManager;
612
+
613
+ /**
614
+ * Callback when the draw plugin is ready
615
+ */
616
+ return function onDrawReady() {
617
+ getFeatures().forEach(feature => addFeatureToMap(feature, drawPlugin, map));
618
+
619
+ // Update the features
620
+ renderList();
621
+ };
622
+ }
623
+
624
+ /**
625
+ * Callback factory function which fires when the map draw plugin creates a new feature
626
+ * @param {Context} context - the UI context
627
+ */
628
+ function onDrawCreatedFactory(context) {
629
+ const {
630
+ featuresManager,
631
+ uiManager
632
+ } = context;
633
+ const {
634
+ addFeature
635
+ } = featuresManager;
636
+ const {
637
+ renderList,
638
+ toggleActionButtons,
639
+ focusDescriptionInput
640
+ } = uiManager;
641
+
642
+ /**
643
+ * Callback when a draw feature has been created
644
+ * @param {Feature} e
645
+ */
646
+ return function onDrawCreated(e) {
647
+ // New feature
648
+ addFeature({
649
+ ...e,
650
+ properties: {
651
+ description: ''
652
+ }
653
+ });
654
+
655
+ // Update the features
656
+ renderList();
657
+ toggleActionButtons(false);
658
+ focusDescriptionInput();
659
+ };
660
+ }
661
+
662
+ /**
663
+ * Callback factory function which fires when the map draw plugin edits a feature
664
+ * @param {Context} context - the UI context
665
+ */
666
+ function onDrawEditedFactory(context) {
667
+ const {
668
+ featuresManager,
669
+ activeFeatureManager,
670
+ uiManager
671
+ } = context;
672
+ const {
673
+ updateFeature
674
+ } = featuresManager;
675
+ const {
676
+ getActiveFeature,
677
+ resetActiveFeature
678
+ } = activeFeatureManager;
679
+ const {
680
+ renderList,
681
+ toggleActionButtons
682
+ } = uiManager;
683
+
684
+ /**
685
+ * Callback when a draw feature has been edited
686
+ * @param {{ id: string, geometry: Geometry }} e
687
+ */
688
+ return function onDrawEdited(e) {
689
+ const changedFeature = e;
690
+ const featureId = changedFeature.id;
691
+ if (getActiveFeature() === featureId) {
692
+ updateFeature(featureId, changedFeature.geometry);
693
+
694
+ // Update the features
695
+ renderList();
696
+ }
697
+ resetActiveFeature();
698
+ toggleActionButtons(false);
699
+ };
700
+ }
701
+
702
+ /**
703
+ * Callback factory function which fires when the map draw plugin cancels the editing of a feature
704
+ * @param {Context} context - the UI context
705
+ */
706
+ function onDrawCancelledFactory(context) {
707
+ const {
708
+ uiManager,
709
+ activeFeatureManager
710
+ } = context;
711
+ const {
712
+ toggleActionButtons
713
+ } = uiManager;
714
+ const {
715
+ resetActiveFeature
716
+ } = activeFeatureManager;
717
+
718
+ /**
719
+ * Callback when a draw feature has been cancelled
720
+ */
721
+ return function onDrawCancelled() {
722
+ toggleActionButtons(false);
723
+ resetActiveFeature();
724
+ };
725
+ }
726
+
727
+ /**
728
+ * Callback factory function that fires when an interact marker has been changed
729
+ * @param {Context} context - the UI context
730
+ */
731
+ function onInteractMarkerChangedFactory(context) {
732
+ const {
733
+ featuresManager,
734
+ activeFeatureManager,
735
+ map,
736
+ interactPlugin,
737
+ uiManager
738
+ } = context;
739
+ const {
740
+ addFeature,
741
+ updateFeature
742
+ } = featuresManager;
743
+ const {
744
+ getActiveFeature,
745
+ resetActiveFeature
746
+ } = activeFeatureManager;
747
+ const {
748
+ renderList,
749
+ focusDescriptionInput,
750
+ toggleActionButtons
751
+ } = uiManager;
752
+
753
+ /**
754
+ * Callback when an interact marker has been changed
755
+ * @param {{ coords: Coordinates }} e
756
+ */
757
+ return function onInteractMarkerChange(e) {
758
+ const activeFeatureId = getActiveFeature();
759
+ if (activeFeatureId) {
760
+ // Editing an existing point
761
+ const feature = updateFeature(activeFeatureId, {
762
+ type: 'Point',
763
+ coordinates: e.coords
764
+ });
765
+ map.addMarker(activeFeatureId, e.coords);
766
+ if (feature) {
767
+ // Update the features
768
+ renderList();
769
+ }
770
+ resetActiveFeature();
771
+ } else {
772
+ // Adding a new point
773
+ const id = generateID();
774
+ addFeature({
775
+ type: 'Feature',
776
+ properties: {
777
+ description: ''
778
+ },
779
+ geometry: {
780
+ type: 'Point',
781
+ coordinates: e.coords
782
+ },
783
+ id
784
+ });
785
+ map.addMarker(id, e.coords);
786
+
787
+ // Update the features
788
+ renderList();
789
+ focusDescriptionInput();
790
+ }
791
+ map.removeMarker('location');
792
+ interactPlugin.disable();
793
+ toggleActionButtons(false);
794
+ };
795
+ }
796
+
797
+ /**
798
+ * Callback factory function that fires a 'click' event is fired on the list container
799
+ * @param {Context} context - the UI context
800
+ */
801
+ function onListElClickFactory(context) {
802
+ const {
803
+ map,
804
+ mapProvider,
805
+ featuresManager,
806
+ activeFeatureManager,
807
+ interactPlugin,
808
+ uiManager,
809
+ drawPlugin
810
+ } = context;
811
+ const {
812
+ getFeature,
813
+ removeFeature
814
+ } = featuresManager;
815
+ const {
816
+ getActiveFeature,
817
+ setActiveFeature
818
+ } = activeFeatureManager;
819
+ const {
820
+ renderList,
821
+ toggleActionButtons
822
+ } = uiManager;
823
+
824
+ /**
825
+ * Delete a feature
826
+ * @param {string} id - the feature id
827
+ * @param {string} type - the feature type
828
+ */
829
+ function deleteFeature(id, type) {
830
+ if (type === 'Point') {
831
+ map.removeMarker(id);
832
+ removeFeature(id);
833
+ } else {
834
+ drawPlugin.deleteFeature(id);
835
+ removeFeature(id);
836
+ }
837
+ renderList();
838
+ }
839
+
840
+ /**
841
+ * Start editing feature
842
+ * @param {string} id - the feature id
843
+ * @param {string} type - the feature type
844
+ */
845
+ function editFeature(id, type) {
846
+ setActiveFeature(id);
847
+
848
+ // "Change" feature link was clicked
849
+ if (type === 'Point') {
850
+ interactPlugin.selectFeature({
851
+ featureId: id
852
+ });
853
+ interactPlugin.enable();
854
+ } else {
855
+ drawPlugin.editFeature(id);
856
+ }
857
+ const feature = getFeature(id);
858
+ if (feature && mapProvider) {
859
+ focusFeature(feature, mapProvider);
860
+ }
861
+ toggleActionButtons(true);
862
+ }
863
+
864
+ /**
865
+ * List container delegated 'click' events handler
866
+ * @param {MouseEvent} e
867
+ */
868
+ return function (e) {
869
+ const target = e.target;
870
+ if (!(target instanceof HTMLElement)) {
871
+ return;
872
+ }
873
+ if (getActiveFeature()) {
874
+ e.preventDefault();
875
+ e.stopPropagation();
876
+ return;
877
+ }
878
+ if (target.tagName === 'A' && target.dataset.action && target.dataset.id && target.dataset.type) {
879
+ const {
880
+ action,
881
+ id,
882
+ type
883
+ } = target.dataset;
884
+ if (action === 'edit') {
885
+ // "Update" feature link was clicked
886
+ editFeature(id, type);
887
+ } else {
888
+ e.preventDefault();
889
+ e.stopPropagation();
890
+ if (action === 'delete') {
891
+ // "Remove" feature link was clicked
892
+ deleteFeature(id, type);
893
+ }
894
+ }
895
+ }
896
+ };
897
+ }
898
+
899
+ /**
900
+ * Callback factory function that fires a 'change' event is fired on the list container
901
+ * @param {Context} context - the UI context
902
+ */
903
+ function onListElChangeFactory(context) {
904
+ const {
905
+ featuresManager,
906
+ uiManager
907
+ } = context;
908
+ const {
909
+ getFeature
910
+ } = featuresManager;
911
+ const {
912
+ renderValue
913
+ } = uiManager;
914
+
915
+ /**
916
+ * List container delegated 'change' events handler
917
+ * Used to update the description of features
918
+ * @param {Event} e
919
+ */
920
+ return function (e) {
921
+ e.preventDefault();
922
+ e.stopPropagation();
923
+ const target = e.target;
924
+ if (!(target instanceof HTMLInputElement) || !target.dataset.id) {
925
+ return;
926
+ }
927
+ const {
928
+ id
929
+ } = target.dataset;
930
+ const feature = getFeature(id);
931
+ if (feature) {
932
+ feature.properties.description = target.value.trim();
933
+ renderValue();
934
+ }
935
+ };
936
+ }
937
+
938
+ /**
939
+ * @import { MapsEnvironmentConfig, InteractiveMap } from '~/src/client/javascripts/map.js'
940
+ */
941
+
942
+ /**
943
+ * @import { Geometry, FeatureCollection, Feature, Coordinates } from '~/src/server/plugins/engine/types.js'
944
+ */
945
+
946
+ /**
947
+ * @typedef {object} GeoJSON
948
+ * @property {'FeatureCollection'} type - the GeoJSON type string
949
+ * @property {FeatureCollection} features - the features
950
+ */
951
+
952
+ /**
953
+ * Gets all the features
954
+ * @callback GetFeatures
955
+ * @returns {FeatureCollection}
956
+ */
957
+
958
+ /**
959
+ * Gets a feature from the geojson by id
960
+ * @callback GetFeature
961
+ * @param {string} id - the feature id
962
+ * @returns {Feature | undefined}
963
+ */
964
+
965
+ /**
966
+ * Add a feature to the geojson
967
+ * @callback AddFeature
968
+ * @param {Feature} feature - the feature to add
969
+ */
970
+
971
+ /**
972
+ * Update a feature in the geojson
973
+ * @callback UpdateFeature
974
+ * @param {string} id - the feature id
975
+ * @param {Geometry} geometry - the feature geometry
976
+ * @returns {Feature | undefined}
977
+ */
978
+
979
+ /**
980
+ * Removes a feature from the geojson
981
+ * @callback RemoveFeature
982
+ * @param {string} id - the feature id
983
+ */
984
+
985
+ /**
986
+ * Gets the active feature id
987
+ * @callback GetActiveFeature
988
+ * @returns {string | undefined}
989
+ */
990
+
991
+ /**
992
+ * Set the active feature id
993
+ * @callback SetActiveFeature
994
+ * @param {string} id - the feature id
995
+ * @returns {void}
996
+ */
997
+
998
+ /**
999
+ * Resets the active feature id
1000
+ * @callback ResetActiveFeature
1001
+ * @returns {void}
1002
+ */
1003
+
1004
+ /**
1005
+ * Renders the features into the list
1006
+ * @callback RenderList
1007
+ * @returns {void}
1008
+ */
1009
+
1010
+ /**
1011
+ * Renders the features JSON into the hidden textarea
1012
+ * @callback RenderValue
1013
+ * @returns {void}
1014
+ */
1015
+
1016
+ /**
1017
+ * Toggles the action button hidden state
1018
+ * @callback ToggleActionButtons
1019
+ * @param {boolean} hidden - whether to hide the action buttons
1020
+ * @returns {void}
1021
+ */
1022
+
1023
+ /**
1024
+ * Set focus to the last description input
1025
+ * @callback FocusDescriptionInput
1026
+ * @returns {void}
1027
+ */
1028
+
1029
+ /**
1030
+ * @typedef {object} FeaturesManager
1031
+ * @property {GetFeatures} getFeatures - function that gets all the features
1032
+ * @property {GetFeature} getFeature - function that gets a feature from the geojson
1033
+ * @property {AddFeature} addFeature - function that adds feature to the geojson
1034
+ * @property {UpdateFeature} updateFeature - function that updates a feature in the geojson
1035
+ * @property {RemoveFeature} removeFeature - function that removes a feature from the geojson
1036
+ */
1037
+
1038
+ /**
1039
+ * @typedef {object} ActiveFeatureManager
1040
+ * @property {GetActiveFeature} getActiveFeature - function that returns the current active feature id
1041
+ * @property {SetActiveFeature} setActiveFeature - function that sets the current active feature id
1042
+ * @property {ResetActiveFeature} resetActiveFeature - function that resets the current active feature id
1043
+ */
1044
+
1045
+ /**
1046
+ * @typedef {object} UIManager
1047
+ * @property {RenderValue} renderValue - function that renders the features JSON into the hidden textarea
1048
+ * @property {RenderList} renderList - function that renders the features into the list
1049
+ * @property {HTMLDivElement} listEl - the summary list of features
1050
+ * @property {ToggleActionButtons} toggleActionButtons - function that toggles the action buttons
1051
+ * @property {FocusDescriptionInput} focusDescriptionInput - function that sets focus to a description input element
1052
+ */
1053
+
1054
+ /**
1055
+ * @typedef {object} Context
1056
+ * @property {InteractiveMap} map - the interactive map
1057
+ * @property {MapLibreMap} [mapProvider] - the interactive map provider
1058
+ * @property {FeaturesManager} featuresManager - the features manager
1059
+ * @property {ActiveFeatureManager} activeFeatureManager - the active feature manager
1060
+ * @property {UIManager} uiManager - the UI manager
1061
+ * @property {any} interactPlugin - the map interact plugin
1062
+ * @property {any} drawPlugin - the map draw plugin
1063
+ */
1064
+
1065
+ /**
1066
+ * @import { MapLibreMap } from '~/src/client/javascripts/map.js'
1067
+ */
1068
+ //# sourceMappingURL=geospatial-map.js.map