@museumwnf/viewer-core 1.14.0 → 1.15.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.
Files changed (43) hide show
  1. package/LICENSE.md +44 -44
  2. package/README.md +658 -635
  3. package/package.json +50 -50
  4. package/src/AppRoot.vue +58 -58
  5. package/src/catalogue/dates.js +209 -209
  6. package/src/catalogue/facets.js +61 -61
  7. package/src/catalogue/keywordIndex.js +289 -289
  8. package/src/catalogue/listQuery.js +100 -100
  9. package/src/catalogue/pagination.js +57 -57
  10. package/src/catalogue/searchLanguage.js +32 -32
  11. package/src/catalogue/timeline.js +220 -220
  12. package/src/composables/useCatalogueData.js +141 -141
  13. package/src/composables/useDataPackage.js +98 -98
  14. package/src/composables/useEntities.js +100 -100
  15. package/src/composables/useRecordLanguage.js +71 -71
  16. package/src/conventions.js +252 -148
  17. package/src/createViewer.js +29 -29
  18. package/src/i18n/I18nText.vue +17 -17
  19. package/src/i18n/I18nTextInline.vue +17 -17
  20. package/src/i18n/index.js +88 -88
  21. package/src/i18n/language.js +127 -127
  22. package/src/i18n/markdown.js +265 -265
  23. package/src/index.js +51 -50
  24. package/src/injectionKeys.js +1 -1
  25. package/src/languages.js +49 -49
  26. package/src/legacy/index.js +58 -58
  27. package/src/record/collectionTree.js +307 -307
  28. package/src/record/derivations.js +154 -154
  29. package/src/record/partners.js +87 -87
  30. package/src/record/recordSheet.js +231 -231
  31. package/src/rights.js +53 -53
  32. package/src/router/index.js +138 -138
  33. package/src/siteConfig.js +37 -37
  34. package/src/styles/base.css +49 -49
  35. package/src/testing/checkOfferedLanguages.js +67 -67
  36. package/src/testing/index.js +3 -3
  37. package/src/testing/smoke.js +103 -103
  38. package/src/testing/viteConfig.js +90 -90
  39. package/src/views/DetailView.vue +53 -53
  40. package/src/views/HomeView.vue +20 -20
  41. package/src/views/ListView.vue +61 -61
  42. package/src/views/NotFoundView.vue +8 -8
  43. package/src/vite.js +8 -8
