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

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