import type { BarPlacement } from '../layout/bar-layout'; import type { SchedulerEvent } from '../data/scheduler.types'; import type { SchedulerModule, SchedulerRuntime } from '../scheduler-runtime'; /** * The slice of the bar renderer the interaction services actually need. * * Declared structurally, and deliberately **not** imported from the renderer: * the services would otherwise pull the whole rendering module graph into their * own, which makes them impossible to unit test without a DOM and couples two * subsystems that only ever exchange two questions -- "where is the element for * this event?" and "what did you paint this frame?". A test supplies a plain * object literal; the plugin supplies the real `EventBarRenderer`, which * satisfies this shape without declaring that it does. * * Both members are cheap accessors over state the renderer already holds, so * calling them per pointer move costs nothing. */ export interface BarSource { /** The live element for an event id, or `null` when the bar is not currently rendered. */ getBarElement(id: string): HTMLElement | null; /** This frame's placements, in row order. Read-only -- services never mutate it. */ getPlacements(): readonly BarPlacement[]; } /** * Selector for an event bar. * * Mirrors `theme/scheduler-styles.ts` by hand because that module exports one * opaque CSS string; there is no structured source to derive the name from. Kept * here as the single definition every service imports, so a rename in the * stylesheet is a one-line change on this side rather than four. */ export declare const BAR_SELECTOR = ".pg-scheduler-bar"; /** Attribute the renderer stamps each bar with, and the only link from DOM back to the model. */ export declare const BAR_ID_ATTRIBUTE = "data-event-id"; /** * Walks up from an event target to the bar element that contains it. * * Delegation is the only viable strategy here: a dense view paints hundreds of * bars per frame and recycles them, so per-element listeners would mean hundreds * of add/remove pairs per scroll frame and a leak for every recycled node that * missed its removal. One listener on the layer costs one `closest` call per * user gesture instead. * * `closest` is feature-detected rather than the target being `instanceof * Element`, because the package's tests run in a `node` environment where the * `Element` global does not exist and the DOM stub would fail the check. */ export declare function closestBar(target: EventTarget | null): HTMLElement | null; /** Reads the event id off a bar element, or `null` when the renderer has not stamped one. */ export declare function barEventId(el: HTMLElement): string | null; /** Payload of {@link SchedulerEventName.SelectionChanged}. */ export interface SchedulerSelectionChangedPayload { /** Selected ids, in selection order. */ readonly ids: readonly string[]; /** The resolved events, so a listener does not have to look each one up. */ readonly events: readonly SchedulerEvent[]; } /** Payload of {@link SchedulerEventName.EventClicked} and `EventDoubleClicked`. */ export interface SchedulerEventPointerPayload { readonly event: SchedulerEvent; /** The originating DOM event, for hosts that need modifier keys or coordinates. */ readonly native: MouseEvent; } /** * Click-driven selection over event bars. * * ## Ordering used by range (shift-click) selection * * A range needs a total order over events, and the only order the user perceives * is "down the rows, then left to right". This service approximates it by * sorting candidates by **`(resourceId, start)` lexicographically** -- resource id * ascending as the primary key, event start ascending as the secondary, event id * as a final tie-break so the order is total and stable. * * That is an approximation with one known trade-off: when the grid is sorted or * grouped, resource-id order is not row order, so a shift-click can include a * resource the user sees elsewhere on screen. The alternative -- deriving order * from the rendered placements -- was rejected because placements are * virtualized: the same two clicks would select different sets depending on how * far the user had scrolled, and a range spanning more rows than fit in the * viewport would silently truncate. A stable, complete, slightly abstract order * beats an intuitive one that changes under scroll. * * ## Why the selection set is mutated in place * * `SchedulerRuntime.selection` is handed to the renderer once and read every * frame. Replacing it would leave the renderer holding a stale set, so this * service mutates the same instance and announces the change through * {@link SchedulerRuntime.requestRender} plus the bus. It is the only writer. */ export declare class EventSelectionService implements SchedulerModule { private readonly runtime; /** One controller for every listener, so {@link destroy} is a single `abort()`. */ private readonly abort; /** * Anchor for range selection: the last event picked by a plain or additive * click. Shift-click deliberately does not move it, which is what lets a user * widen and narrow the same range by shift-clicking repeatedly. */ private anchorId; private readonly bars; /** * @param runtime - Shared scheduler state. The selection set on it is owned by * this service from construction until {@link destroy}. * @param layerEl - The plugin layer holding the bars. Listeners are delegated * here rather than attached per bar. * @param bars - Renderer accessor, used only to move DOM focus onto the bar a * click landed on, so keyboard navigation continues from where the pointer * left off. */ constructor(runtime: SchedulerRuntime, layerEl: HTMLElement, bars: BarSource); /** * Selects every event, optionally filtered. * * Replaces the current selection rather than adding to it, matching the * "select all" gesture everywhere else in the grid. In `'single'` mode this * necessarily selects only the first match, which is why the loop breaks * early rather than selecting and then discarding. * * @param predicate - Optional filter; return `true` to include the event. */ selectAll(predicate?: (event: SchedulerEvent) => boolean): void; /** * Clears the selection. * * No-ops when nothing is selected, so a stray Escape does not cost a render * pass or a spurious `selectionChanged` on the host's bus. */ clear(): void; /** * Selects the given ids programmatically. * * Unknown ids are skipped rather than stored: a selection set containing ids * with no backing event would make {@link getSelected} lossy and would keep * ghosts alive across a data reload. * * @param ids - Event ids to select. * @param additive - When `true`, adds to the current selection instead of * replacing it. Ignored in `'single'` mode, where the last id wins. */ select(ids: readonly string[], additive?: boolean): void; /** * The selected events, resolved through the runtime. * * Allocates a fresh array, which is acceptable because this is only ever * called on a user gesture or by a host reading state -- never per frame. */ getSelected(): readonly SchedulerEvent[]; /** Whether an event id is currently selected. O(1). */ isSelected(id: string): boolean; /** Removes every listener. The selection set itself is left as-is for the plugin to discard. */ destroy(): void; /** * Resolves the clicked bar, applies the modifier-appropriate selection change * and notifies the host. * * The click callback fires *after* the selection has settled, so a host * reacting to it sees the final state rather than the previous one. Clicks * that miss a bar are ignored rather than clearing: the layer also covers the * empty timeline background, which future range-create gestures will own, and * clearing here would fight them. */ private readonly onClick; /** * Reports a double click. * * Selection is untouched: the preceding single click has already set it, and * mutating it again here would make an "open the editor" gesture look like a * second selection change to anything listening on the bus. */ private readonly onDoubleClick; /** Replaces the selection with a single id and re-anchors range selection on it. */ private replaceWith; /** Adds or removes one id, keeping it as the range anchor either way. */ private toggle; /** * Selects everything between two events in the order documented on the class. * * Sorting a copy of every event on each shift-click is O(n log n), which is * the one place this service is not constant-time. It is tolerable because the * gesture is rare and human-paced; caching the sorted array would mean * invalidating it on every add, remove and drag commit, for no perceptible * gain. */ private selectRange; /** * Collapses the selection to its most recent entry in `'single'` mode. * * Applied after the fact rather than guarding every call site, so there is * exactly one place the mode is enforced. `Set` preserves insertion order, * which is what makes "the last one added" well-defined. */ private enforceSingle; /** Repaints and announces the new selection on both the bus and the host callback. */ private commit; } //# sourceMappingURL=event-selection-service.d.ts.map