@datagouv/components-next 0.2.0 → 1.0.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/README.md +1 -1
- package/assets/main.css +49 -22
- package/dist/Control-BNCDn-8E.js +148 -0
- package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-B5lBpOl8.js} +2 -2
- package/dist/Event-BOgJUhNR.js +738 -0
- package/dist/Image-BN-4XkIn.js +247 -0
- package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-Doz1Z0BS.js} +23 -23
- package/dist/Map-BdT3i2C4.js +7609 -0
- package/dist/MapContainer.client-oiieO8H-.js +105 -0
- package/dist/OSM-CamriM9b.js +71 -0
- package/dist/PdfPreview.client-CdAhkDFJ.js +14513 -0
- package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-B0v8tGJQ.js} +3 -3
- package/dist/ScaleLine-BiesrgOv.js +165 -0
- package/dist/Swagger.client-CsK65JnG.js +4 -0
- package/dist/Tile-DCuqwNOI.js +1206 -0
- package/dist/TileImage-CmZf8EdU.js +1067 -0
- package/dist/View-DcDc7N2K.js +2858 -0
- package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-CrjHf74q.js} +17 -17
- package/dist/common-C4rDcQpp.js +243 -0
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +158 -117
- package/dist/components.css +1 -1
- package/dist/{MapContainer.client-DeSo8EvG.js → index-Bbu9rOHt.js} +4975 -21416
- package/dist/leaflet-src-7m1mB8LI.js +6338 -0
- package/dist/{main-Dgri3TQL.js → main-CiH8ZmBI.js} +56973 -51462
- package/dist/proj-CKwYjU38.js +1569 -0
- package/dist/tilecoord-YW3qEH_j.js +884 -0
- package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-Bi_bsV6C.js} +1 -1
- package/package.json +6 -2
- package/src/components/ActivityList/ActivityList.vue +6 -2
- package/src/components/AppLink.vue +4 -1
- package/src/components/Avatar.vue +2 -2
- package/src/components/AvatarWithName.vue +8 -4
- package/src/components/BouncingDots.vue +21 -0
- package/src/components/BrandedButton.vue +2 -0
- package/src/components/CopyButton.vue +19 -7
- package/src/components/DataserviceCard.vue +85 -120
- package/src/components/DatasetCard.vue +110 -171
- package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
- package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
- package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
- package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
- package/src/components/DatasetInformation/index.ts +5 -0
- package/src/components/DatasetQuality.vue +23 -16
- package/src/components/DatasetQualityInline.vue +13 -17
- package/src/components/DatasetQualityScore.vue +12 -15
- package/src/components/DatasetQualityTooltipContent.vue +3 -3
- package/src/components/DescriptionList.vue +1 -4
- package/src/components/DescriptionListDetails.vue +5 -0
- package/src/components/DescriptionListTerm.vue +5 -0
- package/src/components/DiscussionMessageCard.vue +63 -0
- package/src/components/ExtraAccordion.vue +4 -4
- package/src/components/Form/BadgeSelect.vue +35 -0
- package/src/components/Form/FormatSelect.vue +28 -0
- package/src/components/Form/GeozoneSelect.vue +52 -0
- package/src/components/Form/GranularitySelect.vue +29 -0
- package/src/components/Form/LicenseSelect.vue +30 -0
- package/src/components/Form/OrganizationSelect.vue +62 -0
- package/src/components/Form/OrganizationTypeSelect.vue +34 -0
- package/src/components/Form/ReuseTopicSelect.vue +29 -0
- package/src/components/Form/SchemaSelect.vue +30 -0
- package/src/components/Form/SearchableSelect.vue +334 -0
- package/src/components/Form/SelectGroup.vue +132 -0
- package/src/components/Form/TagSelect.vue +38 -0
- package/src/components/LeafletMap.vue +31 -0
- package/src/components/LicenseBadge.vue +24 -0
- package/src/components/LoadingBlock.vue +23 -2
- package/src/components/MarkdownViewer.vue +3 -1
- package/src/components/ObjectCard.vue +42 -0
- package/src/components/ObjectCardBadge.vue +22 -0
- package/src/components/ObjectCardHeader.vue +35 -0
- package/src/components/ObjectCardOwner.vue +43 -0
- package/src/components/ObjectCardShortDescription.vue +28 -0
- package/src/components/OrganizationCard.vue +35 -20
- package/src/components/OrganizationHorizontalCard.vue +87 -0
- package/src/components/OrganizationLogo.vue +1 -1
- package/src/components/OrganizationNameWithCertificate.vue +12 -6
- package/src/components/OwnerTypeIcon.vue +1 -0
- package/src/components/Pagination.vue +1 -1
- package/src/components/Placeholder.vue +5 -2
- package/src/components/PostCard.vue +62 -0
- package/src/components/ProgressBar.vue +31 -0
- package/src/components/RadioGroup.vue +32 -0
- package/src/components/RadioInput.vue +64 -0
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/EditButton.vue +2 -3
- package/src/components/ResourceAccordion/JsonPreview.client.vue +3 -3
- package/src/components/ResourceAccordion/MapContainer.client.vue +21 -17
- package/src/components/ResourceAccordion/Metadata.vue +11 -24
- package/src/components/ResourceAccordion/PdfPreview.client.vue +70 -74
- package/src/components/ResourceAccordion/Pmtiles.client.vue +2 -2
- package/src/components/ResourceAccordion/Preview.vue +2 -2
- package/src/components/ResourceAccordion/ResourceAccordion.vue +35 -28
- package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
- package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
- package/src/components/ResourceAccordion/XmlPreview.client.vue +3 -3
- package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +410 -0
- package/src/components/ReuseCard.vue +8 -28
- package/src/components/ReuseHorizontalCard.vue +80 -0
- package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
- package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
- package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
- package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
- package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
- package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
- package/src/components/Search/Filter/ProducerTypeFilter.vue +49 -0
- package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
- package/src/components/Search/GlobalSearch.vue +707 -0
- package/src/components/Search/SearchInput.vue +63 -0
- package/src/components/Search/Sidemenu.vue +38 -0
- package/src/components/StatBox.vue +5 -5
- package/src/components/Tag.vue +30 -0
- package/src/components/Toggletip.vue +11 -4
- package/src/components/Tooltip.vue +2 -3
- package/src/components/TopicCard.vue +134 -0
- package/src/components/radioGroupContext.ts +9 -0
- package/src/composables/useDebouncedRef.ts +31 -0
- package/src/composables/useHasTabularData.ts +15 -0
- package/src/composables/useMetrics.ts +4 -3
- package/src/composables/useResourceCapabilities.ts +131 -0
- package/src/composables/useRouteQueryBoolean.ts +10 -0
- package/src/composables/useSelectModelSync.ts +89 -0
- package/src/composables/useStableQueryParams.ts +84 -0
- package/src/composables/useTranslation.ts +2 -1
- package/src/config.ts +4 -0
- package/src/functions/api.ts +25 -6
- package/src/functions/api.types.ts +5 -3
- package/src/functions/datasets.ts +1 -29
- package/src/functions/description.ts +33 -0
- package/src/functions/helpers.ts +11 -0
- package/src/functions/markdown.ts +60 -16
- package/src/functions/metrics.ts +33 -0
- package/src/functions/organizations.ts +5 -5
- package/src/functions/resourceCapabilities.ts +55 -0
- package/src/main.ts +96 -7
- package/src/types/dataservices.ts +14 -12
- package/src/types/datasets.ts +20 -7
- package/src/types/discussions.ts +20 -0
- package/src/types/licenses.ts +3 -3
- package/src/types/organizations.ts +13 -1
- package/src/types/owned.ts +4 -2
- package/src/types/pages.ts +70 -0
- package/src/types/posts.ts +27 -0
- package/src/types/resources.ts +16 -0
- package/src/types/reuses.ts +14 -5
- package/src/types/search.ts +407 -0
- package/src/types/users.ts +12 -3
- package/dist/PdfPreview.client-COOkEkRA.js +0 -107
- package/dist/Swagger.client-CpLgaLg6.js +0 -4
- package/dist/pdf-vue3-IkJO65RH.js +0 -273
- package/dist/pdf.min-f72cfa08-CdgJTooZ.js +0 -9501
- package/src/components/DatasetInformationPanel.vue +0 -211
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="fr-input-group"
|
|
4
|
+
:class="{ 'fr-input-group--error': errorText, 'fr-input-group--warning': !errorText && warningText, 'fr-input-group--valid': validText }"
|
|
5
|
+
>
|
|
6
|
+
<label
|
|
7
|
+
:for="id"
|
|
8
|
+
:title="explanation"
|
|
9
|
+
class="fr-label"
|
|
10
|
+
:class="{ 'sr-only': hideLabel }"
|
|
11
|
+
>
|
|
12
|
+
{{ label }}
|
|
13
|
+
<span
|
|
14
|
+
v-if="required"
|
|
15
|
+
class="text-new-primary"
|
|
16
|
+
>*</span>
|
|
17
|
+
<span
|
|
18
|
+
v-if="explanation"
|
|
19
|
+
class="fr-icon-information-line"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
/>
|
|
22
|
+
<span
|
|
23
|
+
v-if="hintText"
|
|
24
|
+
class="fr-hint-text"
|
|
25
|
+
>{{ hintText }}</span>
|
|
26
|
+
</label>
|
|
27
|
+
<Combobox
|
|
28
|
+
v-slot="{ open, activeOption }"
|
|
29
|
+
v-model="model"
|
|
30
|
+
:multiple
|
|
31
|
+
:by="compareTwoOptions"
|
|
32
|
+
:aria-describedby="ariaDescribedBy"
|
|
33
|
+
:disabled="loading"
|
|
34
|
+
nullable
|
|
35
|
+
>
|
|
36
|
+
<div class="relative mt-1">
|
|
37
|
+
<div
|
|
38
|
+
ref="floatingReference"
|
|
39
|
+
class="relative w-full cursor-default overflow-hidden bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm"
|
|
40
|
+
>
|
|
41
|
+
<ComboboxInput
|
|
42
|
+
:id
|
|
43
|
+
class="input shadow-input group-data-[input-color=blue]/form:shadow-input-blue !pr-10"
|
|
44
|
+
:class="showClearButton ? '!pr-[4.5rem]' : '!pr-10'"
|
|
45
|
+
:display-value="(option: unknown) => option ? displayValue(option as ModelType) : ''"
|
|
46
|
+
:placeholder
|
|
47
|
+
@change="query = $event.target.value"
|
|
48
|
+
/>
|
|
49
|
+
<AnimatedLoader
|
|
50
|
+
v-if="loading"
|
|
51
|
+
class="absolute text-lg top-2 right-3 flex items-center justify-end hover:!bg-transparent"
|
|
52
|
+
/>
|
|
53
|
+
<div
|
|
54
|
+
v-else
|
|
55
|
+
class="absolute inset-y-0 flex items-center justify-end pr-2"
|
|
56
|
+
:class="{
|
|
57
|
+
'right-0': open,
|
|
58
|
+
'inset-x-0': !open,
|
|
59
|
+
}"
|
|
60
|
+
>
|
|
61
|
+
<ComboboxButton
|
|
62
|
+
v-if="! open"
|
|
63
|
+
class="w-full h-full hover:!bg-transparent"
|
|
64
|
+
:data-testid="`searchable-select-${simpleSlug(label)}`"
|
|
65
|
+
/>
|
|
66
|
+
<button
|
|
67
|
+
v-if="showClearButton"
|
|
68
|
+
type="button"
|
|
69
|
+
class="p-2"
|
|
70
|
+
:title="t('Effacer')"
|
|
71
|
+
@click.prevent="model = null"
|
|
72
|
+
>
|
|
73
|
+
<RiDeleteBinLine class="size-4 text-gray-800" />
|
|
74
|
+
</button>
|
|
75
|
+
<ComboboxButton class="p-2">
|
|
76
|
+
<RiArrowDownSLine class="size-4 text-gray-800" />
|
|
77
|
+
</ComboboxButton>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<TransitionRoot
|
|
81
|
+
leave="transition ease-in duration-100"
|
|
82
|
+
leave-from="opacity-100"
|
|
83
|
+
leave-to="opacity-0"
|
|
84
|
+
@after-leave="query = ''"
|
|
85
|
+
>
|
|
86
|
+
<ComboboxOptions
|
|
87
|
+
ref="popover"
|
|
88
|
+
:style="floatingStyles"
|
|
89
|
+
class="z-10 mt-1 absolute max-h-60 min-w-80 w-full overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm pl-0"
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
v-if="!filteredAndGroupedOptions && query !== ''"
|
|
93
|
+
class="relative cursor-default select-none px-4 py-2 text-gray-700"
|
|
94
|
+
>
|
|
95
|
+
{{ t('Aucun résultat') }}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
v-for="(groupOptions, group) in filteredAndGroupedOptions"
|
|
100
|
+
:key="group"
|
|
101
|
+
>
|
|
102
|
+
<li
|
|
103
|
+
v-if="group"
|
|
104
|
+
class="relative select-none py-4 px-4 list-none bg-gray-100 uppercase text-gray-800 font-semibold text-xs"
|
|
105
|
+
>
|
|
106
|
+
{{ group }}
|
|
107
|
+
</li>
|
|
108
|
+
<ComboboxOption
|
|
109
|
+
v-for="option in groupOptions"
|
|
110
|
+
:key="getOptionId(toValue(option))"
|
|
111
|
+
v-slot="comboboxSlot"
|
|
112
|
+
as="template"
|
|
113
|
+
:value="option"
|
|
114
|
+
>
|
|
115
|
+
<li
|
|
116
|
+
class="relative cursor-default select-none py-2 pr-4 list-none flex items-center gap-2 text-gray-900"
|
|
117
|
+
:class="{
|
|
118
|
+
'bg-gray-lower': isActive(activeOption, option),
|
|
119
|
+
'pl-2': comboboxSlot.selected,
|
|
120
|
+
'pl-6': !comboboxSlot.selected,
|
|
121
|
+
}"
|
|
122
|
+
>
|
|
123
|
+
<div
|
|
124
|
+
class="flex items-center justify-center aspect-square"
|
|
125
|
+
>
|
|
126
|
+
<RiCheckLine
|
|
127
|
+
v-if="comboboxSlot.selected"
|
|
128
|
+
class="size-4 text-new-primary"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
<slot
|
|
132
|
+
name="option"
|
|
133
|
+
v-bind="{ option, active: isActive(activeOption, option) as boolean }"
|
|
134
|
+
>
|
|
135
|
+
{{ displayValue((multiple ? [option] : option) as ModelType) }}
|
|
136
|
+
</slot>
|
|
137
|
+
</li>
|
|
138
|
+
</ComboboxOption>
|
|
139
|
+
</div>
|
|
140
|
+
</ComboboxOptions>
|
|
141
|
+
</TransitionRoot>
|
|
142
|
+
</div>
|
|
143
|
+
</Combobox>
|
|
144
|
+
<p
|
|
145
|
+
v-if="errorText"
|
|
146
|
+
:id="errorTextId"
|
|
147
|
+
class="fr-error-text"
|
|
148
|
+
>
|
|
149
|
+
{{ errorText }}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
</template>
|
|
153
|
+
|
|
154
|
+
<script setup lang="ts" generic="T extends string | number | object, Multiple extends true | false">
|
|
155
|
+
import { ref, computed, onMounted, useId, toValue, useTemplateRef } from 'vue'
|
|
156
|
+
import { useFloating, autoUpdate, autoPlacement } from '@floating-ui/vue'
|
|
157
|
+
import { watchDebounced } from '@vueuse/core'
|
|
158
|
+
import {
|
|
159
|
+
Combobox,
|
|
160
|
+
ComboboxInput,
|
|
161
|
+
ComboboxButton,
|
|
162
|
+
ComboboxOptions,
|
|
163
|
+
ComboboxOption,
|
|
164
|
+
TransitionRoot,
|
|
165
|
+
} from '@headlessui/vue'
|
|
166
|
+
import { RiArrowDownSLine, RiCheckLine, RiDeleteBinLine } from '@remixicon/vue'
|
|
167
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
168
|
+
import AnimatedLoader from '../AnimatedLoader.vue'
|
|
169
|
+
|
|
170
|
+
type ModelType = Multiple extends false ? T : Array<T>
|
|
171
|
+
|
|
172
|
+
const props = withDefaults(defineProps<{
|
|
173
|
+
validText?: string
|
|
174
|
+
errorText?: string | null
|
|
175
|
+
warningText?: string | null
|
|
176
|
+
hintText?: string
|
|
177
|
+
explanation?: string
|
|
178
|
+
label: string
|
|
179
|
+
placeholder?: string
|
|
180
|
+
loading?: boolean
|
|
181
|
+
hideLabel?: boolean
|
|
182
|
+
|
|
183
|
+
getOptionId?: (option: T) => string | number
|
|
184
|
+
groupBy?: (option: T) => string
|
|
185
|
+
displayValue?: (option: ModelType) => string
|
|
186
|
+
filter?: (option: T, query: string) => boolean
|
|
187
|
+
options?: Array<T>
|
|
188
|
+
suggest?: (query: string) => Promise<Array<T>>
|
|
189
|
+
allowNewOption?: (query: string) => T
|
|
190
|
+
|
|
191
|
+
required?: boolean
|
|
192
|
+
multiple: Multiple
|
|
193
|
+
}>(), {
|
|
194
|
+
required: false,
|
|
195
|
+
loading: false,
|
|
196
|
+
hideLabel: false,
|
|
197
|
+
|
|
198
|
+
displayValue: (): string => '',
|
|
199
|
+
|
|
200
|
+
groupBy: (): string => '',
|
|
201
|
+
getOptionId: (option: T): string | number => {
|
|
202
|
+
if (typeof option === 'string') return option
|
|
203
|
+
if (typeof option === 'number') return option
|
|
204
|
+
if (typeof option === 'object' && 'id' in option) return option.id as string
|
|
205
|
+
|
|
206
|
+
throw new Error('Please set getOptionId()')
|
|
207
|
+
},
|
|
208
|
+
filter: (option: T, query: string): boolean => {
|
|
209
|
+
const searchables = []
|
|
210
|
+
|
|
211
|
+
if (typeof option === 'string') searchables.push(option)
|
|
212
|
+
if (typeof option === 'number') searchables.push(option.toString())
|
|
213
|
+
if (typeof option === 'object') {
|
|
214
|
+
for (const value of Object.values(option)) {
|
|
215
|
+
if (typeof value !== 'string') continue
|
|
216
|
+
|
|
217
|
+
searchables.push(value)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const searchable of searchables) {
|
|
222
|
+
if (searchable.toLocaleLowerCase().includes(query.toLocaleLowerCase())) {
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const model = defineModel<ModelType | null>()
|
|
232
|
+
|
|
233
|
+
const { t } = useTranslation()
|
|
234
|
+
|
|
235
|
+
const id = useId()
|
|
236
|
+
const errorTextId = useId()
|
|
237
|
+
|
|
238
|
+
const ariaDescribedBy = computed(() => {
|
|
239
|
+
if (props.errorText) return errorTextId
|
|
240
|
+
return ''
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const showClearButton = computed(() => !props.required && !props.multiple && model.value)
|
|
244
|
+
|
|
245
|
+
const query = ref('')
|
|
246
|
+
|
|
247
|
+
const suggestedOptions = ref<Array<T> | null>(null)
|
|
248
|
+
const fetchSuggests = async () => {
|
|
249
|
+
if (!props.suggest) return
|
|
250
|
+
|
|
251
|
+
const savedQuery = query.value
|
|
252
|
+
const options = await props.suggest(query.value)
|
|
253
|
+
if (savedQuery === query.value) {
|
|
254
|
+
suggestedOptions.value = options
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function fetchSuggestsQuery(q: string) {
|
|
259
|
+
query.value = q
|
|
260
|
+
return fetchSuggests()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
defineExpose({
|
|
264
|
+
fetchSuggestsQuery,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
onMounted(async () => {
|
|
268
|
+
await fetchSuggests()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
watchDebounced(query, async () => {
|
|
272
|
+
await fetchSuggests()
|
|
273
|
+
}, { debounce: 400, maxWait: 800 })
|
|
274
|
+
|
|
275
|
+
const filteredOptions = computed<Array<T>>(() => {
|
|
276
|
+
if (props.suggest) {
|
|
277
|
+
return suggestedOptions.value as Array<T>
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!props.options) throw new Error('Please set options or suggest')
|
|
281
|
+
|
|
282
|
+
if (!query.value) return props.options
|
|
283
|
+
return props.options.filter(option => props.filter(option, query.value))
|
|
284
|
+
})
|
|
285
|
+
const filteredOptionsWithNewOption = computed(() => {
|
|
286
|
+
if (!props.allowNewOption || !query.value) return filteredOptions.value
|
|
287
|
+
|
|
288
|
+
const newOption = props.allowNewOption(query.value)
|
|
289
|
+
if (filteredOptions.value.find(option => compareTwoOptions(option, newOption))) return filteredOptions.value
|
|
290
|
+
|
|
291
|
+
return [
|
|
292
|
+
newOption,
|
|
293
|
+
...filteredOptions.value,
|
|
294
|
+
]
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const filteredAndGroupedOptions = computed<Record<string, Array<T>>>(() => {
|
|
298
|
+
if (!filteredOptionsWithNewOption.value) return {}
|
|
299
|
+
|
|
300
|
+
const groups = {} as Record<string, Array<T>>
|
|
301
|
+
for (const option of filteredOptionsWithNewOption.value) {
|
|
302
|
+
const group = props.groupBy(option)
|
|
303
|
+
groups[group] = groups[group] || []
|
|
304
|
+
groups[group].push(option)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return groups
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
function compareTwoOptions(a: T | null, b: T | null) {
|
|
311
|
+
if (a === b) return true
|
|
312
|
+
if (!a || !b) return false
|
|
313
|
+
|
|
314
|
+
return props.getOptionId(a) === props.getOptionId(b)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isActive(activeOption: T, currentOption: T) {
|
|
318
|
+
return activeOption ? props.getOptionId(activeOption) === props.getOptionId(currentOption) : false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function simpleSlug(text: string) {
|
|
322
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const referenceRef = useTemplateRef('floatingReference')
|
|
326
|
+
const floatingRef = useTemplateRef<InstanceType<typeof ComboboxOptions>>('popover')
|
|
327
|
+
const { floatingStyles } = useFloating(referenceRef, floatingRef, {
|
|
328
|
+
middleware: [autoPlacement({
|
|
329
|
+
allowedPlacements: ['bottom-start', 'bottom', 'bottom-end'],
|
|
330
|
+
crossAxis: true,
|
|
331
|
+
})],
|
|
332
|
+
whileElementsMounted: autoUpdate,
|
|
333
|
+
})
|
|
334
|
+
</script>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="fr-select-group"
|
|
4
|
+
:class="selectGroupClass"
|
|
5
|
+
>
|
|
6
|
+
<label
|
|
7
|
+
v-if="!hideLabel"
|
|
8
|
+
class="fr-label"
|
|
9
|
+
:for="id"
|
|
10
|
+
>
|
|
11
|
+
{{ label }}
|
|
12
|
+
<span
|
|
13
|
+
v-if="required"
|
|
14
|
+
class="text-new-primary"
|
|
15
|
+
>*</span>
|
|
16
|
+
<span
|
|
17
|
+
v-if="hintText"
|
|
18
|
+
class="fr-hint-text"
|
|
19
|
+
>{{ hintText }}</span>
|
|
20
|
+
</label>
|
|
21
|
+
<select
|
|
22
|
+
:id="id"
|
|
23
|
+
v-model="model"
|
|
24
|
+
class="fr-select group-data-[input-color=blue]/form:!shadow-input-blue"
|
|
25
|
+
:class="{ 'fr-select--error': hasError, 'fr-select--valid': isValid }"
|
|
26
|
+
:aria-describedby="ariaDescribedBy"
|
|
27
|
+
:aria-label="hideLabel ? label : undefined"
|
|
28
|
+
:required="required"
|
|
29
|
+
:disabled="disabled"
|
|
30
|
+
>
|
|
31
|
+
<option
|
|
32
|
+
v-if="! hideNullOption"
|
|
33
|
+
:value="null"
|
|
34
|
+
disabled
|
|
35
|
+
hidden
|
|
36
|
+
>
|
|
37
|
+
{{ t('Sélectionner une option') }}
|
|
38
|
+
</option>
|
|
39
|
+
<option
|
|
40
|
+
v-for="option in options"
|
|
41
|
+
:key="String(option.value ?? option.label)"
|
|
42
|
+
:value="option.value"
|
|
43
|
+
:disabled="option.disabled"
|
|
44
|
+
:hidden="option.hidden"
|
|
45
|
+
>
|
|
46
|
+
{{ option.label }}
|
|
47
|
+
</option>
|
|
48
|
+
</select>
|
|
49
|
+
<p
|
|
50
|
+
v-if="isValid"
|
|
51
|
+
:id="validTextId"
|
|
52
|
+
class="fr-valid-text"
|
|
53
|
+
>
|
|
54
|
+
{{ validText }}
|
|
55
|
+
</p>
|
|
56
|
+
<p
|
|
57
|
+
v-else-if="hasError"
|
|
58
|
+
:id="errorTextId"
|
|
59
|
+
class="fr-error-text"
|
|
60
|
+
>
|
|
61
|
+
{{ errorText }}
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { computed, useId } from 'vue'
|
|
68
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
69
|
+
|
|
70
|
+
export type SelectOption = {
|
|
71
|
+
label: string
|
|
72
|
+
value?: string | boolean | null | undefined
|
|
73
|
+
disabled?: boolean
|
|
74
|
+
hidden?: boolean
|
|
75
|
+
selected?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SelectGroupProps = {
|
|
79
|
+
disabled?: boolean
|
|
80
|
+
errorText?: string | null
|
|
81
|
+
hasError?: boolean
|
|
82
|
+
hintText?: string
|
|
83
|
+
isValid?: boolean
|
|
84
|
+
label: string
|
|
85
|
+
hideLabel?: boolean
|
|
86
|
+
modelValue?: string | boolean | null
|
|
87
|
+
options: Array<SelectOption>
|
|
88
|
+
required?: boolean
|
|
89
|
+
validText?: string
|
|
90
|
+
hideNullOption?: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const model = defineModel<string | boolean | null>()
|
|
94
|
+
|
|
95
|
+
const props = withDefaults(defineProps<SelectGroupProps>(), {
|
|
96
|
+
disabled: false,
|
|
97
|
+
errorText: '',
|
|
98
|
+
hasError: false,
|
|
99
|
+
hintText: '',
|
|
100
|
+
isValid: false,
|
|
101
|
+
modelValue: undefined,
|
|
102
|
+
required: false,
|
|
103
|
+
validText: '',
|
|
104
|
+
hideLabel: false,
|
|
105
|
+
hideNullOption: false,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const { t } = useTranslation()
|
|
109
|
+
|
|
110
|
+
const id = useId()
|
|
111
|
+
|
|
112
|
+
const errorTextId = computed(() => id + '-desc-error')
|
|
113
|
+
const validTextId = computed(() => id + '-desc-valid')
|
|
114
|
+
const ariaDescribedBy = computed(() => {
|
|
115
|
+
let describedBy = ''
|
|
116
|
+
if (props.isValid) {
|
|
117
|
+
describedBy += ' ' + validTextId.value
|
|
118
|
+
}
|
|
119
|
+
else if (props.hasError) {
|
|
120
|
+
describedBy += ' ' + errorTextId.value
|
|
121
|
+
}
|
|
122
|
+
return describedBy
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const selectGroupClass = computed(() => {
|
|
126
|
+
return {
|
|
127
|
+
'fr-select-group--disabled': props.disabled,
|
|
128
|
+
'fr-select-group--error': props.hasError,
|
|
129
|
+
'fr-select-group--valid': props.isValid,
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<SearchableSelect
|
|
3
|
+
v-model="model"
|
|
4
|
+
:options="[]"
|
|
5
|
+
:suggest="suggestTags"
|
|
6
|
+
:label="t('Mots clés')"
|
|
7
|
+
:placeholder="t('Tous les mots clés')"
|
|
8
|
+
:get-option-id="(tag: string) => tag"
|
|
9
|
+
:display-value="(tag: string) => tag"
|
|
10
|
+
:multiple="false"
|
|
11
|
+
/>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { ofetch } from 'ofetch'
|
|
16
|
+
import { useComponentsConfig } from '../../config'
|
|
17
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
18
|
+
import { useStringSelectSync } from '../../composables/useSelectModelSync'
|
|
19
|
+
import SearchableSelect from './SearchableSelect.vue'
|
|
20
|
+
|
|
21
|
+
const model = defineModel<string | null>({ default: null })
|
|
22
|
+
const id = defineModel<string | undefined>('id')
|
|
23
|
+
|
|
24
|
+
const config = useComponentsConfig()
|
|
25
|
+
const { t } = useTranslation()
|
|
26
|
+
|
|
27
|
+
useStringSelectSync({ model, id })
|
|
28
|
+
|
|
29
|
+
type TagSuggest = { text: string }
|
|
30
|
+
|
|
31
|
+
async function suggestTags(q: string) {
|
|
32
|
+
const tags = await ofetch<TagSuggest[]>('/api/1/tags/suggest/', {
|
|
33
|
+
baseURL: config.apiBase,
|
|
34
|
+
query: { q, size: 20 },
|
|
35
|
+
})
|
|
36
|
+
return tags.map(tag => tag.text)
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="containerRef"
|
|
4
|
+
class="h-[400px] w-[400px] fr-raw-link"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { ref, onMounted } from 'vue'
|
|
10
|
+
import type { GeoJsonObject } from 'geojson'
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
geojson: GeoJsonObject
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
17
|
+
|
|
18
|
+
onMounted(async () => {
|
|
19
|
+
await import('leaflet/dist/leaflet.css')
|
|
20
|
+
const L = await import('leaflet')
|
|
21
|
+
|
|
22
|
+
if (!containerRef.value) return
|
|
23
|
+
const map = L.map(containerRef.value)
|
|
24
|
+
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
25
|
+
maxZoom: 19,
|
|
26
|
+
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
27
|
+
}).addTo(map)
|
|
28
|
+
const geoJSON = L.geoJSON(props.geojson).addTo(map)
|
|
29
|
+
map.fitBounds(geoJSON.getBounds())
|
|
30
|
+
})
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppLink
|
|
3
|
+
v-if="license && license.url"
|
|
4
|
+
:to="license.url"
|
|
5
|
+
class="px-1 py-[2px] font-mono bg-gray-some text-gray-medium rounded"
|
|
6
|
+
>
|
|
7
|
+
{{ license.title }}
|
|
8
|
+
</AppLink>
|
|
9
|
+
<span
|
|
10
|
+
v-else-if="license"
|
|
11
|
+
class="px-1 py-[2px] font-mono bg-gray-some text-gray-medium rounded"
|
|
12
|
+
>
|
|
13
|
+
{{ license.title }}
|
|
14
|
+
</span>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import type { License } from '../types/licenses'
|
|
19
|
+
import AppLink from './AppLink.vue'
|
|
20
|
+
|
|
21
|
+
defineProps<{
|
|
22
|
+
license: License
|
|
23
|
+
}>()
|
|
24
|
+
</script>
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative">
|
|
3
3
|
<div :class="{ 'opacity-50 min-h-64': loading }">
|
|
4
|
-
<slot
|
|
4
|
+
<slot
|
|
5
|
+
v-if="!loading && !hasError && data"
|
|
6
|
+
:data="data"
|
|
7
|
+
/>
|
|
8
|
+
<slot
|
|
9
|
+
v-else-if="hasError"
|
|
10
|
+
name="error"
|
|
11
|
+
>
|
|
12
|
+
<div class="text-center py-8 text-gray-500">
|
|
13
|
+
Une erreur est survenue lors du chargement.
|
|
14
|
+
</div>
|
|
15
|
+
</slot>
|
|
5
16
|
</div>
|
|
6
17
|
<div
|
|
7
18
|
v-if="loading"
|
|
@@ -12,15 +23,25 @@
|
|
|
12
23
|
</div>
|
|
13
24
|
</template>
|
|
14
25
|
|
|
15
|
-
<script setup lang="ts">
|
|
26
|
+
<script setup lang="ts" generic="T">
|
|
16
27
|
import { computed } from 'vue'
|
|
17
28
|
import AnimatedLoader from './AnimatedLoader.vue'
|
|
18
29
|
|
|
19
30
|
const props = defineProps<{
|
|
20
31
|
status: string
|
|
32
|
+
data: T | null | undefined
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
defineSlots<{
|
|
36
|
+
default: (props: { data: NonNullable<T> }) => unknown
|
|
37
|
+
error: () => unknown
|
|
21
38
|
}>()
|
|
22
39
|
|
|
23
40
|
const loading = computed(() => {
|
|
24
41
|
return props.status === 'idle' || props.status === 'pending'
|
|
25
42
|
})
|
|
43
|
+
|
|
44
|
+
const hasError = computed(() => {
|
|
45
|
+
return props.status === 'error'
|
|
46
|
+
})
|
|
26
47
|
</script>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<template>
|
|
3
3
|
<div
|
|
4
4
|
:class="size === 'sm' ? markdownSmClasses : markdownClasses"
|
|
5
|
-
v-html="formatMarkdown(content, minHeading)"
|
|
5
|
+
v-html="formatMarkdown(content, { minDepth: minHeading, noHeadings })"
|
|
6
6
|
/>
|
|
7
7
|
</template>
|
|
8
8
|
|
|
@@ -12,9 +12,11 @@ import { formatMarkdown, markdownClasses, markdownSmClasses } from '../functions
|
|
|
12
12
|
withDefaults(defineProps<{
|
|
13
13
|
content: string
|
|
14
14
|
minHeading?: 1 | 2 | 3 | 4 | 5 | 6
|
|
15
|
+
noHeadings?: boolean
|
|
15
16
|
size?: 'sm' | 'md'
|
|
16
17
|
}>(), {
|
|
17
18
|
minHeading: 3,
|
|
18
19
|
size: 'sm',
|
|
20
|
+
noHeadings: false,
|
|
19
21
|
})
|
|
20
22
|
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article
|
|
3
|
+
class="p-4 border bg-white border-gray-default relative hover:bg-gray-some fr-enlarge-link"
|
|
4
|
+
:class="articleClass"
|
|
5
|
+
>
|
|
6
|
+
<slot name="badge" />
|
|
7
|
+
<div class="flex flex-wrap md:flex-nowrap gap-4 items-start">
|
|
8
|
+
<div
|
|
9
|
+
v-if="$slots.media"
|
|
10
|
+
class="flex-none"
|
|
11
|
+
>
|
|
12
|
+
<div
|
|
13
|
+
class="flex justify-center items-center border border-gray-lower bg-[#fff] rounded-md overflow-hidden"
|
|
14
|
+
:class="mediaContainerClass"
|
|
15
|
+
>
|
|
16
|
+
<slot name="media" />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="flex-1 overflow-hidden space-y-1">
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</article>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { computed } from 'vue'
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<{
|
|
30
|
+
articleClass?: string | string[] | Record<string, boolean>
|
|
31
|
+
mediaSize?: 'sm' | 'lg'
|
|
32
|
+
}>(), {
|
|
33
|
+
mediaSize: 'sm',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const mediaContainerClass = computed(() => {
|
|
37
|
+
if (props.mediaSize === 'lg') {
|
|
38
|
+
return 'w-[225px] h-[120px]'
|
|
39
|
+
}
|
|
40
|
+
return 'p-2'
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="absolute top-0 left-4 fr-grid-row fr-grid-row--middle fr-mt-n3v">
|
|
3
|
+
<p class="fr-badge fr-badge--sm fr-badge--mention-grey text-gray-medium">
|
|
4
|
+
<component
|
|
5
|
+
:is="icon"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
class="size-4 mr-1"
|
|
8
|
+
/>
|
|
9
|
+
<slot />
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
// Only one ObjectCardBadge should be rendered at a time (use v-if/v-else-if),
|
|
16
|
+
// because the absolute positioning will cause multiple badges to overlap.
|
|
17
|
+
import type { Component } from 'vue'
|
|
18
|
+
|
|
19
|
+
defineProps<{
|
|
20
|
+
icon: Component
|
|
21
|
+
}>()
|
|
22
|
+
</script>
|