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.
minzoomlevel / maxzoomlevel bounds — programmatic zoom is clamped to the configured rangezones-data / zone) — generic named regions.measures-data / measure) — music measures/bars.mdivs-data / mdiv) — MEI <mdiv> movements (page-level, optional region).measure-numbers-data / show-measure-numbers): Render labelled measure-number boxes on top of the current image, with a persistent show/hide toggle and hover highlighting.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 the tooltip field).fitrect): Fit the viewport to an arbitrary image-pixel rectangle.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:
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 }. 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. |
"" |
measures-data |
string | JSON object mapping measure keys to region objects { page, ulx, uly, lrx, lry }. Lookup map for measure. See Region Navigation. |
"{}" |
measure |
string | Key of the measure to navigate to (must exist in measures-data). Append \|<nonce> to re-fire navigation to the same measure. |
"" |
mdivs-data |
string | JSON object mapping movement (mdiv) keys to objects { page } (with optional region). Lookup map for mdiv. See Region Navigation. |
"{}" |
mdiv |
string | Key of the movement to navigate to (must exist in mdivs-data). Append \|<nonce> to re-fire navigation to the same movement. |
"" |
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 |
measure-numbers-data |
string | JSON array of measure-number overlay boxes. Each entry: { idPrefix, id, name, ulx, uly, lrx, lry }. Rendered as labelled boxes on the current image. See Measure Number Overlays. |
"[]" |
show-measure-numbers |
boolean | Show/hide the rendered measure-number overlays. Toggles visibility without discarding measure-numbers-data; the last state persists across page changes. |
false |
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 pageszoomIn() - 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 levelhome() - Reset view to initial statesetFullScreen(fullScreen) - Set fullscreen mode (true/false)toggleFullScreen() - Toggle fullscreen modeisFullScreen() - Check if in fullscreen moderotate(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:
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/offExample:
const viewer = document.querySelector('edirom-image-viewer');
viewer.setAttribute('triggerhome', 'true'); // Reset to home position
viewer.setAttribute('triggerfullscreen', 'true'); // Toggle fullscreen
The 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 triggeredThe 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. |
measure-changed |
{ key, region } |
Navigation to a measure completes. |
mdiv-changed |
{ key, region } |
Navigation to an mdiv 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('measure-changed', (event) => {
console.log('Navigated to measure:', event.detail.key);
});
The component supports pixel-precise navigation to named rectangular regions on any page, independent of OSD’s own sequence controls. Three parallel lookup maps share the same mechanism and the same region object shape:
| Lookup map | Trigger attribute | Completion event | Typical use |
|---|---|---|---|
zones-data |
zone |
zone-changed |
Generic named regions |
measures-data |
measure |
measure-changed |
Music measures / bars |
mdivs-data |
mdiv |
mdiv-changed |
MEI <mdiv> movements (page-level, optional region) |
Each entry in a *-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. Movements (mdivs-data) may carry only a page to navigate to the movement’s first page without a region fit.
{
"measure_1": { "page": 1, "ulx": 100, "uly": 200, "lrx": 800, "lry": 600 },
"measure_2": { "page": 1, "ulx": 900, "uly": 200, "lrx": 1600, "lry": 600 },
"measure_3": { "page": 2, "ulx": 150, "uly": 300, "lrx": 950, "lry": 700 }
}
zones-data: setting a new zones-data value while a zone is active re-applies the current zone against the updated data.The *-data attribute is a lookup map (set once, performs no navigation on its own). The matching trigger attribute (zone / measure / mdiv) 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('measure', 'measure_1|' + (++nonce)); // jump
viewer.setAttribute('measure', 'measure_1|' + (++nonce)); // jump again to the same measure
Empty trigger values (measure="", mdiv="") are ignored, so they are safe as defaults in markup.
<edirom-image-viewer
id="viewer"
sequencemode="true"
showsequencecontrol="false"
tilesources='[...]'
measures-data='{}'
measure="">
</edirom-image-viewer>
const viewer = document.querySelector('#viewer');
// Populate the lookup map
viewer.setAttribute('measures-data', JSON.stringify({
measure_1: { page: 1, ulx: 100, uly: 200, lrx: 800, lry: 600 },
measure_2: { page: 2, ulx: 150, uly: 300, lrx: 950, lry: 700 }
}));
// Trigger navigation
viewer.setAttribute('measure', 'measure_2');
viewer.addEventListener('measure-changed', (event) => {
console.log('Navigated to measure:', event.detail.key);
});
viewer.addEventListener('page-changed', (event) => {
console.log('Page is now:', event.detail.pageNumber);
});
fitrect)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');
view-mode)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);
});
In addition to navigating to measures (see Region Navigation), the component can render labelled measure-number boxes directly on top of the current image. This uses a push/persist model: the host pushes the full set of boxes via measure-numbers-data, and toggles their visibility via show-measure-numbers. The component owns all rendering, showing, hiding and hover highlighting — the host never touches the DOM.
measure-numbers-data is a JSON array of overlay descriptors. Each entry defines one box in image-pixel coordinates:
[
{ "idPrefix": "viewer1", "id": "m1", "name": "1", "ulx": 100, "uly": 100, "lrx": 200, "lry": 300 },
{ "idPrefix": "viewer1", "id": "m2", "name": "2", "ulx": 220, "uly": 100, "lrx": 320, "lry": 300 }
]
| Field | Description |
|---|---|
idPrefix |
Prefix used (with id) to build the overlay’s unique DOM id. |
id |
Measure id, combined with idPrefix into idPrefix_id. |
name |
Label shown inside the box (the measure number). An empty name is rendered with an “empty” style. |
ulx,uly |
Upper-left corner of the box, in image pixels. |
lrx,lry |
Lower-right corner of the box (box size is lrx-ulx × lry-uly). |
show-measure-numbers toggles the overlays’ visibility (not display), so the pushed data is never discarded. The component remembers the last state and re-applies it to every freshly rendered page, so toggling once persists across page navigation until toggled again. Pushing a new measure-numbers-data re-renders the boxes and re-applies the current visibility.
const viewer = document.querySelector('edirom-image-viewer');
// Push the measure boxes for the current page
viewer.setAttribute('measure-numbers-data', JSON.stringify([
{ idPrefix: 'viewer1', id: 'm1', name: '1', ulx: 100, uly: 100, lrx: 200, lry: 300 },
{ idPrefix: 'viewer1', id: 'm2', name: '2', ulx: 220, uly: 100, lrx: 320, lry: 300 }
]));
// Show them
viewer.setAttribute('show-measure-numbers', 'true');
// Hide them (data is kept, just hidden)
viewer.setAttribute('show-measure-numbers', 'false');
Note:
measure-numbers-data/show-measure-numbers(overlay rendering) are independent ofmeasures-data/measure(region navigation). They can be used together or separately.
The component renders clickable annotation badges on top of the current image using the same push/persist model as the measure-number overlays: 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 formatannotations-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