@tindtechnologies/universalviewer
Version:
The Universal Viewer is a community-developed open source project on a mission to help you share your 📚📜📰📽️📻🗿 with the 🌎
1,346 lines (1,161 loc) • 46.8 kB
HTML
<html>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Universal Viewer Examples</title>
<script type="text/javascript" src="umd/UV.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/modern-normalize@3.0.1/modern-normalize.css"
/>
<style>
body {
color-scheme: light dark;
background-color: Canvas;
}
/* @apply uv w-full */
#uv {
width: 100%;
height: 100svh;
}
/* @apply hidden mx-auto my-8 */
#uv-controls {
display: none;
margin-inline: auto;
margin-block: 2rem;
}
@media (min-width: 768px) {
/* @apply md:w-[90vw] md:mx-auto md:h-[80vh] md:mt-4 */
#uv {
width: 90vw;
height: 80vh;
margin-inline: auto;
margin-block: 0;
}
/* @apply md:!block md:w-[90vw] */
#uv-controls {
display: block;
width: 90vw;
margin-block: 1rem;
}
}
@media (min-width: 1024px) {
/* @apply lg:w-[65vw] */
#uv {
width: 65vw;
}
/* @apply lg:w-[65vw] */
#uv-controls {
width: 65vw;
}
}
#uv-tabs {
width: 100%;
}
@media (max-width: 767px) {
#uv-tabs {
display: none;
}
}
@media (min-width: 768px) {
#uv-tabs {
width: 90vw;
margin-inline: auto;
margin-block: 0.25rem;
}
}
@media (min-width: 1024px) {
#uv-tabs {
width: 65vw;
}
}
#uv-controls {
button,
input[type="text"],
select,
textarea {
appearance: none;
padding-inline: 0.75rem; /* px-3 */
padding-block: 0.5rem; /* py-2 */
font: inherit;
line-height: 24px;
border: 1px solid;
background-color: Canvas;
}
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
}
textarea {
width: 100%;
}
/* Controls */
.controls-button {
appearance: button;
min-width: 10rem;
padding-inline: 1rem;
padding-block: 0.5rem;
line-height: 26px;
white-space: nowrap;
color: white;
border: none;
background-color: black;
}
.controls-group {
padding-block: 0.5rem;
}
.controls-title {
margin-bottom: 0;
padding-bottom: 0.75rem;
font-size: 1.125rem;
font-weight: 600;
}
.controls-tab-content {
padding: 1.5rem;
color: ButtonText;
background-color: ButtonFace;
}
.tab {
display: flex;
gap: 0.1rem;
}
.flex-spacer {
flex-grow: 1;
background-color: white;
min-width: 0;
position: relative;
top: -1.5rem;
left: -1.5rem;
min-width: 300px;
margin-left: -1.5px;
}
.tab button {
padding-inline: 1.5rem;
padding-block: 0.75rem;
font-weight: 500;
font-size: 1rem;
border: none;
background: none;
cursor: pointer;
background-color: white;
color: #000;
position: relative;
top: -1.5rem;
left: -1.5rem;
}
.tab button.active {
background-color: ButtonFace;
color: ButtonText;
}
.tabcontent {
display: none;
}
#general {
display: block;
}
}
.controls-tab {
padding-inline: 1.5rem;
padding-block: 0.75rem;
font-weight: 400;
font-size: 1.25rem;
color: ButtonText;
border: none;
background: ButtonFace;
cursor: pointer;
}
.controls-tab.active {
background-color: white;
border-left: ButtonFace 3px solid;
border-top: ButtonFace 3px solid;
border-right: ButtonFace 3px solid;
font-weight: 500;
color: black;
}
/* Utilities */
.flex {
display: flex;
align-items: center;
gap: 0.5rem;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-initial {
flex: 0 1 auto;
}
.mt-2 {
margin-top: 0.5rem;
}
/* Config builder */
.toggleButton {
display: block;
margin-bottom: 0.5rem;
cursor: pointer;
background-color: Canvas;
border: 1px solid;
padding-inline: 0.75rem;
padding-block: 0.5rem;
font: inherit;
line-height: 24px;
text-align: left;
width: 100%;
}
.toggleButtonBackground {
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
}
.toggleButton.caret-up {
background-image: url('data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 fill=%27none%27 viewBox=%270 0 20 20%27%3e%3cpath stroke=%27%236b7280%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%271.5%27 d=%27M6 12l4-4 4 4%27/%3e%3c/svg%3e');
}
.toggleButton.caret-down {
background-image: url('data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 fill=%27none%27 viewBox=%270 0 20 20%27%3e%3cpath stroke=%27%236b7280%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%271.5%27 d=%27M6 8l4 4 4-4%27/%3e%3c/svg%3e');
}
/* Explanatory text */
details > summary {
display: inline-flex;
align-items: center;
cursor: pointer;
}
details > summary::after {
content: url("data:image/svg+xml,%3Csvg fill='black' xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 400 400'%3E%3Cpath d='M199.996,0C89.719,0,0,89.72,0,200c0,110.279,89.719,200,199.996,200C310.281,400,400,310.279,400,200C400,89.72,310.281,0,199.996,0z M199.996,373.77C104.187,373.77,26.23,295.816,26.23,200c0-95.817,77.957-173.769,173.766-173.769c95.816,0,173.772,77.953,173.772,173.769C373.769,295.816,295.812,373.77,199.996,373.77z'/%3E%3Cpath d='M199.996,91.382c-35.176,0-63.789,28.616-63.789,63.793c0,7.243,5.871,13.115,13.113,13.115c7.246,0,13.117-5.873,13.117-13.115c0-20.71,16.848-37.562,37.559-37.562c20.719,0,37.566,16.852,37.566,37.562c0,20.714-16.849,37.566-37.566,37.566c-7.242,0-13.113,5.873-13.113,13.114v45.684c0,7.243,5.871,13.115,13.113,13.115s13.117-5.872,13.117-13.115v-33.938c28.905-6.064,50.68-31.746,50.68-62.427C263.793,119.998,235.176,91.382,199.996,91.382z'/%3E%3Cpath d='M200.004,273.738c-9.086,0-16.465,7.371-16.465,16.462s7.379,16.465,16.465,16.465c9.094,0,16.457-7.374,16.457-16.465S209.098,273.738,200.004,273.738z'/%3E%3C/svg%3E");
margin-left: 0.5em;
vertical-align: middle;
position: relative;
top: 2px;
}
</style>
<!-- https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ -->
<script>
// First we get the viewport height and we multiple it by 1% to get a value for a vh unit
let vh = window.innerHeight * 0.01;
// Then we set the value in the --vh custom property to the root of the document
document.documentElement.style.setProperty("--vh", `${vh}px`);
window.addEventListener("resize", () => {
// We execute the same script as before
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${vh}px`);
});
</script>
</head>
<body>
<div id="uv-tabs">
<button id="iiifTabButton" class="controls-tab active">
IIIF
</button>
<button id="youTubeTabButton" class="controls-tab">YouTube</button>
</div>
<div
id="uv"
class="uv w-full md:w-[90vw] lg:w-[65vw] md:mx-auto md:h-[80vh] md:mt-4"
></div>
<div
id="uv-controls"
class="hidden md:!block md:w-[90vw] lg:w-[65vw] mx-auto my-8"
>
<div>
<ul id="annotation-list"></ul>
</div>
<div id="iiifTab" class="controls-tab-content">
<div class="tab">
<button class="tablinks active" aria-label="General settings tab" onclick="openTab(event, 'general')">General</button>
<button class="tablinks" aria-label="Open Seadragon settings tab" onclick="openTab(event, 'osd')">OpenSeadragon</button>
<button class="tablinks" aria-label="AV settings tab" onclick="openTab(event, 'av')">AV</button>
<button class="tablinks" aria-label="Annotation settings tab" onclick="openTab(event, 'annotations')">Annotations</button>
<button class="tablinks" aria-label="Configuration settings tab" onclick="openTab(event, 'config')">Configuration</button>
<div class="flex-spacer"></div>
</div>
<!-- General -->
<div id="general" class="tabcontent">
<h2 class="controls-title">Manifest ID</h2>
<div class="flex">
<select id="iiifManifestIdSelect" class="py-2 px-3 mr-2 flex-1"></select>
<input id="iiifManifestId" type="text" value="" class="flex-1" />
</div>
<div class="mt-2">
<button id="setIIIFManifestIdButton" class="controls-button">Set IIIF Manifest ID</button>
</div>
<div class="controls-group">
<h2 class="controls-title"></h2>
<div class="mt-2">
<details><summary aria-label="More information about using target"><b>Set Target</b></summary><i>For OpenSeadragon manifests, edit the 'xywh' values at the end of the URL to modify the view of the image.
For AV manifests, modify the number after 't' to set the timestamp.</i></details>
</div>
<br>
<div class="flex mb-2">
<input id="iiif_target" type="text" value="" class="flex-1" />
<button id="iiif_setTargetButton" class="controls-button flex-initial ml-2">
Set Target
</button>
</div>
</div>
</div>
<!-- OSD -->
<div id="osd" class="tabcontent">
<div class="controls-group">
<h2 class="controls-title">Set degrees of rotation</h2>
<div class="flex">
<input id="iiif_rotation" type="range" value="0" min="0" max="360" class="mr-2" />
<span id="iiif_rotationValue" class="mr-2">0</span>
<button id="iiif_setRotationButton" class="controls-button flex-initial ml-2">
Set Rotation
</button>
<button id="iiif_resetRotationButton" class="controls-button flex-initial ml-2" style="padding: 0.5rem 0.5rem; font-size: 1rem; min-width: 0;">
Reset
</button>
</div>
</div>
</div>
<script>
const slider = document.getElementById('iiif_rotation');
const valueDisplay = document.getElementById('iiif_rotationValue');
const setRotationButton = document.getElementById('iiif_setRotationButton');
const resetButton = document.getElementById('iiif_resetRotationButton');
slider.addEventListener('input', () => {
valueDisplay.textContent = slider.value;
});
resetButton.addEventListener('click', () => {
slider.value = 360;
valueDisplay.textContent = '360';
setRotationButton.click();
});
</script>
<!-- AV -->
<div id="av" class="tabcontent" style="display:none">
<div class="controls-group">
<h2 class="controls-title">Mute sound</h2>
<input type="checkbox" id="iiif_mutedCheckbox" />
</div>
</div>
<!-- Annotations -->
<div id="annotations" class="tabcontent" style="display:none">
<h2 class="controls-title">Annotations</h2>
<textarea id="iiif_annotations" rows="5" class="w-full"></textarea>
<div class="mt-2">
<button id="iiif_setAnnotationsButton" class="controls-button">Set Annotations</button>
<button id="iiif_clearAnnotationsButton" class="controls-button">Clear Annotations</button>
</div>
</div>
<!-- Configuration -->
<div id="config" class="tabcontent" style="display:none">
<p><i>The Universal Viewer can be configured using a JSON file. The controls below allow you to easily test changes to your configuration, or to generate a new configuration file by browsing the available settings.</i></p>
<h2 class="controls-title"></h2><details>
<summary aria-label="More information about custom configuration" style="cursor: pointer;"><b>Custom configuration</b></summary>
<i>Enter your configuration here (in JSON format) to see a preview of how the Universal Viewer will look and behave. Since UV uses browser storage to remember some settings, you may need to check the 'Clear saved state' checkbox to ensure that your changes are immediately visible.</i>
</details>
<br>
<textarea id="customConfig" rows="5" class="w-full"></textarea>
<div class="mt-2">
<input type="checkbox" id="clearStorageCheckbox" />
<label for="clearStorageCheckbox">Clear saved viewer state before applying</label>
</div>
<div class="mt-2">
<button id="setConfigButton" class="controls-button">Apply Configurations</button>
<button id="resetConfigButton" class="controls-button">Reset to Default Configuration</button>
</div>
<div class="controls-group">
<h2 class="controls-title"></h2><details>
<summary aria-label="More information about using the configuration builder" style="cursor: pointer;"><b>Configuration builder</b></summary>
<i>Use the dropdown menus to customise the Universal Viewer. Your selections will automatically populate the custom configuration box above. You will have to click the Apply Configurations button when you are ready to put your changes into effect.</i></details>
<br>
<div id="config-builder"></div>
</div>
</div>
</div>
<script>
function openTab(evt, tabName) {
document.addEventListener("DOMContentLoaded", function () {
document.querySelector(".tablinks").click();
});
const tabcontent = document.getElementsByClassName("tabcontent");
const tablinks = document.getElementsByClassName("tablinks");
for (let i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.classList.add("active");
document.addEventListener("DOMContentLoaded", function () {
document.querySelector(".tablinks").click();
});
}
</script>
<div id="youTubeTab" class="hidden controls-tab-content">
<div class="controls-group">
<h2 class="controls-title">Video Id</h2>
<div class="flex">
<select id="youTubeVideoIdSelect" class="py-2 px-3 flex-1"></select>
<input
id="youTubeVideoId"
type="text"
value=""
class="flex-1 ml-2"
/>
</div>
<div class="mt-2">
<button id="setYouTubeVideoIdButton" class="controls-button">
Set YouTube Video Id
</button>
</div>
</div>
<div class="controls-group">
<h2 class="controls-title">Muted</h2>
<input type="checkbox" id="youtube_mutedCheckbox" />
</div>
<div class="controls-group">
<h2 class="controls-title">Current Time</h2>
<div class="flex">
<input
id="youtube_currentTime"
type="text"
value=""
class="flex-1"
/>
<button
id="youtube_setCurrentTimeButton"
class="controls-button flex-initial ml-2"
>
Set Current Time
</button>
</div>
</div>
<div class="controls-group">
<h2 class="controls-title">Duration</h2>
<div class="flex">
<input
id="youtube_durationStart"
type="number"
value="0"
class="flex-1"
/>
<input
id="youtube_durationEnd"
type="number"
value="0"
class="flex-1 ml-2"
/>
<button
id="youtube_setDurationButton"
class="controls-button flex-initial ml-2"
>
Set Duration
</button>
</div>
</div>
</div>
</div>
<!-- UV Controls -->
<script type="text/javascript">
function createOptGroup(label) {
var optGroup = document.createElement("optgroup");
optGroup.label = label;
return optGroup;
}
function createOption(text, value) {
var option = document.createElement("option");
option.text = text;
option.value = value;
return option;
}
document.addEventListener("DOMContentLoaded", function() {
var iiifTabButton = document.getElementById("iiifTabButton");
var iiifTab = document.getElementById("iiifTab");
var youTubeTabButton = document.getElementById("youTubeTabButton");
var youTubeTab = document.getElementById("youTubeTab");
iiifTabButton.addEventListener("click", function(e) {
e.preventDefault();
activateIIIFTab();
});
youTubeTabButton.addEventListener("click", function(e) {
e.preventDefault();
activateYouTubeTab();
});
var uv, iiifManifestId, youTubeVideoId;
// iiif tab
var $iiifManifestId = iiifTab.querySelector("#iiifManifestId");
var $iiifManifestIdSelect = iiifTab.querySelector(
"#iiifManifestIdSelect"
);
var $iiif_target = iiifTab.querySelector("#iiif_target");
var $iiif_rotation = iiifTab.querySelector("#iiif_rotation");
var $setIIIFManifestIdButton = iiifTab.querySelector(
"#setIIIFManifestIdButton"
);
var $iiif_setTargetButton = iiifTab.querySelector(
"#iiif_setTargetButton"
);
var $iiif_mutedCheckbox = iiifTab.querySelector("#iiif_mutedCheckbox");
var $iiif_setRotationButton = iiifTab.querySelector(
"#iiif_setRotationButton"
);
var $iiif_annotations = iiifTab.querySelector("#iiif_annotations");
var $iiif_setAnnotationsButton = iiifTab.querySelector(
"#iiif_setAnnotationsButton"
);
var $iiif_clearAnnotationsButton = iiifTab.querySelector(
"#iiif_clearAnnotationsButton"
);
// youtube tab
var $setYouTubeVideoIdButton = youTubeTab.querySelector(
"#setYouTubeVideoIdButton"
);
var $youTubeVideoId = youTubeTab.querySelector("#youTubeVideoId");
var $youTubeVideoIdSelect = youTubeTab.querySelector(
"#youTubeVideoIdSelect"
);
var $youtube_currentTime = youTubeTab.querySelector(
"#youtube_currentTime"
);
var $youtube_setCurrentTimeButton = youTubeTab.querySelector(
"#youtube_setCurrentTimeButton"
);
var $youtube_mutedCheckbox = youTubeTab.querySelector(
"#youtube_mutedCheckbox"
);
var $youtube_durationStart = youTubeTab.querySelector(
"#youtube_durationStart"
);
var $youtube_durationEnd = youTubeTab.querySelector(
"#youtube_durationEnd"
);
var $youtube_setDurationButton = youTubeTab.querySelector(
"#youtube_setDurationButton"
);
// config controls
var $customConfig = document.getElementById("customConfig");
var $setConfigButton = document.getElementById("setConfigButton");
var annotations = [];
var urlAdapter;
function parseIIIFCollection(manifests) {
for (var i = 0; i < manifests.collections.length; i++) {
var collection = manifests.collections[i];
if (collection.visible === false) {
continue;
}
var optGroup = createOptGroup(collection.label);
$iiifManifestIdSelect.appendChild(optGroup);
for (var j = 0; j < collection.manifests.length; j++) {
var manifest = collection.manifests[j];
if (manifest.visible !== false) {
var option = createOption(manifest.label, manifest["@id"]);
optGroup.appendChild(option);
}
}
}
}
function parseYouTubeCollection(collection) {
for (var i = 0; i < collection.length; i++) {
var item = collection[i];
if (item.visible === false) {
continue;
}
var option = createOption(item.label, item.id);
$youTubeVideoIdSelect.appendChild(option);
}
}
function loadCollections(cb) {
Promise.all([
fetch("iiif-collection.json"),
fetch("youtube-collection.json"),
])
.then(function(responses) {
// Get a JSON object from each of the responses
return Promise.all(
responses.map(function(response) {
return response.json();
})
);
})
.then(function(data) {
parseIIIFCollection(data[0]);
parseYouTubeCollection(data[1]);
cb();
})
.catch(function(error) {
// if there's an error, log it
console.log(error);
});
}
function setSelectedIIIFManifest() {
var iiifContent = urlAdapter.get("iiif-content");
var legacyIIIFManifestParam =
urlAdapter.get("iiifManifestId") || urlAdapter.get("manifest");
if (iiifContent && iiifContent.indexOf("http") !== -1) {
// if it's a url, not an encoded annotation
iiifManifestId = iiifContent;
} else if (legacyIIIFManifestParam) {
iiifManifestId = legacyIIIFManifestParam;
} else {
// use the first one in the drop down box
var options = document.querySelectorAll(
"#iiifManifestIdSelect optgroup option"
);
if (options.length) {
iiifManifestId = options[0].value;
}
}
$iiifManifestIdSelect.value = iiifManifestId;
$iiifManifestId.value = iiifManifestId;
}
function setSelectedYouTubeVideoId() {
youTubeVideoId = urlAdapter.get("youTubeVideoId");
if (youTubeVideoId) {
$youTubeVideoIdSelect.value = youTubeVideoId;
} else {
var options = document.querySelectorAll(
"#youTubeVideoIdSelect option"
);
if (options.length) {
youTubeVideoId = options[0].value;
}
}
$youTubeVideoId.value = youTubeVideoId;
}
function setAnnotations() {
annotations = JSON.parse($iiif_annotations.value);
uv.set({
annotations: annotations,
});
}
function setConfig() {
// Test if the "Clear saved viewer state" checkbox is checked
const clearStorage = document.getElementById("clearStorageCheckbox").checked;
if (clearStorage) {
localStorage.clear();
sessionStorage.clear();
}
activateIIIFTab();
}
$iiifManifestIdSelect.onchange = function() {
var $selectedOption = document.querySelector(
"#iiifManifestIdSelect option:checked"
);
iiifManifestId = $selectedOption.value;
$iiifManifestId.value = iiifManifestId;
urlAdapter.set("iiifManifestId", iiifManifestId);
};
$setIIIFManifestIdButton.onclick = function() {
iiifManifestId = $iiifManifestId.value;
urlAdapter.set("iiifManifestId", iiifManifestId);
clearAnnotations();
uv.set({
iiifManifestId: iiifManifestId,
youTubeVideoId: undefined,
canvasIndex: 0,
annotations: [],
});
};
$youTubeVideoIdSelect.onchange = function() {
var $selectedOption = document.querySelector(
"#youTubeVideoIdSelect option:checked"
);
youTubeVideoId = $selectedOption.value;
$youTubeVideoId.value = youTubeVideoId;
urlAdapter.set("youTubeVideoId", youTubeVideoId);
};
$setYouTubeVideoIdButton.onclick = function() {
clearAnnotations();
uv.set({
iiifManifestId: undefined,
youTubeVideoId: youTubeVideoId,
autoPlay: true,
annotations: [],
});
};
// iiif inputs
$iiif_setTargetButton.onclick = function() {
var target = $iiif_target.value;
uv.set({
target: target,
});
};
$iiif_mutedCheckbox.onclick = function(e) {
muted = e.target.checked;
uv.set({
muted: muted,
});
};
$iiif_setRotationButton.onclick = function() {
var rotation = parseInt($iiif_rotation.value);
uv.set({
rotation: rotation,
});
};
$iiif_setAnnotationsButton.onclick = function() {
setAnnotations();
};
$iiif_clearAnnotationsButton.onclick = function() {
clearAnnotations();
setAnnotations();
};
$setConfigButton.onclick = function() {
setConfig();
};
// youtube inputs
$youtube_mutedCheckbox.onclick = function(e) {
muted = e.target.checked;
uv.set({
muted: muted,
});
};
$youtube_setCurrentTimeButton.onclick = function() {
var currentTime = $youtube_currentTime.value;
uv.set({
currentTime: currentTime,
});
};
$youtube_setDurationButton.onclick = function() {
var durationStart = $youtube_durationStart.value;
var durationEnd = $youtube_durationEnd.value;
uv.set({
youTubeVideoId: youTubeVideoId,
autoPlay: true,
duration: [durationStart, durationEnd],
});
};
function clearAnnotations() {
annotations = [];
$iiif_annotations.value = JSON.stringify(annotations);
}
function activateIIIFTab() {
iiifTabButton.classList.add("active");
youTubeTabButton.classList.remove("active");
iiifTab.classList.remove("hidden");
youTubeTab.classList.add("hidden");
// uv.dispose();
// uv = UV.init("uv", {
// iiifManifestId: iiifManifestId,
// });
// addUVEventHandlers();
urlAdapter = new UV.IIIFURLAdapter();
setSelectedIIIFManifest();
const data = urlAdapter.getInitialData({
iiifManifestId: iiifManifestId,
debug: false,
});
if (uv) {
uv.dispose();
}
uv = UV.init("uv", data);
urlAdapter.bindTo(uv);
uv.on("configure", function({ config, cb }) {
const manifest = urlAdapter.get("iiifManifestId");
if (
manifest ===
"https://iiif-commons.github.io/iiif-av-component/examples/data/bl/sounds-tests/loose-ends/C1685_98_P3.json"
) {
// Example of custom, inline config.
cb({
options: {
dropEnabled: true,
footerPanelEnabled: true,
headerPanelEnabled: false,
// leftPanelEnabled: false,
// rightPanelEnabled: false,
limitLocales: false,
overrideFullScreen: false,
pagingEnabled: true,
limitToRange: true,
manifestExclude: true,
},
modules: {
footerPanel: {
options: {
fullscreenEnabled: false,
},
},
headerPanel: {
options: {
localeToggleEnabled: false,
},
},
moreInfoRightPanel: {
options: {
limitToRange: true,
},
},
avCenterPanel: {
options: {
autoAdvanceRanges: false,
limitToRange: true,
enableFastRewind: true,
enableFastForward: true,
},
},
contentLeftPanel: {
options: {
defaultToTreeEnabled: true,
},
},
},
});
return;
}
cb(
fetch("uv-iiif-config.json").then(function(resp) {
return resp.json();
})
);
});
// test inline config, enable doubleclick annotation
uv.on("configure", function({ config, cb }) {
cb({
modules: {
modelViewerCenterPanel: {
options: {
doubleClickAnnotationEnabled: true,
},
},
avCenterPanel: {
options: {
// autoAdvanceRanges: false,
// limitToRange: true,
enableFastRewind: true,
enableFastForward: true,
},
},
openSeadragonCenterPanel: {
options: {
doubleClickAnnotationEnabled: true,
},
},
},
});
// config builder
function createConfigBuilderAccordion(config) {
const accordionContainer = document.createElement("div");
function createAccordionItem(key, value, depth = 0, path = "") {
const itemContainer = document.createElement("div");
itemContainer.style.marginLeft = `${depth * 10}px`;
const toggleButton = document.createElement("button");
toggleButton.textContent = key;
toggleButton.className = "toggleButton";
toggleButton.classList.add("toggleButtonBackground");
toggleButton.classList.add("caret-down");
const contentContainer = document.createElement("div");
contentContainer.style.display = "none";
contentContainer.style.marginLeft = "1rem";
toggleButton.addEventListener("click", () => {
const isHidden = contentContainer.style.display === "none";
contentContainer.style.display = isHidden ? "block" : "none";
toggleButton.classList.toggle("caret-down");
toggleButton.classList.toggle("caret-up");
});
const currentPath = path ? `${path}^${key}` : key;
if (typeof value === "object" && value !== null) {
for (const subKey in value) {
contentContainer.appendChild(
createAccordionItem(subKey, value[subKey], depth + 1, currentPath)
);
}
} else {
const valueElement = document.createElement("div");
if (typeof value === "boolean") {
const select = document.createElement("select");
const trueOption = document.createElement("option");
trueOption.value = "true";
trueOption.textContent = "true";
const falseOption = document.createElement("option");
falseOption.value = "false";
falseOption.textContent = "false";
select.appendChild(trueOption);
select.appendChild(falseOption);
select.value = value.toString();
select.style.width = "50%";
select.id = `cb-${currentPath}`; // Set ID based on tree path with prefix
// Add event listener to update #customConfig on change
select.addEventListener("change", (event) => {
const customConfigTextarea = document.getElementById("customConfig");
const currentConfig = customConfigTextarea.value ? JSON.parse(customConfigTextarea.value) : {};
const pathParts = event.target.id.replace(/^cb-/, "").split("^");
let configPointer = currentConfig;
// Traverse the path to set the value
for (let i = 0; i < pathParts.length - 1; i++) {
if (!configPointer[pathParts[i]]) {
configPointer[pathParts[i]] = {};
}
configPointer = configPointer[pathParts[i]];
}
configPointer[pathParts[pathParts.length - 1]] = event.target.value === "true";
customConfigTextarea.value = JSON.stringify(currentConfig, null, 2);
});
valueElement.appendChild(select);
} else {
const input = document.createElement("input");
input.type = "text";
input.value = value;
input.style.width = "50%";
input.id = `cb-${currentPath}`; // Set ID based on tree path with prefix
const updateButton = document.createElement("button");
updateButton.textContent = "Update";
updateButton.style.marginLeft = "0.5rem";
// Add event listener to update #customConfig on button click
updateButton.addEventListener("click", () => {
const customConfigTextarea = document.getElementById("customConfig");
const currentConfig = customConfigTextarea.value ? JSON.parse(customConfigTextarea.value) : {};
const pathParts = input.id.replace(/^cb-/, "").split("^");
let configPointer = currentConfig;
// Traverse the path to set the value
for (let i = 0; i < pathParts.length - 1; i++) {
if (!configPointer[pathParts[i]]) {
configPointer[pathParts[i]] = {};
}
configPointer = configPointer[pathParts[i]];
}
configPointer[pathParts[pathParts.length - 1]] = input.value;
customConfigTextarea.value = JSON.stringify(currentConfig, null, 2);
});
valueElement.appendChild(input);
valueElement.appendChild(updateButton);
}
valueElement.style.marginBottom = "0.5rem";
valueElement.style.marginLeft = `${(depth + 1) * 10}px`;
contentContainer.appendChild(valueElement);
}
itemContainer.appendChild(toggleButton);
itemContainer.appendChild(contentContainer);
return itemContainer;
}
for (const key in config) {
accordionContainer.appendChild(createAccordionItem(key, config[key]));
}
const configBuilder = document.querySelector("#config-builder");
if (configBuilder) {
configBuilder.innerHTML = "";
configBuilder.appendChild(accordionContainer);
}
}
function mergeConfigs(baseConfig, customConfig) {
for (const key in customConfig) {
if (
typeof customConfig[key] === "object" &&
customConfig[key] !== null &&
!Array.isArray(customConfig[key])
) {
if (!baseConfig[key]) {
baseConfig[key] = {};
}
mergeConfigs(baseConfig[key], customConfig[key]);
} else {
baseConfig[key] = customConfig[key];
}
}
}
if ($customConfig.value.length > 0) {
try {
const customConfig = JSON.parse($customConfig.value);
mergeConfigs(config, customConfig);
} catch (e) {
console.error("Could not parse custom config");
}
}
function sortConfigAlphabetically(config) {
if (typeof config !== "object" || config === null) {
return config;
}
const sortedConfig = {};
const keys = Object.keys(config).sort();
keys.forEach((key) => {
sortedConfig[key] = sortConfigAlphabetically(config[key]);
});
return sortedConfig;
}
config = sortConfigAlphabetically(config);
if (config && typeof config === "object") {
createConfigBuilderAccordion(config);
} else {
console.error("Invalid config object passed to createConfigBuilderAccordion:", config);
}
});
document.getElementById("resetConfigButton").addEventListener("click", function() {
document.getElementById("customConfig").value = "";
document.getElementById("clearStorageCheckbox").checked = true;
setConfig();
});
// apply custom configs from the form
uv.on("configure", function({ config, cb}) {
try {
if ($customConfig.value.length > 0) {
var customConfig = JSON.parse($customConfig.value);
cb(customConfig);
}
} catch (e) {
console.error("Could not parse custom config");
}
});
uv.on("targetChange", function(target) {
$iiif_target.value = target;
});
uv.on("openseadragonExtension.doubleClick", function(e) {
annotations.push({
target: e.target,
bodyValue: String(annotations.length + 1),
});
$iiif_annotations.value = JSON.stringify(annotations);
setAnnotations();
});
uv.on("modelviewerExtension.doubleClick", function(e) {
annotations.push({
target: e.target,
bodyValue: String(annotations.length + 1),
});
$iiif_annotations.value = JSON.stringify(annotations);
setAnnotations();
});
uv.on("multiSelectionMade", function(e) {
console.log("multiSelectionMade", e);
});
uv.on("pinpointAnnotationClicked", function(index) {
console.log("pinpointAnnotationClicked", index);
});
uv.on("clearAnnotations", function(e) {
clearAnnotations();
});
uv.on("mediaelementExtension.mediaMuted", function() {
$iiif_mutedCheckbox.checked = true;
});
uv.on("mediaelementExtension.mediaUnmuted", function() {
$iiif_mutedCheckbox.checked = false;
});
// uv.on("load", function(e) {
// console.log(e);
// });
// uv.set({
// iiifManifestId: iiifManifestId,
// youTubeVideoId: undefined,
// });
}
function activateYouTubeTab() {
youTubeTabButton.classList.add("active");
iiifTabButton.classList.remove("active");
youTubeTab.classList.remove("hidden");
iiifTab.classList.add("hidden");
// todo: implement youtube url adapter
urlAdapter = new UV.IIIFURLAdapter();
setSelectedYouTubeVideoId();
if (uv) {
uv.dispose();
}
uv = UV.init("uv", {
youTubeVideoId: youTubeVideoId,
});
uv.on("configure", function({ config, cb }) {
cb(
new Promise(function(resolve) {
fetch("uv-youtube-config.json").then(function(response) {
resolve(response.json());
});
})
);
});
uv.on("load", function(e) {
console.log("load", e);
});
uv.on("unstarted", function() {
console.log("unstarted");
});
uv.on("ended", function() {
console.log("ended");
});
uv.on("playing", function() {
console.log("playing");
});
uv.on("paused", function() {
console.log("paused");
});
uv.on("error", function(e) {
console.log("error", e);
});
// uv.set({
// iiifManifestId: undefined,
// youTubeVideoId: youTubeVideoId,
// });
}
loadCollections(function() {
activateIIIFTab();
});
// test stories
let storyAnnotations, partOf, target, duration;
let muted = false;
// data for https://www.exhibit.so/api/exhibits/Zmtdg3cwREXzNGCuHgmm
// const annotationsJson =
// "http://localhost:3000/api/exhibits/dIVPgGD9ostBUU1ckIZZ";
// const annotationsJson =
// "http://192.168.1.233:3000/api/exhibits/dIVPgGD9ostBUU1ckIZZ";
const annotationList = document.getElementById("annotation-list");
function handleAnnotationClick(e) {
e.preventDefault();
const anno = storyAnnotations.find(
(a) => a.id === e.currentTarget.dataset.annoId
);
partOf = anno.partOf;
target = anno.target;
duration = anno.duration;
const type = partOf.includes("youtube") ? "youtube" : "iiif";
if (uv) {
uv.dispose();
}
uv = UV.init("uv", {
autoPlay: true,
iiifManifestId: type === "iiif" ? partOf : undefined,
youTubeVideoId: type === "youtube" ? partOf : undefined,
duration: duration,
target: target,
muted: muted,
});
uv.on("load", function(args) {
// console.log("load", args);
// args.player.mute();
});
uv.on("configure", function({ config, cb }) {
cb(
new Promise(function(resolve) {
fetch(`uv-${type}-config.json`).then(function(response) {
resolve(response.json());
});
})
);
});
// uv.set({
// autoPlay: true,
// iiifManifestId: type === "iiif" ? partOf : undefined,
// youTubeVideoId: type === "youtube" ? partOf : undefined,
// duration: duration,
// target: target,
// muted: muted,
// });
}
// function initStory() {
// fetch(annotationsJson).then((res) => {
// const data = res.json().then((data) => {
// storyAnnotations = data.annotations.filter(
// (anno) => anno.motivation === "framing"
// );
// storyAnnotations.forEach((anno) => {
// const li = document.createElement("li");
// const a = document.createElement("a");
// a.dataset.annoId = anno.id;
// a.href = `#${anno.id}`;
// a.innerHTML = anno.bodyValue;
// a.onclick = handleAnnotationClick;
// li.appendChild(a);
// annotationList.appendChild(li);
// });
// partOf = storyAnnotations[0].partOf;
// target = storyAnnotations[0].target;
// });
// });
// }
// initStory();
});
</script>
</body>
</html>