Text vIewer for Digital Objects. A highly configurable client for the TextAPI to display texts, annotations and images.
Please visit our demo page where you can run TIDO with your own TextAPI endpoint and also view our production examples.
- Getting Started
- Configuration
- Bookmarking
- Text Sanitization
- Getting Started (Developers)
- TextAPI Support
- Contributing
- Versioning
- Authors
TIDO is provided as npm package. Please follow the steps below to include it for production:
npm i tido@nextYou can use the app in different scenarios, either as embedded bundle or as a React library.
- Add these two files to your application:
tido.min.jsandtido.min.css.
HTML:
<link href="/node_modules/tido/dist/tido.min.css" rel="stylesheet">
<script src="/node_modules/tido/dist/tido.min.js"></script>or JS:
import 'tido/dist/tido.min.js'
import 'tido/dist/tido.min.css'- Add a container element to your application where TIDO can mount onto. Please make sure that this element has your desired dimensions and position.
<style>
#app {
height: 100vh;
width: 100%;
}
</style>
<div id="app"></div>- Create a new TIDO instance and provide optionally your TIDO configuration:
<script>
const tido = new Tido({
rootCollections: ['https://example.com/textapi/collection.json']
});
</script>Use TIDO directly inside your React application (e.g., SPA or Next.js) as a component:
import { Tido } from 'tido'
import 'tido/dist/tido.min.css'
export default function TidoPage() {
return <div className="container">
<Tido config={} />
</div>
}Below you can find a detailed explanation of the configuration object.
You can fully customize the viewer's behaviour by providing a configuration object to the TIDO instance.
First of all, there should be set either a collection or a manifest value.
By default, TIDO will render 5 panels displaying sequence tree, metadata, image, text content and annotation views.
There are options to
- add/remove multiple panels
- freely combine view components in panels
- show/hide header features
- change the color scheme
- and more ...
Example configuration:
Click to open
<script id="tido-config" type="application/json">
{
"allowNewCollections": false,
"defaultPanelMode": "split",
"lang": "de",
"panels": [
{
"collection": "https://api.ahiqar.uni-goettingen.de/textapi/ahiqar/syriac/collection.json",
"manifest": "https://api.ahiqar.sub.uni-goettingen.de/textapi/ahiqar/syriac/3r145/manifest.json"
}
],
"theme": {
"primaryColor": "#1E40AF"
},
"rootCollections": [
"https://textapi.dev.vierwachen.sub.uni-goettingen.de/api/4w/collection.json"
],
"translations": {
"de": {
"accurate": "Handschriftengetreu",
"simplified": "Vereinfacht"
}
},
"title": "Vier Wachen Edition"
}
</script>| Name | Type | Default | Description |
|---|---|---|---|
| allowNewCollections | Boolean | true | Toggles the ability to add new collections to the app through a user input. |
| annotations | AnnotationsConfig | {} | Configures the display of annotations and their filtering options. See annotations chapter. |
| annotations.defaultMode | String | aligned |
An annotations mode toggle is shown. Initial selected mode is aligned mode |
| annotations.filters | AnnotationFiltersConfig | - | Defines a nested object of filter options. |
| annotations.filters.rootSelectionRule | String | multiple |
Controls the overall behavior and layout of the filter UI. Allowed values: multiple or single. When set to single, a tab bar is rendered for root-level nodes. |
| annotations.filters.selectedIndex | Number | - | Index of the initially selected filter node in the filter tree. Only applies when rootSelectionRule is set to single. |
| annotations.filters.items | FilterNode[] | - | Array of filter nodes defining the hierarchical filter tree structure. |
| annotations.filters.items[i].types | String[] | VariantType[] | - | Array of annotation type identifiers that a filter node controls. Can be plain strings (e.g., "person", "place") or variant witness objects (e.g., { Variant: "witness_id" }). |
| annotations.filters.items[i].label | String | - | Label to display for a filter node. |
| annotations.filters.items[i].items | FilterNode[] | - | Child filter nodes for nested filtering. |
| annotations.singleMode | String | - | Specifies only one annotations mode, which can be either aligned or list |
| annotations.types | AnnotationTypeConfigMap | - | A map of config objects for annotation types. This is used to display custom labels and icons in annotation items. |
| annotations.types[i].label | String | - | Custom label to display for an annotation type. |
| annotations.tooltipTypes | String[] | - | Annotation types to display as a tooltip when clicked instead of in the sidebar. These types will be excluded from the filters. |
| container | String | #app |
Specifies the CSS selector where we should append the TIDO app to. |
| lang | String (ISO 639-1 language code) | en |
Specifies the current active language of the app. See translations chapter. |
| panels | PanelConfig[] | [] | Defines an array of panel objects. The panels will appear in the same order. |
| panels[i].collection | String | - | TextAPI collection URL. |
| panels[i].item | String | - | TextAPI item URL. If not specified, the first item of the collection will be used. |
| panels[i].manifest | String | - | TextAPI manifest URL. If not specified, the first manifest of the collection will be used. |
| panels[i].selectedAnnotation | String | - | Specifies the ID of a specific annotation to select/display when the panel loads. |
| panels[i].showSidebar | Boolean | - | Controls whether the annotations sidebar is visible for this panel. |
| panels[i].views | PanelViewConfig[] | - | Overrides the global panelViews config for this specific panel. Use this to customize views per panel while still inheriting unset properties from the global config. |
| panelViews | PanelViewConfig[] | [{label: "Image", view: "image"}, {label: "Text", view: "text"}] |
Configures an initial distribution of content views inside each panel. The defined views here can then be toggled on/off in the "View" dropdown in the panel header. See Panel Views chapter. |
| panelViews[i].activeContentType | String | - | Specifies which content type should be active/selected by default for this view. Use with contentTypes to set the default selection. |
| panelViews[i].contentTypes | String[] | - | Specifies a list of TextAPI content type identifiers. If multiple are given, a content type toggle with a dropdown selection will be shown. Users can then switch the respective content type within a split pane. |
| panelViews[i].label | String | Image | Text |
Specifies a label for a view. It appears as title prefix in the content type toggle and in the "View" selection dropdown in the panel header. |
| panelViews[i].view | image | text |
image | text |
Specifies a view identifier that should be loaded into a split pane. |
| panelViews[i].visible | Boolean | true | Controls whether this view is visible in the panel. When false, the view will be hidden but can be toggled on via the "View" dropdown. |
| rootCollections | String[] | [] | Specifies a list of TextAPI collection URLs that appear in the global tree on the left. Users navigate and open new panels from those collections. |
| showAddNewPanelButton | Boolean | true | Toggles the display of the "add new panel" button. |
| showContentTypeToggle | Boolean | true | Toggles the display of the content type toggle in TextViews. When false, the whole bar is hidden. |
| showGlobalTree | Boolean | true | Toggles the display of the global tree on the left. When false the toggle button in the header is hidden. |
| theme | Object | Object | Specifies theme settings for UI elements. |
| theme.primaryColor | String | #3456aa, rgb(79, 70, 229), hsl(243, 75%, 59%), oklch(0.4743 0.1405 264.94) |
Primary color of UI elements. Used on buttons and other interactive elements. The value can be provided as string in following color systems (hex, rgb, hsl, oklch). Alpha channel is not supported. |
| title | String | empty | Specifies the main title of the app in the header. Translatable. |
| translations | Object | null | Specifies a custom translations object. See translations chapter. |
| translations.[lang] | TranslationNamespace | - | Defines a language key. The value is a TranslationNamespace object. |
| translations.[lang].[namespace] | Object | - | Defines a translation key/value pair for a supported language. You can override existing key/value pairs or define custom key/value pairs. There is a list that we expose for overriding in the configuration. |
| translations.[lang].[namespace].[translationKey] | String | - | Defines a translation key/value pair for a supported language. You can override existing key/value pairs or define custom key/value pairs. There is a list that we expose for overriding in the configuration. |
This array of PanelViewConfig objects defines the actual visible content inside a panel. It is a list of
objects, each of which creates a resizable split pane inside the panel. This configuration will be applied to all
additionally added panels. Users can toggle on/off each split pane by the "View" dropdown in the panel header.
It will show your configured labels of each PanelViewConfig as options. If no label is stated, it will show the raw
content type identifier instead.
We provide configuration options to customize both the display and filtering of annotations.
The configuration is organized by concern. Each aspect can be defined independently using its own configuration object under a dedicated key.
Working with annotations, especially variants, can be complex. Variant annotations reference one or more witnesses, which are identified by string identifiers. Within the configuration, these witness identifiers are treated as annotation types. This allows you to apply the same configuration principles to witnesses as you would to any other annotation type.
If no annotations configuration is provided at all, TIDO will discover the currently available annotations on-the-fly. The visible types and filters are highly dependent on the loaded panel content.
| Key | Type |
|---|---|
| types | AnnotationTypeConfigMap |
A map of config objects for annotation types. Each key represents the annotation type string ("x-content-type").
Each value is an AnnotationTypeConfig object for setting a custom label and icon.
These values will be used in the UI to customize the display of annotations items in the panel sidebar.
As mentioned above, variant annotations are a special case. In order to configure the witnesses display,
you can set a the idno value from the refs-Array in the annotationsPage response.
Example:
{
"annotations": {
"types": {
"Place": { "label": "Ort" },
"witness_a": { "label": "Witness A" }
}
}
}| Key | Type |
|---|---|
| filters | AnnotationFiltersConfig |
Annotations displayed in the sidebar can be filtered by their type. You can define a hierarchical structure for these filter options, referred to as a filter tree. Each node in the filter tree can control multiple annotation types simultaneously. Therefore, you need to specify an array of annotation types that should be affected by a given filter node.
At the root level, you can optionally define a rootSelectionRule (allowed values: multiple or single) to control
the overall behavior and layout of the filter UI:
If set to single, the UI renders a tab bar containing the root-level nodes and a content area below it.
Selecting a tab updates the content area to display the child nodes of the selected root option.
If set to multiple, a multi-selection tree is rendered without a tab bar,
allowing multiple root-level nodes to be selected simultaneously.
If the rule is not specified, we set it to multiple.
Example:
{
"annotations": {
"filters": {
"rootSelectionRule": "single",
"items": [
{
"label": "Variants",
"types": ["Variant"],
"items": [
{
"label": "Witness A",
"types": ["witness_a"]
},
{
"label": "Witness B",
"types": ["witness_b"]
}
]
},
{
"label": "Commentary",
"items": [
{
"label": "Entities",
"items": [
{
"label": "Places",
"types": ["place", "region"]
},
{
"label": "Persons",
"types": ["person"]
}
]
},
{
"label": "Additions",
"types": ["addition"]
}
]
},
{
"label": "Dictionary",
"types": ["dict"]
}
]
}
}
}
By default we provide an AnnotationsMode toggle which enables switching between list and aligned mode. Aligned mode
will arrange annotations at the top position of text targets. List mode will ignore positioning and arrange the annotations
vertically one after another.
To specify the initial selected mode you should add defaultMode in annotations config as following.
Example:
{
"annotations": {
"defaultMode": "list"
}
}You can also have only one mode of annotations. In this case AnnotationsToggle is not shown anymore.
To accomplish this, you should add singleMode key in config as shown below.
Example:
{
"annotations": {
"singleMode": "aligned"
}
}If no key regarding modes is specified, then by default AnnotationsToggle is shown with aligned mode as initially selected.
We provide a flexible way to use TIDO in your desired language. First of all we keep all translation keys in files
under public/translations. Each file has to follow the naming convention [ISO 639-1 language code].json. By default,
we provide a limited amount of supported languages. However, you can configure your custom languages though the TIDO config object.
Append your language under the translations key in the config and set the lang key to your language, so it is treated
as active language.
In TIDO we give you the ability to define custom translations for different collections.
This is how it works:
The translation key/value pairs are always wrapped by a namespace which can be either common or a collection slug.
The default namespace is common and is used as a fallback namespace.
You have to provide at least one namespace in your translation objects. If you don't need to use collection namespaces it still has to be common.
If the namespace is a TextAPI collection slug,
we check at initialization time of each panel whether there is a match between the namespace key and the slug from the loaded collection.
You don't need to provide a full set of translation key/value pairs on each namespace.
As mentioned before, common is a fallback namespace, so if any translation key is missing on your collection namespace, it will still be translated from the commonnamespace.
Hint: There is a difference between fixed keys that we use internally (like add_new_panel) or dynamic keys that we might
receive from the TextAPI (like dynamic metadata keys). If you want to translate the latter ones, just state them as keys to your translation object.
Example:
{
"lang": "fr",
"translations": {
"fr": {
"common": {
"add_new_panel": "Ajouter un nouveau panneau",
"title": "Titre",
"my_custom_key": "Valeur très importante",
"That one string": "Ce texte",
"manifest": "manuscrit"
},
"special_collection": {
"manifest": "livre"
}
}
}
}You can see that the first two keys add_new_panel and title are our fixed app keys and the other two keys are custom
project keys. If you set lang to a language that we don't support and omit certain translations in your config,
we will display those in English. If you don't provide a translation for a custom key, it will be displayed just as-is.
Also notice how the manifest key is being set to a different word for special_collection.
You can save and share your current view with the bookmarking feature. This is useful e.g. when you want to create TIDO-Link from a search result or comparing multiple texts and share it to a colleague.
TIDO reads and creates a string that is assigned as value to the tido GET-parameter at the URL.
We inherit the principles of the IIIF Content State API and use a similar approach.
The value of the tido GET-Parameter can be either a URL that returns a TidoContentState JSON or
an encoded TidoContentState JSON string. The TidoContentState contains all the information needed to recreate
a certain panel configuration. If users want to create a new sharable state, they can click the "Share"-button in the app
header and copy the resulting link.
We export a TypeScript interface TidoContentState where you can inspect the structure of the state object.
Here is an example:
{
type: 'Annotation'
motivation: ['contentState']
target: [
{
id: 'https://example.com/my-item-1',
type: 'Item',
partOf: {
id: 'https://example.com/my-manifest-1'
type: 'Manifest'
partOf: {
id: 'https://example.com/my-collection-1'
type: 'Collection'
}
},
state: {
mode: 'split'
}
}
]
}
This object would open one panel load the content https://example.com/my-item-1 into it. Since TIDO panels need further
information from a manifest and collection, you need to provide those URLs using the partOf key. This is compliant
with the PanelConfig object from the configuration.
Please note that the Item and Manifest targets are not required. TIDO will load the first elements from the respective sequences.
You can append the state key to the "root" target to specify further the state of a panel. Currently only the mode
configuration can be applied.
We provide the encodeState and decodeState functions as export in our npm package. You can use them like this:
import { encodeState, decodeState } from 'tido'
const encoded = await encodeState(myState)
const decoded = await decodeState(encoded)In case you want to implement the mechanism yourself, here is the procedure:
-
Encoding:
- JSON-stringify the state object
- GZIP-compress the JSON string
- Base64 encode the compressed string
- Convert to Base64URL (replace + with - and / with _, remove = )
-
Decoding:
- Convert Base64URL to Base64
- Convert Base64 to binary
- GZIP-decompress the binary
- Parse JSON string to object
TIDO uses DOMPurify to sanitize the incoming text in order to provide the best security possible while working with dynamic HTML strings. We extend the default configuration of DOMPurify with the followings:
inputscriptnoscriptiframeframeframesetnoframesappletbasemetaform
targetrel
To get TIDO up and running you should have the following software installed:
- npm
- nvm
Note:
We recommend to make use of nvm, since there might be issues with npm regarding permissions.
The main purpose of nvm is to have multiple node versions installed in regards to different projects which might demand some sort of backwards compatibility.
It enables you to just switch to the appropriate node version.
Besides it also keeps track of resolving permission issues, since all your global installations go to your home directory (~/.nvm/) instead of being applied systemwide.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
nvm install stableNote:
After the nvm installation is done, please restart your shell session once. That's due to changes to your profile environment.
git clone git@github.com:subugoe/tido.git
git checkout nextHead over to your project directory, where you just cloned the repository to as described above and get all the dependencies needed by typing:
cd /path/to/projectdir
npm installThat's it. You should now be able to run the Viewer.
Please run this command to create a production build.
npm run buildThe output files are located at /dist.
Builds the app in development mode (hot reloading, error reporting, etc.).
npm run devCheck examples/config to find more configurations that we have prepared. You can then run npm run dev [name] to load
that specific config. Example:
npm run dev 4wAlso, you can modify /index.html to load your required TIDO configuration.
The development build will be available under localhost:5173.
You can also inspect some example TIDO configurations that we provide under /examples.
Run this command which will create a TIDO production build, copy the result files into /examples
and start up a local web server:
npm run preview:examplesThese examples are available under localhost:2222. Each example has its own HTML file:
http://localhost:2222/[example-name].html
We provide several mock JSON files in order to create API endpoints for testing purposes. You can start your own local API server with this command:
npm run apiThe server will be available at localhost:8181. Check out /tests/mocks inspect the files.
We run tests only on production code. So you need to make sure to create a TIDO build before starting to run tests.
This is a minimal command to run tests once on your local machine.
npm run api & npm run test
If you want to gain more control during development you can do the following. Prepare the environment before running the tests:
npm run apinpm run preview:examples
Now you can run the tests on your local machine with a proper Cypress UI and selective steps or run the tests only in headless mode which will prompt the results on the console.
npm run cypressornpm run cypress:headless
npm run lint # to lint all the files at once
npm run lint:js # to lint js files only
npm run lint:markdown # to lint the markdown
npm run lint:scss # to lint the styles
npm run lint:vue # to lint vue files onlyWe maintain a JSON file (/.validation/support-matrix.json) that keeps track of supported TextAPI features.
If you need to recreate that file and rerender /SUPPORT.md, here is an explanation:
- Make sure you run NodeJS version >= 20
- Set
GITLAB_ACCESS_TOKENin/.envfile - Run
node ./.validation/create-support-matrix.js - Inspect
/.validation/support-matrix.json, all status values are set to 0 (= not supported) by default - Edit the statuses manually: 0 = not supported, 1 = supported, 2 = partially supported, 3 = unused
- Run
node ./.validation/create-support-matrix.jsagain to generate new/SUPPORT.md
Please view this document to see an overview of supported TextAPI features: State of TextAPI support
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
We use SemVer for versioning. For the versions available, see the tags on this repository.
See the list of contributors who participated in this project.