App.vue 12.1 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
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
      fontsize: 16,
70
      imageurl: '',
71
      isCollection: false,
72
      isLoading: false,
73
      item: {},
74
75
      itemurl: '',
      itemurls: [],
76
      loaded: false,
77
78
79
80
      manifests: [],
      tree: [],
    };
  },
81
82
83
84
85
86
87
88
89
90
  watch: {
    '$route.query': {
      handler: 'onItemUrlChange',
      immediate: true,
    },
    manifests: {
      handler: 'onItemUrlChange',
      immediate: false,
    },
  },
nwindis's avatar
nwindis committed
91
92
93
94
95
  created() {
    this.getConfig();
    this.init();
    this.itemurls.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));

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

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

      return data;
    },
147
148
149
150
151
152

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

269
          this.imageurl = data.image.id || '';
270
271
272
273

          if (data.annotationCollection) {
            this.getAnnotations(data.annotationCollection);
          }
274
275
        });
    },
schneider210's avatar
schneider210 committed
276
277
278
279
280
281
282
    /**
      * caller: *getItemUrls()*
      *
      * @param string nodelabel
      *
      * @return number idx
      */
283
284
285
286
287
288
289
290
291
    getItemIndex(nodelabel) {
      let idx = 0;
      this.itemurls.forEach((item, index) => {
        if (item === nodelabel) {
          idx = index;
        }
      });
      return idx;
    },
292
293
294
295
296
297
298
299
300
301
302
    /**
      * 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
303
304
305
306
307
308
309
310
311
    /**
      * 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
      */
312
    getItemUrls(sequence) {
313
      const urls = [];
314

315
316
      sequence.forEach((item) => {
        const itemLabel = this.getItemLabel(item.id);
317

318
319
320
321
322
323
324
325
326
327
328
329
        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 },
            });
330
          },
331
        });
332
333
334
      });
      return urls;
    },
schneider210's avatar
schneider210 committed
335
336
337
338
339
340
341
342
    /**
      * get the collection label, if provided; otherwise get the manifest label
      * caller: *getCollection()*, *getManifest()*
      *
      * @param object data
      *
      * @return string 'label'
      */
343
344
    getLabel(data) {
      if (Object.keys(this.collection).length) {
345
346
347
        return data.title && data.title[0].title
          ? data.title[0].title
          : data.label;
348
      }
349
350
351
      return data.label
        ? data.label
        : 'Manifest <small>(No label available)</small>';
352
    },
schneider210's avatar
schneider210 committed
353
354
355
356
357
358
    /**
      * get all the data provided on 'manifest level'
      * caller: *init()*, *getCollection()*
      *
      * @param string url
      */
359
360
    async getManifest(url) {
      const data = await this.request(url);
361

362
363
      // if the entrypoint points to a single manifest, initialize the tree
      if (this.isCollection === false) {
364
365
366
367
368
        this.tree.push({
          label: '',
          'label-key': this.config.labels.manifest,
          children: [],
        });
369
      }
370

371
372
373
374
375
376
377
378
379
      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);

380
381
382
383
384
385
      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);
386
        },
387
388
        selectable: false,
      });
389
    },
schneider210's avatar
schneider210 committed
390
391
392
393
394
395
396
    /**
      * caller: *getItemUrls()*
      *
      * @param string label
      *
      * @return number index
      */
397
    getSequenceIndex(label) {
398
399
      let index = 0;
      this.manifests.forEach((manifest, idx) => {
400
        if (manifest.label === label) {
401
402
403
404
405
          index = idx;
        }
      });
      return index;
    },
schneider210's avatar
schneider210 committed
406
407
408
409
410
411
    /**
      * decide whether to start with a collection or a single manifest
      * caller: *created-hook*
      *
      * @return function getCollection() | getManifest()
      */
412
413
414
415
416
    init() {
      return this.config.entrypoint.match(/collection.json\s?$/)
        ? this.getCollection(this.config.entrypoint)
        : this.getManifest(this.config.entrypoint);
    },
417

418
419
420
421
    oncontentindexchange(index) {
      this.contentindex = index;
    },

422
423
424
425
426
427
428
429
430
431
432
433
434
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
    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;
    },
460
  },
Mathias Goebel's avatar
Mathias Goebel committed
461
462
};
</script>
463
464

<style scoped>
465
.root {
466
467
468
  display: flex;
  flex: 1;
  flex-direction: column;
469
  font-size: 16px;
470
471
472
  overflow: hidden;
}

Mathias Goebel's avatar
Mathias Goebel committed
473
.viewport {
474
  height: 100vh;
475
476
}
</style>