(e: PointerEvent, mode: DragMode)
| 151 | }; |
| 152 | |
| 153 | const startDrag = (e: PointerEvent, mode: DragMode): void => { |
| 154 | if (disposed) return; |
| 155 | if (e.button !== 0) return; |
| 156 | |
| 157 | e.preventDefault(); |
| 158 | |
| 159 | // If we somehow start a new drag while another is in-flight, clean up first. |
| 160 | activeDragCleanup?.(); |
| 161 | activeDragCleanup = null; |
| 162 | |
| 163 | const dragStartX = e.clientX; |
| 164 | const startRange = zoomState.getRange(); |
| 165 | |
| 166 | const target = e.currentTarget instanceof Element ? e.currentTarget : windowEl; |
| 167 | setPointerCaptureBestEffort(target, e.pointerId); |
| 168 | |
| 169 | if (mode === 'pan-window') { |
| 170 | windowEl.style.cursor = 'grabbing'; |
| 171 | centerGrip.style.cursor = 'grabbing'; |
| 172 | } |
| 173 | |
| 174 | const onMove = (ev: PointerEvent): void => { |
| 175 | if (disposed) return; |
| 176 | if (ev.pointerId !== e.pointerId) return; |
| 177 | |
| 178 | ev.preventDefault(); |
| 179 | |
| 180 | const dxPercent = pxToPercent(ev.clientX - dragStartX); |
| 181 | if (dxPercent === null) return; |
| 182 | |
| 183 | switch (mode) { |
| 184 | case 'left-handle': { |
| 185 | // UX: don't allow handle crossing; clamp left <= current end. |
| 186 | const nextStart = Math.min(startRange.end, startRange.start + dxPercent); |
| 187 | const anchored = zoomState as unknown as Partial<{ |
| 188 | setRangeAnchored: (start: number, end: number, anchor: 'start' | 'end' | 'center') => void; |
| 189 | }>; |
| 190 | if (anchored.setRangeAnchored) { |
| 191 | // When clamped by minSpan/maxSpan, keep the right edge anchored (prevents jumpiness). |
| 192 | anchored.setRangeAnchored(nextStart, startRange.end, 'end'); |
| 193 | } else { |
| 194 | zoomState.setRange(nextStart, startRange.end); |
| 195 | } |
| 196 | return; |
| 197 | } |
| 198 | case 'right-handle': { |
| 199 | // UX: don't allow handle crossing; clamp right >= current start. |
| 200 | const nextEnd = Math.max(startRange.start, startRange.end + dxPercent); |
| 201 | const anchored = zoomState as unknown as Partial<{ |
| 202 | setRangeAnchored: (start: number, end: number, anchor: 'start' | 'end' | 'center') => void; |
| 203 | }>; |
| 204 | if (anchored.setRangeAnchored) { |
| 205 | // When clamped by minSpan/maxSpan, keep the left edge anchored (prevents jumpiness). |
| 206 | anchored.setRangeAnchored(startRange.start, nextEnd, 'start'); |
| 207 | } else { |
| 208 | zoomState.setRange(startRange.start, nextEnd); |
| 209 | } |
| 210 | return; |
no test coverage detected