App.vue 12.5 KB
Newer Older
Mathias Goebel's avatar
Mathias Goebel committed
1
<template>
2
3
  <q-layout
    id="q-app"
4
    class="root viewport"
5
6
    view="hHh Lpr fFf"
  >
7
    <Header
8
      v-if="config['header_section'].show"
9
10
      :collectiontitle="collectiontitle"
      :config="config"
11
      :default-view="defaultView"
12
13
14
15
16
      :imageurl="imageurl"
      :item="item"
      :itemurls="itemurls"
      :manifests="manifests"
      :panels="panels"
17
18
      :projectcolors="config.colors"
      :standalone="config.standalone"
19
    />
20

21
22
    <q-page-container class="root">
      <router-view
23
        :annotations="annotations"
24
25
        :collection="collection"
        :config="config"
26
        :contentindex="contentindex"
27
        :contenttypes="contentTypes"
28
        :contenturls="contentUrls"
29
        :errormessage="errormessage"
30
31
        :fontsize="fontsize"
        :imageurl="imageurl"
32
        :isloading="isLoading"
33
34
35
        :item="item"
        :labels="config.labels"
        :manifests="manifests"
36
        :oncontentindexchange="oncontentindexchange"
37
38
39
40
41
42
        :panels="panels"
        :request="request"
        :tree="tree"
      />
    </q-page-container>
  </q-layout>
Mathias Goebel's avatar
Mathias Goebel committed
43
44
45
</template>

<script>
46
import Annotation from '@/mixins/annotation';
schneider210's avatar
schneider210 committed
47
import { colors } from 'quasar';
48
import treestore from '@/stores/treestore.js';
49
import Header from '@/components/header.vue';
50
import Panels from '@/mixins/panels';
51

Mathias Goebel's avatar
Mathias Goebel committed
52
export default {
53
  name: 'TIDO',
54
55
56
  components: {
    Header,
  },
57
58
59
60
  mixins: [
    Annotation,
    Panels,
  ],
61
62
  data() {
    return {
63
      annotations: [],
64
      collection: {},
65
      collectiontitle: '',
66
      config: {},
67
      contentindex: 0,
68
      contentTypes: [],
69
      contentUrls: [],
70
      errormessage: false,
71
      fontsize: 16,
72
      imageurl: '',
73
      isCollection: false,
74
      isLoading: false,
75
      item: {},
76
77
      itemurl: '',
      itemurls: [],
78
      loaded: false,
79
80
81
82
      manifests: [],
      tree: [],
    };
  },
83
84
85
86
87
88
89
90
91
92
  watch: {
    '$route.query': {
      handler: 'onItemUrlChange',
      immediate: true,
    },
    manifests: {
      handler: 'onItemUrlChange',
      immediate: false,
    },
  },
nwindis's avatar
nwindis committed
93
94
95
96
97
  created() {
    this.getConfig();
    this.init();
    this.itemurls.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));

schneider210's avatar
schneider210 committed
98
99
    this.$q.dark.set('auto');

nwindis's avatar
nwindis committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
    if (this.config.colors.primary && this.config.colors.secondary && this.config.colors.accent) {
      colors.setBrand('primary', this.config.colors.primary);
      colors.setBrand('secondary', this.config.colors.secondary);
      colors.setBrand('accent', this.config.colors.accent);
    }
  },
  mounted() {
    /**
      * listen to fontsize change (user interaction). emitted in @/components/content.vue
      * in- or rather decrease fontsize of the text by 1px
      * default fontsize: 14px
      *
      * @param number fontsize
      */
    this.$root.$on('update-fontsize', (fontsize) => {
      this.fontsize = fontsize;
    });
    this.$root.$on('panels-position', (newPanels) => {
      this.panels = newPanels;
    });
    /**
      * listen to item change (user interaction).
      * emitted in: *getItemurls*; handler for tree nodes. fired on user interaction
      *
      * @param string url
      */
  },
