import type { ScrollConfig } from '../types/scroll.types'; export type ScrollYCallback = (scrollTop: number) => void; export declare class ScrollController { private scrollTop; private scrollLeft; private totalHeight; private totalCenterWidth; private viewportHeight; private centerViewportWidth; /** Height actually given to the vertical scrollbar spacer — see `scroll-track.ts`. */ private trackHeight; /** * The last few track offsets this controller wrote to the native scrollbar, * as a fixed-size ring. * * Every write echoes back as a `scroll` event; the browser may have rounded * it to a device pixel, which — once scaled back into content space — is no * longer the value we asked for and would fight the gesture that caused it. * Matched entries are cleared, so a genuine user scroll to the same offset is * never swallowed twice. * * A ring rather than a single slot because a wheel glide writes on *every* * animation frame: `scroll` events are dispatched asynchronously and the * browser is free to coalesce or lag them, so an echo can arrive after a * newer write has already been recorded. Against one slot that echo reads as * a user gesture — which would cancel the glide and yank the view back a * frame. Four entries cover any realistic dispatch delay while keeping the * scan a handful of comparisons on the scroll path. * * Slots hold `NaN` when empty; `NaN` never compares within tolerance of a * real offset, so no emptiness check is needed in the scan. */ private readonly recentTrackWrites; /** Next slot in {@link recentTrackWrites} to overwrite. */ private trackWriteCursor; /** * Pixel offset that rendered rows are positioned relative to. Written by * `GridRenderer` alongside the row position stylesheet; see * {@link setRowOrigin}. */ private rowOriginY; private gridEl; private sbVNativeEl; private sbVSpacerEl; private sbHNativeEl; private sbHSpacerEl; private sbHRowEl; private panPointerId; private panStartX; private panStartY; private panLastX; private panLastY; private panLastT; private panScrollStartLeft; private panScrollStartTop; private panMoved; /** The body/header element the active pan pointer was captured to (for release). */ private panCaptureEl; /** Residual finger velocity in scroll-space px/ms, sampled from the last move. */ private velX; private velY; private momentumRAF; /** * Returns `true` while another interaction (column reorder/resize) owns the * pointer, so touch-panning yields to it. Wired by `GridRenderer` to the * HeaderRenderer's busy state; unset means "never busy". */ private gestureGuard; private abortCtrl; private resizeObs; private scrollYCbs; private scrollXCbs; /** * When `true`, the vertical scrollbar column is never collapsed to 0 width * — it stays reserved (a "stable gutter") even while `totalHeight <= * viewportHeight`. Set for Master/Detail grids: expanding/collapsing a * detail row changes total content height, which can tip whether a * scrollbar is needed at all — if the column collapsed and reappeared with * it, every flex column would jump to fill/re-cede that space on every * toggle. Reserving it unconditionally makes that a non-event. */ private reserveVerticalGutter; /** Resolved wheel behaviour; see {@link ScrollConfig}. */ private readonly config; /** Per-gesture mouse-vs-touchpad classifier. */ private readonly wheelSource; /** Eases a notched wheel's discrete steps into continuous motion. */ private readonly wheelGlide; /** * Live `prefers-reduced-motion` state, kept current by a media-query * listener rather than polled per wheel event. */ private reducedMotion; /** * `true` while a scroll offset is being written from inside an animation * frame — the wheel glide or the touch-momentum glide. See * {@link isInAnimationFrame}. */ private inAnimationFrame; /** * @param config - Wheel-scrolling options. Defaults smooth a notched mouse * wheel and leave touchpad gestures untouched. */ constructor(config?: ScrollConfig); /** * `true` while a scroll offset is being written from inside an animation * frame, i.e. during the current synchronous notification of scroll * subscribers. * * Subscribers use this to decide *when* to repaint. A `requestAnimationFrame` * booked from inside a frame callback does not run until the **next** frame, * so a subscriber that always defers would paint its new state one frame * behind the offsets published here — visible on a fast glide as the row * window trailing the panel translate. Seeing `true`, a subscriber should do * its work inline instead: it is already on the frame that will paint. */ isInAnimationFrame(): boolean; /** * Writes an animated scroll offset with {@link inAnimationFrame} raised for * the duration of the subscriber notification it triggers. * * @param value - The new offset in content pixels. * @param vertical - `true` for the Y axis, `false` for X. */ private applyAnimatedScroll; /** * Subscribes to vertical scroll. **Multicast** — every registered callback * runs on each change, in registration order. * * Was a single-slot setter through v2.0.10, where a second call silently * *replaced* the first. That made it a trap for anything outside the renderer: * `GridRenderer` claims a slot for its own `scheduleRender()`, so a plugin * subscribing would have disabled the grid's re-render with no error. The only * signature change is the return value, so existing call sites that discard it * are unaffected. * * @returns Unsubscribe. Callbacks fire **synchronously during the scroll**, * ahead of the animation frame `scheduleRender` books — so do cheap work * here and structural DOM work in the render callback. */ onScrollY(cb: ScrollYCallback): () => void; /** Subscribes to horizontal scroll. Multicast; see {@link onScrollY}. */ onScrollX(cb: () => void): () => void; /** * Notifies vertical-scroll subscribers. * * Iterates a snapshot so a callback that unsubscribes (itself or a sibling) * cannot corrupt the walk, and isolates failures per listener — one bad * subscriber must not stop the grid from re-rendering. */ private fireScrollY; /** Notifies horizontal-scroll subscribers. See {@link fireScrollY}. */ private fireScrollX; setReserveVerticalGutter(reserve: boolean): void; /** * Registers a predicate that, while it returns `true`, suspends touch-panning * so a concurrent column reorder or resize owns the pointer instead. See * {@link gestureGuard}. */ setGestureGuard(fn: () => boolean): void; mount(gridEl: HTMLElement, bodyEl: HTMLElement, centerBodyEl: HTMLElement, sbVNativeEl: HTMLElement, sbVSpacerEl: HTMLElement, sbHNativeEl: HTMLElement, sbHSpacerEl: HTMLElement, sbHRowEl?: HTMLElement): void; updateSizes(totalHeight: number, totalCenterWidth: number): void; /** * Sets the pixel offset that rendered rows are positioned relative to. * * `GridRenderer` writes each rendered row's `top` into the position * stylesheet as `row.top - origin` and calls this with the same origin in the * same synchronous block. The panel transform published here adds it back * (`origin - scrollTop`), so on-screen position is unchanged while every * painted coordinate stays within a viewport's worth of zero — which is what * keeps 1px row borders from rounding away at large scroll depths. See the * note above `.pg-panel__content` in `panels.css.ts`. * * Because the sheet and the origin are always written together, a scroll that * lands between two renders is still correct: only `scrollTop` moves, and the * offset tracks it. */ setRowOrigin(originY: number): void; getScrollTop(): number; getScrollLeft(): number; /** * Pixel offset rendered rows are positioned relative to, as of the last * {@link setRowOrigin}. * * Anything positioning content against rows must subtract this: row `top` * values are in absolute content space, but the position stylesheet writes * `top - rowOriginY` and the panels apply `translateY(--pg-row-offset-y)`. * Note it is **not** `scrollTop` — the two differ by up to a render window. */ getRowOriginY(): number; /** Returns the current visible height of the body viewport in pixels. */ getViewportHeight(): number; /** Returns the current visible width of the center body viewport in pixels. */ getCenterViewportWidth(): number; canScrollLeft(): boolean; canScrollRight(): boolean; canScrollUp(): boolean; canScrollDown(): boolean; scrollToY(y: number): void; scrollToX(x: number): void; /** * Writes a vertical scroll offset without touching the wheel glide. * * The single funnel every vertical scroll passes through — clamping, * CSS-var publication, scrollbar sync and subscriber notification all happen * here exactly once. {@link scrollToY} is this plus glide cancellation; the * animator calls this directly so its own writes do not abort it. */ private applyScrollY; /** Horizontal counterpart of {@link applyScrollY}. */ private applyScrollX; scrollToRow(rowIndex: number, rows: ReadonlyArray<{ top: number; }>): void; scrollToTop(): void; destroy(): void; private clampScroll; private syncCSSVars; /** Furthest the content can scroll, in content pixels. */ private get maxScrollY(); /** Furthest the center panel can scroll horizontally, in content pixels. */ private get maxScrollX(); /** Furthest the native scrollbar can scroll, in track pixels. */ private get maxTrackY(); private toTrackY; private fromTrackY; /** Restates the current scroll position on the native scrollbar. */ private writeTrackY; /** * Whether a `scroll` event's track offset is the echo of one of our own * writes, rather than a user gesture on the scrollbar. * * Consumes the matching entry, so two events at the same offset — our echo * and a later user scroll back to it — are told apart. * * @param track - The offset the native scrollbar now reports. * @returns `true` when the event should be ignored. */ private isTrackEcho; private syncScrollbars; private readonly onVNativeScroll; private readonly onHNativeScroll; private readonly onWheel; /** * Applies a wheel gesture's vertical delta to this grid's scroll. * * The entry point for gestures this controller does not receive directly: * a nested Master/Detail grid forwards its over-scroll here so the parent * continues the motion (see `DetailRowRenderer.attachWheelForwarding`). * Routed through the same classification and smoothing as a gesture over the * grid's own body, so the hand-off is invisible — the parent picks up with * the same feel the nested grid just had. * * @param e - The original wheel event, forwarded unmodified. */ scrollByWheelEvent(e: WheelEvent): void; /** * Decides whether this wheel event should be smoothed, and feeds the * per-gesture device classifier either way. * * @returns `true` when the gesture is a notched wheel that smoothing should * be applied to, under the configured {@link WheelScrollMode}. */ private classifyWheel; /** * Normalizes an event's deltas into content pixels. * * `deltaMode` conversion first — line and page deltas are as real as pixel * ones — then {@link ScrollConfig.wheelStepScale}, which applies to notched * gestures only: a touchpad delta is the user's own finger movement and * scaling it would desynchronize the content from the gesture driving it. */ private toPixelDeltas; /** * Whether this grid can still absorb `delta` on the given axis. * * Compares against the glide target rather than the live offset — see the * call site in {@link onWheel}. */ private canConsumeWheel; /** Routes a normalized wheel delta to the glide or straight to the offset. */ private applyWheelDelta; /** * Tracks `prefers-reduced-motion` for the lifetime of the mount. * * Read from a listener-maintained field rather than queried per event: * `matchMedia` is comparatively expensive and the wheel handler is on the * 60fps path. Registered with the mount's abort signal, so it is released * with every other listener on `destroy()`. */ private watchReducedMotion; private readonly onPanPointerDown; private readonly onPanPointerMove; private readonly onPanPointerUp; /** Ends the active pan contact and releases its pointer capture. */ private releasePan; private startMomentum; private stopMomentum; /** * Installs a one-shot capture-phase click swallower on the grid so the ghost * click synthesized at the end of a touch-pan gesture never reaches cells or * headers. Self-removing, with a timeout fallback in case no click arrives. */ private suppressNextClick; } //# sourceMappingURL=scroll-controller.d.ts.map