App.vue 12.8 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
      :projectcolors="config.colors"
18
    />
19

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

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

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

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

nwindis's avatar
nwindis committed
99
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
    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
      */
  },
126
  methods: {
127
128
129
    defaultView() {
      this.loaded = false;
    },
schneider210's avatar
schneider210 committed
130
131
132
133
134
135
136
137
138
139
    /**
      * 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
      */
140
141
    async request(url, responsetype = 'json') {
      const response = await fetch(url);
142
143
144
      const data = await (responsetype === 'text'
        ? response.text()
        : response.json());
145
146
147

      return data;
    },
148
149
150
151
152
153

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

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

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

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

        if (current.annotationPage.items.length) {
171
          this.annotations = current.annotationPage.items.map((x) => ({ ...x, targetId: this.stripTargetId(x, true) }));
172
173
174
175
176
        } else {
          this.annotations = [];
        }
      } catch (err) {
        this.annotations = [];
177
      } finally {
178
        this.isLoading = true;
179
      }
180
    },
schneider210's avatar
schneider210 committed
181
182
183
184
185
186
187
188
    /**
      * 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
      */
189
    async getCollection(url) {
190
191
      this.isCollection = true;

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

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

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

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

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

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

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

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

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

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

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

270
271
272
          if (data.annotationCollection) {
            this.getAnnotations(data.annotationCollection);
          }
273

274
275
276
277
278
279
280
281
282
283
284
          if (data.image) {
            this.imageurl = data.image.id || '';
            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.
285
              this.errormessage = true;
286
287
            });
          }
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
404
405
406
407

      if (this.manifests?.[0]?.sequence?.[0]?.id && !this.$route.query.itemurl) {
        this.loaded = false;
        this.$router.push({ query: { ...this.$route.query, itemurl: this.manifests?.[0]?.sequence?.[0]?.id } });
      }
408
    },
schneider210's avatar
schneider210 committed
409
410
411
412
413
414
415
    /**
      * caller: *getItemUrls()*
      *
      * @param string label
      *
      * @return number index
      */
416
    getSequenceIndex(label) {
417
418
      let index = 0;
      this.manifests.forEach((manifest, idx) => {
419
        if (manifest.label === label) {
420
421
422
423
424
          index = idx;
        }
      });
      return index;
    },
schneider210's avatar
schneider210 committed
425
426
427
428
429
430
    /**
      * decide whether to start with a collection or a single manifest
      * caller: *created-hook*
      *
      * @return function getCollection() | getManifest()
      */
431
432
433
434
435
    init() {
      return this.config.entrypoint.match(/collection.json\s?$/)
        ? this.getCollection(this.config.entrypoint)
        : this.getManifest(this.config.entrypoint);
    },
436

437
438
439
440
    oncontentindexchange(index) {
      this.contentindex = index;
    },

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
    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) {
470
        treeDom.scrollIntoView({ block: 'center' });
471
472
473
474
475
476
477
478
      }

      // 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;
    },
479
  },
Mathias Goebel's avatar
Mathias Goebel committed
480
481
};
</script>
482
483

<style scoped>
484
.root {
485
486
487
  display: flex;
  flex: 1;
  flex-direction: column;
488
  font-size: 16px;
489
490
491
  overflow: hidden;
}

Mathias Goebel's avatar
Mathias Goebel committed
492
.viewport {
493
  height: 100vh;
494
495
}
</style>