@museumwnf/viewer-core 1.14.0 → 1.15.1
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/LICENSE.md +44 -44
- package/README.md +658 -635
- package/package.json +50 -50
- package/src/AppRoot.vue +58 -58
- package/src/catalogue/dates.js +209 -209
- package/src/catalogue/facets.js +61 -61
- package/src/catalogue/keywordIndex.js +289 -289
- package/src/catalogue/listQuery.js +100 -100
- package/src/catalogue/pagination.js +57 -57
- package/src/catalogue/searchLanguage.js +32 -32
- package/src/catalogue/timeline.js +220 -220
- package/src/composables/useCatalogueData.js +141 -141
- package/src/composables/useDataPackage.js +98 -98
- package/src/composables/useEntities.js +100 -100
- package/src/composables/useRecordLanguage.js +71 -71
- package/src/conventions.js +252 -148
- package/src/createViewer.js +29 -29
- package/src/i18n/I18nText.vue +17 -17
- package/src/i18n/I18nTextInline.vue +17 -17
- package/src/i18n/index.js +88 -88
- package/src/i18n/language.js +127 -127
- package/src/i18n/markdown.js +265 -265
- package/src/index.js +51 -50
- package/src/injectionKeys.js +1 -1
- package/src/languages.js +49 -49
- package/src/legacy/index.js +58 -58
- package/src/record/collectionTree.js +307 -307
- package/src/record/derivations.js +154 -154
- package/src/record/partners.js +87 -87
- package/src/record/recordSheet.js +231 -231
- package/src/rights.js +53 -53
- package/src/router/index.js +138 -138
- package/src/siteConfig.js +37 -37
- package/src/styles/base.css +49 -49
- package/src/testing/checkOfferedLanguages.js +67 -67
- package/src/testing/index.js +3 -3
- package/src/testing/smoke.js +103 -103
- package/src/testing/viteConfig.js +72 -90
- package/src/views/DetailView.vue +53 -53
- package/src/views/HomeView.vue +20 -20
- package/src/views/ListView.vue +61 -61
- package/src/views/NotFoundView.vue +8 -8
- package/src/vite.js +8 -8
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
import { computed } from 'vue'
|
|
2
|
-
import { entityRef } from '../composables/useEntities.js'
|
|
3
|
-
import { eraLabel, yearBucketsFromRange } from './dates.js'
|
|
4
|
-
|
|
5
|
-
// The country chronology (`hcr` in legacy) is one merged, year-ordered list
|
|
6
|
-
// per country, served by legacy `/v2/events` and reproduced identically on
|
|
7
|
-
// every gallery and standalone site. An exhibition can instead carry its own
|
|
8
|
-
// narrative chronology (`source: 'thg_local'` — legacy's `/thg/timeline`),
|
|
9
|
-
// which has no country of its own and is never interleaved with the merge.
|
|
10
|
-
// Sharing History's Timeline additionally splits the merge by the exhibition
|
|
11
|
-
// each event belongs to (`collection_id` on its timeline row, `null` for the
|
|
12
|
-
// Permanent Collection). Every one of the seven websites' Timeline pages is
|
|
13
|
-
// one of these three axes over the same event shape — this is the one place
|
|
14
|
-
// that shape is read.
|
|
15
|
-
//
|
|
16
|
-
// The event overlap rule was triplicated verbatim (islamicart, baroqueart,
|
|
17
|
-
// sharinghistory) and mirrors legacy `class.hcr.inc.php`'s SQL; carpets and
|
|
18
|
-
// water-in-islam wrote a related but distinct copy for the DXA sites. Both
|
|
19
|
-
// are ported here rather than merged into a fourth variant, because the two
|
|
20
|
-
// already ship identical answers on the cases that matter (see the tests):
|
|
21
|
-
// the only real divergence was which of the three label rules a site reached
|
|
22
|
-
// for, which is now one function trying all three in the order the data
|
|
23
|
-
// disambiguates them.
|
|
24
|
-
//
|
|
25
|
-
// What stays out, by design: the DXA legacy 2-letter country code
|
|
26
|
-
// (`legacyCodeOf`/`nameFor` in carpets and water-in-islam) — parsing
|
|
27
|
-
// `backward_compatibility` and falling back to `Intl.DisplayNames` is a
|
|
28
|
-
// concern of the sites still keying their Timeline URLs on that code, not of
|
|
29
|
-
// the merge itself. Those sites pass `countryIdForCode` to translate their
|
|
30
|
-
// code back to the id this module filters on.
|
|
31
|
-
|
|
32
|
-
/** `year_to === 0` mirrors the legacy convention for an open-ended period. */
|
|
33
|
-
export function effectiveYearTo(event) {
|
|
34
|
-
return event?.year_to && event.year_to !== 0 ? event.year_to : null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether `event` falls in `[begin, end]`, either bound optional — legacy's
|
|
39
|
-
* `class.hcr.inc.php` rule: an event with a known end overlaps once that end
|
|
40
|
-
* is past `begin` and its start is before `end`; an open-ended event (no
|
|
41
|
-
* known end) overlaps on its start alone. An event with no `year_from` at
|
|
42
|
-
* all never overlaps a bounded search — there is nothing to compare — but
|
|
43
|
-
* still matches a search with no bound, the same as every dated event does.
|
|
44
|
-
*/
|
|
45
|
-
export function overlapsRange(event, begin, end) {
|
|
46
|
-
const yf = event?.year_from
|
|
47
|
-
const yt = effectiveYearTo(event)
|
|
48
|
-
if (begin == null && end == null) return true
|
|
49
|
-
if (!Number.isFinite(yf)) return false
|
|
50
|
-
if (begin != null && end != null) {
|
|
51
|
-
return yt !== null ? yt > begin && yf < end : yf > begin && yf < end
|
|
52
|
-
}
|
|
53
|
-
if (begin != null) return yt !== null ? yt > begin : yf > begin
|
|
54
|
-
return yf <= end
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The label under an event, trying the three rules the sites share in the
|
|
59
|
-
* order the data disambiguates them: a curated description pair
|
|
60
|
-
* (`date_from_description`/`date_to_description`, for an event whose date
|
|
61
|
-
* is imprecise in a way a bare year cannot say — every package's named event
|
|
62
|
-
* carries a date description too, so the title is not a date), a narrative
|
|
63
|
-
* chronology's own period name (`text.name`, a sort key, only tried when an
|
|
64
|
-
* event has no date description at all), and failing both, the derived year
|
|
65
|
-
* range through `eraLabel` — the one reached for by the worldwide country
|
|
66
|
-
* merge, which has neither.
|
|
67
|
-
*/
|
|
68
|
-
export function eventDateLabel(event, text, t) {
|
|
69
|
-
if (text?.date_from_description) {
|
|
70
|
-
// Legacy printed the pair only when the two differ; the era branch
|
|
71
|
-
// collapses an equal year_from/year_to the same way. Trim before
|
|
72
|
-
// comparing so that whitespace doesn't bloat the display.
|
|
73
|
-
if (text.date_to_description?.trim() === text.date_from_description.trim()) {
|
|
74
|
-
return text.date_from_description
|
|
75
|
-
}
|
|
76
|
-
return text.date_to_description
|
|
77
|
-
? `${text.date_from_description} – ${text.date_to_description}`
|
|
78
|
-
: text.date_from_description
|
|
79
|
-
}
|
|
80
|
-
if (text?.name) return text.name
|
|
81
|
-
if (!Number.isFinite(event?.year_from)) return ''
|
|
82
|
-
const from = eraLabel(event.year_from, t)
|
|
83
|
-
const yt = effectiveYearTo(event)
|
|
84
|
-
return yt !== null ? `${from} – ${eraLabel(yt, t)}` : `${from} –`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The engine of a Timeline results page, one implementation for the three
|
|
89
|
-
* axes legacy split across separate endpoints:
|
|
90
|
-
*
|
|
91
|
-
* - `'country'` — the worldwide country merge (galleries, and the standalone
|
|
92
|
-
* sites that offer only this axis);
|
|
93
|
-
* - `'local'` — an exhibition's own narrative chronology in place of the
|
|
94
|
-
* merge (the `thg_local` timeline row), no country picker;
|
|
95
|
-
* - `'collection'` — the merge again, plus filtering by which exhibition an
|
|
96
|
-
* event's timeline is bound to (`null` = the Permanent Collection), for
|
|
97
|
-
* Sharing History's second axis.
|
|
98
|
-
*
|
|
99
|
-
* `countryLabel(countryId)` names a country for the picker and is the
|
|
100
|
-
* site's own — a country reads as its name, which this module has no way to
|
|
101
|
-
* know. `countryIdForCode`, if given, resolves a `findEvents({ country })`
|
|
102
|
-
* value through the site's own legacy-code table before filtering; omit it
|
|
103
|
-
* and `country` is already the id. `tr(id)` returns an event's translated
|
|
104
|
-
* fields already resolved for entity and language — the site's own `tr`,
|
|
105
|
-
* bound the way every data composable already binds it. `timelinesEntity`
|
|
106
|
-
* / `eventsEntity` name the data package files, for a site that calls its
|
|
107
|
-
* own something else.
|
|
108
|
-
*/
|
|
109
|
-
export function useTimelineEvents({
|
|
110
|
-
scope = 'country',
|
|
111
|
-
countryLabel,
|
|
112
|
-
countryIdForCode,
|
|
113
|
-
tr,
|
|
114
|
-
timelinesEntity = 'timelines',
|
|
115
|
-
eventsEntity = 'timeline_events',
|
|
116
|
-
} = {}) {
|
|
117
|
-
const timelines = entityRef(timelinesEntity)
|
|
118
|
-
const events = entityRef(eventsEntity)
|
|
119
|
-
|
|
120
|
-
// The narrative chronology has no country of its own, so it must be kept
|
|
121
|
-
// out of the merge by `source` rather than by the country filter — on "all
|
|
122
|
-
// countries" it would otherwise interleave into the worldwide list.
|
|
123
|
-
const localTimeline = computed(
|
|
124
|
-
() => (timelines.value ?? []).find((row) => row.source === 'thg_local') ?? null,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
const timelineById = computed(() => new Map((timelines.value ?? []).map((row) => [row.id, row])))
|
|
128
|
-
|
|
129
|
-
/** The events this scope works over — one chronology, never both. */
|
|
130
|
-
const eventPool = computed(() => {
|
|
131
|
-
const all = events.value ?? []
|
|
132
|
-
const local = localTimeline.value
|
|
133
|
-
if (scope === 'local') return local ? all.filter((event) => event.timeline_id === local.id) : []
|
|
134
|
-
return local ? all.filter((event) => event.timeline_id !== local.id) : all
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const hasTimeline = computed(() => eventPool.value.length > 0)
|
|
138
|
-
const usesLocalTimeline = computed(() => scope === 'local' && Boolean(localTimeline.value))
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Countries that actually have a chronology, alphabetized by label, with
|
|
142
|
-
* an "all" marker first — the marker carries no label of its own, so the
|
|
143
|
-
* picker renders it through its own catalogue entry. No picker at all for
|
|
144
|
-
* `'local'`: legacy hides the country select once the section is the
|
|
145
|
-
* exhibition's own chronology, because there is nothing to filter by.
|
|
146
|
-
*/
|
|
147
|
-
const countries = computed(() => {
|
|
148
|
-
if (scope === 'local') return []
|
|
149
|
-
const seen = new Map()
|
|
150
|
-
for (const row of timelines.value ?? []) {
|
|
151
|
-
if (!row.country_id || seen.has(row.country_id)) continue
|
|
152
|
-
seen.set(row.country_id, {
|
|
153
|
-
value: row.country_id,
|
|
154
|
-
label: countryLabel ? countryLabel(row.country_id) : row.country_id,
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
const rows = [...seen.values()].sort((a, b) => String(a.label).localeCompare(String(b.label)))
|
|
158
|
-
return [{ value: 'all', label: null }, ...rows]
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
/** Every event year present in this scope, as the year picker's source range. */
|
|
162
|
-
const yearRange = computed(() => {
|
|
163
|
-
const years = eventPool.value
|
|
164
|
-
.map((event) => event.year_from)
|
|
165
|
-
.filter((value) => Number.isFinite(value) && value !== 0)
|
|
166
|
-
if (!years.length) return [null, null]
|
|
167
|
-
return [Math.min(...years), Math.max(...years)]
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
/** The legacy era buckets over this scope's year range. */
|
|
171
|
-
function yearBuckets(t) {
|
|
172
|
-
const [min, max] = yearRange.value
|
|
173
|
-
return yearBucketsFromRange(min, max, t)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function resolveCountryId(country) {
|
|
177
|
-
if (country == null || country === 'all') return null
|
|
178
|
-
return countryIdForCode ? countryIdForCode(country) : country
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// `null` is Sharing History's Permanent Collection sentinel — a timeline
|
|
182
|
-
// bound to no exhibition — so it must be told apart from "no filter",
|
|
183
|
-
// which `collection: undefined` spells instead.
|
|
184
|
-
function matchesCollection(event, collection) {
|
|
185
|
-
const collectionId = timelineById.value.get(event.timeline_id)?.collection_id ?? null
|
|
186
|
-
return collection === null ? collectionId === null : collectionId === collection
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Legacy's `/events?ic[]=&ya=&yo=` (and Sharing History's exhibition
|
|
191
|
-
* filter alongside it) — this scope's events in `[begin, end]`, for the
|
|
192
|
-
* given country and/or collection, chronological. `display_order` breaks a
|
|
193
|
-
* tie on `year_from`, the order the exhibition itself gave two events of
|
|
194
|
-
* the same year; an event with no year at all has no place in that order
|
|
195
|
-
* and sorts after every dated one.
|
|
196
|
-
*/
|
|
197
|
-
function findEvents({ country, collection, begin, end } = {}) {
|
|
198
|
-
const countryId = resolveCountryId(country)
|
|
199
|
-
const from = begin === '' || begin == null ? null : Number(begin)
|
|
200
|
-
const to = end === '' || end == null ? null : Number(end)
|
|
201
|
-
|
|
202
|
-
return eventPool.value
|
|
203
|
-
.filter((event) => {
|
|
204
|
-
// A narrative chronology's events carry no country_id at all — the
|
|
205
|
-
// filter has nothing to apply itself to, the same reason `countries`
|
|
206
|
-
// renders no picker for this scope.
|
|
207
|
-
if (scope !== 'local' && countryId && event.country_id !== countryId) return false
|
|
208
|
-
if (collection !== undefined && !matchesCollection(event, collection)) return false
|
|
209
|
-
return overlapsRange(event, from, to)
|
|
210
|
-
})
|
|
211
|
-
.map((event) => ({ ...event, text: tr ? tr(event.id) : {} }))
|
|
212
|
-
.sort((a, b) => {
|
|
213
|
-
const ya = Number.isFinite(a.year_from) ? a.year_from : Infinity
|
|
214
|
-
const yb = Number.isFinite(b.year_from) ? b.year_from : Infinity
|
|
215
|
-
return ya - yb || (a.display_order ?? 0) - (b.display_order ?? 0)
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return { countries, yearRange, yearBuckets, hasTimeline, usesLocalTimeline, findEvents }
|
|
220
|
-
}
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { entityRef } from '../composables/useEntities.js'
|
|
3
|
+
import { eraLabel, yearBucketsFromRange } from './dates.js'
|
|
4
|
+
|
|
5
|
+
// The country chronology (`hcr` in legacy) is one merged, year-ordered list
|
|
6
|
+
// per country, served by legacy `/v2/events` and reproduced identically on
|
|
7
|
+
// every gallery and standalone site. An exhibition can instead carry its own
|
|
8
|
+
// narrative chronology (`source: 'thg_local'` — legacy's `/thg/timeline`),
|
|
9
|
+
// which has no country of its own and is never interleaved with the merge.
|
|
10
|
+
// Sharing History's Timeline additionally splits the merge by the exhibition
|
|
11
|
+
// each event belongs to (`collection_id` on its timeline row, `null` for the
|
|
12
|
+
// Permanent Collection). Every one of the seven websites' Timeline pages is
|
|
13
|
+
// one of these three axes over the same event shape — this is the one place
|
|
14
|
+
// that shape is read.
|
|
15
|
+
//
|
|
16
|
+
// The event overlap rule was triplicated verbatim (islamicart, baroqueart,
|
|
17
|
+
// sharinghistory) and mirrors legacy `class.hcr.inc.php`'s SQL; carpets and
|
|
18
|
+
// water-in-islam wrote a related but distinct copy for the DXA sites. Both
|
|
19
|
+
// are ported here rather than merged into a fourth variant, because the two
|
|
20
|
+
// already ship identical answers on the cases that matter (see the tests):
|
|
21
|
+
// the only real divergence was which of the three label rules a site reached
|
|
22
|
+
// for, which is now one function trying all three in the order the data
|
|
23
|
+
// disambiguates them.
|
|
24
|
+
//
|
|
25
|
+
// What stays out, by design: the DXA legacy 2-letter country code
|
|
26
|
+
// (`legacyCodeOf`/`nameFor` in carpets and water-in-islam) — parsing
|
|
27
|
+
// `backward_compatibility` and falling back to `Intl.DisplayNames` is a
|
|
28
|
+
// concern of the sites still keying their Timeline URLs on that code, not of
|
|
29
|
+
// the merge itself. Those sites pass `countryIdForCode` to translate their
|
|
30
|
+
// code back to the id this module filters on.
|
|
31
|
+
|
|
32
|
+
/** `year_to === 0` mirrors the legacy convention for an open-ended period. */
|
|
33
|
+
export function effectiveYearTo(event) {
|
|
34
|
+
return event?.year_to && event.year_to !== 0 ? event.year_to : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether `event` falls in `[begin, end]`, either bound optional — legacy's
|
|
39
|
+
* `class.hcr.inc.php` rule: an event with a known end overlaps once that end
|
|
40
|
+
* is past `begin` and its start is before `end`; an open-ended event (no
|
|
41
|
+
* known end) overlaps on its start alone. An event with no `year_from` at
|
|
42
|
+
* all never overlaps a bounded search — there is nothing to compare — but
|
|
43
|
+
* still matches a search with no bound, the same as every dated event does.
|
|
44
|
+
*/
|
|
45
|
+
export function overlapsRange(event, begin, end) {
|
|
46
|
+
const yf = event?.year_from
|
|
47
|
+
const yt = effectiveYearTo(event)
|
|
48
|
+
if (begin == null && end == null) return true
|
|
49
|
+
if (!Number.isFinite(yf)) return false
|
|
50
|
+
if (begin != null && end != null) {
|
|
51
|
+
return yt !== null ? yt > begin && yf < end : yf > begin && yf < end
|
|
52
|
+
}
|
|
53
|
+
if (begin != null) return yt !== null ? yt > begin : yf > begin
|
|
54
|
+
return yf <= end
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The label under an event, trying the three rules the sites share in the
|
|
59
|
+
* order the data disambiguates them: a curated description pair
|
|
60
|
+
* (`date_from_description`/`date_to_description`, for an event whose date
|
|
61
|
+
* is imprecise in a way a bare year cannot say — every package's named event
|
|
62
|
+
* carries a date description too, so the title is not a date), a narrative
|
|
63
|
+
* chronology's own period name (`text.name`, a sort key, only tried when an
|
|
64
|
+
* event has no date description at all), and failing both, the derived year
|
|
65
|
+
* range through `eraLabel` — the one reached for by the worldwide country
|
|
66
|
+
* merge, which has neither.
|
|
67
|
+
*/
|
|
68
|
+
export function eventDateLabel(event, text, t) {
|
|
69
|
+
if (text?.date_from_description) {
|
|
70
|
+
// Legacy printed the pair only when the two differ; the era branch
|
|
71
|
+
// collapses an equal year_from/year_to the same way. Trim before
|
|
72
|
+
// comparing so that whitespace doesn't bloat the display.
|
|
73
|
+
if (text.date_to_description?.trim() === text.date_from_description.trim()) {
|
|
74
|
+
return text.date_from_description
|
|
75
|
+
}
|
|
76
|
+
return text.date_to_description
|
|
77
|
+
? `${text.date_from_description} – ${text.date_to_description}`
|
|
78
|
+
: text.date_from_description
|
|
79
|
+
}
|
|
80
|
+
if (text?.name) return text.name
|
|
81
|
+
if (!Number.isFinite(event?.year_from)) return ''
|
|
82
|
+
const from = eraLabel(event.year_from, t)
|
|
83
|
+
const yt = effectiveYearTo(event)
|
|
84
|
+
return yt !== null ? `${from} – ${eraLabel(yt, t)}` : `${from} –`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The engine of a Timeline results page, one implementation for the three
|
|
89
|
+
* axes legacy split across separate endpoints:
|
|
90
|
+
*
|
|
91
|
+
* - `'country'` — the worldwide country merge (galleries, and the standalone
|
|
92
|
+
* sites that offer only this axis);
|
|
93
|
+
* - `'local'` — an exhibition's own narrative chronology in place of the
|
|
94
|
+
* merge (the `thg_local` timeline row), no country picker;
|
|
95
|
+
* - `'collection'` — the merge again, plus filtering by which exhibition an
|
|
96
|
+
* event's timeline is bound to (`null` = the Permanent Collection), for
|
|
97
|
+
* Sharing History's second axis.
|
|
98
|
+
*
|
|
99
|
+
* `countryLabel(countryId)` names a country for the picker and is the
|
|
100
|
+
* site's own — a country reads as its name, which this module has no way to
|
|
101
|
+
* know. `countryIdForCode`, if given, resolves a `findEvents({ country })`
|
|
102
|
+
* value through the site's own legacy-code table before filtering; omit it
|
|
103
|
+
* and `country` is already the id. `tr(id)` returns an event's translated
|
|
104
|
+
* fields already resolved for entity and language — the site's own `tr`,
|
|
105
|
+
* bound the way every data composable already binds it. `timelinesEntity`
|
|
106
|
+
* / `eventsEntity` name the data package files, for a site that calls its
|
|
107
|
+
* own something else.
|
|
108
|
+
*/
|
|
109
|
+
export function useTimelineEvents({
|
|
110
|
+
scope = 'country',
|
|
111
|
+
countryLabel,
|
|
112
|
+
countryIdForCode,
|
|
113
|
+
tr,
|
|
114
|
+
timelinesEntity = 'timelines',
|
|
115
|
+
eventsEntity = 'timeline_events',
|
|
116
|
+
} = {}) {
|
|
117
|
+
const timelines = entityRef(timelinesEntity)
|
|
118
|
+
const events = entityRef(eventsEntity)
|
|
119
|
+
|
|
120
|
+
// The narrative chronology has no country of its own, so it must be kept
|
|
121
|
+
// out of the merge by `source` rather than by the country filter — on "all
|
|
122
|
+
// countries" it would otherwise interleave into the worldwide list.
|
|
123
|
+
const localTimeline = computed(
|
|
124
|
+
() => (timelines.value ?? []).find((row) => row.source === 'thg_local') ?? null,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const timelineById = computed(() => new Map((timelines.value ?? []).map((row) => [row.id, row])))
|
|
128
|
+
|
|
129
|
+
/** The events this scope works over — one chronology, never both. */
|
|
130
|
+
const eventPool = computed(() => {
|
|
131
|
+
const all = events.value ?? []
|
|
132
|
+
const local = localTimeline.value
|
|
133
|
+
if (scope === 'local') return local ? all.filter((event) => event.timeline_id === local.id) : []
|
|
134
|
+
return local ? all.filter((event) => event.timeline_id !== local.id) : all
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const hasTimeline = computed(() => eventPool.value.length > 0)
|
|
138
|
+
const usesLocalTimeline = computed(() => scope === 'local' && Boolean(localTimeline.value))
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Countries that actually have a chronology, alphabetized by label, with
|
|
142
|
+
* an "all" marker first — the marker carries no label of its own, so the
|
|
143
|
+
* picker renders it through its own catalogue entry. No picker at all for
|
|
144
|
+
* `'local'`: legacy hides the country select once the section is the
|
|
145
|
+
* exhibition's own chronology, because there is nothing to filter by.
|
|
146
|
+
*/
|
|
147
|
+
const countries = computed(() => {
|
|
148
|
+
if (scope === 'local') return []
|
|
149
|
+
const seen = new Map()
|
|
150
|
+
for (const row of timelines.value ?? []) {
|
|
151
|
+
if (!row.country_id || seen.has(row.country_id)) continue
|
|
152
|
+
seen.set(row.country_id, {
|
|
153
|
+
value: row.country_id,
|
|
154
|
+
label: countryLabel ? countryLabel(row.country_id) : row.country_id,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
const rows = [...seen.values()].sort((a, b) => String(a.label).localeCompare(String(b.label)))
|
|
158
|
+
return [{ value: 'all', label: null }, ...rows]
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
/** Every event year present in this scope, as the year picker's source range. */
|
|
162
|
+
const yearRange = computed(() => {
|
|
163
|
+
const years = eventPool.value
|
|
164
|
+
.map((event) => event.year_from)
|
|
165
|
+
.filter((value) => Number.isFinite(value) && value !== 0)
|
|
166
|
+
if (!years.length) return [null, null]
|
|
167
|
+
return [Math.min(...years), Math.max(...years)]
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/** The legacy era buckets over this scope's year range. */
|
|
171
|
+
function yearBuckets(t) {
|
|
172
|
+
const [min, max] = yearRange.value
|
|
173
|
+
return yearBucketsFromRange(min, max, t)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveCountryId(country) {
|
|
177
|
+
if (country == null || country === 'all') return null
|
|
178
|
+
return countryIdForCode ? countryIdForCode(country) : country
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// `null` is Sharing History's Permanent Collection sentinel — a timeline
|
|
182
|
+
// bound to no exhibition — so it must be told apart from "no filter",
|
|
183
|
+
// which `collection: undefined` spells instead.
|
|
184
|
+
function matchesCollection(event, collection) {
|
|
185
|
+
const collectionId = timelineById.value.get(event.timeline_id)?.collection_id ?? null
|
|
186
|
+
return collection === null ? collectionId === null : collectionId === collection
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Legacy's `/events?ic[]=&ya=&yo=` (and Sharing History's exhibition
|
|
191
|
+
* filter alongside it) — this scope's events in `[begin, end]`, for the
|
|
192
|
+
* given country and/or collection, chronological. `display_order` breaks a
|
|
193
|
+
* tie on `year_from`, the order the exhibition itself gave two events of
|
|
194
|
+
* the same year; an event with no year at all has no place in that order
|
|
195
|
+
* and sorts after every dated one.
|
|
196
|
+
*/
|
|
197
|
+
function findEvents({ country, collection, begin, end } = {}) {
|
|
198
|
+
const countryId = resolveCountryId(country)
|
|
199
|
+
const from = begin === '' || begin == null ? null : Number(begin)
|
|
200
|
+
const to = end === '' || end == null ? null : Number(end)
|
|
201
|
+
|
|
202
|
+
return eventPool.value
|
|
203
|
+
.filter((event) => {
|
|
204
|
+
// A narrative chronology's events carry no country_id at all — the
|
|
205
|
+
// filter has nothing to apply itself to, the same reason `countries`
|
|
206
|
+
// renders no picker for this scope.
|
|
207
|
+
if (scope !== 'local' && countryId && event.country_id !== countryId) return false
|
|
208
|
+
if (collection !== undefined && !matchesCollection(event, collection)) return false
|
|
209
|
+
return overlapsRange(event, from, to)
|
|
210
|
+
})
|
|
211
|
+
.map((event) => ({ ...event, text: tr ? tr(event.id) : {} }))
|
|
212
|
+
.sort((a, b) => {
|
|
213
|
+
const ya = Number.isFinite(a.year_from) ? a.year_from : Infinity
|
|
214
|
+
const yb = Number.isFinite(b.year_from) ? b.year_from : Infinity
|
|
215
|
+
return ya - yb || (a.display_order ?? 0) - (b.display_order ?? 0)
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { countries, yearRange, yearBuckets, hasTimeline, usesLocalTimeline, findEvents }
|
|
220
|
+
}
|