@fieldia/map 0.14.0 → 0.15.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 (90) hide show
  1. package/README.md +74 -7
  2. package/package.json +5 -4
  3. package/src/index.d.mts +3 -0
  4. package/src/index.d.ts +3 -0
  5. package/src/index.js +5 -1
  6. package/src/index.js.map +1 -1
  7. package/src/index.mjs +2 -0
  8. package/src/index.mjs.map +1 -1
  9. package/src/lib/area.js +29 -246
  10. package/src/lib/area.js.map +1 -1
  11. package/src/lib/area.mjs +29 -246
  12. package/src/lib/area.mjs.map +1 -1
  13. package/src/lib/card.d.mts +45 -0
  14. package/src/lib/card.d.ts +45 -0
  15. package/src/lib/card.js +89 -0
  16. package/src/lib/card.js.map +1 -0
  17. package/src/lib/card.mjs +85 -0
  18. package/src/lib/card.mjs.map +1 -0
  19. package/src/lib/cluster.d.mts +12 -0
  20. package/src/lib/cluster.d.ts +12 -0
  21. package/src/lib/cluster.js +60 -0
  22. package/src/lib/cluster.js.map +1 -0
  23. package/src/lib/cluster.mjs +57 -0
  24. package/src/lib/cluster.mjs.map +1 -0
  25. package/src/lib/drawing.d.mts +36 -0
  26. package/src/lib/drawing.d.ts +36 -0
  27. package/src/lib/drawing.js +267 -0
  28. package/src/lib/drawing.js.map +1 -0
  29. package/src/lib/drawing.mjs +263 -0
  30. package/src/lib/drawing.mjs.map +1 -0
  31. package/src/lib/kinds.js +9 -2
  32. package/src/lib/kinds.js.map +1 -1
  33. package/src/lib/kinds.mjs +9 -2
  34. package/src/lib/kinds.mjs.map +1 -1
  35. package/src/lib/labels.d.mts +32 -0
  36. package/src/lib/labels.d.ts +32 -0
  37. package/src/lib/labels.js +88 -1
  38. package/src/lib/labels.js.map +1 -1
  39. package/src/lib/labels.mjs +86 -0
  40. package/src/lib/labels.mjs.map +1 -1
  41. package/src/lib/list-view.d.mts +13 -0
  42. package/src/lib/list-view.d.ts +13 -0
  43. package/src/lib/list-view.js +481 -0
  44. package/src/lib/list-view.js.map +1 -0
  45. package/src/lib/list-view.mjs +478 -0
  46. package/src/lib/list-view.mjs.map +1 -0
  47. package/src/lib/map-core.d.mts +4 -2
  48. package/src/lib/map-core.d.ts +4 -2
  49. package/src/lib/map-core.js +19 -7
  50. package/src/lib/map-core.js.map +1 -1
  51. package/src/lib/map-core.mjs +18 -7
  52. package/src/lib/map-core.mjs.map +1 -1
  53. package/src/lib/map-icons.d.mts +8 -0
  54. package/src/lib/map-icons.d.ts +8 -0
  55. package/src/lib/map-icons.js +21 -0
  56. package/src/lib/map-icons.js.map +1 -0
  57. package/src/lib/map-icons.mjs +18 -0
  58. package/src/lib/map-icons.mjs.map +1 -0
  59. package/src/lib/pins.d.mts +56 -0
  60. package/src/lib/pins.d.ts +56 -0
  61. package/src/lib/pins.js +161 -0
  62. package/src/lib/pins.js.map +1 -0
  63. package/src/lib/pins.mjs +154 -0
  64. package/src/lib/pins.mjs.map +1 -0
  65. package/src/lib/place.d.mts +1 -1
  66. package/src/lib/place.d.ts +1 -1
  67. package/src/lib/place.js +46 -6
  68. package/src/lib/place.js.map +1 -1
  69. package/src/lib/place.mjs +48 -8
  70. package/src/lib/place.mjs.map +1 -1
  71. package/src/lib/providers.d.mts +29 -8
  72. package/src/lib/providers.d.ts +29 -8
  73. package/src/lib/providers.js +217 -33
  74. package/src/lib/providers.js.map +1 -1
  75. package/src/lib/providers.mjs +217 -33
  76. package/src/lib/providers.mjs.map +1 -1
  77. package/src/lib/search-box.d.mts +4 -2
  78. package/src/lib/search-box.d.ts +4 -2
  79. package/src/lib/search-box.js +20 -6
  80. package/src/lib/search-box.js.map +1 -1
  81. package/src/lib/search-box.mjs +20 -6
  82. package/src/lib/search-box.mjs.map +1 -1
  83. package/src/lib/settings.d.mts +5 -2
  84. package/src/lib/settings.d.ts +5 -2
  85. package/src/lib/styles.d.mts +9 -1
  86. package/src/lib/styles.d.ts +9 -1
  87. package/src/lib/styles.js +146 -2
  88. package/src/lib/styles.js.map +1 -1
  89. package/src/lib/styles.mjs +144 -1
  90. package/src/lib/styles.mjs.map +1 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @fieldia/map
