This web component displays IIIF images using the OpenSeadragon library. It is intended to be used in the Edirom Online, but can also be (re-)used in other web applications. No compilation or building is necessary to use the web component.
Note: This repository only contains the bare JavaScript-based component. There is a separate demo suite for web components developed in the Edirom Online Reloaded project, where the component can be seen and tested.
- IIIF Support: Load IIIF manifests or direct tile sources
- Image Navigation: Navigate through multi-page image sequences
- Zoom & Pan: Interactive zoom and pan controls
- Zoom Limits: Enforced
minzoomlevel/maxzoomlevelbounds — programmatic zoom is clamped to the configured range - Rotation: Rotate images to any angle
- Customizable Controls: Show/hide navigator, zoom buttons, home button, fullscreen toggle
- Attribute-Driven: All interactions through standard HTML attributes
- Custom Configuration: Pass advanced OpenSeadragon options via JSON
- Event Communication: Custom events for state changes
- Region Navigation (
zones-data/zone): Define a lookup map of named regions (zones) and jump to any of them with pixel-precise, aspect-preserving viewport fits, including automatic cross-page navigation. All navigable regions — including music measures/bars and MEI<mdiv>movements — are expressed as ordinary zone entries; the host chooses the key convention (e.g. namespacedmeasure:.../mdiv:...keys). - Annotation Overlays (
annotations-data/show-annotations): Render clickable annotation badges on top of the current image, with a persistent show/hide toggle, category/priority filtering (visible-categories/visible-priorities), and component-rendered hover tooltips (host-supplied HTML via thetooltipfield). - Rectangle Fit (
fitrect): Fit the viewport to an arbitrary image-pixel rectangle. - View Mode (
view-mode): Declarative view-mode attribute that is recorded and re-broadcast for host code to react to.
The edirom-image-viewer.js comes with the MIT license.
The imported OpenSeadragon library comes with the BSD-3-Clause license.
Add the web component script to your HTML page's <head>:
<script src="https://cdn.jsdelivr.net/npm/openseadragon@4.1.1/build/openseadragon/openseadragon.min.js"></script>
<script src="path/to/edirom-image-viewer.js"></script>Include the custom element in your HTML <body>:
<edirom-image-viewer
tilesources='["https://example.com/iiif/manifest.json"]'
shownavigator="true"
showzoomcontrol="true"
sequencemode="true">
</edirom-image-viewer>Control the component by setting attributes programmatically:
const viewer = document.querySelector('edirom-image-viewer');
viewer.setAttribute('zoom', '2.5');
viewer.setAttribute('rotation', '90');
viewer.setAttribute('pagenumber', '5');The component fires custom events when state changes:
viewer.addEventListener('communicate-zoom-update', (event) => {
console.log('Zoom changed:', event.detail);
});Note: All attribute values are strings. The data type information indicates the expected format.
Important Note on Page Numbering: Page numbers are 1-based for user-facing interactions. This means:
- Page 1 = First image
- Page 2 = Second image
- And so on...
This applies to pagenumber attribute and all page-related methods. The component automatically converts between 1-based (user) and 0-based (internal) indexing.
| Attribute | Type | Description | Default |
|---|---|---|---|
tilesources |
string | JSON array of IIIF manifest URLs or tile source URLs. Example: '["https://example.com/manifest.json"]' or '["https://example.com/info.json"]' |
"" |
pagenumber |
number | Current page number in a multi-image sequence (1-based, where 1 = first image). | 1 |
zoom |
number | Zoom level of the viewer. Values are clamped to [minzoomlevel, maxzoomlevel]. |
1 |
rotation |
number | Rotation angle in degrees (0-360). | 0 |
preserveviewport |
boolean | Preserve the current viewport (zoom/pan) when changing pages. | false |
clicktozoom |
boolean | Enable click-to-zoom functionality. | true |
minzoomlevel |
number | Minimum allowed zoom level. Programmatic zoom is clamped to this lower bound. |
OSD default |
maxzoomlevel |
number | Maximum allowed zoom level. Programmatic zoom is clamped to this upper bound. |
OSD default |
shownavigationcontrol |
boolean | Show/hide all navigation controls. | true |
sequencemode |
boolean | Enable sequence mode for multi-image navigation. | false |
shownavigator |
boolean | Show/hide the navigator mini-map. | true |
showzoomcontrol |
boolean | Show/hide zoom in/out buttons. | true |
showhomecontrol |
boolean | Show/hide the home/reset view button. | true |
showfullpagecontrol |
boolean | Show/hide the fullscreen toggle button. | true |
showsequencecontrol |
boolean | Show/hide previous/next page buttons (requires sequencemode="true"). |
true |
triggerhome |
boolean | Trigger home position reset (set to "true" to reset view to initial state). |
"false" |
triggerfullscreen |
boolean | Trigger fullscreen mode toggle (set to "true" to toggle fullscreen). |
"false" |
openseadragon-options |
string | JSON object with additional OpenSeadragon configuration options. Example: '{"showNavigator": true}' |
"" |
zones-data |
string | JSON object mapping zone keys to zone objects. Each zone: { page: number, ulx: number, uly: number, lrx: number, lry: number } (coordinates optional). Also used for measures and movements via host-chosen namespaced keys. See Region Navigation for details. |
"{}" |
zone |
string | Key of the zone to navigate to. Must exist in zones-data. Setting this attribute triggers navigation to the zone. Append |<nonce> to re-fire navigation to the same zone. |
"" |
annotations-data |
string | JSON array of annotation overlays. Each entry: { idPrefix, id, title, uri, categories, priority, fn, tooltip, plist }, where tooltip is optional host-supplied HTML rendered by the component on hover and plist is an array of image-pixel regions { id, ulx, uly, lrx, lry, type }. Rendered as clickable badges. See Annotation Overlays. |
"[]" |
show-annotations |
boolean | Show/hide the rendered annotation overlays. Toggles visibility without discarding annotations-data; the last state persists across page changes. |
false |
visible-categories |
string | JSON array of category ids that should remain visible. ["undefined"] (no category taxonomy) or absent shows all; [] hides all; otherwise a badge is shown only if one of its categories is listed. See Annotation Overlays. |
null |
visible-priorities |
string | JSON array of priority ids that should remain visible. Same ["undefined"] / [] / list semantics as visible-categories. A badge is shown only when it passes both filters. |
null |
fitrect |
string | Fit the viewport to an image-pixel rectangle "x,y,width,height". An optional trailing ,<nonce> token re-fires the same fit. |
"" |
view-mode |
string | Declarative view mode (e.g. pageBasedView / measureBasedView). Recorded and re-broadcast via the view-mode-changed event for host code to react to. |
"" |
The component provides the following public methods:
nextPage()- Navigate to the next pagepreviousPage()- Navigate to the previous pagegoToPage(pageNumber)- Navigate to a specific pagegetCurrentPage()- Get the current page numbergetTotalPages()- Get the total number of pages
zoomIn()- Zoom in by 20%zoomOut()- Zoom out by 20%setZoom(level)- Set zoom to a specific level (clamped to[minzoomlevel, maxzoomlevel])getZoom()- Get the current zoom level
home()- Reset view to initial statesetFullScreen(fullScreen)- Set fullscreen mode (true/false)toggleFullScreen()- Toggle fullscreen modeisFullScreen()- Check if in fullscreen mode
rotate(degrees)- Rotate by specified degrees (relative)setRotation(degrees)- Set rotation to specific angle (absolute)getRotation()- Get current rotation angle
<edirom-image-viewer
tilesources='["https://example.com/iiif/manifest.json"]'>
</edirom-image-viewer><edirom-image-viewer
id="viewer"
tilesources='["https://content.staatsbibliothek-berlin.de/dc/69007087X-0001/info.json", "https://content.staatsbibliothek-berlin.de/dc/69007087X-0002/info.json"]'
pagenumber="1"
zoom="1"
rotation="0"
triggerhome="false"
triggerfullscreen="false"
sequencemode="true"
showsequencecontrol="true"
shownavigator="true"
showzoomcontrol="true"
showhomecontrol="true"
showfullpagecontrol="true">
</edirom-image-viewer>const viewer = document.querySelector('edirom-image-viewer');
// Navigate to page 3
viewer.setAttribute('pagenumber', '3');
// Zoom to level 2
viewer.setAttribute('zoom', '2');
// Reset to home position
viewer.setAttribute('triggerhome', 'true');
// Toggle fullscreen
viewer.setAttribute('triggerfullscreen', 'true');
The component supports both IIIF manifests and direct image tile sources:
- IIIF Manifests: Automatically fetches and parses IIIF Presentation API manifests to extract image URLs
- Direct Tile Sources: Use IIIF Image API info.json URLs directly
Trigger attributes are used to invoke actions on the viewer. Set these attributes to "true" to trigger the corresponding action:
triggerhome: Reset view to initial/home positiontriggerfullscreen: Toggle fullscreen mode on/off
Example:
const viewer = document.querySelector('edirom-image-viewer');
viewer.setAttribute('triggerhome', 'true'); // Reset to home position
viewer.setAttribute('triggerfullscreen', 'true'); // Toggle fullscreenThe component fires a generic communicate-[property]-update event whenever any observed attribute changes:
communicate-zoom-update- Fired when zoom level changescommunicate-rotation-update- Fired when rotation changescommunicate-pagenumber-update- Fired when page changescommunicate-triggerhome-update- Fired when home is triggeredcommunicate-triggerfullscreen-update- Fired when fullscreen is triggered- And one for every other observable attribute
The component also fires dedicated semantic events:
| Event | Detail | Fired when |
|---|---|---|
page-changed |
{ pageNumber } (1-based) |
The viewer navigates to a new page. |
zone-changed |
{ zoneKey, zone } |
Navigation to a zone completes. |
view-mode-changed |
{ viewMode } |
The view-mode attribute changes. |
zoom |
{ zoom } |
The OpenSeadragon viewport zoom changes. |
image-ready |
— | The image/tiles have finished loading. |
annotation-click |
{ id, uri, fn, title, element } |
An annotation badge is clicked. |
annotation-mouseenter / annotation-mouseleave |
{ id, uri, fn, title, element } |
The pointer enters/leaves an annotation badge. |
annotation-filter-changed |
{ visibleCategories, visiblePriorities } |
visible-categories or visible-priorities changes (incl. externally). |
viewer.addEventListener('page-changed', (event) => {
console.log('Navigated to page:', event.detail.pageNumber);
});
viewer.addEventListener('zone-changed', (event) => {
console.log('Navigated to zone:', event.detail.zoneKey);
});The component supports pixel-precise navigation to named rectangular regions (zones) on any page, independent of OSD's own sequence controls. A single lookup map drives all region navigation. Measures, movements and any other navigable region are expressed as ordinary zone entries — the host picks the key convention (e.g. namespaced measure:... / mdiv:... keys).
| Lookup map | Trigger attribute | Completion event | Typical use |
|---|---|---|---|
zones-data |
zone |
zone-changed |
Any named region: generic zones, measures/bars, MEI <mdiv> movements |
Each entry in the zones-data map must have a 1-based page number and (for precise fits) pixel coordinates (ulx, uly, lrx, lry) defining the upper-left and lower-right corners of the region. An entry may carry only a page (no coordinates) to navigate to that page and show it whole — useful for movement first pages.
{
"measure:1": { "page": 1, "ulx": 100, "uly": 200, "lrx": 800, "lry": 600 },
"measure:2": { "page": 1, "ulx": 900, "uly": 200, "lrx": 1600, "lry": 600 },
"mdiv:mov2": { "page": 2 }
}- Same page: the viewer fits the region directly (aspect-preserving).
- Cross-page: the component navigates to the target page first, waits until its tiles are loaded, then applies the region — ensuring coordinate conversion is always accurate.
- Updating
zones-data: setting a newzones-datavalue while a zone is active re-applies the current zone against the updated data.
The zones-data attribute is a lookup map (set once, performs no navigation on its own). The zone attribute is the navigation trigger and must hold a key that exists in the map. To re-fire navigation to the same key, append a |<nonce> token to the trigger value — it is stripped before lookup:
let nonce = 0;
viewer.setAttribute('zone', 'measure:1|' + (++nonce)); // jump
viewer.setAttribute('zone', 'measure:1|' + (++nonce)); // jump again to the same zoneAn empty trigger value (zone="") is ignored, so it is safe as a default in markup.
<edirom-image-viewer
id="viewer"
sequencemode="true"
showsequencecontrol="false"
tilesources='[...]'
zones-data='{}'
zone="">
</edirom-image-viewer>const viewer = document.querySelector('#viewer');
// Populate the lookup map
viewer.setAttribute('zones-data', JSON.stringify({
'measure:1': { page: 1, ulx: 100, uly: 200, lrx: 800, lry: 600 },
'mdiv:mov2': { page: 2 }
}));
// Trigger navigation
viewer.setAttribute('zone', 'measure:1');
viewer.addEventListener('zone-changed', (event) => {
console.log('Navigated to zone:', event.detail.zoneKey);
});
viewer.addEventListener('page-changed', (event) => {
console.log('Page is now:', event.detail.pageNumber);
});Fit the viewport to an arbitrary image-pixel rectangle, independent of any lookup map. The value is "x,y,width,height" in image-pixel coordinates, with an optional trailing ,<nonce> token to re-fire the same fit:
viewer.setAttribute('fitrect', '500,300,1200,800');A declarative attribute the host can set to record the active view mode (e.g. pageBasedView / measureBasedView). The component stores it and re-broadcasts it via the view-mode-changed event; the actual layout swap is owned by the surrounding host application:
viewer.setAttribute('view-mode', 'measureBasedView');
viewer.addEventListener('view-mode-changed', (event) => {
console.log('View mode:', event.detail.viewMode);
});The component renders clickable annotation badges on top of the current image using a push/persist model: the host pushes the full set of annotations via annotations-data, toggles their visibility via show-annotations, and narrows them down by category/priority via visible-categories / visible-priorities. The component owns all rendering, showing, hiding and filtering — the host never touches the DOM.
annotations-data is a JSON array of annotation descriptors. Annotations pointing at the same region share one stacked container, and each badge carries the CSS classes annotIcon {categories} {priority} {type} so edition stylesheets can target them:
viewer.setAttribute('annotations-data', JSON.stringify([
{
idPrefix: 'viewer1',
id: 'annot1',
title: 'Slur added',
uri: 'xmldb:exist:///db/.../annot1.xml',
categories: 'wega.annotation.category.bogensetzung',
priority: 'ediromAnnotPrio1',
fn: '', // host click action (opaque to the component)
tooltip: '<div class="annotTip">…host-supplied HTML…</div>', // rendered by the component on hover
plist: [ // one or more image-pixel regions
{ id: 'm1', ulx: 100, uly: 100, lrx: 160, lry: 160, type: 'measure' }
]
}
]));
viewer.setAttribute('show-annotations', 'true');Each badge dispatches annotation-click, annotation-mouseenter and annotation-mouseleave CustomEvents (with detail = { id, uri, fn, title, element }). The host uses annotation-click (via the opaque fn) for its click behaviour. The tooltip is rendered by the component itself: if an annotation carries a tooltip HTML string, the component shows it in a positioned, reusable tooltip element on hover and hides it on leave — the host only supplies the HTML (and may still listen to the mouse events for its own highlighting).
visible-categories and visible-priorities are JSON arrays of the category / priority ids that should remain visible. A badge is shown only when it passes both filters (one of its categories is listed and its priority is listed). The sentinel values mirror the host's filter menus:
| Value | Meaning |
|---|---|
absent / null |
no filter pushed yet — show all |
["undefined"] |
the edition has no such taxonomy — show all |
[] |
every item unchecked — hide all |
["catA", "catB"] |
show only badges whose category/priority is listed |
// show only the "bogensetzung" category, any priority
viewer.setAttribute('visible-categories', JSON.stringify(['wega.annotation.category.bogensetzung']));
viewer.setAttribute('visible-priorities', JSON.stringify(['undefined']));Filtering hides individual badges via display, and a stacked container is hidden once none of its badges pass the filter. Both show-annotations and the filters toggle visibility (not display) at the container level so OpenSeadragon redraws don't override the hide. The chosen show/hide state and filter are remembered and re-applied to every freshly rendered page, so they persist across page navigation until changed.
Whenever visible-categories or visible-priorities changes (including when set externally, e.g. via DevTools), the component dispatches an annotation-filter-changed CustomEvent with detail = { visibleCategories, visiblePriorities } (the current filter arrays, or null for "no filter"). The host can listen for it to keep its own filter UI (e.g. menu checkboxes) in sync with the component's state.
The component uses modern web standards (Custom Elements, Shadow DOM) and requires a modern browser with ES6+ support