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
      :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
          this.imageurl = data.image.id || '';
271
272
273
274

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

          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;
          });
287
288
        });
    },
schneider210's avatar
schneider210 committed
289
290
291
292
293
294
295
    /**
      * caller: *getItemUrls()*
      *
      * @param string nodelabel
      *
      * @return number idx
      */
296
297
298
299
300
301
302
303
304
    getItemIndex(nodelabel) {
      let idx = 0;
      this.itemurls.forEach((item, index) => {
        if (item === nodelabel) {
          idx = index;
        }
      });
      return idx;
    },
305
306
307
308
309
310
311
312
313
314
315
    /**
      * 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
316
317
318
319
320
321
322
323
324
    /**
      * 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
      */
325
    getItemUrls(sequence) {
326
      const urls = [];
327

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

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

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

384
385
386
387
388
389
390
391
392
      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);

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

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

435
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
    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;
    },
473
  },
Mathias Goebel's avatar
Mathias Goebel committed
474
475
};
</script>
476
477

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

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