2
2
 
3
3
  [Fieldia](https://fieldia.dev)'s maps: a **place** and an **area** as fields of a
4
- form, on the map provider you choose. Built on [Leaflet](https://leafletjs.com),
5
- loaded only when a map is shown.
4
+ form, and a **list's records on a map**, on the map provider you choose. Built
5
+ on [Leaflet](https://leafletjs.com), loaded only when a map is shown.
6
6
 
7
7
  ```sh
8
8
  npm install @fieldia/map
@@ -53,6 +53,57 @@ the whole area drags to move it, and a point between two adds one.
53
53
  Options: `shapes` (any of polygon, box, circle), `areas` (named areas to choose
54
54
  instead of drawing), `height`.
55
55
 
56
+ ## A list on a map
57
+
58
+ A list page with `map` shows its records on a map too, beside the table or
59
+ instead of it, sharing its search and its records chosen: a pin lights its row
60
+ and a row its pin, a pin opens a card (a picture, a few fields, Choose, Open),
61
+ pins that crowd become a bubble with their count, and "Search this area" puts
62
+ the map's view into the search bar.
63
+
64
+ ```ts
65
+ import { mapListViews, azureMaps } from '@fieldia/map';
66
+
67
+ mountViewer(host, { page, dataSource, listViews: mapListViews({ provider: azureMaps({ key }) }) });
68
+ ```
69
+
70
+ ```json
71
+ "map": {
72
+ "latitude": "lat", "longitude": "lng",
73
+ "pin": { "label": "price", "tones": [{ "tone": "info", "when": "offer == 'rent'" }] },
74
+ "card": { "title": "name", "image": "photo", "fields": ["price", "bedrooms"] },
75
+ "view": "both"
76
+ }
77
+ ```
78
+
79
+ Where each record is: `point` (a json field holding a GeoJSON Point), or
80
+ `latitude` and `longitude`. A pin's `look` is `pin`, `label`, `icon`, `photo`
81
+ or `dot`; `label` names the field it says (an amount or a number written short,
82
+ "E£9.5M"), `icon` or `iconField` its icon (the map's own car, arrow, scooter,
83
+ bike, bus, person, shop, food and flag, Fieldia's, or the app's `icons`),
84
+ `image` its photo, `turn` a number field in degrees (a car seen from above
85
+ turns whole; other icons keep upright, a pointer on their edge turning), and
86
+ `tones` its colour by condition. `widget` names the app's own pin, given as
87
+ `mapListViews({ pins: { name: (record, { document }) => element } })`. `card`
88
+ takes `title`, `image`, `fields` and `open`; `cluster: false` draws every pin
89
+ on its own, though pins near one another then overlap and a pointer cannot
90
+ reach one under another (the keys still reach each); `view` is `list`, `map` or `both`; `limit` is 500 unless said.
91
+
92
+ The map's own tools draw an area to search in: a polygon, a box or a circle,
93
+ by mouse, finger or keys, as the area field draws one. It becomes a search of
94
+ its own ("In the area drawn"), shown on the map until it leaves the search bar.
95
+
96
+ "Search this area" asks for `between` on a latitude and a longitude, or the
97
+ `within` condition on a point: `{ south, west, north, east }` in degrees, west
98
+ beyond east across the antimeridian. An area drawn asks for `within` with
99
+ `{ polygon: [[lng, lat], …] }` or `{ center: [lng, lat], radius }` in metres;
100
+ on a latitude field, `longitude` names the field of its longitude. The memory
101
+ data source answers all of them; a server's adapter turns them into its own
102
+ query.
103
+
104
+ A form with only a place or an area never carries the list map's code, its
105
+ styles or its words.
106
+
56
107
  ## Without a mouse
57
108
 
58
109
  Tab reaches the map, which shows a cross at its centre: the arrow keys move the
@@ -63,6 +114,10 @@ arrow keys and leaves with Delete. A scroll passes over a map to the page
63
114
  unless Ctrl (⌘) is held, and on a phone one finger scrolls the page while two
64
115
  move the map.
65
116
 
117
+ On a list's map, Tab goes from the map to each pin and bubble by its name;
118
+ Enter or Space opens a pin's card (Ctrl or ⌘ with it chooses the record) or
119
+ zooms into a bubble, and Escape closes the card, back on its pin.
120
+
66
121
  ## Providers
67
122
 
68
123
  | Provider | Road | Dark | Satellite | Address search |
@@ -71,15 +126,22 @@ move the map.
71
126
  | `googleMaps({ key, region, logo })` (Map Tiles API) | yes | no | yes | the app's own (`search`) |
72
127
  | `mapbox({ token })` | yes | yes | yes | yes, as people type |
73
128
  | `tiles({ url, credit })` | the app's own map images | | | the app's own |
74
- | `openStreetMap()` | yes, **for demos only** | no | no | Nominatim, on Enter |
129
+ | `openStreetMap()` | yes, **for demos only** | no | no | Photon, as people type, nearest first (or `{ search: 'nominatim' }`, on Enter) |
75
130
 
76
131
  Keys never live in Fieldia: pass one restricted to your sites, or a function
77
132
  fetching a short-lived token from your server. Each provider's credits are shown
78
133
  in the map's corner, and its mark where its terms ask. Read your provider's
79
134
  terms on keeping search results: a place field keeps what it finds (Mapbox's
80
- permanent geocoding is asked for by default). OpenStreetMap's public map images
81
- and Nominatim are for trying things out: their policies treat an app in use as
82
- heavy use.
135
+ permanent geocoding is asked for by default). OpenStreetMap's public map images,
136
+ Photon's public server and Nominatim are for trying things out: their policies
137
+ treat an app in use as heavy use. Photon can run on your own server
138
+ (`openStreetMap({ photonUrl })`).
139
+
140
+ Suggestions come as people type where the provider allows it, nearest where the
141
+ map is looking first, each on two lines (the place, then where it is); Enter
142
+ takes the first. "My location" says how sure it is: a rough location (a
143
+ desktop's, placed by its network) shows the circle it may be anywhere in, says
144
+ how wide, and names no address from it.
83
145
 
84
146
  Pass your own `search` (and `reverse`) to `mapWidgets` to use your own address
85
147
  search with any provider.
@@ -87,6 +149,11 @@ search with any provider.
87
149
  ## In the designer
88
150
 
89
151
  `mapKinds({ locale })` gives the designer Place and Area for its toolbox, with
90
- their settings; pass the map widgets to the designer and its editors too.
152
+ their settings (a place's address search, "My location", the field its address
153
+ goes into, the map's height; an area's shapes); pass the map widgets to the
154
+ designer and its editors too. A list page's panel has its Map: where each
155
+ record is, its pins, its card, and how the list opens. Give the screen editor
156
+ `listViews` (and `sampleCentre`, where its made-up records are) and Try it shows
157
+ the map too.
91
158
 
92
159
  MIT. Leaflet is BSD-2-Clause; its stylesheet is included with its licence.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fieldia/map",
3
- "version": "0.14.0",
4
- "description": "Fieldia's maps: a place and an area as fields, on any map provider. Leaflet, loaded only where a map is shown.",
3
+ "version": "0.15.0",
4
+ "description": "Fieldia's maps: a place and an area as fields, and a list's records on a map, on any map provider. Leaflet, loaded only where a map is shown.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://fieldia.dev",
7
7
  "type": "commonjs",
@@ -21,8 +21,9 @@
21
21
  "./package.json": "./package.json"
22
22
  },
23
23
  "dependencies": {
24
- "@fieldia/core": "0.14.0",
25
- "@fieldia/widgets": "0.14.0",
24
+ "@fieldia/core": "0.15.0",
25
+ "@fieldia/viewer": "0.15.0",
26
+ "@fieldia/widgets": "0.15.0",
26
27
  "leaflet": "^1.9.4",
27
28
  "tslib": "^2.8.1"
28
29
  },
package/src/index.d.mts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { mapWidgets } from './lib/widgets.mjs';
2
+ export { mapListViews } from './lib/list-view.mjs';
3
+ export { MAP_ICONS } from './lib/map-icons.mjs';
4
+ export type { MapRecord, PinDrawer } from './lib/pins.mjs';
2
5
  export { mapKinds, type MapKind, type MapKindContext } from './lib/kinds.mjs';
3
6
  export type { MapSettings } from './lib/settings.mjs';
4
7
  export { azureMaps, googleMaps, mapbox, openStreetMap, tiles, type AddressSearch, type AzureMapsOptions, type BaseMap, type Credit, type GoogleMapsOptions, type MapboxOptions, type MapProvider, type MapView, type OpenStreetMapOptions, type PlaceFound, type ReverseSearch, type SearchRequest, type TileSpec, type TilesOptions, } from './lib/providers.mjs';
package/src/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { mapWidgets } from './lib/widgets';
2
+ export { mapListViews } from './lib/list-view';
3
+ export { MAP_ICONS } from './lib/map-icons';
4
+ export type { MapRecord, PinDrawer } from './lib/pins';
2
5
  export { mapKinds, type MapKind, type MapKindContext } from './lib/kinds';
3
6
  export type { MapSettings } from './lib/settings';
4
7
  export { azureMaps, googleMaps, mapbox, openStreetMap, tiles, type AddressSearch, type AzureMapsOptions, type BaseMap, type Credit, type GoogleMapsOptions, type MapboxOptions, type MapProvider, type MapView, type OpenStreetMapOptions, type PlaceFound, type ReverseSearch, type SearchRequest, type TileSpec, type TilesOptions, } from './lib/providers';
package/src/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MAP_LABELS = exports.metresBetween = exports.writePoint = exports.writeArea = exports.readPoint = exports.readArea = exports.tiles = exports.openStreetMap = exports.mapbox = exports.googleMaps = exports.azureMaps = exports.mapKinds = exports.mapWidgets = void 0;
3
+ exports.MAP_LABELS = exports.metresBetween = exports.writePoint = exports.writeArea = exports.readPoint = exports.readArea = exports.tiles = exports.openStreetMap = exports.mapbox = exports.googleMaps = exports.azureMaps = exports.mapKinds = exports.MAP_ICONS = exports.mapListViews = exports.mapWidgets = void 0;
4
4
  var widgets_1 = require("./lib/widgets");
5
5
  Object.defineProperty(exports, "mapWidgets", { enumerable: true, get: function () { return widgets_1.mapWidgets; } });
6
+ var list_view_1 = require("./lib/list-view");
7
+ Object.defineProperty(exports, "mapListViews", { enumerable: true, get: function () { return list_view_1.mapListViews; } });
8
+ var map_icons_1 = require("./lib/map-icons");
9
+ Object.defineProperty(exports, "MAP_ICONS", { enumerable: true, get: function () { return map_icons_1.MAP_ICONS; } });
6
10
  var kinds_1 = require("./lib/kinds");
7
11
  Object.defineProperty(exports, "mapKinds", { enumerable: true, get: function () { return kinds_1.mapKinds; } });
8
12
  var providers_1 = require("./lib/providers");
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":";;;AAAA,yCAA2C;AAAlC,qGAAA,UAAU,OAAA;AACnB,qCAA0E;AAAjE,iGAAA,QAAQ,OAAA;AAEjB,6CAoByB;AAnBvB,sGAAA,SAAS,OAAA;AACT,uGAAA,UAAU,OAAA;AACV,mGAAA,MAAM,OAAA;AACN,0GAAA,aAAa,OAAA;AACb,kGAAA,KAAK,OAAA;AAgBP,iCAAgK;AAAvJ,+FAAA,QAAQ,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,iGAAA,UAAU,OAAA;AAAE,oGAAA,aAAa,OAAA;AAClE,uCAA0D;AAAjD,oGAAA,UAAU,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":";;;AAAA,yCAA2C;AAAlC,qGAAA,UAAU,OAAA;AACnB,6CAA+C;AAAtC,yGAAA,YAAY,OAAA;AACrB,6CAA4C;AAAnC,sGAAA,SAAS,OAAA;AAElB,qCAA0E;AAAjE,iGAAA,QAAQ,OAAA;AAEjB,6CAoByB;AAnBvB,sGAAA,SAAS,OAAA;AACT,uGAAA,UAAU,OAAA;AACV,mGAAA,MAAM,OAAA;AACN,0GAAA,aAAa,OAAA;AACb,kGAAA,KAAK,OAAA;AAgBP,iCAAgK;AAAvJ,+FAAA,QAAQ,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,gGAAA,SAAS,OAAA;AAAE,iGAAA,UAAU,OAAA;AAAE,oGAAA,aAAa,OAAA;AAClE,uCAA0D;AAAjD,oGAAA,UAAU,OAAA"}
package/src/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  export { mapWidgets } from './lib/widgets.mjs';
2
+ export { mapListViews } from './lib/list-view.mjs';
3
+ export { MAP_ICONS } from './lib/map-icons.mjs';
2
4
  export { mapKinds } from './lib/kinds.mjs';
3
5
  export { azureMaps, googleMaps, mapbox, openStreetMap, tiles, } from './lib/providers.mjs';
4
6
  export { readArea, readPoint, writeArea, writePoint, metresBetween } from './lib/geo.mjs';
package/src/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAqC,MAAM,aAAa,CAAC;AAE1E,OAAO,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACb,KAAK,GAeN,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAA4E,MAAM,WAAW,CAAC;AAChK,OAAO,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../../libs/map/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAqC,MAAM,aAAa,CAAC;AAE1E,OAAO,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACb,KAAK,GAeN,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAA4E,MAAM,WAAW,CAAC;AAChK,OAAO,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC"}
package/src/lib/area.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.areaWidget = areaWidget;
4
+ const drawing_1 = require("./drawing");
4
5
  const geo_1 = require("./geo");
5
6
  const labels_1 = require("./labels");
6
7
  const map_core_1 = require("./map-core");
@@ -22,15 +23,6 @@ const map_core_1 = require("./map-core");
22
23
  * `height`.
23
24
  */
24
25
  const ALL = ['polygon', 'box', 'circle'];
25
- const ICONS = {
26
- polygon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 8l7-5 7 6-3 11H7z"/></svg>',
27
- box: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="6" width="16" height="12" rx="1"/></svg>',
28
- circle: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>',
29
- };
30
- /** How near the first point a press closes the ring: a fingertip's width, not the mark's. */
31
- const CLOSE_PX = 22;
32
- /** A press that moves less than this is a press, not a drag. */
33
- const DRAG_PX = 4;
34
26
  const same = (a, b) => JSON.stringify(a && (0, geo_1.writeArea)(a)) === JSON.stringify(b && (0, geo_1.writeArea)(b));
35
27
  function areaWidget(settings) {
36
28
  return (context) => {
@@ -47,11 +39,6 @@ function areaWidget(settings) {
47
39
  : [];
48
40
  let readonly = false;
49
41
  let area = null;
50
- let mode = null;
51
- /** A polygon's points so far. */
52
- let points = [];
53
- /** Where a box or a circle began, by pointer or by Space. */
54
- let anchor = null;
55
42
  const element = doc.createElement('div');
56
43
  element.className = 'fd-map-field fd-area';
57
44
  // The field's label names the group, so each tool keeps its own name ("Polygon", not "Area").
@@ -85,9 +72,9 @@ function areaWidget(settings) {
85
72
  button.type = 'button';
86
73
  button.className = 'fd-button';
87
74
  button.setAttribute('aria-pressed', 'false');
88
- button.innerHTML = ICONS[shape];
75
+ button.innerHTML = drawing_1.SHAPE_ICONS[shape];
89
76
  button.append(labels[shape]);
90
- button.addEventListener('click', () => arm(mode === shape ? null : shape));
77
+ button.addEventListener('click', () => drawing.arm(drawing.mode === shape ? null : shape));
91
78
  toolButtons.set(shape, button);
92
79
  tools.append(button);
93
80
  });
@@ -111,7 +98,7 @@ function areaWidget(settings) {
111
98
  const chosen = choice ? (0, geo_1.readArea)(choice.value) : null;
112
99
  if (!chosen)
113
100
  return;
114
- arm(null);
101
+ drawing.arm(null);
115
102
  commit(chosen, true);
116
103
  });
117
104
  tools.append(chooser);
@@ -122,31 +109,30 @@ function areaWidget(settings) {
122
109
  clear.textContent = labels.clear;
123
110
  clear.hidden = true;
124
111
  clear.addEventListener('click', () => {
125
- arm(null);
112
+ drawing.arm(null);
126
113
  commit(null);
127
114
  announce(labels.shapeCleared);
128
115
  (toolButtons.get(shapes[0]) ?? box.element).focus();
129
116
  });
130
117
  tools.append(clear);
131
- // The bar over the map while a tool is in hand: what to do, and the ways out.
132
- const hint = doc.createElement('div');
133
- hint.className = 'fd-map-hint';
134
- hint.hidden = true;
135
- const hintWords = doc.createElement('p');
136
- const finishButton = doc.createElement('button');
137
- finishButton.type = 'button';
138
- finishButton.className = 'fd-button fd-button-primary';
139
- finishButton.textContent = labels.finish;
140
- finishButton.addEventListener('click', () => finish());
141
- const cancelButton = doc.createElement('button');
142
- cancelButton.type = 'button';
143
- cancelButton.className = 'fd-button';
144
- cancelButton.textContent = labels.cancel;
145
- cancelButton.addEventListener('click', () => arm(null));
146
- hint.append(hintWords, finishButton, cancelButton);
147
- box.element.append(hint);
148
- box.shield(hint);
149
- box.follow(hint);
118
+ // Drawing a new shape: the tools' buttons say which is in hand; the shape drawn becomes the value.
119
+ const drawing = (0, drawing_1.shapeDrawing)({
120
+ box,
121
+ labels,
122
+ keys: element,
123
+ announce,
124
+ armed: (mode) => {
125
+ for (const [shape, button] of toolButtons)
126
+ button.setAttribute('aria-pressed', String(shape === mode));
127
+ draw();
128
+ },
129
+ done: (drawn) => {
130
+ commit(drawn);
131
+ announce(labels.shapeDrawn);
132
+ },
133
+ escaped: () => (toolButtons.get(shapes[0]) ?? box.element).focus(),
134
+ finishesOn: (target) => target === toolButtons.get('polygon'),
135
+ });
150
136
  element.append(tools, box.element, live);
151
137
  // ---- the value -------------------------------------------------------------
152
138
  function commit(next, fit = false) {
@@ -159,12 +145,9 @@ function areaWidget(settings) {
159
145
  let map = null;
160
146
  let L = null;
161
147
  let shapeLayer = null;
162
- let trace = null;
163
148
  const grips = [];
164
149
  /** The grip under the hand: the others follow it, it follows the hand. */
165
150
  let held = null;
166
- /** A polygon's points while it is drawn, and the line reaching for the pointer. */
167
- const marks = [];
168
151
  function clearLayers() {
169
152
  shapeLayer?.remove();
170
153
  shapeLayer = null;
@@ -173,15 +156,15 @@ function areaWidget(settings) {
173
156
  }
174
157
  function layerFor(shape, className) {
175
158
  return shape.shape === 'circle'
176
- ? L.circle([shape.center.lat, shape.center.lng], { radius: shape.radius, className, interactive: !readonly && !mode })
177
- : L.polygon(shape.points.map((p) => [p.lat, p.lng]), { className, interactive: !readonly && !mode });
159
+ ? L.circle([shape.center.lat, shape.center.lng], { radius: shape.radius, className, interactive: !readonly && !drawing.mode })
160
+ : L.polygon(shape.points.map((p) => [p.lat, p.lng]), { className, interactive: !readonly && !drawing.mode });
178
161
  }
179
162
  /** The shape as it now is, and its grips; `fit` brings it into view. */
180
163
  function draw(fit = false) {
181
164
  if (!map || !L)
182
165
  return;
183
166
  clearLayers();
184
- if (!area || mode)
167
+ if (!area || drawing.mode)
185
168
  return;
186
169
  shapeLayer = layerFor(area, `fd-map-shape${readonly ? '' : ' fd-map-movable'}`).addTo(map);
187
170
  if (!readonly) {
@@ -327,7 +310,7 @@ function areaWidget(settings) {
327
310
  if (!el)
328
311
  return;
329
312
  el.addEventListener('pointerdown', (event) => {
330
- if (!map || !area || readonly || mode || event.button !== 0)
313
+ if (!map || !area || readonly || drawing.mode || event.button !== 0)
331
314
  return;
332
315
  event.stopPropagation();
333
316
  const start = map.mouseEventToLatLng(event);
@@ -356,210 +339,10 @@ function areaWidget(settings) {
356
339
  });
357
340
  }
358
341
  // ---- the tools ---------------------------------------------------------------
359
- function sayStep() {
360
- if (!mode)
361
- return;
362
- const words = mode === 'polygon' ? labels.drawPolygon : mode === 'box' ? labels.drawBox : labels.drawCircle;
363
- hintWords.textContent = mode === 'polygon' && points.length ? `${words} ${(0, labels_1.fill)(labels.points, { n: points.length })}.` : words;
364
- finishButton.hidden = mode !== 'polygon' || points.length < 3;
365
- }
366
- function arm(next) {
367
- if (readonly)
368
- next = null;
369
- mode = next;
370
- points = [];
371
- anchor = null;
372
- trace?.remove();
373
- trace = null;
374
- remark();
375
- for (const [shape, button] of toolButtons)
376
- button.setAttribute('aria-pressed', String(shape === mode));
377
- hint.hidden = !mode;
378
- // Every tool holds the ground still: a press is a point, never a pan.
379
- box.setPannable(!mode);
380
- box.ready((m) => (mode ? m.doubleClickZoom.disable() : m.doubleClickZoom.enable()));
381
- box.element.dataset['drawing'] = mode ?? '';
382
- if (!mode)
383
- delete box.element.dataset['drawing'];
384
- sayStep();
385
- draw();
386
- if (mode)
387
- announce(hintWords.textContent ?? '');
388
- }
389
- /** The marks of a polygon's points: made again only when the points change, never as the pointer moves. */
390
- function remark() {
391
- for (const m of marks.splice(0))
392
- m.remove();
393
- if (!map || !L || mode !== 'polygon')
394
- return;
395
- points.forEach((p, i) => {
396
- const mark = L.marker([p.lat, p.lng], { icon: gripIcon(), interactive: i === 0, keyboard: false }).addTo(map);
397
- // Back on the first point closes the ring.
398
- if (i === 0)
399
- mark.on('click', () => points.length >= 3 && finish());
400
- marks.push(mark);
401
- });
402
- }
403
- /** The outline being drawn, toward the pointer: changed in place as it moves. */
404
- function retrace(toward) {
405
- if (!map || !L)
406
- return;
407
- let ring = null;
408
- let circle = null;
409
- if (mode === 'polygon') {
410
- ring = [...points, ...(toward ? [toward] : [])];
411
- if (ring.length < 2)
412
- ring = null;
413
- marks[0]?.getElement()?.toggleAttribute('data-closes', nearFirst(toward));
414
- }
415
- else if (anchor && toward) {
416
- if (mode === 'box')
417
- ring = (0, geo_1.boxFrom)(anchor, toward);
418
- else
419
- circle = { center: anchor, radius: (0, geo_1.metresBetween)(anchor, toward) };
420
- }
421
- const latLngs = ring?.map((p) => [p.lat, p.lng]);
422
- if (trace && latLngs && trace instanceof L.Polygon && !(trace instanceof L.Rectangle)) {
423
- trace.setLatLngs(latLngs);
424
- return;
425
- }
426
- if (trace && circle && trace instanceof L.Circle) {
427
- trace.setLatLng([circle.center.lat, circle.center.lng]);
428
- trace.setRadius(circle.radius);
429
- return;
430
- }
431
- trace?.remove();
432
- trace = null;
433
- if (latLngs)
434
- trace = L.polygon(latLngs, { className: 'fd-map-trace', interactive: false }).addTo(map);
435
- else if (circle)
436
- trace = L.circle([circle.center.lat, circle.center.lng], { radius: circle.radius, className: 'fd-map-trace', interactive: false }).addTo(map);
437
- }
438
- function nearFirst(at) {
439
- if (!map || !at || points.length < 3)
440
- return false;
441
- return map.latLngToContainerPoint([points[0].lat, points[0].lng]).distanceTo(map.latLngToContainerPoint([at.lat, at.lng])) <= CLOSE_PX;
442
- }
443
- function addPoint(at) {
444
- if (mode === 'polygon') {
445
- if (nearFirst(at))
446
- return finish();
447
- points.push(at);
448
- sayStep();
449
- remark();
450
- retrace();
451
- announce((0, labels_1.fill)(labels.points, { n: points.length }));
452
- return;
453
- }
454
- if (!anchor) {
455
- anchor = at;
456
- retrace(at);
457
- return;
458
- }
459
- const end = at;
460
- const start = anchor;
461
- if (mode === 'box')
462
- complete({ shape: 'box', points: (0, geo_1.boxFrom)(start, end) });
463
- else if (mode === 'circle')
464
- complete({ shape: 'circle', center: start, radius: Math.max(1, (0, geo_1.metresBetween)(start, end)) });
465
- }
466
- function finish() {
467
- if (mode !== 'polygon' || points.length < 3)
468
- return;
469
- complete({ shape: 'polygon', points: [...points] });
470
- }
471
- function complete(drawn) {
472
- mode = null;
473
- arm(null);
474
- commit(drawn);
475
- announce(labels.shapeDrawn);
476
- }
477
342
  box.ready((m, leaflet) => {
478
343
  map = m;
479
344
  L = leaflet;
480
345
  draw(!!area);
481
- m.on('click', (event) => {
482
- if (mode !== 'polygon' || readonly)
483
- return;
484
- addPoint({ lat: event.latlng.lat, lng: event.latlng.lng });
485
- });
486
- m.on('dblclick', () => {
487
- if (mode !== 'polygon')
488
- return;
489
- // The two clicks of a double-click have each placed a point, on top of each other: one is enough.
490
- const [a, b] = points.slice(-2);
491
- if (a && b && m.latLngToContainerPoint([a.lat, a.lng]).distanceTo(m.latLngToContainerPoint([b.lat, b.lng])) < 6) {
492
- points.pop();
493
- remark();
494
- }
495
- finish();
496
- });
497
- m.on('mousemove', (event) => {
498
- if (mode === 'polygon' && points.length)
499
- retrace({ lat: event.latlng.lat, lng: event.latlng.lng });
500
- });
501
- // A box and a circle are drawn by dragging: pointer events, which a mouse, a finger and a pen all raise.
502
- let pressed = null;
503
- box.element.addEventListener('pointerdown', (event) => {
504
- if ((mode !== 'box' && mode !== 'circle') || readonly || event.button !== 0)
505
- return;
506
- if (event.target.closest('.fd-map-hint, .fd-map-zoom, .fd-map-credits, .fd-map-logo'))
507
- return;
508
- const at = m.mouseEventToLatLng(event);
509
- pressed = { x: event.clientX, y: event.clientY, at: { lat: at.lat, lng: at.lng }, id: event.pointerId };
510
- anchor = pressed.at;
511
- box.element.setPointerCapture?.(event.pointerId);
512
- });
513
- box.element.addEventListener('pointermove', (event) => {
514
- if (!pressed || event.pointerId !== pressed.id)
515
- return;
516
- const at = m.mouseEventToLatLng(event);
517
- retrace({ lat: at.lat, lng: at.lng });
518
- });
519
- const release = (event) => {
520
- if (!pressed || event.pointerId !== pressed.id)
521
- return;
522
- const moved = Math.hypot(event.clientX - pressed.x, event.clientY - pressed.y);
523
- const at = m.mouseEventToLatLng(event);
524
- pressed = null;
525
- // A press with no drag is no shape: the tool stays in hand.
526
- if (event.type === 'pointercancel' || moved < DRAG_PX) {
527
- anchor = null;
528
- retrace();
529
- return;
530
- }
531
- addPoint({ lat: at.lat, lng: at.lng });
532
- };
533
- box.element.addEventListener('pointerup', release);
534
- box.element.addEventListener('pointercancel', release);
535
- });
536
- // The keys of drawing, wherever in the field the focus is.
537
- element.addEventListener('keydown', (event) => {
538
- if (!mode)
539
- return;
540
- const onMap = event.target === box.element;
541
- if (event.key === 'Escape') {
542
- event.preventDefault();
543
- arm(null);
544
- (toolButtons.get(shapes[0]) ?? box.element).focus();
545
- }
546
- else if (event.key === 'Enter' && mode === 'polygon' && points.length >= 3 && (onMap || event.target === finishButton || toolButtons.get('polygon') === event.target)) {
547
- event.preventDefault();
548
- finish();
549
- }
550
- else if (event.key === 'Backspace' && mode === 'polygon' && points.length && onMap) {
551
- event.preventDefault();
552
- points.pop();
553
- sayStep();
554
- remark();
555
- retrace();
556
- announce((0, labels_1.fill)(labels.points, { n: points.length }));
557
- }
558
- else if (event.key === ' ' && onMap && map) {
559
- event.preventDefault();
560
- const c = map.getCenter();
561
- addPoint({ lat: c.lat, lng: c.lng });
562
- }
563
346
  });
564
347
  return {
565
348
  element,
@@ -569,8 +352,8 @@ function areaWidget(settings) {
569
352
  const wasReadonly = readonly;
570
353
  readonly = state.readonly;
571
354
  tools.hidden = readonly;
572
- if (readonly && mode)
573
- arm(null);
355
+ if (readonly && drawing.mode)
356
+ drawing.arm(null);
574
357
  const next = (0, geo_1.readArea)(state.value);
575
358
  const outside = !same(next, area);
576
359
  if (outside)