@@ -1,100 +1,100 @@
1
- import { computed, reactive, watch } from 'vue'
2
- import { useRoute, useRouter } from 'vue-router'
3
-
4
- // The state of a list page lives in the URL, and the URL is what a visitor
5
- // shares, bookmarks and comes back to through history. Every results page of
6
- // every website therefore does the same four things: read the filters and the
7
- // page out of `route.query`, keep them in step when the route changes under
8
- // it, write them back when the visitor acts, and start again from page one
9
- // when a filter changes. Eleven views wrote those four things by hand before
10
- // this existed, each with its own idiom and its own small differences.
11
- //
12
- // The convention this encodes is rule R8 of the alignment pass: filters and
13
- // the page travel in the query, never in the path; `page` is dropped when it
14
- // is 1; a filter change is a new place in history (`push`), a page change
15
- // replaces the current one (`replace`), and either navigates on the route the
16
- // page is already on, by name, so no view writes a path.
17
-
18
- const PAGE = 'page'
19
-
20
- function firstValue(value) {
21
- return Array.isArray(value) ? (value[0] ?? '') : value ?? ''
22
- }
23
-
24
- function pageOf(query) {
25
- const parsed = Number.parseInt(firstValue(query[PAGE]), 10)
26
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 1
27
- }
28
-
29
- /**
30
- * Filters and page bound to the route query.
31
- *
32
- * `keys` names the filters this page knows. `filters` is a reactive object
33
- * with one string per key — bind a control to it, then `apply()` to navigate
34
- * with what it holds (or `apply(patch)` to change some and navigate at once,
35
- * for a control that navigates on change). `reset()` clears every filter and
36
- * navigates. `goToPage(n)` changes only the page. `page` is the current page,
37
- * read from the query. `active` says whether any filter is set.
38
- *
39
- * Query parameters the page does not own — `lang`, a parameter of another
40
- * component — are carried across every navigation untouched.
41
- */
42
- export function useListQuery({ keys = [], page = true } = {}) {
43
- const route = useRoute()
44
- const router = useRouter()
45
-
46
- const filters = reactive(Object.fromEntries(keys.map((key) => [key, ''])))
47
-
48
- function readFromRoute() {
49
- for (const key of keys) filters[key] = String(firstValue(route.query[key]))
50
- }
51
- readFromRoute()
52
- // The route changing under the page — back, forward, a link to the same
53
- // page with other filters — is the URL speaking; the controls follow it.
54
- watch(() => route.query, readFromRoute)
55
-
56
- const current = computed(() => (page ? pageOf(route.query) : 1))
57
- const active = computed(() => keys.some((key) => filters[key] !== ''))
58
-
59
- function foreignQuery() {
60
- const out = {}
61
- for (const [key, value] of Object.entries(route.query)) {
62
- if (!keys.includes(key) && key !== PAGE) out[key] = value
63
- }
64
- return out
65
- }
66
-
67
- function ownQuery() {
68
- const out = {}
69
- for (const key of keys) if (filters[key] !== '') out[key] = filters[key]
70
- return out
71
- }
72
-
73
- function location(query) {
74
- return { name: route.name, params: route.params, query }
75
- }
76
-
77
- /** Navigate with the filters as they are, or with `patch` applied first. Starts at page 1. */
78
- function apply(patch = {}) {
79
- for (const [key, value] of Object.entries(patch)) {
80
- if (keys.includes(key)) filters[key] = value == null ? '' : String(value)
81
- }
82
- return router.push(location({ ...foreignQuery(), ...ownQuery() }))
83
- }
84
-
85
- /** Clear every filter and navigate. */
86
- function reset() {
87
- for (const key of keys) filters[key] = ''
88
- return router.push(location(foreignQuery()))
89
- }
90
-
91
- /** Another page of the same results: the same place in history, so back leaves the list. */
92
- function goToPage(n) {
93
- const target = Math.max(1, Number.parseInt(n, 10) || 1)
94
- const query = { ...foreignQuery(), ...ownQuery() }
95
- if (target > 1) query[PAGE] = String(target)
96
- return router.replace(location(query))
97
- }
98
-
99
- return { filters, page: current, active, apply, reset, goToPage }
100
- }
1
+ import { computed, reactive, watch } from 'vue'
2
+ import { useRoute, useRouter } from 'vue-router'
3
+
4
+ // The state of a list page lives in the URL, and the URL is what a visitor
5
+ // shares, bookmarks and comes back to through history. Every results page of
6
+ // every website therefore does the same four things: read the filters and the
7
+ // page out of `route.query`, keep them in step when the route changes under
8
+ // it, write them back when the visitor acts, and start again from page one
9
+ // when a filter changes. Eleven views wrote those four things by hand before
10
+ // this existed, each with its own idiom and its own small differences.
11
+ //
12
+ // The convention this encodes is rule R8 of the alignment pass: filters and
13
+ // the page travel in the query, never in the path; `page` is dropped when it
14
+ // is 1; a filter change is a new place in history (`push`), a page change
15
+ // replaces the current one (`replace`), and either navigates on the route the
16
+ // page is already on, by name, so no view writes a path.
17
+
18
+ const PAGE = 'page'
19
+
20
+ function firstValue(value) {
21
+ return Array.isArray(value) ? (value[0] ?? '') : value ?? ''
22
+ }
23
+
24
+ function pageOf(query) {
25
+ const parsed = Number.parseInt(firstValue(query[PAGE]), 10)
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1
27
+ }
28
+
29
+ /**
30
+ * Filters and page bound to the route query.
31
+ *
32
+ * `keys` names the filters this page knows. `filters` is a reactive object
33
+ * with one string per key — bind a control to it, then `apply()` to navigate
34
+ * with what it holds (or `apply(patch)` to change some and navigate at once,
35
+ * for a control that navigates on change). `reset()` clears every filter and
36
+ * navigates. `goToPage(n)` changes only the page. `page` is the current page,
37
+ * read from the query. `active` says whether any filter is set.
38
+ *
39
+ * Query parameters the page does not own — `lang`, a parameter of another
40
+ * component — are carried across every navigation untouched.
41
+ */
42
+ export function useListQuery({ keys = [], page = true } = {}) {
43
+ const route = useRoute()
44
+ const router = useRouter()
45
+
46
+ const filters = reactive(Object.fromEntries(keys.map((key) => [key, ''])))
47
+
48
+ function readFromRoute() {
49
+ for (const key of keys) filters[key] = String(firstValue(route.query[key]))
50
+ }
51
+ readFromRoute()
52
+ // The route changing under the page — back, forward, a link to the same
53
+ // page with other filters — is the URL speaking; the controls follow it.
54
+ watch(() => route.query, readFromRoute)
55
+
56
+ const current = computed(() => (page ? pageOf(route.query) : 1))
57
+ const active = computed(() => keys.some((key) => filters[key] !== ''))
58
+
59
+ function foreignQuery() {
60
+ const out = {}
61
+ for (const [key, value] of Object.entries(route.query)) {
62
+ if (!keys.includes(key) && key !== PAGE) out[key] = value
63
+ }
64
+ return out
65
+ }
66
+
67
+ function ownQuery() {
68
+ const out = {}
69
+ for (const key of keys) if (filters[key] !== '') out[key] = filters[key]
70
+ return out
71
+ }
72
+
73
+ function location(query) {
74
+ return { name: route.name, params: route.params, query }
75
+ }
76
+
77
+ /** Navigate with the filters as they are, or with `patch` applied first. Starts at page 1. */
78
+ function apply(patch = {}) {
79
+ for (const [key, value] of Object.entries(patch)) {
80
+ if (keys.includes(key)) filters[key] = value == null ? '' : String(value)
81
+ }
82
+ return router.push(location({ ...foreignQuery(), ...ownQuery() }))
83
+ }
84
+
85
+ /** Clear every filter and navigate. */
86
+ function reset() {
87
+ for (const key of keys) filters[key] = ''
88
+ return router.push(location(foreignQuery()))
89
+ }
90
+
91
+ /** Another page of the same results: the same place in history, so back leaves the list. */
92
+ function goToPage(n) {
93
+ const target = Math.max(1, Number.parseInt(n, 10) || 1)
94
+ const query = { ...foreignQuery(), ...ownQuery() }
95
+ if (target > 1) query[PAGE] = String(target)
96
+ return router.replace(location(query))
97
+ }
98
+
99
+ return { filters, page: current, active, apply, reset, goToPage }
100
+ }
@@ -1,57 +1,57 @@
1
- import { computed, toValue } from 'vue'
2
-
3
- // One page arithmetic for every list. Five implementations existed — this
4
- // package's own ListView, a block inlined into six standalone views, a
5
- // component copied into four DXA sites, and two idioms for the count — and
6
- // they disagreed on nothing but shape. This is the DXA shape, which answers
7
- // everything a page needs to render itself: where it is, how far it goes,
8
- // and which rows it holds.
9
-
10
- const DEFAULT_SIZE = 20
11
-
12
- /**
13
- * `{ total, lastPage, currentPage, from, to, rows }` for one page of `list`.
14
- * A page past the end clamps to the last one; a page below one is the first;
15
- * `from`/`to` are 1-based and both 0 for an empty list. The size is the
16
- * caller's: twenty on the standalone sites and nine on the DXA sites are
17
- * both legacy facts, and neither is this function's to decide.
18
- */
19
- export function paginate(list, page, size = DEFAULT_SIZE) {
20
- const rows = list ?? []
21
- const total = rows.length
22
- const perPage = Math.max(1, Number(size) || DEFAULT_SIZE)
23
- const lastPage = Math.max(1, Math.ceil(total / perPage))
24
- const requested = Number.parseInt(page, 10)
25
- const currentPage = Math.min(Math.max(1, Number.isFinite(requested) ? requested : 1), lastPage)
26
- const start = (currentPage - 1) * perPage
27
- return {
28
- total,
29
- lastPage,
30
- currentPage,
31
- from: total === 0 ? 0 : start + 1,
32
- to: Math.min(start + perPage, total),
33
- rows: rows.slice(start, start + perPage),
34
- }
35
- }
36
-
37
- /**
38
- * `paginate` over reactive sources: `list` and `page` may each be a ref, a
39
- * computed, a getter or a plain value — a `useListQuery().page`, typically.
40
- */
41
- export function usePagination(list, { page = 1, size = DEFAULT_SIZE } = {}) {
42
- return computed(() => paginate(toValue(list), toValue(page), toValue(size)))
43
- }
44
-
45
- /**
46
- * Chronological order by `key` (a record's `start_date`), undated records
47
- * last — or first, with `undated: 'first'`. Stable: two records of the same
48
- * year keep the order they arrived in. Returns a new array.
49
- */
50
- export function sortChronological(list, { key = 'start_date', undated = 'last' } = {}) {
51
- const missing = undated === 'first' ? -Infinity : Infinity
52
- const year = (record) => {
53
- const value = record?.[key]
54
- return typeof value === 'number' && Number.isFinite(value) ? value : missing
55
- }
56
- return [...(list ?? [])].sort((a, b) => year(a) - year(b))
57
- }
1
+ import { computed, toValue } from 'vue'
2
+
3
+ // One page arithmetic for every list. Five implementations existed — this
4
+ // package's own ListView, a block inlined into six standalone views, a
5
+ // component copied into four DXA sites, and two idioms for the count — and
6
+ // they disagreed on nothing but shape. This is the DXA shape, which answers
7
+ // everything a page needs to render itself: where it is, how far it goes,
8
+ // and which rows it holds.
9
+
10
+ const DEFAULT_SIZE = 20
11
+
12
+ /**
13
+ * `{ total, lastPage, currentPage, from, to, rows }` for one page of `list`.
14
+ * A page past the end clamps to the last one; a page below one is the first;
15
+ * `from`/`to` are 1-based and both 0 for an empty list. The size is the
16
+ * caller's: twenty on the standalone sites and nine on the DXA sites are
17
+ * both legacy facts, and neither is this function's to decide.
18
+ */
19
+ export function paginate(list, page, size = DEFAULT_SIZE) {
20
+ const rows = list ?? []
21
+ const total = rows.length
22
+ const perPage = Math.max(1, Number(size) || DEFAULT_SIZE)
23
+ const lastPage = Math.max(1, Math.ceil(total / perPage))
24
+ const requested = Number.parseInt(page, 10)
25
+ const currentPage = Math.min(Math.max(1, Number.isFinite(requested) ? requested : 1), lastPage)
26
+ const start = (currentPage - 1) * perPage
27
+ return {
28
+ total,
29
+ lastPage,
30
+ currentPage,
31
+ from: total === 0 ? 0 : start + 1,
32
+ to: Math.min(start + perPage, total),
33
+ rows: rows.slice(start, start + perPage),
34
+ }
35
+ }
36
+
37
+ /**
38
+ * `paginate` over reactive sources: `list` and `page` may each be a ref, a
39
+ * computed, a getter or a plain value — a `useListQuery().page`, typically.
40
+ */
41
+ export function usePagination(list, { page = 1, size = DEFAULT_SIZE } = {}) {
42
+ return computed(() => paginate(toValue(list), toValue(page), toValue(size)))
43
+ }
44
+
45
+ /**
46
+ * Chronological order by `key` (a record's `start_date`), undated records
47
+ * last — or first, with `undated: 'first'`. Stable: two records of the same
48
+ * year keep the order they arrived in. Returns a new array.
49
+ */
50
+ export function sortChronological(list, { key = 'start_date', undated = 'last' } = {}) {
51
+ const missing = undated === 'first' ? -Infinity : Infinity
52
+ const year = (record) => {
53
+ const value = record?.[key]
54
+ return typeof value === 'number' && Number.isFinite(value) ? value : missing
55
+ }
56
+ return [...(list ?? [])].sort((a, b) => year(a) - year(b))
57
+ }
@@ -1,32 +1,32 @@
1
- import { computed, ref, watch } from 'vue'
2
- import { useDataPackage } from '../composables/useDataPackage.js'
3
-
4
- /**
5
- * The search-language rule: a sorted list of languages the entity's
6
- * translations exist in, and a watcher that loads them when the selection
7
- * changes. Three Database pages carry this watch locally, reading three
8
- * different entity names.
9
- */
10
- export function useSearchLanguage(entity) {
11
- const { availableLanguages, loadTranslations } = useDataPackage()
12
-
13
- // The languages the entity actually carries translations in, sorted.
14
- const languages = computed(() => {
15
- const available = availableLanguages(entity)
16
- return available.slice().sort()
17
- })
18
-
19
- // The user's current selection.
20
- const language = ref('')
21
-
22
- // Load translations when the selection changes; the watch runs immediately
23
- // so the reference language (English, the index language) is always there.
24
- watch(() => language.value, (lang) => {
25
- if (lang) loadTranslations(entity, lang)
26
- }, { immediate: true })
27
-
28
- return {
29
- languages,
30
- language,
31
- }
32
- }
1
+ import { computed, ref, watch } from 'vue'
2
+ import { useDataPackage } from '../composables/useDataPackage.js'
3
+
4
+ /**
5
+ * The search-language rule: a sorted list of languages the entity's
6
+ * translations exist in, and a watcher that loads them when the selection
7
+ * changes. Three Database pages carry this watch locally, reading three
8
+ * different entity names.
9
+ */
10
+ export function useSearchLanguage(entity) {
11
+ const { availableLanguages, loadTranslations } = useDataPackage()
12
+
13
+ // The languages the entity actually carries translations in, sorted.
14
+ const languages = computed(() => {
15
+ const available = availableLanguages(entity)
16
+ return available.slice().sort()
17
+ })
18
+
19
+ // The user's current selection.
20
+ const language = ref('')
21
+
22
+ // Load translations when the selection changes; the watch runs immediately
23
+ // so the reference language (English, the index language) is always there.
24
+ watch(() => language.value, (lang) => {
25
+ if (lang) loadTranslations(entity, lang)
26
+ }, { immediate: true })
27
+
28
+ return {
29
+ languages,
30
+ language,
31
+ }
32
+ }