import type { SchedulerEvent } from '../data/scheduler.types'; import type { SchedulerRuntime } from '../scheduler-runtime'; import type { TimeRange } from '../time/calendar'; import type { Timeline, TimelineConfig, TimelineViewName } from '../time/timeline-engine'; /** * Callback the plugin injects so the API can hand back a rebuilt timeline. * * The API owns the *decision* to rebuild - a view switch, a zoom, a range pan - * but not the consequences of one: swapping `runtime.timeline`, re-rendering the * header bands, resetting horizontal scroll extents. Inverting it this way keeps * the API free of DOM and lets it be unit-tested against a plain object, which * is the same reason {@link SchedulerRuntime} is an interface rather than a class. */ export type SchedulerViewChangeHandler = (next: Timeline) => void; /** * The plugin capabilities the API cannot implement itself. * * Everything here needs either DOM or state the API deliberately does not own. * Scrolling is the grid's, selection is the selection service's, and the * timeline swap is the plugin's. Passing them in as one object rather than * reaching into the plugin is what keeps the dependency one-directional: the * plugin knows the API, the API knows only these four function shapes. */ export interface SchedulerApiHooks { /** Installs a freshly built timeline. See {@link SchedulerViewChangeHandler}. */ readonly setTimeline: SchedulerViewChangeHandler; /** Scrolls the timeline horizontally to a content-space pixel offset. */ readonly scrollToPx: (px: number) => void; /** Scrolls vertically to the grid row backing a resource, if it is displayed. */ readonly scrollToRowByResourceId: (id: string) => void; /** The selection service, which owns the selected set and its change event. */ readonly selection: { select(ids: readonly string[], additive?: boolean): void; clear(): void; getSelected(): readonly SchedulerEvent[]; }; } /** * The scheduler's public API. * * ## What this is for * * Everything a host can do to a scheduler from code: mutate events, drive * selection, change the view, navigate. It is the *only* supported surface - * {@link SchedulerRuntime} is shared internal state and the event index is a * data structure, and a host reaching into either would be writing against * implementation details that carry no compatibility promise. * * ## Veto and hook semantics * * Mutating methods run the matching `onBefore*` config hook and abort when it * returns `false`, returning `false` themselves. Programmatic and interactive * mutations therefore go through exactly one veto path, which is what stops a * host's validation from being enforced on drag but bypassed by * `api.moveEvent()`. The bus events fire either way, `before` even for a vetoed * action, so telemetry sees attempts. * * ## Managed mode does not apply here * * `draggable.managed: false` and `resizable.managed: false` mean "the pointer * gesture is a *request*; the host persists and confirms it". That contract is * about gestures the user makes. A call to {@link moveEvent} is not a request - * it *is* the host acting - so these methods always apply their change, and the * intent they pass to hooks reports `managed: true`. A host in unmanaged mode * typically handles `beforeMove`, persists, and then calls {@link moveEvent} to * commit; if this method deferred as well, nothing would ever move. * * ## `locked` does not apply here either * * `SchedulerEventTypeConfig.locked` suppresses the drag and resize affordances * on a bar. It is an interaction affordance, not an authorisation rule - the API * is the escape hatch a host uses to move a locked event from its own UI, and a * check here would leave no way to do that at all. * * ## Rendering * * No method paints. Each requests a frame through * {@link SchedulerRuntime.requestRender}, which coalesces into the grid's own * animation frame - so a loop of a thousand `updateEvent` calls costs one * render, not a thousand. */ export declare class SchedulerApi { private readonly runtime; private readonly hooks; /** * The active preset, or `null` once a raw {@link TimelineConfig} supersedes it. * * Tracked rather than derived because a `TimelineConfig` cannot be reduced * back to a preset name: several presets share `{ unit: 'day', step: 1 }` and * differ only in header bands, so a reverse lookup would report the wrong view * and a host persisting it would restore the wrong one. */ private view; /** The span the timeline covers. Kept here so a zoom can rebuild without re-deriving it. */ private range; /** Effective slot width, which zoom changes independently of the view. */ private slotWidth; /** * @param runtime - Shared scheduler state. Read for events, hooks and config; * its event index is the single source of truth this API mutates. * @param hooks - Plugin capabilities the API delegates to. See * {@link SchedulerApiHooks}. */ constructor(runtime: SchedulerRuntime, hooks: SchedulerApiHooks); /** * Adds one event. * * Emits `beforeCreate`, then consults `onBeforeCreate`; returning `false` from * that hook aborts before the index is touched, so a rejected event leaves no * trace. On success the index is updated, `onAfterCreate` and `afterCreate` * fire, and a frame is requested. * * Adding an event whose id already exists is *not* rejected here - the index * treats it as an insert and the duplicate would shadow the original in * lookups. Hosts generating ids should ensure uniqueness; this method does not * pay an id existence check on every add to catch a caller bug. * * @returns `false` when `onBeforeCreate` vetoed, otherwise `true`. */ addEvent(event: SchedulerEvent): boolean; /** * Adds many events, vetoing per event. * * Per-event rather than all-or-nothing because a bulk import where one record * is invalid should land the other nine hundred and ninety-nine; a host that * wants transactional semantics validates first and calls this once. * * Cheaper than a loop of {@link addEvent} only in the number of render * requests, which is the cost that actually matters: one frame, not one per * event. * * @returns How many events were accepted. */ addEvents(events: readonly SchedulerEvent[]): number; /** * Removes an event by id. * * Emits `beforeDelete`, then consults `onBeforeDelete`; `false` aborts. The * event is also dropped from the selection, because a selected id with no * event behind it would make `getSelectedEvents()` shorter than the selection * and every consumer of that pair inconsistent. * * @returns `false` when the id is unknown or `onBeforeDelete` vetoed. */ removeEvent(id: string): boolean; /** * Replaces an event in place, matched by id. * * Has no veto hook by design: `onBeforeCreate` and `onBeforeDelete` guard the * *existence* of events, and an update changes only their content, which a * host is free to validate before calling. Adding a third hook here would also * put a host callback on the drag-commit path twice, since a move already * fires `onBeforeMove`. * * An unknown id is treated as an insert by the index, matching the "upsert" * behaviour hosts expect when replaying a server delta. */ updateEvent(event: SchedulerEvent): void; /** * Moves an event to another resource and/or start time, preserving duration. * * Duration preservation is what makes this a *move* rather than a reschedule: * `end` is recomputed as `toStart + (end - start)`, so a two-hour meeting * dropped on a new day is still two hours. Use {@link resizeEvent} to change * duration. * * Emits `beforeMove`, consults `onBeforeMove`, and on success bumps * `SchedulerEvent.version` so the bar renderer can skip untouched bars by * comparison rather than by deep equality. * * @returns `false` when the id is unknown or `onBeforeMove` vetoed. */ moveEvent(id: string, toResourceId: string, toStart: number): boolean; /** * Changes an event's bounds. * * Rejects an inverted or zero-length span outright, and anything shorter than * `resizable.minDuration`, before any hook runs - an event with `end <= start` * would be invisible to the index's half-open overlap query and would appear * to have vanished, which is far worse than a refused call. * * The resized `edge` reported to hooks is inferred: `'start'` when the start * moved, `'end'` otherwise. A call that moves both is reported as `'start'`, * since that is the edge whose movement a host is most likely to be guarding. * * @returns `false` when the id is unknown, the span is invalid, or * `onBeforeResize` vetoed. */ resizeEvent(id: string, start: number, end: number): boolean; /** Resolves one event by id, or `undefined`. O(1). */ getEvent(id: string): SchedulerEvent | undefined; /** * Every loaded event. * * Returned `readonly` and **by reference** - copying is not a safety measure * here, it is a per-call allocation proportional to the dataset, and a host * calling this in a change handler on half a million events would allocate * megabytes per frame. Mutating the array or its events corrupts the index. */ getEvents(): readonly SchedulerEvent[]; /** * Events overlapping `range`, half-open on both sides. * * With `resourceId` this goes through the index and costs O(log n + k) - the * query the whole data structure exists for. Without one it is a linear scan, * because the index is partitioned by resource and a cross-resource range * query has no better path through it; that asymmetry is deliberate (see * `EventIndex`) and callers on the render path should always pass a resource. * * "Overlapping" follows the model's half-open convention: an event ending * exactly at `range.start` is excluded, and one starting exactly at * `range.end` is too. */ getEventsInRange(range: TimeRange, resourceId?: string): readonly SchedulerEvent[]; /** * Replaces the entire event set. * * Rebuilds the index in one O(n log n) pass rather than n inserts, so this is * the right call for a datasource response and the wrong one for a delta. * * Selection is cleared, not filtered: after a wholesale replacement the * surviving ids are a coincidence of the new payload, and silently keeping * some of a user's selection is more surprising than keeping none. * * The `onBefore*` hooks are **not** consulted - they veto individual actions, * and running them n times over a bulk load would turn a data refresh into an * n-call host callback storm. */ setEvents(events: readonly SchedulerEvent[]): void; /** * Selects one event. * * Delegated to the selection service rather than implemented here, so * programmatic and pointer selection share one path - including the * single/multiple mode check and the `selectionChanged` emit, which is why * this method does not fire that event itself. * * @param additive - When `true`, adds to the current selection instead of * replacing it, mirroring a Ctrl-click. Ignored in `'single'` mode. */ selectEvent(id: string, additive?: boolean): void; /** * Replaces the selection with `ids`. * * Always replacing - there is no additive form - because a caller that wants * to extend can concatenate, whereas a caller that wants to replace cannot * un-extend without first clearing, which would emit twice. */ selectEvents(ids: readonly string[]): void; /** * The selected events, resolved. * * Ids whose events have since been removed are not reported, so the result can * be shorter than the selected id set during a transaction. */ getSelectedEvents(): readonly SchedulerEvent[]; /** Clears the selection. A no-op emitting nothing when it is already empty. */ clearSelection(): void; /** * Switches to a named view preset, keeping the current range and slot width. * * Keeping the range is what makes view switching feel like a zoom rather than * a navigation: a user looking at March who picks `'week'` expects to still be * looking at March, not to be thrown to the current week. * * Emits `viewChanged` in addition to the `timelineChanged` every rebuild * emits, so a host can persist the preset name without having to infer that a * rebuild was a view switch. */ setView(view: TimelineViewName): void; /** * Changes the span the timeline covers, keeping the view. * * The range is snapped outward to whole slots by the timeline builder, so a * range starting mid-day still produces aligned slots - passing exact * boundaries is not required. */ setRange(range: TimeRange): void; /** * Overrides the timeline config directly, for views the presets do not cover. * * Merged over the current config, so `{ step: 15 }` changes only the step. * Supplying `unit`, `step` or `headerBands` clears the active preset: the * timeline is no longer the thing the preset describes, and continuing to * report a view name would make {@link getTimeline} and a persisted view * disagree. */ setTimelineConfig(partial: Partial): void; /** * The time span currently on screen. * * Derived from live scroll geometry, not from the timeline's configured range * - that is what {@link getTimeline}`().config.range` reports. This is the * range a host needs to lazy-load data for, or to label a "showing 3-9 March" * caption, and it changes on every scroll without the timeline changing at all. * * Clamped to the axis, so an over-scrolled or rubber-banded viewport never * reports times outside the timeline. */ getVisibleRange(): TimeRange; /** * The current timeline: axis, header bands and the config that produced them. * * Immutable, so holding the reference is safe - and comparing it by identity * is the supported way to ask "did the timeline change?", which is exactly how * the renderer skips work on a pure scroll. */ getTimeline(): Timeline; /** * Widens slots by 25%, clamped to {@link MAX_SLOT_WIDTH}. * * The time under the centre of the viewport is held fixed across the rebuild. * Anchoring on the centre rather than the left edge is what makes repeated * zooming feel like a magnifier over the thing being examined; anchoring left * would walk whatever the user was looking at off the right of the screen. * * A no-op at the clamp, emitting nothing. */ zoomIn(): void; /** Narrows slots by 20%, clamped to {@link MIN_SLOT_WIDTH}. See {@link zoomIn}. */ zoomOut(): void; /** * Scrolls horizontally so `t` is at the left edge of the viewport. * * Projected through the axis, so this is correct in both slot-width modes and * across DST and uneven months - which arithmetic on the range would not be. * Times outside the timeline project outside it too and are clamped by the * scroll container, not here. */ scrollToDate(t: number): void; /** * Scrolls vertically to a resource's row. * * A no-op when the resource is filtered out or collapsed under a group: it has * no displayed row to scroll to, and forcing one into view would mean undoing * the user's own filter. */ scrollToResource(id: string): void; /** * Scrolls to the current time. * * Reads the clock at call time rather than caching a "now", so a page left * open overnight still lands on today. */ scrollToNow(): void; /** * Rebuilds the timeline and repaints everything. * * The blunt instrument, for a host that changed something the scheduler has no * way to observe - a theme swap, a locale change, a container resize the * observer missed. Prefer {@link refreshEvents} or {@link refreshTimeline}: a * full rebuild reallocates the axis and header cells, which the other two do * not. */ refresh(): void; /** * Repaints bars from the current index, without rebuilding the timeline. * * For a host that mutated event objects in place - the one thing this API * cannot detect, because the index stores positions rather than watching * objects. Note that in-place edits to `start`, `end` or `resourceId` are * **not** picked up by this: those change an event's indexed position and must * go through {@link updateEvent}. */ refreshEvents(): void; /** * Rebuilds the timeline from the current view, range and slot width. * * The case this exists for is a container resize in `'proportional'` mode, * where nothing about the config changed but the pixel projection must be * recomputed. */ refreshTimeline(): void; /** * Builds the next timeline, installs it, and announces it. * * The single funnel every view/range/zoom change passes through, so * "rebuilt but forgot to emit" and "emitted but forgot to re-render" are not * states this class can reach. */ private rebuild; /** * Applies a zoom factor while holding the viewport's centre time fixed. * * The centre is sampled *before* the rebuild because the old axis is the only * thing that can convert the current scroll offset back into a time; once * `setTimeline` has run, that mapping is gone. */ private zoomBy; /** Requests a frame and announces an event-collection change in one place. */ private notifyChanged; private emitCreate; private emitDelete; private emitMove; private emitResize; /** The payload shared by `viewChanged` and `timelineChanged`. */ private viewPayload; } //# sourceMappingURL=scheduler-api.d.ts.map