UNPKG

@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
<!DOCTYPE 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>