Viewer
A UI component that renders a multicanvas IIIF item viewer with pan-zoom support for Image
via OpenSeadragon (opens in a new tab) and Video
and Sound
content resources using the HTML video element (opens in a new tab).
Features
Provide a IIIF Presentation API (opens in a new tab) Manifest or Collection and the component:
- Renders a multi-canvas Video, Sound, and Image viewer
- Renders thumbnails as navigation between canvases
- Renders annotations with the motivation (opens in a new tab) of
supplementing
with a content resource having the format oftext/vtt
for Video and Sound - Video and Sound are rendered within a HTML5
<video>
element - Image canvases are renderered with OpenSeadragon (opens in a new tab)
- Supports HLS streaming for Video and Audio canvases
- Supports IIIF Collections and toggling between child Manifests
- Supports
placeholderCanvas
for Image canvases.
Installation
npm install @samvera/clover-iiif
Usage
React
Add the Viewer
component to your jsx
or tsx
code.
import Viewer from "@samvera/clover-iiif/viewer";
Render Viewer
with a IIIF Manifest or Collection URI. The only required prop is the iiifContent
, which is the URI of the IIIF Manifest or Collection.
<Viewer iiifContent="https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif" />
Vanilla JavaScript
The Viewer can also be implemented in Vanilla Javascript by use of a web component. This web component example sources a registered <clover-viewer>
web component.
<html>
<head>
<title>Clover IIIF - Viewer - Web Component</title>
<meta charset="UTF-8" />
</head>
<body>
<script src="https://www.unpkg.com/@samvera/clover-iiif@latest/dist/web-components/index.umd.js"></script>
<clover-viewer
id="https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif"
/>
</body>
</html>
Next.js
Implementation with Next.js requires a dynamic import (opens in a new tab) utilizing next/dynamic
. This is due to Next's node compilation method creating issue with an OpenSeadragon
(a dependency of Clover IIIF) assumption of a browser document
object.
import dynamic from "next/dynamic";
const Viewer = dynamic(
() => import("@samvera/clover-iiif").then((Clover) => Clover.Viewer),
{
ssr: false,
},
);
const MyCustomViewer = () => {
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd?as=iiif";
return <Viewer iiifContent={iiifContent} />;
};
API Reference
Viewer
can configured through an options
prop, which will serve as a object for common options.
Prop | Type | Required | Default |
---|---|---|---|
iiifContent | string | Yes | |
iiifContentSearchQuery | See Content Search | No | |
canvasIdCallback | function | No | |
customDisplays | See Custom Displays | No | |
customTheme | object | No | |
plugins | See Plugins | No | |
options | object | No | |
options.background | string CSS (opens in a new tab) | No | transparent |
options.canvasBackgroundColor | string CSS (opens in a new tab) | No | #1a1d1e |
options.canvasHeight | string CSS (opens in a new tab) | No | 500px |
options.ignoreCaptionLabels | string[] | No | [] |
options.openSeadragon | OpenSeadragon.Options | No | |
options.informationPanel | See Information Panel | No | |
options.requestHeaders | IncomingHttpHeaders | No | { "Content-Type": "application/json" } |
options.showDownload | boolean | No | true |
options.showIIIFBadge | boolean | No | true |
options.showTitle | boolean | No | true |
options.withCredentials | boolean | No | false |
options.contentSearch | See Content Search | No |
- Options
canvasBackgroundColor
andcanvasHeight
will apply to both<video>
elements and the OpenseaDragon canvas. - Option
withCredentials
being set astrue
will inform IIIF resource requests to be made using credentials (opens in a new tab) such as cookies, authorization headers or TLS client certificates. - Option
options.openSeadragon
will grant you ability to override the OpenSeadragon default options (opens in a new tab) set within the Clover IIIF Viewer to adjust touch and mouse gesture settings and various other configurations.
Canvas Height
The height of the canvas can be set using the options.canvasHeight
prop. This prop accepts a string value that is a valid CSS height value. The default value is 500px
.
Automatic Height
If the height is set to auto
or 100%
, the Viewer will expand to the height of its wrapping container element. The wrapping element must have a position relative
, along with a defined height
for this to display as expected. Be aware to set a z-index
value of 0
or an applicable value within your consuming application to ensure the viewer does not conflict with other page elements.
export default function App() {
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
const options = {
canvasHeight: "auto", // or "100%"
};
return (
<div style={{ position: "relative", height: "80vh", zIndex: "0" }}>
<Viewer iiifContent={iiifContent} options={options} />
</div>
);
}
Information Panel
The information panel is a collapsible panel that displays information about the current Manifest and renders supplementing resources for the active canvas. It is rendered by default, but can be configured to be hidden or to render only certain tabs.
Prop | Type | Required | Default |
---|---|---|---|
options.informationPanel.open | boolean | No | true |
options.informationPanel.vtt.autoScroll | See Auto Scroll | No | true |
options.informationPanel.renderAbout | boolean | No | true |
options.informationPanel.renderAnnotation | boolean | No | true |
options.informationPanel.renderSupplementing | boolean | No | true |
options.informationPanel.renderToggle | boolean | No | true |
options.informationPanel.renderContentSearch | boolean | No | true |
options.informationPanel.defaultTab | string | No |
If renderAbout
is true, Clover will use the About tab as the default tab. Use options.informationPanel.defaultTab
to set the default tab.
If you want content search to be the default tab, use manifest-content-search
<Viewer
iiifContent="https://example.com/manifest/1"
options={{
informationPanel: {
defaultTab: "manifest-content-search",
},
}}
/>
If the manifest has annotations, and you want one of the anotations to be the default tab, use the id from the annotation page.
{
"@context": ["http://iiif.io/api/presentation/3/context.json"],
"type": "Manifest",
...
"items": [
{
"id": "http://example.com/manifest/canvas/1",
"type": "Canvas",
"height": 1000,
"width": 2000,
"annotations": [
{
"id": "http://example.com/manifest/annotation/1",
"type": "AnnotationPage",
"items": [...]
}
]
}
]
}
<Viewer
iiifContent="https://example.com/manifest/1"
options={{
informationPanel: {
defaultTab: "http://example.com/manifest/annotation/1",
},
}}
/>
Auto Scroll
When VTT annotations are displayed, the Clover IIIF Viewer can automatically scroll the information panel to keep the currently active caption in view. Whether it does so (and how) is governed by the informationPanel.vtt.autoScroll
configuration option. The possible values are:
{ behavior: (behavior), block: (block) }
: auto-scroll using the given settings (see below).true
: Auto-scroll using the default behavior. This is equivalent to{ behavior: "smooth", block: "center" }
.false
: Do not auto-scroll.
The settings take the form { behavior: "auto" | "instant" | "smooth", block: "center" | "end" | "nearest" | "start" }
, and have the same effect as the scrollIntoViewOptions
object documented with the Element.scrollIntoView()
(opens in a new tab) Web API method. (The inline
option does not apply since there is no horizontal scrolling involved.)
Custom Displays
Clients may wish to use their own display components (for example a PDF Viewer, or an audio player, etc). To configure custom displays, use the customDisplays
prop, which is an array of objects defining display
and target
properties. See an example implementation
display.component
is a custom React component, and display.componentProps
are pass-through props which Viewer
will attach to your Custom Display component. The target
object provides two methods of matching a Canvas to a Custom Display: target.canvasId
which is a manifest's canvas id
. Or by target.paintingFormat
(ie. application/pdf
) which is the body.type
in a canvas's Annotation
of type "painting".
Prop | Type | Required | Default |
---|---|---|---|
customDisplays.display.component | React.Node | No | |
customDisplays.display.componentProps | object | No | |
customDisplays.target.canvasId | string[] | No | |
customDisplays.target.paintingFormat | string[] | No |
Content search
Clover supports IIIF content search v2. In order to display content search in the information panel, add a content search service to the manifest.
{
"@context": ["http://iiif.io/api/presentation/3/context.json"],
"type": "Manifest",
...
"service": [
{
"id": "https://example.com/search",
"type": "SearchService2"
}
]
}
Prop | Type | Required | Default |
---|---|---|---|
iiifContentSearchQuery.q | string | No | |
options.contentSearch.searchResultsLimit | number | No | 20 |
options.contentSearch.overlays.backgroundColor | string | No | #ff66660 |
options.contentSearch.overlays.borderColor | string | No | #990000 |
options.contentSearch.overlays.borderType | string | No | solid |
options.contentSearch.overlays.borderWidth | string | No | 1px |
options.contentSearch.overlays.opacity | string | No | 0.5 |
options.contentSearch.overlays.renderOverlays | boolean | No | true |
options.contentSearch.overlays.zoomLevel | number | No | 4 |
Many sites have a search functionality that returns a list objects that match the search terms. Clicking on a search result will display a details page for one object. If you want to highlight the search terms when the Clover image viewer loads on the details page, use iiifContentSearchQuery
when setting up the viewer.
<Viewer
iiifContent="https://example.com/manifest/1"
iiifContentSearchQuery={{
q: "my search terms",
}}
/>
options.contentSearch.searchResultsLimit
is the maximum number of search results to display per canvas in the information panel. If set to undefined
, Clover will show all search results.
By default, Clover's content search will draw light red boxes in the image viewer for each search result returned by the search service. You can set the appearance of the highlighted boxes using options.contentSearch.overlays
. If you don't want the search results to be highlighted, set options.contentSearch.overlays.renderOverlays
to false
.
When you click on the list of search results, Clover will pan and zoom to the location of that search result. You can set the zoom level using options.contentSearch.overlays.zoomLevel
. A small zoom level will zoom in real close; a large zoom level will zoom in less.
Deprecated Options
Prop | In Favor Of | Deprecated |
---|---|---|
id | iiifContent | v2.0.0 |
manifestId | iiifContent | v2.0.0 |
options.renderAbout | options.informationPanel.renderAbout | v2.0.3 |
options.showInformationToggle | options.informationPanel.renderToggle | v2.0.3 |
Basic Configuration
Example customization of various options
.
const options = {
// Primary title (Manifest label) for top level canvas. Defaults to true
showTitle: false,
// IIIF Badge and popover containing options. Defaults to true
showIIIFBadge: false,
// Ignore supplementing canvases by label value that are not for captioning
ignoreCaptionLabels: ['Chapters'],
// Override canvas background color, defaults to #1a1d1e
canvasBackgroundColor: "#000",
// Set canvas zooming onScoll (this defaults to false)
openSeadragon: {
gestureSettingsMouse: {
scrollToZoom: true;
}
}
}
<Viewer
iiifContent="https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif"
options={options}
/>
Active Canvas
Example on using canvasIdCallback
to return to your consuming application the active canvas ID. This will return as a string.
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
const handlCanvasIdCallback = (activeCanvasId) => {
if (activeCanvasId) console.log(activeCanvasId);
};
return (
<Viewer iiifContent={iiifContent} canvasIdCallback={handlCanvasIdCallback} />
);
Captions
WebVTT content resources are the source for both content mapped closed captioning <track/>
elements in the HTML 5 video player and to the navigator panel adjacent to it. You may ignore these resources as tracks if they are not intended for closed captioning or subtitling by string values matching the label of the content resource. This is a manual option within the viewer as there is no defined way for a manifest to prescribe motivation for these resources beyond supplementing
.
export default function App() {
const iiifContent =
"https://raw.githubusercontent.com/samvera-labs/clover-iiif/main/public/fixtures/iiif/manifests/captions.json";
const options = {
ignoreCaptionLabels: ["Chapters"],
};
return <Viewer iiifContent={iiifContent} options={options} />;
}
Custom Theme
You may choose to override the base theme by setting optional colors and fonts. Naming conventions for colors are limited to those shown in the config example below.
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
const customTheme = {
colors: {
/**
* Black and dark grays in a light theme.
* All must contrast to 4.5 or greater with `secondary`.
*/
primary: "#37474F",
primaryMuted: "#546E7A",
primaryAlt: "#263238",
/**
* Key brand color(s).
* `accent` must contrast to 4.5 or greater with `secondary`.
*/
accent: "#C62828",
accentMuted: "#E57373",
accentAlt: "#B71C1C",
/**
* White and light grays in a light theme.
* All must must contrast to 4.5 or greater with `primary` and `accent`.
*/
secondary: "#FFFFFF",
secondaryMuted: "#ECEFF1",
secondaryAlt: "#CFD8DC",
},
fonts: {
sans: "'Helvetica Neue', sans-serif",
display: "Optima, Georgia, Arial, sans-serif",
},
};
return <Viewer iiifContent={iiifContent} customTheme={customTheme} />;
CSS Classes
Additional CSS classes are made available on structural HTML elements in the Viewer, which may be referenced in a client's own CSS files/style definitions to further customize the Viewer's appearance. You may inspect the DOM to see classes applied to each element, but in general it follows a pattern similar to:
<div class="clover-viewer">
<header class="clover-viewer-header" />
<div class="clover-viewer-content">
<div class="clover-viewer-painting">...</div>
</div>
</div>
Request Headers
In some cases, a client may need to request Manifest or Collection resources with custom request headers, ex: Authorization
. This can be done by passing a requestHeaders
object to the options
prop. This object will be passed to the request call made by the Viewer. Accepted header keys are defined in the IncomingHttpHeaders (opens in a new tab) interface.
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
return (
<Viewer
iiifContent={iiifContent}
options={{
requestHeaders: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
}}
/>
);
Custom canvas displays
Clients may wish to use their own display components instead of Clover Viewer's default displays (OpenSeadragon (opens in a new tab) for images and HTML Video Player (opens in a new tab) for audio/video). The Viewer
component allows a client to target individual canvas
items in a IIIF Manifest by either direct reference to a canvas id
or format
(ie. video/ogg
). See the Type Definition below for CustomDisplay
, and an example implementation.
import AnotherCustomDisplay from "./AnotherCustomDisplay";
type CustomDisplay = {
display: {
component: React.ElementType;
componentProps: {
// Any custom props you want to pass to your component
[key: string]: any;
};
};
target: {
canvasId: string[];
paintingFormat: string[]; // "application/pdf" or "application/epub+zip"
};
};
function MyCustomDisplay({ id, annotationBody, ...restProps }: CustomDisplay) {
return (
<div>
<h1>My Custom Display</h1>
<p>Canvas ID: {id}</p>
<p>Annotation Body:</p>
<pre>{JSON.stringify(annotationBody)}</pre>
<p>Custom props:</p>
<pre>{JSON.stringify(restProps)}</pre>
...your display here
</div>
);
}
<Viewer
iiifContent={iiifContent}
customDisplays={[
{
display: {
component: MyCustomDisplay,
componentProps: {
foo: "bar",
},
},
target: {
canvasId: [
"https://uri-for-a-canvas-id/access/0",
"https://uri-for-a-canvas-id/access/1",
],
},
},
{
display: {
component: AnotherCustomDisplay,
},
target: {
paintingFormat: ["application/pdf", "image/gif"],
},
},
]}
/>;
The Viewer
component will pass the following props to your custom display component:
id
: The canvasid
for the resource being rendered. This may be helpful if you wanted to use the canvasid
to fetch additional data from your application's API.annotationBody
: Thebody
value for a canvasAnnotation
item withmotivation
"painting".
{
"id": "https://uri-for-a-canvas-id/access/0",
"type": "Annotation",
"motivation": "painting",
"body": {
"format": "application/pdf",
"height": 1686,
"id": "http://localhost:3000/media/pdf/file-sample_150kB.pdf",
"width": 1192
}
}
See a complete recipe for a PDF Viewer (opens in a new tab) using custom canvas displays.
Plugins
Clover supports 3rd-party plugins as a way to add more functionality to Clover. To add plugins, use the plugins
prop. plugins
will accept an array of objects, with each object representing a plugin.
People can use prebuilt plugins or create their own plugins. To install a prebuilt plugin, people should follow the instructions for the particular plugin.
Here's an example of using two plugins. PluginA adds a button to imageViewer.controls
. PluginB adds a tab to informationPanel
.
import PluginA from "plugin-A";
import PluginB from "plugin-B";
export default function App() {
return (
<Viewer
iiifContent={iiifContent}
plugins={[
{
id: "Plugin A",
imageViewer: {
controls: {
component: PluginA,
},
},
},
{
id: "Plugin B",
informationPanel: {
component: PluginB,
componentProps: {
objectId: 456,
},
label: { none: ["My Plugin"] },
},
},
]}
/>
);
}
Prop | Type | Required | Default |
---|---|---|---|
id | string | yes | |
imageViewer.controls | object | No | |
imageViewer.controls.component | React Component | yes | |
imageViewer.controls.componentProps | object | no | |
informationPanel | object | No | |
informationPanel.component | React Component | yes | |
informationPanel.componentProps | object | no | |
informationPanel.label | object | yes |
id
- unique id for the plugin
imageViewer.controls
- If you want to add a custom component to image viewer controls, use imageViewer.controls
.
imageViewer.controls.component
- The component to render.
imageViewer.controls.componentProps
- Props passed to the controls component.
informationPanel
- If you want to add a custom component to the information panel, use informationPanel
.
informationPanel.component
- The component to render.
informationPanel.componentProps
- Props passed to the information panel component.
informationPanel.label
- The text that is displayed in the tab.
Instructions for creating a plugin
Clover offers these features to support plugins:
- Renders custom React components in certain areas of the Clover Viewer. Areas include image viewer controls and information panel.
- Gives plugins the ability to access and change the state of the Clover Viewer via props
- Pass in props to the plugin components
Prop | Description |
---|---|
canvas | active canvas object |
useViewerDispatch | React hook to update the Viewer context store (opens in a new tab) |
useViewerState | React hook to access the Viewer context store (opens in a new tab) |
Here is an example plugin that has both imageViewer.controls
and informationPanel
. Plugin components have access to canvas, useViewerDispatch, and useViewerState via props.
// PluginButton.tsx
export default function PluginButton(props) {
const { canvas, useViewerDispatch, useViewerState } = props;
// use useViewerState to access to viewer state properties such as openSeadragonViewer, etc
const viewerState = useViewerState();
const { openSeadragonViewer, activeManifest } = viewerState;
// use useViewerDispatch to update viewer state
const dispatch: any = useViewerDispatch();
function clickHandler() {
dispatch({
type: "updateOSDImageLoaded",
OSDImageLoaded: false,
});
}
return (
<button
onClick={clickHandler}
style={{
backgroundColor: "var(--colors-primary)",
borderRadius: "2rem",
width: "2rem",
margin: "0px 0px 0px 0.618rem",
}}
>
<b>P</b>
</button>
);
}
// PluginInfoPanel.tsx
export default function PluginInfoPanel(props) {
const {
canvas,
useViewerDispatch,
useViewerState,
// componentProps
objectId,
} = props;
return (
<div style={{ padding: "0px 1.618rem 2rem" }}>
<p>Plugin Info Panel</p>
<p>objectId: {objectId}</p>
</div>
);
}
// App.tsx
import PluginButton from "PluginButton";
import PluginInfoPanel from "PluginInfoPanel";
export default function App() {
return (
<Viewer
iiifContent="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
plugins={[
{
id: "Demo",
imageViewer: {
controls: {
component: PluginButton,
},
},
informationPanel: {
component: PluginInfoPanel,
label: { none: ["My Plugin"] },
componentProps: {
objectId: 456,
},
},
},
]}
/>
);
}
If the imageViewer.controls.component
and informationPanel.component
need to share state, wrap the Viewer in a context provider that is provided by the plugin.
import PluginButton from "PluginButton";
import PluginInfoPanel from "PluginInfoPanel";
import { PluginProvider } from "PluginContext";
export default function App() {
return (
<PluginProvider>
<Viewer
iiifContent="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
plugins={[
{
id: "Demo",
imageViewer: {
controls: {
component: PluginButton,
},
},
informationPanel: {
component: PluginInfoPanel,
label: { none: ["My Plugin"] },
componentProps: {
objectId: 456,
},
},
},
]}
/>
</PluginProvider>
);
}