Skip to content

Commit e5f6d14

Browse files
committed
Implement search control UI
Make icons a bit bolder to match the search icon which is paths of a certain width Add disabled support to search control. Bound to checked, but not bound to hidden. Update linux screenshots due to bolding of zoom, reload controls to match magnifying glass search control icon boldness. Remove now out of date win32 screenshots for same reason Make calculation of total size of controls omit the search control mostly, for backwards compatibility Add waitUntil: 'networkidle', increase some timeouts to de-flake some tests Implement custom search handler. Use geoname search as example. Add search example using geonames.org Search panel grabs focus on mouseenter Add localization messages for search button. Add localization and non-Latin character search tests. Add second search layer to index.html mapml-viewer (multiple search link handler) Change search default - clicking a suggestion triggers a search, navigation - panel stays open until user dismisses it - panel now requires 2 Esc / clicks to close it - update broken tests Simplify search events' API Add HTMLMapmlViewerElement.zoomToExtent(xmin,ymin,xmax,ymax) to support documentation change for custom search handler development De-flake a test Update markup skills to add search controlslist token De-flake a couple more tests Update win32 screen caps to account for bold control icons to match search icon boldness Clean up
1 parent decb9cc commit e5f6d14

87 files changed

Lines changed: 2704 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/skills/map-link-markup/SKILL.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ defines several uses of existing and new `rel` keyword values.
3737
| `zoomout` | The link `href` is followed automatically by the polyfill when the map is zoomed out by the user to a value less than the minimum value of the zoom range of the current layer. The referenced map layer resource replaces the current map layer. The polyfill does not represent this link as a user-visible affordance, it is followed automatically. If the remote resource does not contain a reciprocal `zoomin` link, the map state change is one-way i.e. the layer is permanently replaced. |
3838
| `legend` | The `legend` link relation designates a link to metadata, typically an image, describing the symbology used by the current layer. Currently, the polyfill creates a hyperlink for the label of the layer in the layer control, which opens in a new browsing context. |
3939
| `query` | The `query` link relation is used in combination with the `tref="..."` attribute to establish a URL template that composes a map query URL based on user map gestures such as click or touch. These URLs are fetched and the response presented on top of the map as a popup. Such queries can return text/html or text/mapml responses. In the latter case, the response may contain more than one feature, in which case a 'paged' popup is generated, allowing the user to cycle through the features' individual metadata. |
40+
| `search` | The `search` link relation is used with the `tref="..."` attribute to define a URL template for a search endpoint. The template must contain the `{searchTerms}` variable reference, which is replaced with the user's URL-encoded search query. The search is triggered when the user presses Enter or clicks a suggestion. The response is expected to be a GeoJSON `FeatureCollection` (the default handler format). Only the first `<map-link rel="search">` per `<map-layer>` is honored. The link must be a direct child of `<map-layer>` (for local/inline layers) or placed inside `<map-head>` (for remote `.mapml` layers). The search control is opt-in via `controlslist="search"` on the `<mapml-viewer>` or `<map is="web-map">` element. The search button is disabled when no visible (checked) layer provides a `<map-link rel="search">`. |
41+
| `suggestions` | The `suggestions` link relation is used with the `tref="..."` attribute to define a URL template for a suggestions/autocomplete endpoint. Like `search`, the template must contain the `{searchTerms}` variable reference. Suggestions are fetched automatically as the user types (debounced, minimum 2 characters). The default handler expects a GeoJSON `FeatureCollection` response, rendering each feature as a clickable result button (using `properties.display_name` or `properties.name`). Only the first `<map-link rel="suggestions">` per `<map-layer>` is honored. Suggestions are optional — if no `rel="suggestions"` link exists, only Enter triggers a search. |
4042
| `stylesheet` | The link imports a CSS stylesheet from the `href` value. |
4143

4244

