@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,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
|
+
}
|