├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── gh-pages.yml │ └── node.js.yml ├── .gitignore ├── .nvmrc ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── UPGRADING.md ├── build ├── create-icons.js ├── create-translation.js ├── i18n.js └── test-translations.js ├── cypress.config.js ├── dist ├── index.html ├── tify.css ├── tify.ico ├── tify.js └── translations │ ├── de.json │ ├── eo.json │ ├── fr.json │ ├── hr.json │ ├── it.json │ ├── nl.json │ └── pl.json ├── doc ├── api.md ├── introduction.md └── user-guide.md ├── favicon.ico ├── index.html ├── package-lock.json ├── package.json ├── public └── translations │ ├── bg.json │ ├── de.json │ ├── eo.json │ ├── fr.json │ ├── hr.json │ ├── it.json │ ├── nl.json │ ├── pl.json │ ├── sq.json │ └── tr.json ├── src ├── App.vue ├── components │ ├── AppHeader.vue │ ├── CollectionNode.vue │ ├── MetadataList.vue │ ├── PageSelect.vue │ ├── PaginationButtons.vue │ ├── TocList.vue │ ├── ViewCollection.vue │ ├── ViewExport.vue │ ├── ViewFulltext.vue │ ├── ViewHelp.vue │ ├── ViewInfo.vue │ ├── ViewScan.vue │ ├── ViewThumbnails.vue │ └── ViewToc.vue ├── config.js ├── main.js ├── modules │ ├── filter.js │ ├── keyboard.js │ ├── promise.js │ ├── scroll.js │ └── validation.js ├── plugins │ ├── api.js │ ├── i18n.js │ └── store.js └── styles │ ├── extends │ ├── button.scss │ └── panel.scss │ ├── functions │ └── g.scss │ ├── main.scss │ ├── mixins │ ├── dropdown.scss │ ├── hover.scss │ └── range.scss │ ├── sections │ ├── collection.scss │ ├── error.scss │ ├── export.scss │ ├── fulltext.scss │ ├── header.scss │ ├── help.scss │ ├── icon.scss │ ├── info.scss │ ├── list.scss │ ├── loading.scss │ ├── main.scss │ ├── page-select.scss │ ├── scan.scss │ ├── sr-only.scss │ ├── thumbnails.scss │ └── toc.scss │ └── util │ ├── base.scss │ └── settings.scss ├── tests ├── e2e │ ├── api.spec.js │ ├── collection.spec.js │ ├── content-state.spec.js │ ├── export.spec.js │ ├── fulltext.spec.js │ ├── iiif3.spec.js │ ├── info.spec.js │ ├── main.spec.js │ ├── multi-instance.spec.js │ ├── pagination.spec.js │ ├── scan.spec.js │ ├── support │ │ └── e2e.js │ ├── thumbnails.spec.js │ ├── toc.spec.js │ └── views.spec.js ├── iiif-api │ ├── data │ │ ├── annotation-lists │ │ │ ├── 47174896.json │ │ │ └── gdz-PPN235181684_0029-00000001.json │ │ ├── annotations │ │ │ ├── default.json │ │ │ └── gdz-PPN235181684_0029-00000001.xml │ │ ├── images │ │ │ └── default.jpg │ │ ├── infos │ │ │ ├── 2.json │ │ │ └── default.json │ │ └── manifests │ │ │ ├── aku-pal-375.json │ │ │ ├── amherst-63cc6105-570d-407b-af8c-07fda3f8c620.json │ │ │ ├── bdrc-wio_bdr_MW22084_bdr_W22084.json │ │ │ ├── bl-vdc_00000004216E.json │ │ │ ├── bodleian-faeff7fb-f8a7-44b5-95ed-cff9a9ffd198.json │ │ │ ├── cambridge-MS-ADD-08640.json │ │ │ ├── cookbook-recipe-0019-html-in-annotations.json │ │ │ ├── cookbook-recipe-0021-tagging.json │ │ │ ├── cookbook-recipe-0266-full-canvas-annotation.json │ │ │ ├── cookbook-recipe-0283-missing-image.json │ │ │ ├── cookbook-recipe-0377-image-in-annotation.json │ │ │ ├── digitale-sammlungen-bsb00026283.json │ │ │ ├── gdz-DE_611_BF_5619_1801_1806.json │ │ │ ├── gdz-HANS_DE_7_w042081.json │ │ │ ├── gdz-PPN140716181.json │ │ │ ├── gdz-PPN235181684_0029.json │ │ │ ├── gdz-PPN857449303.json │ │ │ ├── harvard-art-museum-299843.json │ │ │ ├── invalid.json │ │ │ ├── mskgent-8210.json │ │ │ ├── not-json.json │ │ │ ├── ubl-0000000001.json │ │ │ ├── utrecht-1874-325480.json │ │ │ ├── wellcome-b18035723.json │ │ │ ├── wellcome-b19974760.json │ │ │ ├── wellcome-b19974760_1.json │ │ │ ├── wellcome-b19974760_1_0004.json │ │ │ ├── wellcome-b19974760_1_0007.json │ │ │ └── wellcome-b24738918.json │ └── server.js └── unit │ ├── App.spec.js │ ├── components │ ├── MetadataList.spec.js │ ├── PageSelect.spec.js │ └── ViewToc.spec.js │ ├── modules │ ├── filter.spec.js │ └── validation.spec.js │ └── plugins │ └── store.spec.js ├── vite.config.js ├── vitest.config.js └── vitest.setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{.babelrc,codecept.json,package.json}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:cypress/recommended', 9 | 'plugin:vue/vue3-recommended', 10 | '@vue/airbnb', 11 | ], 12 | globals: { 13 | ENV: true, // defined in vite.config.js 14 | }, 15 | ignorePatterns: ['dist'], 16 | overrides: [ 17 | { 18 | files: ['*.html'], 19 | parser: '@html-eslint/parser', 20 | extends: ['plugin:@html-eslint/recommended'], 21 | }, 22 | ], 23 | parserOptions: { 24 | ecmaVersion: 'latest', 25 | }, 26 | plugins: [ 27 | '@html-eslint', 28 | 'html', 29 | ], 30 | rules: { 31 | '@html-eslint/indent': ['error', 'tab', { 32 | tagChildrenIndent: { html: 0 }, 33 | }], 34 | '@html-eslint/require-closing-tags': ['error', { 35 | selfClosingCustomPatterns: ['html'], 36 | }], 37 | 'import/prefer-default-export': 'off', 38 | 'import/no-extraneous-dependencies': ['error', { 39 | optionalDependencies: ['tests/unit/index.js'], 40 | }], 41 | indent: ['error', 'tab', { SwitchCase: 1 }], 42 | 'no-continue': 'off', 43 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 44 | 'no-tabs': 'off', 45 | 'object-curly-newline': ['error', { 46 | ImportDeclaration: { multiline: true }, 47 | }], 48 | 'vue/attribute-hyphenation': ['error', 'never'], 49 | 'vue/component-name-in-template-casing': ['error', 'PascalCase', { 50 | registeredComponentsOnly: false, 51 | }], 52 | 'vue/html-indent': ['error', 'tab'], 53 | 'vue/max-len': ['error', 120], 54 | 'vue/no-v-html': 'off', 55 | }, 56 | settings: { 57 | 'import/resolver': { 58 | typescript: {}, // load /tsconfig.json to eslint, required for @iiif/parser 59 | }, 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '39 15 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to GH Pages 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | # Leave path empty for latest tag, otherwise path is PR number or branch name 9 | path: ${{ github.ref_type != 'tag' && (github.event.pull_request.number || github.ref_name) || '' }} 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Install and build 19 | run: | 20 | # Deploy each branch and pull request to tify-iiif-viewer.github.io/tify/ 21 | # Deploy latest tag to tify-iiif-viewer.github.io/tify 22 | echo BASE=/tify/${{ env.path }} >> .env 23 | echo HASHED=1 >> .env 24 | echo OUTDIR=./dist-ci/${{ env.path }} >> .env 25 | npm ci 26 | npm run build 27 | 28 | - name: Deploy 29 | uses: JamesIves/github-pages-deploy-action@v4 30 | with: 31 | branch: gh-pages 32 | clean: false 33 | folder: dist-ci 34 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build-linux: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20, 22] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Get npm cache directory 23 | id: npm-cache-dir 24 | run: | 25 | echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT" 26 | - name: Cache node modules 27 | id: npm-cache 28 | uses: actions/cache@v3 29 | with: 30 | path: ${{ steps.npm-cache-dir.outputs.dir }} 31 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 32 | restore-keys: | 33 | ${{ runner.os }}-node${{ matrix.node-version }}- 34 | 35 | - name: Cache Cypress binary 36 | uses: actions/cache@v3 37 | with: 38 | path: ~/.cache/Cypress 39 | key: cypress-${{ runner.os }}-cypress-node${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} 40 | restore-keys: | 41 | cypress-${{ runner.os }}-cypress-node${{ matrix.node-version }}- 42 | 43 | - name: Install dependencies 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - name: Run tests 48 | run: | 49 | npm ci 50 | npm run test:unit && npm run build && npm run test:e2e 51 | - name: Save test video folder 52 | if: always() 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: video-${{ runner.os }}-node${{ matrix.node-version }} 56 | if-no-files-found: error 57 | path: tests/e2e/ 58 | 59 | build-windows: 60 | runs-on: windows-latest 61 | 62 | strategy: 63 | matrix: 64 | node-version: [20, 22] 65 | 66 | steps: 67 | - uses: actions/checkout@v3 68 | - name: Get npm cache directory 69 | id: npm-cache-dir 70 | shell: pwsh 71 | run: | 72 | echo "dir=$(npm config get cache)" >> "${env:GITHUB_OUTPUT} " 73 | - name: Cache node modules 74 | id: npm-cache 75 | uses: actions/cache@v3 76 | with: 77 | path: ${{ steps.npm-cache-dir.outputs.dir }} 78 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 79 | restore-keys: | 80 | ${{ runner.os }}-node${{ matrix.node-version }}- 81 | 82 | - name: Cache Cypress binary 83 | uses: actions/cache@v3 84 | with: 85 | path: ~/.cache/Cypress 86 | key: cypress-${{ runner.os }}-cypress-node${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} 87 | restore-keys: | 88 | cypress-${{ runner.os }}-cypress-node${{ matrix.node-version }}- 89 | 90 | - name: Install dependencies 91 | uses: actions/setup-node@v3 92 | with: 93 | node-version: ${{ matrix.node-version }} 94 | - name: Run tests 95 | run: | 96 | npm ci 97 | npm run test:unit && npm run build && npm run test:e2e 98 | 99 | - name: Save test video folder 100 | if: always() 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: video-${{ runner.os }}-node${{ matrix.node-version }} 104 | if-no-files-found: error 105 | path: tests/e2e/ 106 | 107 | build-mac: 108 | runs-on: macos-latest 109 | strategy: 110 | matrix: 111 | node-version: [20, 22] 112 | 113 | steps: 114 | - uses: actions/checkout@v3 115 | - name: Get npm cache directory 116 | id: npm-cache-dir 117 | run: | 118 | echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT" 119 | - name: Cache node modules 120 | id: npm-cache 121 | uses: actions/cache@v3 122 | with: 123 | path: ${{ steps.npm-cache-dir.outputs.dir }} 124 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 125 | restore-keys: | 126 | ${{ runner.os }}-node${{ matrix.node-version }}- 127 | 128 | - name: Cache Cypress binary 129 | uses: actions/cache@v3 130 | with: 131 | path: ~/.cache/Cypress 132 | key: cypress-${{ runner.os }}-cypress-node${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} 133 | restore-keys: | 134 | cypress-${{ runner.os }}-cypress-node${{ matrix.node-version }}- 135 | 136 | - name: Install dependencies 137 | uses: actions/setup-node@v3 138 | with: 139 | node-version: ${{ matrix.node-version }} 140 | - name: Run tests 141 | run: | 142 | npm ci 143 | npm run test:unit && npm run build && npm run test:e2e 144 | - name: Save test video folder 145 | if: always() 146 | uses: actions/upload-artifact@v4 147 | with: 148 | name: video-${{ runner.os }}-node${{ matrix.node-version }} 149 | if-no-files-found: error 150 | path: tests/e2e/ 151 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/components/icons 3 | 4 | # Local-only files 5 | .DS_Store 6 | *.local 7 | /*.sh 8 | 9 | # Test output 10 | tests/e2e/output 11 | tests/e2e/screenshots 12 | tests/e2e/videos 13 | tests/unit/coverage 14 | 15 | # Log files 16 | *.log 17 | logs 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | pnpm-debug.log* 22 | lerna-debug.log* 23 | 24 | # Editor directories and files 25 | .idea 26 | .vscode 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | * [Tobias Schäfer](https://aspectis.net) 4 | * [Paul Pestov](https://github.com/paulpestov) 5 | * [Ingo Pfennigstorf](https://github.com/ipf) 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | TIFY 4 | 5 |

