Triiiceratops Drive programmatically

Drive Triiiceratops Programmatically

Reach into a viewer's live state from your own code: read where it is, command it to move, and hear when it changes.

Every viewer owns one live ViewerState object. It is the same object the viewer's own toolbar, panels, and plugins use, and your code can access it too. Use it to read the current position, call commands to change it, and subscribe to changes.

Commands can do anything the built-in UI can do. Use them to build your own page controls, connect the viewer to the rest of your app, or script it from a test.

  1. Get the state from your host: a property on the element, a hook in React, a template ref in Vue, a bound prop in Svelte.

  2. Call commands on it, such as nextCanvas(), setCanvas(id), or zoomIn().

  3. Read and subscribe when your UI needs to show what the viewer is doing.

Only step 1 differs between hosts. From step 2 on, the code is the same everywhere.

1. Get the state

The element exposes the state as a getter-only viewerState property, and fires viewerstateavailable when it appears:

import 'triiiceratops/element/register';
import type { TriiiceratopsViewerElement } from 'triiiceratops';

type ViewerState = NonNullable<TriiiceratopsViewerElement['viewerState']>;

const el = document.querySelector<TriiiceratopsViewerElement>(
    'triiiceratops-viewer',
)!;

function drive(state: ViewerState) {
    document
        .querySelector('button#next')!
        .addEventListener('click', () => state.nextCanvas());
}

// Listen first, then check: catches state published before or after this runs.
el.addEventListener('viewerstateavailable', (event) => {
    drive((event as CustomEvent<ViewerState>).detail);
});
if (el.viewerState) drive(el.viewerState);

2. Call commands

Commands are methods on the state. Call them; do not assign to its properties. state.setCanvas(id) updates everything that depends on the canvas, while state.canvasId = id skips that work and is not a supported API.

import type { ViewerState } from 'triiiceratops';

declare const state: ViewerState;

// Step through the manifest, or jump to a canvas by id.
state.nextCanvas();
state.setCanvas('https://example.org/iiif/canvas/p3');

// Open the annotations panel if it is closed.
if (!state.showAnnotations) state.toggleAnnotations();

// Frame a detail of the current canvas, in canvas coordinates.
state.fitBounds({ x: 1200, y: 800, width: 600, height: 400 });

// Open a different manifest at a chosen canvas.
void state.setManifest('https://example.org/iiif/other/manifest.json', {
    canvasId: 'https://example.org/iiif/other/canvas/1',
});

Common commands

Command

What it does

nextCanvas(), previousCanvas()

Step one canvas, or one spread in paged mode. Does nothing at either end.

setCanvas(canvasId, time?, region?)

Go to a canvas. Optionally start media at { seconds } or frame a region { x, y, width, height }.

setSequenceIndex(index)

Switch to another sequence in a manifest that has several.

selectChoice(canvasId, choiceId)

Pick one image from a IIIF Choice on a canvas.

setManifest(manifestId, { canvasId? })

Fetch and open a manifest, optionally at a canvas. Returns a promise.

setManifestData(manifestId, json, { canvasId? })

Open manifest JSON you already have. Returns a promise.

zoomIn(), zoomOut()

Zoom one step, like the toolbar's + and − buttons.

zoomTo(scale)

Zoom to an absolute scale, in screen pixels per canvas unit (the unit viewportScale reads).

panTo({ x, y }, canvasId?)

Centre the view on a point in canvas coordinates.

fitBounds({ x, y, width, height }, canvasId?)

Frame a box in canvas coordinates.

fitCanvas(canvasId?)

Fit a whole canvas: the current one, or go to the named one and fit it.

fitView()

Re-fit what is on screen, like the Home key.

toggleAnnotations(), toggleSearchPanel(), toggleMetadataPanel(), toggleStructuresPanel(), toggleCollectionPanel(), toggleCanvasInfo()

Open or close that panel.

toggleThumbnailGallery(), setGalleryExpanded(expanded), setDockSide(side)

Show the gallery, expand it into a grid, or dock it to 'top', 'bottom', 'left', or 'right'.

toggleToolbar()

Open or close the toolbar.

toggleFullScreen()

Enter or leave fullscreen. Browsers only allow this from a user gesture, such as a click handler.

setViewingMode(mode)

Switch between 'individuals', 'paged', and 'continuous'.

togglePagedOffset()

In paged mode, shift the spreads by one page, for example to show a cover on its own.

setLocale(locale)

Change the chrome's language. null reverts to config.locale.

search(query)

Run a IIIF content search; results are written to searchResults. It does not open the search panel. Returns a promise. See programmatic search.

setAnnotationVisible(id, visible), setAllAnnotationsVisible(visible)

Show or hide annotation overlays.

setActiveAnnotationId(id)

Select an annotation, as tapping it does. Passing the selected id again deselects it.

setImageAdjustments({ brightness, contrast, saturation, invert, grayscale }), resetImageAdjustments()

Adjust how the image renders. Pass only the keys you change; numbers are percentages, with 100 meaning unchanged.

setPluginOpen(pluginId, open), togglePluginOpen(pluginId)

Open or close a plugin's panel or flyout. See controlling plugin UI at runtime.

getPluginState(pluginId)

Access a plugin's own commands, such as the audio and video plugin's play() and pause(). See commanding playback.

This is the common set, not the complete list. The state inventory records every member of ViewerState and the command that changes it. To change configuration, set the config prop or property as described in Configuration.

3. Read and subscribe

Read members directly. Reads are synchronous and always current.

Member

Holds

manifestId, canvasId

The open manifest and canvas, or null.

canvases, currentCanvasIndex

The current sequence's canvases, and the open one's position in it (-1 when none).

hasNext, hasPrevious

Whether nextCanvas() or previousCanvas() would move.

viewingMode

'individuals', 'paged', or 'continuous'.

showAnnotations, showSearchPanel, showMetadataPanel, showStructuresPanel, showThumbnailGallery

Whether each panel is open.

searchQuery, searchResults, isSearching

The last search and its results.

visibleCanvasIds

The canvases on screen: one, a spread, or the folios in view while scrolling.

rendererReady

Whether zoom, pan, and fit commands will take effect yet.

viewportScale, viewportCentre, viewportBounds

Where the view is right now. Read these on demand; they change every frame and never notify.

To be notified of changes, call subscribe(). Notifications are batched and carry no payload: they mean “state changed, read what you need”. It returns a function that unsubscribes.

import type { ViewerState } from 'triiiceratops';

declare const state: ViewerState;
declare const counter: HTMLElement;

const stop = state.subscribe(() => {
    counter.textContent = `${state.currentCanvasIndex + 1} / ${state.canvases.length}`;
});

// Later, when your control goes away:
stop();

Viewport values are the exception: they move every frame, so subscribe() does not notify for them. Use subscribeFrame(), which has the same shape and fires as the view animates.

In a framework, prefer its reactive reads over a manual subscription: useViewerSelector() in React and Vue, or plain $derived in Svelte. The custom element also fires DOM events such as canvaschange; see reacting to state changes.

Common pitfalls

  • Viewport commands need a renderer. zoomIn(), panTo(), and the fit commands do nothing until rendererReady is true.

  • `manifestId` and `canvasId` set where the viewer starts. They do not hold it there: treat them as starting points, and call setCanvas() to navigate later. See controlling the active canvas.

  • One state per viewer. Two viewers on a page have two independent states. If the element is removed and re-added, it gets a new state and fires viewerstateavailable again; drop the old one. The React and Vue handles follow this for you.

For a full custom toolbar in every framework, see building your own chrome.