ar-timeline-view 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +106 -2
- package/dist/timelineview.css +2 -0
- package/dist/timelineview.js +1 -0
- package/dist/timelineview.mjs +1341 -0
- package/dist/types/api.d.ts +16 -0
- package/dist/types/dates.d.ts +11 -0
- package/dist/types/drag.d.ts +47 -0
- package/dist/types/duration.d.ts +11 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/model.d.ts +127 -0
- package/dist/types/render.d.ts +25 -0
- package/dist/types/scale.d.ts +23 -0
- package/dist/types/svg.d.ts +8 -0
- package/dist/types/types.d.ts +132 -0
- package/dist/types/view.d.ts +173 -0
- package/package.json +33 -4
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Id, MovePatchData, MoveResultData, TimelineViewData } from "./types";
|
|
2
|
+
/** A non-2xx response. `body` is the parsed JSON body, if there was one. */
|
|
3
|
+
export declare class ApiError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly body: unknown;
|
|
6
|
+
constructor(status: number, body: unknown);
|
|
7
|
+
}
|
|
8
|
+
/** Client for the REST API described in CLAUDE.md and docs/api/openapi.yaml. */
|
|
9
|
+
export declare class RestClient {
|
|
10
|
+
private readonly fetchImpl;
|
|
11
|
+
private readonly baseUrl;
|
|
12
|
+
constructor(baseUrl: string, fetchImpl?: typeof fetch);
|
|
13
|
+
getView(viewId: Id): Promise<TimelineViewData>;
|
|
14
|
+
moveEntry(viewId: Id, timelineId: Id, taskId: Id, patch: MovePatchData, version: number | undefined): Promise<MoveResultData>;
|
|
15
|
+
private request;
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dates are handled in UTC throughout, so day and month arithmetic is not
|
|
3
|
+
* affected by daylight saving time in the viewer's zone.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseDate(text: string): Date;
|
|
6
|
+
/** Formats as `YYYY-MM-DD`, adding `THH:mm` when the time is not midnight. */
|
|
7
|
+
export declare function formatDate(date: Date): string;
|
|
8
|
+
export declare function daysInMonth(year: number, month: number): number;
|
|
9
|
+
/** Adds calendar months, clamping the day to the end of the target month. */
|
|
10
|
+
export declare function addMonths(date: Date, months: number): Date;
|
|
11
|
+
export declare function addDays(date: Date, days: number): Date;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Offset } from "./types";
|
|
2
|
+
export type DropTarget = {
|
|
3
|
+
timeline: string;
|
|
4
|
+
placement: "on";
|
|
5
|
+
line: number;
|
|
6
|
+
newLine: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
timeline: string;
|
|
9
|
+
placement: "above" | "below";
|
|
10
|
+
};
|
|
11
|
+
/** A finished drag. IDs are the DOM's string form; the view maps them back. */
|
|
12
|
+
export interface DropEvent {
|
|
13
|
+
timeline: string;
|
|
14
|
+
task: string;
|
|
15
|
+
target: DropTarget;
|
|
16
|
+
/** Set when the target is a callout area. */
|
|
17
|
+
offset?: Offset;
|
|
18
|
+
}
|
|
19
|
+
/** A marker label dropped somewhere: its new `marker_offset`. The id is the DOM's string form. */
|
|
20
|
+
export interface MarkerDropEvent {
|
|
21
|
+
marker: string;
|
|
22
|
+
offset: Offset;
|
|
23
|
+
}
|
|
24
|
+
interface Rect {
|
|
25
|
+
left: number;
|
|
26
|
+
top: number;
|
|
27
|
+
right: number;
|
|
28
|
+
bottom: number;
|
|
29
|
+
width: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Callout offset for a label dropped at `box` in the callout area `area`.
|
|
33
|
+
* `at` is the attachment point as a fraction of the area's width. The
|
|
34
|
+
* attachment point is on the bar's edge: the area's bottom above the bar, its
|
|
35
|
+
* top below it. `y` is clamped so the label stays on its own side of the bar.
|
|
36
|
+
*/
|
|
37
|
+
export declare function calloutOffset(box: Rect, area: Rect, at: number, placement: "above" | "below"): Offset;
|
|
38
|
+
/**
|
|
39
|
+
* Makes every `[data-entry]` inside `root` draggable. A press and release
|
|
40
|
+
* without moving past the drag threshold is a click instead, reported to
|
|
41
|
+
* `onClick` with the entry's element; a drag never reports a click.
|
|
42
|
+
* The same kind of click elsewhere in a timeline or on the view's heading
|
|
43
|
+
* (not on an entry) goes to `onBackgroundClick`, with the pressed element.
|
|
44
|
+
* Returns a function that detaches.
|
|
45
|
+
*/
|
|
46
|
+
export declare function attachDrag(root: HTMLElement, onDrop: (event: DropEvent) => void, onClick?: (entry: HTMLElement, event: PointerEvent) => void, onBackgroundClick?: (target: HTMLElement, event: PointerEvent) => void, onMarkerDrop?: (event: MarkerDropEvent) => void): () => void;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DurationUnit = "h" | "d" | "w" | "m" | "y";
|
|
2
|
+
export interface Duration {
|
|
3
|
+
amount: number;
|
|
4
|
+
unit: DurationUnit;
|
|
5
|
+
}
|
|
6
|
+
/** Parses `"2d"`, `"3m"` and so on. `m` is months; there is no minutes unit. */
|
|
7
|
+
export declare function parseDuration(text: string): Duration;
|
|
8
|
+
/** Adds a duration. Months and years are calendar units, clamped to the month's end. */
|
|
9
|
+
export declare function addDuration(start: Date, { amount, unit }: Duration): Date;
|
|
10
|
+
/** A task is a milestone when its duration is missing or zero. */
|
|
11
|
+
export declare function isMilestoneDuration(text: string | undefined): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "./styles.css";
|
|
2
|
+
import type { Id, MovePatchData, Offset, TimelineViewData } from "./types";
|
|
3
|
+
import { TimelineView, type TimelineEventName, type TimelineEvents, type TimelineHandlers, type TimelineOptions } from "./view";
|
|
4
|
+
export * from "./types";
|
|
5
|
+
export { ApiError, RestClient } from "./api";
|
|
6
|
+
export { MoveError, TimelineModel, firstLine, lineCount, patchFromData, trimLines, timelineToData } from "./model";
|
|
7
|
+
export { parseDuration, addDuration } from "./duration";
|
|
8
|
+
export { MoveCancelledError, TimelineView, type EntryMovedEvent, type MarkerMovedEvent, type MoveFailedEvent, type TimelineEvents, type TimelineEventName, type TimelineHandlers, type TimelineOptions, } from "./view";
|
|
9
|
+
export declare function init(container: string | HTMLElement, options?: TimelineOptions): TimelineView;
|
|
10
|
+
export declare const parse: (data: TimelineViewData) => void;
|
|
11
|
+
export declare const load: (viewId: Id) => Promise<void>;
|
|
12
|
+
export declare const serialize: () => TimelineViewData;
|
|
13
|
+
export declare const toSVG: () => string;
|
|
14
|
+
export declare const moveEntry: (timelineId: Id, taskId: Id, patch: MovePatchData) => Promise<void>;
|
|
15
|
+
export declare const attachEvent: <K extends TimelineEventName>(name: K, handler: TimelineHandlers[K]) => number;
|
|
16
|
+
export declare const detachEvent: (eventId: number) => boolean;
|
|
17
|
+
export declare const selectTask: (id: Id, add?: boolean) => boolean;
|
|
18
|
+
export declare const selectTimeline: (id: Id, add?: boolean) => boolean;
|
|
19
|
+
export declare const unselectTask: (id: Id) => boolean;
|
|
20
|
+
export declare const unselectTimeline: (id: Id) => boolean;
|
|
21
|
+
export declare const clearSelection: () => void;
|
|
22
|
+
export declare const getSelectedTasks: () => Id[];
|
|
23
|
+
export declare const getSelectedTimelines: () => Id[];
|
|
24
|
+
export declare const setMarkerOffset: (markerId: Id, offset: Offset) => void;
|
|
25
|
+
export declare const moveTimelineUp: (id: Id) => boolean;
|
|
26
|
+
export declare const moveTimelineDown: (id: Id) => boolean;
|
|
27
|
+
export declare const moveTimeline: (id: Id, delta: number) => boolean;
|
|
28
|
+
export declare const on: <K extends keyof TimelineEvents>(name: K, handler: (event: TimelineEvents[K]) => void) => () => void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Id, MilestoneSymbol, MovePatchData, NamePosition, Offset, Placement, Scale, TimelineData, TimelineViewData } from "./types";
|
|
2
|
+
export interface View {
|
|
3
|
+
id?: Id;
|
|
4
|
+
name: string;
|
|
5
|
+
absolute: boolean;
|
|
6
|
+
scale: Scale;
|
|
7
|
+
namePosition: NamePosition;
|
|
8
|
+
milestoneSymbol: MilestoneSymbol;
|
|
9
|
+
showDates: boolean;
|
|
10
|
+
/** Whether clicks select tasks and timelines. */
|
|
11
|
+
selection: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface Task {
|
|
14
|
+
id: Id;
|
|
15
|
+
text: string;
|
|
16
|
+
start: Date;
|
|
17
|
+
/** `start + duration`; equal to `start` for milestones. */
|
|
18
|
+
finish: Date;
|
|
19
|
+
duration?: string;
|
|
20
|
+
isMilestone: boolean;
|
|
21
|
+
symbol?: MilestoneSymbol;
|
|
22
|
+
/** CSS color overriding the theme's task or milestone color. */
|
|
23
|
+
color?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Marker {
|
|
26
|
+
id: Id;
|
|
27
|
+
text: string;
|
|
28
|
+
date: Date;
|
|
29
|
+
global: boolean;
|
|
30
|
+
color?: string;
|
|
31
|
+
/** Label position; `y ≤ 0` above the topmost bar, `y > 0` below the bottommost. */
|
|
32
|
+
offset: Offset;
|
|
33
|
+
}
|
|
34
|
+
export type Entry = {
|
|
35
|
+
taskId: Id;
|
|
36
|
+
placement: "on";
|
|
37
|
+
line: number;
|
|
38
|
+
} | {
|
|
39
|
+
taskId: Id;
|
|
40
|
+
placement: "above" | "below";
|
|
41
|
+
calloutOffset: Offset;
|
|
42
|
+
};
|
|
43
|
+
export interface Timeline {
|
|
44
|
+
id: Id;
|
|
45
|
+
name: string;
|
|
46
|
+
start: Date;
|
|
47
|
+
finish: Date;
|
|
48
|
+
entries: Entry[];
|
|
49
|
+
/** Ids of the (non-global) markers shown on this timeline. */
|
|
50
|
+
markers: Id[];
|
|
51
|
+
}
|
|
52
|
+
export interface MovePatch {
|
|
53
|
+
timelineId?: Id;
|
|
54
|
+
placement?: Placement;
|
|
55
|
+
line?: number;
|
|
56
|
+
newLine?: boolean;
|
|
57
|
+
calloutOffset?: Offset;
|
|
58
|
+
}
|
|
59
|
+
/** Mirrors the REST status codes, so a local move fails the same way a server move would. */
|
|
60
|
+
export type MoveErrorCode = "not_found" | "conflict" | "invalid";
|
|
61
|
+
export declare class MoveError extends Error {
|
|
62
|
+
readonly code: MoveErrorCode;
|
|
63
|
+
constructor(code: MoveErrorCode, message: string);
|
|
64
|
+
}
|
|
65
|
+
/** Default distance, in px, between the bar and a callout label without an offset. */
|
|
66
|
+
export declare const DEFAULT_CALLOUT_GAP = 16;
|
|
67
|
+
export declare const defaultOffset: (placement: "above" | "below") => Offset;
|
|
68
|
+
export declare class TimelineModel {
|
|
69
|
+
view: View;
|
|
70
|
+
tasks: Task[];
|
|
71
|
+
timelines: Timeline[];
|
|
72
|
+
markers: Marker[];
|
|
73
|
+
version?: number;
|
|
74
|
+
private constructor();
|
|
75
|
+
static fromData(data: TimelineViewData): TimelineModel;
|
|
76
|
+
toData(): TimelineViewData;
|
|
77
|
+
/** Converts and validates one timeline. */
|
|
78
|
+
timelineFromData(t: TimelineData): Timeline;
|
|
79
|
+
private entryFromData;
|
|
80
|
+
findTask(id: Id): Task | undefined;
|
|
81
|
+
/** Moves a marker's label. `y ≤ 0` puts it above the topmost bar, `y > 0` below the bottommost. */
|
|
82
|
+
setMarkerOffset(id: Id, offset: Offset): Marker;
|
|
83
|
+
findMarker(id: Id): Marker | undefined;
|
|
84
|
+
findTimeline(id: Id): Timeline | undefined;
|
|
85
|
+
findEntry(timelineId: Id, taskId: Id): Entry | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Moves an entry to another bar line, callout position, area or timeline.
|
|
88
|
+
* `newLine` inserts a new bar line at `line`, shifting the lines from there
|
|
89
|
+
* down; a `line` one past the last also opens one. Afterwards the affected
|
|
90
|
+
* timelines are trimmed (see `trimLines`): a line an entry leaves stays as
|
|
91
|
+
* an empty gap only between used lines.
|
|
92
|
+
*
|
|
93
|
+
* Returns the timelines the move changed (source first).
|
|
94
|
+
*/
|
|
95
|
+
moveEntry(timelineId: Id, taskId: Id, patch: MovePatch): Timeline[];
|
|
96
|
+
/**
|
|
97
|
+
* Moves a timeline `delta` places in the stack: -1 is up, 1 is down.
|
|
98
|
+
* Returns false, changing nothing, if it would move past the top or bottom.
|
|
99
|
+
*/
|
|
100
|
+
moveTimeline(id: Id, delta: number): boolean;
|
|
101
|
+
/** Replaces whole timelines, as returned by a successful REST move. */
|
|
102
|
+
replaceTimelines(data: TimelineData[]): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The time axis the view draws on: from the earliest timeline start to the
|
|
106
|
+
* latest finish. With `absolute`, every bar is placed on this axis.
|
|
107
|
+
*/
|
|
108
|
+
export declare function viewRange(model: TimelineModel): {
|
|
109
|
+
start: Date;
|
|
110
|
+
finish: Date;
|
|
111
|
+
} | undefined;
|
|
112
|
+
/** The lowest `line` the bar entries use; 0 when there are none. */
|
|
113
|
+
export declare function firstLine(timeline: Timeline): number;
|
|
114
|
+
/**
|
|
115
|
+
* Removes blank bar lines above the first used one by renumbering the
|
|
116
|
+
* entries so the first used line is 0. Blank lines below the last used one
|
|
117
|
+
* need no trimming: `lineCount` doesn't count them. Gaps between used lines stay.
|
|
118
|
+
*/
|
|
119
|
+
export declare function trimLines(timeline: Timeline): void;
|
|
120
|
+
/**
|
|
121
|
+
* Number of bar lines, counted from line 0: one more than the highest `line`
|
|
122
|
+
* the entries use, at least 1. It is never stored.
|
|
123
|
+
*/
|
|
124
|
+
export declare function lineCount(timeline: Timeline): number;
|
|
125
|
+
export declare function timelineToData(t: Timeline): TimelineData;
|
|
126
|
+
/** Converts a public move body to the internal form. */
|
|
127
|
+
export declare function patchFromData(p: MovePatchData): MovePatch;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type TimelineModel } from "./model";
|
|
2
|
+
/** Bar line height in px. The drag code reads positions from the DOM, so only rendering uses this. */
|
|
3
|
+
export declare const BAR_LINE_HEIGHT = 28;
|
|
4
|
+
/**
|
|
5
|
+
* How far, in px, each callout's edge marker sticks out from the bar. The
|
|
6
|
+
* connector starts there: at the marker's outer edge, in its middle.
|
|
7
|
+
* Keep in step with .tlv-edge-bar and .tlv-edge-symbol in styles.css.
|
|
8
|
+
*/
|
|
9
|
+
export declare const EDGE_BAR_HEIGHT = 6;
|
|
10
|
+
export declare const EDGE_SYMBOL_REACH = 8;
|
|
11
|
+
/** Replaces the contents of `root` with the rendered view. */
|
|
12
|
+
export declare function render(root: HTMLElement, model: TimelineModel): void;
|
|
13
|
+
/**
|
|
14
|
+
* Places each marker line: horizontally at its date on the first timeline's
|
|
15
|
+
* span, vertically from that timeline's bar top (or row top) to the last
|
|
16
|
+
* one's bar bottom (or row bottom). Being one element, it is drawn with
|
|
17
|
+
* the same width everywhere.
|
|
18
|
+
*
|
|
19
|
+
* Positions are measured in the viewport and converted using where a line
|
|
20
|
+
* placed at 0,0 actually lands, so borders and scrolling of the view (and
|
|
21
|
+
* how the browser treats them) don't matter.
|
|
22
|
+
*/
|
|
23
|
+
export declare function layoutMarkerLines(root: HTMLElement): void;
|
|
24
|
+
/** Stops watching the view's layout (when the view is destroyed). */
|
|
25
|
+
export declare function unwatchLayout(root: HTMLElement): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Scale } from "./types";
|
|
2
|
+
export interface Tick {
|
|
3
|
+
date: Date;
|
|
4
|
+
/** Position in the timeline's time span, 0 to 1. */
|
|
5
|
+
at: number;
|
|
6
|
+
label: string;
|
|
7
|
+
/**
|
|
8
|
+
* Running number of the period that starts at this tick, counted from a
|
|
9
|
+
* fixed date. Its parity shades periods alternately, the same way on every
|
|
10
|
+
* timeline whatever its start date.
|
|
11
|
+
*/
|
|
12
|
+
serial: number;
|
|
13
|
+
}
|
|
14
|
+
/** Upper bound so a fine scale on a long span cannot create a huge DOM. */
|
|
15
|
+
export declare const MAX_TICKS = 1000;
|
|
16
|
+
/** Position of a date in the span, 0 at `start` and 1 at `finish`. Not clamped. */
|
|
17
|
+
export declare function fraction(date: Date, start: Date, finish: Date): number;
|
|
18
|
+
/** ISO 8601 week number. */
|
|
19
|
+
export declare function isoWeek(date: Date): number;
|
|
20
|
+
/** Running number of the scale period that holds `date`. */
|
|
21
|
+
export declare function periodSerial(date: Date, scale: Scale): number;
|
|
22
|
+
/** Tick marks for the time scale, from the first boundary at or after `start` up to `finish`. */
|
|
23
|
+
export declare function ticks(start: Date, finish: Date, scale: Scale): Tick[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG export. Rather than describing the layout a second time, this reads the
|
|
3
|
+
* rendered view: element boxes, measured text and computed colors. The result
|
|
4
|
+
* is plain SVG (rects, paths, lines, text) that matches what is on screen,
|
|
5
|
+
* including theming and dark mode. The view must be rendered and visible.
|
|
6
|
+
*/
|
|
7
|
+
/** Builds an SVG document of the view rendered in `root`. */
|
|
8
|
+
export declare function toSVG(root: HTMLElement): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public data format: what `parse()` accepts, what `serialize()` returns and
|
|
3
|
+
* what the REST API exchanges. Uses snake_case (see CLAUDE.md, Conventions).
|
|
4
|
+
*/
|
|
5
|
+
export type Id = string | number;
|
|
6
|
+
export type Placement = "above" | "on" | "below";
|
|
7
|
+
export type Scale = "hour" | "day" | "week" | "month" | "year";
|
|
8
|
+
export type MilestoneSymbol = "diamond" | "circle" | "square" | "triangle" | "star";
|
|
9
|
+
export type NamePosition = "left" | "right";
|
|
10
|
+
/**
|
|
11
|
+
* Callout label position, in px, relative to the attachment point on the
|
|
12
|
+
* bar's edge: the middle of a task's span, or a milestone's date. `x` is to
|
|
13
|
+
* the label's left edge. `y` is to the label's edge nearest the bar, and
|
|
14
|
+
* also picks the side: `y > 0` is below the bar, `y ≤ 0` above it.
|
|
15
|
+
*/
|
|
16
|
+
export interface Offset {
|
|
17
|
+
/** Px from the attachment point to the label's left edge. */
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ViewData {
|
|
22
|
+
/** Needed when a REST backend is configured. */
|
|
23
|
+
id?: Id;
|
|
24
|
+
/** Shown as a heading above the timelines. Empty for no heading. */
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* `false` (default): each timeline's bar fills the full width, scaled to its
|
|
28
|
+
* own dates. `true`: all timelines share one time axis, from the earliest
|
|
29
|
+
* start to the latest finish, and each bar is drawn at its offset and width
|
|
30
|
+
* on it, so the same date lines up across timelines.
|
|
31
|
+
*/
|
|
32
|
+
absolute?: boolean;
|
|
33
|
+
/** Unit of the time scale. Default: `day`. */
|
|
34
|
+
scale?: Scale;
|
|
35
|
+
/** Side of each timeline the name label is shown on. Default: `left`. */
|
|
36
|
+
name_position?: NamePosition;
|
|
37
|
+
/** Symbol used for milestones. Default: `diamond`. */
|
|
38
|
+
milestone_symbol?: MilestoneSymbol;
|
|
39
|
+
/** Show each timeline's start and finish dates, with "Start" and "Finish" captions, beside its bar. Default: `true`. */
|
|
40
|
+
show_dates?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Clicking a task or timeline selects and highlights it, and fires
|
|
43
|
+
* `onTaskSelected` / `onTimelineSelected`. Default: `true`.
|
|
44
|
+
*/
|
|
45
|
+
selection?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** A task's finish date is not stored: it is `start_date + duration`. */
|
|
48
|
+
export interface TaskData {
|
|
49
|
+
id: Id;
|
|
50
|
+
text: string;
|
|
51
|
+
start_date: string;
|
|
52
|
+
/** A whole number plus `h`, `d`, `w`, `m` or `y`. Missing or zero means milestone. */
|
|
53
|
+
duration?: string;
|
|
54
|
+
/** Overrides the view's `milestone_symbol` for this task. */
|
|
55
|
+
symbol?: MilestoneSymbol;
|
|
56
|
+
/**
|
|
57
|
+
* Any CSS color, such as `"#0a7f5a"` or `"rebeccapurple"`. Overrides the
|
|
58
|
+
* theme's `--tlv-task` (bars) or `--tlv-milestone` (symbols) for this task,
|
|
59
|
+
* on the bar and on its callout's edge marker.
|
|
60
|
+
*/
|
|
61
|
+
color?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Where a task appears on a timeline. The area follows from the fields:
|
|
65
|
+
* - no `callout_offset`: on the bar, on bar `line` (default 0);
|
|
66
|
+
* - `callout_offset` with `y > 0`: a callout below the bar;
|
|
67
|
+
* - `callout_offset` with `y ≤ 0`: a callout above the bar.
|
|
68
|
+
*/
|
|
69
|
+
export interface TaskEntryData {
|
|
70
|
+
task: Id;
|
|
71
|
+
/** 0-based bar line, for an entry on the bar. Default: 0. Not allowed with `callout_offset`. */
|
|
72
|
+
line?: number;
|
|
73
|
+
/** Makes the entry a callout, and places its label (see `Offset`). */
|
|
74
|
+
callout_offset?: Offset;
|
|
75
|
+
}
|
|
76
|
+
/** Shows a (non-global) marker on this timeline. */
|
|
77
|
+
export interface MarkerEntryData {
|
|
78
|
+
marker: Id;
|
|
79
|
+
}
|
|
80
|
+
/** An entry is a task on the timeline, or a marker shown on it. */
|
|
81
|
+
export type EntryData = TaskEntryData | MarkerEntryData;
|
|
82
|
+
/**
|
|
83
|
+
* A date marker: a vertical line at `date` across the bars of the timelines
|
|
84
|
+
* that show it, with a label at its top or bottom end.
|
|
85
|
+
*/
|
|
86
|
+
export interface MarkerData {
|
|
87
|
+
id: Id;
|
|
88
|
+
text: string;
|
|
89
|
+
/** ISO 8601: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm`. */
|
|
90
|
+
date: string;
|
|
91
|
+
/** `true`: shown on every timeline. `false` (default): only on timelines with a `{ marker: id }` entry. */
|
|
92
|
+
global?: boolean;
|
|
93
|
+
/** Any CSS color. Default: the theme's `--tlv-marker` (red). */
|
|
94
|
+
color?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Places the label, like `callout_offset`: px from the line's end on the
|
|
97
|
+
* bar's edge to the label. `y ≤ 0`: above the topmost bar; `y > 0`: below
|
|
98
|
+
* the bottommost. Default: `{ x: 0, y: -16 }`.
|
|
99
|
+
*/
|
|
100
|
+
marker_offset?: Offset;
|
|
101
|
+
}
|
|
102
|
+
export interface TimelineData {
|
|
103
|
+
id: Id;
|
|
104
|
+
name: string;
|
|
105
|
+
/** The timeline's time range. ISO 8601: `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm`. */
|
|
106
|
+
start_date: string;
|
|
107
|
+
finish_date: string;
|
|
108
|
+
entries: EntryData[];
|
|
109
|
+
}
|
|
110
|
+
export interface TimelineViewData {
|
|
111
|
+
/** Server version, used for optimistic concurrency with the REST API. */
|
|
112
|
+
version?: number;
|
|
113
|
+
view: ViewData;
|
|
114
|
+
tasks: TaskData[];
|
|
115
|
+
timelines: TimelineData[];
|
|
116
|
+
/** Date markers. Optional. */
|
|
117
|
+
markers?: MarkerData[];
|
|
118
|
+
}
|
|
119
|
+
/** Body of `PATCH .../entries/{task_id}`, minus `version`. Send only what changes. */
|
|
120
|
+
export interface MovePatchData {
|
|
121
|
+
timeline_id?: Id;
|
|
122
|
+
placement?: Placement;
|
|
123
|
+
line?: number;
|
|
124
|
+
/** With placement `on`: insert a new bar line at `line` (0 to one past the last line); lines from there down shift by one. */
|
|
125
|
+
new_line?: boolean;
|
|
126
|
+
callout_offset?: Offset;
|
|
127
|
+
}
|
|
128
|
+
/** Response of a successful move. */
|
|
129
|
+
export interface MoveResultData {
|
|
130
|
+
version: number;
|
|
131
|
+
timelines: TimelineData[];
|
|
132
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Id, MovePatchData, Offset, TimelineViewData } from "./types";
|
|
2
|
+
export interface TimelineOptions {
|
|
3
|
+
/** Base URL of the REST backend. Without it, changes are only reported through events. */
|
|
4
|
+
api_url?: string;
|
|
5
|
+
/** Replacement for `fetch`, mainly for tests and mock backends. */
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export interface EntryMovedEvent {
|
|
9
|
+
timeline_id: Id;
|
|
10
|
+
task_id: Id;
|
|
11
|
+
patch: MovePatchData;
|
|
12
|
+
}
|
|
13
|
+
export interface MoveFailedEvent extends EntryMovedEvent {
|
|
14
|
+
error: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface MarkerMovedEvent {
|
|
17
|
+
marker_id: Id;
|
|
18
|
+
marker_offset: Offset;
|
|
19
|
+
}
|
|
20
|
+
export interface TimelineEvents {
|
|
21
|
+
/** A move was applied on screen. Fired before the REST request, if any, is sent. */
|
|
22
|
+
entry_moved: EntryMovedEvent;
|
|
23
|
+
/** A move was rejected, locally or by the server. The view shows the restored state. */
|
|
24
|
+
move_failed: MoveFailedEvent;
|
|
25
|
+
/**
|
|
26
|
+
* A marker's label was moved (dragged, or `setMarkerOffset`). Markers have
|
|
27
|
+
* no REST call yet, so saving the new `marker_offset` is up to the host.
|
|
28
|
+
*/
|
|
29
|
+
marker_moved: MarkerMovedEvent;
|
|
30
|
+
}
|
|
31
|
+
type Handler<T> = (event: T) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Handlers for `attachEvent`. Inside a handler written as a `function`, `this`
|
|
34
|
+
* is the TimelineView, with the same methods as the global `timeline`.
|
|
35
|
+
*/
|
|
36
|
+
export interface TimelineHandlers {
|
|
37
|
+
/**
|
|
38
|
+
* A task bar, milestone or callout label was clicked (pressed and released
|
|
39
|
+
* without dragging). `id` is the task's id; `timelineId` the timeline it was
|
|
40
|
+
* clicked on, since a task can appear on several.
|
|
41
|
+
*/
|
|
42
|
+
onTaskClick: (this: TimelineView, id: Id, e: MouseEvent, timelineId: Id) => void;
|
|
43
|
+
/**
|
|
44
|
+
* A timeline was clicked outside its tasks: its name, bar background, time
|
|
45
|
+
* scale, dates or callout areas.
|
|
46
|
+
*/
|
|
47
|
+
onTimelineClick: (this: TimelineView, id: Id, e: MouseEvent) => void;
|
|
48
|
+
/** The selected tasks changed; `ids` are all selected tasks now, empty when none are. */
|
|
49
|
+
onTaskSelected: (this: TimelineView, ids: Id[]) => void;
|
|
50
|
+
/** The selected timelines changed; `ids` are all selected timelines now, empty when none are. */
|
|
51
|
+
onTimelineSelected: (this: TimelineView, ids: Id[]) => void;
|
|
52
|
+
}
|
|
53
|
+
export type TimelineEventName = keyof TimelineHandlers;
|
|
54
|
+
/** Thrown for moves queued behind one that failed; their changes were rolled back with it. */
|
|
55
|
+
export declare class MoveCancelledError extends Error {
|
|
56
|
+
constructor();
|
|
57
|
+
}
|
|
58
|
+
export declare class TimelineView {
|
|
59
|
+
readonly container: HTMLElement;
|
|
60
|
+
private model?;
|
|
61
|
+
private readonly api?;
|
|
62
|
+
private readonly listeners;
|
|
63
|
+
/** `attachEvent` handlers by event id. */
|
|
64
|
+
private readonly handlers;
|
|
65
|
+
private lastEventId;
|
|
66
|
+
/**
|
|
67
|
+
* Selection: tasks or timelines, never both at once. A selected task is
|
|
68
|
+
* highlighted wherever it appears.
|
|
69
|
+
*/
|
|
70
|
+
private selected;
|
|
71
|
+
private readonly detachDrag;
|
|
72
|
+
/** REST requests run one at a time, in the order the moves were made. */
|
|
73
|
+
private queue;
|
|
74
|
+
private pending;
|
|
75
|
+
/** Bumped whenever local state is reset after a failure; queued requests from before are dropped. */
|
|
76
|
+
private generation;
|
|
77
|
+
constructor(container: string | HTMLElement, options?: TimelineOptions);
|
|
78
|
+
/** Loads data in the public format and renders it. */
|
|
79
|
+
parse(data: TimelineViewData): void;
|
|
80
|
+
/** Fetches a view from the REST backend and renders it. */
|
|
81
|
+
load(viewId: Id): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* The view as a standalone SVG document, as it looks on screen now. The
|
|
84
|
+
* view must be rendered in the page and visible, since the export reads
|
|
85
|
+
* its layout.
|
|
86
|
+
*/
|
|
87
|
+
toSVG(): string;
|
|
88
|
+
/** The current data, in the same format `parse()` takes. */
|
|
89
|
+
serialize(): TimelineViewData;
|
|
90
|
+
on<K extends keyof TimelineEvents>(name: K, handler: Handler<TimelineEvents[K]>): () => void;
|
|
91
|
+
/**
|
|
92
|
+
* Subscribes to an event, such as `onTaskClick`. Returns an event id for
|
|
93
|
+
* `detachEvent`. Inside a `function` handler, `this` is this view.
|
|
94
|
+
*/
|
|
95
|
+
attachEvent<K extends TimelineEventName>(name: K, handler: TimelineHandlers[K]): number;
|
|
96
|
+
/** Removes a handler added with `attachEvent`. Returns false if the id is unknown. */
|
|
97
|
+
detachEvent(eventId: number): boolean;
|
|
98
|
+
/** The selected tasks' ids, in the order they were selected; empty when timelines (or nothing) are selected. */
|
|
99
|
+
getSelectedTasks(): Id[];
|
|
100
|
+
/** The selected timelines' ids, in the order they were selected; empty when tasks (or nothing) are selected. */
|
|
101
|
+
getSelectedTimelines(): Id[];
|
|
102
|
+
/**
|
|
103
|
+
* Selects a task, highlighted wherever it appears. With `add`, it joins the
|
|
104
|
+
* tasks already selected (as Ctrl+Click does); otherwise it replaces the
|
|
105
|
+
* selection. Selecting a task always deselects timelines. Fires the selected
|
|
106
|
+
* events for what changed. Returns false when selection is turned off
|
|
107
|
+
* (`view.selection: false`).
|
|
108
|
+
*/
|
|
109
|
+
selectTask(id: Id, add?: boolean): boolean;
|
|
110
|
+
/** Like `selectTask`, for a timeline. Selecting a timeline always deselects tasks. */
|
|
111
|
+
selectTimeline(id: Id, add?: boolean): boolean;
|
|
112
|
+
/** Deselects one task. Returns false if it wasn't selected. */
|
|
113
|
+
unselectTask(id: Id): boolean;
|
|
114
|
+
/** Deselects one timeline. Returns false if it wasn't selected. */
|
|
115
|
+
unselectTimeline(id: Id): boolean;
|
|
116
|
+
/** Deselects everything. */
|
|
117
|
+
clearSelection(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Moves a marker's label, as dragging it does: `y ≤ 0` above the topmost
|
|
120
|
+
* bar showing the marker, `y > 0` below the bottommost. Fires `marker_moved`.
|
|
121
|
+
*/
|
|
122
|
+
setMarkerOffset(markerId: Id, offset: Offset): void;
|
|
123
|
+
/** Moves a timeline one place up the stack. Returns false if it is already at the top. */
|
|
124
|
+
moveTimelineUp(id: Id): boolean;
|
|
125
|
+
/** Moves a timeline one place down the stack. Returns false if it is already at the bottom. */
|
|
126
|
+
moveTimelineDown(id: Id): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Moves a timeline `delta` places: negative is up, positive down. Returns
|
|
129
|
+
* false, changing nothing, past the top or bottom. The new order shows in
|
|
130
|
+
* `serialize()`; the REST API has no call for it yet.
|
|
131
|
+
*/
|
|
132
|
+
moveTimeline(id: Id, delta: number): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Moves an entry: shows the move at once, fires `entry_moved` and, with a
|
|
135
|
+
* REST backend, sends the PATCH. Rolls back and fires `move_failed` if the
|
|
136
|
+
* move is invalid or the server rejects it.
|
|
137
|
+
*/
|
|
138
|
+
moveEntry(timelineId: Id, taskId: Id, patch: MovePatchData): Promise<void>;
|
|
139
|
+
/** Removes the view's DOM and event listeners. */
|
|
140
|
+
destroy(): void;
|
|
141
|
+
private rollback;
|
|
142
|
+
private onDrop;
|
|
143
|
+
/** Calls every `attachEvent` handler for `name`, with this view as `this`. */
|
|
144
|
+
private callHandlers;
|
|
145
|
+
private onMarkerDrop;
|
|
146
|
+
private onEntryClick;
|
|
147
|
+
/** A click on a timeline outside its tasks, or on the view's heading. */
|
|
148
|
+
private onBackgroundClick;
|
|
149
|
+
/**
|
|
150
|
+
* Selection by clicking. Click: select only this item, or, if it is
|
|
151
|
+
* already selected, clear the selection. Ctrl+Click (Cmd+Click on a Mac):
|
|
152
|
+
* add or remove this item; on the other kind of item it starts a new
|
|
153
|
+
* selection with just this one.
|
|
154
|
+
*/
|
|
155
|
+
private clickSelect;
|
|
156
|
+
private isSelected;
|
|
157
|
+
private select;
|
|
158
|
+
private unselect;
|
|
159
|
+
/**
|
|
160
|
+
* Sets the selection and updates the highlight. Fires `onTaskSelected`
|
|
161
|
+
* and/or `onTimelineSelected` for each kind whose selection changed (the
|
|
162
|
+
* kind being left reports an empty list).
|
|
163
|
+
*/
|
|
164
|
+
private setSelection;
|
|
165
|
+
/** After new data: forgets selected tasks or timelines that are gone, or everything if selection is off. */
|
|
166
|
+
private dropStaleSelection;
|
|
167
|
+
/** Marks the selected tasks' elements, or the selected timelines, in the DOM. */
|
|
168
|
+
private applySelection;
|
|
169
|
+
private emit;
|
|
170
|
+
private requireModel;
|
|
171
|
+
private render;
|
|
172
|
+
}
|
|
173
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ar-timeline-view",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A web-based timeline component for displaying and editing timelines.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/timelineview.js",
|
|
11
|
+
"module": "./dist/timelineview.mjs",
|
|
12
|
+
"types": "./dist/types/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/types/index.d.ts",
|
|
16
|
+
"import": "./dist/timelineview.mjs",
|
|
17
|
+
"default": "./dist/timelineview.js"
|
|
18
|
+
},
|
|
19
|
+
"./timelineview.css": "./dist/timelineview.css"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "vite build && tsc -p tsconfig.build.json && node scripts/export-examples.mjs",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"lint": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"jsdom": "^31.0.0",
|
|
30
|
+
"puppeteer": "^25.16.0",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"vite": "^8.3.6",
|
|
33
|
+
"vitest": "^5.1.0"
|
|
34
|
+
}
|
|
35
|
+
}
|