@@ -135,4 +137,206 @@ Projection values [defined by the polyfill](../mapml-viewer#projection) include:
135137
</map-extent>
136138
</layer->
137139
</mapml-viewer>
140+
```
141+
142+
### Search and Suggestions
143+
144+
The search control is opt-in: add `controlslist="search"` to your `<mapml-viewer>`
145+
or `<map is="web-map">` element. A magnifying-glass button appears in the
146+
top-left controls. The button is disabled (grayed out, `aria-disabled="true"`)
147+
when no visible (checked) `<map-layer>` has a descendant `<map-link rel="search">`.
148+
149+
The `{searchTerms}` template variable in `tref` is the only required variable.
150+
It is replaced with the user's URL-encoded query string. No sibling
151+
`<map-input>` elements are needed for search/suggestions links.
152+
153+
#### Placement rules
154+
155+
- **Inline (local) layers:** place `<map-link rel="search">` and
156+
`<map-link rel="suggestions">` as direct children of `<map-layer>`.
157+
- **Remote layers (`.mapml` files):** place them inside `<map-head>`.
158+
- Do **not** place search/suggestions links inside `<map-extent>`.
159+
- Only the **first** `<map-link rel="search">` and first
160+
`<map-link rel="suggestions">` per `<map-layer>` are honored.
161+
- Multiple layers may each contribute their own search/suggestions links;
162+
responses are merged.
163+
164+
#### Default handler format (GeoJSON)
165+
166+
The default handler expects a **GeoJSON `FeatureCollection`** response from
167+
both search and suggestions endpoints. Each `Feature` should include:
168+
169+
- `geometry` with coordinates (used for `setView` fallback)
170+
- `bbox` (4-element array `[west, south, east, north]`; used for `fitBounds`)
171+
- `properties.display_name` or `properties.name` (rendered as button text)
172+
173+
This format is compatible with Nominatim (`format=geojson`) and Photon out of
174+
the box. Example minimal response:
175+
176+
```json
177+
{
178+
"type": "FeatureCollection",
179+
"features": [
180+
{
181+
"type": "Feature",
182+
"bbox": [-75.76, 45.35, -75.62, 45.46],
183+
"geometry": { "type": "Point", "coordinates": [-75.69, 45.42] },
184+
"properties": { "display_name": "Ottawa, Ontario, Canada" }
185+
}
186+
]
187+
}
188+
```
189+
190+
#### Default handler — inline layer example
191+
192+
```html
193+
<mapml-viewer projection="OSMTILE" zoom="14" lat="45.4" lon="-75.7"
194+
controls controlslist="search">
195+
<map-layer label="OpenStreetMap" checked>
196+
<!-- suggestions fetched as the user types (debounced, min 2 chars) -->
197+
<map-link rel="suggestions"
198+
tref="https://nominatim.openstreetmap.org/search?q={searchTerms}&format=geojson&limit=5"></map-link>
199+
<!-- search fetched on Enter -->
200+
<map-link rel="search"
201+
tref="https://nominatim.openstreetmap.org/search?q={searchTerms}&format=geojson&limit=10"></map-link>
202+
<map-extent units="OSMTILE" checked>
203+
<map-input name="z" type="zoom" min="0" max="18"></map-input>
204+
<map-input name="x" type="location" units="tilematrix" axis="column"></map-input>
205+
<map-input name="y" type="location" units="tilematrix" axis="row"></map-input>
206+
<map-link rel="tile" tref="https://tile.openstreetmap.org/{z}/{x}/{y}.png"></map-link>
207+
</map-extent>
208+
</map-layer>
209+
</mapml-viewer>
210+
```
211+
212+
#### Default handler — remote layer example
213+
214+
In the HTML page:
215+
216+
```html
217+
<mapml-viewer projection="CBMTILE" zoom="5" lat="45.4" lon="-75.7"
218+
controls controlslist="search">
219+
<map-layer label="Canada Base Map" src="canada.mapml" checked></map-layer>
220+
</mapml-viewer>
221+
```
222+
223+
In `canada.mapml`:
224+
225+
```xml
226+
<mapml->
227+
<map-head>
228+
<map-meta charset="utf-8"></map-meta>
229+
<map-link rel="suggestions" tref="https://geogratis.gc.ca/services/geoname/en/geonames.json?q={searchTerms}*&num=20"></map-link>
230+
<map-link rel="search" tref="https://geogratis.gc.ca/services/geoname/en/geonames.json?q={searchTerms}&num=20"></map-link>
231+
</map-head>
232+
<map-body>
233+
<map-extent units="CBMTILE" checked>
234+
<map-input name="z" type="zoom" min="0" max="17"></map-input>
235+
<map-input name="y" type="location" units="tilematrix" axis="row"></map-input>
236+
<map-input name="x" type="location" units="tilematrix" axis="column"></map-input>
237+
<map-link rel="tile" tref="https://example.com/tiles/{z}/{y}/{x}.png"></map-link>
238+
</map-extent>
239+
</map-body>
240+
</mapml->
241+
```
242+
243+
#### Custom handler — overriding the default with `preventDefault()`
244+
245+
When the server's response format does not match GeoJSON `FeatureCollection`,
246+
use `preventDefault()` on the `mapsuggestions` and/or `mapsearch` events to
247+
suppress the default handler and render results yourself.
248+
249+
The polyfill dispatches two cancelable `CustomEvent`s on the `<mapml-viewer>`
250+
(or `<map is="web-map">`) element:
251+
252+
| Event | Fires when | `e.detail` properties |
253+
|-------------------|-------------------------------------------------|-----------------------------------|
254+
| `mapsuggestions` | Suggestion responses arrive (user is typing) | `query`, `responses` |
255+
| `mapsearch` | Search responses arrive (user pressed Enter) | `query`, `responses` |
256+
257+
`e.detail.responses` is an array of `{ data, link, layer }` objects — one per
258+
layer that contributed a link. `data` is the parsed JSON response body; `link`
259+
is the `<map-link>` element; `layer` is the `<map-layer>` element.
260+
261+
To render results, query the search panel's results container from the map and
262+
append your own HTML. Results should use the class `mapml-search-result` on
263+
`<button>` elements for consistent styling.
264+
265+
Example using the geonames.gc.ca API (non-GeoJSON response shape):
266+
267+
```html
268+
<mapml-viewer projection="CBMTILE" zoom="5" lat="45.4" lon="-75.7"
269+
controls controlslist="search">
270+
<map-layer label="Geonames Layer" src="geonames-layer.mapml" checked></map-layer>
271+
</mapml-viewer>
272+
273+
<script>
274+
const viewer = document.querySelector('mapml-viewer');
275+
276+
viewer.addEventListener('mapsuggestions', (e) => {
277+
e.preventDefault();
278+
// Access the results container via the map's internal DOM
279+
const container = viewer._map._container.querySelector('.mapml-search-results');
280+
container.innerHTML = '';
281+
for (const { data } of e.detail.responses) {
282+
if (!data || !data.items) continue;
283+
for (const item of data.items) {
284+
const btn = document.createElement('button');
285+
btn.className = 'mapml-search-result';
286+
btn.setAttribute('type', 'button');
287+
btn.textContent = item.name;
288+
btn.addEventListener('click', () => {
289+
if (item.bbox && item.bbox.length === 4) {
290+
const [west, south, east, north] = item.bbox;
291+
viewer._map.fitBounds([[south, west], [north, east]]);
292+
} else {
293+
viewer._map.setView([item.latitude, item.longitude], 10);
294+
}
295+
});
296+
container.appendChild(btn);
297+
}
298+
}
299+
});
300+
301+
viewer.addEventListener('mapsearch', (e) => {
302+
e.preventDefault();
303+
// Same pattern — parse the non-standard response and render results
304+
const container = viewer._map._container.querySelector('.mapml-search-results');
305+
container.innerHTML = '';
306+
for (const { data } of e.detail.responses) {
307+
if (!data || !data.items) continue;
308+
for (const item of data.items) {
309+
const btn = document.createElement('button');
310+
btn.className = 'mapml-search-result';
311+
btn.setAttribute('type', 'button');
312+
btn.textContent = item.name;
313+
btn.addEventListener('click', () => {
314+
if (item.bbox && item.bbox.length === 4) {
315+
const [west, south, east, north] = item.bbox;
316+
viewer._map.fitBounds([[south, west], [north, east]]);
317+
} else {
318+
viewer._map.setView([item.latitude, item.longitude], 10);
319+
}
320+
});
321+
container.appendChild(btn);
322+
}
323+
}
324+
});
325+
</script>
326+
```
327+
328+
#### Search without suggestions
329+
330+
Suggestions are optional. If only `<map-link rel="search">` is provided
331+
(no `rel="suggestions"`), the control will not fetch anything as the user
332+
types — only pressing Enter will trigger a search request.
333+
334+
```html
335+
<map-layer label="Search Only" checked>
336+
<map-link rel="search"
337+
tref="https://nominatim.openstreetmap.org/search?q={searchTerms}&format=geojson&limit=10"></map-link>
338+
<map-extent units="OSMTILE" checked>
339+
<!-- ... map-inputs and tile link ... -->
340+
</map-extent>
341+
</map-layer>
138342
```

.github/skills/mapml-viewer-markup/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ The default projection is `OSMTILE`.
7979

8080
### `controlslist`
8181

82-
`controlslist` - an enumerated attribute, possible values are: "`nofullscreen`", "`nolayer`", "`noreload`", "`noscale`" and "`nozoom`". Occasionally, you may not want your users to have access to a particular control, so you may prune the set of controls automatically presented (when you have used the `controls` boolean attribute).
82+
`controlslist` - an enumerated attribute, possible values are: "`nofullscreen`", "`nolayer`", "`noreload`", "`noscale`", "`nozoom`", "`geolocation`" and "`search`". The `no*` tokens let you prune the set of controls automatically presented (when you have used the `controls` boolean attribute), while unlike the `no*` tokens (which hide default controls), the `geolocation` and `search` tokens are **opt-in**. `geolocation` and `search` add location and search controls to the map, respectively. The search control is `disabled` by default, and becomes enabled only when at least one `checked` [`<map-layer>`](../layer/) contains a [`<map-link rel="search">`](../link/) element.
8383

8484
---
8585

0 commit comments

Comments
 (0)