Temporeum Timelinev1.0.2

Groups and item kinds

Nested groups you can collapse and reorder, four kinds of item, adding and removing items from the timeline itself, and HTML templates. Everything on this page works with the mouse, with a finger and with the keyboard alone, and every change that touches your data leaves as a command with an inverse: the library never saves.

The plugins

Both halves of this page are plugins, each on its own entry point. Nested groups are groupsPlugin, from @temporeum/timeline-element/groups:

import { createTimeline, registerPlugin } from "@temporeum/timeline-element";
import { groupsPlugin } from "@temporeum/timeline-element/groups";

// Nested groups are a plugin: registered once, before the first timeline.
registerPlugin(groupsPlugin);

From the running example example/groups

The box, point and background kinds, templates, and adding, removing and dropping items are itemsPlugin, from @temporeum/timeline-element/items:

import { createTimeline, registerPlugin } from "@temporeum/timeline-element";
import { itemsPlugin } from "@temporeum/timeline-element/items";

// Boxes, points and backgrounds are a plugin: registered once, before the first timeline.
registerPlugin(itemsPlugin);

From the running example example/item-kinds

They import the core's @temporeum/timeline-core/groups and @temporeum/timeline-core/items, so a timeline of plain lanes and ranges — a Gantt chart's tasks among them — downloads neither. Moving and resizing need no plugin; nor does kind: "range", the default (kind: "milestone" is the Gantt plugin's). Without its plugin, groups, an item of kind "box", "point" or "background", template, or editing.add, editing.remove or editing.drop throws a TypeError where the timeline is created, updated (update, setItems, addItems with such an item) or rendered on a server, naming the import that is missing — An item of kind "point" needs the items plugin: import { itemsPlugin } from "@temporeum/timeline-element/items" and call registerPlugin(itemsPlugin) before the timeline draws — and the instance is left as it was; the items of a fetched source are checked when they arrive. registerPlugin is the page's: once is enough for every timeline on it, and the server renderer draws with it too. The <script>-tag build registers every plugin on load.

Nested groups

const groups = [
  { id: "web", label: "Web team" },
  { id: "ann", label: "Ann", parentId: "web" },
  { id: "bob", label: "Bob", parentId: "web" },
  { id: "ops", label: "Operations" },
  { id: "eve", label: "Eve", parentId: "ops" },
];

From the running example example/groups

A group names its parent with parentId, and an item names its group with laneId. Each group is drawn as a header row — the group's name, indented by its depth — followed by the rows of its own items and then its subgroups. When groups is given, lanes is ignored.

Live example: Nested groups you can collapse and reorder

Loading…

Run “Nested groups you can collapse and reorder” on your own machine
  1. Make an empty folder and put this example’s two files in it:

    index.html — the page the code runs in, its markup and styles:

    <div id="timeline" style="height: 360px"></div>
    <p class="log" id="log" aria-live="polite">Click a group to collapse it, drag it to reorder — or Tab to the groups and use the arrows, Enter and Alt + arrow.</p>
    

    main.js — the code, as it is in the editor now, your edits included:

  2. In a terminal in that folder, install the library and Vite (it lets the browser follow the imports), then start it:

    npm init -y
    npm install @temporeum/timeline-element
    npm install --save-dev vite
    npx vite
  3. Open the address it prints, http://localhost:5173/.

  • Collapse and expand: click or tap a header, or Tab to the groups and press Enter, Space, or the arrow that points into or out of the tree (right and left in a horizontal timeline, down and up in a vertical one). A collapsed group's items and subgroups are gone from the drawing and from the accessibility tree; expanding it brings back exactly the layout there was. This is view state, like the selection: the timeline reports it (groupToggled) and keeps it, and group.collapsed is only where it starts.
  • Reorder: drag a header along its siblings, or press Alt with the arrow along the tree. A reorder changes your data, so it comes out as a groupMoved whose command.inverse undoes it — timeline.applyCommand(command.inverse).
  • Walk the tree: the arrows along the tree, Home and End. The headers are a role="tree" beside the timeline's listbox, with each group's level, position and expanded state.

Item kinds

    { id: "sprint", kind: "background", start: start, end: start + 14 * day, label: "Sprint 1" },
    { id: "build", start: start + day, end: start + 9 * day, label: "Build" },
    { id: "review", kind: "box", start: start + 11 * day, label: "Review" },
    { id: "freeze", kind: "point", start: start + 13 * day, label: "Code freeze" },

From the running example example/item-kinds

kindDrawn as
range (default)A bar over its span.
boxThe item's bar, with a line from its middle to the axis.
pointA dot at its start, with a line to the axis.
backgroundA band behind every row. It is not an item you can select, drag or hit; it never covers one.

Live example: Ranges, boxes, points and a background

Loading…

Run “Ranges, boxes, points and a background” on your own machine
  1. Make an empty folder and put this example’s two files in it:

    index.html — the page the code runs in, its markup and styles:

    <div id="timeline" style="height: 260px"></div>
    

    main.js — the code, as it is in the editor now, your edits included:

  2. In a terminal in that folder, install the library and Vite (it lets the browser follow the imports), then start it:

    npm init -y
    npm install @temporeum/timeline-element
    npm install --save-dev vite
    npx vite
  3. Open the address it prints, http://localhost:5173/.

A box and a point are packed in time like any instant, so their rows do not change while you zoom. A background spans every row: a background bound to one group's rows is not supported yet.

Adding and removing items

  editing: {
    enabled: true,
    add: true,
    remove: true,
    drop: true,
    snapMs: day,
    canRemove: (item) => item.id !== "ship",
  },

From the running example example/add-remove

PointerKeyboard
AddDouble-click or double-tap empty row space. (On the axis a double tap still zooms.)n or Insert: in the focused item's row, in the middle of the view.
RemoveThe remove handle drawn on a selected item.Delete or Backspace on the focused item.
Drop inDrag an element carrying application/x-temporeum-item data onto a row.timeline.dropItem(payload), bound to your list's own key: the focused row, the middle of the view.

The dropped data is a JSON object with an item's fields. It lands where it was dropped: start and end (epoch milliseconds or date strings), or a durationMs, give only its length, and the row decides its lane. Fields an item does not have, or values that are not valid for them (a sublaneSpan of 0, a progress outside 0–1), are left out. A drop can come from any page or app the reader drags from, so treat its fields as untrusted input: data reaches the item as it was dropped, unchecked — check it in canAdd (or build the item yourself in createItem) before anything renders it as HTML.

Both ways reach the same command. editing.createItem(target) makes the item an add creates (by default a range with a free item-N id); canAdd and canRemove veto, and a removal of a selection is refused whole if one item is refused. The command's patches carry the whole item and its place in the list, so the inverse of a removal puts the item back exactly where it was. Adding and removing is not available with a fetched source: the source owns its items.

Live example: Add, remove and drop items in

Loading…

Run “Add, remove and drop items in” on your own machine
  1. Make an empty folder and put this example’s two files in it:

    index.html — the page the code runs in, its markup and styles:

    <ul class="sources" aria-label="Tasks to schedule">
      <li draggable="true" tabindex="0" data-task='{"label":"Write docs","durationMs":259200000}'>Write docs</li>
      <li draggable="true" tabindex="0" data-task='{"label":"Code review","durationMs":86400000}'>Code review</li>
    </ul>
    <div id="timeline" style="height: 260px"></div>
    <p class="log" id="log" aria-live="polite">Double-click empty space or press n to add; select and press Delete to remove; drag a task in, or focus it and press Enter.</p>
    

    main.js — the code, as it is in the editor now, your edits included:

  2. In a terminal in that folder, install the library and Vite (it lets the browser follow the imports), then start it:

    npm init -y
    npm install @temporeum/timeline-element
    npm install --save-dev vite
    npx vite
  3. Open the address it prints, http://localhost:5173/.

HTML templates

  template: (item) => `<strong>${item.label ?? ""}</strong>`,

From the running example example/add-remove

A template draws each item's content as HTML, in place of its label. It is sanitised by default — the string is tokenised by the library, never given to innerHTML: <script>, <style>, <iframe> and their content are dropped, every on… handler is dropped, and so is every tag and attribute outside an allow-list (formatting tags; class, title, lang, dir, datetime, data-*, aria-*). templateOptions widens or narrows the lists, but a widened list still keeps only an http, https, mailto, tel or relative address, a style that loads nothing, and never <base>, <meta>, <link> or <form>. The timeline's own markers — a data-tl-* attribute, a tl-* class — are dropped too, so one item's template cannot act as another item's control. sanitize: false keeps everything and is only for a template you wrote from data you trust. The same template gives the same markup on the server and in the browser, so a server-rendered timeline hydrates with it. The item's accessible name stays its label and date range — so a template should show only words that are in that name, in the same order (WCAG 2.5.3, Label in Name): a speech-input user says what they see. To show more, put it in the label.

Without the element

The pieces are pure functions on two entry points of the core, for a host that drives the engine itself: groupView, moveGroupCommand and groupKeyStep on @temporeum/timeline-core/groups; itemKindHooks, splitBackgrounds, addCommand, removeCommand, applyItemPatches and templateHooks on @temporeum/timeline-core/items. The tree of group headers over them is the element's own, inside groupsPlugin. See entry points.