@dative-gpi/foundation-shared-components 0.0.7 → 0.0.9

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 (139) hide show
  1. package/{models/FSButtons.ts → aliases/FSButton.ts} +24 -21
  2. package/aliases/index.ts +1 -0
  3. package/components/FSAutocompleteField.vue +207 -0
  4. package/components/FSBadge.vue +38 -0
  5. package/components/FSBreadcrumbs.vue +49 -55
  6. package/components/FSButton.vue +116 -101
  7. package/components/FSCalendar.vue +184 -0
  8. package/components/FSCalendarTwin.vue +412 -0
  9. package/components/FSCard.vue +77 -0
  10. package/components/FSCarousel.vue +63 -0
  11. package/components/FSCheckbox.vue +111 -104
  12. package/components/FSChip.vue +140 -0
  13. package/components/FSClock.vue +172 -0
  14. package/components/FSCol.vue +104 -98
  15. package/components/FSColor.vue +61 -64
  16. package/components/FSColorIcon.vue +67 -0
  17. package/components/FSContainer.vue +64 -0
  18. package/components/FSDateField.vue +211 -0
  19. package/components/FSDateRangeField.vue +225 -0
  20. package/components/FSDateTimeField.vue +257 -0
  21. package/components/FSDateTimeRangeField.vue +286 -0
  22. package/components/FSDialog.vue +103 -0
  23. package/components/FSDivider.vue +39 -0
  24. package/components/FSFadeOut.vue +49 -59
  25. package/components/FSFileButton.vue +245 -0
  26. package/components/FSHeaderButton.vue +17 -0
  27. package/components/FSIcon.vue +23 -23
  28. package/components/FSIconField.vue +232 -0
  29. package/components/FSImage.vue +142 -0
  30. package/components/FSLoadTile.vue +93 -0
  31. package/components/FSNumberField.vue +51 -53
  32. package/components/FSPasswordField.vue +99 -99
  33. package/components/FSRadio.vue +107 -110
  34. package/components/FSRadioGroup.vue +55 -57
  35. package/components/FSRemoveDialog.vue +123 -0
  36. package/components/FSRichTextField.vue +551 -0
  37. package/components/FSRow.vue +110 -104
  38. package/components/FSSearchField.vue +114 -105
  39. package/components/FSSelectField.vue +188 -0
  40. package/components/FSSlideGroup.vue +45 -49
  41. package/components/FSSlider.vue +130 -0
  42. package/components/FSSpan.vue +53 -37
  43. package/components/FSSubmitDialog.vue +165 -0
  44. package/components/FSSwitch.vue +110 -109
  45. package/components/FSTab.vue +61 -61
  46. package/components/FSTabs.vue +53 -55
  47. package/components/FSTag.vue +88 -84
  48. package/components/FSTagField.vue +183 -128
  49. package/components/FSTagGroup.vue +38 -45
  50. package/components/FSText.vue +74 -64
  51. package/components/FSTextArea.vue +209 -0
  52. package/components/FSTextField.vue +152 -149
  53. package/components/FSTile.vue +90 -0
  54. package/components/FSToggleSet.vue +282 -0
  55. package/components/FSTooltip.vue +21 -0
  56. package/components/FSWindow.vue +26 -16
  57. package/components/FSWrapGroup.vue +44 -47
  58. package/components/deviceOrganisations/FSConnectivity.vue +114 -0
  59. package/components/deviceOrganisations/FSStatus.vue +117 -0
  60. package/components/deviceOrganisations/FSStatusesCarousel.vue +105 -0
  61. package/components/deviceOrganisations/FSStatusesRow.vue +66 -0
  62. package/components/deviceOrganisations/FSWorstAlert.vue +165 -0
  63. package/components/lists/FSDataIteratorGroup.vue +7 -0
  64. package/components/lists/FSDataIteratorItem.vue +103 -0
  65. package/components/lists/FSDataTable.vue +964 -0
  66. package/components/lists/FSFilterButton.vue +176 -0
  67. package/components/lists/FSHeaderButton.vue +99 -0
  68. package/components/lists/FSHiddenButton.vue +79 -0
  69. package/components/tiles/FSDeviceOrganisationTileUI.vue +232 -0
  70. package/components/tiles/FSGroupTileUI.vue +192 -0
  71. package/composables/index.ts +2 -1
  72. package/composables/useBreakpoints.ts +33 -0
  73. package/composables/useColors.ts +53 -23
  74. package/composables/useSlots.ts +43 -0
  75. package/index.ts +6 -0
  76. package/models/breadcrumbs.ts +8 -0
  77. package/models/colors.ts +17 -0
  78. package/models/deviceAlerts.ts +10 -0
  79. package/models/deviceConnectivities.ts +11 -0
  80. package/models/deviceStatuses.ts +16 -0
  81. package/models/dispositions.ts +33 -0
  82. package/models/index.ts +9 -0
  83. package/models/modelStatuses.ts +11 -0
  84. package/models/rules.ts +50 -0
  85. package/models/toggleSets.ts +7 -0
  86. package/package.json +13 -4
  87. package/plugins/colorPlugin.ts +2 -2
  88. package/shims-plugin.d.ts +1 -1
  89. package/styles/components/fs_autocomplete_field.scss +123 -0
  90. package/styles/components/fs_button.scss +4 -14
  91. package/styles/components/fs_calendar.scss +138 -0
  92. package/styles/components/fs_card.scss +4 -0
  93. package/styles/components/fs_carousel.scss +4 -0
  94. package/styles/components/fs_chip.scss +33 -0
  95. package/styles/components/fs_clock.scss +43 -0
  96. package/styles/components/fs_col.scss +2 -0
  97. package/styles/components/fs_color_icon.scss +37 -0
  98. package/styles/components/fs_container.scss +16 -0
  99. package/styles/components/fs_data_iterator_item.scss +19 -0
  100. package/styles/components/fs_data_table.scss +97 -0
  101. package/styles/components/fs_date_field.scss +8 -0
  102. package/styles/components/fs_dialog.scss +30 -0
  103. package/styles/components/fs_divider.scss +5 -0
  104. package/styles/components/fs_fade_out.scss +10 -2
  105. package/styles/components/fs_filter_button.scss +12 -0
  106. package/styles/components/fs_header_button.scss +4 -0
  107. package/styles/components/fs_icon.scss +14 -4
  108. package/styles/components/fs_icon_field.scss +12 -0
  109. package/styles/components/fs_image.scss +7 -0
  110. package/styles/components/fs_load_tile.scss +49 -0
  111. package/styles/components/fs_password_field.scss +2 -2
  112. package/styles/components/fs_rich_text_field.scss +67 -0
  113. package/styles/components/fs_row.scss +4 -1
  114. package/styles/components/fs_select_field.scss +71 -0
  115. package/styles/components/fs_slide_group.scss +6 -0
  116. package/styles/components/fs_slider.scss +40 -0
  117. package/styles/components/fs_span.scss +8 -0
  118. package/styles/components/fs_submit_dialog.scss +9 -0
  119. package/styles/components/fs_tabs.scss +4 -0
  120. package/styles/components/fs_tag_field.scss +6 -8
  121. package/styles/components/fs_text_area.scss +105 -0
  122. package/styles/components/fs_text_field.scss +23 -15
  123. package/styles/components/fs_tile.scss +33 -0
  124. package/styles/components/fs_tooltip.scss +5 -0
  125. package/styles/components/fs_wrap_group.scss +7 -8
  126. package/styles/components/index.scss +26 -0
  127. package/styles/globals/breakpoints.scss +7 -0
  128. package/styles/globals/overrides.scss +20 -7
  129. package/styles/globals/text_fonts.scss +8 -8
  130. package/themes/default.ts +1 -11
  131. package/utils/css.ts +11 -0
  132. package/utils/icons.ts +75416 -0
  133. package/utils/index.ts +5 -0
  134. package/utils/levenshtein.ts +97 -0
  135. package/utils/lexical.ts +27 -0
  136. package/utils/sort.ts +9 -0
  137. package/composables/useTouch.ts +0 -9
  138. package/models/FSTags.ts +0 -8
  139. package/models/FSTextFields.ts +0 -17
