@museumwnf/viewer-core 2.5.1 → 2.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@museumwnf/viewer-core",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "Application engine for MWNF websites: turns a dataset.config.js into a mounted Vue application.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -21,6 +21,13 @@
21
21
  // collapsed behind a toggle (the view's, folded after the description);
22
22
  // when only one exists it is relabelled plain "Description". EPM records
23
23
  // are the common case of the latter.
24
+ //
25
+ // The artist's life data goes on a line under the artist, and the Period /
26
+ // Dynasty row is legacy's free-text line (`dynasty`); the record's linked
27
+ // dynasties stand in when it has none, and keep their own popouts
28
+ // (decision D5, inventory-app#2233).
29
+
30
+ import { artistLine } from '../../record/derivations.js'
24
31
 
25
32
  /**
26
33
  * The exhibition's item-sheet spec, over its data layer's `data`.
@@ -54,14 +61,14 @@ export function useExhibitionSheet(data, config = {}) {
54
61
  { key: 'originalOwner', label: 'sheet.field.originalOwner', value: 'initial_owner' },
55
62
  { key: 'currentOwner', label: 'sheet.field.currentOwner', value: 'owner' },
56
63
  { key: 'date', label: 'sheet.field.date', value: 'dates' },
57
- { key: 'artist', label: 'sheet.field.artists', value: (c) => c.record.artist_names, join: ', ' },
64
+ { key: 'artist', label: 'sheet.field.artists', value: (c) => artistLine((c.record.artist_names ?? []).join(', '), c.text, { style: 'line' }) },
58
65
  { key: 'scribe', label: 'sheet.field.scribe', value: 'scriber' },
59
66
  { key: 'workshop', label: 'sheet.field.workshop', value: 'workshop' },
60
67
  { key: 'type', label: 'sheet.field.type', value: 'type' },
61
68
  { key: 'inventoryNumber', label: 'sheet.field.inventoryNumber', value: (c) => c.record.owner_reference },
62
69
  { key: 'materials', label: 'sheet.field.materials', value: (c) => c.text.materials, join: '; ' },
63
70
  { key: 'dimensions', label: 'sheet.field.dimensions', value: 'dimensions' },
64
- { key: 'dynasty', label: 'sheet.field.periodDynasty', value: dynastyNames },
71
+ { key: 'dynasty', label: 'sheet.field.periodDynasty', value: (c) => c.text?.dynasty || dynastyNames(c) },
65
72
  { key: 'production', label: 'sheet.field.placeOfProduction', value: 'place_of_production' },
66
73
  { key: 'provenance', label: 'sheet.field.provenance', value: 'provenance' },
67
74
  { key: 'binding', label: 'sheet.field.binding', value: 'binding_desc' },
@@ -21,6 +21,13 @@
21
21
  // collapsed behind a toggle (the view's, folded after the description);
22
22
  // when only one exists it is relabelled plain "Description". EPM records
23
23
  // are the common case of the latter.
24
+ //
25
+ // The artist's life data goes on a line under the artist, and the Period /
26
+ // Dynasty row is legacy's free-text line (`dynasty`); the record's linked
27
+ // dynasties stand in when it has none, and keep their own popouts
28
+ // (decision D5, inventory-app#2233).
29
+
30
+ import { artistLine } from '../../record/derivations.js'
24
31
 
25
32
  /**
26
33
  * The gallery's item-sheet spec, over its data layer's `data`.
@@ -54,14 +61,14 @@ export function useGallerySheet(data, config = {}) {
54
61
  { key: 'originalOwner', label: 'sheet.field.originalOwner', value: 'initial_owner' },
55
62
  { key: 'currentOwner', label: 'sheet.field.currentOwner', value: 'owner' },
56
63
  { key: 'date', label: 'sheet.field.date', value: 'dates' },
57
- { key: 'artist', label: 'sheet.field.artists', value: (c) => c.record.artist_names, join: ', ' },
64
+ { key: 'artist', label: 'sheet.field.artists', value: (c) => artistLine((c.record.artist_names ?? []).join(', '), c.text, { style: 'line' }) },
58
65
  { key: 'scribe', label: 'sheet.field.scribe', value: 'scriber' },
59
66
  { key: 'workshop', label: 'sheet.field.workshop', value: 'workshop' },
60
67
  { key: 'type', label: 'sheet.field.type', value: 'type' },
61
68
  { key: 'inventoryNumber', label: 'sheet.field.inventoryNumber', value: (c) => c.record.owner_reference },
62
69
  { key: 'materials', label: 'sheet.field.materials', value: (c) => c.text.materials, join: '; ' },
63
70
  { key: 'dimensions', label: 'sheet.field.dimensions', value: 'dimensions' },
64
- { key: 'dynasty', label: 'sheet.field.periodDynasty', value: dynastyNames },
71
+ { key: 'dynasty', label: 'sheet.field.periodDynasty', value: (c) => c.text?.dynasty || dynastyNames(c) },
65
72
  { key: 'production', label: 'sheet.field.placeOfProduction', value: 'place_of_production' },
66
73
  { key: 'provenance', label: 'sheet.field.provenance', value: 'provenance' },
67
74
  { key: 'binding', label: 'sheet.field.binding', value: 'binding_desc' },
@@ -93,7 +93,16 @@ export function resolveInitialLanguage(offered) {
93
93
  export function connectLanguageToRouter({ locale, offered, router }) {
94
94
  const multilingual = offered.length > 1
95
95
 
96
+ // The navigation under way, from the guard until it lands. Its URL already
97
+ // carries the language the guard set; rewriting the page being left
98
+ // instead cancels it, and the visitor stays there in the new language.
99
+ let pending = null
100
+ const settle = (to) => {
101
+ if (pending === to) pending = null
102
+ }
103
+
96
104
  router.beforeEach((to) => {
105
+ pending = to
97
106
  const next = negotiateLanguage(offered, {
98
107
  requested: multilingual ? to.query.lang : undefined,
99
108
  stored: readStoredLanguage(),
@@ -107,18 +116,28 @@ export function connectLanguageToRouter({ locale, offered, router }) {
107
116
  return true
108
117
  })
109
118
 
110
- // A change of language from the switcher rewrites the URL. Not before the
111
- // first navigation has resolved, though: the router is then still at its
112
- // start location, `/`, and a replace from there cancels the navigation the
113
- // visitor arrived with — every deep link landed on the home page. The guard
114
- // above puts the language in the URL of that first navigation itself.
119
+ router.afterEach((to, from, failure) => {
120
+ settle(to)
121
+ // The visitor switched language while this page loaded.
122
+ if (!failure && multilingual && to.query.lang !== locale.value) {
123
+ router.replace({ query: { ...to.query, lang: locale.value } })
124
+ }
125
+ })
126
+ router.onError((error, to) => settle(to))
127
+
128
+ // A change of language from the switcher rewrites the URL. Not while a
129
+ // navigation is under way (see `pending`), and so not before the first one
130
+ // has resolved: the router is then still at its start location, `/`, and a
131
+ // replace from there cancels the navigation the visitor arrived with, so
132
+ // every deep link landed on the home page. The guard above puts the
133
+ // language in the URL of that first navigation itself.
115
134
  watch(
116
135
  locale,
117
136
  (code) => {
118
137
  applyDocumentLanguage(code)
119
138
  storeLanguage(code)
120
139
  const current = router.currentRoute.value
121
- if (multilingual && current !== START_LOCATION && current.query.lang !== code) {
140
+ if (multilingual && !pending && current !== START_LOCATION && current.query.lang !== code) {
122
141
  router.replace({ query: { ...current.query, lang: code } })
123
142
  }
124
143
  },
package/src/index.js CHANGED
@@ -39,7 +39,8 @@ export {
39
39
  glossaryEntries, glossaryTermsFor, glossaryTermsForText, sheetRows, useRecordSheet,
40
40
  } from './record/recordSheet.js'
41
41
  export {
42
- citation, relatedRecords, searchGlossary, timelineLinkFor, useGlossaryPopup, useRelatedRecords,
42
+ artistLine, citation, relatedRecords, removeEndingPoint, searchGlossary, timelineLinkFor, useGlossaryPopup,
43
+ useRelatedRecords,
43
44
  } from './record/derivations.js'
44
45
  export { buildCollectionTree, collectionTreeFromThemes, useCollectionTree } from './record/collectionTree.js'
45
46
  export { groupByCountry, partnerHierarchy, partnerView } from './record/partners.js'
@@ -152,3 +152,64 @@ export function timelineLinkFor(record, { name = 'timeline-results', keys = {},
152
152
  if (to != null) query[keys.end ?? 'end'] = String(to)
153
153
  return { name, query }
154
154
  }
155
+
156
+ // ── Legacy's sheet lines ───────────────────────────────────────────────────
157
+ //
158
+ // How legacy's sheets printed a value, written once so each website prints
159
+ // it the way its own legacy did.
160
+
161
+ /**
162
+ * `text` without the period(s) that end it, as legacy's `removeEndingPoint`
163
+ * printed most sheet values — sparing a period after "C", as in "B.C.".
164
+ * Anything but a string comes back as ''.
165
+ *
166
+ * Legacy's `preg_replace("/([^C])\.+[[:space:]]*$/", "\\1", …)`, read as a
167
+ * scan from the end rather than as that regex, which backtracks
168
+ * polynomially on a long run of periods. Same answers: the final run of
169
+ * periods goes with the whitespace after it; after a "C", or at the very
170
+ * start, the run's first period stays.
171
+ */
172
+ export function removeEndingPoint(text) {
173
+ if (typeof text !== 'string') return ''
174
+ const body = text.trimEnd()
175
+ let start = body.length
176
+ while (start > 0 && body[start - 1] === '.') start--
177
+ if (start === body.length) return text
178
+ if (start === 0 || body[start - 1] === 'C') return body.length - start > 1 ? body.slice(0, start + 1) : text
179
+ return body.slice(0, start)
180
+ }
181
+
182
+ const lifePart = (value) => (typeof value === 'string' ? value.trim() : '')
183
+
184
+ /**
185
+ * The artist line as Markdown: `artist`, then the artist's life data the
186
+ * record's translation carries (`artist_birthdate`, `artist_birthplace`,
187
+ * `artist_deathdate`, `artist_deathplace`), each part only when set and
188
+ * joined exactly as legacy joined it, in one of legacy's two shapes:
189
+ *
190
+ * - `'bracketed'` — Baroque Art's and Sharing History's `database_item.php`:
191
+ * on the artist's line, after a no-break space,
192
+ * `Name (1660, Chemnitz-1736, Augsburg)`; a missing birth date keeps the
193
+ * comma before the place, as it did there. Nothing without an artist.
194
+ * - `'line'` — the galleries' and exhibitions' `DatabaseItem.vue`
195
+ * (`returnArtistData`): on a line of its own under the artist,
196
+ * `1660, Chemnitz - 1736, Augsburg`.
197
+ *
198
+ * Islamic Art's sheet printed the artist alone. '' when there is nothing
199
+ * to print.
200
+ */
201
+ export function artistLine(artist, text, { style = 'bracketed' } = {}) {
202
+ const name = lifePart(artist)
203
+ const born = lifePart(text?.artist_birthdate)
204
+ const bornAt = lifePart(text?.artist_birthplace)
205
+ const died = lifePart(text?.artist_deathdate)
206
+ const diedAt = lifePart(text?.artist_deathplace)
207
+ if (style === 'line') {
208
+ const birth = [born, bornAt].filter(Boolean).join(', ')
209
+ const death = [died, diedAt].filter(Boolean).join(', ')
210
+ return [name, [birth, death].filter(Boolean).join(' - ')].filter(Boolean).join(' \n')
211
+ }
212
+ if (!name) return ''
213
+ if (!(born || bornAt || died || diedAt)) return name
214
+ return `${name} (${born}${bornAt ? `, ${bornAt}` : ''}${died ? `-${died}` : ''}${diedAt ? `, ${diedAt}` : ''})`
215
+ }