@museumwnf/viewer-core 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -566,6 +566,9 @@ below for whichever future site needs its own).
|
|
|
566
566
|
| `useGalleryPartner(data, collection, config)` / `useExhibitionPartner(data, config)` | `data`, and (gallery only) `collection` for its `tile` | `partnerList`/`partnerListSpec`, `partnerSheet`/`partnerSheetSpec`, and (gallery only) `partnerObjects` — the exhibition shape's `partnerSpecs.js` never carried an objects-page spec |
|
|
567
567
|
| `useGallerySheet(data, config)` / `useExhibitionSheet(data, config)` | `data`; `config` unused today | `{ itemSheet }`, the `RecordView` spec — the exhibition shape's `citation.project` reads `data.projectName`, absent from the gallery shape |
|
|
568
568
|
| `PAGE_SIZE`, `DATE_MODE`, `FACET_CATEGORIES`, `FACET_LABEL_KEYS`, `useFacetLabels()` | — | the five THG catalogue facets, shared verbatim by both families (nine tiles a page, containment dates, `type`/`dynasty`/`subject`/`material`/`artist`) |
|
|
569
|
+
| `useGalleryItemDetail(data, timeline)` / `useExhibitionItemDetail(data, timeline)` | `data`, the family's `timeline` object; the site's `projectColors` and `noticeProjects` through `useSiteConfig()`, read lazily | `{ itemDetail }`, the item page's `RecordSheetView` spec: the family sheet plus the source-database chip, the Explore-partner notice, the holding-museum link (none for an exhibition's hidden partner), and the related block's labels, outside-reference chips, dynasty popouts and timeline popout. The exhibition gives a record with no project the `mwnf-chip--Explore` chip, and its "search this period" link carries only the timeline controls' keys |
|
|
570
|
+
| `useExhibitionThemes(data)` | `data` | the exhibition's themes: `themeTree` (a `useCollectionTree` over themes.json, `entity: 'themes'`), `themes`, `aboutTheme`, `listedThemes`, `themeByRouteId`/`themeRouteId` (legacy's `display_order - 1`), `romanFor`, `themePictures`, `pictureById`, `themeText`/`pictureText`, `owningTheme`, `subIndexOf`, `pictureParent`, `itemDetailString`, and `useThemePictures(node)`, the live `pictures` `PictureGallery`/`PictureNarrative` read |
|
|
571
|
+
| `buildThemePictures(nodePictures, deps)`, `romanFor(displayOrder)` | — | the pure halves of the above: a node's pictures with their `related`/`backRelated` links resolved across the whole tree, and legacy's Roman theme numeral (display order 2 is "I") |
|
|
569
572
|
|
|
570
573
|
### The declaration outside a component
|
|
571
574
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { eraLabel, roundOutward } from '../../catalogue/dates.js'
|
|
2
|
+
import { useSiteConfig } from '../../siteConfig.js'
|
|
3
|
+
import { useExhibitionSheet } from './useExhibitionSheet.js'
|
|
4
|
+
|
|
5
|
+
// An exhibition's item page, as viewer-layout's `RecordSheetView` spec
|
|
6
|
+
// (inventory-app#2053): `useExhibitionSheet`'s field sheet, plus the blocks
|
|
7
|
+
// the view reads through `spec.sourceDatabase`, `spec.notice`, `spec.museum`
|
|
8
|
+
// and `spec.related.*`. The six exhibitions each carried this layer as their
|
|
9
|
+
// own `composables/sheet.js`, the same code in all six.
|
|
10
|
+
//
|
|
11
|
+
// What stays an exhibition's own is read from its `dataset.config.js`
|
|
12
|
+
// through `useSiteConfig()`, lazily, as the galleries do:
|
|
13
|
+
//
|
|
14
|
+
// * `projectColors`, project id → one of viewer-layout's `mwnf-chip--*`
|
|
15
|
+
// classes, for the source-database chip and an outside reference's chip;
|
|
16
|
+
// a record with no project at all is an Explore record, whose chip is
|
|
17
|
+
// `mwnf-chip--Explore`;
|
|
18
|
+
// * `noticeProjects`, the projects whose sheets still carry legacy's
|
|
19
|
+
// Explore-partner notice.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The exhibition's item-page spec, over its data layer's `data` and its
|
|
23
|
+
* timeline layer's `timeline` (`useExhibitionTimeline`).
|
|
24
|
+
*
|
|
25
|
+
* Reads `data.defaultLang`, `data.dynastyById`, `data.isExploreRecord`,
|
|
26
|
+
* `data.isHiddenPartner`, `data.labelOf`, `data.partnerById`,
|
|
27
|
+
* `data.partnerRoute`, `data.translations`, what `useExhibitionSheet` reads,
|
|
28
|
+
* and `timeline.countryIdForCode`, `timeline.findEvents`,
|
|
29
|
+
* `timeline.hasTimeline`, `timeline.timelineCountries`, `timeline.timelineSpec`.
|
|
30
|
+
*/
|
|
31
|
+
export function useExhibitionItemDetail(data, timeline) {
|
|
32
|
+
const {
|
|
33
|
+
defaultLang, dynastyById, isExploreRecord, isHiddenPartner, labelOf, partnerById, partnerRoute, translations,
|
|
34
|
+
} = data
|
|
35
|
+
const { countryIdForCode, findEvents, hasTimeline, timelineCountries, timelineSpec } = timeline
|
|
36
|
+
const { itemSheet: platformSheet } = useExhibitionSheet(data)
|
|
37
|
+
|
|
38
|
+
function chipClass(record) {
|
|
39
|
+
return useSiteConfig().projectColors?.[record.project_id] ?? (isExploreRecord(record) ? 'mwnf-chip--Explore' : null)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dynastyTr(dynasty, language) {
|
|
43
|
+
return translations('dynasties', language)[dynasty.id] ?? translations('dynasties', defaultLang)[dynasty.id] ?? {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Legacy popped out a dynasty only when it had a history to show.
|
|
47
|
+
function dynastiesWithHistory(record, language) {
|
|
48
|
+
return (record.dynasty_ids ?? [])
|
|
49
|
+
.map((id) => dynastyById.value.get(id))
|
|
50
|
+
.filter((d) => d && dynastyTr(d, language).history)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The country the "Timeline for this item" popout opens on: the legacy
|
|
54
|
+
// two-letter code whose id is the record's own country.
|
|
55
|
+
function countryCodeOf(countryId) {
|
|
56
|
+
for (const [code] of timelineCountries.value) {
|
|
57
|
+
if (countryIdForCode(code) === countryId) return code
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The popout's "search this period" link: the timeline results filtered to
|
|
63
|
+
// the country and the period, under the keys the results page's controls
|
|
64
|
+
// read — never a key it has no control for (a local timeline has no
|
|
65
|
+
// country), which it would ignore (inventory-app#2022).
|
|
66
|
+
function periodSearch(countryCode, range) {
|
|
67
|
+
const keys = new Set((timelineSpec.value?.controls ?? []).map((control) => control.key))
|
|
68
|
+
const query = { country: countryCode, begin: range[0], end: range[1] }
|
|
69
|
+
return { name: 'timeline-results', query: Object.fromEntries(Object.entries(query).filter(([key]) => keys.has(key))) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const itemDetail = {
|
|
73
|
+
...platformSheet,
|
|
74
|
+
|
|
75
|
+
sourceDatabase: {
|
|
76
|
+
chipClass: (record) => chipClass(record),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
notice: {
|
|
80
|
+
show: (record) => (useSiteConfig().noticeProjects ?? []).includes(record.project_id),
|
|
81
|
+
label: 'exhibition.item.explorePartnerNote',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// A hidden museum keeps its name on the sheet and loses the link: it has
|
|
85
|
+
// no page to link to.
|
|
86
|
+
museum: {
|
|
87
|
+
route: (partnerId) => {
|
|
88
|
+
const partner = partnerById.value.get(partnerId)
|
|
89
|
+
if (!partner || isHiddenPartner(partner)) return null
|
|
90
|
+
return partnerRoute(partner)
|
|
91
|
+
},
|
|
92
|
+
label: (partnerId) => labelOf('partners', partnerId),
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
related: {
|
|
96
|
+
...platformSheet.related,
|
|
97
|
+
title: 'exhibition.related.title',
|
|
98
|
+
description: 'exhibition.related.description',
|
|
99
|
+
outsideChip: (ref) => chipClass(ref),
|
|
100
|
+
notInPackageLabel: 'exhibition.results.notInThisExhibition',
|
|
101
|
+
artisticIntroductionLabel: 'exhibition.nav.artisticIntroduction',
|
|
102
|
+
databaseLabel: 'exhibition.search.relatedDatabase',
|
|
103
|
+
overallDatabase: { label: 'exhibition.search.overallDatabase', linkLabel: 'exhibition.nav.overallDatabase' },
|
|
104
|
+
onDisplayIn: { linkPendingLabel: 'exhibition.item.linkPending' },
|
|
105
|
+
dynasties: (record, language) => ({
|
|
106
|
+
records: dynastiesWithHistory(record, language),
|
|
107
|
+
tr: (d) => dynastyTr(d, language),
|
|
108
|
+
}),
|
|
109
|
+
// Withheld when the exhibition reports no chronology, or the item has
|
|
110
|
+
// no date to anchor a range on: legacy then printed no "timeline"
|
|
111
|
+
// anywhere on the sheet.
|
|
112
|
+
timeline: (record, ctx) => {
|
|
113
|
+
if (!hasTimeline.value) return null
|
|
114
|
+
const range = roundOutward(record.start_date, record.end_date)
|
|
115
|
+
if (range[0] == null) return null
|
|
116
|
+
return {
|
|
117
|
+
heading: 'exhibition.section.timeline',
|
|
118
|
+
countries: timelineCountries.value.map(([value, label]) => ({ value, label })),
|
|
119
|
+
defaultCountry: () => countryCodeOf(record.country_id) ?? 'all',
|
|
120
|
+
events: (countryCode) => findEvents({ countryCode, start: range[0], end: range[1] }),
|
|
121
|
+
range,
|
|
122
|
+
era: (year) => eraLabel(year, ctx.t),
|
|
123
|
+
searchTo: periodSearch,
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { itemDetail }
|
|
130
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { useI18n } from '../../i18n/index.js'
|
|
3
|
+
import { entityRef } from '../../composables/useEntities.js'
|
|
4
|
+
import { useCollectionTree } from '../../record/collectionTree.js'
|
|
5
|
+
|
|
6
|
+
// An exhibition's themes: the tour its theme pages, its themes list and its
|
|
7
|
+
// theme galleries read (inventory-app#2053). The six exhibitions each carried
|
|
8
|
+
// this as `composables/themes.js`, `useThemePresentation.js` and
|
|
9
|
+
// `useThemePictures.js`, the same code in all six.
|
|
10
|
+
//
|
|
11
|
+
// themes.json is the ordered tree: top-level themes, each with its sub-themes
|
|
12
|
+
// and its curated picture selections. `useCollectionTree({ source: 'themes' })`
|
|
13
|
+
// walks it, and its `previous`/`next` (depth-first) is legacy's tour order:
|
|
14
|
+
// About → theme 1 overview → its sub-themes → theme 2 overview → ….
|
|
15
|
+
//
|
|
16
|
+
// Two rules the data fixes rather than taste:
|
|
17
|
+
//
|
|
18
|
+
// * Theme 0 ("About the Exhibition") is an ordinary top-level theme legacy
|
|
19
|
+
// renders at /about and skips on /themes. Its display order is 1, so the
|
|
20
|
+
// themes list starts at display order 2 and numbers those "Theme I"
|
|
21
|
+
// upwards — which is why `romanFor` subtracts one.
|
|
22
|
+
// * The theme id in the keyspace is not the display order. The route
|
|
23
|
+
// carries `display_order - 1`, as legacy's `theme.display - 1` did, so a
|
|
24
|
+
// legacy address pasted after the `#` lands on the same theme.
|
|
25
|
+
//
|
|
26
|
+
// A theme's selections point at `picture` items, which are not members of the
|
|
27
|
+
// exhibition and so not in items.json — only their parents are. Every label a
|
|
28
|
+
// theme page shows (name, holding museum, location, country) is read off the
|
|
29
|
+
// parent record, which is also what "see the full record" links to.
|
|
30
|
+
// `parent_in_package` says whether the parent is there at all: a curated
|
|
31
|
+
// picture whose parent was not exported still renders its own image.
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Legacy numbered its themes in Roman numerals, counting from the About
|
|
35
|
+
* theme. EssayView's `numbering: 'roman'` counts a node's position among its
|
|
36
|
+
* siblings, which it derives from `tree.root` — always null for a themes.json
|
|
37
|
+
* package — so it would number every top-level theme "I". The numeral an
|
|
38
|
+
* exhibition shows is always the owning theme's, so one function serves
|
|
39
|
+
* themes and sub-themes.
|
|
40
|
+
*/
|
|
41
|
+
export function romanFor(displayOrder) {
|
|
42
|
+
const lookup = [
|
|
43
|
+
['M', 1000], ['CM', 900], ['D', 500], ['CD', 400], ['C', 100], ['XC', 90],
|
|
44
|
+
['L', 50], ['XL', 40], ['X', 10], ['IX', 9], ['V', 5], ['IV', 4], ['I', 1],
|
|
45
|
+
]
|
|
46
|
+
let n = displayOrder - 1
|
|
47
|
+
let out = ''
|
|
48
|
+
for (const [sym, value] of lookup) {
|
|
49
|
+
while (n >= value) { out += sym; n -= value }
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The `pictures` PictureGallery/PictureNarrative read (viewer-layout's
|
|
56
|
+
* docs/theme-components.md) for one node, as a pure function of its data so
|
|
57
|
+
* the graph rules are tested without Vue or a package.
|
|
58
|
+
*
|
|
59
|
+
* A picture's `related`/`backRelated` targets resolve against `pictureById`,
|
|
60
|
+
* the WHOLE tree's picture index, not the node's own selections:
|
|
61
|
+
* `theme_item_related` rows can name a picture curated under another theme,
|
|
62
|
+
* and scoping the lookup to the node is what used to drop such a link.
|
|
63
|
+
*
|
|
64
|
+
* @param nodePictures - the node's own selections, in curator order
|
|
65
|
+
* @param deps.pictureById - Map<pictureId, rawPicture> for every picture in the tree
|
|
66
|
+
* @param deps.resolvePicture - (raw) => `{ id, image, imageAlt, name, detail, to }`,
|
|
67
|
+
* the node-independent shape, used for the node's pictures and every target
|
|
68
|
+
* @param deps.imageCaptionFor - (raw) => this node's own caption for the picture
|
|
69
|
+
* @param deps.fieldsFor - (raw) => this node's panel `fields` for the picture
|
|
70
|
+
* @param deps.relationText - (link) => the forward relation's text
|
|
71
|
+
* @param deps.reciprocalText - (link) => the backward relation's text
|
|
72
|
+
*/
|
|
73
|
+
export function buildThemePictures(nodePictures, deps) {
|
|
74
|
+
const {
|
|
75
|
+
pictureById: byId, resolvePicture, imageCaptionFor, fieldsFor, relationText, reciprocalText,
|
|
76
|
+
} = deps
|
|
77
|
+
|
|
78
|
+
return (nodePictures ?? []).map((raw) => {
|
|
79
|
+
const related = (raw.related ?? [])
|
|
80
|
+
.map((link) => {
|
|
81
|
+
const target = byId.get(link.picture_item_id)
|
|
82
|
+
return target ? { picture: resolvePicture(target), text: relationText(link) } : null
|
|
83
|
+
})
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
|
|
86
|
+
// Every other picture anywhere in the tree whose own `related` names this
|
|
87
|
+
// one, so a target on another theme's page still shows "related to" back.
|
|
88
|
+
const backRelated = []
|
|
89
|
+
for (const other of byId.values()) {
|
|
90
|
+
for (const link of other.related ?? []) {
|
|
91
|
+
if (link.picture_item_id === raw.picture_item_id) {
|
|
92
|
+
backRelated.push({ picture: resolvePicture(other), reciprocalText: reciprocalText(link) })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...resolvePicture(raw),
|
|
99
|
+
imageCaption: imageCaptionFor(raw),
|
|
100
|
+
fields: fieldsFor(raw),
|
|
101
|
+
// Not part of the components' contract: the theme page turns a
|
|
102
|
+
// selection back into legacy's `?image=<display_order>` with it.
|
|
103
|
+
displayOrder: raw.display_order,
|
|
104
|
+
related,
|
|
105
|
+
backRelated,
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The exhibition's themes, over its data layer's `data`.
|
|
112
|
+
*
|
|
113
|
+
* Reads `data.defaultLang`, `data.itemById`, `data.itemRoute`, `data.labelOf`,
|
|
114
|
+
* `data.mdInline`, `data.tr`.
|
|
115
|
+
*/
|
|
116
|
+
export function useExhibitionThemes(data) {
|
|
117
|
+
const { defaultLang, itemById, itemRoute, labelOf, mdInline, tr } = data
|
|
118
|
+
|
|
119
|
+
// EssayView reads `spec.tree.entity` to know where a node's own text lives,
|
|
120
|
+
// even for a tree built here rather than from its `{ themes: true }`
|
|
121
|
+
// shorthand; without it, every title/quote/body lookup would read
|
|
122
|
+
// 'collections', not a themes.json package's entity.
|
|
123
|
+
const themeTree = useCollectionTree({ source: 'themes', entity: 'themes' })
|
|
124
|
+
themeTree.entity = 'themes'
|
|
125
|
+
|
|
126
|
+
// The raw top-level array, in the order the themes list and the tour read
|
|
127
|
+
// them: the tree's own `root` is always null for this shape, so it cannot
|
|
128
|
+
// hand back every top-level theme itself.
|
|
129
|
+
const rawThemes = entityRef('themes')
|
|
130
|
+
const themes = computed(() => rawThemes.value ?? [])
|
|
131
|
+
|
|
132
|
+
const aboutTheme = computed(() => themes.value.find((t) => t.display_order === 1) ?? null)
|
|
133
|
+
|
|
134
|
+
/** The themes the themes page lists: everything after the About theme. */
|
|
135
|
+
const listedThemes = computed(() => themes.value.filter((t) => t.display_order > 1))
|
|
136
|
+
|
|
137
|
+
/** Route id → theme: legacy's `/theme/:id` carries `display_order - 1`. */
|
|
138
|
+
function themeByRouteId(id) {
|
|
139
|
+
const n = Number(id)
|
|
140
|
+
return themes.value.find((t) => t.display_order - 1 === n) ?? null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function themeRouteId(theme) {
|
|
144
|
+
return (theme?.display_order ?? 1) - 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A node's picture selections, in curator order. */
|
|
148
|
+
function themePictures(theme) {
|
|
149
|
+
return [...(theme?.pictures ?? [])].sort((a, b) => (a.display_order ?? 0) - (b.display_order ?? 0))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every picture selection in the tree, by its own id (not a catalogue id). */
|
|
153
|
+
const pictureById = computed(() => {
|
|
154
|
+
const map = new Map()
|
|
155
|
+
for (const node of themeTree.byId.value.values()) {
|
|
156
|
+
for (const picture of node.pictures ?? []) map.set(picture.picture_item_id, picture)
|
|
157
|
+
}
|
|
158
|
+
return map
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// translations/themes.<lang>.json is keyed two ways: by theme id for the
|
|
162
|
+
// theme's own title/quote/presentation, and by `<theme id>/<picture item
|
|
163
|
+
// id>` for one picture's curated text in that theme — the same picture in
|
|
164
|
+
// two themes carries two descriptions.
|
|
165
|
+
function themeText(theme, lang = defaultLang) {
|
|
166
|
+
return tr('themes', theme?.id, lang)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pictureText(theme, picture, lang = defaultLang) {
|
|
170
|
+
if (!theme?.id || !picture?.picture_item_id) return {}
|
|
171
|
+
return tr('themes', `${theme.id}/${picture.picture_item_id}`, lang)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The top-level theme that owns a node: the node itself, or its parent. */
|
|
175
|
+
function owningTheme(node) {
|
|
176
|
+
if (!node) return null
|
|
177
|
+
const parents = themeTree.parents(node.id)
|
|
178
|
+
return parents.length ? parents[0] : node
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A node's 1-based position among its owning theme's sub-themes; null for the theme itself. */
|
|
182
|
+
function subIndexOf(node) {
|
|
183
|
+
if (!node) return null
|
|
184
|
+
const owner = owningTheme(node)
|
|
185
|
+
if (!owner || owner.id === node.id) return null
|
|
186
|
+
const index = themeTree.children(owner.id).findIndex((child) => child.id === node.id)
|
|
187
|
+
return index === -1 ? null : index + 1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A picture selection's parent record, or null when it is not a member. */
|
|
191
|
+
function pictureParent(picture) {
|
|
192
|
+
if (!picture?.parent_in_package) return null
|
|
193
|
+
return itemById.value.get(picture.parent_item_id) ?? null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Legacy's `itemDetailString`: museum, location, country, blanks dropped. */
|
|
197
|
+
function itemDetailString(item) {
|
|
198
|
+
if (!item) return ''
|
|
199
|
+
const sheet = tr('items', item.id, defaultLang)
|
|
200
|
+
return [
|
|
201
|
+
sheet.holder || labelOf('partners', item.partner_id),
|
|
202
|
+
sheet.location,
|
|
203
|
+
labelOf('countries', item.country_id),
|
|
204
|
+
].filter(Boolean).join(', ')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A node's `pictures`, live: its selections resolved against the parent
|
|
209
|
+
* records, the translations and the current locale. Call it in a
|
|
210
|
+
* component's setup; `node` is a ref.
|
|
211
|
+
*/
|
|
212
|
+
function useThemePictures(node) {
|
|
213
|
+
const { t, locale } = useI18n()
|
|
214
|
+
|
|
215
|
+
function resolvePicture(raw) {
|
|
216
|
+
const parent = pictureParent(raw)
|
|
217
|
+
const name = parent ? labelOf('items', parent.id) : ''
|
|
218
|
+
return {
|
|
219
|
+
id: raw.picture_item_id,
|
|
220
|
+
image: raw.image_url,
|
|
221
|
+
imageAlt: name,
|
|
222
|
+
name: mdInline(name),
|
|
223
|
+
detail: itemDetailString(parent),
|
|
224
|
+
to: parent ? itemRoute(parent) : null,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function imageCaptionFor(raw) {
|
|
229
|
+
if (!node.value?.id) return ''
|
|
230
|
+
return tr('themes', `${node.value.id}/${raw.picture_item_id}`, locale.value).image_caption ?? ''
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Legacy labelled only "also known as"; artist names and dates stand
|
|
234
|
+
// alone, as `fields` rows with an empty label.
|
|
235
|
+
function fieldsFor(raw) {
|
|
236
|
+
const parent = pictureParent(raw)
|
|
237
|
+
if (!parent) return []
|
|
238
|
+
const sheet = tr('items', parent.id, defaultLang)
|
|
239
|
+
const fields = []
|
|
240
|
+
if (sheet.alternate_name) {
|
|
241
|
+
fields.push({ label: `${t('sheet.field.alsoKnownAs')}:`, value: mdInline(sheet.alternate_name) })
|
|
242
|
+
}
|
|
243
|
+
if (parent.artist_names?.length) fields.push({ label: '', value: parent.artist_names.join(', ') })
|
|
244
|
+
if (sheet.dates) fields.push({ label: '', value: sheet.dates })
|
|
245
|
+
return fields
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const relationText = (link) => link?.descriptions?.[locale.value] ?? link?.descriptions?.en ?? ''
|
|
249
|
+
const reciprocalText = (link) => link?.reciprocal_descriptions?.[locale.value] ?? link?.reciprocal_descriptions?.en ?? ''
|
|
250
|
+
|
|
251
|
+
return computed(() => buildThemePictures(themePictures(node.value), {
|
|
252
|
+
pictureById: pictureById.value,
|
|
253
|
+
resolvePicture,
|
|
254
|
+
imageCaptionFor,
|
|
255
|
+
fieldsFor,
|
|
256
|
+
relationText,
|
|
257
|
+
reciprocalText,
|
|
258
|
+
}))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
themeTree,
|
|
263
|
+
themes,
|
|
264
|
+
aboutTheme,
|
|
265
|
+
listedThemes,
|
|
266
|
+
themeByRouteId,
|
|
267
|
+
themeRouteId,
|
|
268
|
+
romanFor,
|
|
269
|
+
themePictures,
|
|
270
|
+
pictureById,
|
|
271
|
+
themeText,
|
|
272
|
+
pictureText,
|
|
273
|
+
owningTheme,
|
|
274
|
+
subIndexOf,
|
|
275
|
+
pictureParent,
|
|
276
|
+
itemDetailString,
|
|
277
|
+
useThemePictures,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { eraLabel, roundOutward } from '../../catalogue/dates.js'
|
|
2
|
+
import { useSiteConfig } from '../../siteConfig.js'
|
|
3
|
+
import { useGallerySheet } from './useGallerySheet.js'
|
|
4
|
+
|
|
5
|
+
// A gallery's item page, as viewer-layout's `RecordSheetView` spec
|
|
6
|
+
// (inventory-app#2054): `useGallerySheet`'s field sheet, plus the blocks the
|
|
7
|
+
// view reads through `spec.sourceDatabase`, `spec.notice`, `spec.museum` and
|
|
8
|
+
// `spec.related.*` — the project chips, the Explore-partner notice, the
|
|
9
|
+
// holding-museum link, and the related block's outside references,
|
|
10
|
+
// cross-database links, dynasty popouts and timeline popout. The 37
|
|
11
|
+
// galleries each carried this layer in their own `composables/gallery.js`,
|
|
12
|
+
// the same code in all 37.
|
|
13
|
+
//
|
|
14
|
+
// What stays a gallery's own is read from its `dataset.config.js` through
|
|
15
|
+
// `useSiteConfig()`, lazily, inside the functions the view calls at render
|
|
16
|
+
// time — `dataset.config.js` is imported before `createViewer()` sets the
|
|
17
|
+
// config, so a read at module level would only ever see the empty default:
|
|
18
|
+
//
|
|
19
|
+
// * `projectColors`, project id → one of viewer-layout's `mwnf-chip--*`
|
|
20
|
+
// classes, for the source-database chip and an outside reference's chip;
|
|
21
|
+
// * `noticeProjects`, the projects whose sheets still carry legacy's
|
|
22
|
+
// Explore-partner notice.
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The gallery's item-page spec, over its data layer's `data` and its
|
|
26
|
+
* timeline layer's `timeline` (`useGalleryTimeline`).
|
|
27
|
+
*
|
|
28
|
+
* Reads `data.defaultLang`, `data.dynastyById`, `data.labelOf`,
|
|
29
|
+
* `data.partnerById`, `data.partnerRoute`, `data.translations`, what
|
|
30
|
+
* `useGallerySheet` reads, and `timeline.countryIdForCode`,
|
|
31
|
+
* `timeline.timelineEvents` (`countries`, `findEvents`).
|
|
32
|
+
*/
|
|
33
|
+
export function useGalleryItemDetail(data, timeline) {
|
|
34
|
+
const { defaultLang, dynastyById, labelOf, partnerById, partnerRoute, translations } = data
|
|
35
|
+
const { countryIdForCode, timelineEvents } = timeline
|
|
36
|
+
const { countries: timelineCountries, findEvents } = timelineEvents
|
|
37
|
+
const { itemSheet: platformSheet } = useGallerySheet(data)
|
|
38
|
+
|
|
39
|
+
const chipClass = (record) => useSiteConfig().projectColors?.[record.project_id] ?? null
|
|
40
|
+
|
|
41
|
+
function museumRoute(partnerId) {
|
|
42
|
+
const partner = partnerById.value.get(partnerId)
|
|
43
|
+
return partner ? partnerRoute(partner) : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dynastyTranslation(dynasty, language) {
|
|
47
|
+
return translations('dynasties', language)[dynasty.id] ?? translations('dynasties', defaultLang)[dynasty.id] ?? {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Legacy popped out a dynasty only when it had a history to show.
|
|
51
|
+
function itemDynasties(record, language) {
|
|
52
|
+
const records = (record.dynasty_ids ?? [])
|
|
53
|
+
.map((id) => dynastyById.value.get(id))
|
|
54
|
+
.filter((d) => d && dynastyTranslation(d, language).history)
|
|
55
|
+
return records.length ? { records, tr: (d) => dynastyTranslation(d, language) } : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The timeline popout, for the record's own date range: the gallery's own
|
|
59
|
+
// countries, events and search route.
|
|
60
|
+
function itemTimeline(record, ctx) {
|
|
61
|
+
const [from, to] = roundOutward(record.start_date, record.end_date)
|
|
62
|
+
if (from == null) return null
|
|
63
|
+
return {
|
|
64
|
+
heading: 'gallery.section.timeline',
|
|
65
|
+
countries: timelineCountries.value.map((c) => ({ value: c.value, label: c.label ?? ctx.t('timeline.form.allCountries') })),
|
|
66
|
+
defaultCountry: () => {
|
|
67
|
+
for (const { value: code } of timelineCountries.value) {
|
|
68
|
+
if (countryIdForCode(code) === record.country_id) return code
|
|
69
|
+
}
|
|
70
|
+
return 'all'
|
|
71
|
+
},
|
|
72
|
+
events: (country) => findEvents({ country, begin: from, end: to }),
|
|
73
|
+
range: [from, to],
|
|
74
|
+
era: (year) => eraLabel(year, ctx.t),
|
|
75
|
+
searchTo: (country, [begin, end]) => ({ name: 'timeline-results', query: { country, begin, end } }),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const itemDetail = {
|
|
80
|
+
...platformSheet,
|
|
81
|
+
sourceDatabase: {
|
|
82
|
+
chipClass,
|
|
83
|
+
},
|
|
84
|
+
notice: {
|
|
85
|
+
show: (record) => (useSiteConfig().noticeProjects ?? []).includes(record.project_id),
|
|
86
|
+
label: 'gallery.item.explorePartnerNote',
|
|
87
|
+
},
|
|
88
|
+
museum: {
|
|
89
|
+
route: (partnerId) => museumRoute(partnerId),
|
|
90
|
+
label: (partnerId) => labelOf('partners', partnerId),
|
|
91
|
+
},
|
|
92
|
+
related: {
|
|
93
|
+
...platformSheet.related,
|
|
94
|
+
title: 'gallery.related.title',
|
|
95
|
+
description: 'gallery.related.description',
|
|
96
|
+
notInPackageLabel: 'gallery.results.notInThisGallery',
|
|
97
|
+
// An outside `related_items` reference carries its `project_id`
|
|
98
|
+
// (inventory-app#1807), so it reads the same `projectColors` map.
|
|
99
|
+
outsideChip: chipClass,
|
|
100
|
+
artisticIntroductionLabel: 'gallery.nav.artisticIntroduction',
|
|
101
|
+
databaseLabel: 'gallery.search.relatedDatabase',
|
|
102
|
+
overallDatabase: { label: 'gallery.search.overallDatabase', linkLabel: 'gallery.nav.overallDatabase' },
|
|
103
|
+
onDisplayIn: { linkPendingLabel: 'gallery.item.linkPending' },
|
|
104
|
+
dynasties: (record, language) => itemDynasties(record, language),
|
|
105
|
+
timeline: itemTimeline,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { itemDetail }
|
|
110
|
+
}
|
package/src/dxa/index.js
CHANGED
|
@@ -26,9 +26,12 @@ export { useGalleryCollection } from './gallery/useGalleryCollection.js'
|
|
|
26
26
|
export { useGalleryTimeline } from './gallery/useGalleryTimeline.js'
|
|
27
27
|
export { useGalleryPartner } from './gallery/useGalleryPartner.js'
|
|
28
28
|
export { useGallerySheet } from './gallery/useGallerySheet.js'
|
|
29
|
+
export { useGalleryItemDetail } from './gallery/useGalleryItemDetail.js'
|
|
29
30
|
|
|
30
31
|
export { useExhibitionData } from './exhibition/useExhibitionData.js'
|
|
31
32
|
export { useExhibitionCollection } from './exhibition/useExhibitionCollection.js'
|
|
32
33
|
export { useExhibitionTimeline } from './exhibition/useExhibitionTimeline.js'
|
|
33
34
|
export { useExhibitionPartner } from './exhibition/useExhibitionPartner.js'
|
|
34
35
|
export { useExhibitionSheet } from './exhibition/useExhibitionSheet.js'
|
|
36
|
+
export { useExhibitionItemDetail } from './exhibition/useExhibitionItemDetail.js'
|
|
37
|
+
export { buildThemePictures, romanFor, useExhibitionThemes } from './exhibition/useExhibitionThemes.js'
|