PageSourceSearch

https://bikeatthebeach.com/js/map-view/highlight-key.js

js bikeatthebeach.com collected 2026-09-24 13:56:55 UTC 6,729 bytes, 196 lines download raw bytes

1/* =====================================================================
2 * Highlight-key shortcut.
3 *
4 * H            toggles the highlight on the feature under the pointer.
5 *              With the pointer off any feature it falls back to the
6 *              feature whose modal is open, or was most recently open,
7 *              which mirrors presentation mode.
8 * Shift+H      clears every highlight in the current map session
9 *
10 * The highlight is the same yellow glow the existing-lines-highlight,
11 * existing-polygons-highlight, and existing-icons-highlight layers
12 * already paint. We just flip feature.is_highlighted in wmState and
13 * trigger refreshMapData; the layers do the rest.
14 *
15 * State is per-session — closing the modal doesn't clear, but a page
16 * reload starts blank. Persisting would require a server PATCH; not
17 * worth the round-trip when the use case is "look at a road for a
18 * minute, then move on."
19 *
20 * Gated on settings.enable_highlight_key (default off). When off the
21 * key listener simply isn't installed, so the H key falls through to
22 * the browser default.
23 * ================================================================== */
24
25let _installed = false;
26
27function isTypingTarget() {
28  const a = document.activeElement;
29  if (!a) return false;
30  if (a.isContentEditable) return true;
31  const t = a.tagName;
32  return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT';
33}
34
35async function refresh() {
36  try {
37    const { refreshMapData } = await import('./drawing/layers.js');
38    refreshMapData({ force: true });
39  } catch (e) {
40    console.warn('[highlight-key] refresh failed:', e);
41  }
42}
43
44function clearAll() {
45  const feats = window.wmState?.features;
46  if (!Array.isArray(feats)) return 0;
47  let cleared = 0;
48  for (const f of feats) {
49    if (f.is_highlighted) { f.is_highlighted = false; cleared++; }
50  }
51  if (cleared) refresh();
52  return cleared;
53}
54
55// Layers a feature can be hovered on. Queried only when H is pressed.
56const HOVER_LAYERS = [
57  'existing-icons',
58  'existing-points-circles',
59  'existing-lines',
60  'existing-lines-dashed',
61  'existing-lines-dotted',
62  'existing-lines-routes',
63  'existing-polygons',
64  'existing-polygons-above'
65];
66
67// Last pointer position, in map pixels. Storing a coordinate pair is the
68// whole cost of tracking hover; the hit test happens on keypress.
69let _lastPoint = null;
70
71/**
72 * The feature under the pointer, resolved at the moment H is pressed.
73 *
74 * An earlier version registered a mousemove handler per interactive layer.
75 * Mapbox implements layer-scoped events by hit-testing on every pointer
76 * move, so eight layers meant eight queryRenderedFeatures calls for every
77 * pixel of mouse travel, and the whole map went sluggish. Nothing needs the
78 * hovered id continuously — only this keypress does — so the query moved
79 * here, where it runs once.
80 */
81function hoveredFeatureId(map) {
82  if (!map || !_lastPoint) return null;
83  const layers = HOVER_LAYERS.filter(id => map.getLayer(id));
84  if (!layers.length) return null;
85  let hits;
86  try {
87    // Small box rather than a point, so thin lines are reachable.
88    const b = 4;
89    hits = map.queryRenderedFeatures(
90      [[_lastPoint.x - b, _lastPoint.y - b], [_lastPoint.x + b, _lastPoint.y + b]],
91      { layers }
92    );
93  } catch { return null; }
94  for (const h of hits || []) {
95    if (h?.properties?.__draft) continue;
96    const n = Number(h?.properties?.feature_id);
97    if (Number.isFinite(n) && n > 0) return n;
98  }
99  return null;
100}
101
102function activeFeatureId(map) {
103  // Hover wins. The setting describes this key as acting on "the feature
104  // under the cursor", and it previously only ever acted on the open
105  // modal's feature, so hovering and pressing H did nothing at all.
106  //
107  // After hover, fall back to ui.activeFeatureId (set in feature-render and
108  // stable across modal close), then window.currentFeatureId for
109  // compatibility, then the geometry-edit pointer for the rare
110  // cancel-modal-then-press-H case.
111  const candidates = [
112    hoveredFeatureId(map),
113    window.wmState?.ui?.activeFeatureId,
114    window.currentFeatureId,
115    window.wmState?.editingFeatureId
116  ];
117  for (const c of candidates) {
118    const n = Number(c);
119    if (Number.isFinite(n) && n > 0) return n;
120  }
121  return null;
122}
123
124function toggleActive(map) {
125  const fid = activeFeatureId(map);
126  if (fid == null) return false;
127  const feat = (window.wmState?.features || []).find(f => +f.id === fid);
128  if (!feat) return false;
129  feat.is_highlighted = !feat.is_highlighted;
130  refresh();
131  return feat.is_highlighted;
132}
133
134/**
135 * Visually highlight a feature on click. Single-selection: clears the
136 * previous click-highlight before applying the new one, so the user
137 * sees exactly one click-selected feature at a time.
138 *
139 * Does NOT touch features that were highlighted via the H key (which
140 * may persist beyond a click) unless the previously-click-highlighted
141 * feature happens to be one of them. That tradeoff keeps the
142 * implementation simple; press H again on the same feature to
143 * re-highlight if needed.
144 */
145export function setClickHighlight(featureId) {
146  const feats = window.wmState?.features;
147  if (!Array.isArray(feats)) return;
148  const fid = Number(featureId);
149  if (!Number.isFinite(fid) || fid <= 0) return;
150
151  const prev = Number(window.wmState?._clickHighlightedId);
152  if (Number.isFinite(prev) && prev > 0 && prev !== fid) {
153    const prevFeat = feats.find(f => +f.id === prev);
154    if (prevFeat) prevFeat.is_highlighted = false;
155  }
156
157  const target = feats.find(f => +f.id === fid);
158  if (!target) return;
159  target.is_highlighted = true;
160  if (window.wmState) window.wmState._clickHighlightedId = fid;
161  refresh();
162}
163
164export function initHighlightKey(map) {
165  if (_installed) return;
166  if (!map) return;
167  if (window.projectSettings?.enable_highlight_key !== true) return;
168
169  // One map-level handler that only records a coordinate. No hit testing,
170  // so this stays cheap no matter how many layers are on the map.
171  map.on('mousemove', (e) => { _lastPoint = e.point; });
172  map.on('mouseout',  () => { _lastPoint = null; });
173
174  document.addEventListener('keydown', (e) => {
175    if (e.key !== 'h' && e.key !== 'H') return;
176    if (isTypingTarget()) return;
177
178    if (e.shiftKey) {
179      e.preventDefault();
180      const n = clearAll();
181      console.log(`[highlight-key] cleared ${n} highlights`);
182      return;
183    }
184
185    e.preventDefault();
186    const result = toggleActive(map);
187    if (result === false) {
188      console.log('[highlight-key] no feature under the pointer and none open');
189    } else {
190      console.log(`[highlight-key] highlight = ${result}`);
191    }
192  });
193
194  _installed = true;
195  console.log('[highlight-key] H / Shift+H listener installed');
196}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.