6 | 7 | TIFY is a slim and mobile-friendly [IIIF](https://iiif.io/) document viewer built with [Vue.js](https://vuejs.org/). It supports [IIIF Presentation API and Image API](https://iiif.io/api/) version 2 and 3. 8 | 9 | Continue reading to learn how to integrate TIFY into your website or application and about its options and API, [check out the website for usage examples](https://tify.rocks/), or [have a look at the documentation](doc). 10 | 11 | ## Embedding TIFY 12 | 13 | TIFY is available as an [npm package](https://www.npmjs.com/package/tify): 14 | 15 | ``` bash 16 | npm install tify 17 | ``` 18 | 19 | Embed TIFY into your website in three easy steps: 20 | 21 | 1. Include both the JavaScript and the stylesheet. 22 | 23 | - Either download TIFY and copy the contents of the `dist` directory to your server: 24 | 25 | ``` html 26 | 27 | 28 | ``` 29 | 30 | > To avoid issues with browser caching, add a query parameter with the current version, e.g. `?v0.33.0`. 31 | 32 | - Or use [jsDelivr](https://www.jsdelivr.com/): 33 | 34 | ``` html 35 | 36 | 37 | ``` 38 | 39 | - Or `import` TIFY into your web application: 40 | 41 | ``` js 42 | import 'tify' 43 | import 'tify/dist/tify.css' 44 | ``` 45 | 46 | 2. Add an HTML block element with an `id` and set its `height`. 47 | 48 | ``` html 49 |
50 | ``` 51 | 52 | 3. Create a TIFY instance. 53 | 54 | ``` html 55 | 61 | ``` 62 | 63 | Many aspects of the theme can be modified with SCSS variables or CSS custom properties, allowing you to easily adapt TIFY’s appearance to your website. [See the theme settings file](src/styles/util/settings.scss) for all available variables. 64 | 65 | ## Upgrading 66 | 67 | If you are are upgrading from any previous version, [have a look at the upgrading guidelines](UPGRADING.md). 68 | 69 | ## Configuration 70 | 71 | TIFY takes an options object as its only parameter. While optional, you usually want to set `container` and `manifestUrl`. 72 | 73 | See [config.js](src/config.js) for a documentation of all available options. 74 | 75 | An example with most options set to non-default values: 76 | 77 | ``` js 78 | new Tify({ 79 | container: '#tify', 80 | language: 'de', 81 | manifestUrl: 'https://example.org/iiif-manifest.json', 82 | pageLabelFormat: 'P (L)', 83 | pages: [2, 3], 84 | pan: { x: .45, y: .6 }, 85 | translationsDirUrl: '/translations/tify', 86 | urlQueryKey: 'tify', 87 | urlQueryParams: ['pages'], 88 | view: '', 89 | viewer: { 90 | immediateRender: false, 91 | }, 92 | zoom: 1.2, 93 | }) 94 | ``` 95 | 96 | ## API 97 | 98 | TIFY provides an API for controlling most of its features, see [API documentation](doc/api.md). 99 | 100 | ## Build Setup 101 | 102 | You need to have Node.js v18.0 or above, npm (usually comes with Node.js) and git installed. 103 | 104 | Install dependencies: 105 | 106 | ``` bash 107 | npm install 108 | ``` 109 | 110 | Run in development mode with hot reload and automatic linting: 111 | 112 | ``` bash 113 | npm run dev 114 | ``` 115 | 116 | Build for production with minification: 117 | 118 | ``` bash 119 | npm run build 120 | ``` 121 | 122 | The production build will be stored in `dist`. 123 | 124 | ### Running Tests 125 | 126 | Run unit tests: `npm run test:unit` 127 | 128 | Run end-to-end tests: 129 | - Development build: `npm run dev` 130 | - Production build: `npm run build && npm run test:e2e` 131 | 132 | ## Translations 133 | 134 | Translations reside in `public/translations`. Each language is represented by a JSON file, where the file name is the language’s [ISO 639 alpha-2 code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). Each file consists of a single object of key-value pairs; the key is the original English string, the value is the translation. The first key `$language` denotes the native name of the translation’s language. There are a few other special keys starting with `$`; while all other keys are to be translated literally, these keys serve as placeholders for longer sections of text. Search the source files for these keys to reveal their corresponding texts. 135 | 136 | To create a new empty translation, run `node build/create-translation.js` and follow the prompts. 137 | 138 | To check all translations for validity and completeness, use `npm run test:i18n` or `npm run test:i18n:fix`, the latter adding missing keys, removing unused keys, and sorting keys. 139 | 140 | --- 141 | 142 | 143 | Göttingen State and University Library 144 | 145 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading TIFY 2 | 3 | ## Upgrading to v0.33 4 | 5 | No breaking changes. 6 | 7 | ## Upgrading to v0.32 8 | 9 | - `'scan'` is no longer a valid value for `view`. Use the default value `null` instead, or omit the `view` option. 10 | - The default page label format has been changed to `P · L`, resulting in for example `1 · Cover`. For pages without a label, only the number is displayed, regardless of the format. 11 | 12 | ## Upgrading to v0.31 13 | 14 | No breaking changes. 15 | 16 | ## Upgrading to v0.30 17 | 18 | TIFY now supports IIIF Presentation API and Image API version 2 and 3. There are no breaking changes. 19 | 20 | ## Upgrading to v0.29 21 | 22 | - Only concerns local development: Node script names have been changed to match current Vue defaults, e.g. `npm run serve` is now `npm run dev`. See README.md and package.json for more details. 23 | 24 | ## Upgrading to v0.28 25 | 26 | - Layout breakpoints have changed, so TIFY may show different interface elements than before, depending on its container size. Modify the `breakpoints` option if required. 27 | - If you are using a customized stylesheet, it probably needs updating. 28 | 29 | ## Upgrading to v0.27 30 | 31 | - The initially displayed page is now determined by the manifest’s `startCanvas`. To keep the previous behavior of starting with the first page regardless of `startCanvas`, add `pages: [1]` to TIFY’s options. 32 | 33 | ## Upgrading to v0.26 34 | 35 | - Support for setting the manifest URL via query parameter `manifest` has been removed. If you need this feature, use something like this: 36 | ``` js 37 | new Tify({ 38 | container: '#tify', 39 | manifestUrl: (new URLSearchParams(window.location.search)).get('manifest'), 40 | }) 41 | ``` 42 | 43 | ## Upgrading to v0.25 44 | 45 | - The stylesheet is no longer loaded automatically. Add `` to the `` of your HTML. 46 | - TIFY is now a class and must be instantiated, taking an options object as the only parameter instead of setting options globally via `tifyOptions`. To get the previous behavior, set `container`, `manifestUrl` (if not set via URL query), `urlQueryKey`, and the initial `view`: 47 | ``` js 48 | new Tify({ 49 | container: '#tify', 50 | manifestUrl: 'https://example.org/iiif-manifest.json', 51 | urlQueryKey: 'tify', 52 | view: 'info', 53 | }) 54 | ``` 55 | - Changed options: 56 | - `immediateRender` has been replaced with `viewer.immediateRender`. 57 | - `init` has been removed. 58 | - `manifest` has been renamed to `manifestUrl`. 59 | - `panX` and `panY` have been merged into `pan`, an object with two properties `x` and `y`. Old URLs with `panX` and `panY` are still supported. 60 | - `stylesheet` has been removed. 61 | - `title` has been removed. 62 | - `view` is now an empty string by default instead of `info`, meaning TIFY only displays the scan. 63 | - Only relevant if you are using custom styles or added event handlers: In all HTML and CSS class names, `_` has been replaced with `-`. The wrapper class has been changed from `tify-app` to `tify`. 64 | - Internet Explorer 11 is no longer supported. 65 | -------------------------------------------------------------------------------- /build/create-icons.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import url from 'url'; 3 | 4 | const rootDir = url.fileURLToPath(new URL('..', import.meta.url)); 5 | 6 | if (!fs.existsSync(`${rootDir}/node_modules/@mdi/js`)) { 7 | process.exit(); 8 | } 9 | 10 | const mdi = await import('@mdi/js'); 11 | 12 | const iconsDir = `${rootDir}/src/components/icons`; 13 | 14 | fs.mkdirSync(iconsDir, { recursive: true }); 15 | 16 | Object.keys(mdi).forEach((key) => { 17 | const componentHtml = ` 18 | 19 | 24 | 25 | `; 26 | 27 | fs.writeFileSync(`${iconsDir}/Icon${key.substring(3)}.vue`, `${componentHtml.trim()}\n`); 28 | }); 29 | -------------------------------------------------------------------------------- /build/create-translation.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import readline from 'readline'; 3 | 4 | import chalk from 'chalk'; 5 | 6 | import { 7 | findTranslatedStrings, 8 | indention, 9 | rootDir, 10 | } from './i18n.js'; // eslint-disable-line import/extensions 11 | 12 | const rl = readline.createInterface({ 13 | input: process.stdin, 14 | output: process.stdout, 15 | }); 16 | 17 | const langCode = (await new Promise((resolve) => { 18 | rl.question('Two-letter language code (ISO 639-1): ', resolve); 19 | })).toLowerCase(); 20 | 21 | if (!langCode.match(/^[a-z]{2}$/)) { 22 | console.warn(chalk.redBright('Invalid language code')); 23 | process.exit(1); 24 | } 25 | 26 | const fileName = `${rootDir}public/translations/${langCode}.json`; 27 | 28 | if (fs.existsSync(fileName)) { 29 | console.warn(chalk.redBright(`${chalk.bold(fileName)} already exists`)); 30 | process.exit(1); 31 | } 32 | 33 | const language = await new Promise((resolve) => { 34 | rl.question('Native language name: ', resolve); 35 | }); 36 | 37 | rl.close(); 38 | 39 | const translationObject = { 40 | $language: language, 41 | }; 42 | 43 | findTranslatedStrings(`${rootDir}/src`, '\\$translate').forEach((item) => { 44 | translationObject[item.key] = ''; 45 | }); 46 | 47 | const json = JSON.stringify(translationObject, null, indention); 48 | 49 | fs.writeFileSync(fileName, `${json}\n`); 50 | 51 | console.log(`\nCreated ${chalk.dim('file://')}${chalk.bold(fileName)}`); 52 | -------------------------------------------------------------------------------- /build/test-translations.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { 4 | checkTranslationFiles, 5 | findTranslatedStrings, 6 | rootDir, 7 | } from './i18n.js'; // eslint-disable-line import/extensions 8 | 9 | const translatedStrings = findTranslatedStrings(`${rootDir}/src`, '\\$translate') 10 | .map((result) => result.key); 11 | 12 | if (!translatedStrings.length) { 13 | console.log('No translated strings found'); 14 | process.exit(1); 15 | } 16 | 17 | // TODO: Alert on missing language name 18 | translatedStrings.unshift('$language'); 19 | 20 | const options = { 21 | addMissing: process.argv.includes('--add'), 22 | removeUnused: process.argv.includes('--remove'), 23 | sort: process.argv.includes('--sort'), 24 | }; 25 | 26 | const translationsDir = `${rootDir}public/translations`; 27 | const results = checkTranslationFiles(translationsDir, translatedStrings, options); 28 | 29 | let translationsWithIssuesCount = 0; 30 | 31 | results.forEach((result) => { 32 | console.log(`${chalk.dim('file://')}${translationsDir}/${chalk.bold(result.lang)}.json`); 33 | 34 | ['empty', 'missing', 'unused'].forEach((type) => { 35 | const issues = result.issues.filter((issue) => issue.type === type); 36 | const label = `${type.charAt(0).toUpperCase() + type.slice(1)} keys`; 37 | if (issues.length) { 38 | console.log(` ${chalk.redBright(label)}`); 39 | console.log(` ${chalk.red(issues.map((issue) => issue.key).join('\n '))}`); 40 | } 41 | }); 42 | 43 | result.notes.forEach((note) => { 44 | console.log(` ${chalk.cyanBright(note)}`); 45 | }); 46 | 47 | if (result.notes.length || result.issues.length) { 48 | translationsWithIssuesCount += 1; 49 | } else { 50 | console.log(chalk.greenBright(' Shiny!')); 51 | } 52 | 53 | console.log(); 54 | }); 55 | 56 | console.log(`Checked ${results.length} languages, ${ 57 | translationsWithIssuesCount 58 | ? chalk.redBright(`found issues with ${translationsWithIssuesCount}.`) 59 | : chalk.greenBright('found no issues.') 60 | }`); 61 | 62 | process.exit(translationsWithIssuesCount ? 1 : 0); 63 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import htmlvalidate from 'cypress-html-validate/plugin'; 5 | 6 | // eslint-disable-next-line import/extensions 7 | import server from './tests/iiif-api/server.js'; 8 | 9 | const iiifApiPort = 8082; 10 | 11 | server.start(iiifApiPort); 12 | 13 | export default defineConfig({ 14 | e2e: { 15 | baseUrl: 'http://localhost:4173', 16 | defaultCommandTimeout: 2000, 17 | specPattern: 'tests/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', 18 | 19 | viewportWidth: 1600, 20 | viewportHeight: 900, 21 | 22 | fixturesFolder: 'tests/e2e/fixtures', 23 | screenshotsFolder: 'tests/e2e/screenshots', 24 | videosFolder: 'tests/e2e/videos', 25 | downloadsFolder: 'tests/e2e/downloads', 26 | supportFile: 'tests/e2e/support/e2e.js', 27 | 28 | setupNodeEvents(on) { 29 | htmlvalidate.install(on); 30 | }, 31 | }, 32 | env: { 33 | iiifApiUrl: `http://0.0.0.0:${iiifApiPort}`, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /dist/tify.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/e3d9404c9d5c95bf7d5b160939c3fd15fa7b5983/dist/tify.ico -------------------------------------------------------------------------------- /dist/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Deutsch", 3 | "$copyright": "Copyright © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY ist ein schlanker und für Mobilgeräte optimierter IIIF-Dokumenten­betrachter, veröffentlicht unter der GNU Affero General Public License 3.0.", 5 | "About TIFY": "Über TIFY", 6 | "Brightness": "Helligkeit", 7 | "Close PDF list": "PDF-Liste schließen", 8 | "Collapse": "Einklappen", 9 | "Collapse all": "Alle einklappen", 10 | "Collection": "Sammlung", 11 | "Contents": "Inhalt", 12 | "Contrast": "Kontrast", 13 | "Contributors": "Beitragende", 14 | "Could not load child manifest": "Kind-Manifest konnte nicht geladen werden", 15 | "Current Element": "Aktuelles Element", 16 | "Current page:": "Aktuelle Seite:", 17 | "Description": "Beschreibung", 18 | "Dismiss": "Ausblenden", 19 | "Document": "Dokument", 20 | "Documentation": "Dokumentation", 21 | "Download Individual Images": "Einzelbilder herunterladen", 22 | "Exit fullscreen": "Vollbildmodus verlassen", 23 | "Expand": "Ausklappen", 24 | "Expand all": "Alle ausklappen", 25 | "Export": "Export", 26 | "Filter collection": "Sammlung filtern", 27 | "Filter pages": "Seiten filtern", 28 | "First page": "Erste Seite", 29 | "Fullscreen": "Vollbildmodus", 30 | "Fulltext": "Volltext", 31 | "Fulltext not available for this page": "Volltext nicht verfügbar", 32 | "Help": "Hilfe", 33 | "IIIF manifest": "IIIF-Manifest", 34 | "IIIF manifest (collection)": "IIIF-Manifest (Sammlung)", 35 | "IIIF manifest (current document)": "IIIF-Manifest (Dokument)", 36 | "Image filters": "Bildfilter", 37 | "Info": "Info", 38 | "Last page": "Letzte Seite", 39 | "License": "Lizenz", 40 | "Loading": "Wird geladen", 41 | "Logo": "Logo", 42 | "Metadata": "Metadaten", 43 | "Next page": "Nächste Seite", 44 | "Next section": "Nächster Abschnitt", 45 | "No results": "Keine Treffer", 46 | "Other Formats": "Andere Formate", 47 | "page": "Seite", 48 | "Page": "Seite", 49 | "pages": "Seiten", 50 | "Pages": "Seiten", 51 | "PDFs for each element": "PDFs für einzelne Elemente", 52 | "Previous page": "Vorige Seite", 53 | "Previous section": "Voriger Abschnitt", 54 | "Provided by": "Bereitgestellt von", 55 | "Related Resources": "Zugehörige Quellen", 56 | "Renderings": "Bilddaten", 57 | "Report a bug": "Fehler melden", 58 | "Reset": "Zurücksetzen", 59 | "Rotate": "Drehen", 60 | "Saturation": "Sättigung", 61 | "Scan": "Scan", 62 | "Source code": "Quellcode", 63 | "Table of Contents": "Inhaltsverzeichnis", 64 | "Title": "Titel", 65 | "Toggle annotations": "Annotationen umschalten", 66 | "Toggle double-page": "Doppelseite umschalten", 67 | "Toggle image filters": "Bildfilter umschalten", 68 | "Toggle page select": "Seitenauswahl umschalten", 69 | "Version": "Version", 70 | "View": "Ansicht", 71 | "Zoom in": "Vergrößern", 72 | "Zoom out": "Verkleinern" 73 | } 74 | -------------------------------------------------------------------------------- /dist/translations/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Esperanto", 3 | "$copyright": "Kopirajtoj © 2017–2025 Universitato Goettingen / Ŝtata kaj Universitata Biblioteko Goettingen", 4 | "$info": "TIFY estas pli svelta kaj pli movebla amika IIIF-dokumentrigardilo publikigita sub la Ĝenerala Publika Permesilo 3.0 de GNU Affero.", 5 | "About TIFY": "Per TIFY", 6 | "Brightness": "Helecon", 7 | "Close PDF list": "Fermu PDF-liston", 8 | "Collapse": "Kolapso", 9 | "Collapse all": "Kolapu ĉion", 10 | "Collection": "Kolekto", 11 | "Contents": "Enhavojn", 12 | "Contrast": "Kontrasto", 13 | "Contributors": "Kontribuanto", 14 | "Could not load child manifest": "", 15 | "Current Element": "Nuna ero", 16 | "Current page:": "Nuna paĝo:", 17 | "Description": "Priskribo", 18 | "Dismiss": "", 19 | "Document": "Dokumento", 20 | "Documentation": "", 21 | "Download Individual Images": "Elŝutu unuopajn bildojn", 22 | "Exit fullscreen": "Eliru plenekranan reĝimon", 23 | "Expand": "Disfaldas", 24 | "Expand all": "Plivastigu ĉion", 25 | "Export": "Eksporti", 26 | "Filter collection": "", 27 | "Filter pages": "Filtrilaj paĝoj", 28 | "First page": "Unua paĝo", 29 | "Fullscreen": "Plena ekrana reĝimo", 30 | "Fulltext": "Plena teksto", 31 | "Fulltext not available for this page": "Plena teksto ne havebla", 32 | "Help": "Helpu", 33 | "IIIF manifest": "IIIF-Manifesto", 34 | "IIIF manifest (collection)": "", 35 | "IIIF manifest (current document)": "", 36 | "Image filters": "Bilda filtrilo", 37 | "Info": "Info", 38 | "Last page": "Lasta paĝo", 39 | "License": "Permesilo", 40 | "Loading": "Ŝarĝante", 41 | "Logo": "Emblemo", 42 | "Metadata": "Metadatenoj", 43 | "Next page": "Sekva paĝo", 44 | "Next section": "Sekva sekcio", 45 | "No results": "", 46 | "Other Formats": "Aliaj formatoj", 47 | "page": "paĝo", 48 | "Page": "Paĝo", 49 | "pages": "paĝoj", 50 | "Pages": "Paĝoj", 51 | "PDFs for each element": "PDF-oj por individuaj eroj", 52 | "Previous page": "Antaŭa paĝo", 53 | "Previous section": "Antaŭa sekcio", 54 | "Related Resources": "Rilataj fontoj", 55 | "Renderings": "Bildaj datumoj", 56 | "Report a bug": "Raportu eraron", 57 | "Reset": "Restarigi al defaŭlta", 58 | "Rotate": "Turni", 59 | "Saturation": "Saturiĝo", 60 | "Scan": "Skani", 61 | "Source code": "Fontkodo", 62 | "Table of Contents": "Enhavtabelo", 63 | "Title": "Titolo", 64 | "Toggle double-page": "Ŝaltu duoblan paĝon", 65 | "Toggle image filters": "Ŝaltu bildfiltrilojn", 66 | "Toggle page select": "", 67 | "Version": "Versio", 68 | "View": "Vido", 69 | "Zoom in": "Zomi", 70 | "Zoom out": "Malzomi" 71 | } 72 | -------------------------------------------------------------------------------- /dist/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Français", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "À propos de TIFY", 6 | "Brightness": "Luminosité", 7 | "Close PDF list": "Fermer la liste de PDFs", 8 | "Collapse": "Replier", 9 | "Collapse all": "Tout replier", 10 | "Collection": "Collection", 11 | "Contents": "Contenu", 12 | "Contrast": "Contraste", 13 | "Contributors": "Contributeurs", 14 | "Could not load child manifest": "", 15 | "Current Element": "Élement actuel", 16 | "Current page:": "Page actuelle :", 17 | "Description": "Description", 18 | "Dismiss": "Rejeter", 19 | "Document": "Document", 20 | "Documentation": "", 21 | "Download Individual Images": "Télécharger les images individuellement", 22 | "Exit fullscreen": "Quitter le mode plein écran", 23 | "Expand": "Déplier", 24 | "Expand all": "Tout déplier", 25 | "Export": "Exporter", 26 | "Filter collection": "Filtrer la collection", 27 | "Filter pages": "Filtrer les pages", 28 | "First page": "Première page", 29 | "Fullscreen": "Plein écran", 30 | "Fulltext": "Texte intégral", 31 | "Fulltext not available for this page": "Texte intégral non disponible pour cette page", 32 | "Help": "Aide", 33 | "IIIF manifest": "Manifeste IIIF", 34 | "IIIF manifest (collection)": "Manifeste IIIF (collection)", 35 | "IIIF manifest (current document)": "Manifeste IIIF (document actuel)", 36 | "Image filters": "Filtres d’image", 37 | "Info": "Info", 38 | "Last page": "Dernière page", 39 | "License": "License", 40 | "Loading": "Chargement", 41 | "Logo": "Logo", 42 | "Metadata": "Metadonnées", 43 | "Next page": "Page suivante", 44 | "Next section": "Section suivante", 45 | "No results": "Pas de résultats", 46 | "Other Formats": "Autres formats", 47 | "page": "page", 48 | "Page": "Page", 49 | "pages": "pages", 50 | "Pages": "Pages", 51 | "PDFs for each element": "PDFs pour chaque élément", 52 | "Previous page": "Page précédente", 53 | "Previous section": "Section précédente", 54 | "Related Resources": "Ressources associées", 55 | "Renderings": "Rendus", 56 | "Report a bug": "Signaler un bogue", 57 | "Reset": "Réinitialiser", 58 | "Rotate": "Pivoter", 59 | "Saturation": "Saturation", 60 | "Scan": "Scan", 61 | "Source code": "Code source", 62 | "Table of Contents": "Table des matières", 63 | "Title": "Titre", 64 | "Toggle double-page": "Basculer en mode double page", 65 | "Toggle image filters": "Appliquer les filtres visuels", 66 | "Toggle page select": "", 67 | "Version": "Version", 68 | "View": "Vue", 69 | "Zoom in": "Zoomer", 70 | "Zoom out": "Dézoomer" 71 | } 72 | -------------------------------------------------------------------------------- /dist/translations/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Hrvatski", 3 | "$copyright": "Autorska prava © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY je mali i optimiziran za mobilne uređaje preglednik IIIF dokumenata, otorenog koda prema GNU Affero General Public License 3.0.", 5 | "About TIFY": "O TIFY-ju", 6 | "Brightness": "Svjetlina", 7 | "Close PDF list": "Zatvori popis PDF-a", 8 | "Collapse": "Smanji", 9 | "Collapse all": "Smanji sve", 10 | "Collection": "Zbirka", 11 | "Contents": "Sadržaj", 12 | "Contrast": "Kontrast", 13 | "Contributors": "Suradnici", 14 | "Could not load child manifest": "", 15 | "Current Element": "Trenutni element", 16 | "Current page:": "Trenutna stranica:", 17 | "Description": "Opis", 18 | "Dismiss": "Odbaci", 19 | "Document": "Dokument", 20 | "Documentation": "", 21 | "Download Individual Images": "Skini pojedine slike", 22 | "Exit fullscreen": "Isključi preko cijelog ekrana", 23 | "Expand": "Proširi", 24 | "Expand all": "Proširi sve", 25 | "Export": "Izvoz", 26 | "Filter collection": "Filtriraj zbirku", 27 | "Filter pages": "Filtriraj stranice", 28 | "First page": "Prva stranica", 29 | "Fullscreen": "Preko cijelog ekrana", 30 | "Fulltext": "Cjeloviti tekst", 31 | "Fulltext not available for this page": "Cjeloviti tekst nije dostupan za ovu stranicu", 32 | "Help": "Pomoć", 33 | "IIIF manifest": "IIIF-Manifest", 34 | "IIIF manifest (collection)": "IIIF-Manifest (zbirka)", 35 | "IIIF manifest (current document)": "IIIF-Manifest (trenutno)", 36 | "Image filters": "Filteri slike", 37 | "Info": "Info", 38 | "Last page": "Posljednja stranica", 39 | "License": "Licenca", 40 | "Loading": "Učitavam", 41 | "Logo": "Logo", 42 | "Metadata": "Metapodaci", 43 | "Next page": "Sljedeća stranica", 44 | "Next section": "Sljedeća sekcija", 45 | "No results": "Nema rezultata", 46 | "Other Formats": "Ostali formati", 47 | "page": "stranica", 48 | "Page": "Stranica", 49 | "pages": "stranice", 50 | "Pages": "Stranice", 51 | "PDFs for each element": "PDF za svaki element", 52 | "Previous page": "Prethodna stranica", 53 | "Previous section": "Prethodna sekcija", 54 | "Related Resources": "Povezani resursi", 55 | "Renderings": "Renderiranja", 56 | "Report a bug": "Prijavi grešku", 57 | "Reset": "Reset", 58 | "Rotate": "Rotiraj", 59 | "Saturation": "Zasićenje", 60 | "Scan": "Sken", 61 | "Source code": "Izvorni kod", 62 | "Table of Contents": "Sadržaj", 63 | "Title": "Naslov", 64 | "Toggle double-page": "Dvije stranice na stranici", 65 | "Toggle image filters": "Filtri slika", 66 | "Toggle page select": "", 67 | "Version": "Verzija", 68 | "View": "Pogled", 69 | "Zoom in": "Uvećaj", 70 | "Zoom out": "Umanji" 71 | } 72 | -------------------------------------------------------------------------------- /dist/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Italiano", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Informazioni su TIFY", 6 | "Brightness": "Luminosità", 7 | "Close PDF list": "Chiudi l’elenco dei PDF", 8 | "Collapse": "Comprimi", 9 | "Collapse all": "Comprimi tutto", 10 | "Collection": "Collezione", 11 | "Contents": "Contenuto", 12 | "Contrast": "Contrasto", 13 | "Contributors": "", 14 | "Could not load child manifest": "", 15 | "Current Element": "Elemento corrente", 16 | "Current page:": "Pagina corrente:", 17 | "Description": "Descrizione", 18 | "Dismiss": "", 19 | "Document": "Documento", 20 | "Documentation": "", 21 | "Download Individual Images": "Scarica le singole immagini", 22 | "Exit fullscreen": "Esci dalla modalità a schermo intero", 23 | "Expand": "Espandi", 24 | "Expand all": "Espandi tutto", 25 | "Export": "Esporta", 26 | "Filter collection": "", 27 | "Filter pages": "", 28 | "First page": "Prima pagina", 29 | "Fullscreen": "Schermo intero", 30 | "Fulltext": "Testo integrale", 31 | "Fulltext not available for this page": "Testo integrale non disponibile per questa pagina", 32 | "Help": "Aiuto", 33 | "IIIF manifest": "Manifest IIIF", 34 | "IIIF manifest (collection)": "", 35 | "IIIF manifest (current document)": "", 36 | "Image filters": "", 37 | "Info": "Informazioni", 38 | "Last page": "Ultima pagina", 39 | "License": "Licenza", 40 | "Loading": "Caricamento", 41 | "Logo": "", 42 | "Metadata": "Metadati", 43 | "Next page": "Pagina successiva", 44 | "Next section": "Sezione successiva", 45 | "No results": "", 46 | "Other Formats": "Altri formati", 47 | "page": "pagina", 48 | "Page": "Pagina", 49 | "pages": "pagine", 50 | "Pages": "Pagine", 51 | "PDFs for each element": "PDF per ogni elemento", 52 | "Previous page": "Pagina precedente", 53 | "Previous section": "Sezione precedente", 54 | "Related Resources": "Risorse correlate", 55 | "Renderings": "Rendering", 56 | "Report a bug": "", 57 | "Reset": "Ripristina", 58 | "Rotate": "Ruota", 59 | "Saturation": "Saturazione", 60 | "Scan": "Scansione", 61 | "Source code": "Codice sorgente", 62 | "Table of Contents": "Indice", 63 | "Title": "Titolo", 64 | "Toggle double-page": "Attiva/disattiva doppia pagina", 65 | "Toggle image filters": "Attiva/disattiva filtri immagine", 66 | "Toggle page select": "", 67 | "Version": "", 68 | "View": "Vista", 69 | "Zoom in": "Zoom in", 70 | "Zoom out": "Zoom out" 71 | } 72 | -------------------------------------------------------------------------------- /dist/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Nederlands", 3 | "$copyright": "Copyright © 2017–2025 Universiteit Göttingen / Staats- en universiteitsbibliotheek Göttingen", 4 | "$info": "TIFY is een lichtgewicht en voor mobiel geoptimaliseerde IIIF-documentviewer, uitgebracht onder de GNU Affero General Public License 3.0.", 5 | "About TIFY": "Over TIFY", 6 | "Brightness": "Helderheid", 7 | "Close PDF list": "PDF-lijst sluiten", 8 | "Collapse": "Inklappen", 9 | "Collapse all": "Alles inklappen", 10 | "Collection": "Collectie", 11 | "Contents": "Inhoud", 12 | "Contrast": "Contrast", 13 | "Contributors": "Bijdragers", 14 | "Could not load child manifest": "Kon het kind manifest niet laden", 15 | "Current Element": "Huidig element", 16 | "Current page:": "Huidige pagina:", 17 | "Description": "Beschrijving", 18 | "Dismiss": "Verbergen", 19 | "Document": "Document", 20 | "Documentation": "", 21 | "Download Individual Images": "Afzonderlijke afbeeldingen downloaden", 22 | "Exit fullscreen": "Sluit de volledig scherm", 23 | "Expand": "Uitklappen", 24 | "Expand all": "Alles uitklappen", 25 | "Export": "Export", 26 | "Filter collection": "Collectie filter", 27 | "Filter pages": "Filter pagina’s", 28 | "First page": "Eerste pagina", 29 | "Fullscreen": "Volledige scherm", 30 | "Fulltext": "Volledige tekst", 31 | "Fulltext not available for this page": "Volledige tekst niet beschikbaar", 32 | "Help": "Help", 33 | "IIIF manifest": "IIIF-Manifest", 34 | "IIIF manifest (collection)": "IIIF-Manifest (collectie)", 35 | "IIIF manifest (current document)": "IIIF-Manifest (document)", 36 | "Image filters": "Afbeeldingsfilter", 37 | "Info": "Info", 38 | "Last page": "Laatste pagina", 39 | "License": "Licentie", 40 | "Loading": "Word geladen", 41 | "Logo": "Logo", 42 | "Metadata": "Metadata", 43 | "Next page": "Volgende pagina", 44 | "Next section": "Volgende sectie", 45 | "No results": "Geen resultaten", 46 | "Other Formats": "Andere formaten", 47 | "page": "pagina", 48 | "Page": "Pagina", 49 | "pages": "pagina’s", 50 | "Pages": "Pagina’s", 51 | "PDFs for each element": "PDF’s voor individuele elementen", 52 | "Previous page": "Vorige pagina", 53 | "Previous section": "Vorige sectie", 54 | "Related Resources": "Gerelateerde bronnen", 55 | "Renderings": "Afbeeldingsgegevens", 56 | "Report a bug": "Meld een fout", 57 | "Reset": "Reset", 58 | "Rotate": "Draaien", 59 | "Saturation": "Verzadiging", 60 | "Scan": "Scan", 61 | "Source code": "Broncode", 62 | "Table of Contents": "Inhoudsopgave", 63 | "Title": "Titel", 64 | "Toggle double-page": "Dubbele pagina wisselen", 65 | "Toggle image filters": "Schakel afbeeldingsfilter in", 66 | "Toggle page select": "Schakel paginaselectie in", 67 | "Version": "Versie", 68 | "View": "Weergave", 69 | "Zoom in": "Vergroten", 70 | "Zoom out": "Verkleinen" 71 | } 72 | -------------------------------------------------------------------------------- /dist/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Polski", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Więcej o TIFY", 6 | "Brightness": "Jasność", 7 | "Close PDF list": "Zamknij liste plików PDF", 8 | "Collapse": "Zwiń", 9 | "Collapse all": "Zwiń wszystko", 10 | "Collection": "Kolekcja", 11 | "Contents": "Zawartość", 12 | "Contrast": "Kontrast", 13 | "Contributors": "", 14 | "Could not load child manifest": "", 15 | "Current Element": "Obecny element", 16 | "Current page:": "Obecna strona:", 17 | "Description": "Opis", 18 | "Dismiss": "", 19 | "Document": "Dokument", 20 | "Documentation": "", 21 | "Download Individual Images": "Pobierz obrazy indywidualnie", 22 | "Exit fullscreen": "Wyjdź z trybu pełnoekranowego", 23 | "Expand": "Rozwiń", 24 | "Expand all": "Rozwiń wszystko", 25 | "Export": "Eksportuj", 26 | "Filter collection": "", 27 | "Filter pages": "", 28 | "First page": "Pierwsza Strona", 29 | "Fullscreen": "Widok pełnoekranowy", 30 | "Fulltext": "Pełen tekst", 31 | "Fulltext not available for this page": "Pełen tekst niedostępny dla tej strony", 32 | "Help": "Pomoc", 33 | "IIIF manifest": "Manifest IIIF", 34 | "IIIF manifest (collection)": "", 35 | "IIIF manifest (current document)": "", 36 | "Image filters": "", 37 | "Info": "Informacje", 38 | "Last page": "Ostatnia strona", 39 | "License": "Licencja", 40 | "Loading": "Ładowanie", 41 | "Logo": "", 42 | "Metadata": "Metadane", 43 | "Next page": "Następna strona", 44 | "Next section": "Następna sekcja", 45 | "No results": "", 46 | "Other Formats": "Inne formaty", 47 | "page": "strona", 48 | "Page": "Strona", 49 | "pages": "strony", 50 | "Pages": "Strony", 51 | "PDFs for each element": "Pliki PDF dla każdego elementy", 52 | "Previous page": "Poprzednia strona", 53 | "Previous section": "Poprzednia sekcja", 54 | "Related Resources": "Powiązane zasoby", 55 | "Renderings": "Rendery", 56 | "Report a bug": "", 57 | "Reset": "Reset", 58 | "Rotate": "Obróć", 59 | "Saturation": "Saturacja", 60 | "Scan": "Skanuj", 61 | "Source code": "Kod źródłowy", 62 | "Table of Contents": "Spis treści", 63 | "Title": "Tytuł", 64 | "Toggle double-page": "Przejdź do widoku dwóch stron", 65 | "Toggle image filters": "Włącz filtry obrazów", 66 | "Toggle page select": "", 67 | "Version": "", 68 | "View": "Widok", 69 | "Zoom in": "Przybliżenie", 70 | "Zoom out": "Oddalenie" 71 | } 72 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | # TIFY API 2 | 3 | TIFY provides an API for controlling most of its features. With the exception of `mount` and `destroy`, all API functions are only available after TIFY has been mounted and the manifest has been loaded. Then the `ready` promise is fulfilled. There is no API function to load a new manifest; just replace the instance. 4 | 5 | Use the API like this: 6 | 7 | ``` js 8 | const tify = new Tify({ manifestUrl: 'https://example.org/iiif-manifest.json' }) 9 | 10 | tify.mount('#tify') 11 | 12 | tify.ready.then(() => { 13 | tify.setPage([1, 12, 13]) 14 | tify.setView('thumbnails') 15 | tify.viewer.viewport.zoomTo(2) 16 | }) 17 | ``` 18 | 19 | ## Functions 20 | 21 | - **`destroy`** 22 | 23 | Destroys the current instance and removes event listeners. If you are using TIFY in an SPA, this should be called every time a page containing TIFY is unmounted to avoid memory leaks. 24 | 25 | No parameters. 26 | 27 | - **`mount`** 28 | 29 | Mounts TIFY. 30 | 31 | Parameters: 32 | 33 | - `container`: string or HTMLElement, required 34 | 35 | CSS selector pointing to a single HTML node or the node itself into which TIFY is mounted. 36 | 37 | - **`resetScan`** 38 | 39 | Resets the scan display options. 40 | 41 | Parameters: 42 | 43 | - `includingFiltersAndRotation`: boolean, default `false` 44 | 45 | By default, only pan and zoom are reset. If `true`, image filters and rotation are reset, too. 46 | 47 | - **`setPage`** 48 | 49 | Changes the active page or pages. 50 | 51 | Parameters: 52 | 53 | - `pageOrPages`: 1-based integer or array thereof (required) 54 | 55 | Provide a number to display a single page or an array of numbers to display multiple pages at once. If the number (or any of the numbers in the array) is smaller than `1` or greater than the number of pages in the document, the command is ignored. 56 | 57 | Returns an array of the current pages or `false` if `pageOrPages` is invalid. 58 | 59 | - **`setLanguage`** 60 | 61 | Changes the frontend language and loads the associated translation. This function returns a Promise. 62 | 63 | Parameters: 64 | 65 | - `language`: string, default `'en'` 66 | 67 | The language to load. A JSON file containing the translations for this language must be present in `public/translations`. Untranslated strings are displayed in English. 68 | 69 | - **`setView`** 70 | 71 | Changes the active view (panel). 72 | 73 | Parameters: 74 | 75 | - `name`: string (required) 76 | 77 | The view’s name; `'export'`, `'fulltext'`, `'help'`, `'info'`, `'thumbnails'`, `'toc'`, or `null` to display (only) the scan. 78 | 79 | - **`toggleDoublePage`** 80 | 81 | Switches from single to double page (“book view”) and vice versa. 82 | 83 | Parameters: 84 | 85 | - `forced`: boolean, default `false` 86 | 87 | Double page is forced on (`true`) or off (`false`). 88 | 89 | - **`toggleFullscreen`** 90 | 91 | Toggles fullscreen mode. For security reasons, most browsers require a user interaction to enter fullscreen mode; a button calling this function via `onclick` works, but trying to do so automatically does probably not. 92 | 93 | Parameters: 94 | 95 | - `forced`: boolean, default `false` 96 | 97 | Fullscreen is forced on (`true`) or off (`false`). 98 | 99 | ## OpenSeadragon API 100 | 101 | The `viewer` object exposes the full [OpenSeadragon API](https://openseadragon.github.io/docs/OpenSeadragon.html). If you want to control the scan view programmatically, the [methods of `viewer.viewport`](https://openseadragon.github.io/docs/OpenSeadragon.Viewport.html) are probably of interest. 102 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | # TIFY Introduction 2 | 3 | See [readme](../README.md). 4 | -------------------------------------------------------------------------------- /doc/user-guide.md: -------------------------------------------------------------------------------- 1 | # TIFY User Guide 2 | 3 | TIFY is a slim and mobile-friendly IIIF document viewer, created with performance and usability in mind. 4 | 5 | IIIF, which stands for “International Image Interoperability Framework”, defines a set of standardized APIs for describing and delivering images along with presentational and structural metadata over the web. This allows digitized artworks, books, newspapers, manuscripts, maps, scrolls, and archival materials to be shared between institutions and repositories. Any IIIF-compliant application can consume and display those images and metadata. [Get more information about IIIF.](http://iiif.io/about/) 6 | 7 | ## Viewing Multiple Pages 8 | 9 | Any number of pages can be viewed next to each other. 10 | 11 | Open the pages view and select multiple pages by clicking on them while pressing Ctrl – or long-press if you are using a touch screen. 12 | 13 | ## Key Bindings 14 | 15 | TIFY can be fully controlled via keyboard. 16 | 17 | ### View 18 | 19 | | Action | Key | 20 | | --- | :---: | 21 | | Fulltext (if available) | 1 | 22 | | Pages | 2 | 23 | | Contents (if available) | 3 | 24 | | Info | 4 | 25 | | Export | 5 | 26 | | Collection (if available) | 6 | 27 | | Help | 7 | 28 | | Scan | Backspace | 29 | | Toggle fullscreen | F | 30 | 31 | ### Turning Pages 32 | 33 | | Action | Key | 34 | | --- | :---: | 35 | | Previous page | Q or , | 36 | | Next page | E or . | 37 | | First page | ⇧Q | 38 | | Last page | ⇧E | 39 | | Jump to page | X | 40 | | Toggle double-page | B | 41 | 42 | ### Scan 43 | 44 | | Action | Key | 45 | | --- | :---: | 46 | | Pan | W S A D | 47 | | Zoom in | ⇧W or + | 48 | | Zoom out | ⇧S or - | 49 | | Rotate (90 degrees clockwise) | R | 50 | | Toggle filters | I | 51 | | Reset pan and zoom | 0 | 52 | | Reset rotation | + R | 53 | | Reset filters | + I | 54 | | Reset all | + 0 | 55 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/e3d9404c9d5c95bf7d5b160939c3fd15fa7b5983/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tify", 3 | "version": "0.33.0", 4 | "description": "A slim and mobile-friendly IIIF document viewer", 5 | "homepage": "https://tify.rocks/", 6 | "repository": { 7 | "type:": "git", 8 | "url": "git+https://github.com/tify-iiif-viewer/tify.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/tify-iiif-viewer/tify/issues" 12 | }, 13 | "keywords": [ 14 | "iiif", 15 | "viewer", 16 | ":)" 17 | ], 18 | "license": "AGPL-3.0", 19 | "main": "dist/tify.js", 20 | "type": "module", 21 | "scripts": { 22 | "build": "vite build", 23 | "dev": "vite", 24 | "lint": "eslint . --ext .cjs,.html,.js,.jsx,.mjs,.vue --fix --ignore-path .gitignore", 25 | "mock-api": "node -e 'import(`./tests/iiif-api/server.js`).then(server => server.default.start())'", 26 | "postinstall": "node build/create-icons.js", 27 | "preversion": "npm install-clean && npm run test:unit && npm run build && npm run test:e2e", 28 | "preview": "vite preview", 29 | "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", 30 | "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", 31 | "test:i18n": "node build/test-translations.js", 32 | "test:i18n:fix": "node build/test-translations.js --add --remove --sort", 33 | "test:unit": "vitest run", 34 | "version": "npm run build; git add dist" 35 | }, 36 | "devDependencies": { 37 | "@html-eslint/eslint-plugin": "^0.34.0", 38 | "@html-eslint/parser": "^0.34.0", 39 | "@iiif/parser": "^2.1.7", 40 | "@mdi/js": "^7.4.47", 41 | "@rushstack/eslint-patch": "^1.10.5", 42 | "@vitejs/plugin-vue": "^5.2.1", 43 | "@vitest/eslint-plugin": "^1.1.28", 44 | "@vue/eslint-config-airbnb": "^8.0.0", 45 | "@vue/test-utils": "^2.4.6", 46 | "chalk": "^5.4.1", 47 | "click-outside-vue3": "^4.0.1", 48 | "cypress": "^14.0.3", 49 | "cypress-html-validate": "^7.1.0", 50 | "diff": "^7.0.0", 51 | "dotenv": "^16.4.7", 52 | "eslint": "^8.57.1", 53 | "eslint-import-resolver-typescript": "^3.7.0", 54 | "eslint-plugin-cypress": "^3.6.0", 55 | "eslint-plugin-html": "^8.1.2", 56 | "eslint-plugin-vue": "^9.32.0", 57 | "html-validate": "^9.4.0", 58 | "jsdom": "^26.0.0", 59 | "openseadragon": "^5.0.1", 60 | "petite-vue": "^0.4.1", 61 | "start-server-and-test": "^2.0.10", 62 | "striptags": "^3.2.0", 63 | "unplugin-vue-components": "^28.0.0", 64 | "vite": "^6.1.0", 65 | "vite-plugin-banner": "^0.8.0", 66 | "vite-plugin-eslint": "^1.8.1", 67 | "vite-plugin-sass-glob-import": "^5.0.0", 68 | "vite-plugin-vue-devtools": "^7.7.1", 69 | "vitest": "^3.0.5", 70 | "vitest-canvas-mock": "^0.3.3", 71 | "vue": "^3.5.13" 72 | }, 73 | "engines": { 74 | "node": ">= 20", 75 | "npm": ">= 9" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/translations/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Български", 3 | "$copyright": "Авторско право © 2017–2025 Университет Гьотинген / Държавна и университетска библиотека на Гьотинген", 4 | "$info": "TIFY е лек и мобилен IIIF визуализатор на документи, издаден под лиценза GNU Affero General Public License 3.0.", 5 | "About TIFY": "Относно TIFY", 6 | "Brightness": "Яркост", 7 | "Close PDF list": "Затвори списъка с PDF", 8 | "Collapse": "Свий", 9 | "Collapse all": "Свий всички", 10 | "Collection": "Колекция", 11 | "Contents": "Съдържание", 12 | "Contrast": "Контраст", 13 | "Contributors": "Сътрудници", 14 | "Could not load child manifest": "Неуспешно зареждане на подманифест", 15 | "Current Element": "Текущ елемент", 16 | "Current page:": "Текуща страница:", 17 | "Description": "Описание", 18 | "Dismiss": "Затвори", 19 | "Document": "Документ", 20 | "Documentation": "Документация", 21 | "Download Individual Images": "Изтегли отделни изображения", 22 | "Exit fullscreen": "Изход от цял екран", 23 | "Expand": "Разгъни", 24 | "Expand all": "Разгъни всички", 25 | "Export": "Експортиране", 26 | "Filter collection": "Филтрирай колекцията", 27 | "Filter pages": "Филтрирай страниците", 28 | "First page": "Първа страница", 29 | "Fullscreen": "Цял екран", 30 | "Fulltext": "Пълен текст", 31 | "Fulltext not available for this page": "Пълният текст не е наличен за тази страница", 32 | "Help": "Помощ", 33 | "IIIF manifest": "IIIF манифест", 34 | "IIIF manifest (collection)": "IIIF манифест (колекция)", 35 | "IIIF manifest (current document)": "IIIF манифест (текущ документ)", 36 | "Image filters": "Филтри за изображения", 37 | "Info": "Информация", 38 | "Last page": "Последна страница", 39 | "License": "Лиценз", 40 | "Loading": "Зареждане", 41 | "Logo": "Лого", 42 | "Metadata": "Метаданни", 43 | "Next page": "Следваща страница", 44 | "Next section": "Следващ раздел", 45 | "No results": "Няма резултати", 46 | "Other Formats": "Други формати", 47 | "page": "страница", 48 | "Page": "Страница", 49 | "pages": "страници", 50 | "Pages": "Страници", 51 | "PDFs for each element": "PDF файлове за всеки елемент", 52 | "Previous page": "Предишна страница", 53 | "Previous section": "Предишен раздел", 54 | "Provided by": "Предоставено от", 55 | "Related Resources": "Свързани ресурси", 56 | "Renderings": "Рендери", 57 | "Report a bug": "Докладвай за проблем", 58 | "Reset": "Нулирай", 59 | "Rotate": "Завърти", 60 | "Saturation": "Наситеност", 61 | "Scan": "Сканирай", 62 | "Source code": "Изходен код", 63 | "Table of Contents": "Съдържание", 64 | "Title": "Заглавие", 65 | "Toggle annotations": "Превключване на анотации", 66 | "Toggle double-page": "Превключване на двустранен изглед", 67 | "Toggle image filters": "Превключи филтрите на изображения", 68 | "Toggle page select": "Превключи избора на страница", 69 | "Version": "Версия", 70 | "View": "Изглед", 71 | "Zoom in": "Увеличи", 72 | "Zoom out": "Намали" 73 | } 74 | -------------------------------------------------------------------------------- /public/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Deutsch", 3 | "$copyright": "Copyright © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY ist ein schlanker und für Mobilgeräte optimierter IIIF-Dokumenten­betrachter, veröffentlicht unter der GNU Affero General Public License 3.0.", 5 | "About TIFY": "Über TIFY", 6 | "Brightness": "Helligkeit", 7 | "Close PDF list": "PDF-Liste schließen", 8 | "Collapse": "Einklappen", 9 | "Collapse all": "Alle einklappen", 10 | "Collection": "Sammlung", 11 | "Contents": "Inhalt", 12 | "Contrast": "Kontrast", 13 | "Contributors": "Beitragende", 14 | "Could not load child manifest": "Kind-Manifest konnte nicht geladen werden", 15 | "Current Element": "Aktuelles Element", 16 | "Current page:": "Aktuelle Seite:", 17 | "Description": "Beschreibung", 18 | "Dismiss": "Ausblenden", 19 | "Document": "Dokument", 20 | "Documentation": "Dokumentation", 21 | "Download Individual Images": "Einzelbilder herunterladen", 22 | "Exit fullscreen": "Vollbildmodus verlassen", 23 | "Expand": "Ausklappen", 24 | "Expand all": "Alle ausklappen", 25 | "Export": "Export", 26 | "Filter collection": "Sammlung filtern", 27 | "Filter pages": "Seiten filtern", 28 | "First page": "Erste Seite", 29 | "Fullscreen": "Vollbildmodus", 30 | "Fulltext": "Volltext", 31 | "Fulltext not available for this page": "Volltext nicht verfügbar", 32 | "Help": "Hilfe", 33 | "IIIF manifest": "IIIF-Manifest", 34 | "IIIF manifest (collection)": "IIIF-Manifest (Sammlung)", 35 | "IIIF manifest (current document)": "IIIF-Manifest (Dokument)", 36 | "Image filters": "Bildfilter", 37 | "Info": "Info", 38 | "Last page": "Letzte Seite", 39 | "License": "Lizenz", 40 | "Loading": "Wird geladen", 41 | "Logo": "Logo", 42 | "Metadata": "Metadaten", 43 | "Next page": "Nächste Seite", 44 | "Next section": "Nächster Abschnitt", 45 | "No results": "Keine Treffer", 46 | "Other Formats": "Andere Formate", 47 | "page": "Seite", 48 | "Page": "Seite", 49 | "pages": "Seiten", 50 | "Pages": "Seiten", 51 | "PDFs for each element": "PDFs für einzelne Elemente", 52 | "Previous page": "Vorige Seite", 53 | "Previous section": "Voriger Abschnitt", 54 | "Provided by": "Bereitgestellt von", 55 | "Related Resources": "Zugehörige Quellen", 56 | "Renderings": "Bilddaten", 57 | "Report a bug": "Fehler melden", 58 | "Reset": "Zurücksetzen", 59 | "Rotate": "Drehen", 60 | "Saturation": "Sättigung", 61 | "Scan": "Scan", 62 | "Source code": "Quellcode", 63 | "Table of Contents": "Inhaltsverzeichnis", 64 | "Title": "Titel", 65 | "Toggle annotations": "Annotationen umschalten", 66 | "Toggle double-page": "Doppelseite umschalten", 67 | "Toggle image filters": "Bildfilter umschalten", 68 | "Toggle page select": "Seitenauswahl umschalten", 69 | "Version": "Version", 70 | "View": "Ansicht", 71 | "Zoom in": "Vergrößern", 72 | "Zoom out": "Verkleinern" 73 | } 74 | -------------------------------------------------------------------------------- /public/translations/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Esperanto", 3 | "$copyright": "Kopirajtoj © 2017–2025 Universitato Goettingen / Ŝtata kaj Universitata Biblioteko Goettingen", 4 | "$info": "TIFY estas pli svelta kaj pli movebla amika IIIF-dokumentrigardilo publikigita sub la Ĝenerala Publika Permesilo 3.0 de GNU Affero.", 5 | "About TIFY": "Per TIFY", 6 | "Brightness": "Helecon", 7 | "Close PDF list": "Fermu PDF-liston", 8 | "Collapse": "Kolapso", 9 | "Collapse all": "Kolapu ĉion", 10 | "Collection": "Kolekto", 11 | "Contents": "Enhavojn", 12 | "Contrast": "Kontrasto", 13 | "Contributors": "Kontribuanto", 14 | "Could not load child manifest": "", 15 | "Current Element": "Nuna ero", 16 | "Current page:": "Nuna paĝo:", 17 | "Description": "Priskribo", 18 | "Dismiss": "", 19 | "Document": "Dokumento", 20 | "Documentation": "", 21 | "Download Individual Images": "Elŝutu unuopajn bildojn", 22 | "Exit fullscreen": "Eliru plenekranan reĝimon", 23 | "Expand": "Disfaldas", 24 | "Expand all": "Plivastigu ĉion", 25 | "Export": "Eksporti", 26 | "Filter collection": "", 27 | "Filter pages": "Filtrilaj paĝoj", 28 | "First page": "Unua paĝo", 29 | "Fullscreen": "Plena ekrana reĝimo", 30 | "Fulltext": "Plena teksto", 31 | "Fulltext not available for this page": "Plena teksto ne havebla", 32 | "Help": "Helpu", 33 | "IIIF manifest": "IIIF-Manifesto", 34 | "IIIF manifest (collection)": "", 35 | "IIIF manifest (current document)": "", 36 | "Image filters": "Bilda filtrilo", 37 | "Info": "Info", 38 | "Last page": "Lasta paĝo", 39 | "License": "Permesilo", 40 | "Loading": "Ŝarĝante", 41 | "Logo": "Emblemo", 42 | "Metadata": "Metadatenoj", 43 | "Next page": "Sekva paĝo", 44 | "Next section": "Sekva sekcio", 45 | "No results": "", 46 | "Other Formats": "Aliaj formatoj", 47 | "page": "paĝo", 48 | "Page": "Paĝo", 49 | "pages": "paĝoj", 50 | "Pages": "Paĝoj", 51 | "PDFs for each element": "PDF-oj por individuaj eroj", 52 | "Previous page": "Antaŭa paĝo", 53 | "Previous section": "Antaŭa sekcio", 54 | "Related Resources": "Rilataj fontoj", 55 | "Renderings": "Bildaj datumoj", 56 | "Report a bug": "Raportu eraron", 57 | "Reset": "Restarigi al defaŭlta", 58 | "Rotate": "Turni", 59 | "Saturation": "Saturiĝo", 60 | "Scan": "Skani", 61 | "Source code": "Fontkodo", 62 | "Table of Contents": "Enhavtabelo", 63 | "Title": "Titolo", 64 | "Toggle double-page": "Ŝaltu duoblan paĝon", 65 | "Toggle image filters": "Ŝaltu bildfiltrilojn", 66 | "Toggle page select": "", 67 | "Version": "Versio", 68 | "View": "Vido", 69 | "Zoom in": "Zomi", 70 | "Zoom out": "Malzomi" 71 | } 72 | -------------------------------------------------------------------------------- /public/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Français", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "À propos de TIFY", 6 | "Brightness": "Luminosité", 7 | "Close PDF list": "Fermer la liste de PDFs", 8 | "Collapse": "Replier", 9 | "Collapse all": "Tout replier", 10 | "Collection": "Collection", 11 | "Contents": "Contenu", 12 | "Contrast": "Contraste", 13 | "Contributors": "Contributeurs", 14 | "Could not load child manifest": "", 15 | "Current Element": "Élement actuel", 16 | "Current page:": "Page actuelle :", 17 | "Description": "Description", 18 | "Dismiss": "Rejeter", 19 | "Document": "Document", 20 | "Documentation": "", 21 | "Download Individual Images": "Télécharger les images individuellement", 22 | "Exit fullscreen": "Quitter le mode plein écran", 23 | "Expand": "Déplier", 24 | "Expand all": "Tout déplier", 25 | "Export": "Exporter", 26 | "Filter collection": "Filtrer la collection", 27 | "Filter pages": "Filtrer les pages", 28 | "First page": "Première page", 29 | "Fullscreen": "Plein écran", 30 | "Fulltext": "Texte intégral", 31 | "Fulltext not available for this page": "Texte intégral non disponible pour cette page", 32 | "Help": "Aide", 33 | "IIIF manifest": "Manifeste IIIF", 34 | "IIIF manifest (collection)": "Manifeste IIIF (collection)", 35 | "IIIF manifest (current document)": "Manifeste IIIF (document actuel)", 36 | "Image filters": "Filtres d’image", 37 | "Info": "Info", 38 | "Last page": "Dernière page", 39 | "License": "License", 40 | "Loading": "Chargement", 41 | "Logo": "Logo", 42 | "Metadata": "Metadonnées", 43 | "Next page": "Page suivante", 44 | "Next section": "Section suivante", 45 | "No results": "Pas de résultats", 46 | "Other Formats": "Autres formats", 47 | "page": "page", 48 | "Page": "Page", 49 | "pages": "pages", 50 | "Pages": "Pages", 51 | "PDFs for each element": "PDFs pour chaque élément", 52 | "Previous page": "Page précédente", 53 | "Previous section": "Section précédente", 54 | "Related Resources": "Ressources associées", 55 | "Renderings": "Rendus", 56 | "Report a bug": "Signaler un bogue", 57 | "Reset": "Réinitialiser", 58 | "Rotate": "Pivoter", 59 | "Saturation": "Saturation", 60 | "Scan": "Scan", 61 | "Source code": "Code source", 62 | "Table of Contents": "Table des matières", 63 | "Title": "Titre", 64 | "Toggle double-page": "Basculer en mode double page", 65 | "Toggle image filters": "Appliquer les filtres visuels", 66 | "Toggle page select": "", 67 | "Version": "Version", 68 | "View": "Vue", 69 | "Zoom in": "Zoomer", 70 | "Zoom out": "Dézoomer" 71 | } 72 | -------------------------------------------------------------------------------- /public/translations/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Hrvatski", 3 | "$copyright": "Autorska prava © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY je mali i optimiziran za mobilne uređaje preglednik IIIF dokumenata, otorenog koda prema GNU Affero General Public License 3.0.", 5 | "About TIFY": "O TIFY-ju", 6 | "Brightness": "Svjetlina", 7 | "Close PDF list": "Zatvori popis PDF-a", 8 | "Collapse": "Smanji", 9 | "Collapse all": "Smanji sve", 10 | "Collection": "Zbirka", 11 | "Contents": "Sadržaj", 12 | "Contrast": "Kontrast", 13 | "Contributors": "Suradnici", 14 | "Could not load child manifest": "", 15 | "Current Element": "Trenutni element", 16 | "Current page:": "Trenutna stranica:", 17 | "Description": "Opis", 18 | "Dismiss": "Odbaci", 19 | "Document": "Dokument", 20 | "Documentation": "", 21 | "Download Individual Images": "Skini pojedine slike", 22 | "Exit fullscreen": "Isključi preko cijelog ekrana", 23 | "Expand": "Proširi", 24 | "Expand all": "Proširi sve", 25 | "Export": "Izvoz", 26 | "Filter collection": "Filtriraj zbirku", 27 | "Filter pages": "Filtriraj stranice", 28 | "First page": "Prva stranica", 29 | "Fullscreen": "Preko cijelog ekrana", 30 | "Fulltext": "Cjeloviti tekst", 31 | "Fulltext not available for this page": "Cjeloviti tekst nije dostupan za ovu stranicu", 32 | "Help": "Pomoć", 33 | "IIIF manifest": "IIIF-Manifest", 34 | "IIIF manifest (collection)": "IIIF-Manifest (zbirka)", 35 | "IIIF manifest (current document)": "IIIF-Manifest (trenutno)", 36 | "Image filters": "Filteri slike", 37 | "Info": "Info", 38 | "Last page": "Posljednja stranica", 39 | "License": "Licenca", 40 | "Loading": "Učitavam", 41 | "Logo": "Logo", 42 | "Metadata": "Metapodaci", 43 | "Next page": "Sljedeća stranica", 44 | "Next section": "Sljedeća sekcija", 45 | "No results": "Nema rezultata", 46 | "Other Formats": "Ostali formati", 47 | "page": "stranica", 48 | "Page": "Stranica", 49 | "pages": "stranice", 50 | "Pages": "Stranice", 51 | "PDFs for each element": "PDF za svaki element", 52 | "Previous page": "Prethodna stranica", 53 | "Previous section": "Prethodna sekcija", 54 | "Related Resources": "Povezani resursi", 55 | "Renderings": "Renderiranja", 56 | "Report a bug": "Prijavi grešku", 57 | "Reset": "Reset", 58 | "Rotate": "Rotiraj", 59 | "Saturation": "Zasićenje", 60 | "Scan": "Sken", 61 | "Source code": "Izvorni kod", 62 | "Table of Contents": "Sadržaj", 63 | "Title": "Naslov", 64 | "Toggle double-page": "Dvije stranice na stranici", 65 | "Toggle image filters": "Filtri slika", 66 | "Toggle page select": "", 67 | "Version": "Verzija", 68 | "View": "Pogled", 69 | "Zoom in": "Uvećaj", 70 | "Zoom out": "Umanji" 71 | } 72 | -------------------------------------------------------------------------------- /public/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Italiano", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Informazioni su TIFY", 6 | "Brightness": "Luminosità", 7 | "Close PDF list": "Chiudi l’elenco dei PDF", 8 | "Collapse": "Comprimi", 9 | "Collapse all": "Comprimi tutto", 10 | "Collection": "Collezione", 11 | "Contents": "Contenuto", 12 | "Contrast": "Contrasto", 13 | "Contributors": "", 14 | "Could not load child manifest": "", 15 | "Current Element": "Elemento corrente", 16 | "Current page:": "Pagina corrente:", 17 | "Description": "Descrizione", 18 | "Dismiss": "", 19 | "Document": "Documento", 20 | "Documentation": "", 21 | "Download Individual Images": "Scarica le singole immagini", 22 | "Exit fullscreen": "Esci dalla modalità a schermo intero", 23 | "Expand": "Espandi", 24 | "Expand all": "Espandi tutto", 25 | "Export": "Esporta", 26 | "Filter collection": "", 27 | "Filter pages": "", 28 | "First page": "Prima pagina", 29 | "Fullscreen": "Schermo intero", 30 | "Fulltext": "Testo integrale", 31 | "Fulltext not available for this page": "Testo integrale non disponibile per questa pagina", 32 | "Help": "Aiuto", 33 | "IIIF manifest": "Manifest IIIF", 34 | "IIIF manifest (collection)": "", 35 | "IIIF manifest (current document)": "", 36 | "Image filters": "", 37 | "Info": "Informazioni", 38 | "Last page": "Ultima pagina", 39 | "License": "Licenza", 40 | "Loading": "Caricamento", 41 | "Logo": "", 42 | "Metadata": "Metadati", 43 | "Next page": "Pagina successiva", 44 | "Next section": "Sezione successiva", 45 | "No results": "", 46 | "Other Formats": "Altri formati", 47 | "page": "pagina", 48 | "Page": "Pagina", 49 | "pages": "pagine", 50 | "Pages": "Pagine", 51 | "PDFs for each element": "PDF per ogni elemento", 52 | "Previous page": "Pagina precedente", 53 | "Previous section": "Sezione precedente", 54 | "Related Resources": "Risorse correlate", 55 | "Renderings": "Rendering", 56 | "Report a bug": "", 57 | "Reset": "Ripristina", 58 | "Rotate": "Ruota", 59 | "Saturation": "Saturazione", 60 | "Scan": "Scansione", 61 | "Source code": "Codice sorgente", 62 | "Table of Contents": "Indice", 63 | "Title": "Titolo", 64 | "Toggle double-page": "Attiva/disattiva doppia pagina", 65 | "Toggle image filters": "Attiva/disattiva filtri immagine", 66 | "Toggle page select": "", 67 | "Version": "", 68 | "View": "Vista", 69 | "Zoom in": "Zoom in", 70 | "Zoom out": "Zoom out" 71 | } 72 | -------------------------------------------------------------------------------- /public/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Nederlands", 3 | "$copyright": "Copyright © 2017–2025 Universiteit Göttingen / Staats- en universiteitsbibliotheek Göttingen", 4 | "$info": "TIFY is een lichtgewicht en voor mobiel geoptimaliseerde IIIF-documentviewer, uitgebracht onder de GNU Affero General Public License 3.0.", 5 | "About TIFY": "Over TIFY", 6 | "Brightness": "Helderheid", 7 | "Close PDF list": "PDF-lijst sluiten", 8 | "Collapse": "Inklappen", 9 | "Collapse all": "Alles inklappen", 10 | "Collection": "Collectie", 11 | "Contents": "Inhoud", 12 | "Contrast": "Contrast", 13 | "Contributors": "Bijdragers", 14 | "Could not load child manifest": "Kon het kind manifest niet laden", 15 | "Current Element": "Huidig element", 16 | "Current page:": "Huidige pagina:", 17 | "Description": "Beschrijving", 18 | "Dismiss": "Verbergen", 19 | "Document": "Document", 20 | "Documentation": "", 21 | "Download Individual Images": "Afzonderlijke afbeeldingen downloaden", 22 | "Exit fullscreen": "Sluit de volledig scherm", 23 | "Expand": "Uitklappen", 24 | "Expand all": "Alles uitklappen", 25 | "Export": "Export", 26 | "Filter collection": "Collectie filter", 27 | "Filter pages": "Filter pagina’s", 28 | "First page": "Eerste pagina", 29 | "Fullscreen": "Volledige scherm", 30 | "Fulltext": "Volledige tekst", 31 | "Fulltext not available for this page": "Volledige tekst niet beschikbaar", 32 | "Help": "Help", 33 | "IIIF manifest": "IIIF-Manifest", 34 | "IIIF manifest (collection)": "IIIF-Manifest (collectie)", 35 | "IIIF manifest (current document)": "IIIF-Manifest (document)", 36 | "Image filters": "Afbeeldingsfilter", 37 | "Info": "Info", 38 | "Last page": "Laatste pagina", 39 | "License": "Licentie", 40 | "Loading": "Word geladen", 41 | "Logo": "Logo", 42 | "Metadata": "Metadata", 43 | "Next page": "Volgende pagina", 44 | "Next section": "Volgende sectie", 45 | "No results": "Geen resultaten", 46 | "Other Formats": "Andere formaten", 47 | "page": "pagina", 48 | "Page": "Pagina", 49 | "pages": "pagina’s", 50 | "Pages": "Pagina’s", 51 | "PDFs for each element": "PDF’s voor individuele elementen", 52 | "Previous page": "Vorige pagina", 53 | "Previous section": "Vorige sectie", 54 | "Related Resources": "Gerelateerde bronnen", 55 | "Renderings": "Afbeeldingsgegevens", 56 | "Report a bug": "Meld een fout", 57 | "Reset": "Reset", 58 | "Rotate": "Draaien", 59 | "Saturation": "Verzadiging", 60 | "Scan": "Scan", 61 | "Source code": "Broncode", 62 | "Table of Contents": "Inhoudsopgave", 63 | "Title": "Titel", 64 | "Toggle double-page": "Dubbele pagina wisselen", 65 | "Toggle image filters": "Schakel afbeeldingsfilter in", 66 | "Toggle page select": "Schakel paginaselectie in", 67 | "Version": "Versie", 68 | "View": "Weergave", 69 | "Zoom in": "Vergroten", 70 | "Zoom out": "Verkleinen" 71 | } 72 | -------------------------------------------------------------------------------- /public/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Polski", 3 | "$copyright": "", 4 | "$info": "", 5 | "About TIFY": "Więcej o TIFY", 6 | "Brightness": "Jasność", 7 | "Close PDF list": "Zamknij liste plików PDF", 8 | "Collapse": "Zwiń", 9 | "Collapse all": "Zwiń wszystko", 10 | "Collection": "Kolekcja", 11 | "Contents": "Zawartość", 12 | "Contrast": "Kontrast", 13 | "Contributors": "", 14 | "Could not load child manifest": "", 15 | "Current Element": "Obecny element", 16 | "Current page:": "Obecna strona:", 17 | "Description": "Opis", 18 | "Dismiss": "", 19 | "Document": "Dokument", 20 | "Documentation": "", 21 | "Download Individual Images": "Pobierz obrazy indywidualnie", 22 | "Exit fullscreen": "Wyjdź z trybu pełnoekranowego", 23 | "Expand": "Rozwiń", 24 | "Expand all": "Rozwiń wszystko", 25 | "Export": "Eksportuj", 26 | "Filter collection": "", 27 | "Filter pages": "", 28 | "First page": "Pierwsza Strona", 29 | "Fullscreen": "Widok pełnoekranowy", 30 | "Fulltext": "Pełen tekst", 31 | "Fulltext not available for this page": "Pełen tekst niedostępny dla tej strony", 32 | "Help": "Pomoc", 33 | "IIIF manifest": "Manifest IIIF", 34 | "IIIF manifest (collection)": "", 35 | "IIIF manifest (current document)": "", 36 | "Image filters": "", 37 | "Info": "Informacje", 38 | "Last page": "Ostatnia strona", 39 | "License": "Licencja", 40 | "Loading": "Ładowanie", 41 | "Logo": "", 42 | "Metadata": "Metadane", 43 | "Next page": "Następna strona", 44 | "Next section": "Następna sekcja", 45 | "No results": "", 46 | "Other Formats": "Inne formaty", 47 | "page": "strona", 48 | "Page": "Strona", 49 | "pages": "strony", 50 | "Pages": "Strony", 51 | "PDFs for each element": "Pliki PDF dla każdego elementy", 52 | "Previous page": "Poprzednia strona", 53 | "Previous section": "Poprzednia sekcja", 54 | "Related Resources": "Powiązane zasoby", 55 | "Renderings": "Rendery", 56 | "Report a bug": "", 57 | "Reset": "Reset", 58 | "Rotate": "Obróć", 59 | "Saturation": "Saturacja", 60 | "Scan": "Skanuj", 61 | "Source code": "Kod źródłowy", 62 | "Table of Contents": "Spis treści", 63 | "Title": "Tytuł", 64 | "Toggle double-page": "Przejdź do widoku dwóch stron", 65 | "Toggle image filters": "Włącz filtry obrazów", 66 | "Toggle page select": "", 67 | "Version": "", 68 | "View": "Widok", 69 | "Zoom in": "Przybliżenie", 70 | "Zoom out": "Oddalenie" 71 | } 72 | -------------------------------------------------------------------------------- /public/translations/sq.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Shqip", 3 | "$copyright": "E drejta e autorit © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen", 4 | "$info": "TIFY është një aplikacion i vogël web-i që është i optimizuar për të shfaqur dokumentet e formatit IIIF dhe është i publikuar nën linkun GNU Affero General Public License 3.0. Ky aplikacion mund të përdoret lehtësisht në celular gjithashtu.", 5 | "About TIFY": "Rreth TIFY", 6 | "Brightness": "Ndriçimi", 7 | "Close PDF list": "Mbyllni listën PDF", 8 | "Collapse": "Fshihni", 9 | "Collapse all": "Fshihni të gjitha", 10 | "Collection": "Koleksion", 11 | "Contents": "Përmbajtja", 12 | "Contrast": "Kontrasti", 13 | "Contributors": "Kontribuesit", 14 | "Could not load child manifest": "Manifesti-fëmijë nuk mundi të ngarkohej", 15 | "Current Element": "Elementi i tanishëm", 16 | "Current page:": "Faqja e tanishme:", 17 | "Description": "Përshkrimi", 18 | "Dismiss": "Hiqni", 19 | "Document": "Dokument", 20 | "Documentation": "Dokumentim", 21 | "Download Individual Images": "Shkarkoni imazhet teke", 22 | "Exit fullscreen": "Largohuni nga ekrani i plotë", 23 | "Expand": "Shfaqni", 24 | "Expand all": "Shfaqini të gjitha", 25 | "Export": "Eksportoni", 26 | "Filter collection": "Filtroni koleksionin", 27 | "Filter pages": "Filtroni faqet", 28 | "First page": "Faqja e parë", 29 | "Fullscreen": "Ekrani i plotë", 30 | "Fulltext": "Teksti i plotë", 31 | "Fulltext not available for this page": "Teksti nuk është i disponueshëm për këtë faqe", 32 | "Help": "Ndihmë", 33 | "IIIF manifest": "Manifesti (dokumenti)-IIIF", 34 | "IIIF manifest (collection)": "Manifesti (dokumenti) (koleksion)", 35 | "IIIF manifest (current document)": "IIIF manifest (dokumenti i tanishëm)", 36 | "Image filters": "Filterat e imazhit", 37 | "Info": "Info", 38 | "Last page": "Faqja e fundit", 39 | "License": "Licensë", 40 | "Loading": "Duke u ngarkuar", 41 | "Logo": "Logo", 42 | "Metadata": "Metatëdhëna", 43 | "Next page": "Faqja pasardhëse", 44 | "Next section": "Seksioni pasardhës", 45 | "No results": "Nuk ka rezultate", 46 | "Other Formats": "Formate të tjera", 47 | "page": "Faqja", 48 | "Page": "Faqja", 49 | "pages": "Faqet", 50 | "Pages": "Faqet", 51 | "PDFs for each element": "PDF-të për çdo element", 52 | "Previous page": "Faqja paraardhëse", 53 | "Previous section": "Seksioni paraardhës", 54 | "Provided by": "Siguruar nga", 55 | "Related Resources": "Burime të ndërlidhura", 56 | "Renderings": "Të dhënat e imazhit", 57 | "Report a bug": "Raportoni një gabim në aplikacion", 58 | "Reset": "Rivendosni", 59 | "Rotate": "Rrotulloni", 60 | "Saturation": "Intensiteti", 61 | "Scan": "Skanoni", 62 | "Source code": "Kodi", 63 | "Table of Contents": "Tabela e përmbajtjes", 64 | "Title": "Titulli", 65 | "Toggle annotations": "Shfaqni/Fshihni shënimet", 66 | "Toggle double-page": "Shfaqni/Fshihni formatin në 2-faqe", 67 | "Toggle image filters": "Shfaqni/Fshihni filterat e imazhit", 68 | "Toggle page select": "Shfaqni/Fshihni përzgjedhjen e faqes", 69 | "Version": "Versioni", 70 | "View": "Pamja", 71 | "Zoom in": "Zmadhoni", 72 | "Zoom out": "Zvogëloni" 73 | } 74 | -------------------------------------------------------------------------------- /public/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$language": "Türkçe", 3 | "$copyright": "Telif Hakkı © 2017–2025 Göttingen Üniversitesi / Göttingen Eyalet ve Üniversite Kütüphanesi", 4 | "$info": "TIFY, GNU Affero Genel Kamu Lisansı 3.0 altında yayımlanan, ince ve mobil uyumlu bir IIIF belge görüntüleyicisidir.", 5 | "About TIFY": "TIFY Hakkında", 6 | "Brightness": "Parlaklık", 7 | "Close PDF list": "PDF listesini kapat", 8 | "Collapse": "Daralt", 9 | "Collapse all": "Tümünü daralt", 10 | "Collection": "Koleksiyon", 11 | "Contents": "İçindekiler", 12 | "Contrast": "Kontrast", 13 | "Contributors": "Katkıda Bulunanlar", 14 | "Could not load child manifest": "Alt manifest yüklenemedi", 15 | "Current Element": "Mevcut Öğe", 16 | "Current page:": "Mevcut sayfa:", 17 | "Description": "Açıklama", 18 | "Dismiss": "Kapat", 19 | "Document": "Belge", 20 | "Documentation": "Dokümantasyon", 21 | "Download Individual Images": "Tekil Görselleri İndir", 22 | "Exit fullscreen": "Tam ekrandan çık", 23 | "Expand": "Genişlet", 24 | "Expand all": "Tümünü genişlet", 25 | "Export": "Dışa aktar", 26 | "Filter collection": "Koleksiyonu filtrele", 27 | "Filter pages": "Sayfaları filtrele", 28 | "First page": "İlk sayfa", 29 | "Fullscreen": "Tam ekran", 30 | "Fulltext": "Tam metin", 31 | "Fulltext not available for this page": "Bu sayfa için tam metin mevcut değil", 32 | "Help": "Yardım", 33 | "IIIF manifest": "IIIF manifestosu", 34 | "IIIF manifest (collection)": "IIIF manifestosu (koleksiyon)", 35 | "IIIF manifest (current document)": "IIIF manifestosu (mevcut belge)", 36 | "Image filters": "Görsel filtreleri", 37 | "Info": "Bilgi", 38 | "Last page": "Son sayfa", 39 | "License": "Lisans", 40 | "Loading": "Yükleniyor", 41 | "Logo": "Logo", 42 | "Metadata": "Metadata", 43 | "Next page": "Sonraki sayfa", 44 | "Next section": "Sonraki bölüm", 45 | "No results": "Sonuç bulunamadı", 46 | "Other Formats": "Diğer Formatlar", 47 | "page": "sayfa", 48 | "Page": "Sayfa", 49 | "pages": "sayfalar", 50 | "Pages": "Sayfalar", 51 | "PDFs for each element": "Her öğe için PDF'ler", 52 | "Previous page": "Önceki sayfa", 53 | "Previous section": "Önceki bölüm", 54 | "Provided by": "Sağlayan", 55 | "Related Resources": "İlgili Kaynaklar", 56 | "Renderings": "Görselleştirmeler", 57 | "Report a bug": "Hata bildir", 58 | "Reset": "Sıfırla", 59 | "Rotate": "Döndür", 60 | "Saturation": "Doygunluk", 61 | "Scan": "Tara", 62 | "Source code": "Kaynak kodu", 63 | "Table of Contents": "İçindekiler", 64 | "Title": "Başlık", 65 | "Toggle annotations": "Açıklamaları aç/kapat", 66 | "Toggle double-page": "Çift sayfa görünümünü aç/kapat", 67 | "Toggle image filters": "Görsel filtrelerini aç/kapat", 68 | "Toggle page select": "Sayfa seçim modunu aç/kapat", 69 | "Version": "Sürüm", 70 | "View": "Görüntü", 71 | "Zoom in": "Yakınlaştır", 72 | "Zoom out": "Uzaklaştır" 73 | } 74 | -------------------------------------------------------------------------------- /src/components/CollectionNode.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 108 | -------------------------------------------------------------------------------- /src/components/MetadataList.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 110 | -------------------------------------------------------------------------------- /src/components/PaginationButtons.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 75 | -------------------------------------------------------------------------------- /src/components/ViewCollection.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 70 | -------------------------------------------------------------------------------- /src/components/ViewExport.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 183 | -------------------------------------------------------------------------------- /src/components/ViewFulltext.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 98 | -------------------------------------------------------------------------------- /src/components/ViewHelp.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 59 | -------------------------------------------------------------------------------- /src/components/ViewToc.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 79 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue'; 2 | 3 | import App from './App.vue'; 4 | 5 | import defaultOptions from './config'; 6 | 7 | import api from './plugins/api'; 8 | import i18n from './plugins/i18n'; 9 | import store from './plugins/store'; 10 | 11 | window.Tify = function Tify(userOptions = {}) { 12 | this.options = { 13 | // Create independent deep clone 14 | ...JSON.parse(JSON.stringify(defaultOptions)), 15 | ...userOptions, 16 | }; 17 | 18 | if (!this.options.translationsDirUrl) { 19 | const scripts = document.getElementsByTagName('script'); 20 | const tifyScript = [...scripts].find((script) => script.src.includes('/tify')); 21 | if (tifyScript) { 22 | const { src } = tifyScript; 23 | this.options.translationsDirUrl = `${src.substring(0, src.lastIndexOf('/'))}/translations`; 24 | } 25 | } 26 | 27 | let readyPromise = null; 28 | this.ready = new Promise((resolve, reject) => { 29 | readyPromise = { resolve, reject }; 30 | }); 31 | 32 | const instance = this; 33 | this.app = createApp({ 34 | render: () => h(App, { readyPromise }), 35 | }) 36 | .use(api, { instance }) 37 | .use(i18n) 38 | .use(store, { options: this.options }); 39 | 40 | // TODO: Add test 41 | let mounted = false; 42 | this.mount = (container) => { 43 | if (mounted) { 44 | throw new Error('TIFY is already mounted'); 45 | } 46 | 47 | const containerEl = typeof container === 'string' 48 | ? document.querySelector(container) 49 | : container; 50 | 51 | if (!containerEl) { 52 | throw new Error('Container element not found'); 53 | } 54 | 55 | const style = window.getComputedStyle(containerEl); 56 | if (style.position === 'static') { 57 | containerEl.style.position = 'relative'; 58 | } 59 | 60 | this.app.mount(containerEl); 61 | 62 | mounted = true; 63 | }; 64 | 65 | // TODO: Add test 66 | this.destroy = () => { 67 | this.app.unmount(); 68 | }; 69 | 70 | if (this.options.container) { 71 | this.mount(this.options.container); 72 | } 73 | }; 74 | 75 | export default window.Tify; 76 | -------------------------------------------------------------------------------- /src/modules/filter.js: -------------------------------------------------------------------------------- 1 | import striptags from 'striptags'; 2 | 3 | export function filterHtml(html) { 4 | // See http://iiif.io/api/presentation/2.1/#html-markup-in-property-values 5 | const allowedTags = ['a', 'b', 'br', 'i', 'img', 'p', 'span']; 6 | const allowedAttributes = { a: ['href'], img: ['alt', 'src'] }; 7 | 8 | // TODO: striptags removes '<' and '>' inside attribute values 9 | let filteredHtml = striptags(html, allowedTags); 10 | 11 | // Iterate over all opening (including self-closing) HTML tags 12 | const htmlTagsRegex = /<(\w+)((\s+.+?(\s*=\s*(?:".*?"|'.*?'|.*?|[\^'">\s]+))?)+\s*|\s*)>/g; 13 | filteredHtml = filteredHtml.replace(htmlTagsRegex, (match, tag, attributes) => { 14 | if (!attributes) { 15 | return `<${tag}>`; 16 | } 17 | 18 | // Iterate over all attributes and keep only allowed ones 19 | const attributesRegex = /(?:([^\s]+)=(?:"(.*?)"|'(.*?)'))|([^\s]+)/g; 20 | const keptAttributes = []; 21 | attributes.replace(attributesRegex, (tuple, key) => { 22 | if (tuple !== key && allowedAttributes[tag] && allowedAttributes[tag].includes(key)) { 23 | keptAttributes.push(tuple); 24 | } 25 | }); 26 | 27 | return keptAttributes.length > 0 ? `<${tag} ${keptAttributes.join(' ')}>` : `<${tag}>`; 28 | }); 29 | 30 | return filteredHtml; 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/keyboard.js: -------------------------------------------------------------------------------- 1 | export function preventEvent(event) { 2 | if (event.altKey || event.ctrlKey || event.metaKey) { 3 | return true; 4 | } 5 | 6 | if (['INPUT', 'SELECT', 'TEXTAREA'].includes(event.target.nodeName) && event.target.type !== 'range') { 7 | return true; 8 | } 9 | 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/promise.js: -------------------------------------------------------------------------------- 1 | export function createPromise() { 2 | let resolveFunction; 3 | let rejectFunction; 4 | 5 | const promise = new Promise((resolve, reject) => { 6 | resolveFunction = resolve; 7 | rejectFunction = reject; 8 | }); 9 | 10 | promise.resolve = resolveFunction; 11 | promise.reject = rejectFunction; 12 | 13 | return promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/scroll.js: -------------------------------------------------------------------------------- 1 | export function scrollTo(element, to, animationDuration = 120) { 2 | const el = element; 3 | const duration = animationDuration === true ? 120 : animationDuration; 4 | 5 | if (!duration || duration < 0) { 6 | el.scrollTop = to; 7 | return; 8 | } 9 | 10 | const difference = to - element.scrollTop; 11 | const perTick = difference / duration / 0.1; 12 | 13 | setTimeout(() => { 14 | el.scrollTop += perTick; 15 | if (el.scrollTop === to) { 16 | return; 17 | } 18 | 19 | scrollTo(el, to, duration - 10); 20 | }, 10); 21 | } 22 | 23 | export function updateScrollPos(selector, ancestorElement, animated = true) { 24 | const elements = ancestorElement.querySelectorAll(selector); 25 | if (!elements.length) { 26 | return; 27 | } 28 | 29 | let topCurrentElement = elements[0]; 30 | const bottomCurrentElement = elements[elements.length - 1]; 31 | Array.prototype.forEach.call(elements, (element) => { 32 | if (element.dataset.level >= topCurrentElement.dataset.level) { 33 | topCurrentElement = element; 34 | } 35 | }); 36 | 37 | const listRect = ancestorElement.getBoundingClientRect(); 38 | const topCurrentElementRect = topCurrentElement.getBoundingClientRect(); 39 | const bottomCurrentElementRect = bottomCurrentElement.getBoundingClientRect(); 40 | 41 | if (topCurrentElementRect.top < listRect.top) { 42 | const targetPos = topCurrentElementRect.top - listRect.top + ancestorElement.scrollTop; 43 | scrollTo(ancestorElement, targetPos - 50, animated); 44 | } else if (bottomCurrentElementRect.bottom > listRect.bottom) { 45 | const targetPos = bottomCurrentElementRect.bottom - listRect.bottom + ancestorElement.scrollTop; 46 | scrollTo(ancestorElement, targetPos + 50, animated); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/validation.js: -------------------------------------------------------------------------------- 1 | export function isValidPagesArray(pages, pageCount) { 2 | if (!(pages instanceof Array) || !pageCount) { 3 | return false; 4 | } 5 | 6 | // Checking for duplicates 7 | if (new Set(pages).size !== pages.length) { 8 | return false; 9 | } 10 | 11 | for (let i = 0, len = pages.length; i < len; i += 1) { 12 | if (!Number.isInteger(pages[i]) 13 | || (i > 0 && pages[i] > 0 && pages[i] <= pages[i - 1]) 14 | || pages[i] < 0 15 | || pages[i] > pageCount 16 | ) return false; 17 | } 18 | 19 | return true; 20 | } 21 | 22 | export function isValidUrl(string, allowedProtocols = ['https:', 'http:']) { 23 | let url; 24 | 25 | try { 26 | url = new URL(string); 27 | } catch { 28 | return false; 29 | } 30 | 31 | return allowedProtocols.includes(url.protocol); 32 | } 33 | -------------------------------------------------------------------------------- /src/plugins/api.js: -------------------------------------------------------------------------------- 1 | function Api(instance) { 2 | return { 3 | expose(method, name) { 4 | // eslint-disable-next-line no-param-reassign 5 | instance[name || method.name.replace('bound ', '')] = method; 6 | }, 7 | }; 8 | } 9 | 10 | export default { 11 | install: (app, options) => { 12 | // eslint-disable-next-line no-param-reassign 13 | app.config.globalProperties.$api = new Api(options.instance); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | export default { 4 | install: (app) => { 5 | const translation = ref(null); 6 | 7 | // eslint-disable-next-line no-param-reassign 8 | app.config.globalProperties.$translate = (string, fallback) => { 9 | if (translation.value?.[string]) { 10 | return translation.value[string]; 11 | } 12 | 13 | if (import.meta.env.DEV && translation.value) { 14 | // eslint-disable-next-line no-console 15 | console.warn(`Missing translation for "${string}"`); 16 | } 17 | 18 | return fallback || string; 19 | }; 20 | 21 | // NOTE: translationObject contains any number of key-value pairs, where 22 | // the key is the string in the default language (usually English), the 23 | // value is the translated string, e.g. { key: 'Schlüssel' } 24 | // eslint-disable-next-line no-param-reassign 25 | app.config.globalProperties.$translate.setTranslation = (translationObject) => { 26 | translation.value = translationObject; 27 | }; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/styles/extends/button.scss: -------------------------------------------------------------------------------- 1 | %button { 2 | align-items: center; 3 | background: $grey-light linear-gradient(to bottom, #fff7, #fff0); 4 | border-radius: $br; 5 | border: 0; 6 | box-shadow: 0 0 0 1px $border-color inset; 7 | color: inherit; 8 | display: inline-flex; 9 | font: inherit; 10 | justify-content: center; 11 | padding: g(.25) g(.5); 12 | text-align: center; 13 | user-select: none; 14 | vertical-align: middle; 15 | 16 | &:not(:disabled) { 17 | cursor: pointer; 18 | 19 | @include hover { 20 | background: $grey-light linear-gradient(to bottom, #fff, #fff7); 21 | } 22 | 23 | &:active { 24 | box-shadow: $inset-shadow, 0 0 0 1px $border-color inset; 25 | } 26 | 27 | &:focus-visible { 28 | outline: 2px solid $base-color-light; 29 | z-index: 1; 30 | } 31 | } 32 | 33 | &[disabled] { 34 | opacity: .3; 35 | } 36 | } 37 | 38 | %button-active { 39 | background: $link-color linear-gradient(to bottom, #fff3, #fff0); 40 | color: $white; 41 | z-index: 1; 42 | 43 | &:not(:disabled) { 44 | @include hover { 45 | background: $link-color linear-gradient(to bottom, #fff1, #0002); 46 | color: $white; 47 | } 48 | } 49 | } 50 | 51 | %button-small { 52 | @extend %button; 53 | font-size: $font-size-small; 54 | padding: 0 g(.5); 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/extends/panel.scss: -------------------------------------------------------------------------------- 1 | %panel { 2 | background: $white; 3 | box-shadow: -1px 0 $border-color; 4 | flex: 1; 5 | min-width: g(15); 6 | overflow-y: auto; 7 | padding: g(.5); 8 | position: relative; 9 | 10 | .tify.-medium & { 11 | border: 0; 12 | bottom: 0; 13 | min-width: 0; 14 | position: absolute; 15 | top: 0; 16 | width: 100%; 17 | z-index: 0; 18 | } 19 | 20 | &:first-child { 21 | border: 0; 22 | } 23 | 24 | &.-active { 25 | display: block; 26 | } 27 | 28 | &.-always-active { 29 | display: block !important; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/functions/g.scss: -------------------------------------------------------------------------------- 1 | @function g($factor: 1) { 2 | @if ($factor == 1) { 3 | @return $grid-base; 4 | } 5 | 6 | @return calc(#{$grid-base} * #{$factor}); 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'functions/*'; 2 | 3 | @import 'util/settings'; 4 | 5 | @import 'mixins/*'; 6 | 7 | @import 'extends/*'; 8 | 9 | @import 'util/base'; 10 | 11 | @import 'sections/*'; 12 | -------------------------------------------------------------------------------- /src/styles/mixins/dropdown.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | @mixin dropdown($position: left, $alignment: bottom) { 4 | background: $white; 5 | border: $br solid $white; 6 | border-radius: $br; 7 | filter: drop-shadow($drop-shadow); 8 | min-width: 6em; 9 | overflow: visible; 10 | position: absolute; 11 | text-shadow: none; 12 | z-index: 9; 13 | 14 | @if ($alignment == bottom) { 15 | margin: g(.25) 0; 16 | } @else if ($alignment == top) { 17 | margin: g(.25) 0; 18 | bottom: 100%; 19 | top: auto; 20 | transform: none; 21 | } @else if ($alignment == right) { 22 | transform: translateY(-50%); 23 | } 24 | 25 | @if ($position == left) { 26 | left: 0; 27 | right: auto; 28 | } @else if ($position == right) { 29 | left: auto; 30 | right: g(.25); 31 | } 32 | 33 | // Wedge 34 | &::before { 35 | position: absolute; 36 | content: ''; 37 | background: $white; 38 | width: g(.5); 39 | height: g(.5); 40 | transform: rotate(45deg); 41 | z-index: -1; 42 | 43 | @if ($alignment == bottom) { 44 | bottom: auto; 45 | top: calc(#{g(-.25)} - #{$br}); 46 | 47 | @if ($position == left) { 48 | left: g(.5); 49 | right: auto; 50 | } @else if ($position == right) { 51 | left: auto; 52 | right: g(.5); 53 | } 54 | } @else if ($alignment == top) { 55 | top: auto; 56 | bottom: calc(#{g(-.25)} - #{$br}); 57 | 58 | @if ($position == left) { 59 | left: g(.5); 60 | right: auto; 61 | } @else if ($position == right) { 62 | left: auto; 63 | right: g(.5); 64 | } 65 | } @else if ($alignment == right) { 66 | left: calc(#{g(-.25)} - #{$br}); 67 | top: calc(50% - #{g(.25)}); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/styles/mixins/hover.scss: -------------------------------------------------------------------------------- 1 | @mixin hover { 2 | &:hover, 3 | &:focus, 4 | &:active { 5 | @content; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/mixins/range.scss: -------------------------------------------------------------------------------- 1 | // Based on Styling Cross-Browser Compatible Range Inputs with Sass v1.4.1 2 | // Github: https://github.com/darlanrod/input-range-sass 3 | // Author: Darlan Rod https://github.com/darlanrod 4 | // MIT License 5 | 6 | // NOTE: Styles with different vendor prefixes must not be grouped 7 | 8 | $track-color: $border-color; 9 | $track-focus-color: null; 10 | 11 | $thumb-bg: $link-color linear-gradient(to bottom, #fff3, #fff0); 12 | $thumb-focus-bg: $link-color linear-gradient(to bottom, #fff1, #0002); 13 | 14 | $thumb-radius: $br; 15 | $thumb-height: g(); 16 | $thumb-width: g(.5); 17 | 18 | $track-height: g(.25); 19 | 20 | $track-radius: $br; 21 | 22 | @mixin range-track { 23 | cursor: pointer; 24 | height: $track-height; 25 | width: 100%; 26 | } 27 | 28 | @mixin range-thumb { 29 | background: $thumb-bg; 30 | border: 0; 31 | border-radius: $thumb-radius; 32 | cursor: pointer; 33 | height: $thumb-height; 34 | width: $thumb-width; 35 | } 36 | 37 | @mixin range { 38 | -webkit-appearance: none; 39 | margin: g(.25) 0 g(-.25); 40 | width: 100%; 41 | 42 | &::-webkit-slider-runnable-track { 43 | @include range-track; 44 | background: $track-color; 45 | border-radius: $track-radius; 46 | margin: 0; 47 | } 48 | 49 | &::-webkit-slider-thumb { 50 | @include range-thumb; 51 | -webkit-appearance: none; 52 | margin-top: calc((#{$track-height} * .5) - (#{$thumb-height} * .5)); 53 | } 54 | 55 | &::-moz-range-track { 56 | @include range-track; 57 | background: $track-color; 58 | border: 0; 59 | border-radius: $track-radius; 60 | } 61 | 62 | &::-moz-range-thumb { 63 | @include range-thumb; 64 | } 65 | 66 | &:focus { 67 | &::-webkit-slider-runnable-track { 68 | background: $track-focus-color; 69 | box-shadow: $inset-shadow; 70 | } 71 | 72 | &::-webkit-slider-thumb { 73 | background: $thumb-focus-bg; 74 | box-shadow: $inset-shadow; 75 | } 76 | 77 | &::-moz-range-track { 78 | background: $track-focus-color; 79 | box-shadow: $inset-shadow; 80 | } 81 | 82 | &::-moz-range-thumb { 83 | background: $thumb-focus-bg; 84 | box-shadow: $inset-shadow; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/styles/sections/collection.scss: -------------------------------------------------------------------------------- 1 | .tify-collection { 2 | @extend %panel; 3 | overflow-y: scroll; 4 | padding: g(.5); 5 | } 6 | 7 | .tify-collection-controls { 8 | display: flex; 9 | 10 | > :first-child { 11 | border-radius: $br 0 0 $br; 12 | } 13 | 14 | > :last-child { 15 | border-radius: 0 $br $br 0; 16 | } 17 | } 18 | 19 | .tify-collection-filter { 20 | flex: 1; 21 | max-width: 36em; 22 | 23 | &:focus { 24 | z-index: 1; 25 | } 26 | } 27 | 28 | .tify-collection-list { 29 | list-style: none; 30 | margin: 0; 31 | padding: 0; 32 | 33 | & & { 34 | margin-left: g(1); 35 | margin-top: g(.25); 36 | } 37 | 38 | li { 39 | margin-bottom: g(.25); 40 | } 41 | } 42 | 43 | .tify-collection-link { 44 | @extend %button; 45 | color: inherit !important; 46 | display: block; 47 | padding: g(.25) g(.5); 48 | text-align: left; 49 | text-decoration: none; 50 | width: 100%; 51 | 52 | &.-has-children { 53 | font-weight: bold; 54 | } 55 | 56 | .tify-collection-item.-current & { 57 | @extend %button-active; 58 | color: $white !important; 59 | } 60 | 61 | .tify-icon { 62 | margin-left: g(-.25); 63 | } 64 | } 65 | 66 | .tify-collection-no-results { 67 | color: $text-color-muted; 68 | } 69 | 70 | .tify-collection-reset { 71 | @extend %button-small; 72 | margin-left: -1px; 73 | white-space: nowrap; 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/sections/error.scss: -------------------------------------------------------------------------------- 1 | .tify-error { 2 | background: rgba(#d22, .8); 3 | border-radius: 0 $br 0 0; 4 | bottom: 0; 5 | color: $white; 6 | display: flex; 7 | font-weight: bold; 8 | max-height: 50%; 9 | max-width: 90%; 10 | position: absolute; 11 | 12 | p { 13 | margin: 0; 14 | } 15 | } 16 | 17 | .tify-error-close { 18 | align-self: flex-start; 19 | background: 0; 20 | border: 0; 21 | color: $white; 22 | cursor: pointer; 23 | display: flex; 24 | padding: g(.25); 25 | 26 | @include hover { 27 | background: $shade-light; 28 | } 29 | } 30 | 31 | .tify-error-messages { 32 | overflow: auto; 33 | padding: g(.25) g(.5) g(.25) 0; 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/sections/export.scss: -------------------------------------------------------------------------------- 1 | .tify-export { 2 | @extend %panel; 3 | } 4 | 5 | .tify-export-container { 6 | margin: g(.5) 0 g(); 7 | position: relative; 8 | } 9 | 10 | .tify-export-section { 11 | margin: 0 0 g(); 12 | } 13 | 14 | .tify-export-toc { 15 | border: 1px solid $border-color; 16 | border-radius: $br; 17 | margin: g(.5) 0 0; 18 | padding: g(.25); 19 | position: relative; 20 | 21 | h4 { 22 | margin: g(.25) g(.75); 23 | } 24 | 25 | ul { 26 | margin: 0 0 0 g(.5); 27 | padding: 0; 28 | } 29 | } 30 | 31 | .tify-export-toggle { 32 | @extend %button-small; 33 | 34 | &.-close { 35 | border-radius: 0 $br; 36 | padding: g(.25); 37 | position: absolute; 38 | right: 0; 39 | z-index: 1; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/sections/fulltext.scss: -------------------------------------------------------------------------------- 1 | .tify-fulltext { 2 | @extend %panel; 3 | overflow-y: auto; 4 | } 5 | 6 | .tify-fulltext-item { 7 | margin: 0 g(-.75); 8 | padding: 0 g(.5); 9 | 10 | &.-current { 11 | box-shadow: g(-.25) 0 $base-color-light; 12 | color: $link-color; 13 | } 14 | 15 | img { 16 | height: auto; 17 | max-width: 100%; 18 | } 19 | } 20 | 21 | .tify-fulltext-toggle { 22 | border-radius: $br; 23 | cursor: pointer; 24 | display: block; 25 | padding: g(.25); 26 | text-decoration: none; 27 | word-break: break-word; 28 | 29 | @include hover { 30 | background: $base-color-lighter; 31 | color: $link-color; 32 | 33 | // Element label and page number 34 | > span { 35 | background: $base-color-lighter; 36 | } 37 | } 38 | 39 | p { 40 | margin: 0; 41 | } 42 | } 43 | 44 | .tify-fulltext-list { 45 | list-style: none; 46 | padding: 0 !important; 47 | } 48 | 49 | .tify-fulltext-none { 50 | color: $text-color-muted; 51 | font-style: italic; 52 | } 53 | 54 | .tify-fulltext-page { 55 | margin: 0 g(.25) g() g(.5); 56 | } 57 | -------------------------------------------------------------------------------- /src/styles/sections/header.scss: -------------------------------------------------------------------------------- 1 | .tify-header { 2 | background: $header-bg; 3 | box-shadow: 0 1px $border-color; 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: space-between; 7 | z-index: 9; 8 | } 9 | 10 | .tify-header-button-group { 11 | align-items: center; 12 | display: flex; 13 | margin: g(.25) 0; 14 | padding: 0 g(.25); 15 | position: relative; 16 | 17 | &.-page-select { 18 | align-self: center; 19 | box-shadow: 0 0 0 1px $border-color inset; 20 | border-radius: $br; 21 | margin: 0 g(.25); 22 | padding: 0; 23 | 24 | .tify.-small & { 25 | margin: 0; 26 | } 27 | } 28 | 29 | &.-pagination { 30 | padding-left: 0; // There is always the page dropdown to the left, which needs less spacing 31 | 32 | .tify.-small & { 33 | display: none; 34 | } 35 | 36 | .tify-header-popup & { 37 | box-shadow: 0 -1px $border-color; 38 | display: none; 39 | margin: calc(#{$br} * 2 - 1px) 0 0; 40 | padding: $br 0 0; 41 | 42 | .tify.-small & { 43 | display: flex; 44 | } 45 | } 46 | } 47 | 48 | &.-toggle { 49 | display: none; 50 | 51 | .tify.-large & { 52 | display: flex; 53 | } 54 | 55 | .tify-header-column:not(:nth-child(2)) & { 56 | border-left: 1px solid $border-color; 57 | 58 | .tify.-small & { 59 | border: 0; 60 | } 61 | } 62 | } 63 | 64 | &.-view { 65 | .tify.-large & { 66 | display: block; 67 | margin: 0; 68 | padding: 0; 69 | } 70 | 71 | &:nth-child(n+2), 72 | .tify-header-column.-pagination ~ .tify-header-column & { 73 | border-left: 1px solid $border-color; 74 | 75 | .tify.-large & { 76 | border-left: 0; 77 | } 78 | } 79 | } 80 | } 81 | 82 | .tify-header-button { 83 | @extend %button; 84 | align-self: center; 85 | background: none; 86 | border-radius: $br; 87 | border: 0; 88 | box-shadow: none; 89 | margin: 0; // Safari fix 90 | min-height: g(1.5); 91 | min-width: g(1.5); 92 | padding: 0; 93 | 94 | &:not(:disabled) { 95 | @include hover { 96 | box-shadow: 0 0 0 1px $shade-light inset; 97 | } 98 | 99 | &:active { 100 | box-shadow: $inset-shadow, 0 0 0 1px $shade-light inset; 101 | } 102 | } 103 | 104 | &.-active { 105 | @extend %button-active; 106 | } 107 | 108 | &.-icon-only { 109 | display: block; 110 | font-size: 0 !important; // Prevent label from showing 111 | 112 | .tify.-large & { 113 | font-size: inherit !important; 114 | } 115 | } 116 | 117 | &.-scan { 118 | display: none !important; 119 | 120 | .tify.-medium & { 121 | display: block !important; 122 | } 123 | } 124 | 125 | .tify-header-button-group.-toggle & { 126 | padding: g(.375); 127 | 128 | .tify.-tiny & { 129 | margin: g(.125) 0; 130 | padding: g(.25); 131 | } 132 | } 133 | 134 | .tify-header-button-group.-view & { 135 | font-size: .75em; 136 | 137 | &:not(.-icon-only) { 138 | align-items: center; 139 | display: flex; 140 | flex-direction: column; 141 | font-size: $font-size-small; 142 | line-height: 1; 143 | min-height: g(1.75); 144 | min-width: g(1.75); 145 | padding: 0 .5em 4px; 146 | } 147 | 148 | .tify.-large & { 149 | align-items: flex-start; // Chrome, not working in Firefox 150 | display: block; 151 | font: inherit; 152 | min-height: 0; 153 | padding: g(.25); 154 | text-align: left; // Firefox, not working in Chrome 155 | width: 100%; 156 | } 157 | } 158 | 159 | .tify-page-select + & { 160 | border-radius: 0 $br $br 0; 161 | margin-left: -1px; 162 | 163 | &:not(:disabled) { 164 | @include hover { 165 | box-shadow: 0 0 0 1px $border-color inset; 166 | } 167 | 168 | &:active { 169 | box-shadow: $inset-shadow, 0 0 0 1px $border-color inset; 170 | } 171 | } 172 | } 173 | } 174 | 175 | .tify-header-column { 176 | display: flex; 177 | flex-wrap: wrap; 178 | justify-content: space-between; 179 | min-width: 0; 180 | 181 | &:first-child { 182 | flex: 1; 183 | } 184 | } 185 | 186 | .tify-header-popup { 187 | display: flex; 188 | 189 | .tify.-large & { 190 | @include dropdown(right); 191 | box-shadow: none; 192 | display: none; 193 | top: g(2); 194 | 195 | &.-visible { 196 | display: block; 197 | } 198 | } 199 | } 200 | 201 | .tify-header-title { 202 | -webkit-box-orient: vertical; 203 | -webkit-line-clamp: 2; 204 | align-self: center; 205 | display: -webkit-box; 206 | font-size: 1em; 207 | font-weight: normal; 208 | line-height: g(); 209 | margin: 0; 210 | overflow: hidden; 211 | margin: g(.125) g(.5); 212 | text-align: left; 213 | text-overflow: ellipsis; 214 | } 215 | -------------------------------------------------------------------------------- /src/styles/sections/help.scss: -------------------------------------------------------------------------------- 1 | .tify-help { 2 | @extend %panel; 3 | overflow-y: auto; 4 | } 5 | 6 | .tify-help-footer { 7 | box-shadow: 0 1px $border-color inset; 8 | font-size: $font-size-small; 9 | margin-top: g(); 10 | padding-top: g(.5); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/sections/icon.scss: -------------------------------------------------------------------------------- 1 | .tify-icon { 2 | fill: currentColor; 3 | height: 24px; 4 | vertical-align: middle; 5 | width: 24px; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/sections/info.scss: -------------------------------------------------------------------------------- 1 | $info-content-max-height: g(6.5); 2 | 3 | .tify-info { 4 | @extend %panel; 5 | overflow-y: auto; 6 | } 7 | 8 | .tify-info-button { 9 | @extend %button-small; 10 | border-radius: 0; 11 | 12 | &:first-child { 13 | border-radius: $br 0 0 $br; 14 | } 15 | 16 | &:last-child { 17 | border-radius: 0 $br $br 0; 18 | } 19 | 20 | & + & { 21 | margin-left: -1px; 22 | } 23 | 24 | &.-active { 25 | @extend %button-active; 26 | } 27 | } 28 | 29 | .tify-info-content { 30 | position: relative; 31 | 32 | &.-collapsed { 33 | max-height: $info-content-max-height; 34 | } 35 | } 36 | 37 | .tify-info-header { 38 | display: inline-flex; 39 | margin: 0 0 g(.75); 40 | position: relative; 41 | } 42 | 43 | .tify-info-logo { 44 | display: block; 45 | max-height: g(6); 46 | max-width: g(12); 47 | height: auto; 48 | width: auto; 49 | } 50 | 51 | .tify-info-metadata { 52 | > div { 53 | margin-bottom: g(.5); 54 | } 55 | } 56 | 57 | .tify-info-section { 58 | margin: 0 0 g(); 59 | word-break: break-word; 60 | 61 | &.-attribution { 62 | // Content may contain an image, insert break before and after 63 | img { 64 | display: block; 65 | } 66 | } 67 | 68 | &.-logo { 69 | > a { 70 | border: 0; 71 | box-shadow: none; 72 | display: inline-block; 73 | } 74 | } 75 | 76 | &.-title { 77 | > p { 78 | font-weight: bold; 79 | } 80 | } 81 | } 82 | 83 | .tify-info-toggle { 84 | @extend %button-small; 85 | margin: g(.5) 0; 86 | padding-left: g(.25); // move icon closer to the left 87 | position: relative; 88 | } 89 | 90 | .tify-info-value { 91 | > div:last-child > :last-child { 92 | margin-bottom: 0; 93 | } 94 | 95 | .tify-info-content.-collapsed & { 96 | max-height: calc(#{$info-content-max-height} - #{g(2)}); // 2 = button height 97 | overflow: hidden; 98 | 99 | &::after { 100 | background: linear-gradient(rgba($white, 0), rgba($white, 1)); 101 | bottom: g(2); // 2 = button height 102 | content: ''; 103 | height: g(2); 104 | position: absolute; 105 | width: 100%; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/styles/sections/list.scss: -------------------------------------------------------------------------------- 1 | .tify-list { 2 | margin: 0 0 g(.5); 3 | padding: 0 0 0 g(); 4 | 5 | li { 6 | margin: 0; 7 | padding: 0; 8 | 9 | &:only-child { 10 | list-style: none; 11 | margin-left: g(-1); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/sections/loading.scss: -------------------------------------------------------------------------------- 1 | .tify-loading { 2 | animation: fadeIn .5s forwards .5s, spin 1.2s infinite ease-in-out; 3 | background: $base-color-light; 4 | border-radius: $br; 5 | bottom: g(.5); 6 | height: g(1.5); 7 | left: g(.5); 8 | opacity: 0; 9 | pointer-events: none; 10 | position: absolute; 11 | width: g(1.5); 12 | 13 | @keyframes fadeIn { 14 | 0% { 15 | opacity: 0; 16 | } 17 | 18 | 100% { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | @keyframes spin { 24 | 0% { 25 | transform: perspective(120px) rotateX(0deg) rotateY(0deg); 26 | } 27 | 28 | 50% { 29 | transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); 30 | } 31 | 32 | 100% { 33 | transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/sections/main.scss: -------------------------------------------------------------------------------- 1 | .tify-main { 2 | display: flex; 3 | flex: 1; 4 | overflow: hidden; 5 | position: relative; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/sections/page-select.scss: -------------------------------------------------------------------------------- 1 | .tify-page-select { 2 | a { 3 | border: 0; 4 | box-shadow: none; 5 | } 6 | } 7 | 8 | .tify-page-select-button { 9 | @extend %button; 10 | background: none; 11 | border-radius: $br 0 0 $br; 12 | box-shadow: none; 13 | display: block; 14 | height: 100%; 15 | max-width: g(10); 16 | min-width: g(3); 17 | overflow: hidden; 18 | padding: g(.25) .5em; 19 | position: relative; 20 | text-overflow: ellipsis; 21 | white-space: nowrap; 22 | 23 | &:not(:disabled) { 24 | @include hover { 25 | box-shadow: 0 0 0 1px $border-color inset; 26 | } 27 | 28 | &:active { 29 | box-shadow: $inset-shadow, 0 0 0 1px $border-color inset; 30 | } 31 | } 32 | 33 | .tify.-medium & { 34 | max-width: g(8); 35 | } 36 | 37 | .tify.-small & { 38 | max-width: g(6); 39 | } 40 | 41 | .tify.-tiny & { 42 | max-width: g(4); 43 | } 44 | } 45 | 46 | .tify-page-select-dropdown { 47 | @include dropdown; 48 | max-width: 100%; 49 | text-align: center; 50 | 51 | .tify.-small & { 52 | left: 0; 53 | } 54 | } 55 | 56 | .tify-page-select-filter { 57 | padding: $br $br calc(#{$br} * 2); 58 | } 59 | 60 | .tify-page-select-input { 61 | width: 100%; 62 | } 63 | 64 | .tify-page-select-list { 65 | list-style: none; 66 | margin: 0; 67 | max-height: g(11); 68 | min-width: 100%; 69 | overflow-y: scroll; 70 | padding: 0; 71 | position: relative; 72 | 73 | > li { 74 | margin: 0; 75 | user-select: none; 76 | 77 | + li { 78 | box-shadow: 0 1px $border-color inset; 79 | } 80 | 81 | > a { 82 | @include hover { 83 | background: $shade-light; 84 | } 85 | } 86 | 87 | &.-current > a { 88 | background: $shade-light; 89 | } 90 | 91 | &.-highlighted > a { 92 | background: $link-color; 93 | color: $white; 94 | } 95 | } 96 | 97 | a { 98 | color: inherit; 99 | display: block; 100 | padding: g(.125) .5em; 101 | text-decoration: none; 102 | 103 | @include hover { 104 | color: inherit; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/styles/sections/scan.scss: -------------------------------------------------------------------------------- 1 | .tify-scan { 2 | @extend %panel; 3 | background: transparent; 4 | box-shadow: none; 5 | flex: 3; 6 | padding: 0; 7 | user-select: none; 8 | z-index: 0; 9 | } 10 | 11 | .tify-scan-buttons { 12 | border-radius: $br; 13 | display: flex; 14 | flex-direction: column; 15 | left: g(.25); 16 | position: absolute; 17 | top: g(.25); 18 | z-index: 9; 19 | } 20 | 21 | .tify-scan-button { 22 | @extend %button; 23 | background: none; 24 | box-shadow: none; 25 | color: $white; 26 | height: g(1.5); 27 | padding: 0; 28 | position: relative; 29 | width: g(1.5); 30 | 31 | &:not(:disabled) { 32 | @include hover { 33 | backdrop-filter: $blur; 34 | background: $shade; 35 | } 36 | } 37 | 38 | &.-active { 39 | // dot marker 40 | &::after { 41 | background: $base-color; 42 | box-shadow: 0 0 g(.25) $base-color-lighter; 43 | border-radius: 50%; 44 | content: ''; 45 | display: block; 46 | height: .5em; 47 | position: absolute; 48 | right: g(.25); 49 | top: g(.25); 50 | width: .5em; 51 | } 52 | } 53 | 54 | .tify-icon { 55 | filter: drop-shadow(0 0 2px $shade) drop-shadow(0 0 1px $shade-dark); 56 | } 57 | } 58 | 59 | .tify-scan-filters { 60 | position: relative; 61 | } 62 | 63 | .tify-scan-filters-popup { 64 | @include dropdown(left, right); 65 | left: g(1.75); 66 | padding: g(.5); 67 | top: g(.75); 68 | width: g(10); 69 | 70 | label { 71 | // "100 %" 72 | > b { 73 | float: right; 74 | font-size: $font-size-small; 75 | } 76 | } 77 | 78 | > p { 79 | margin: 0; 80 | 81 | + p { 82 | margin-top: g(.5); 83 | } 84 | } 85 | } 86 | 87 | // NOTE: OpenSeadragon adds `position: relative` 88 | .tify-scan-image { 89 | height: 100%; 90 | white-space: nowrap; 91 | width: 100%; 92 | 93 | .openseadragon-canvas { 94 | outline: 0; 95 | } 96 | } 97 | 98 | // Big exception using an ID for styling because that 99 | // is all we have here, thanks to OpenSeadragon 100 | // TODO: Revisit after next OpenSeadragon release 101 | [id^=overlay-wrapper-tify] { 102 | border-radius: $br; 103 | box-shadow: 0 0 0 1px $link-color, 0 0 0 1.5px $shine; 104 | cursor: pointer; 105 | 106 | @include hover { 107 | box-shadow: 0 0 0 2px $link-color, 0 0 0 2.5px $shine; 108 | } 109 | } 110 | 111 | .tify-scan-overlay { 112 | &.-current { 113 | border-radius: $br; 114 | outline: calc(g(.25) - 2px) solid $white; 115 | outline-offset: 2px; 116 | mix-blend-mode: difference; 117 | } 118 | } 119 | 120 | .tify-scan-page-button { 121 | @extend %button; 122 | backdrop-filter: $blur; 123 | background: $shine; 124 | border: 0; 125 | box-shadow: 0 0 1px $border-color; 126 | height: g(2.5); 127 | margin-top: g(-1.25); 128 | padding: 0; 129 | position: absolute; 130 | top: 50%; 131 | width: g(1.25); 132 | z-index: 1; 133 | 134 | .tify.-short & { 135 | bottom: 0; 136 | height: g(1.75); 137 | width: g(1.75); 138 | top: auto; 139 | } 140 | 141 | &.-previous { 142 | border-radius: 0 g(1.25) g(1.25) 0; 143 | left: 0; 144 | justify-content: flex-start; 145 | 146 | .tify.-short & { 147 | border-radius: 0 g(1.75) 0 0; 148 | padding: g(.375) 0 0 g(.25); 149 | } 150 | } 151 | 152 | &.-next { 153 | border-radius: g(1.25) 0 0 g(1.25); 154 | right: 0; 155 | justify-content: flex-end; 156 | 157 | .tify.-short & { 158 | border-radius: g(1.75) 0 0; 159 | padding: g(.375) g(.25) 0 0; 160 | } 161 | } 162 | } 163 | 164 | .tify-scan-range { 165 | @include range; 166 | } 167 | 168 | .tify-scan-reset { 169 | @extend %button; 170 | width: 100%; 171 | } 172 | -------------------------------------------------------------------------------- /src/styles/sections/sr-only.scss: -------------------------------------------------------------------------------- 1 | .tify-sr-only { 2 | border: 0; 3 | clip: rect(0, 0, 0, 0); 4 | height: 1px; 5 | margin: -1px; 6 | overflow: hidden; 7 | padding: 0; 8 | position: absolute; 9 | width: 1px; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/sections/thumbnails.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | .tify-thumbnails { 4 | @extend %panel; 5 | min-height: 100%; 6 | overflow-y: scroll; // NOTE: This is required for thumbnails to be calculated correctly 7 | position: relative; 8 | user-select: none; 9 | 10 | a { 11 | color: inherit; 12 | border: 0; 13 | box-shadow: none; 14 | } 15 | } 16 | 17 | .tify-thumbnails-list { 18 | margin: g(-.25); 19 | } 20 | 21 | .tify-thumbnails-item { 22 | background: currentColor; 23 | border-radius: $br; 24 | cursor: pointer; 25 | float: left; 26 | margin: g(.25); 27 | overflow: hidden; 28 | padding-bottom: g(); 29 | position: relative; 30 | 31 | @include hover { 32 | background: currentColor; 33 | filter: brightness(1.2); 34 | } 35 | 36 | img { 37 | display: block; 38 | height: $thumbnail-height; 39 | margin: auto; 40 | object-fit: contain; 41 | pointer-events: none; 42 | max-width: 100%; 43 | width: $thumbnail-width; 44 | } 45 | 46 | &.-current { 47 | outline: g(math.div(.5, 3)) solid $base-color-light; 48 | } 49 | } 50 | 51 | .tify-thumbnails-page { 52 | bottom: 0; 53 | box-shadow: 0 -1px $shine; 54 | color: $white; 55 | display: block; 56 | font-size: $font-size-small; 57 | font-weight: bold; 58 | height: g(); 59 | overflow: hidden; 60 | padding: 0 g(.25); 61 | position: absolute; 62 | text-align: center; 63 | text-overflow: ellipsis; 64 | white-space: nowrap; 65 | width: 100%; 66 | 67 | @at-root { 68 | .tify-thumbnails-item { 69 | 70 | &.-current { 71 | .tify-thumbnails-page { 72 | background: $link-color; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/styles/sections/toc.scss: -------------------------------------------------------------------------------- 1 | %toc-row-item { 2 | background: $white; 3 | position: relative; 4 | z-index: 1; 5 | } 6 | 7 | .tify-toc { 8 | @extend %panel; 9 | overflow-y: auto; 10 | position: relative; 11 | z-index: 0; 12 | } 13 | 14 | .tify-toc-header { 15 | margin: 0 g(.25) g(.5); 16 | } 17 | 18 | .tify-toc-label { 19 | @extend %toc-row-item; 20 | padding-right: .2em; 21 | transition: inherit; 22 | } 23 | 24 | .tify-toc-link { 25 | border-radius: $br; 26 | color: $link-color; 27 | cursor: pointer; 28 | display: block; 29 | overflow: hidden; 30 | padding: g(.25); 31 | position: relative; 32 | text-decoration: none; 33 | 34 | @include hover { 35 | background: $base-color-lighter; 36 | 37 | // Element label and page number 38 | > span { 39 | background: $base-color-lighter; 40 | } 41 | } 42 | 43 | &.-dots { 44 | // Dotted line below 45 | &::after { 46 | bottom: calc(#{g(.5)} - 1px); 47 | content: ''; 48 | left: g(.25); 49 | min-width: 4em; 50 | border-bottom: 1px dotted; 51 | position: absolute; 52 | right: g(.25); 53 | } 54 | } 55 | } 56 | 57 | .tify-toc-list { 58 | margin: 0 0 g(.25) g(.25); 59 | padding: 0; 60 | position: relative; 61 | z-index: 0; 62 | 63 | & & { 64 | // Make space for vertical connector to the left 65 | margin: 0 0 0 g(1.25); 66 | } 67 | 68 | a { 69 | border: 0; 70 | box-shadow: none; 71 | } 72 | } 73 | 74 | .tify-toc-page { 75 | @extend %toc-row-item; 76 | float: right; 77 | padding-left: .2em; 78 | transition: inherit; 79 | z-index: 1; 80 | } 81 | 82 | .tify-toc-structure { 83 | display: block; 84 | margin: 0; // For smooth embedding 85 | position: relative; 86 | 87 | &.-current { 88 | // Bold vertical marker 89 | box-shadow: calc(#{g(-.5)} + 1px) 0 $white, g(-.75) 0 $base-color-light; 90 | } 91 | 92 | &.-expanded { 93 | // Vertical connector to the left 94 | &::after { 95 | border-left: 1px solid $base-color-light; 96 | content: ''; 97 | left: g(.75); 98 | height: 100%; 99 | position: absolute; 100 | top: g(.25); 101 | z-index: -2; 102 | } 103 | } 104 | 105 | & & { 106 | // Horizontal connector 107 | &::before { 108 | border-top: 1px solid $base-color-light; 109 | content: ''; 110 | display: block; 111 | height: 100%; 112 | left: g(-.5); 113 | position: absolute; 114 | top: g(.75); 115 | width: g(.5); 116 | } 117 | 118 | // Prevent vertical line from protruding at the bottom 119 | &:not(.-current):last-child::before { 120 | background: $white; 121 | } 122 | } 123 | } 124 | 125 | .tify-toc-toggle-all { 126 | @extend %button-small; 127 | margin: g(.25); 128 | } 129 | 130 | .tify-toc-toggle { 131 | @extend %button; 132 | float: left; 133 | margin: g(.25) 0 0 g(.25); 134 | padding: 0; 135 | position: relative; 136 | 137 | // White overlay so vertical line does not touch button 138 | &::after { 139 | content: ''; 140 | border-bottom: g(.25) solid $white; 141 | bottom: g(-.25); 142 | left: g(.5); 143 | pointer-events: none; 144 | position: absolute; 145 | width: 1px; 146 | z-index: -1; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/styles/util/base.scss: -------------------------------------------------------------------------------- 1 | .tify { 2 | background: $main-bg; 3 | box-sizing: border-box; 4 | color: $text-color; 5 | display: flex; 6 | flex-direction: column; 7 | font-size: $font-size; 8 | height: 100%; 9 | line-height: $line-height; 10 | min-height: 240px; 11 | min-width: 240px; 12 | overflow: hidden; 13 | position: relative; 14 | -webkit-tap-highlight-color: $shade-light; 15 | 16 | *, 17 | *::before, 18 | *::after { 19 | box-sizing: inherit; 20 | } 21 | 22 | a { 23 | color: $link-color; 24 | word-wrap: break-word; 25 | 26 | @include hover { 27 | color: $link-hover-color; 28 | } 29 | 30 | &:focus-visible { 31 | outline: 2px solid $base-color-light; 32 | } 33 | } 34 | 35 | h3 { 36 | box-shadow: 0 1px $border-color; 37 | font-size: .75em; 38 | font-weight: bold; 39 | letter-spacing: .1em; 40 | margin: 0 0 g(.5); 41 | color: $text-color-muted; 42 | padding: 0; 43 | text-transform: uppercase; 44 | } 45 | 46 | h4 { 47 | font-size: 1em; 48 | font-weight: normal; 49 | margin: 0; 50 | color: $text-color-muted; 51 | padding: 0; 52 | 53 | &:nth-of-type(n+2) { 54 | margin-top: g(.5); 55 | } 56 | } 57 | 58 | label { 59 | cursor: pointer; 60 | font-size: inherit; 61 | font-weight: normal; 62 | } 63 | 64 | p { 65 | margin: 0 0 g(.5); 66 | padding: 0; 67 | } 68 | 69 | [type=text] { 70 | background: $white; 71 | border: 1px solid $shade; 72 | border-radius: $br; 73 | color: inherit; 74 | font: inherit; 75 | padding: calc(#{g(.25)} - 1px) .5em; 76 | 77 | &:focus { 78 | border-color: $base-color; 79 | outline: 2px solid $base-color-lighter; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/styles/util/settings.scss: -------------------------------------------------------------------------------- 1 | // Opaque shades 2 | $grey: #666 !default; 3 | $grey-dark: #333 !default; 4 | $grey-light: #f5f5f5 !default; 5 | $white: #fff !default; 6 | 7 | // Semitransparent shades 8 | $shade: rgba(black, .2) !default; 9 | $shade-dark: rgba(black, .5) !default; 10 | $shade-darker: rgba(black, .8) !default; 11 | $shade-light: rgba(black, .1) !default; 12 | $shine: rgba($white, .4) !default; 13 | 14 | // Semantic colors 15 | // NOTE: These colors must be solid 16 | $base-color: var(--tify-base-color, #06b) !default; 17 | 18 | $base-color-light: var(--tify-base-color-light, mix(white, #06b, 45%)) !default; 19 | $base-color-lighter: var(--tify-base-color-lighter, mix(white, #06b, 90%)) !default; 20 | $border-color: var(--tify-border-color, $shade) !default; 21 | $link-color: var(--tify-link-color, $base-color) !default; 22 | $link-hover-color: var(--tify-link-hover-color, $link-color) !default; 23 | $text-color: var(--tify-text-color, $grey-dark) !default; 24 | $text-color-muted: var(--tify-text-muted-color, $grey) !default; 25 | 26 | // Dimensions 27 | $br: var(--tify-border-radius, 2px) !default; 28 | $font-size: var(--tify-font-size, 16px) !default; 29 | $font-size-small: var(--tify-font-size-small, calc(#{$font-size} * .8125)) !default; 30 | $grid-base: var(--tify-grid-base, 24px) !default; 31 | $line-height: var(--tify-line-height, #{g()}) !default; 32 | $thumbnail-height: var(--tify-thumbnail-height, g(4.5)) !default; 33 | $thumbnail-width: var(--tify-thumbnail-width, g(4)) !default; 34 | 35 | // Combined values 36 | $blur: blur(2px); 37 | $drop-shadow: 0 0 g(.25) rgba(black, .5) !default; 38 | $header-bg: var(--tify-header-bg, $grey-light) !default; 39 | $inset-shadow: 0 .5px 3px $shade inset !default; 40 | $main-bg: var(--tify-body-bg, $grey url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAEElEQVQImWNgwAGkcElgAgAEogAbFZcwKQAAAABJRU5ErkJggg==)) !default; // dots pattern 41 | -------------------------------------------------------------------------------- /tests/e2e/api.spec.js: -------------------------------------------------------------------------------- 1 | // TODO: Extend API tests 2 | 3 | describe('API', () => { 4 | it('controls TIFY programmatically', () => { 5 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303`); 6 | 7 | // NOTE: Cypress’ afterEach hook (used for HTML validation) breaks tests 8 | // within nested promises for unknown reasons, so we use a small hack to 9 | // exclude it from API tests below. 10 | cy.bypassAfterEach = true; 11 | 12 | cy.window().its('tify').then((tify) => { 13 | tify.ready.then(() => { 14 | tify.viewer.viewport.zoomTo(2); 15 | cy.get('[title="Zoom in"]').should('be.disabled'); 16 | 17 | tify.setLanguage('de'); 18 | cy.contains('Seiten'); 19 | 20 | tify.setPage(2); 21 | cy.contains('.tify-page-select-button', '2 · -'); 22 | 23 | tify.setView('export'); 24 | cy.contains('.-active', 'Export'); 25 | 26 | tify.setLanguage('de'); 27 | cy.contains('.tify-header-button', 'Seiten'); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/e2e/collection.spec.js: -------------------------------------------------------------------------------- 1 | describe('Collection', () => { 2 | it('shows the collection view', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760`); 4 | 5 | // Only Info and Collection buttons should be visible 6 | cy.contains('Info'); 7 | cy.contains('Collection'); 8 | cy.should('not.contain', 'Fulltext'); 9 | cy.should('not.contain', 'Scan'); 10 | cy.should('not.contain', 'Contents'); 11 | cy.should('not.contain', 'Export'); 12 | 13 | cy.contains('Volume 1, 1859').click(); 14 | cy.contains('15. September 1859').click(); 15 | cy.contains('The chemist and druggist, 15. September 1859'); 16 | }); 17 | 18 | it('filters the collection list', () => { 19 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760`); 20 | 21 | cy.get('[aria-label="Filter collection"]').type('2008'); 22 | cy.get('.tify-collection-item').should('have.length', 2); 23 | cy.get('[aria-label="Filter collection"]').type('nope'); 24 | cy.contains('No results'); 25 | }); 26 | 27 | it('loads a child manifest', () => { 28 | const encodedParams = encodeURIComponent(JSON.stringify({ 29 | childManifestUrl: `${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760_1_0004`, 30 | })); 31 | 32 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760&tify=${encodedParams}`); 33 | 34 | cy.contains('Info').click(); 35 | cy.contains('.tify-info-button', 'Collection').click(); 36 | cy.contains('h1', 'The chemist and druggist, 15. September 1859'); 37 | 38 | cy.contains('.tify-header-button', 'Collection').click(); 39 | cy.contains('Volume 1').click(); 40 | cy.contains('15. October 1859').click(); 41 | cy.contains('h1', 'The chemist and druggist, 15. October 1859'); 42 | }); 43 | 44 | it('highlights the current child manifest', () => { 45 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760`); 46 | 47 | cy.contains('Volume 1').click(); 48 | cy.contains('15. September 1859').click(); 49 | cy.contains('.tify-collection-item.-current', '15. September 1859'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/e2e/content-state.spec.js: -------------------------------------------------------------------------------- 1 | describe('IIIF Content State', () => { 2 | it('supports setting the manifest URL via URL query "iiif-content"', () => { 3 | cy.visit(`/?iiif-content=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303`); 4 | 5 | cy.get('.header').should('not.exist'); 6 | cy.contains('De Supputatione Multitudinis'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/e2e/export.spec.js: -------------------------------------------------------------------------------- 1 | describe('Export', () => { 2 | it('displays export links', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303`); 4 | 5 | cy.contains('Export').click(); 6 | cy.contains('Download Individual Images').should('be.visible'); 7 | cy.contains('Page 1').should('be.visible'); // NOTE: Page set by startCanvas 8 | 9 | cy.get('[title="Next page"]').first().click(); 10 | cy.contains('Page 2').should('be.visible'); 11 | 12 | cy.contains('PDFs for each element').click(); 13 | cy.contains('Titelseite').should('be.visible'); 14 | 15 | cy.contains('a', 'IIIF manifest'); 16 | 17 | cy.get('a[href$="/download/pdf/PPN857449303/LOG_0001.pdf"]'); 18 | 19 | cy.contains('Other Formats').next().find('a').should('have.length', 4); 20 | }); 21 | 22 | it('hides "Other Formats" if not available', () => { 23 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/aku-pal-375`); 24 | 25 | cy.contains('Export').click(); 26 | 27 | cy.should('not.contain', 'Other Formats'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/fulltext.spec.js: -------------------------------------------------------------------------------- 1 | describe('Fulltext', () => { 2 | it('displays fulltext and annotation overlays at the correct positions', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b18035723&tify={"pages":[14,15,16]}`); 4 | 5 | cy.contains('Fulltext').click(); 6 | cy.contains('Alles höhere Leben - ob Tier oder').should('be.visible'); 7 | 8 | cy.get('.tify-scan-overlay').should('have.length', 102); 9 | 10 | // Check the first annotation overlay of each page 11 | cy.get('[style*="left: 18.8309px; top: 282.988px"]') 12 | .children('.tify-scan-overlay[style*="width: 113.797px; height: 5.0324px"]'); 13 | cy.get('[style*="left: 414.136px; top: 282.988px"]') 14 | .children('.tify-scan-overlay[style*="width: 113.797px; height: 5.0324px"]'); 15 | cy.get('[style*="left: 809.44px; top: 282.988px"]') 16 | .children('.tify-scan-overlay[style*="width: 113.797px; height: 5.0324px"]'); 17 | }); 18 | 19 | it('loads and displays an annotation list', () => { 20 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/harvard-art-museum-299843`); 21 | 22 | cy.get('[title="Toggle annotations"]').should('not.exist'); 23 | 24 | cy.contains('Fulltext').click(); 25 | 26 | cy.get('[title="Toggle annotations"]').click(); 27 | cy.get('.tify-scan-overlay').should('not.exist'); 28 | cy.get('[title="Toggle annotations"]').click(); 29 | cy.get('.tify-scan-overlay').should('have.length', 5); 30 | 31 | cy.contains('.tify-fulltext-toggle', 'Painting'); 32 | cy.contains('.tify-fulltext-toggle', 'Person'); 33 | }); 34 | 35 | it('displays XML fulltext', () => { 36 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN235181684_0029&tify={"view":"fulltext"}`); 37 | 38 | cy.contains('Unter Mitwirkung der Herren').should('be.visible'); 39 | }); 40 | 41 | it('highlights the corresponding fulltext section when an overlay is clicked and vice versa', () => { 42 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b18035723&tify={"view":"fulltext"}`); 43 | 44 | cy.get('.tify-scan-overlay:eq(22)').click(); 45 | cy.get('.tify-scan-overlay:eq(22)').should('have.class', '-current'); 46 | cy.contains('.tify-fulltext-item.-current', 'näher kommen'); 47 | }); 48 | 49 | // TODO: Split this up, one test per recipe 50 | it('correctly displays manifests from the IIIF cookbook', () => { 51 | cy.visit( 52 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/cookbook-recipe-0019-html-in-annotations` 53 | + '&tify={"view":"fulltext"}', 54 | ); 55 | cy.contains('.tify-fulltext-toggle', 'Gänseliesel Brunnen'); 56 | 57 | cy.visit( 58 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/cookbook-recipe-0021-tagging` 59 | + '&tify={"view":"fulltext"}', 60 | ); 61 | cy.contains('Gänseliesel-Brunnen').click(); 62 | cy.get('.tify-scan-overlay').should('have.length', 1).should('have.class', '-current'); 63 | 64 | cy.visit( 65 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/cookbook-recipe-0266-full-canvas-annotation` 66 | + '&tify={"view":"fulltext"}', 67 | ); 68 | cy.get('.tify-scan-overlay').should('not.exist'); 69 | }); 70 | 71 | it('displays images in annotations', () => { 72 | cy.visit( 73 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/cookbook-recipe-0377-image-in-annotation` 74 | + '&tify={"view":"fulltext"}', 75 | ); 76 | 77 | cy.get('.tify-fulltext img[src$="918ecd18c2592080851777620de9bcb5-fountain/full/300,/0/default.jpg"]'); 78 | cy.contains('.tify-fulltext', 'Night picture of the Gänseliesel fountain'); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/e2e/iiif3.spec.js: -------------------------------------------------------------------------------- 1 | describe('Support for native IIIF 3 manifests', () => { 2 | it('displays correct metadata', () => { 3 | cy.visit( 4 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/aku-pal-375` 5 | + '&tify={"view":"info"}', 6 | ); 7 | 8 | cy.contains('h1', 'Dipinto Assiut, Gebel Assiut al-gharbi, N13.1 TN2'); 9 | cy.contains('a', 'AKU-PAL database record'); 10 | }); 11 | 12 | it('displays the TOC', () => { 13 | cy.visit( 14 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/amherst-63cc6105-570d-407b-af8c-07fda3f8c620.json` 15 | + '&tify={"view":"toc"}', 16 | ); 17 | 18 | cy.get('.tify-toc-structure.-current:not(.-expanded)'); 19 | cy.contains('.tify-toc-link', 'Transcription of Emily Dickinson\'s "I\'ll clutch - and clutch" - Image 3'); 20 | cy.get('.tify-toc-link').should('have.length', 1); 21 | }); 22 | 23 | it('displays attribution HTML', () => { 24 | cy.visit( 25 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/mskgent-8210.json` 26 | + '&tify={"view":"info"}', 27 | ); 28 | 29 | cy.get('.tify-info-section.-attribution').find('img'); // logo in attribution section 30 | }); 31 | 32 | it('displays a dash for metadata with an empty value', () => { 33 | cy.visit( 34 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/utrecht-1874-325480` 35 | + '&tify={"view":"info"}', 36 | ); 37 | 38 | cy.contains('h4', 'Published').next().should('have.text', '—'); // — 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/e2e/info.spec.js: -------------------------------------------------------------------------------- 1 | describe('Info', () => { 2 | it('displays related metadata', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b18035723`); 4 | cy.contains('Info').click(); 5 | cy.contains('Related Resources'); 6 | cy.contains('Wunder der Vererbung / von Fritz Bolle.'); 7 | 8 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/ubl-0000000001`); 9 | cy.contains('Info').click(); 10 | cy.contains('Related Resources'); 11 | cy.contains('/object/viewid/0000000001'); 12 | cy.contains('/0000000001/manifest'); 13 | 14 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/digitale-sammlungen-bsb00026283`); 15 | cy.contains('Info').click(); 16 | cy.get('.tify-info-section.-related li').should('have.length', 2); 17 | cy.get('a[href$="/details:bsb00026283"]').contains('Details'); 18 | cy.get('a[href$="/title/BV023398264"]').contains('OPAC'); 19 | }); 20 | 21 | it('collapses long metadata values', () => { 22 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081`); 23 | cy.contains('button', 'Expand'); 24 | }); 25 | 26 | it('shows metadata of the current structure', () => { 27 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303`); 28 | cy.contains('Info').click(); 29 | cy.contains('Current Element').should('be.visible'); 30 | cy.contains('Titelseite'); 31 | 32 | cy.get('[title="Last page"]').first().click(); 33 | cy.contains('Current Element').should('not.exist'); 34 | }); 35 | 36 | it('shows metadata of a nested structure', () => { 37 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-DE_611_BF_5619_1801_1806`); 38 | cy.contains('Info').click(); 39 | Cypress._.times(4, () => cy.get('[title="Next page"]').first().click()); 40 | cy.contains('Current Element').should('be.visible'); 41 | cy.contains( 42 | '.tify-info-section.-metadata.-structure', 43 | '[Brief des Barons von Asch an Heyne vom 29.01./10.02.1801]', 44 | ); 45 | }); 46 | 47 | it('displays all provider information', () => { 48 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b24738918`); 49 | cy.contains('Info').click(); 50 | 51 | cy.fixture('../../iiif-api/data/manifests/wellcome-b24738918.json').then((manifest) => { 52 | const provider = manifest.provider[0]; 53 | const providerStringWithoutUrl = provider.label.en.slice(0, -1).join(''); 54 | cy.get('.tify-info-section.-provider').should('contain.text', providerStringWithoutUrl); 55 | cy.contains('.tify-info-section.-provider a', provider.homepage[0].label.en.join('')); 56 | }); 57 | }); 58 | 59 | it('only displays a related homepage once for converted IIIF 2 manifests', () => { 60 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN140716181`); 61 | cy.contains('Info').click(); 62 | cy.get('.tify-info-section.-related a[href$="/DB=1/PPN?PPN=140716181"]') 63 | .contains('OPAC'); 64 | cy.get('.tify-info-section.-provider').contains('OPAC').should('not.exist'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/e2e/main.spec.js: -------------------------------------------------------------------------------- 1 | describe('Main', () => { 2 | it('starts the app', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303`); 4 | cy.get('.tify'); 5 | }); 6 | 7 | it('checks the manifest (valid JSON, but not IIIF)', () => { 8 | // Prevent the test from failing due to an uncaught exception (which is expected) 9 | cy.on('uncaught:exception', () => false); 10 | 11 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/invalid`); 12 | cy.contains('Please provide a valid IIIF Presentation API manifest'); 13 | }); 14 | 15 | it('checks the manifest (invalid JSON)', () => { 16 | // Prevent the test from failing due to an uncaught exception (which is expected) 17 | cy.on('uncaught:exception', () => false); 18 | 19 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/not-json`); 20 | cy.contains('Error loading IIIF manifest'); 21 | }); 22 | 23 | it('loads a translation', () => { 24 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303&language=de`); 25 | 26 | cy.get('.tify-header'); 27 | cy.contains('Seiten'); 28 | cy.contains('Inhalt'); 29 | }); 30 | 31 | it('gracefully handles a missing translation', () => { 32 | // Prevent the test from failing due to an uncaught exception (which is expected) 33 | cy.on('uncaught:exception', () => false); 34 | 35 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303&language=nope`); 36 | 37 | cy.get('.tify-header'); 38 | cy.contains('Pages'); 39 | cy.contains('Contents'); 40 | cy.contains('Error loading translation for "nope"'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/e2e/multi-instance.spec.js: -------------------------------------------------------------------------------- 1 | describe('Multi-instance', () => { 2 | it('starts multiple independent apps with different manifests', () => { 3 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN857449303` 4 | + `&manifest2=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081`); 5 | 6 | cy.contains('De Supputatione Multitudinis'); 7 | cy.contains('Algebra : Vorlesungsmanuskript'); 8 | 9 | cy.get('[title="Next page"]').eq(0).click(); 10 | cy.get('.tify-page-select-button').eq(0).contains('2 · -'); 11 | cy.get('.tify-page-select-button').eq(1).contains('1 · -'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/e2e/pagination.spec.js: -------------------------------------------------------------------------------- 1 | describe('Pagination', () => { 2 | const currentPage = '.tify-page-select-button'; 3 | 4 | it('changes the page via buttons', () => { 5 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081&tify={"pages":[15]}`); 6 | 7 | cy.contains(currentPage, '15 · 7r'); 8 | 9 | cy.get('[title="First page"]').first().click(); 10 | cy.contains(currentPage, '1 · -'); 11 | 12 | Cypress._.times(2, () => cy.get('[title="Next page"]').first().click()); 13 | cy.contains(currentPage, '3 · 1r'); 14 | 15 | Cypress._.times(2, () => cy.get('[title="Next section"]').first().click()); 16 | cy.contains(currentPage, '7 · 3r'); 17 | 18 | cy.get('[title="Last page"]').first().click(); 19 | cy.contains(currentPage, '69 · -'); 20 | 21 | Cypress._.times(4, () => cy.get('[title="Previous section"]').first().click()); 22 | cy.contains('16 · 7v'); 23 | 24 | cy.get('[title="Toggle double-page"]').first().click(); 25 | cy.get('[title="Toggle double-page"].-active'); 26 | }); 27 | 28 | it('changes the page via keyboard', () => { 29 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081&tify={"pages":[15]}`); 30 | 31 | cy.contains(currentPage, '15 · 7r'); 32 | 33 | cy.get('.tify').type('q'); 34 | cy.contains(currentPage, '14 · 6v'); 35 | cy.get('.tify').type('e'); 36 | cy.contains(currentPage, '15 · 7r'); 37 | 38 | cy.get('.tify').type('b'); 39 | cy.contains(currentPage, '14 · 6v'); 40 | cy.get('[title="Toggle double-page"].-active'); 41 | 42 | cy.get('.tify').type('q'); 43 | cy.contains(currentPage, '12 · 5v'); 44 | cy.get('.tify').type(','); 45 | cy.contains(currentPage, '10 · 4v'); 46 | 47 | cy.get('.tify').type('e'); 48 | cy.contains(currentPage, '12 · 5v'); 49 | cy.get('.tify').type('.'); 50 | cy.contains(currentPage, '14 · 6v'); 51 | 52 | cy.get('.tify').type('Q'); 53 | cy.contains(currentPage, '1 · -'); 54 | 55 | cy.get('.tify').type('E'); 56 | cy.contains(currentPage, '68 · -'); 57 | 58 | cy.get('.tify').type('b'); 59 | cy.contains(currentPage, '68 · -'); 60 | cy.get('[title="Toggle double-page"]:not(.-active)'); 61 | 62 | cy.get('.tify').type('Q'); 63 | cy.contains(currentPage, '1 · -'); 64 | 65 | cy.get('.tify').type('E'); 66 | cy.contains(currentPage, '69 · -'); 67 | }); 68 | 69 | it('highlights the current page after a page change', () => { 70 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081`); 71 | cy.get('[title="Last page"]').first().click(); 72 | cy.contains('Current page: 69 · -').first().click(); 73 | cy.contains('.-current.-highlighted', '69 · -'); 74 | }); 75 | 76 | it('changes the page on small touchscreens', () => { 77 | cy.viewport(375, 667); 78 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081`); 79 | 80 | cy.get('[title="View"]').click(); 81 | cy.contains('Pages').should('be.visible'); 82 | cy.get('[title="View"]').click(); 83 | cy.contains('Pages').should('not.be.visible'); 84 | 85 | cy.get('[title="View"]').click(); 86 | cy.get('.tify-header-popup [title="Last page"]').click(); 87 | cy.get('.tify-header-popup [title="Next page"]').should('be.disabled'); 88 | cy.get('.tify-header-popup [title="Next section"]').should('be.disabled'); 89 | cy.get('.tify-header-popup [title="Last page"]').should('be.disabled'); 90 | 91 | cy.get('.tify-header-popup [title="Previous section"]').click(); 92 | cy.contains(currentPage, '67 · -'); 93 | cy.get('.tify-header').click(); 94 | cy.get('.tify-header-popup').should('not.be.visible'); 95 | }); 96 | 97 | it('hides section buttons if there are less than 2 sections', () => { 98 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-PPN140716181.json`); 99 | 100 | cy.contains('Von Gottes Gnaden'); 101 | cy.contains('Previous section').should('not.exist'); 102 | cy.contains('Next section').should('not.exist'); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /tests/e2e/scan.spec.js: -------------------------------------------------------------------------------- 1 | describe('Scan', () => { 2 | it('uses image filters', () => { 3 | const encodedParams = encodeURIComponent(JSON.stringify({ 4 | filters: { 5 | saturate: 0, 6 | }, 7 | })); 8 | 9 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081&tify=${encodedParams}`); 10 | cy.get('[title="Toggle image filters"]').click(); 11 | cy.get('.tify-scan-filters-popup').contains('Saturation 0'); 12 | }); 13 | 14 | it('resets pan, zoom, rotation and filters at once', () => { 15 | const encodedParams = encodeURIComponent(JSON.stringify({ 16 | filters: { 17 | brightness: 1.1, 18 | contrast: 0.9, 19 | saturate: 1.1, 20 | }, 21 | pan: { 22 | x: 0.5, 23 | y: 0.5, 24 | }, 25 | rotation: 90, 26 | zoom: 2, 27 | })); 28 | 29 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081&tify=${encodedParams}`); 30 | 31 | cy.get('[title="Rotate"].-active'); 32 | cy.get('[title="Toggle image filters"].-active'); 33 | 34 | cy.get('.tify').type('{shift}0'); 35 | cy.url().should( 36 | 'include', 37 | `/?manifest=${encodeURIComponent(`${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081`)}`, 38 | ); 39 | }); 40 | 41 | it('controls the scan via keyboard', () => { 42 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/gdz-HANS_DE_7_w042081`); 43 | 44 | // Zoom 45 | cy.contains('Reset').should('be.disabled'); 46 | cy.get('.tify').type('+'); 47 | cy.get('[title="Reset"]').should('not.be.disabled'); 48 | cy.get('.tify').type('+=W'); 49 | cy.get('[title="Zoom in"]').should('be.disabled'); 50 | cy.get('.tify').type('-'); 51 | cy.get('[title="Zoom in"]').should('not.be.disabled'); 52 | cy.get('.tify').type('-_S'); 53 | cy.get('[title="Zoom out"]').should('be.disabled'); 54 | 55 | // Pan 56 | cy.get('.tify').type('0', { delay: 500 }); 57 | cy.get('[title="Reset"]').should('be.disabled'); 58 | cy.get('.tify').type('wd'); 59 | cy.get('[title="Reset"]').should('not.be.disabled'); 60 | cy.get('.tify').type('sa'); 61 | cy.get('[title="Reset"]').should('be.disabled'); 62 | 63 | // Rotate 64 | cy.get('.tify').type('r'); 65 | cy.get('[title="Rotate"].-active').should('be.visible'); 66 | cy.get('.tify').type('r'); 67 | cy.get('.tify').type('r'); 68 | cy.get('.tify').type('r'); 69 | cy.get('[title="Rotate"]:not(.-active)').should('be.visible'); 70 | 71 | // Filter 72 | cy.get('.tify').type('i'); 73 | cy.contains('Brightness').should('be.visible') 74 | .type('i'); 75 | cy.contains('Brightness').should('not.be.visible'); 76 | cy.get('.tify').type('i'); 77 | cy.contains('Brightness').should('be.visible') 78 | .type('{esc}'); 79 | cy.contains('Brightness').should('not.be.visible'); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/e2e/support/e2e.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import 'cypress-html-validate/commands'; 3 | 4 | afterEach(() => { 5 | if (cy.bypassAfterEach) { 6 | return; 7 | } 8 | 9 | cy.htmlvalidate( 10 | { 11 | rules: { 12 | 'heading-level': [ 13 | 'error', 14 | { 15 | allowMultipleH1: true, 16 | }, 17 | ], 18 | 'prefer-native-element': 'off', 19 | 'require-sri': 'off', 20 | }, 21 | }, 22 | { 23 | exclude: [ 24 | // Attribution may contain invalid HTML if the manifest provides such. 25 | '.tify-info-section.-attribution', 26 | ], 27 | }, 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/thumbnails.spec.js: -------------------------------------------------------------------------------- 1 | it('handles a missing thumbnail', () => { 2 | cy.visit( 3 | `/?manifest=${Cypress.env('iiifApiUrl')}/manifest/cookbook-recipe-0283-missing-image` 4 | + '&tify={"view":"thumbnails"}', 5 | ); 6 | 7 | cy.get('.tify-thumbnails-item').should('have.length', 4); 8 | cy.get('.tify-thumbnails-item:eq(1) img[src="data:,"]'); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/e2e/views.spec.js: -------------------------------------------------------------------------------- 1 | describe('Views', () => { 2 | it('changes the view via buttons', () => { 3 | const encodedParams = encodeURIComponent(JSON.stringify({ 4 | childManifestUrl: `${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760_1_0004`, 5 | })); 6 | 7 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760&tify=${encodedParams}`); 8 | 9 | cy.contains('Fulltext').click(); 10 | cy.get('.-active').contains('Fulltext'); 11 | 12 | cy.contains('Pages').click(); 13 | cy.get('.-active').contains('Pages'); 14 | 15 | cy.contains('Contents').click(); 16 | cy.get('.-active').contains('Contents'); 17 | 18 | cy.contains('Info').click(); 19 | cy.get('.-active').contains('Info'); 20 | 21 | cy.contains('Export').click(); 22 | cy.get('.-active').contains('Export'); 23 | 24 | cy.contains('Collection').click(); 25 | cy.get('.-active').contains('Collection'); 26 | 27 | cy.contains('Help').click(); 28 | cy.get('.-active').contains('Help'); 29 | }); 30 | 31 | it('changes the view via keyboard', () => { 32 | const encodedParams = encodeURIComponent(JSON.stringify({ 33 | childManifestUrl: `${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760_1_0004`, 34 | })); 35 | 36 | cy.visit(`/?manifest=${Cypress.env('iiifApiUrl')}/manifest/wellcome-b19974760&tify=${encodedParams}`); 37 | 38 | cy.contains('The chemist and druggist'); 39 | 40 | cy.get('.tify').type('1'); 41 | cy.get('.-active').contains('Fulltext'); 42 | 43 | cy.get('.tify').type('2'); 44 | cy.get('.-active').contains('Pages'); 45 | 46 | cy.get('.tify').type('3'); 47 | cy.get('.-active').contains('Contents'); 48 | 49 | cy.get('.tify').type('4'); 50 | cy.get('.-active').contains('Info'); 51 | 52 | cy.get('.tify').type('5'); 53 | cy.get('.-active').contains('Export'); 54 | 55 | cy.get('.tify').type('6'); 56 | cy.get('.-active').contains('Collection'); 57 | 58 | cy.get('.tify').type('7'); 59 | cy.get('.-active').contains('Help'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/iiif-api/data/annotation-lists/47174896.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://www.shared-canvas.org/ns/context.json", 3 | "@id": "https://iiif.harvardartmuseums.org/manifests/object/299843/list/47174896", 4 | "@type": "sc:AnnotationList", 5 | "resources": [ 6 | { 7 | "@context": "http://iiif.io/api/presentation/2/context.json", 8 | "@id": "https://iiif.harvardartmuseums.org/annotations/9641482", 9 | "@type": "oa:Annotation", 10 | "motivation": [ 11 | "oa:commenting" 12 | ], 13 | "on": { 14 | "@type": "oa:SpecificResource", 15 | "full": "https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896", 16 | "selector": { 17 | "@type": "oa:FragmentSelector", 18 | "value": "xywh=622,591,642,940" 19 | }, 20 | "within": { 21 | "@id": "https://iiif.harvardartmuseums.org/manifests/object/299843", 22 | "@type": "sc:Manifest" 23 | } 24 | }, 25 | "resource": [ 26 | { 27 | "@type": "dctypes:Text", 28 | "chars": "

age: 35-52
gender: Female(66.337677%)
CALM: 55.438412%
CONFUSED: 3.949288%
SURPRISED: 2.33092%
DISGUSTED: 0.545727%
HAPPY: 1.549943%
ANGRY: 2.082294%
SAD: 34.103416%

Generated by AWS Rekognition

", 29 | "format": "text/html" 30 | } 31 | ] 32 | }, 33 | { 34 | "@context": "http://iiif.io/api/presentation/2/context.json", 35 | "@id": "https://iiif.harvardartmuseums.org/annotations/9641484", 36 | "@type": "oa:Annotation", 37 | "motivation": [ 38 | "oa:commenting" 39 | ], 40 | "on": { 41 | "@type": "oa:SpecificResource", 42 | "full": "https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896", 43 | "selector": { 44 | "@type": "oa:FragmentSelector", 45 | "value": "xywh=19,91,2035,2413" 46 | }, 47 | "within": { 48 | "@id": "https://iiif.harvardartmuseums.org/manifests/object/299843", 49 | "@type": "sc:Manifest" 50 | } 51 | }, 52 | "resource": [ 53 | { 54 | "@type": "dctypes:Text", 55 | "chars": "Painting

Generated by AWS Rekognition

", 56 | "format": "text/html" 57 | } 58 | ] 59 | }, 60 | { 61 | "@context": "http://iiif.io/api/presentation/2/context.json", 62 | "@id": "https://iiif.harvardartmuseums.org/annotations/9641485", 63 | "@type": "oa:Annotation", 64 | "motivation": [ 65 | "oa:commenting" 66 | ], 67 | "on": { 68 | "@type": "oa:SpecificResource", 69 | "full": "https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896", 70 | "selector": { 71 | "@type": "oa:FragmentSelector", 72 | "value": "xywh=51,338,1784,2172" 73 | }, 74 | "within": { 75 | "@id": "https://iiif.harvardartmuseums.org/manifests/object/299843", 76 | "@type": "sc:Manifest" 77 | } 78 | }, 79 | "resource": [ 80 | { 81 | "@type": "dctypes:Text", 82 | "chars": "Person

Generated by AWS Rekognition

", 83 | "format": "text/html" 84 | } 85 | ] 86 | }, 87 | { 88 | "@context": "http://iiif.io/api/presentation/2/context.json", 89 | "@id": "https://iiif.harvardartmuseums.org/annotations/76409", 90 | "@type": "oa:Annotation", 91 | "motivation": [ 92 | "oa:commenting" 93 | ], 94 | "on": { 95 | "@type": "oa:SpecificResource", 96 | "full": "https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896", 97 | "selector": { 98 | "@type": "oa:FragmentSelector", 99 | "value": "xywh=451,421,1018,1187" 100 | }, 101 | "within": { 102 | "@id": "https://iiif.harvardartmuseums.org/manifests/object/299843", 103 | "@type": "sc:Manifest" 104 | } 105 | }, 106 | "resource": [ 107 | { 108 | "@type": "dctypes:Text", 109 | "chars": "

surprise: VERY_UNLIKELY
anger: VERY_UNLIKELY
sorrow: VERY_UNLIKELY
joy: VERY_UNLIKELY
headwear: VERY_UNLIKELY
blurred: VERY_UNLIKELY

Generated by Google Vision

", 110 | "format": "text/html" 111 | } 112 | ] 113 | }, 114 | { 115 | "@context": "http://iiif.io/api/presentation/2/context.json", 116 | "@id": "https://iiif.harvardartmuseums.org/annotations/76410", 117 | "@type": "oa:Annotation", 118 | "motivation": [ 119 | "oa:commenting" 120 | ], 121 | "on": { 122 | "@type": "oa:SpecificResource", 123 | "full": "https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174896", 124 | "selector": { 125 | "@type": "oa:FragmentSelector", 126 | "value": "xywh=726,753,696,696" 127 | }, 128 | "within": { 129 | "@id": "https://iiif.harvardartmuseums.org/manifests/object/299843", 130 | "@type": "sc:Manifest" 131 | } 132 | }, 133 | "resource": [ 134 | { 135 | "@type": "dctypes:Text", 136 | "chars": "

age: 58
gender: Male

Generated by Microsoft Cognitive Services

", 137 | "format": "text/html" 138 | } 139 | ] 140 | } 141 | ] 142 | } 143 | -------------------------------------------------------------------------------- /tests/iiif-api/data/annotation-lists/gdz-PPN235181684_0029-00000001.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/2/context.json", 3 | "@id": "https://manifests.sub.uni-goettingen.de/iiif/presentation/PPN235181684_0029/list/gdz:PPN235181684_0029:00000001", 4 | "@type": "sc:AnnotationList", 5 | "resources": [ 6 | { 7 | "@context": "http://iiif.io/api/presentation/2/context.json", 8 | "@type": "oa:Annotation", 9 | "motivation": "sc:painting", 10 | "resource": { 11 | "@id": "https://gdz.sub.uni-goettingen.de/fulltext/PPN235181684_0029/00000001.xml", 12 | "@type": "dctypes:Text", 13 | "format": "text/html" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/iiif-api/data/annotations/gdz-PPN235181684_0029-00000001.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | MATHEMATISCHE ANNALEN.

5 |

6 | IN VERBINDUNG MIT С NEUMANN

7 |

8 | BEGRÜNDET DURCH

9 |

10 | EUDOLF FEIEDEIOH ALFEED CLEBSCH.

11 |

12 | Unter Mitwirkung der Herren

13 |

14 | Prof 15 | , P. Gordan zu Erlangen, Prof. С Neumann zu Leipzig, Prof. K. VonderMühll zu Leipzig

16 |

17 | gegenwärtig herausgegeben von

18 |

19 | Prof 20 | . Felix Klein und Prof. Adolph Mayer

21 |

22 | zu Gbttingen. » ÏU Leipzig.

23 |

24 | XXIX 25 | . Band. 1. Heft. Mit 3 Figurentafeln.

26 |

27 | LEIPZIG 28 | ,

29 |

30 | DRUCK UND VEKLAG VON B. G. TEÜBNER.

31 |

32 | 1887 33 | .

34 | 35 |
36 | -------------------------------------------------------------------------------- /tests/iiif-api/data/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tify-iiif-viewer/tify/e3d9404c9d5c95bf7d5b160939c3fd15fa7b5983/tests/iiif-api/data/images/default.jpg -------------------------------------------------------------------------------- /tests/iiif-api/data/infos/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/2/context.json", 3 | "@id": "http://localhost:8081/image", 4 | "width": 1732, 5 | "height": 2236, 6 | "protocol": "http://iiif.io/api/image", 7 | "sizes": [ 8 | { 9 | "height": 140, 10 | "width": 109 11 | }, 12 | { 13 | "height": 280, 14 | "width": 217 15 | }, 16 | { 17 | "height": 559, 18 | "width": 433 19 | }, 20 | { 21 | "height": 1118, 22 | "width": 866 23 | }, 24 | { 25 | "height": 2236, 26 | "width": 1732 27 | } 28 | ], 29 | "tiles": [ 30 | { 31 | "width": 1024, 32 | "height": 1024, 33 | "scaleFactors": [ 34 | 1, 35 | 2, 36 | 4, 37 | 8, 38 | 16 39 | ] 40 | } 41 | ], 42 | "profile": "http://iiif.io/api/image/2/level1" 43 | } 44 | -------------------------------------------------------------------------------- /tests/iiif-api/data/infos/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "extraFormats": [ 4 | "jpg", 5 | "png" 6 | ], 7 | "extraQualities": [ 8 | "default", 9 | "color", 10 | "gray" 11 | ], 12 | "height": 3024, 13 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 14 | "profile": "level1", 15 | "protocol": "http://iiif.io/api/image", 16 | "tiles": [ 17 | { 18 | "height": 512, 19 | "scaleFactors": [ 20 | 1, 21 | 2, 22 | 4 23 | ], 24 | "width": 512 25 | } 26 | ], 27 | "type": "ImageService3", 28 | "width": 4032 29 | } 30 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/aku-pal-375.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "homepage": [ 4 | { 5 | "format": "text/html", 6 | "id": "https://aku-pal.uni-mainz.de/objects/375", 7 | "label": { 8 | "en": [ 9 | "AKU-PAL database record" 10 | ] 11 | }, 12 | "type": "Text" 13 | } 14 | ], 15 | "id": "https://aku-pal.uni-mainz.de/api/manifests/cae2c87f-52d0-408e-9e87-508e73baa954", 16 | "items": [ 17 | { 18 | "height": 3743, 19 | "id": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141", 20 | "items": [ 21 | { 22 | "id": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141/page/p1/1", 23 | "items": [ 24 | { 25 | "body": { 26 | "format": "image/jpeg", 27 | "height": 3743, 28 | "id": "https://aku-pal.uni-mainz.de:8183/iiif/3/data%2Fdig%2Fdig_10.tif/full/max/0/default.jpg", 29 | "type": "Image", 30 | "width": 5613 31 | }, 32 | "id": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141/annotation/p1", 33 | "motivation": "painting", 34 | "target": "https://089f3bbe-c4f4-4c8f-a242-38f919c74141", 35 | "type": "Annotation" 36 | } 37 | ], 38 | "type": "AnnotationPage" 39 | } 40 | ], 41 | "label": { 42 | "none": [ 43 | "Dipinto" 44 | ] 45 | }, 46 | "type": "Canvas", 47 | "width": 5613 48 | } 49 | ], 50 | "label": { 51 | "en": [ 52 | "Dipinto Assiut, Gebel Assiut al-gharbi, N13.1 TN2" 53 | ] 54 | }, 55 | "logo": { 56 | "id": "https://www.aegyptologie.uni-mainz.de/files/2014/08/Logo_neu.jpg", 57 | "type": "Image" 58 | }, 59 | "metadata": [ 60 | { 61 | "label": { 62 | "de": [ 63 | "Objekt" 64 | ], 65 | "en": [ 66 | "Object" 67 | ] 68 | }, 69 | "value": { 70 | "de": [ 71 | "Dipinto" 72 | ], 73 | "en": [ 74 | "Dipinto" 75 | ] 76 | } 77 | }, 78 | { 79 | "label": { 80 | "de": [ 81 | "Standort" 82 | ], 83 | "en": [ 84 | "Location" 85 | ] 86 | }, 87 | "value": { 88 | "de": [ 89 | "Assiut, Gebel Assiut al-gharbi, N13.1 TN2" 90 | ], 91 | "en": [ 92 | "Asyut, Gebel Asyut al-gharbi, N13.1 TN2" 93 | ] 94 | } 95 | }, 96 | { 97 | "label": { 98 | "de": [ 99 | "Datierung" 100 | ], 101 | "en": [ 102 | "Date" 103 | ] 104 | }, 105 | "value": { 106 | "de": [ 107 | "Neues Reich, 18. Dynastie" 108 | ], 109 | "en": [ 110 | "New Kingdom, 18th Dynasty" 111 | ] 112 | } 113 | } 114 | ], 115 | "requiredStatement": { 116 | "label": { 117 | "en": [ 118 | "Attribution" 119 | ] 120 | }, 121 | "value": { 122 | "en": [ 123 | "The Asyut Project, photo: Fritz Barthel" 124 | ] 125 | } 126 | }, 127 | "rights": "http://rightsstatements.org/vocab/InC/1.0/", 128 | "type": "Manifest" 129 | } 130 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/amherst-63cc6105-570d-407b-af8c-07fda3f8c620.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://www.w3.org/ns/anno.jsonld", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "type": "Manifest", 7 | "id": "https://acdc.amherst.edu/do/63cc6105-570d-407b-af8c-07fda3f8c620/metadata/iiifmanifest/default.jsonld", 8 | "label": { 9 | "en": [ 10 | "Transcription of Emily Dickinson's \"I'll clutch - and clutch\" - Image 3" 11 | ] 12 | }, 13 | "requiredStatement": { 14 | "label": { 15 | "en": [ 16 | "Attribution" 17 | ] 18 | }, 19 | "value": { 20 | "en": [ 21 | "Provided by Archipelago Deployment" 22 | ] 23 | } 24 | }, 25 | "metadata": [ 26 | { 27 | "label": { 28 | "en": [ 29 | "description" 30 | ] 31 | }, 32 | "value": { 33 | "en": [ 34 | "" 35 | ] 36 | } 37 | }, 38 | { 39 | "label": { 40 | "en": [ 41 | "navDate" 42 | ] 43 | }, 44 | "value": { 45 | "en": [ 46 | "January 23rd at 7:11pm" 47 | ] 48 | } 49 | }, 50 | { 51 | "label": { 52 | "en": [ 53 | "license" 54 | ] 55 | }, 56 | "value": { 57 | "en": [ 58 | "http:\/\/rightsstatements.org\/vocab\/InC-EDU\/1.0\/" 59 | ] 60 | } 61 | } 62 | ], 63 | "within": "", 64 | "partOf": [ 65 | { 66 | "id": "/metadata/iiifmanifest3cws/default.jsonld", 67 | "type": "Manifest" 68 | } 69 | ], 70 | "items": [ 71 | { 72 | "id": "https://acdc.amherst.edu/view/EmilyDickinson/ma00167-14-40-00508-0003/iiif/canvas/p1", 73 | "type": "Canvas", 74 | "label": { 75 | "en": [ 76 | "Transcription of Emily Dickinson's \"I'll clutch - and clutch\" - Image 3" 77 | ] 78 | }, 79 | "thumbnail": [ 80 | { 81 | "id": "https://acdc.amherst.edu/cantaloupe/iiif/2/media%2FEmilyDickinson%2Fma00167-14-40-00508%2Fimage-ma00167-14-40-00508-0003-a75da8c0-f701-45ab-8aff-48d72502bf95.jpg/full/240,/0/default.jpg", 82 | "type": "Image", 83 | "format": "image/jpeg", 84 | "width": 240, 85 | "height": 375 86 | } 87 | ], 88 | "height": 4035, 89 | "width": 2585, 90 | "items": [ 91 | { 92 | "id": "https://acdc.amherst.edu/view/EmilyDickinson/ma00167-14-40-00508-0003/iiif/page/p1", 93 | "type": "AnnotationPage", 94 | "items": [ 95 | { 96 | "id": "https://acdc.amherst.edu/view/EmilyDickinson/ma00167-14-40-00508-0003/iiif/annotation/p1", 97 | "type": "Annotation", 98 | "motivation": "painting", 99 | "thumbnail": [ 100 | { 101 | "id": "https://acdc.amherst.edu/cantaloupe/iiif/2/media%2FEmilyDickinson%2Fma00167-14-40-00508%2Fimage-ma00167-14-40-00508-0003-a75da8c0-f701-45ab-8aff-48d72502bf95.jpg/full/240,/0/default.jpg", 102 | "type": "Image", 103 | "format": "image/jpeg", 104 | "width": 240, 105 | "height": 375 106 | } 107 | ], 108 | "body": { 109 | "id": "https://acdc.amherst.edu/cantaloupe/iiif/2/media%2FEmilyDickinson%2Fma00167-14-40-00508%2Fimage-ma00167-14-40-00508-0003-a75da8c0-f701-45ab-8aff-48d72502bf95.jpg/full/full/0/default.jpg", 110 | "type": "Image", 111 | "format": "image/jpeg", 112 | "service": [ 113 | { 114 | "id": "https://acdc.amherst.edu/cantaloupe/iiif/2/media%2FEmilyDickinson%2Fma00167-14-40-00508%2Fimage-ma00167-14-40-00508-0003-a75da8c0-f701-45ab-8aff-48d72502bf95.jpg", 115 | "type": "ImageService2", 116 | "profile": "level2" 117 | } 118 | ], 119 | "height": 4035, 120 | "width": 2585 121 | }, 122 | "target": "https://acdc.amherst.edu/view/EmilyDickinson/ma00167-14-40-00508-0003/iiif/canvas/p1" 123 | } 124 | ] 125 | } 126 | ] 127 | } 128 | ], 129 | "structures": [ 130 | { 131 | "id": "https://acdc.amherst.edu/view/EmilyDickinson/ma00167-14-40-00508-0003/iiif/range/r1", 132 | "type": "Range", 133 | "label": { 134 | "en": [ 135 | "Transcription of Emily Dickinson's \"I'll clutch - and clutch\" - Image 3" 136 | ] 137 | }, 138 | "items": [ 139 | { 140 | "id": "https://acdc.amherst.edu/view/EmilyDickinson/ma00167-14-40-00508-0003/iiif/canvas/p1", 141 | "type": "Canvas" 142 | } 143 | ] 144 | } 145 | ] 146 | } 147 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0019-html-in-annotations.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-1/anno-1", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-2", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1/annopage-2/anno-1", 51 | "type": "Annotation", 52 | "motivation": "commenting", 53 | "body": { 54 | "type": "TextualBody", 55 | "language": "de", 56 | "format": "text/html", 57 | "value": "

Göttinger Marktplatz mit Gänseliesel Brunnen Wikipedia logo

" 58 | }, 59 | "target": "https://iiif.io/api/cookbook/recipe/0019-html-in-annotations/canvas-1" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0021-tagging.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/page/p1/1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/annotation/p0001-image", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/page/p2/1", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0021-tagging/annotation/p0002-tag", 51 | "type": "Annotation", 52 | "motivation": "tagging", 53 | "body": { 54 | "type": "TextualBody", 55 | "value": "Gänseliesel-Brunnen", 56 | "language": "de", 57 | "format": "text/plain" 58 | }, 59 | "target": "https://iiif.io/api/cookbook/recipe/0021-tagging/canvas/p1#xywh=265,661,1260,1239" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0266-full-canvas-annotation.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-1/anno-1", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-2", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1/annopage-2/anno-1", 51 | "type": "Annotation", 52 | "motivation": "commenting", 53 | "body": { 54 | "type": "TextualBody", 55 | "language": "de", 56 | "format": "text/plain", 57 | "value": "Göttinger Marktplatz mit Gänseliesel Brunnen" 58 | }, 59 | "target": "https://iiif.io/api/cookbook/recipe/0266-full-canvas-annotation/canvas-1" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0283-missing-image.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Ethiopic Ms 10" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p1", 13 | "type": "Canvas", 14 | "label": { 15 | "en": [ 16 | "f. 1r" 17 | ] 18 | }, 19 | "height": 2504, 20 | "width": 1768, 21 | "items": [ 22 | { 23 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/page/p1/1", 24 | "type": "AnnotationPage", 25 | "items": [ 26 | { 27 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/annotation/p0001-image", 28 | "type": "Annotation", 29 | "motivation": "painting", 30 | "body": { 31 | "id": "https://iiif.io/api/image/3.0/example/reference/d3bbf5397c6df6b894c5991195c912ab-1-21198-zz001d8m41_774608_master/full/max/0/default.jpg", 32 | "type": "Image", 33 | "format": "image/jpeg", 34 | "height": 2504, 35 | "width": 1768, 36 | "service": [ 37 | { 38 | "id": "https://iiif.io/api/image/3.0/example/reference/d3bbf5397c6df6b894c5991195c912ab-1-21198-zz001d8m41_774608_master", 39 | "type": "ImageService3", 40 | "profile": "level1" 41 | } 42 | ] 43 | }, 44 | "target": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p1" 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | { 51 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p2", 52 | "type": "Canvas", 53 | "label": { 54 | "en": [ 55 | "f. 1v — MISSING" 56 | ] 57 | }, 58 | "height": 2504, 59 | "width": 1768, 60 | "metadata": [ 61 | { 62 | "label": { 63 | "en": [ 64 | "Description" 65 | ] 66 | }, 67 | "value": { 68 | "en": [ 69 | "Image unavailable or does not exist" 70 | ] 71 | } 72 | } 73 | ], 74 | "items": [] 75 | }, 76 | { 77 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p3", 78 | "type": "Canvas", 79 | "label": { 80 | "en": [ 81 | "f. 2r" 82 | ] 83 | }, 84 | "height": 2456, 85 | "width": 1792, 86 | "items": [ 87 | { 88 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/page/p3/1", 89 | "type": "AnnotationPage", 90 | "items": [ 91 | { 92 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/annotation/p0003-image", 93 | "type": "Annotation", 94 | "motivation": "painting", 95 | "body": { 96 | "id": "https://iiif.io/api/image/3.0/example/reference/d3bbf5397c6df6b894c5991195c912ab-3-21198-zz001d8tm5_775004_master/full/max/0/default.jpg", 97 | "type": "Image", 98 | "format": "image/jpeg", 99 | "height": 2456, 100 | "width": 1792, 101 | "service": [ 102 | { 103 | "id": "https://iiif.io/api/image/3.0/example/reference/d3bbf5397c6df6b894c5991195c912ab-3-21198-zz001d8tm5_775004_master", 104 | "type": "ImageService3", 105 | "profile": "level1" 106 | } 107 | ] 108 | }, 109 | "target": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p3" 110 | } 111 | ] 112 | } 113 | ] 114 | }, 115 | { 116 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p4", 117 | "type": "Canvas", 118 | "label": { 119 | "en": [ 120 | "f. 2v" 121 | ] 122 | }, 123 | "height": 2440, 124 | "width": 1760, 125 | "items": [ 126 | { 127 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/page/p4/1", 128 | "type": "AnnotationPage", 129 | "items": [ 130 | { 131 | "id": "https://iiif.io/api/cookbook/recipe/0283-missing-image/annotation/p0004-image", 132 | "type": "Annotation", 133 | "motivation": "painting", 134 | "body": { 135 | "id": "https://iiif.io/api/image/3.0/example/reference/d3bbf5397c6df6b894c5991195c912ab-4-21198-zz001d8tnp_775007_master/full/max/0/default.jpg", 136 | "type": "Image", 137 | "format": "image/jpeg", 138 | "height": 2440, 139 | "width": 1760, 140 | "service": [ 141 | { 142 | "id": "https://iiif.io/api/image/3.0/example/reference/d3bbf5397c6df6b894c5991195c912ab-4-21198-zz001d8tnp_775007_master", 143 | "type": "ImageService3", 144 | "profile": "level1" 145 | } 146 | ] 147 | }, 148 | "target": "https://iiif.io/api/cookbook/recipe/0283-missing-image/canvas/p4" 149 | } 150 | ] 151 | } 152 | ] 153 | } 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/cookbook-recipe-0377-image-in-annotation.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1", 13 | "type": "Canvas", 14 | "height": 3024, 15 | "width": 4032, 16 | "items": [ 17 | { 18 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-1/anno-1", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 27 | "type": "Image", 28 | "format": "image/jpeg", 29 | "height": 3024, 30 | "width": 4032, 31 | "service": [ 32 | { 33 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 34 | "profile": "level1", 35 | "type": "ImageService3" 36 | } 37 | ] 38 | }, 39 | "target": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1" 40 | } 41 | ] 42 | } 43 | ], 44 | "annotations": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-2", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1/annopage-2/anno-1", 51 | "type": "Annotation", 52 | "motivation": "commenting", 53 | "body": [ 54 | { 55 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-fountain/full/300,/0/default.jpg", 56 | "type": "Image", 57 | "format": "image/jpeg" 58 | }, 59 | { 60 | "type": "TextualBody", 61 | "language": "en", 62 | "value": "Night picture of the Gänseliesel fountain in Göttingen taken during the 2019 IIIF Conference" 63 | } 64 | ], 65 | "target": "https://iiif.io/api/cookbook/recipe/0377-image-in-annotation/canvas-1#xywh=138,550,1477,1710" 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/invalid.json: -------------------------------------------------------------------------------- 1 | "This is not a valid IIIF manifest." 2 | -------------------------------------------------------------------------------- /tests/iiif-api/data/manifests/not-json.json: -------------------------------------------------------------------------------- 1 | This is not valid JSON. 2 | -------------------------------------------------------------------------------- /tests/iiif-api/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import http from 'http'; 3 | import url from 'url'; 4 | 5 | const dataDir = url.fileURLToPath(new URL('data', import.meta.url)); 6 | const server = http.createServer(); 7 | 8 | server.on('request', (req, res) => { 9 | function outputJpeg(fileName) { 10 | fs.readFile(fileName, (error, data) => { 11 | if (error) { 12 | res.writeHead(404); 13 | } else { 14 | res.writeHead(200, { 'Content-type': 'image/jpeg' }); 15 | res.write(data); 16 | } 17 | 18 | res.end(); 19 | }); 20 | } 21 | 22 | function outputJson(fileName) { 23 | fs.readFile(fileName, 'utf8', (error, data) => { 24 | if (error) { 25 | res.writeHead(404); 26 | } else { 27 | res.writeHead(200, { 'Content-type': 'application/json' }); 28 | 29 | // Rewrite all remote URLs to local ones, except IIIF API profiles 30 | const dataWithLocalUrls = data.replace( 31 | /(?!http:\/\/iiif.io\/api\/)https?:\/\/[a-z0-9-.:]*/gi, 32 | `http://127.0.0.1:${server.port}`, 33 | ); 34 | 35 | res.write(dataWithLocalUrls); 36 | } 37 | 38 | res.end(); 39 | }); 40 | } 41 | 42 | const { path } = url.parse(req.url); 43 | const segments = path.split('/'); 44 | let action; 45 | let file; 46 | if (segments[1] === 'presentation' && segments[2] === 'v2') { 47 | // Rewrite collection child manifest URLs for local testing 48 | action = 'manifest'; 49 | file = `wellcome-${segments[3]}.json`; 50 | } else if (segments[4] === 'list') { 51 | // Rewrite annotation lists URLs for local testing 52 | action = 'annotation-lists'; 53 | file = `${(segments[5] || 'default').replace(/:/g, '-')}.json`; 54 | } else if (segments[1] === 'fulltext') { 55 | // Rewrite fulltext URL for local testing 56 | action = 'annotations'; 57 | file = `gdz-${segments[2]}-${segments[3]}`; 58 | } else if (path.endsWith('.jpg')) { 59 | action = 'image'; 60 | } else { 61 | action = segments.at(-1) === 'info.json' ? 'info' : segments[1]; 62 | 63 | if (segments[2] && action !== 'annotations') { 64 | file = segments[2] + (segments[2].endsWith('.json') ? '' : '.json'); 65 | } 66 | } 67 | 68 | req.on('data', () => {}); 69 | 70 | req.on('end', () => { 71 | res.setHeader('Access-Control-Allow-Origin', '*'); 72 | res.setHeader('Access-Control-Request-Method', '*'); 73 | res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); 74 | res.setHeader('Access-Control-Allow-Headers', '*'); 75 | 76 | if (req.method === 'OPTIONS') { 77 | res.writeHead(200); 78 | res.end(); 79 | return; 80 | } 81 | 82 | if (action === 'manifest') { 83 | outputJson(`${dataDir}/manifests/${file}`); 84 | } else if (action === 'annotation-lists') { 85 | outputJson(`${dataDir}/annotation-lists/${file}`); 86 | } else if (action === 'annotations') { 87 | outputJson(`${dataDir}/annotations/${file || 'default.json'}`); 88 | } else if (action === 'info') { 89 | outputJson(`${dataDir}/infos/default.json`); 90 | } else if (['image', 'images', 'logos'].includes(action)) { 91 | outputJpeg(`${dataDir}/images/default.jpg`); 92 | } else { 93 | res.writeHead(400); 94 | res.end(); 95 | } 96 | }); 97 | }); 98 | 99 | export default { 100 | start(port = 8081) { 101 | server.listen(port); 102 | server.port = port; 103 | 104 | // eslint-disable-next-line no-console 105 | console.log(`\n 🤖 Mock IIIF API listening at http://localhost:${port}\n`); 106 | }, 107 | stop() { 108 | server.close(); 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /tests/unit/App.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import App from '../../src/App.vue'; 5 | 6 | import api from '../../src/plugins/api'; 7 | import store from '../../src/plugins/store'; 8 | import i18n from '../../src/plugins/i18n'; 9 | 10 | describe('setLanguage', () => { 11 | const { vm } = mount(App, { 12 | global: { 13 | plugins: [ 14 | [api, { instance: this }], 15 | i18n, 16 | store, 17 | ], 18 | }, 19 | props: { 20 | readyPromise: {}, 21 | }, 22 | }); 23 | 24 | // Replace fetchJson to return a mock object 25 | vm.$store.fetchJson = (url) => new Promise((resolve, reject) => { 26 | if (url === 'undefined/de.json') { 27 | resolve({ Dismiss: 'Ausblenden' }); 28 | } else { 29 | reject(new Error()); 30 | } 31 | }); 32 | 33 | it('loads the translation and changes the language', async () => { 34 | const result = await vm.setLanguage('de'); 35 | expect(result).toEqual('de'); 36 | }); 37 | 38 | it('throws an error when the translation cannot be loaded', async () => { 39 | try { 40 | await vm.setLanguage('-_-'); 41 | } catch { 42 | expect(vm.$store.errors.at(-1)).toContain('Error loading translation for "-_-"'); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/unit/components/MetadataList.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import MetadataList from '../../../src/components/MetadataList.vue'; 5 | 6 | describe('MetadataList', () => { 7 | const { vm } = mount(MetadataList); 8 | 9 | it('formats a label', () => { 10 | const label = 'example_label'; 11 | const cleanedLabel = vm.cleanLabel(label); 12 | expect(cleanedLabel).toEqual('Example label'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/components/PageSelect.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import PageSelect from '../../../src/components/PageSelect.vue'; 5 | 6 | import i18n from '../../../src/plugins/i18n'; 7 | import store from '../../../src/plugins/store'; 8 | 9 | import manifest from '../../iiif-api/data/manifests/utrecht-1874-325480.json'; 10 | 11 | describe('PageSelect', () => { 12 | const { vm } = mount(PageSelect, { 13 | global: { 14 | plugins: [ 15 | i18n, 16 | [store, { 17 | manifest, 18 | options: { 19 | language: 'en', 20 | pageLabelFormat: 'P : L', 21 | pages: [1], 22 | }, 23 | rootElement: { addEventListener() {} }, 24 | }], 25 | ], 26 | }, 27 | }); 28 | 29 | it('filters and updates canvases', () => { 30 | vm.filter = '10'; 31 | vm.updateFilteredCanvases(); 32 | 33 | expect(vm.highlightIndex).toEqual(0); 34 | expect(vm.filteredCanvases.length).toEqual(12); // Should contain pages 5, 15, 25, 35 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/unit/components/ViewToc.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import ViewToc from '../../../src/components/ViewToc.vue'; 5 | 6 | import i18n from '../../../src/plugins/i18n'; 7 | import store from '../../../src/plugins/store'; 8 | 9 | import manifestForLabels from '../../iiif-api/data/manifests/digitale-sammlungen-bsb00026283.json'; 10 | import manifestForPages from '../../iiif-api/data/manifests/gdz-DE_611_BF_5619_1801_1806.json'; 11 | 12 | describe('ViewToc', () => { 13 | const { vm } = mount(ViewToc, { 14 | global: { 15 | plugins: [ 16 | i18n, 17 | [store, { 18 | options: { language: 'en' }, 19 | manifest: manifestForLabels, 20 | }], 21 | ], 22 | }, 23 | }); 24 | 25 | it('selects a label in the current language', () => { 26 | const label = vm.$store.localize(vm.$store.structures[0].label); 27 | expect(label).toEqual('Miniatur: Jesu Gebet in Gethsemane'); 28 | }); 29 | 30 | it('orders pages by logical page number', () => { 31 | vm.$store.manifest = manifestForPages; 32 | 33 | const pages = vm.$store.structures[0].canvases.map((structure) => structure.firstPage); 34 | expect(pages.toString()).toEqual(pages.sort((a, b) => a - b).toString()); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/unit/modules/filter.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { filterHtml } from '../../../src/modules/filter'; 4 | 5 | describe('filterHtml', () => { 6 | it('filters HTML', () => { 7 | const html = ` 8 |

9 | 10 | label 11 | 12 |

13 |

14 | keep 15 |
16 | keep this 17 |

18 | `; 19 | 20 | const filteredHtml = ` 21 | ${''} 22 | 23 | label 24 | 25 | ${''} 26 |

27 | keep 28 |
29 | keep this 30 |

31 | `; 32 | 33 | expect(filterHtml(html)).toEqual(filteredHtml); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/unit/modules/validation.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { isValidPagesArray, isValidUrl } from '../../../src/modules/validation'; 4 | 5 | describe('isValidPagesArray', () => { 6 | it('validates page numbers', () => { 7 | const pageCount = 5; 8 | 9 | expect(isValidPagesArray([1], pageCount)).toEqual(true); 10 | expect(isValidPagesArray([0, 1], pageCount)).toEqual(true); 11 | expect(isValidPagesArray([1, 3, 5], pageCount)).toEqual(true); 12 | 13 | expect(isValidPagesArray(['nope'], pageCount)).toEqual(false); 14 | expect(isValidPagesArray([-1], pageCount)).toEqual(false); 15 | expect(isValidPagesArray([999], pageCount)).toEqual(false); 16 | expect(isValidPagesArray([5, 3, 1], pageCount)).toEqual(false); 17 | }); 18 | }); 19 | 20 | describe('isValidUrl', () => { 21 | it('validates a URL', () => { 22 | const validUrl = 'http://example.org'; 23 | expect(isValidUrl(validUrl)).toEqual(true); 24 | 25 | const validUrlWithQuery = 'https://example.org/?query=something'; 26 | expect(isValidUrl(validUrlWithQuery)).toEqual(true); 27 | 28 | const notAUrl = 'example.org'; 29 | expect(isValidUrl(notAUrl)).toEqual(false); 30 | 31 | const notAHttpUrl = 'ftp://example.js'; 32 | expect(isValidUrl(notAHttpUrl)).toEqual(false); 33 | expect(isValidUrl(notAHttpUrl, ['ftp:'])).toEqual(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/unit/plugins/store.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import store from '../../../src/plugins/store'; 4 | 5 | import manifest from '../../iiif-api/data/manifests/bl-vdc_00000004216E.json'; 6 | 7 | const app = { config: { globalProperties: {} } }; 8 | 9 | store.install(app, { 10 | manifest: store.convertManifest(manifest), 11 | options: { 12 | language: 'en', 13 | pageLabelFormat: 'P : L', 14 | translationsDirUrl: '', 15 | }, 16 | }); 17 | 18 | const { $store } = app.config.globalProperties; 19 | 20 | describe('getPageLabel', () => { 21 | it('gets the page label', () => { 22 | expect($store.getPageLabel(1, 'label')).toEqual('1 : label'); 23 | }); 24 | 25 | it('gets only the page number if the label is empty', () => { 26 | expect($store.getPageLabel(1, {})).toEqual('1'); 27 | }); 28 | }); 29 | 30 | describe('getStartPage', () => { 31 | it('determines the start page based on startCanvas', () => { 32 | expect($store.getStartPage()).toEqual(7); 33 | }); 34 | }); 35 | 36 | describe('setPage', () => { 37 | it('sets the page', () => { 38 | expect($store.setPage(1)).toEqual([1]); 39 | expect($store.setPage([0, 1])).toEqual([0, 1]); 40 | expect($store.setPage([1, 3, 5])).toEqual([1, 3, 5]); 41 | }); 42 | 43 | it('throws an error when trying to set an invalid page', () => { 44 | expect(() => $store.setPage('nope')).toThrow(RangeError); 45 | expect(() => $store.setPage(-1)).toThrow(RangeError); 46 | expect(() => $store.setPage(999)).toThrow(RangeError); 47 | expect(() => $store.setPage([5, 3, 1])).toThrow(RangeError); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | import { readdirSync, readFileSync } from 'node:fs'; 4 | import { inspect } from 'node:util'; 5 | 6 | import vue from '@vitejs/plugin-vue'; 7 | import componentsAutoImport from 'unplugin-vue-components/vite'; // eslint-disable-line import/no-unresolved 8 | import banner from 'vite-plugin-banner'; 9 | import eslint from 'vite-plugin-eslint'; 10 | import sassGlobImport from 'vite-plugin-sass-glob-import'; 11 | 12 | import 'dotenv/config'; 13 | 14 | import pkg from './package.json'; 15 | 16 | const repositoryUrl = pkg.repository.url.replace(/git\+(.+)\.git/, '$1'); 17 | 18 | const transformIndexPlugin = () => ({ 19 | transformIndexHtml(html) { 20 | const translationsDir = './public/translations'; 21 | 22 | const petiteVue = readFileSync('./node_modules/petite-vue/dist/petite-vue.iife.js').toString().trim(); 23 | 24 | const languages = { en: 'English' }; 25 | readdirSync(translationsDir).forEach((file) => { 26 | languages[file.split('.')[0]] = JSON.parse(readFileSync(`${translationsDir}/${file}`)).$language; 27 | }); 28 | 29 | return html 30 | .replace(/\$VITE_PETITE_VUE;?/, petiteVue) 31 | .replace('$VITE_LANGUAGES', inspect(languages, { breakLength: Infinity, compact: true })); 32 | }, 33 | }); 34 | 35 | // https://vitejs.dev/config/ 36 | export default defineConfig({ 37 | base: process.env.BASE || '/', 38 | build: { 39 | outDir: process.env.OUTDIR || './dist', 40 | rollupOptions: { 41 | output: { 42 | // https://rollupjs.org/guide/en/#outputentryfilenames 43 | entryFileNames: process.env.HASHED ? 'tify.[hash].js' : 'tify.js', 44 | // https://rollupjs.org/guide/en/#outputassetfilenames 45 | assetFileNames: process.env.HASHED ? 'tify.[hash].[ext]' : 'tify.[ext]', 46 | }, 47 | }, 48 | }, 49 | // https://vitejs.dev/config/#environment-variables 50 | define: { 51 | ENV: { 52 | blobBaseUrl: `${repositoryUrl}/blob/v${pkg.version}`, 53 | bugsUrl: pkg.bugs.url, 54 | license: pkg.license, 55 | repositoryUrl, 56 | version: pkg.version, 57 | }, 58 | }, 59 | plugins: [ 60 | // Prepend copyright notice to each compiled file 61 | banner( 62 | '/*!' 63 | + `\nTIFY v${pkg.version}` 64 | + `\n(c) 2017-${new Date().getFullYear()}` 65 | + ' Göttingen State and University Library (https://www.sub.uni-goettingen.de/)' 66 | + `\n${pkg.license}` 67 | + `\n${pkg.homepage}` 68 | + '\n*/', 69 | ), 70 | componentsAutoImport({ 71 | dts: false, // disable generating components.d.ts file 72 | resolvers: [ 73 | (componentName) => { 74 | // NOTE: Full path required for unit tests with Vitest 75 | // Replacing "\" with "/" so it works on Windows; path.normalize cannot help here 76 | const baseDir = __dirname.replaceAll('\\', '/'); 77 | const dir = `${baseDir}/src/components${componentName.startsWith('Icon') ? '/icons' : ''}`; 78 | return { 79 | name: componentName, 80 | from: `${dir}/${componentName}.vue`, 81 | }; 82 | }, 83 | ], 84 | }), 85 | eslint({ 86 | cache: true, 87 | // Poor man's ignore file parser, since --ignore-path is not supported 88 | exclude: readFileSync('.gitignore') 89 | .toString() 90 | .split('\n') 91 | .filter((line) => line && !line.startsWith('#')) 92 | .map((line) => (!line.endsWith('*') ? `${line}/**` : line)), 93 | fix: true, 94 | }), 95 | sassGlobImport(), 96 | transformIndexPlugin(), 97 | vue(), 98 | ], 99 | }); 100 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'; 3 | import viteConfig from './vite.config'; 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | include: ['tests/unit/**/*.spec.js'], 11 | exclude: [...configDefaults.exclude], 12 | root: fileURLToPath(new URL('./', import.meta.url)), 13 | 14 | // Prevent canvas-related error in unit tests 15 | // https://github.com/wobsoriano/vitest-canvas-mock 16 | setupFiles: ['./vitest.setup.js'], 17 | deps: { 18 | optimizer: { 19 | web: { 20 | include: ['vitest-canvas-mock'], 21 | }, 22 | }, 23 | }, 24 | }, 25 | }), 26 | ); 27 | -------------------------------------------------------------------------------- /vitest.setup.js: -------------------------------------------------------------------------------- 1 | // Prevent canvas-related error in unit tests 2 | // https://github.com/wobsoriano/vitest-canvas-mock 3 | import 'vitest-canvas-mock'; 4 | 5 | // Prevent error "jest is not defined" when using vitest-canvas-mock 6 | // https://github.com/vitest-dev/vitest/issues/2667#issuecomment-1383071037 7 | import { vi } from 'vitest'; 8 | 9 | global.jest = vi; 10 | --------------------------------------------------------------------------------