@datagouv/components-next 1.2.0 → 1.4.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 +7 -6
- package/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
- package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
- package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
- package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
- package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
- package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
- package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +201 -170
- package/dist/components.css +2 -2
- package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
- package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
- package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +40 -39
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +17 -6
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +30 -19
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +140 -184
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +28 -10
- package/src/components/Search/SearchInput.vue +1 -1
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +21 -5
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +127 -780
- package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
- package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +40 -28
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/api.ts +13 -7
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +88 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +222 -5
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js +0 -61
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- An instant is formatted in the server's timezone at render time and in the
|
|
3
|
+
reader's at read time, so its text and `title` legitimately differ and Vue is
|
|
4
|
+
told to accept it. A plain date carries no time, so nothing about it varies —
|
|
5
|
+
exempting it would hide a real bug instead of a legitimate difference. Vue only
|
|
6
|
+
honours `data-allow-mismatch` on an element whose children are plain text,
|
|
7
|
+
never on a text node coming from a slot: hence two static elements instead of a
|
|
8
|
+
`<component :is>`, which would pass the text through a slot. -->
|
|
9
|
+
<time
|
|
10
|
+
v-if="datetime"
|
|
11
|
+
:datetime
|
|
12
|
+
:title
|
|
13
|
+
:data-allow-mismatch="allowMismatch"
|
|
14
|
+
>{{ formatted }}</time>
|
|
15
|
+
<!-- No date to format: nothing here depends on the timezone or on "now". -->
|
|
16
|
+
<span v-else>{{ formatted }}</span>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed } from 'vue'
|
|
21
|
+
import { PLAIN_DATE, parseDateValue, useFormatDate } from '../functions/dates'
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<{
|
|
24
|
+
date: Date | string | null | undefined
|
|
25
|
+
/**
|
|
26
|
+
* `date`: the full date. `relative`: "3 days ago" while the date is less than a
|
|
27
|
+
* month old, the full date beyond. `from-now`: always relative, however old.
|
|
28
|
+
*/
|
|
29
|
+
format?: 'date' | 'relative' | 'from-now'
|
|
30
|
+
options?: Intl.DateTimeFormatOptions
|
|
31
|
+
}>(), {
|
|
32
|
+
format: 'date',
|
|
33
|
+
options: () => ({}),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const { formatDate, formatFromNow, formatRelativeIfRecentDate } = useFormatDate()
|
|
37
|
+
|
|
38
|
+
const plainDate = computed(() => typeof props.date === 'string' ? props.date.match(PLAIN_DATE) : null)
|
|
39
|
+
|
|
40
|
+
const parsed = computed(() => parseDateValue(props.date))
|
|
41
|
+
|
|
42
|
+
// Announce the precision the value carries, never more: a month stays a month.
|
|
43
|
+
const datetime = computed(() => {
|
|
44
|
+
if (!parsed.value) return undefined
|
|
45
|
+
return plainDate.value ? plainDate.value[0] : parsed.value.toISOString()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Only what genuinely differs between server and browser is exempted: an instant, and
|
|
49
|
+
// the relative forms, which are measured against "now".
|
|
50
|
+
const allowMismatch = computed(() => !plainDate.value || props.format !== 'date'
|
|
51
|
+
? 'text,attribute'
|
|
52
|
+
: undefined)
|
|
53
|
+
|
|
54
|
+
const formatted = computed(() => {
|
|
55
|
+
if (props.format === 'relative') return formatRelativeIfRecentDate(parsed.value, props.options)
|
|
56
|
+
if (props.format === 'from-now') return formatFromNow(parsed.value)
|
|
57
|
+
return formatDate(parsed.value, props.options)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Hovering an instant gives the exact moment, which the displayed form usually omits:
|
|
61
|
+
// the relative forms drop it entirely, and most styles drop the time. A plain date has
|
|
62
|
+
// no moment to reveal — a `title` there would be one we made up, and a form that already
|
|
63
|
+
// spells the moment out needs no tooltip repeating it word for word.
|
|
64
|
+
const title = computed(() => {
|
|
65
|
+
if (!parsed.value || plainDate.value) return undefined
|
|
66
|
+
const exact = formatDate(parsed.value, { dateStyle: 'long', timeStyle: 'short' })
|
|
67
|
+
return exact === formatted.value ? undefined : exact
|
|
68
|
+
})
|
|
69
|
+
</script>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative">
|
|
3
3
|
<div :class="{ 'opacity-50 min-h-64': loading }">
|
|
4
|
+
<!-- Keep the previous content while reloading, dimmed under the loader:
|
|
5
|
+
dropping it collapses the page height, which makes the browser clamp
|
|
6
|
+
the scroll and throws the reader back to the top. -->
|
|
4
7
|
<slot
|
|
5
|
-
v-if="!
|
|
8
|
+
v-if="!hasError && data"
|
|
6
9
|
:data="data"
|
|
7
10
|
/>
|
|
8
11
|
<slot
|
|
@@ -28,12 +28,15 @@ import { computed } from 'vue'
|
|
|
28
28
|
|
|
29
29
|
const props = withDefaults(defineProps<{
|
|
30
30
|
articleClass?: string | string[] | Record<string, boolean>
|
|
31
|
-
mediaSize?: 'sm' | 'lg'
|
|
31
|
+
mediaSize?: 'sm' | 'lg' | 'xl'
|
|
32
32
|
}>(), {
|
|
33
33
|
mediaSize: 'sm',
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
const mediaContainerClass = computed(() => {
|
|
37
|
+
if (props.mediaSize === 'xl') {
|
|
38
|
+
return 'w-[250px] h-[190px]'
|
|
39
|
+
}
|
|
37
40
|
if (props.mediaSize === 'lg') {
|
|
38
41
|
return 'w-[225px] h-[120px]'
|
|
39
42
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
class="-mr-0.5 flex-initial truncate"
|
|
5
5
|
>
|
|
6
6
|
<AppLink
|
|
7
|
+
v-if="organizationUrl !== null"
|
|
7
8
|
class="link text-sm overflow-hidden flex items-center relative z-[2] truncate"
|
|
8
9
|
:to="organizationUrl || organization.page"
|
|
9
10
|
>
|
|
@@ -12,6 +13,16 @@
|
|
|
12
13
|
size="sm"
|
|
13
14
|
/>
|
|
14
15
|
</AppLink>
|
|
16
|
+
<span
|
|
17
|
+
v-else
|
|
18
|
+
class="text-sm overflow-hidden flex items-center truncate"
|
|
19
|
+
>
|
|
20
|
+
<OrganizationNameWithCertificate
|
|
21
|
+
:organization
|
|
22
|
+
size="sm"
|
|
23
|
+
color-class="text-gray-title"
|
|
24
|
+
/>
|
|
25
|
+
</span>
|
|
15
26
|
</div>
|
|
16
27
|
<div
|
|
17
28
|
v-else-if="ownerName"
|
|
@@ -33,7 +44,8 @@ import OrganizationNameWithCertificate from './OrganizationNameWithCertificate.v
|
|
|
33
44
|
const props = defineProps<{
|
|
34
45
|
organization?: OrganizationReference | null
|
|
35
46
|
owner?: UserReference | null
|
|
36
|
-
|
|
47
|
+
// undefined falls back to organization.page; pass null to render plain, non-linked text
|
|
48
|
+
organizationUrl?: RouteLocationRaw | null
|
|
37
49
|
}>()
|
|
38
50
|
|
|
39
51
|
const ownerName = computed(() => {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</li>
|
|
20
20
|
<li>
|
|
21
21
|
<a
|
|
22
|
-
:href="getHref(
|
|
22
|
+
:href="getHref(currentPage - 1)"
|
|
23
23
|
class="fr-pagination__link fr-pagination__link--prev fr-pagination__link--lg-label"
|
|
24
24
|
data-testid="previous-page"
|
|
25
25
|
@click.prevent.stop="previousPage"
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
</li>
|
|
30
30
|
<li>
|
|
31
31
|
<a
|
|
32
|
-
:aria-current="
|
|
32
|
+
:aria-current="currentPage === 1 ? 'page' : undefined"
|
|
33
33
|
:href="getHref(1)"
|
|
34
34
|
class="fr-pagination__link"
|
|
35
|
-
:class="{ 'fr-hidden fr-unhidden-sm':
|
|
35
|
+
:class="{ 'fr-hidden fr-unhidden-sm': currentPage > 1 }"
|
|
36
36
|
:title="t('Page {nb}', { nb: 1 })"
|
|
37
37
|
:data-testid="1"
|
|
38
38
|
@click.prevent.stop="onClick(1)"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
<a
|
|
48
48
|
v-if="index"
|
|
49
49
|
class="fr-pagination__link"
|
|
50
|
-
:class="{ 'fr-hidden fr-unhidden-lg': index <
|
|
51
|
-
:aria-current="
|
|
50
|
+
:class="{ 'fr-hidden fr-unhidden-lg': index < currentPage - 1 || index > currentPage + 1 }"
|
|
51
|
+
:aria-current="currentPage === index ? 'page' : undefined"
|
|
52
52
|
:href="getHref(index)"
|
|
53
53
|
:title="t('Page {nb}', { nb: index })"
|
|
54
54
|
:data-testid="index"
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<li>
|
|
67
67
|
<a
|
|
68
68
|
class="fr-pagination__link"
|
|
69
|
-
:aria-current="
|
|
69
|
+
:aria-current="currentPage === pageCount ? 'page' : undefined"
|
|
70
70
|
:href="getHref(pageCount)"
|
|
71
71
|
:title="t('Page {nb}', { nb: pageCount })"
|
|
72
72
|
:data-testid="pageCount"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<li>
|
|
79
79
|
<a
|
|
80
80
|
class="fr-pagination__link fr-pagination__link--next fr-pagination__link--lg-label"
|
|
81
|
-
:href="getHref(
|
|
81
|
+
:href="getHref(currentPage + 1)"
|
|
82
82
|
data-testid="next-page"
|
|
83
83
|
@click.prevent.stop="nextPage"
|
|
84
84
|
>
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
import { computed, useTemplateRef } from 'vue'
|
|
104
104
|
import { useRoute } from 'vue-router'
|
|
105
105
|
import { useTranslation } from '../composables/useTranslation'
|
|
106
|
+
import { clampPage, getVisiblePages } from '../functions/paginate'
|
|
107
|
+
import { scrollToBlockTop } from '../functions/scroll'
|
|
106
108
|
|
|
107
109
|
type Props = {
|
|
108
110
|
/**
|
|
@@ -128,78 +130,36 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
128
130
|
pageSize: 20,
|
|
129
131
|
})
|
|
130
132
|
|
|
131
|
-
const PAGES_AROUND = 3
|
|
132
|
-
|
|
133
|
-
function range(size: number, startAt = 1) {
|
|
134
|
-
return [...Array(size).keys()].map(i => i + startAt)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getPages(pageCount: number) {
|
|
138
|
-
return range(pageCount)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getPagesShown(pages: { length: number }, currentPage: number) {
|
|
142
|
-
return Math.min(
|
|
143
|
-
PAGES_AROUND * 2 + 1,
|
|
144
|
-
pages.length - 2,
|
|
145
|
-
PAGES_AROUND + currentPage - 1,
|
|
146
|
-
PAGES_AROUND + pages.length - currentPage,
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getStartPage(currentPage: number) {
|
|
151
|
-
return Math.max(
|
|
152
|
-
currentPage - PAGES_AROUND, // we want to start 3 pages before the current one
|
|
153
|
-
2, // we don't want to start below page 2
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function getVisiblePages(currentPage: number, pageCount: number) {
|
|
158
|
-
const pages = getPages(pageCount)
|
|
159
|
-
const start = getStartPage(currentPage)
|
|
160
|
-
if (pageCount <= 2) {
|
|
161
|
-
return []
|
|
162
|
-
}
|
|
163
|
-
const pagination: Array<number | null> = range(getPagesShown(pages, currentPage), start)
|
|
164
|
-
if (!pagination.includes(2)) {
|
|
165
|
-
pagination.unshift(null)
|
|
166
|
-
}
|
|
167
|
-
if (!pagination.includes(pageCount - 1)) {
|
|
168
|
-
pagination.push(null)
|
|
169
|
-
}
|
|
170
|
-
return pagination
|
|
171
|
-
}
|
|
172
|
-
|
|
173
133
|
const { t } = useTranslation()
|
|
174
134
|
const route = useRoute()
|
|
175
135
|
const pageCount = computed(() => Math.ceil(props.totalResults / props.pageSize))
|
|
176
|
-
|
|
136
|
+
// The `page` prop often comes straight from a query string, so it is normalised
|
|
137
|
+
// once here and every link below builds on an existing page.
|
|
138
|
+
const currentPage = computed(() => clampPage(props.page, pageCount.value))
|
|
139
|
+
const visiblePages = computed(() => getVisiblePages(currentPage.value, pageCount.value))
|
|
177
140
|
|
|
178
141
|
const nav = useTemplateRef('navRef')
|
|
179
142
|
function change(index: number) {
|
|
180
143
|
emit('change', index)
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
nav.value.parentElement.style.scrollMarginTop = '100px'
|
|
185
|
-
nav.value.parentElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
145
|
+
scrollToBlockTop(nav.value?.parentElement)
|
|
186
146
|
}
|
|
187
147
|
|
|
188
148
|
function onClick(index: number) {
|
|
189
|
-
if (index !==
|
|
149
|
+
if (index !== currentPage.value) {
|
|
190
150
|
change(index)
|
|
191
151
|
}
|
|
192
152
|
}
|
|
193
153
|
|
|
194
154
|
function nextPage() {
|
|
195
|
-
const index =
|
|
155
|
+
const index = currentPage.value + 1
|
|
196
156
|
if (index <= pageCount.value) {
|
|
197
157
|
change(index)
|
|
198
158
|
}
|
|
199
159
|
}
|
|
200
160
|
|
|
201
161
|
function previousPage() {
|
|
202
|
-
const index =
|
|
162
|
+
const index = currentPage.value - 1
|
|
203
163
|
if (index > 0) {
|
|
204
164
|
change(index)
|
|
205
165
|
}
|
|
@@ -209,7 +169,7 @@ function getHref(forPage: number) {
|
|
|
209
169
|
if (forPage < 1 || forPage > pageCount.value) {
|
|
210
170
|
return undefined
|
|
211
171
|
}
|
|
212
|
-
if (
|
|
172
|
+
if (currentPage.value === forPage) {
|
|
213
173
|
return undefined
|
|
214
174
|
}
|
|
215
175
|
const search = new URLSearchParams(route.query as Record<string, string>)
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
<script setup lang="ts">
|
|
12
12
|
import { computed } from 'vue'
|
|
13
|
-
import { RiBookShelfLine, RiBuilding2Line, RiDatabase2Line, RiLineChartLine, RiTerminalLine } from '@remixicon/vue'
|
|
13
|
+
import { RiBarChartBoxLine, RiBookShelfLine, RiBuilding2Line, RiDatabase2Line, RiLineChartLine, RiTerminalLine } from '@remixicon/vue'
|
|
14
14
|
|
|
15
15
|
const props = defineProps<{
|
|
16
|
-
type: 'Dataset' | 'Dataservice' | 'Reuse' | 'Organization' | 'Topic'
|
|
16
|
+
type: 'Dataset' | 'Dataservice' | 'Reuse' | 'Organization' | 'Topic' | 'Chart'
|
|
17
17
|
}>()
|
|
18
18
|
|
|
19
19
|
const icon = computed(() => {
|
|
@@ -23,6 +23,7 @@ const icon = computed(() => {
|
|
|
23
23
|
Reuse: RiLineChartLine,
|
|
24
24
|
Organization: RiBuilding2Line,
|
|
25
25
|
Topic: RiBookShelfLine,
|
|
26
|
+
Chart: RiBarChartBoxLine,
|
|
26
27
|
}[props.type]
|
|
27
28
|
})
|
|
28
29
|
</script>
|
|
@@ -23,12 +23,20 @@
|
|
|
23
23
|
|
|
24
24
|
<ObjectCardShortDescription :text="post.headline || post.content" />
|
|
25
25
|
|
|
26
|
-
<
|
|
26
|
+
<TranslationT
|
|
27
27
|
v-if="post.published || post.created_at"
|
|
28
|
+
tag="div"
|
|
28
29
|
class="text-sm text-gray-medium mt-1"
|
|
30
|
+
keypath="Publié {date}"
|
|
29
31
|
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
<template #date>
|
|
33
|
+
<FormattedDate
|
|
34
|
+
:date="post.published || post.created_at"
|
|
35
|
+
format="relative"
|
|
36
|
+
:options="{ dateStyle: 'long', timeStyle: 'short' }"
|
|
37
|
+
/>
|
|
38
|
+
</template>
|
|
39
|
+
</TranslationT>
|
|
32
40
|
|
|
33
41
|
<slot />
|
|
34
42
|
</ObjectCard>
|
|
@@ -37,26 +45,16 @@
|
|
|
37
45
|
<script setup lang="ts">
|
|
38
46
|
import { RiArticleLine } from '@remixicon/vue'
|
|
39
47
|
import type { RouteLocationRaw } from 'vue-router'
|
|
40
|
-
import { useFormatDate } from '../functions/dates'
|
|
41
|
-
import { useTranslation } from '../composables/useTranslation'
|
|
42
48
|
import type { Post } from '../types/posts'
|
|
43
49
|
import Placeholder from './Placeholder.vue'
|
|
44
50
|
import ObjectCard from './ObjectCard.vue'
|
|
45
51
|
import ObjectCardHeader from './ObjectCardHeader.vue'
|
|
46
52
|
import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
|
|
53
|
+
import FormattedDate from './FormattedDate.vue'
|
|
54
|
+
import TranslationT from './TranslationT.vue'
|
|
47
55
|
|
|
48
56
|
defineProps<{
|
|
49
57
|
post: Post
|
|
50
58
|
postUrl?: RouteLocationRaw
|
|
51
59
|
}>()
|
|
52
|
-
|
|
53
|
-
const { t } = useTranslation()
|
|
54
|
-
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
55
|
-
|
|
56
|
-
const formatDate = (dateString: string) => {
|
|
57
|
-
return formatRelativeIfRecentDate(dateString, {
|
|
58
|
-
dateStyle: 'long',
|
|
59
|
-
timeStyle: 'short',
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
60
|
</script>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/>
|
|
11
11
|
</div>
|
|
12
12
|
<PreviewUnavailable v-else>
|
|
13
|
-
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le
|
|
13
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le pour le consulter.") }}
|
|
14
14
|
</PreviewUnavailable>
|
|
15
15
|
</div>
|
|
16
16
|
</template>
|
|
@@ -47,9 +47,21 @@
|
|
|
47
47
|
/>
|
|
48
48
|
</dd>
|
|
49
49
|
<template v-if="generatedFormats.length">
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
<TranslationT
|
|
51
|
+
tag="dt"
|
|
52
|
+
class="font-bold fr-text--sm fr-mb-0"
|
|
53
|
+
keypath="Formats générés automatiquement par {platform} (dernière mise à jour {date})"
|
|
54
|
+
>
|
|
55
|
+
<template #platform>
|
|
56
|
+
{{ config.name }}
|
|
57
|
+
</template>
|
|
58
|
+
<template #date>
|
|
59
|
+
<FormattedDate
|
|
60
|
+
:date="conversionsFinishedAt"
|
|
61
|
+
format="relative"
|
|
62
|
+
/>
|
|
63
|
+
</template>
|
|
64
|
+
</TranslationT>
|
|
53
65
|
<dd
|
|
54
66
|
v-for="generatedFormat in generatedFormats"
|
|
55
67
|
:key="generatedFormat.format"
|
|
@@ -132,7 +144,8 @@ import { filesize } from '../../functions/helpers'
|
|
|
132
144
|
import { getResourceFilesize } from '../../functions/resources'
|
|
133
145
|
import { trackEvent } from '../../functions/matomo'
|
|
134
146
|
import { useComponentsConfig } from '../../config'
|
|
135
|
-
import
|
|
147
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
148
|
+
import TranslationT from '../TranslationT.vue'
|
|
136
149
|
import { useTranslation } from '../../composables/useTranslation'
|
|
137
150
|
import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
|
|
138
151
|
import type { Resource } from '../../types/resources'
|
|
@@ -145,7 +158,6 @@ const props = defineProps<{
|
|
|
145
158
|
|
|
146
159
|
const { t } = useTranslation()
|
|
147
160
|
const config = useComponentsConfig()
|
|
148
|
-
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
149
161
|
|
|
150
162
|
const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabilities(
|
|
151
163
|
() => props.resource,
|
|
@@ -154,7 +166,7 @@ const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabi
|
|
|
154
166
|
|
|
155
167
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
156
168
|
|
|
157
|
-
const
|
|
158
|
-
|
|
169
|
+
const conversionsFinishedAt = computed(() =>
|
|
170
|
+
props.resource.extras['analysis:parsing:finished_at'] as string | undefined,
|
|
159
171
|
)
|
|
160
172
|
</script>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-xs">
|
|
3
|
+
<PreviewUnavailable v-if="!isSizeAllowed">
|
|
4
|
+
{{ fileSizeBytes
|
|
5
|
+
? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le pour le consulter.", { fileType })
|
|
6
|
+
: t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le pour le consulter.")
|
|
7
|
+
}}
|
|
8
|
+
</PreviewUnavailable>
|
|
9
|
+
<PreviewUnavailable v-else-if="hasError">
|
|
10
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le pour le consulter.") }}
|
|
11
|
+
</PreviewUnavailable>
|
|
12
|
+
<img
|
|
13
|
+
v-else
|
|
14
|
+
:src="resource.url"
|
|
15
|
+
alt=""
|
|
16
|
+
class="block w-full h-auto max-w-[800px]"
|
|
17
|
+
@error="hasError = true"
|
|
18
|
+
>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { computed, ref } from 'vue'
|
|
24
|
+
import PreviewUnavailable from './PreviewUnavailable.vue'
|
|
25
|
+
import type { Resource } from '../../types/resources'
|
|
26
|
+
import { getResourceFilesize } from '../../functions/resources'
|
|
27
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
28
|
+
import { useComponentsConfig } from '../../config'
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
resource: Resource
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const { t } = useTranslation()
|
|
35
|
+
const config = useComponentsConfig()
|
|
36
|
+
|
|
37
|
+
const hasError = ref(false)
|
|
38
|
+
|
|
39
|
+
const fileType = computed(() => props.resource.format?.toUpperCase() ?? t('Fichier'))
|
|
40
|
+
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
|
|
41
|
+
|
|
42
|
+
const isSizeAllowed = computed(() => {
|
|
43
|
+
const size = fileSizeBytes.value
|
|
44
|
+
const max = config.maxImagePreviewByteSize
|
|
45
|
+
if (!size || !max) return false
|
|
46
|
+
return size <= max
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
</PreviewUnavailable>
|
|
5
5
|
<div
|
|
6
6
|
v-else
|
|
7
|
-
id="map"
|
|
8
7
|
ref="mapRef"
|
|
8
|
+
class="size-full"
|
|
9
9
|
/>
|
|
10
10
|
</template>
|
|
11
11
|
|
|
@@ -128,10 +128,3 @@ onMounted(() => {
|
|
|
128
128
|
displayMap()
|
|
129
129
|
})
|
|
130
130
|
</script>
|
|
131
|
-
|
|
132
|
-
<style>
|
|
133
|
-
#map {
|
|
134
|
-
width: 100%;
|
|
135
|
-
height: 500px;
|
|
136
|
-
}
|
|
137
|
-
</style>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
+
import { RiInformationLine } from '@remixicon/vue'
|
|
3
4
|
import type { Resource } from '../../types/resources'
|
|
4
5
|
import CopyButton from '../CopyButton.vue'
|
|
5
6
|
import DescriptionDetails from '../DescriptionDetails.vue'
|
|
6
7
|
import DescriptionTerm from '../DescriptionTerm.vue'
|
|
7
|
-
import
|
|
8
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
8
9
|
import { filesize } from '../../functions/helpers'
|
|
9
10
|
import ExtraAccordion from '../ExtraAccordion.vue'
|
|
11
|
+
import Toggletip from '../Toggletip.vue'
|
|
10
12
|
import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
|
|
11
13
|
import { useTranslation } from '../../composables/useTranslation'
|
|
12
14
|
|
|
@@ -19,7 +21,6 @@ const resourceTitleId = computed(() => getResourceTitleId(props.resource))
|
|
|
19
21
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
20
22
|
|
|
21
23
|
const { t } = useTranslation()
|
|
22
|
-
const { formatDate } = useFormatDate()
|
|
23
24
|
</script>
|
|
24
25
|
|
|
25
26
|
<template>
|
|
@@ -42,8 +43,23 @@ const { formatDate } = useFormatDate()
|
|
|
42
43
|
</a>
|
|
43
44
|
</code>
|
|
44
45
|
</DescriptionDetails>
|
|
45
|
-
<DescriptionTerm>
|
|
46
|
+
<DescriptionTerm class="flex items-center">
|
|
46
47
|
{{ t('URL stable') }}
|
|
48
|
+
<Toggletip
|
|
49
|
+
:styled-button="false"
|
|
50
|
+
button-class="ml-1 border-transparent -outline-offset-2 inline-flex items-center justify-center rounded-sm p-1 text-gray-medium hover:bg-gray-lower transition-colors"
|
|
51
|
+
>
|
|
52
|
+
<RiInformationLine
|
|
53
|
+
class="size-4"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
<span class="sr-only">{{ t(`Qu'est-ce qu'une URL stable ?`) }}</span>
|
|
57
|
+
<template #toggletip>
|
|
58
|
+
<p class="fr-text--sm m-0">
|
|
59
|
+
{{ t(`Cette URL redirige toujours vers la dernière version du fichier. L'URL du fichier, elle, change à chaque mise à jour : privilégiez l'URL stable pour partager la ressource ou l'utiliser dans un script.`) }}
|
|
60
|
+
</p>
|
|
61
|
+
</template>
|
|
62
|
+
</Toggletip>
|
|
47
63
|
<CopyButton
|
|
48
64
|
:label="t(`Copier l'URL stable`)"
|
|
49
65
|
:copied-label="t('URL stable copiée !')"
|
|
@@ -94,11 +110,11 @@ const { formatDate } = useFormatDate()
|
|
|
94
110
|
>
|
|
95
111
|
<DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
|
|
96
112
|
<DescriptionDetails>
|
|
97
|
-
|
|
113
|
+
<FormattedDate :date="resource.created_at" />
|
|
98
114
|
</DescriptionDetails>
|
|
99
115
|
<DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
|
|
100
116
|
<DescriptionDetails>
|
|
101
|
-
|
|
117
|
+
<FormattedDate :date="resource.last_modified" />
|
|
102
118
|
</DescriptionDetails>
|
|
103
119
|
</dl>
|
|
104
120
|
<dl
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<PreviewWrapper
|
|
3
|
-
v-slot="{ data }"
|
|
4
3
|
file-type="PDF"
|
|
5
4
|
:resource="resource"
|
|
6
5
|
:max-size="config.maxPdfPreviewByteSize"
|
|
7
6
|
:load="load"
|
|
7
|
+
:fill="fill"
|
|
8
8
|
@loaded="renderAllPages"
|
|
9
9
|
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
<!-- While pdfjs downloads/parses the document, show the A4 skeleton instead of a
|
|
11
|
+
bare "loading" line — it matches the reader framing below seamlessly. -->
|
|
12
|
+
<template #loading>
|
|
13
|
+
<PdfSkeleton :fill="fill" />
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<!-- Cap the page width (A4 never needs more) and center on a light backdrop so
|
|
17
|
+
it reads like a document reader. The width cap applies in both modes since
|
|
18
|
+
the render width follows the container; `fill` only drives the height
|
|
19
|
+
(fullscreen fills down, inline stays capped). The scroll container is a plain
|
|
20
|
+
block (not a flex) so the trailing `pb` is reliably scrollable — flex + overflow
|
|
21
|
+
drops the end padding of flex items. -->
|
|
22
|
+
<template #default="{ data }">
|
|
23
|
+
<div
|
|
24
|
+
class="overflow-y-auto bg-gray-100"
|
|
25
|
+
:class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
|
|
26
|
+
>
|
|
27
|
+
<div class="flex justify-center p-4 pb-16">
|
|
28
|
+
<div
|
|
29
|
+
ref="containerRef"
|
|
30
|
+
class="w-[800px] max-w-full space-y-3"
|
|
31
|
+
>
|
|
32
|
+
<canvas
|
|
33
|
+
v-for="page in (data as PDFDocumentProxy).numPages"
|
|
34
|
+
:key="page"
|
|
35
|
+
:ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
|
|
36
|
+
class="w-full bg-white shadow-sm"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
21
42
|
</PreviewWrapper>
|
|
22
43
|
</template>
|
|
23
44
|
|
|
@@ -27,6 +48,7 @@ import * as pdfjsLib from 'pdfjs-dist'
|
|
|
27
48
|
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
|
28
49
|
import type { PDFDocumentProxy } from 'pdfjs-dist'
|
|
29
50
|
import PreviewWrapper from './PreviewWrapper.vue'
|
|
51
|
+
import PdfSkeleton from './PdfSkeleton.vue'
|
|
30
52
|
import { useComponentsConfig } from '../../config'
|
|
31
53
|
import type { Resource } from '../../types/resources'
|
|
32
54
|
|
|
@@ -34,6 +56,8 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
|
|
|
34
56
|
|
|
35
57
|
const props = defineProps<{
|
|
36
58
|
resource: Resource
|
|
59
|
+
// Fill the available height (fullscreen) instead of the default capped height.
|
|
60
|
+
fill?: boolean
|
|
37
61
|
}>()
|
|
38
62
|
|
|
39
63
|
const config = useComponentsConfig()
|