PluginProbe
The Innovative Form Builder – IvyForms / 0.1.2
The Innovative Form Builder – IvyForms v0.1.2
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / table / IvyTable.vue

IvyTable.vue in The Innovative Form Builder – IvyForms 0.1.2, at frontend/src/views/_components/table/IvyTable.vue

303 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div class="ivyforms-table__container">
3 <ElAutoResizer>
4 <template #default="{ width, height }">
5 <ElTableV2
6 ref="tableV2Ref"
7 v-bind="$attrs"
8 :columns="formattedColumns"
9 :data="props.data"
10 :width="Math.max(width, props.minTableWidth)"
11 :height="computedTableHeight(height)"
12 class="ivyforms-table"
13 :fixed="props.fixed"
14 :row-class="isRowSelected"
15 >
16 <!-- Slot for custom header when row is selected -->
17 <template v-if="isCustomHeader" #header>
18 <slot name="header" />
19 </template>
20
21 <!-- Slot for custom column headers with sorting -->
22 <template #header-cell="{ column }">
23 <div
24 :style="{
25 display: 'flex',
26 alignItems: 'center',
27 cursor: column.sortable ? 'pointer' : 'default',
28 }"
29 @click="emitSortChange(column)"
30 >
31 <component :is="column.headerRenderer()" v-if="column.headerRenderer" />
32
33 <div
34 v-else-if="column.comingSoon"
35 style="display: flex; align-items: center; gap: 8px"
36 >
37 <div style="opacity: 0.5; cursor: not-allowed">{{ column.title }}</div>
38 <ComingSoonBadge size="s" />
39 </div>
40 <span v-else>{{ column.title }}</span>
41
42 <!-- Sorting icons -->
43 <span v-if="column.sortable" style="margin-left: 5px">
44 <span v-if="sortColumn === column.key">
45 <span v-if="sortOrder === 'asc'">
46 <IvyIcon
47 size="d"
48 outer-size="20px"
49 :name="isDarkMode ? 'sort-asc-dark' : 'sort-asc'"
50 category="table"
51 />
52 </span>
53 <span v-else-if="sortOrder === 'desc'">
54 <IvyIcon
55 size="d"
56 outer-size="20px"
57 :name="isDarkMode ? 'sort-desc-dark' : 'sort-desc'"
58 category="table"
59 />
60 </span>
61 </span>
62 <span v-else>
63 <IvyIcon
64 size="d"
65 outer-size="20px"
66 name="arrow-up-down-2"
67 category="arrows"
68 type="outline"
69 color="var(--map-base-dusk-symbol-2)"
70 />
71 </span>
72 </span>
73 </div>
74 </template>
75
76 <!-- Slot for empty table/no filters match -->
77 <template #empty>
78 <slot name="empty" />
79 </template>
80 </ElTableV2>
81
82 <!-- Pagination -->
83 <slot v-if="pagination && data.length" name="pagination">
84 <IvyPagination :total="props.data.length" class="ivyforms-table__pagination" />
85 </slot>
86 </template>
87 </ElAutoResizer>
88 </div>
89 </template>
90
91 <script lang="ts" setup>
92 import { computed, ref, h, onUnmounted } from 'vue'
93 import type { TableColumn } from '@/views/_components/table/IvyTableColumn.ts'
94 import type { CellRenderProps } from '@/views/_components/table/IvyCellRendererProps.ts'
95
96 interface Props {
97 data: Array<{ [key: string]: string | number | boolean }>
98 columns?: TableColumn[]
99 loading?: boolean
100 pagination?: boolean
101 groupBy?: string
102 fixed?: boolean
103 isCustomHeader?: boolean
104 minTableWidth?: number
105 }
106
107 const props = withDefaults(defineProps<Props>(), {
108 loading: false,
109 pagination: true,
110 isCustomHeader: false,
111 columns: () => [],
112 groupBy: '',
113 minTableWidth: 600,
114 })
115
116 const emit = defineEmits<{
117 (e: 'sort-change', sortEvent: { column: string | null; order: 'asc' | 'desc' | null }): void
118 }>()
119
120 // Sort icons based on theme
121 const isDarkMode = ref(document.body.classList.contains('ivyforms-theme-dark'))
122
123 const observer = new MutationObserver(() => {
124 isDarkMode.value = document.body.classList.contains('ivyforms-theme-dark')
125 })
126
127 observer.observe(document.body, { attributes: true, attributeFilter: ['class'] })
128
129 onUnmounted(() => {
130 observer.disconnect()
131 })
132 // Adjust the height and width
133 const computedTableHeight = (parentHeight: number) => {
134 const rowHeight = 50
135 const numRows = props.data.length + 1
136 const maxTableHeight = numRows * rowHeight
137
138 // Set a minimum height for the table when there is no data
139 const minHeight = 550 // TODO Adjust this value - currently 10 rows + header
140 return Math.max(minHeight, Math.min(maxTableHeight, parentHeight))
141 }
142
143 // Format columns to use custom components inside headers and cells
144 const formattedColumns = computed(
145 () =>
146 props.columns
147 ?.filter((col) => col.visible)
148 .map((col) => ({
149 ...col,
150 sortable: col.sortable || false,
151 headerRenderer: col.headerRenderer || undefined,
152 cellRenderer:
153 col.cellRenderer || ((props: CellRenderProps) => h('span', {}, props.rowData[col.key])),
154 minWidth:
155 typeof col.minWidth === 'string' ? parseInt(col.minWidth, 10) || 100 : col.minWidth, // Ensure minWidth is a number
156 width: typeof col.width === 'string' ? parseInt(col.width, 10) || 100 : col.width, // Ensure width is a number
157 })) || [],
158 )
159
160 // Sorting state (only for UI purposes)
161 const sortColumn = ref<string | null>(null)
162 const sortOrder = ref<'asc' | 'desc' | null>(null)
163
164 // Emit sorting changes
165 const emitSortChange = (column) => {
166 if (!column.sortable) return
167
168 if (sortColumn.value === column.key) {
169 // Toggle order
170 sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
171 } else {
172 // New column selected, set ascending order
173 sortColumn.value = column.key
174 sortOrder.value = 'asc'
175 }
176
177 // Emit the sorting change event
178 const sortEvent = {
179 column: sortColumn.value,
180 order: sortOrder.value,
181 }
182 emit('sort-change', sortEvent)
183 }
184
185 // Class for selected rows
186 const isRowSelected = ({ rowData }: { rowData }) => {
187 return rowData.selected ? 'is-selected' : ''
188 }
189 </script>
190
191 <style lang="scss">
192 .ivyforms-table__container {
193 display: flex;
194 flex-direction: column;
195 width: 100%;
196 height: 100%;
197 min-width: 0;
198 min-height: 0;
199 overflow-x: auto;
200 white-space: nowrap;
201
202 .ivyforms-table {
203 // Header
204 .el-table-v2__header {
205 border-bottom: 1px solid var(--map-divider);
206 background: var(--map-ground-level-1-foreground);
207 transition: none;
208
209 // Columns
210 .el-table-v2__header-cell {
211 display: flex;
212 background: var(--map-ground-level-1-foreground);
213 color: var(--map-base-text-0);
214 text-align: center;
215 font-size: 14px;
216 font-style: normal;
217 font-weight: 400;
218 line-height: 20px;
219
220 // Sortable
221 &.is-sortable {
222 span {
223 margin-left: 0 !important;
224 }
225 .el-table-v2__sort-icon {
226 svg {
227 display: none;
228 }
229 }
230 }
231 }
232 }
233
234 // Body
235 .el-table-v2__body {
236 background: var(--map-ground-level-1-foreground);
237 transition: none;
238
239 // Rows
240 .el-table-v2__row {
241 display: flex;
242 height: 48px;
243 align-items: center;
244 flex-shrink: 0;
245 border-bottom: 1px solid var(--map-divider);
246 background: var(--map-ground-level-1-foreground);
247 color: var(--map-base-dusk-symbol-2);
248 transition: none;
249
250 &:hover {
251 background: var(--map-hover);
252 }
253
254 // Selected
255 &.is-selected {
256 background: linear-gradient(
257 0deg,
258 var(--map-accent-amber-o05) 0%,
259 var(--map-accent-amber-o05) 100%
260 ),
261 var(--map-ground-level-1-foreground);
262 }
263 }
264 }
265
266 // Scrollbar
267 .el-virtual-scrollbar {
268 &.el-vl__vertical {
269 height: 20%;
270
271 .el-scrollbar__thumb {
272 opacity: 1;
273 background: var(--map-base-purple-fill-0);
274 }
275 }
276
277 &.el-vl__horizontal {
278 width: 20%;
279
280 .el-scrollbar__thumb {
281 opacity: 1;
282 background: var(--map-base-purple-fill-0);
283 }
284 }
285 }
286 }
287 }
288
289 .ivyforms-table__pagination {
290 margin-top: 8px;
291 }
292
293 .el-skeleton {
294 &.ivyforms-skeleton {
295 margin: 1px;
296
297 .ivyforms-skeleton-item {
298 height: 48px;
299 }
300 }
301 }
302 </style>
303