@@ -0,0 +1,964 @@
1
+ <template>
2
+ <FSCol
3
+ gap="16px"
4
+ >
5
+ <FSRow
6
+ align="bottom-center"
7
+ >
8
+ <FSSearchField
9
+ prependInnerIcon="mdi-magnify"
10
+ :hideHeader="true"
11
+ v-model="innerSearch"
12
+ />
13
+ <FSButton
14
+ prependIcon="mdi-filter-variant"
15
+ :variant="showFilters ? 'full' : 'standard'"
16
+ @click="showFilters = !showFilters"
17
+ />
18
+ <slot name="toolbar" />
19
+ <v-spacer />
20
+ <FSToggleSet
21
+ v-if="!$props.disableTable && !$props.disableIterator"
22
+ :values="modeOptions"
23
+ :required="true"
24
+ v-model="innerMode"
25
+ />
26
+ </FSRow>
27
+ <FSRow
28
+ v-if="(showFilters && filterableHeaders.length > 0) || hiddenHeaders.length > 0"
29
+ >
30
+ <template v-if="showFilters">
31
+ <FSFilterButton
32
+ v-for="(header, index) in filterableHeaders"
33
+ :key="index"
34
+ :header="header"
35
+ :filters="filters[header.value]"
36
+ @update:filter="(value) => toggleFilter(header.value, value)"
37
+ >
38
+ <template #default="{ filter }">
39
+ <slot :name="filterSlot(header)" v-bind="{ filter }" />
40
+ </template>
41
+ </FSFilterButton>
42
+ <FSChip
43
+ v-if="resetable"
44
+ variant="standard"
45
+ :label="$tr('ui.data-table.reset-filters', 'Reset')"
46
+ :color="ColorEnum.Error"
47
+ :editable="true"
48
+ @click="resetFilter"
49
+ />
50
+ </template>
51
+ <FSHiddenButton
52
+ v-if="hiddenHeaders.length > 0"
53
+ :headers="hiddenHeaders"
54
+ :color="$props.color"
55
+ @update:show="(value) => updateHeader(value, 'hidden', false)"
56
+ />
57
+ </FSRow>
58
+ <template v-if="innerMode === 'table'">
59
+ <v-data-table
60
+ v-if="!isExtraSmall"
61
+ selectStrategy="all"
62
+ :itemValue="$props.itemValue"
63
+ :showSelect="$props.showSelect"
64
+ :singleSelect="$props.singleSelect"
65
+ :headers="extraHeaders.concat(innerHeaders)"
66
+ :groupBy="$props.groupBy ? [$props.groupBy] : []"
67
+ :items="innerItems"
68
+ :fixedHeader="true"
69
+ :multiSort="true"
70
+ :hover="true"
71
+ :style="style"
72
+ :class="classes"
73
+ :page="innerPage"
74
+ :itemsPerPage="innerRowsPerPage"
75
+ :modelValue="innerValue"
76
+ @auxclick:row="onClickRow"
77
+ @click:row="onClickRow"
78
+ v-model:sortBy="innerSortBy"
79
+ >
80
+ <template #no-data>
81
+ <FSText
82
+ font="text-overline"
83
+ >
84
+ {{ $tr("ui.data-table.empty", "No data") }}
85
+ </FSText>
86
+ </template>
87
+ <template #[`header.data-table-select`]="props">
88
+ <FSRow
89
+ v-if="!$props.singleSelect"
90
+ class="fs-data-table-select"
91
+ align="bottom-center"
92
+ width="hug"
93
+ >
94
+ <FSCheckbox
95
+ :modelValue="props.allSelected"
96
+ :indeterminate="props.someSelected && !props.allSelected"
97
+ @update:modelValue="toggleSelectAll(props.allSelected)"
98
+ />
99
+ </FSRow>
100
+ </template>
101
+ <template #[`item.data-table-select`]="props">
102
+ <FSRow
103
+ class="fs-data-table-select"
104
+ align="bottom-center"
105
+ width="hug"
106
+ >
107
+ <FSCheckbox
108
+ :modelValue="innerValue.includes(props.item[$props.itemValue])"
109
+ @update:modelValue="toggleSelect(props.item)"
110
+ />
111
+ </FSRow>
112
+ </template>
113
+ <template #[`header.data-table-group`]="props">
114
+ <slot name="header.data-table-group" v-bind="props" />
115
+ </template>
116
+ <template #[`item.data-table-group`]="props">
117
+ <slot name="item.data-table-group" v-bind="props" />
118
+ </template>
119
+ <template #group-header="props">
120
+ <template :ref="() => { if (!props.isGroupOpen(props.item)) { props.toggleGroup(props.item) } }" />
121
+ <tr class="fs-data-table-group-header">
122
+ <td />
123
+ <td
124
+ class="fs-data-table-group-header"
125
+ :colspan="extraHeaders.concat(innerHeaders).length + 1"
126
+ >
127
+ <slot name="group-header" v-bind="props">
128
+ <FSCard
129
+ padding="12px 16px"
130
+ >
131
+ <FSRow
132
+ align="center-left"
133
+ width="hug"
134
+ >
135
+ <FSText>
136
+ <slot name="group-header-title" v-bind="props">
137
+ {{ props.item.value }}
138
+ </slot>
139
+ </FSText>
140
+ </FSRow>
141
+ </FSCard>
142
+ </slot>
143
+ </td>
144
+ </tr>
145
+ </template>
146
+ <template v-for="(header, index) in headersSlots" #[header.slotName]="props">
147
+ <slot :name="header.slotName" v-bind="props">
148
+ <FSRow
149
+ align="center-left"
150
+ :wrap="false"
151
+ :key="index"
152
+ >
153
+ <slot :name="`${header.slotName}-prepend`" />
154
+ <slot :name="`${header.slotName}-title`">
155
+ <FSText>
156
+ {{ header.text }}
157
+ </FSText>
158
+ </slot>
159
+ <slot :name="`${header.slotName}-append`" />
160
+ <v-spacer />
161
+ <slot :name="`${header.slotName}-configuration`">
162
+ <FSHeaderButton
163
+ :first="index === 0"
164
+ :last="index === headersSlots.length - 1"
165
+ @update:hide="updateHeader(header, 'hidden', !header.hidden)"
166
+ @update:left="updateHeader(header, 'index', -1)"
167
+ @update:right="updateHeader(header, 'index', 1)"
168
+ />
169
+ <FSButton
170
+ v-if="header.sortable"
171
+ variant="icon"
172
+ :color="sortColor(header, props)"
173
+ :icon="sortIcon(header, props)"
174
+ />
175
+ </slot>
176
+ </FSRow>
177
+ </slot>
178
+ </template>
179
+ <template v-for="(item, index) in itemsSlots" #[item.slotName]="props">
180
+ <slot :name="item.slotName" v-bind="props">
181
+ <FSRow
182
+ align="center-left"
183
+ :key="index"
184
+ >
185
+ <FSText
186
+ font="text-overline"
187
+ >
188
+ {{ props.item[item.value] }}
189
+ </FSText>
190
+ </FSRow>
191
+ </slot>
192
+ </template>
193
+ <template #bottom>
194
+ <FSRow
195
+ class="fs-data-table-footer"
196
+ align="center-right"
197
+ padding="16px"
198
+ gap="24px"
199
+ >
200
+ <template v-if="$props.modelValue.length >= innerItems.length">
201
+ <FSRow
202
+ gap="2px"
203
+ >
204
+ <FSText
205
+ font="text-button"
206
+ >
207
+ {{ $tr("ui.data-table.all-selected-bold", "Warning:") }}
208
+ </FSText>
209
+ <FSText>
210
+ {{ $tr("ui.data-table.all-selected-regular", "All elements selected") }}
211
+ </FSText>
212
+ </FSRow>
213
+ </template>
214
+ <template v-else-if="$props.modelValue.length">
215
+ <FSText>
216
+ {{ $tr("ui.data-table.some-selected", "{0} element(s) selected", $props.modelValue.length.toString()) }}
217
+ </FSText>
218
+ </template>
219
+ <v-spacer />
220
+ <FSRow
221
+ align="center-right"
222
+ :wrap="false"
223
+ >
224
+ <FSText
225
+ font="text-overline"
226
+ >
227
+ {{ $tr("ui.data-table.rows-per-page", "Rows per page") }}
228
+ </FSText>
229
+ <FSRow
230
+ width="120px"
231
+ >
232
+ <FSSelectField
233
+ :clearable="false"
234
+ :hideHeader="true"
235
+ :items="rowsPerPageOptions"
236
+ v-model="innerRowsPerPage"
237
+ />
238
+ </FSRow>
239
+ </FSRow>
240
+ <FSToggleSet
241
+ v-if="innerRowsPerPage !== -1"
242
+ class="fs-data-table-pagination"
243
+ variant="slide"
244
+ :values="pageOptions"
245
+ :required="true"
246
+ v-model="innerPage"
247
+ />
248
+ </FSRow>
249
+ </template>
250
+ <template v-for="(_, name) in innerSlots" #[name]="props">
251
+ <slot :name="name" v-bind="props" />
252
+ </template>
253
+ </v-data-table>
254
+ <v-data-iterator
255
+ v-else
256
+ class="fs-data-table-iterator"
257
+ :items="innerItems"
258
+ :page="innerPage"
259
+ :itemsPerPage="innerRowsPerPage"
260
+ >
261
+ <template #default="{ items }">
262
+ <FSCol
263
+ width="fill"
264
+ >
265
+ <template v-for="(item, index) in items">
266
+ <slot name="item.iterator" v-bind="{ item, index }">
267
+ <FSDataIteratorItem
268
+ v-if="item.type === 'item'"
269
+ :key="index"
270
+ :item="item.raw"
271
+ :color="$props.color"
272
+ :itemTo="$props.itemTo"
273
+ :showSelect="$props.showSelect"
274
+ :headers="innerHeaders.filter(h => !$props.sneakyHeaders.includes(h.value))"
275
+ :modelValue="innerValue.includes(item.raw[$props.itemValue])"
276
+ @update:modelValue="toggleSelect"
277
+ >
278
+ <template #[`item.top`]="props">
279
+ <slot name="item.top" v-bind="props" />
280
+ </template>
281
+ <template v-for="(item, index) in itemsSlots" #[item.slotName]="props">
282
+ <slot :name="item.slotName" v-bind="props">
283
+ <FSText
284
+ :key="index"
285
+ >
286
+ {{ props.item[item.value] }}
287
+ </FSText>
288
+ </slot>
289
+ </template>
290
+ <template #[`item.bottom`]="props">
291
+ <slot name="item.bottom" v-bind="props" />
292
+ </template>
293
+ </FSDataIteratorItem>
294
+ </slot>
295
+ </template>
296
+ </FSCol>
297
+ </template>
298
+ <template #footer>
299
+ <FSRow
300
+ class="fs-data-table-footer"
301
+ align="center-right"
302
+ padding="16px"
303
+ gap="24px"
304
+ >
305
+ <template v-if="$props.modelValue.length >= innerItems.length">
306
+ <FSRow
307
+ gap="2px"
308
+ >
309
+ <FSText
310
+ font="text-button"
311
+ >
312
+ {{ $tr("ui.data-table.all-selected-bold", "Attention:") }}
313
+ </FSText>
314
+ <FSText>
315
+ {{ $tr("ui.data-table.all-selected-regular", "All elements selected") }}
316
+ </FSText>
317
+ </FSRow>
318
+ </template>
319
+ <template v-else-if="$props.modelValue.length">
320
+ <FSText>
321
+ {{ $tr("ui.data-table.some-selected", "{0} element(s) selected", $props.modelValue.length.toString()) }}
322
+ </FSText>
323
+ </template>
324
+ <v-spacer />
325
+ <FSRow
326
+ align="center-right"
327
+ :wrap="false"
328
+ >
329
+ <FSText
330
+ font="text-overline"
331
+ >
332
+ {{ $tr("ui.data-table.rows-per-page", "Rows per page") }}
333
+ </FSText>
334
+ <FSRow
335
+ width="120px"
336
+ >
337
+ <FSSelectField
338
+ :clearable="false"
339
+ :hideHeader="true"
340
+ :items="rowsPerPageOptions"
341
+ v-model="innerRowsPerPage"
342
+ />
343
+ </FSRow>
344
+ </FSRow>
345
+ <FSToggleSet
346
+ v-if="innerRowsPerPage !== -1"
347
+ class="fs-data-table-pagination"
348
+ variant="slide"
349
+ :values="pageOptions"
350
+ :required="true"
351
+ v-model="innerPage"
352
+ />
353
+ </FSRow>
354
+ </template>
355
+ </v-data-iterator>
356
+ </template>
357
+ <template v-else-if="innerMode === 'iterator'">
358
+ <v-data-iterator
359
+ class="fs-data-table-iterator"
360
+ :items="innerItems"
361
+ :itemsPerPage="size"
362
+ >
363
+ <template #default="{ items }">
364
+ <FSRow
365
+ width="hug"
366
+ >
367
+ <template v-for="(item, index) in items">
368
+ <FSDataIteratorItem
369
+ v-if="item.type === 'item'"
370
+ :key="index"
371
+ :item="item.raw"
372
+ :color="$props.color"
373
+ :itemTo="$props.itemTo"
374
+ :showSelect="$props.showSelect"
375
+ :headers="innerHeaders.filter(h => !$props.sneakyHeaders.includes(h.value))"
376
+ :modelValue="innerValue.includes(item.raw[$props.itemValue])"
377
+ @update:modelValue="toggleSelect"
378
+ >
379
+ <template #[`item.top`]="props">
380
+ <slot name="item.top" v-bind="props" />
381
+ </template>
382
+ <template v-for="(item, index) in itemsSlots" #[item.slotName]="props">
383
+ <slot :name="item.slotName" v-bind="props">
384
+ <FSText
385
+ :key="index"
386
+ >
387
+ {{ props.item[item.value] }}
388
+ </FSText>
389
+ </slot>
390
+ </template>
391
+ <template #[`item.bottom`]="props">
392
+ <slot name="item.bottom" v-bind="props" />
393
+ </template>
394
+ </FSDataIteratorItem>
395
+ </template>
396
+ </FSRow>
397
+ </template>
398
+ </v-data-iterator>
399
+ </template>
400
+ <div class="fs-data-table-intersection" />
401
+ </FSCol>
402
+ </template>
403
+
404
+ <script lang="ts">
405
+ import { computed, defineComponent, getCurrentInstance, onMounted, onUnmounted, PropType, ref, Slot, watch } from "vue";
406
+ import { useRouter } from "vue-router";
407
+
408
+ import { ColorEnum, FSDataTableColumn, FSDataTableFilter, FSDataTableOrder, FSToggle } from "@dative-gpi/foundation-shared-components/models";
409
+ import { useBreakpoints, useColors, useSlots } from "@dative-gpi/foundation-shared-components/composables";
410
+ import { useTranslationsProvider } from "@dative-gpi/foundation-shared-services/composables";
411
+
412
+ import FSDataIteratorItem from "./FSDataIteratorItem.vue";
413
+ import FSFilterButton from "./FSFilterButton.vue";
414
+ import FSHiddenButton from "./FSHiddenButton.vue";
415
+ import FSHeaderButton from "./FSHeaderButton.vue";
416
+ import FSSearchField from "../FSSearchField.vue";
417
+ import FSSelectField from "../FSSelectField.vue";
418
+ import FSContainer from "../FSContainer.vue";
419
+ import FSToggleSet from "../FSToggleSet.vue";
420
+ import FSCheckbox from "../FSCheckbox.vue";
421
+ import FSCard from "../FSCard.vue";
422
+ import FSChip from "../FSChip.vue";
423
+ import FSText from "../FSText.vue";
424
+ import FSRow from "../FSRow.vue";
425
+ import FSCol from "../FSCol.vue";
426
+
427
+ export default defineComponent({
428
+ name: "FSDataTable",
429
+ components: {
430
+ FSDataIteratorItem,
431
+ FSFilterButton,
432
+ FSHiddenButton,
433
+ FSHeaderButton,
434
+ FSSearchField,
435
+ FSSelectField,
436
+ FSContainer,
437
+ FSToggleSet,
438
+ FSCheckbox,
439
+ FSCard,
440
+ FSChip,
441
+ FSText,
442
+ FSRow,
443
+ FSCol
444
+ },
445
+ props: {
446
+ headers: {
447
+ type: Array as PropType<FSDataTableColumn[]>,
448
+ required: true
449
+ },
450
+ sneakyHeaders: {
451
+ type: Array as PropType<string[]>,
452
+ required: false,
453
+ default: () => []
454
+ },
455
+ items: {
456
+ type: Array as PropType<any[]>,
457
+ required: true
458
+ },
459
+ itemValue: {
460
+ type: String,
461
+ required: false,
462
+ default: "id"
463
+ },
464
+ itemTo: {
465
+ type: Function,
466
+ required: false,
467
+ default: null
468
+ },
469
+ rowsPerPage: {
470
+ type: Number,
471
+ required: false,
472
+ default: 10
473
+ },
474
+ page: {
475
+ type: Number,
476
+ required: false,
477
+ default: 1
478
+ },
479
+ groupBy: {
480
+ type: Object as PropType<FSDataTableOrder>,
481
+ required: false,
482
+ default: null
483
+ },
484
+ sortBy: {
485
+ type: Array as PropType<FSDataTableOrder[]>,
486
+ required: false,
487
+ default: () => []
488
+ },
489
+ modelValue: {
490
+ type: Array as PropType<string[]>,
491
+ required: false,
492
+ default: () => []
493
+ },
494
+ color: {
495
+ type: String as PropType<ColorEnum>,
496
+ required: false,
497
+ default: ColorEnum.Primary
498
+ },
499
+ showSelect: {
500
+ type: Boolean,
501
+ required: false,
502
+ default: true
503
+ },
504
+ singleSelect: {
505
+ type: Boolean,
506
+ required: false,
507
+ default: false
508
+ },
509
+ selectedOnly: {
510
+ type: Boolean,
511
+ required: false,
512
+ default: false
513
+ },
514
+ mode: {
515
+ type: String as PropType<"table" | "iterator">,
516
+ required: false,
517
+ default: "table"
518
+ },
519
+ disableTable: {
520
+ type: Boolean,
521
+ required: false,
522
+ default: false
523
+ },
524
+ disableIterator: {
525
+ type: Boolean,
526
+ required: false,
527
+ default: false
528
+ },
529
+ sizeIterator: {
530
+ type: Number,
531
+ required: false,
532
+ default: 20
533
+ }
534
+ },
535
+ emits: ["update:modelValue", "update:headers", "update:filters", "update:mode", "update:sortBy", "update:rowsPerPage", "update:page", "click:row"],
536
+ setup(props, { emit }) {
537
+ const { isExtraSmall } = useBreakpoints();
538
+ const { $tr } = useTranslationsProvider();
539
+ const router = useRouter();
540
+
541
+ const backgrounds = useColors().getColors(ColorEnum.Background);
542
+ const lights = useColors().getColors(ColorEnum.Light);
543
+
544
+ const filters = ref<{ [key: string]: FSDataTableFilter[] }>({});
545
+ const innerRowsPerPage = ref(props.rowsPerPage);
546
+ const innerValue = ref(props.modelValue);
547
+ const innerSortBy = ref(props.sortBy);
548
+ const innerMode = ref(props.mode);
549
+ const innerPage = ref(props.page);
550
+ const innerSearch = ref(null);
551
+ const showFilters = ref(true);
552
+ const resetable = ref(false);
553
+
554
+ const intersectionObserver = ref<IntersectionObserver | null>(null);
555
+ const size = ref(props.sizeIterator);
556
+
557
+ const modeOptions: FSToggle[] = [
558
+ { id: "table", prependIcon: "mdi-table" },
559
+ { id: "iterator", prependIcon: "mdi-apps" }
560
+ ];
561
+
562
+ const rowsPerPageOptions: { id: number, label: string }[] = [
563
+ { id: 10, label: "10" },
564
+ { id: 30, label: "30" },
565
+ { id: -1, label: $tr("ui.data-table.all-rows", "All") }
566
+ ];
567
+
568
+ const innerSlots = computed((): { [label: string]: Slot<any> } => {
569
+ const slots = { ...useSlots().slots };
570
+ delete slots["toolbar"];
571
+ delete slots["no-data"];
572
+ delete slots["header.data-table-select"];
573
+ delete slots["item.data-table-select"];
574
+ delete slots["header.data-table-group"];
575
+ delete slots["item.data-table-group"];
576
+ delete slots["group-header"];
577
+ delete slots["group-header-title"];
578
+ delete slots["bottom"];
579
+ for (const header of filterableHeaders.value) {
580
+ delete slots[filterSlot(header)];
581
+ }
582
+ for (const header of headersSlots.value) {
583
+ delete slots[header.slotName];
584
+ delete slots[header.slotName + "-prepend"];
585
+ delete slots[header.slotName + "-title"];
586
+ delete slots[header.slotName + "-append"];
587
+ delete slots[header.slotName + "-configuration"];
588
+ }
589
+ for (const item of itemsSlots.value) {
590
+ delete slots[item.slotName];
591
+ }
592
+ return slots;
593
+ });
594
+
595
+ const style = computed((): { [code: string]: string } & Partial<CSSStyleDeclaration> => {
596
+ return {
597
+ "--fs-data-table-background-color" : backgrounds.base,
598
+ "--fs-data-table-border-color" : lights.base
599
+ };
600
+ });
601
+
602
+ const classes = computed((): string[] => {
603
+ const innerClasses = ["fs-data-table"];
604
+ if (props.groupBy) {
605
+ innerClasses.push("fs-data-table-grouped");
606
+ }
607
+ return innerClasses;
608
+ });
609
+
610
+ const extraHeaders = computed((): any[] => {
611
+ const extra = [];
612
+ if (props.groupBy) {
613
+ extra.push({
614
+ key: "data-table-group",
615
+ width: "0%"
616
+ });
617
+ }
618
+ if (props.showSelect) {
619
+ extra.push({
620
+ key: "data-table-select",
621
+ width: "0%"
622
+ });
623
+ }
624
+ return extra;
625
+ });
626
+
627
+ const innerHeaders = computed((): FSDataTableColumn[] => {
628
+ return props.headers.filter(c => !c.hidden)
629
+ .sort((c1, c2) => c1.index - c2.index).map((c) => {
630
+ if (c.sort) {
631
+ return c;
632
+ }
633
+ return {
634
+ ...c,
635
+ sort: (a: any, b: any): number => JSON.stringify(a)
636
+ .localeCompare(JSON.stringify(b), undefined, { numeric: true })
637
+ };
638
+ })
639
+ });
640
+
641
+ const hiddenHeaders = computed((): FSDataTableColumn[] => {
642
+ return props.headers.filter(c => c.hidden);
643
+ });
644
+
645
+ const filterableHeaders = computed((): FSDataTableColumn[] => {
646
+ return innerHeaders.value.filter((c) => c.filterable);
647
+ });
648
+
649
+ const innerItems = computed((): any[] => {
650
+ const activeFilters: { key: string, filter: FSDataTableFilter }[] = Object.keys(filters.value).reduce((acc, key) => {
651
+ return acc.concat(filters.value[key].filter((filter) => filter.hidden).map((filter) => ({ key, filter })));
652
+ }, []);
653
+ return props.items.filter((item) => {
654
+ if (props.selectedOnly && !innerValue.value.includes(item[props.itemValue])) {
655
+ return false;
656
+ }
657
+ if (innerSearch.value) {
658
+ if (!JSON.stringify(item).toLowerCase().includes(innerSearch.value.toString().toLowerCase())) {
659
+ return false;
660
+ }
661
+ }
662
+ if (activeFilters.some(af => af.filter.filter(af.filter.value, item[af.key], item))) {
663
+ return false;
664
+ }
665
+ return true;
666
+ });
667
+ });
668
+
669
+ const headersSlots = computed((): FSDataTableColumn[] => {
670
+ return innerHeaders.value.map((c) => ({ ...c, slotName: `header.${c.value}` }));
671
+ });
672
+
673
+ const itemsSlots = computed((): FSDataTableColumn[] => {
674
+ return innerHeaders.value.map((c) => ({ ...c, slotName: `item.${c.value}` }));
675
+ });
676
+
677
+ const groups = computed((): { [key: string]: any[] } => {
678
+ if (props.groupBy) {
679
+ return innerItems.value.reduce((acc, item) => {
680
+ const key = item[props.groupBy.key];
681
+ if (!acc[key]) {
682
+ acc[key] = [];
683
+ }
684
+ acc[key].push(item);
685
+ return acc;
686
+ }, {});
687
+ }
688
+ return {};
689
+ });
690
+
691
+ const pageOptions = computed((): { id: number, label: string }[] => {
692
+ if (innerRowsPerPage.value === -1) {
693
+ return [];
694
+ }
695
+ else {
696
+ const total = Math.ceil((innerItems.value.length + Object.keys(groups.value).length) / innerRowsPerPage.value);
697
+ return Array.from(Array(total).keys()).map(i => ({
698
+ id: i + 1,
699
+ label: (i + 1).toString()
700
+ }));
701
+ }
702
+ });
703
+
704
+ const toggleSelectAll = (allSelected: boolean): void => {
705
+ if (allSelected) {
706
+ innerValue.value = [];
707
+ }
708
+ else {
709
+ innerValue.value = innerItems.value.map((item) => item[props.itemValue]);
710
+ }
711
+ emit("update:modelValue", innerValue.value);
712
+ };
713
+
714
+ const toggleSelect = (item: any): void => {
715
+ const index = innerValue.value.indexOf(item[props.itemValue]);
716
+ if (index > -1) {
717
+ innerValue.value.splice(index, 1);
718
+ }
719
+ else {
720
+ innerValue.value.push(item[props.itemValue]);
721
+ }
722
+ emit("update:modelValue", innerValue.value);
723
+ };
724
+
725
+ const toggleFilter = (header: string, value: FSDataTableFilter[]): void => {
726
+ filters.value[header] = value;
727
+ emit("update:filters", filters.value);
728
+ // If a filter is hidden, the reset button will be shown
729
+ resetable.value = Object.keys(filters.value)
730
+ .some((key) => filters.value[key].some((filter) => filter.hidden));
731
+ };
732
+
733
+ const resetFilter = (): void => {
734
+ for (const key in filters.value) {
735
+ filters.value[key] = filters.value[key].map((filter) => ({ ...filter, hidden: false }));
736
+ }
737
+ emit("update:filters", filters.value);
738
+ resetable.value = false;
739
+ };
740
+
741
+ const filterSlot = (header: FSDataTableColumn): string => {
742
+ return `filter.${header.value}`;
743
+ };
744
+
745
+ const updateHeader = (header: FSDataTableColumn, property: "hidden" | "index", value: boolean | number) => {
746
+ const innerColumns = props.headers.slice(0);
747
+ const innerColumn = innerColumns.find((column) => column.value === header.value);
748
+ if (innerColumn) {
749
+ switch (property) {
750
+ case "hidden":
751
+ innerColumn.hidden = value as boolean;
752
+ break;
753
+ case "index":
754
+ switch (value) {
755
+ case 1:
756
+ for (const column of innerColumns) {
757
+ if (column.index === header.index + 1) {
758
+ column.index--;
759
+ }
760
+ }
761
+ break;
762
+ case -1:
763
+ for (const column of innerColumns) {
764
+ if (column.index === header.index - 1) {
765
+ column.index++;
766
+ }
767
+ }
768
+ break;
769
+ }
770
+ innerColumn.index += value as number;
771
+ innerColumns.sort((c1, c2) => c1.index - c2.index);
772
+ for (let i = 0; i < innerColumns.length; i++) {
773
+ innerColumns[i].index = i;
774
+ }
775
+ break;
776
+ }
777
+ emit("update:headers", innerColumns);
778
+ }
779
+ };
780
+
781
+ const computeFilters = (): void => {
782
+ const filterDictionary: { [key: string]: FSDataTableFilter[] } = {};
783
+
784
+ for (const header of innerHeaders.value.filter((h) => h.filterable)) {
785
+ const key = header.value!;
786
+ const currentFilters = filters.value[key];
787
+
788
+ let value: FSDataTableFilter[] = [];
789
+
790
+ if (header.fixedFilters) {
791
+ value = header.fixedFilters.map((ff): FSDataTableFilter => ({
792
+ hidden: currentFilters?.find((cf) => cf.value == (ff.value || null))?.hidden ?? false,
793
+ text: ff.text?.toString() ?? "—",
794
+ value: ff.value || null,
795
+ filter: header.methodFilter ?? ((value, property) => {
796
+ property = [property].flat();
797
+ return Array.isArray(property) ? property.includes(value) || (!value && property.length == 0) : (!value && !property) || value == property;
798
+ })
799
+ }));
800
+ }
801
+ else {
802
+ const mapToInnerValue = header.innerValue ? header.innerValue : (i: any) => i;
803
+ const itemValues = props.items.flatMap((item) => Array.isArray(item[key]) && item[key].length == 0 ? undefined : item[key]).map(mapToInnerValue);
804
+ const distinctValues = [...new Set(itemValues)];
805
+
806
+ value = distinctValues.map((dv): FSDataTableFilter => ({
807
+ hidden: currentFilters?.find((cf) => cf.value == (dv || null))?.hidden ?? false,
808
+ text: dv?.toString() ?? "—",
809
+ value: dv || null,
810
+ filter: header.methodFilter ?? ((_, property) => {
811
+ property = [property].flat().map(mapToInnerValue);
812
+ return Array.isArray(property) ? property.includes(dv) || (!dv && property.length == 0) : (!dv && !property) || dv == property;
813
+ })
814
+ }));
815
+ }
816
+ filterDictionary[key] = value.sort((v1, v2) => {
817
+ return v1.text.localeCompare(v2.text, undefined, { numeric: true });
818
+ });
819
+ }
820
+ filters.value = filterDictionary;
821
+ };
822
+
823
+ const sortColor = (header: FSDataTableColumn, slotProps: any) => {
824
+ const sort = slotProps.sortBy.find((s: any) => s.key == header.value);
825
+ if (sort) {
826
+ return props.color;
827
+ }
828
+ return ColorEnum.Light;
829
+ };
830
+
831
+ const sortIcon = (header: FSDataTableColumn, slotProps: any) => {
832
+ const sort = slotProps.sortBy.find((s: any) => s.key == header.value);
833
+ if (sort) {
834
+ switch (sort.order) {
835
+ case "asc": return "mdi-sort-reverse-variant";
836
+ case "desc": return "mdi-sort-variant";
837
+ }
838
+ }
839
+ return "mdi-sort-variant-off";
840
+ };
841
+
842
+ const observeIntersection = (): void => {
843
+ switch (innerMode.value) {
844
+ case "table":
845
+ if (intersectionObserver.value && document.querySelector(".fs-data-table-intersection")) {
846
+ intersectionObserver.value.unobserve(document.querySelector(".fs-data-table-intersection"));
847
+ }
848
+ return;
849
+ case "iterator":
850
+ if (!intersectionObserver.value) {
851
+ intersectionObserver.value = new IntersectionObserver(entries => {
852
+ entries.forEach((entry) => {
853
+ if (entry.boundingClientRect.bottom < window.innerHeight * 1.25) {
854
+ if (innerItems.value.length > size.value) {
855
+ size.value = Math.min(size.value + props.sizeIterator, innerItems.value.length);
856
+ }
857
+ }
858
+ });
859
+ }, { threshold: [0.9] });
860
+ }
861
+ if (document.querySelector(".fs-data-table-intersection")) {
862
+ intersectionObserver.value.observe(document.querySelector(".fs-data-table-intersection"));
863
+ }
864
+ return;
865
+ }
866
+ }
867
+
868
+ const onClickRow = computed(() => {
869
+ if (!!getCurrentInstance()?.vnode.props?.['onClick:row'] || props.itemTo) {
870
+ return (event: PointerEvent, row: any) => {
871
+ if (props.itemTo && router) {
872
+ if (event.metaKey || event.ctrlKey || event.button === 1) {
873
+ window.open(router.resolve(props.itemTo(row.item)).href, "_blank");
874
+ }
875
+ else {
876
+ router.push(props.itemTo(row.item));
877
+ }
878
+ }
879
+ else {
880
+ emit("click:row", row.item);
881
+ }
882
+ };
883
+ }
884
+ else {
885
+ return null;
886
+ }
887
+ });
888
+
889
+ onMounted(() => {
890
+ computeFilters();
891
+ observeIntersection();
892
+ });
893
+
894
+ onUnmounted(() => {
895
+ if (intersectionObserver.value) {
896
+ intersectionObserver.value.disconnect();
897
+ }
898
+ });
899
+
900
+ watch(innerSearch, () => {
901
+ innerPage.value = 1;
902
+ });
903
+
904
+ watch(innerMode, () => {
905
+ emit("update:mode", innerMode.value);
906
+ size.value = props.sizeIterator;
907
+ observeIntersection();
908
+ });
909
+
910
+ watch(innerSortBy, () => {
911
+ emit("update:sortBy", innerSortBy.value);
912
+ });
913
+
914
+ watch(innerPage, () => {
915
+ emit("update:page", innerPage.value);
916
+ });
917
+
918
+ watch(innerRowsPerPage, () => {
919
+ emit("update:rowsPerPage", innerRowsPerPage.value);
920
+ });
921
+
922
+ watch(() => props.headers, () => {
923
+ computeFilters();
924
+ });
925
+
926
+ return {
927
+ ColorEnum,
928
+ innerSlots,
929
+ rowsPerPageOptions,
930
+ innerRowsPerPage,
931
+ innerSearch,
932
+ innerMode,
933
+ modeOptions,
934
+ innerPage,
935
+ pageOptions,
936
+ showFilters,
937
+ extraHeaders,
938
+ innerHeaders,
939
+ hiddenHeaders,
940
+ filterableHeaders,
941
+ filters,
942
+ resetable,
943
+ innerSortBy,
944
+ innerItems,
945
+ headersSlots,
946
+ itemsSlots,
947
+ innerValue,
948
+ classes,
949
+ style,
950
+ size,
951
+ onClickRow,
952
+ isExtraSmall,
953
+ toggleSelectAll,
954
+ toggleSelect,
955
+ updateHeader,
956
+ toggleFilter,
957
+ resetFilter,
958
+ filterSlot,
959
+ sortColor,
960
+ sortIcon,
961
+ };
962
+ }
963
+ })
964
+ </script>