PluginProbe
The Innovative Form Builder – IvyForms / 0.3
The Innovative Form Builder – IvyForms v0.3
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.3, at frontend/src/views/_components/table/IvyTable.vue

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