UNPKG

tify

Version:

A slim and mobile-friendly IIIF document viewer

408 lines (354 loc) 8.81 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <link rel="icon" href="/favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TIFY</title> <style> :root { --base-color: #06b; --base-color-lighter: #e6f0f8; --border-color: #0003; --button-hover-bg: #0058a2; } *, *::before, *::after { margin: 0; padding: 0; font: inherit; } [v-cloak] { display: none !important; } html { background: #666; color: #333; font-size: 24px; } body { font: 16px/1rem sans-serif; margin: 0; } .badge { background: var(--base-color); float: left; border-radius: 2px; color: #fff; font-size: .618em; font-weight: bold; line-height: 1; min-width: .75rem; padding: .2em; text-transform: uppercase; text-align: center; margin: .2rem .4em 0 0; } .bold { font-weight: bold; } .button { background: var(--base-color) linear-gradient(#fff2, #fff0); border: 0; border-radius: 2px; color: #fff; cursor: pointer; font: inherit; font-size: .8125em; font-weight: bold; margin: .125rem; min-width: 1.25rem; padding: .125rem .5rem; white-space: nowrap; } .button:focus, .button:not(:disabled):hover { background: var(--base-color) linear-gradient(#fff4, #fff2); } .button:not(:disabled):active { box-shadow: 0 .5px 3px #0004 inset, 0 0 0 1px #0002 inset; } .button:disabled { opacity: .5; } .container { height: 100vh; } .header + .container { height: calc(100vh - 1.75rem); } .form { display: flex; align-items: center; padding: .125rem; width: 100%; } .header { background: #fff; backdrop-filter: blur(1px); display: flex; position: relative; z-index: 10; box-shadow: 0 -1px var(--border-color) inset; } .input { background: #fff; border: 1px solid transparent; border-radius: 2px; color: inherit; flex: 1; font: inherit; margin: .125rem; min-width: 0; padding: calc(.125rem - 1px) calc(.25rem - 1px); } .input:focus { border-color: var(--base-color); outline: 2px solid var(--base-color-lighter); } .instances { display: flex; flex-wrap: wrap; } .instance { box-shadow: -1px 0 var(--border-color), 0 -1px var(--border-color); flex: 1; position: relative; z-index: 1; } .link { color: var(--base-color); text-decoration: none; } .link:focus, .link:hover { text-decoration: underline; } .menu { position: relative; } .menu-dropdown { background: #fff; border-radius: 2px; display: block; filter: drop-shadow(0 0 .5rem #0006); list-style: none; min-width: 5rem; opacity: 1; padding: .5rem .75rem; position: absolute; right: .125rem; top: calc(100% + .125rem); transition: all .2s; white-space: nowrap; } .menu-dropdown[hidden] { opacity: 0; top: calc(100% - .125rem); visibility: hidden; } .menu-dropdown::before { bottom: 100%; content: ''; border: .25rem solid; border-color: transparent transparent #fff; position: absolute; right: .375rem; } .menu-list { list-style: none; } .menu-list + .menu-list { box-shadow: 0 1px var(--border-color) inset; margin-top: .5rem; padding-top: .5rem; } </style> </head> <body v-scope @vue:mounted="mounted"> <main class="instances"> <section v-for="instance in instances" class="instance" :key="instance.id" > <header v-if="!instance.contentStateActive" v-cloak class="header" > <form class="form" @submit.prevent="instance.loadManifest()"> <input v-model="instance.manifestUrl" type="url" :id="`manifest${instance.id}`" class="input" aria-label="IIIF manifest URL" placeholder="IIIF manifest URL" @focus="event.target.select()" > <button type="submit" class="button" :disabled="!instance.manifestUrl" > Load </button> <div class="menu" @click.stop> <button type="button" class="button" aria-label="Toggle instance and language menu" :aria-controls="`dropdown${instance.id}`" :aria-expanded="instance.dropdownOpen" @click="instance.dropdownOpen = !instance.dropdownOpen" ></button> <div v-cloak :id="`dropdown${instance.id}`" class="menu-dropdown" :hidden="!instance.dropdownOpen" > <ul class="menu-list"> <li> <a class="link" href="javascript:;" @click="addInstance()" > Add instance </a> </li> <li v-if="instances.length > 1"> <a class="link" href="javascript:;" @click="instance.remove()" > Remove instance </a> </li> </ul> <ul class="menu-list"> <li v-for="name, code in languages" :key="code" :class="{ 'bold': code === instance.language }" > <a class="link" href="javascript:;" @click="instance.setLanguage(code)" > <span class="badge">{{ code }}</span> {{ name }} </a> </li> </ul> </div> </div> </form> </header> <div :id="`container${instance.id}`" class="container"></div> </section> </main> <script type="module" src="/src/main.js"></script> <script> /* global PetiteVue, Tify */ $VITE_PETITE_VUE; // eslint-disable-line const instances = PetiteVue.reactive([]); class Instance { constructor(options = {}) { this.id = options.id || ''; while (instances.find((instance) => instance.id === this.id)) { this.id = ((parseInt(this.id, 10) || 1) + 1).toString(); } this.contentStateActive = (new URL(window.location)).searchParams.get('iiif-content'); this.dropdownOpen = false; this.language = options.language || 'en'; this.manifestUrl = options.manifestUrl || ''; this.tify = null; instances.push(this); } remove() { this.tify?.destroy(); instances.splice(instances.findIndex((instance) => instance.id === this.id), 1); const url = new URL(window.location); url.searchParams.delete(`language${this.id}`); url.searchParams.delete(`manifest${this.id}`); url.searchParams.delete(`tify${this.id}`); window.history.pushState(null, '', url.toString()); } loadManifest() { this.tify?.destroy(); const url = new URL(window.location); // Update URL query if manifest was changed via form input if (!this.contentStateActive && url.searchParams.get(`manifest${this.id}`) !== this.manifestUrl ) { url.searchParams.delete(`tify${this.id}`); url.searchParams.set(`manifest${this.id}`, this.manifestUrl); window.history.pushState(null, '', url.toString()); } this.tify = new Tify({ container: document.getElementById(`container${this.id}`), contentStateEnabled: this.contentStateActive, manifestUrl: this.manifestUrl, translationsDirUrl: 'translations', language: this.language, urlQueryKey: `tify${this.id}`, }); // Expose latest instance for e2e tests window.tify = this.tify; } setLanguage(code) { this.language = code; this.tify?.setLanguage(code); const url = new URL(window.location); if (code === 'en') { url.searchParams.delete(`language${this.id}`); } else { url.searchParams.set(`language${this.id}`, code); } window.history.pushState(null, '', url.toString()); } closeDropdown() { this.dropdownOpen = false; } } const app = PetiteVue.createApp({ instances, languages: $VITE_LANGUAGES, // eslint-disable-line mounted() { // Restore state from URL query const params = (new URL(window.location)).searchParams; params.forEach((url, key) => { if (!key.startsWith('manifest') && key !== 'iiif-content') { return; } const id = key === 'iiif-content' ? '' : key.replace('manifest', ''); const instance = new Instance({ id, language: params.get(`language${id}`), manifestUrl: key === 'iiif-content' ? null : url, urlQueryKey: `tify${id}`, }); this.$nextTick(() => { instance.loadManifest(); }); }); if (!instances.length) { new Instance(); // eslint-disable-line no-new } document.addEventListener('click', () => instances.forEach((instance) => instance.closeDropdown())); }, addInstance() { new Instance(); // eslint-disable-line no-new }, }); window.addEventListener('load', () => app.mount('body')); </script> </body>