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.