127
  methods: {
128
129
130
    defaultView() {
      this.loaded = false;
    },
schneider210's avatar
schneider210 committed
131
132
133
134
135
136
137
138
139
140
    /**
      * get resources using JavaScript's native fetch api
      * caller: *getCollection()*, *getItemData()*, *getManifest()*
      *         *@/components/content.vue::getSupport()*, *@/components/content.vue::created-hook*
      *
      * @param string url
      * @param string responsetype
      *
      * @return promise data
      */
141
142
    async request(url, responsetype = 'json') {
      const response = await fetch(url);
143
144
145
      const data = await (responsetype === 'text'
        ? response.text()
        : response.json());
146
147
148

      return data;
    },
149
150
151
152
153
154

    /**
      * get annotations of the current item
      * caller: *getItemData()*
      * @param string url
      */
155
    async getAnnotations(url) {
156
      this.annotations = [];
157
      this.isLoading = false;
158

159
160
161
162
163
164
165
166
      try {
        const annotations = await this.request(url);

        if (!annotations.annotationCollection.first) {
          this.annotations = [];
          return;
        }

167
168
169
        const current = await this.request(
          annotations.annotationCollection.first,
        );
170
171

        if (current.annotationPage.items.length) {
172
          this.annotations = current.annotationPage.items.map((x) => ({ ...x, targetId: this.stripTargetId(x, true) }));
173
174
175
176
177
        } else {
          this.annotations = [];
        }
      } catch (err) {
        this.annotations = [];
178
      } finally {
179
        this.isLoading = true;
180
      }
181
    },
schneider210's avatar
schneider210 committed
182
183
184
185
186
187
188
189
    /**
      * get collection data according to 'entrypoint'
      * (number of requests equal the number of manifests contained within a collection)
      * initialize the tree's root node
      * caller: *init()*
      *
      * @param string url
      */
190
    async getCollection(url) {
191
192
      this.isCollection = true;

193
      const data = await this.request(url);
194

195
196
197
      this.collection = data;
      this.collectiontitle = this.getLabel(data);

198
199
200
201
      this.tree.push({
        children: [],
        handler: (node) => {
          this.$root.$emit('update-tree-knots', node.label);
202
        },
203
204
205
206
        label: this.collectiontitle,
        'label-key': this.collectiontitle,
        selectable: false,
      });
207
208
209
210
211
212
213
214
215

      if (Array.isArray(data.sequence)) {
        const promises = [];
        data.sequence.forEach((seq) => promises.push(this.getManifest(seq.id)));

        await Promise.all(promises);
      }
      if (this.manifests?.[0]?.sequence?.[0]?.id && !this.$route.query.itemurl) {
        this.loaded = false;
216
        this.$router.push({ query: { ...this.$route.query, itemurl: this.manifests?.[0]?.sequence?.[0]?.id } });
217
      }
218
    },
schneider210's avatar
schneider210 committed
219
220
221
222
    /**
      * get config object (JSON) from index.html
      * caller: *created-hook*
      */
223
    getConfig() {
224
      this.config = JSON.parse(document.getElementById('tido-config').text);
225
    },
226
    /**
227
      * filter all urls that match either of the MIME types "application/xhtml+xml" and "text/html"
228
229
230
231
232
233
      * caller: *getItemData()*
      *
      * @param string array
      *
      * @return array
      */
234
    getContentUrls(content) {
235
236
237
      const urls = [];

      if (Array.isArray(content) && content.length) {
238
239
        this.contentTypes = [];

240
241
242
        content.forEach((c) => {
          if (c.type.match(/(application\/xhtml\+xml|text\/html)/)) {
            urls.push(c.url);
243
244

            this.contentTypes.push(c.type.split('type=')[1]);
245
246
247
248
          }
        });
      }
      return urls;
249
    },
schneider210's avatar
schneider210 committed
250
251
252
253
254
255
    /**
      * fetch all data provided on 'item level'
      * caller: *mounted-hook*, *getManifest()*
      *
      * @param string url
      */
256
    getItemData(url) {
257
258
      this.request(url)
        .then((data) => {
259
          this.item = data;
260

261
262
          const previousManifest = (this.contentUrls[0] || '').split('/').pop().split('-')[0];

263
          this.contentUrls = this.getContentUrls(data.content);
264
265
266
267
268
269
270

          const currentManifest = this.contentUrls[0].split('/').pop().split('-')[0];

          if (previousManifest !== currentManifest) {
            this.$root.$emit('manifest-changed');
          }

271
          this.imageurl = data.image.id || '';
272
273
274
275

          if (data.annotationCollection) {
            this.getAnnotations(data.annotationCollection);
          }
276
277
278
279
280
281
282
283
284
285
286
287

          fetch(this.imageurl).then((response) => {
            if (response.status === 200 || response.status === 201) {
              this.errormessage = false;
            } else {
              // for vpn error.
              this.errormessage = true;
            }
          }).catch(() => {
            // for CORS error.
            this.errormessage = true;
          });
288
289
        });
    },
schneider210's avatar
schneider210 committed
290
291
292
293
294
295
296
    /**
      * caller: *getItemUrls()*
      *
      * @param string nodelabel
      *
      * @return number idx
      */
297
298
299
300
301
302
303
304
305
    getItemIndex(nodelabel) {
      let idx = 0;
      this.itemurls.forEach((item, index) => {
        if (item === nodelabel) {
          idx = index;
        }
      });
      return idx;
    },
306
307
308
309
310
311
312
313
314
315
316
    /**
      * extract the 'label part' of the itemurl
      * caller: *getItemUrls()*
      *
      * @param string itemurl
      *
      * @return string 'label part'
      */
    getItemLabel(itemurl) {
      return itemurl.replace(/.*-(.*)\/latest.*$/, '$1');
    },
schneider210's avatar
schneider210 committed
317
318
319
320
321
322
323
324
325
    /**
      * get all itemurls hosted by each manifest's sequence to populate the aprropriate tree node
      * caller: *getManifest()*
      *
      * @param array sequence
      * @param string label
      *
      * @return array urls
      */
326
    getItemUrls(sequence) {
327
      const urls = [];
328

329
330
      sequence.forEach((item) => {
        const itemLabel = this.getItemLabel(item.id);
331

332
333
334
335
336
337
338
339
340
341
342
343
        urls.push({
          label: item.id,
          'label-key': `${itemLabel}`,
          labelSheet: true,
          handler: (node) => {
            if (this.itemurl === node.label) {
              return;
            }
            this.loaded = false;
            this.$router.push({
              query: { ...this.$route.query, itemurl: node.label },
            });
344
          },
345
        });
346
347
348
      });
      return urls;
    },
schneider210's avatar
schneider210 committed
349
350
351
352
353
354
355
356
    /**
      * get the collection label, if provided; otherwise get the manifest label
      * caller: *getCollection()*, *getManifest()*
      *
      * @param object data
      *
      * @return string 'label'
      */
357
358
    getLabel(data) {
      if (Object.keys(this.collection).length) {
359
360
361
        return data.title && data.title[0].title
          ? data.title[0].title
          : data.label;
362
      }
363
364
365
      return data.label
        ? data.label
        : 'Manifest <small>(No label available)</small>';
366
    },
schneider210's avatar
schneider210 committed
367
368
369
370
371
372
    /**
      * get all the data provided on 'manifest level'
      * caller: *init()*, *getCollection()*
      *
      * @param string url
      */
373
374
    async getManifest(url) {
      const data = await this.request(url);
375

376
377
      // if the entrypoint points to a single manifest, initialize the tree
      if (this.isCollection === false) {
378
379
380
381
382
        this.tree.push({
          label: '',
          'label-key': this.config.labels.manifest,
          children: [],
        });
383
      }
384

385
386
387
388
389
390
391
392
393
      if (!Array.isArray(data.sequence)) {
        data.sequence = [data.sequence];
      }

      if (data.sequence[0] !== 'undefined') {
        data.sequence.map((seq) => this.itemurls.push(seq.id));
      }
      this.manifests.push(data);

394
395
396
397
398
399
      this.tree[0].children.push({
        children: this.getItemUrls(data.sequence, data.label),
        label: data.label,
        'label-key': data.label,
        handler: (node) => {
          this.$root.$emit('update-tree-knots', node.label);
400
        },
401
402
        selectable: false,
      });
403
    },
schneider210's avatar
schneider210 committed
404
405
406
407
408
409
410
    /**
      * caller: *getItemUrls()*
      *
      * @param string label
      *
      * @return number index
      */
411
    getSequenceIndex(label) {
412
413
      let index = 0;
      this.manifests.forEach((manifest, idx) => {
414
        if (manifest.label === label) {
415
416
417
418
419
          index = idx;
        }
      });
      return index;
    },
schneider210's avatar
schneider210 committed
420
421
422
423
424
425
    /**
      * decide whether to start with a collection or a single manifest
      * caller: *created-hook*
      *
      * @return function getCollection() | getManifest()
      */
426
427
428
429
430
    init() {
      return this.config.entrypoint.match(/collection.json\s?$/)
        ? this.getCollection(this.config.entrypoint)
        : this.getManifest(this.config.entrypoint);
    },
431

432
433
434
435
    oncontentindexchange(index) {
      this.contentindex = index;
    },

436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
    onItemUrlChange() {
      if (this.loaded) {
        return;
      }

      this.itemurl = this.$route.query.itemurl;

      if (!this.itemurl) {
        return;
      }

      const item = this.manifests.find((manifest) => manifest.sequence.find((manifestItem) => manifestItem.id === this.itemurl));

      if (!item) {
        return;
      }

      const { label } = item;
      const seqIdx = this.getSequenceIndex(label);

      treestore.updateselectedtreeitem(this.itemurl);
      treestore.updatetreesequence(seqIdx);
      this.$root.$emit('update-item', this.itemurl, seqIdx);
      this.$root.$emit('update-item-index', this.getItemIndex(this.itemurl));
      this.$root.$emit('update-sequence-index', seqIdx);

      const treeDom = document.getElementById(this.itemurl);

      if (treeDom) {
        treeDom.scrollIntoView();
      }

      // NOTE: Set imageurl to an empty string. Otherwise, if there is no corresponding image,
      // the "preceding" image according to the "preceding" item will be shown.
      this.imageurl = '';
      this.getItemData(this.itemurl);
      this.loaded = true;
    },
474
  },
Mathias Goebel's avatar
Mathias Goebel committed
475
476
};
</script>
477
478

<style scoped>
479
.root {
480
481
482
  display: flex;
  flex: 1;
  flex-direction: column;
483
  font-size: 16px;
484
485
486
  overflow: hidden;
}

Mathias Goebel's avatar
Mathias Goebel committed
487
.viewport {
488
  height: 100vh;
489
490
}
</style>