App.vue 11.4 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>
schneider210's avatar
schneider210 committed
43
import { colors } from 'quasar';
44
import treestore from '@/stores/treestore.js';
45
import Header from '@/components/header.vue';
46
import Panels from '@/mixins/panels';
47

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

schneider210's avatar
schneider210 committed
89
90
    this.$q.dark.set('auto');

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

      return data;
    },
138
139
140
141
142
143

    /**
      * get annotations of the current item
      * caller: *getItemData()*
      * @param string url
      */
144
    async getAnnotations(url) {
145
      this.annotations = [];
146
      this.isLoading = false;
147

148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
      try {
        const annotations = await this.request(url);

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

        const current = await this.request(annotations.annotationCollection.first);

        if (current.annotationPage.items.length) {
          this.annotations = current.annotationPage.items;
        } else {
          this.annotations = [];
        }
      } catch (err) {
        this.annotations = [];
165
      } finally {
166
        this.isLoading = true;
167
      }
168
    },
schneider210's avatar
schneider210 committed
169
170
171
172
173
174
175
176
    /**
      * 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
      */
177
    async getCollection(url) {
178
179
      this.isCollection = true;

180
      const data = await this.request(url);
181

182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
      this.collection = data;
      this.collectiontitle = this.getLabel(data);

      this.tree.push(
        {
          children: [],
          handler: (node) => {
            this.$root.$emit('update-tree-knots', node.label);
          },
          label: this.collectiontitle,
          'label-key': this.collectiontitle,
          selectable: false,
        },
      );

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

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

229
230
231
        content.forEach((c) => {
          if (c.type.match(/(application\/xhtml\+xml|text\/html)/)) {
            urls.push(c.url);
232
233

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

250
          this.contentUrls = this.getContentUrls(data.content);
251
          this.imageurl = data.image.id || '';
252
253
254
255

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

297
298
      sequence.forEach((item) => {
        const itemLabel = this.getItemLabel(item.id);
299

300
301
        urls.push(
          {
302
            label: item.id,
303
304
            'label-key': `${itemLabel}`,
            labelSheet: true,
305
306
307
308
            handler: (node) => {
              if (this.itemurl === node.label) {
                return;
              }
309
              this.loaded = false;
310
              this.$router.push({ query: { ...this.$route.query, itemurl: node.label } });
311
312
313
314
315
316
            },
          },
        );
      });
      return urls;
    },
schneider210's avatar
schneider210 committed
317
318
319
320
321
322
323
324
    /**
      * get the collection label, if provided; otherwise get the manifest label
      * caller: *getCollection()*, *getManifest()*
      *
      * @param object data
      *
      * @return string 'label'
      */
325
326
327
328
329
330
    getLabel(data) {
      if (Object.keys(this.collection).length) {
        return data.title && data.title[0].title ? data.title[0].title : data.label;
      }
      return data.label ? data.label : 'Manifest <small>(No label available)</small>';
    },
schneider210's avatar
schneider210 committed
331
332
333
334
335
336
    /**
      * get all the data provided on 'manifest level'
      * caller: *init()*, *getCollection()*
      *
      * @param string url
      */
337
338
    async getManifest(url) {
      const data = await this.request(url);
339

340
341
342
343
      // if the entrypoint points to a single manifest, initialize the tree
      if (this.isCollection === false) {
        this.tree.push({ label: '', 'label-key': this.config.labels.manifest, children: [] });
      }
344

345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
      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);

      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);
          },
          selectable: false,
        },
      );
365
    },
schneider210's avatar
schneider210 committed
366
367
368
369
370
371
372
    /**
      * caller: *getItemUrls()*
      *
      * @param string label
      *
      * @return number index
      */
373
    getSequenceIndex(label) {
374
375
      let index = 0;
      this.manifests.forEach((manifest, idx) => {
376
        if (manifest.label === label) {
377
378
379
380
381
          index = idx;
        }
      });
      return index;
    },
schneider210's avatar
schneider210 committed
382
383
384
385
386
387
    /**
      * decide whether to start with a collection or a single manifest
      * caller: *created-hook*
      *
      * @return function getCollection() | getManifest()
      */
388
389
390
391
392
    init() {
      return this.config.entrypoint.match(/collection.json\s?$/)
        ? this.getCollection(this.config.entrypoint)
        : this.getManifest(this.config.entrypoint);
    },
393
394
395
396
397
398
399
400
401
402
403
404
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

    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;
    },
432
  },
Mathias Goebel's avatar
Mathias Goebel committed
433
434
};
</script>
435
436

<style scoped>
437
.root {
438
439
440
  display: flex;
  flex: 1;
  flex-direction: column;
441
  font-size: 16px;
442
443
444
  overflow: hidden;
}

Mathias Goebel's avatar
Mathias Goebel committed
445
.viewport {
446
  height: 100vh;
447
448
}
</style>