universalviewer
Version:
The Universal Viewer is a community-developed open source project on a mission to help you share your 📚📜📰📽️📻🗿 with the 🌎
936 lines (816 loc) • 28.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: 1rem;
}
/* @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-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 {
padding-inline: 1.5rem;
padding-block: 0.75rem;
font-weight: 500;
font-size: 1.25rem;
border: none;
background: none;
cursor: pointer;
}
.controls-tab.active {
background-color: ButtonFace;
}
.controls-tab-content {
padding: 1.5rem;
color: ButtonText;
background-color: ButtonFace;
}
}
/* 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;
}
</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"
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>
<button id="iiifTabButton" class="controls-tab active">
IIIF
</button>
<button id="youTubeTabButton" class="controls-tab">YouTube</button>
<div id="iiifTab" class="controls-tab-content">
<div class="controls-group">
<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"
href="#"
>
Set IIIF Manifest Id
</button>
</div>
</div>
<div class="controls-group">
<h2 class="controls-title">Target</h2>
<div class="flex">
<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 class="controls-group">
<h2 class="controls-title">Muted</h2>
<input type="checkbox" id="iiif_mutedCheckbox" />
</div>
<div class="controls-group">
<h2 class="controls-title">Rotation</h2>
<div class="flex">
<input id="iiif_rotation" type="text" value="" class="flex-1" />
<button
id="iiif_setRotationButton"
class="controls-button flex-initial ml-2"
>
Set Rotation
</button>
</div>
</div>
<div class="controls-group">
<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>
</div>
<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"
);
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,
});
}
$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();
};
// 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,
},
},
},
});
});
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>