App.vue 11.6 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
26
        :collection="collection"
        :config="config"
        :contenttypes="contentTypes"
27
        :contenturls="contentUrls"
28
29
        :fontsize="fontsize"
        :imageurl="imageurl"
30
        :isloading="isLoading"
31
32
33
34
35
36
37
38
39
        :item="item"
        :labels="config.labels"
        :manifests="manifests"
        :panels="panels"
        :request="request"
        :tree="tree"
      />
    </q-page-container>
  </q-layout>
Mathias Goebel's avatar
Mathias Goebel committed
40
41
42
</template>

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

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

schneider210's avatar
schneider210 committed
93
94
    this.$q.dark.set('auto');

nwindis's avatar
nwindis committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
    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
      */
  },
122
  methods: {
123
124
125
    defaultView() {
      this.loaded = false;
    },
schneider210's avatar
schneider210 committed
126
127
128
129
130
131
132
133
134
135
    /**
      * 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
      */
136
137
    async request(url, responsetype = 'json') {
      const response = await fetch(url);
138
139
140
      const data = await (responsetype === 'text'
        ? response.text()
        : response.json());
141
142
143

      return data;
    },
144
145
146
147
148
149

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

154
155
156
157
158
159
160
161
      try {
        const annotations = await this.request(url);

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

162
163
164
        const current = await this.request(
          annotations.annotationCollection.first,
        );
165
166

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

188
      const data = await this.request(url);
189

190
191
192
      this.collection = data;
      this.collectiontitle = this.getLabel(data);

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

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

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

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

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

256
          this.contentUrls = this.getContentUrls(data.content);
257
          this.imageurl = data.image.id || '';
258
259
260
261

          if (data.annotationCollection) {
            this.getAnnotations(data.annotationCollection);
          }
262
263
        });
    },
schneider210's avatar
schneider210 committed
264
265
266
267
268
269
270
    /**
      * caller: *getItemUrls()*
      *
      * @param string nodelabel
      *
      * @return number idx
      */
271
272
273
274
275
276
277
278
279
    getItemIndex(nodelabel) {
      let idx = 0;
      this.itemurls.forEach((item, index) => {
        if (item === nodelabel) {
          idx = index;
        }
      });
      return idx;
    },
280
281
282
283
284
285
286
287
288
289
290
    /**
      * 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
291
292
293
294
295
296
297
298
299
    /**
      * 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
      */
300
    getItemUrls(sequence) {
301
      const urls = [];
302

303
304
      sequence.forEach((item) => {
        const itemLabel = this.getItemLabel(item.id);
305

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

350
351
      // if the entrypoint points to a single manifest, initialize the tree
      if (this.isCollection === false) {
352
353
354
355
356
        this.tree.push({
          label: '',
          'label-key': this.config.labels.manifest,
          children: [],
        });
357
      }
358

359
360
361
362
363
364
365
366
367
      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);

368
369
370
371
372
373
      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);
374
        },
375
376
        selectable: false,
      });
377
    },
schneider210's avatar
schneider210 committed
378
379
380
381
382
383
384
    /**
      * caller: *getItemUrls()*
      *
      * @param string label
      *
      * @return number index
      */
385
    getSequenceIndex(label) {
386
387
      let index = 0;
      this.manifests.forEach((manifest, idx) => {
388
        if (manifest.label === label) {
389
390
391
392
393
          index = idx;
        }
      });
      return index;
    },
schneider210's avatar
schneider210 committed
394
395
396
397
398
399
    /**
      * decide whether to start with a collection or a single manifest
      * caller: *created-hook*
      *
      * @return function getCollection() | getManifest()
      */
400
401
402
403
404
    init() {
      return this.config.entrypoint.match(/collection.json\s?$/)
        ? this.getCollection(this.config.entrypoint)
        : this.getManifest(this.config.entrypoint);
    },
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443

    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;
    },
444
  },
Mathias Goebel's avatar
Mathias Goebel committed
445
446
};
</script>
447
448

<style scoped>
449
.root {
450
451
452
  display: flex;
  flex: 1;
  flex-direction: column;
453
  font-size: 16px;
454
455
456
  overflow: hidden;
}

Mathias Goebel's avatar
Mathias Goebel committed
457
.viewport {
458
  height: 100vh;
459
460
}
</style>