@taskctrl/canvas-timeline 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/canvas-timeline.cjs.js +1 -1
- package/dist/canvas-timeline.cjs.js.map +1 -1
- package/dist/canvas-timeline.es.js +1136 -1100
- package/dist/canvas-timeline.es.js.map +1 -1
- package/dist/core/HitTest.d.ts +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/interaction/InteractionHandler.d.ts +2 -2
- package/dist/interaction/interactionInfo.d.ts +27 -0
- package/dist/interaction/snapResolve.d.ts +41 -0
- package/dist/interaction/snapUtils.d.ts +1 -1
- package/dist/types.d.ts +63 -1
- package/package.json +1 -1
package/dist/core/HitTest.d.ts
CHANGED
|
@@ -19,3 +19,18 @@ export declare function resolveCanResize(item: Item, globalCanResize: false | 'l
|
|
|
19
19
|
* falling through to the global value.
|
|
20
20
|
*/
|
|
21
21
|
export declare function resolveCanMove(item: Item, globalCanMove: boolean): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Decide which interaction (if any) a pointer-down at `edge` should start for
|
|
24
|
+
* `item`, given the timeline-wide `canResize`/`canMove` props. Returns `null`
|
|
25
|
+
* when nothing is permitted — the caller then starts no interaction (no ghost)
|
|
26
|
+
* and shows no affordance (default cursor). Centralising this keeps the cursor
|
|
27
|
+
* and the interaction decision in lockstep.
|
|
28
|
+
*
|
|
29
|
+
* An item explicitly resize-locked per-item (`item.canResize === false`) has
|
|
30
|
+
* inert edges: grabbing an edge returns `null` rather than falling through to a
|
|
31
|
+
* move, so a user trying to resize a locked item sees no UI. This is scoped to
|
|
32
|
+
* the *explicit* per-item override — an item inheriting a global `canResize:
|
|
33
|
+
* false` (field undefined) keeps the move-only behaviour where the whole item,
|
|
34
|
+
* edges included, is draggable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveInteractionMode(item: Item, edge: 'left' | 'right' | 'body', globalCanResize: false | 'left' | 'right' | 'both', globalCanMove: boolean): 'move' | 'resize-left' | 'resize-right' | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Group, Item, ItemBounds, ItemState, DrawHelpers, CanvasItemRenderer, CanvasGroupItemRenderer, DayStyle, RowStyle, Dependency, TimeRangeHighlight, TimelineTheme, MarkerConfig, CanvasTimelineProps, CanvasTimelineRef, CaptureOptions, } from './types';
|
|
1
|
+
export type { Group, Item, ItemBounds, ItemState, DrawHelpers, CanvasItemRenderer, CanvasGroupItemRenderer, DayStyle, RowStyle, Dependency, TimeRangeHighlight, TimelineTheme, MarkerConfig, InteractionInfo, DragSnapFn, DragSnapResult, DragSnapAnchor, CanvasTimelineProps, CanvasTimelineRef, CaptureOptions, } from './types';
|
|
2
2
|
export { DEFAULT_THEME } from './types';
|
|
3
3
|
export { CanvasTimeline } from './CanvasTimeline';
|
|
4
4
|
export { HierarchyEngine } from './core/HierarchyEngine';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Item } from '../types';
|
|
1
|
+
import type { Item, DragSnapFn } from '../types';
|
|
2
2
|
export type InteractionMode = 'move' | 'resize-left' | 'resize-right';
|
|
3
3
|
export interface InteractionState {
|
|
4
4
|
item: Item;
|
|
@@ -15,7 +15,7 @@ export declare class InteractionHandler {
|
|
|
15
15
|
private state;
|
|
16
16
|
private dragSnap;
|
|
17
17
|
private activated;
|
|
18
|
-
constructor(dragSnap: number);
|
|
18
|
+
constructor(dragSnap: number | DragSnapFn | undefined);
|
|
19
19
|
startInteraction(item: Item, mode: InteractionMode, x: number, y: number): void;
|
|
20
20
|
update(x: number, y: number): void;
|
|
21
21
|
setCurrentGroup(groupId: string | number): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Item, InteractionInfo, DragSnapFn } from '../types';
|
|
2
|
+
import type { ViewState } from '../core/ViewState';
|
|
3
|
+
import type { IntervalTree } from '../core/IntervalTree';
|
|
4
|
+
import type { HierarchyEngine } from '../core/HierarchyEngine';
|
|
5
|
+
import type { InteractionState } from './InteractionHandler';
|
|
6
|
+
/** The subset of props the snap helpers need. */
|
|
7
|
+
export interface SnapDeps {
|
|
8
|
+
canvasWidth: number;
|
|
9
|
+
dragSnap?: number | DragSnapFn;
|
|
10
|
+
timezone?: string;
|
|
11
|
+
intervalTree: IntervalTree<Item>;
|
|
12
|
+
hierarchyEngine: HierarchyEngine;
|
|
13
|
+
}
|
|
14
|
+
/** Snapped new start time for a move, matching the pointer-up commit path:
|
|
15
|
+
* item-edge snap (start edge first, then end edge) with grid snap as fallback. */
|
|
16
|
+
export declare function snappedMoveStart(state: InteractionState, vs: ViewState, p: SnapDeps): number;
|
|
17
|
+
/** Snapped edge time for a resize, matching the pointer-up commit path: item-edge
|
|
18
|
+
* snap with grid-snap fallback, then hierarchy resize constraints. */
|
|
19
|
+
export declare function snappedResizeEdge(state: InteractionState, vs: ViewState, p: SnapDeps): {
|
|
20
|
+
edge: 'left' | 'right';
|
|
21
|
+
time: number;
|
|
22
|
+
};
|
|
23
|
+
/** Build the public InteractionInfo payload for the current drag/resize state. */
|
|
24
|
+
export declare function buildInteractionInfo(state: InteractionState, vs: ViewState, p: SnapDeps, pointer: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
}): InteractionInfo;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Item, DragSnapFn, DragSnapResult } from '../types';
|
|
2
|
+
/** Reset the once-per-session invalid-snap warning. Test-only. */
|
|
3
|
+
export declare function _resetSnapWarning(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a `dragSnap` prop to a concrete snap spec for the given item/edge.
|
|
6
|
+
*
|
|
7
|
+
* - `undefined` → `1` (no snap).
|
|
8
|
+
* - `number` → returned as-is (preserves legacy behavior, including `0`).
|
|
9
|
+
* - `DragSnapFn` → invoked with `(item, edge)`. `edge` is `'left' | 'right'`
|
|
10
|
+
* during a resize and `undefined` during a move. It may return either a bare
|
|
11
|
+
* interval (ms) — tz-aware default anchoring (see {@link gridSnap}) — or a
|
|
12
|
+
* `{ interval, anchor }` spec to control phase explicitly. An invalid return
|
|
13
|
+
* (`interval` that is `0`, `NaN`, or negative, or a non-finite `anchor`) falls
|
|
14
|
+
* back to `1` (no snap) and warns once per session.
|
|
15
|
+
*
|
|
16
|
+
* Hot path: called on every pointer-move frame. Returns the consumer's object
|
|
17
|
+
* by reference — no allocation or copying here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveDragSnap(dragSnap: number | DragSnapFn | undefined, item: Item, edge?: 'left' | 'right'): DragSnapResult;
|
|
20
|
+
/**
|
|
21
|
+
* Snap `time` (epoch ms) to a snap grid.
|
|
22
|
+
*
|
|
23
|
+
* When `snap` is a `{ interval, anchor }` spec, the grid is `anchor + N *
|
|
24
|
+
* interval` and the `timezone` is ignored — the consumer owns the phase (see
|
|
25
|
+
* {@link DragSnapAnchor} for the DST caveat). This branch is pure arithmetic
|
|
26
|
+
* (no dayjs), so it is the cheapest path.
|
|
27
|
+
*
|
|
28
|
+
* When `snap` is a bare `number` interval:
|
|
29
|
+
* - For day-or-larger intervals with a `timezone`, the grid is anchored to
|
|
30
|
+
* tz-local midnight rather than the UTC epoch, so the snap lands on the same
|
|
31
|
+
* day boundaries the grid/headers render (which are also tz-local). Day
|
|
32
|
+
* boundaries in a tz with a non-zero UTC offset are not multiples of
|
|
33
|
+
* 86_400_000 since the epoch, so a naive `Math.round(time / interval) *
|
|
34
|
+
* interval` drifts by the offset. The snap is computed in local-calendar-day
|
|
35
|
+
* space, so it stays correct across DST transitions (where a local day is 23
|
|
36
|
+
* or 25 hours long).
|
|
37
|
+
* - Sub-day intervals (1h, 15min, …) stay UTC-anchored: hours and quarter-hours
|
|
38
|
+
* line up between UTC and any whole- or fractional-hour offset, so no
|
|
39
|
+
* translation is needed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function gridSnap(time: number, snap: DragSnapResult, timezone?: string): number;
|
|
@@ -11,5 +11,5 @@ export declare function collectItemEdges(items: SnapItem[], excludeItemId: numbe
|
|
|
11
11
|
* Find the best snap target from a list of edge X positions.
|
|
12
12
|
* Returns the snapped X position if an edge is within threshold, or null to fall back to time-grid snap.
|
|
13
13
|
*/
|
|
14
|
-
export declare function findSnapTarget(currentX: number, edgeXPositions: number[], thresholdPx: number, _pixelsPerMs: number, _dragSnap:
|
|
14
|
+
export declare function findSnapTarget(currentX: number, edgeXPositions: number[], thresholdPx: number, _pixelsPerMs: number, _dragSnap: import('../types').DragSnapResult): number | null;
|
|
15
15
|
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -107,6 +107,60 @@ export interface MarkerConfig {
|
|
|
107
107
|
width: number;
|
|
108
108
|
label?: string;
|
|
109
109
|
}
|
|
110
|
+
/** Live state of an in-progress drag/resize, emitted via `onInteractionUpdate`.
|
|
111
|
+
* All times are the snapped values that will commit on drop (item-edge snap +
|
|
112
|
+
* grid snap, plus hierarchy resize constraints) — i.e. where the item will
|
|
113
|
+
* actually land, matching the snap guide. The consumer-supplied
|
|
114
|
+
* `moveResizeValidator` is NOT applied here (it may run per frame). */
|
|
115
|
+
export interface InteractionInfo {
|
|
116
|
+
itemId: number;
|
|
117
|
+
mode: 'move' | 'resize-left' | 'resize-right';
|
|
118
|
+
/** Set for resize interactions; which edge is being dragged. */
|
|
119
|
+
edge?: 'left' | 'right';
|
|
120
|
+
/** The headline time to display: the new start for `move`/`resize-left`, the
|
|
121
|
+
* new end for `resize-right`. */
|
|
122
|
+
time: number;
|
|
123
|
+
/** The item's snapped start time during this interaction. */
|
|
124
|
+
startTime: number;
|
|
125
|
+
/** The item's snapped end time during this interaction. */
|
|
126
|
+
endTime: number;
|
|
127
|
+
/** Pointer position in viewport coordinates (clientX/clientY), for
|
|
128
|
+
* positioning a `position: fixed` tooltip without further math. */
|
|
129
|
+
pointerX: number;
|
|
130
|
+
pointerY: number;
|
|
131
|
+
}
|
|
132
|
+
/** Anchored snap grid: snap to `anchor + N * interval`. Returning this from a
|
|
133
|
+
* {@link DragSnapFn} overrides the default tz-local-midnight anchoring — the
|
|
134
|
+
* consumer is fully in charge of phase. */
|
|
135
|
+
export interface DragSnapAnchor {
|
|
136
|
+
/** Snap interval in ms. Must be finite and `> 0`, else snapping is disabled
|
|
137
|
+
* (falls back to no snap, one `console.warn` per session). */
|
|
138
|
+
interval: number;
|
|
139
|
+
/** Absolute epoch ms. The snap grid is built as
|
|
140
|
+
* `anchor + N * interval`. Because `interval` is exact ms, the grid does
|
|
141
|
+
* NOT track wall-clock hour across DST — at the spring-forward boundary the
|
|
142
|
+
* snapped position will drift one hour from local time and stay drifted
|
|
143
|
+
* until the next anchor reset. Consumers that need wall-clock-stable
|
|
144
|
+
* snapping should re-pick an anchor on the DST side they care about.
|
|
145
|
+
* The timeline's `timezone` prop is ignored when an anchor is supplied. */
|
|
146
|
+
anchor: number;
|
|
147
|
+
}
|
|
148
|
+
/** What a {@link DragSnapFn} may return: a bare interval (ms) — tz-aware
|
|
149
|
+
* default anchoring — or an explicit `{ interval, anchor }` grid. */
|
|
150
|
+
export type DragSnapResult = number | DragSnapAnchor;
|
|
151
|
+
/**
|
|
152
|
+
* Per-drag snap resolver. Called on every pointer-move frame during a
|
|
153
|
+
* drag/resize, so keep it cheap — the library does not memoize the result.
|
|
154
|
+
*
|
|
155
|
+
* @param item The item being dragged/resized.
|
|
156
|
+
* @param edge `'left' | 'right'` during a resize; `undefined` during a move.
|
|
157
|
+
* @returns Either a snap interval in ms (varies only the *interval*; phase stays
|
|
158
|
+
* grid-aligned to multiples of it — tz-local midnight for day-or-larger
|
|
159
|
+
* intervals when `timezone` is set), or a {@link DragSnapAnchor} `{ interval,
|
|
160
|
+
* anchor }` to also control phase (tz is ignored in that case). A `0`, `NaN`,
|
|
161
|
+
* or negative `interval` (or non-finite `anchor`) falls back to no snap.
|
|
162
|
+
*/
|
|
163
|
+
export type DragSnapFn = (item: Item, edge?: 'left' | 'right') => DragSnapResult;
|
|
110
164
|
export interface CanvasTimelineProps {
|
|
111
165
|
groups: Group[];
|
|
112
166
|
items: Item[];
|
|
@@ -122,7 +176,10 @@ export interface CanvasTimelineProps {
|
|
|
122
176
|
canMove: boolean;
|
|
123
177
|
canResize: false | 'left' | 'right' | 'both';
|
|
124
178
|
canChangeGroup: boolean;
|
|
125
|
-
|
|
179
|
+
/** Snap interval for drag/resize, in ms. A `number` snaps everything to that
|
|
180
|
+
* interval; a {@link DragSnapFn} resolves the interval per item/edge. When
|
|
181
|
+
* omitted, no snapping is applied. */
|
|
182
|
+
dragSnap?: number | DragSnapFn;
|
|
126
183
|
minZoom: number;
|
|
127
184
|
maxZoom: number;
|
|
128
185
|
theme?: Partial<TimelineTheme>;
|
|
@@ -148,6 +205,11 @@ export interface CanvasTimelineProps {
|
|
|
148
205
|
/** Validate and optionally constrain move/resize. Return the (possibly modified) time. */
|
|
149
206
|
moveResizeValidator?: (action: 'move' | 'resize', itemId: number, time: number, edge?: 'left' | 'right') => number;
|
|
150
207
|
onItemHover?: (itemId: number | null, e: PointerEvent) => void;
|
|
208
|
+
/** Fires once per animation frame during an active drag/resize, then once with
|
|
209
|
+
* `null` when it ends. Intended for a live "current time" tooltip. This is a
|
|
210
|
+
* hot path: do NOT call setState here — mutate a ref'd DOM node's text/transform
|
|
211
|
+
* directly so neither the library nor your app re-renders per frame. */
|
|
212
|
+
onInteractionUpdate?: (info: InteractionInfo | null) => void;
|
|
151
213
|
onCanvasDoubleClick?: (groupId: number, time: number) => void;
|
|
152
214
|
onCanvasContextMenu?: (groupId: number, time: number, e: PointerEvent) => void;
|
|
153
215
|
onTimeChange?: (start: number, end: number) => void;
|