infotable 0.1.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1043 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +1412 -0
- data/app/assets/javascripts/infotable/controllers/table_controller.js +921 -0
- data/app/assets/javascripts/infotable/index.js +14 -0
- data/app/controllers/infotable/application_controller.rb +4 -0
- data/app/controllers/infotable/tables_controller.rb +299 -0
- data/app/helpers/infotable/application_helper.rb +5 -0
- data/app/helpers/infotable/tables_helper.rb +354 -0
- data/app/jobs/infotable/application_job.rb +4 -0
- data/app/mailers/infotable/application_mailer.rb +6 -0
- data/app/models/infotable/application_record.rb +5 -0
- data/app/views/infotable/tables/_column_visibility.html.erb +100 -0
- data/app/views/infotable/tables/_filter_drawer.html.erb +65 -0
- data/app/views/infotable/tables/_next_page_frame.html.erb +39 -0
- data/app/views/infotable/tables/_pagination.html.erb +101 -0
- data/app/views/infotable/tables/_side_panel.html.erb +63 -0
- data/app/views/infotable/tables/_table.html.erb +135 -0
- data/app/views/infotable/tables/_table_rows.html.erb +38 -0
- data/app/views/infotable/tables/filters/_boolean.html.erb +22 -0
- data/app/views/infotable/tables/filters/_date.html.erb +30 -0
- data/app/views/infotable/tables/filters/_number.html.erb +32 -0
- data/app/views/infotable/tables/filters/_select.html.erb +68 -0
- data/app/views/infotable/tables/filters/_text.html.erb +38 -0
- data/app/views/infotable/tables/show.html.erb +113 -0
- data/app/views/layouts/infotable/application.html.erb +15 -0
- data/config/importmap.rb +6 -0
- data/config/locales/en.yml +88 -0
- data/config/locales/pl.yml +88 -0
- data/config/routes.rb +6 -0
- data/lib/generators/infotable/install/install_generator.rb +565 -0
- data/lib/generators/infotable/install/templates/TAILWIND_CONFIG.md +131 -0
- data/lib/generators/infotable/install/templates/initializer.rb +26 -0
- data/lib/generators/infotable/install/templates/tailwind.config.js +284 -0
- data/lib/generators/infotable/table/table_generator.rb +95 -0
- data/lib/generators/infotable/table/templates/table.rb.tt +26 -0
- data/lib/infotable/base.rb +463 -0
- data/lib/infotable/batch_actions.rb +34 -0
- data/lib/infotable/configuration.rb +53 -0
- data/lib/infotable/engine.rb +51 -0
- data/lib/infotable/filter_processor.rb +326 -0
- data/lib/infotable/formatters/action_formatter.rb +141 -0
- data/lib/infotable/formatters/avatar_formatter.rb +49 -0
- data/lib/infotable/formatters/badge_formatter.rb +80 -0
- data/lib/infotable/formatters/base_formatter.rb +47 -0
- data/lib/infotable/formatters/boolean_formatter.rb +38 -0
- data/lib/infotable/formatters/currency_formatter.rb +22 -0
- data/lib/infotable/formatters/date_formatter.rb +58 -0
- data/lib/infotable/formatters/link_formatter.rb +48 -0
- data/lib/infotable/formatters/percent_formatter.rb +27 -0
- data/lib/infotable/search_processor.rb +42 -0
- data/lib/infotable/table_resolver.rb +45 -0
- data/lib/infotable/version.rb +3 -0
- data/lib/infotable.rb +54 -0
- data/lib/tasks/infotable.rake +55 -0
- metadata +216 -0
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import Sortable from "sortablejs"
|
|
3
|
+
|
|
4
|
+
// Drawer controller for the filter drawer and column visibility
|
|
5
|
+
// Handles opening/closing the right-side drawer with smooth animations
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ["drawer", "content", "tabButton", "tabContent", "panelTitle", "columnBadge", "columnList"]
|
|
8
|
+
static values = {
|
|
9
|
+
open: { type: Boolean, default: false },
|
|
10
|
+
currentTab: { type: String, default: "filters" }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
// Close drawer on escape key
|
|
15
|
+
this.boundCloseOnEscape = this.closeOnEscape.bind(this)
|
|
16
|
+
document.addEventListener("keydown", this.boundCloseOnEscape)
|
|
17
|
+
|
|
18
|
+
// Close drawer when clicking outside
|
|
19
|
+
this.boundCloseOnClickOutside = this.closeOnClickOutside.bind(this)
|
|
20
|
+
|
|
21
|
+
// Determine default tab based on which tab content is visible by default
|
|
22
|
+
// If filters tab doesn't exist, default to columns tab
|
|
23
|
+
const hasFiltersTab = this.tabContentTargets.some(content => content.dataset.tab === "filters")
|
|
24
|
+
if (!hasFiltersTab && this.currentTabValue === "filters") {
|
|
25
|
+
this.currentTabValue = "columns"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Initialize to show appropriate tab by default
|
|
29
|
+
this.switchTabTo(this.currentTabValue)
|
|
30
|
+
|
|
31
|
+
// Initialize debounced filter submission
|
|
32
|
+
this.debounceTimer = null
|
|
33
|
+
this.debounceDelay = 500 // 500ms delay
|
|
34
|
+
|
|
35
|
+
// Set up debounced filter inputs
|
|
36
|
+
this.setupDebouncedFilters()
|
|
37
|
+
|
|
38
|
+
// Initialize Sortable for column ordering
|
|
39
|
+
this.initializeSortable()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
openValueChanged() {
|
|
43
|
+
if (this.openValue) {
|
|
44
|
+
this.showDrawer()
|
|
45
|
+
// Show appropriate tab by default when opening
|
|
46
|
+
if (!this.currentTabValue) {
|
|
47
|
+
const hasFiltersTab = this.tabContentTargets.some(content => content.dataset.tab === "filters")
|
|
48
|
+
this.currentTabValue = hasFiltersTab ? "filters" : "columns"
|
|
49
|
+
} else {
|
|
50
|
+
this.switchTabTo(this.currentTabValue)
|
|
51
|
+
}
|
|
52
|
+
// Sync checkboxes and order with localStorage when drawer opens
|
|
53
|
+
if (this.currentTabValue === "columns") {
|
|
54
|
+
this.syncColumnCheckboxes()
|
|
55
|
+
this.syncColumnListOrder()
|
|
56
|
+
} else {
|
|
57
|
+
// Update badge even if not on columns tab, so it's ready when switching
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
this.updateAllColumnBadges()
|
|
60
|
+
}, 50)
|
|
61
|
+
}
|
|
62
|
+
// Setup debounced filters when drawer opens (in case form is rendered dynamically)
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
this.setupDebouncedFilters()
|
|
65
|
+
}, 50)
|
|
66
|
+
// Add click outside listener when drawer opens
|
|
67
|
+
// Use setTimeout to avoid immediate closure from the click that opened it
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
document.addEventListener("click", this.boundCloseOnClickOutside)
|
|
70
|
+
}, 100)
|
|
71
|
+
} else {
|
|
72
|
+
this.hideDrawer()
|
|
73
|
+
// Reset to appropriate default tab when closing
|
|
74
|
+
const hasFiltersTab = this.tabContentTargets.some(content => content.dataset.tab === "filters")
|
|
75
|
+
this.currentTabValue = hasFiltersTab ? "filters" : "columns"
|
|
76
|
+
// Remove click outside listener when drawer closes
|
|
77
|
+
document.removeEventListener("click", this.boundCloseOnClickOutside)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
currentTabValueChanged() {
|
|
82
|
+
if (this.currentTabValue) {
|
|
83
|
+
this.switchTabTo(this.currentTabValue)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
disconnect() {
|
|
88
|
+
document.removeEventListener("keydown", this.boundCloseOnEscape)
|
|
89
|
+
document.removeEventListener("click", this.boundCloseOnClickOutside)
|
|
90
|
+
|
|
91
|
+
// Clear debounce timer on disconnect
|
|
92
|
+
if (this.debounceTimer) {
|
|
93
|
+
clearTimeout(this.debounceTimer)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Remove filter input listeners
|
|
97
|
+
const form = this.contentTarget?.querySelector("form")
|
|
98
|
+
if (form && this.boundHandleFilterInput) {
|
|
99
|
+
form.removeEventListener('input', this.boundHandleFilterInput, true)
|
|
100
|
+
form.removeEventListener('change', this.boundHandleFilterInput, true)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Destroy Sortable instances
|
|
104
|
+
if (this.sortableInstances) {
|
|
105
|
+
this.sortableInstances.forEach(sortable => sortable.destroy())
|
|
106
|
+
this.sortableInstances.clear()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
toggle() {
|
|
111
|
+
this.openValue = !this.openValue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
open() {
|
|
115
|
+
// Default to appropriate tab when opening
|
|
116
|
+
if (!this.currentTabValue) {
|
|
117
|
+
const hasFiltersTab = this.tabContentTargets.some(content => content.dataset.tab === "filters")
|
|
118
|
+
this.currentTabValue = hasFiltersTab ? "filters" : "columns"
|
|
119
|
+
}
|
|
120
|
+
this.openValue = true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
close() {
|
|
124
|
+
this.openValue = false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Open drawer with columns tab (kept for backward compatibility)
|
|
128
|
+
openColumns(event) {
|
|
129
|
+
if (event) {
|
|
130
|
+
event.preventDefault()
|
|
131
|
+
event.stopPropagation()
|
|
132
|
+
}
|
|
133
|
+
this.currentTabValue = "columns"
|
|
134
|
+
this.openValue = true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Open drawer with filters tab (kept for backward compatibility)
|
|
138
|
+
openFilters(event) {
|
|
139
|
+
if (event) {
|
|
140
|
+
event.preventDefault()
|
|
141
|
+
event.stopPropagation()
|
|
142
|
+
}
|
|
143
|
+
this.currentTabValue = "filters"
|
|
144
|
+
this.openValue = true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
showDrawer() {
|
|
150
|
+
// Slide in drawer from its side using inline transform
|
|
151
|
+
this.previouslyFocused = document.activeElement
|
|
152
|
+
this.drawerTarget.inert = false
|
|
153
|
+
this.drawerTarget.removeAttribute("aria-hidden")
|
|
154
|
+
this.drawerTarget.style.transform = "translateX(0)"
|
|
155
|
+
this.drawerTarget.querySelector("input:not([type=hidden]), select, button")?.focus({ preventScroll: true })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
hideDrawer() {
|
|
159
|
+
this.drawerTarget.inert = true
|
|
160
|
+
this.drawerTarget.setAttribute("aria-hidden", "true")
|
|
161
|
+
if (this.previouslyFocused?.isConnected) this.previouslyFocused.focus({ preventScroll: true })
|
|
162
|
+
this.previouslyFocused = null
|
|
163
|
+
|
|
164
|
+
// Slide out drawer to its side (data-drawer-side) using inline transform
|
|
165
|
+
this.drawerTarget.style.transform = this.drawerTarget.dataset.drawerSide === "left" ? "translateX(-100%)" : "translateX(100%)"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
closeOnEscape(event) {
|
|
169
|
+
if (event.key === "Escape" && this.openValue) {
|
|
170
|
+
this.close()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
closeOnClickOutside(event) {
|
|
175
|
+
// Don't close if drawer is not open
|
|
176
|
+
if (!this.openValue) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Don't close if drawer target doesn't exist
|
|
181
|
+
if (!this.hasDrawerTarget || !this.drawerTarget) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Don't close if click is inside the drawer
|
|
186
|
+
if (this.drawerTarget.contains(event.target)) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Don't close if clicking on buttons that open the drawer
|
|
191
|
+
const clickedElement = event.target.closest('button[data-action*="infotable--drawer#open"]')
|
|
192
|
+
if (clickedElement) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Don't close if clicking on elements inside buttons that open the drawer (like SVG icons)
|
|
197
|
+
const buttonParent = event.target.closest('button')
|
|
198
|
+
if (buttonParent && buttonParent.hasAttribute('data-action') &&
|
|
199
|
+
buttonParent.getAttribute('data-action').includes('infotable--drawer#open')) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Close the drawer when clicking outside (inside or outside the table)
|
|
204
|
+
event.stopPropagation()
|
|
205
|
+
this.close()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
// Handle filter form submission
|
|
210
|
+
submitFilters(event) {
|
|
211
|
+
if (event) {
|
|
212
|
+
event.preventDefault()
|
|
213
|
+
}
|
|
214
|
+
const form = event?.target || this.contentTarget.querySelector("form")
|
|
215
|
+
if (!form) return
|
|
216
|
+
|
|
217
|
+
const formData = new FormData(form)
|
|
218
|
+
|
|
219
|
+
// Build URL with filter params
|
|
220
|
+
const params = new URLSearchParams(formData)
|
|
221
|
+
|
|
222
|
+
// Update the table via Turbo Frame
|
|
223
|
+
const tableFrame = document.getElementById(form.dataset.tableFrame)
|
|
224
|
+
|
|
225
|
+
// Keep search and sort from the current frame URL
|
|
226
|
+
if (tableFrame?.src) {
|
|
227
|
+
const current = new URL(tableFrame.src, window.location.href).searchParams
|
|
228
|
+
for (const key of ["search", "sort", "sort_column", "sort_direction"]) {
|
|
229
|
+
if (current.has(key) && !params.has(key)) params.set(key, current.get(key))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const url = form.action + "?" + params.toString()
|
|
233
|
+
|
|
234
|
+
if (tableFrame) {
|
|
235
|
+
tableFrame.src = url
|
|
236
|
+
|
|
237
|
+
// Dispatch event to update export link after frame loads
|
|
238
|
+
tableFrame.addEventListener("turbo:frame-load", () => {
|
|
239
|
+
const tableName = form.action.match(/tables\/([^\/]+)/)?.[1]
|
|
240
|
+
if (tableName) {
|
|
241
|
+
document.dispatchEvent(new CustomEvent("infotable:filters-changed", {
|
|
242
|
+
detail: { tableName }
|
|
243
|
+
}))
|
|
244
|
+
}
|
|
245
|
+
}, { once: true })
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Setup debounced filter inputs using event delegation
|
|
250
|
+
setupDebouncedFilters() {
|
|
251
|
+
// Find form in the drawer content (might be in filters tab)
|
|
252
|
+
const form = this.contentTarget?.querySelector("form")
|
|
253
|
+
if (!form) return
|
|
254
|
+
|
|
255
|
+
// Check if autofilter is enabled (default: true)
|
|
256
|
+
// HTML data attributes are strings, so check for string 'false'
|
|
257
|
+
const autofilterValue = form.dataset.autofilter
|
|
258
|
+
// If attribute is missing or is the string 'true', enable autofilter
|
|
259
|
+
// Only disable if explicitly set to 'false'
|
|
260
|
+
const autofilterEnabled = autofilterValue !== 'false'
|
|
261
|
+
|
|
262
|
+
// Remove existing listeners if any (to avoid duplicates)
|
|
263
|
+
if (this.boundHandleFilterInput) {
|
|
264
|
+
form.removeEventListener('input', this.boundHandleFilterInput, true)
|
|
265
|
+
form.removeEventListener('change', this.boundHandleFilterInput, true)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Only setup debounced listeners if autofilter is enabled
|
|
269
|
+
if (!autofilterEnabled) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Create bound handler for event delegation
|
|
274
|
+
this.boundHandleFilterInput = (event) => {
|
|
275
|
+
const target = event.target
|
|
276
|
+
// Only handle filter inputs (not hidden inputs, buttons, etc.)
|
|
277
|
+
if (target.tagName === 'INPUT' && target.type === 'hidden') return
|
|
278
|
+
if (target.tagName === 'BUTTON') return
|
|
279
|
+
|
|
280
|
+
// Check if it's a filter input
|
|
281
|
+
const isFilterInput = (
|
|
282
|
+
(target.tagName === 'INPUT' && ['text', 'number', 'date'].includes(target.type)) ||
|
|
283
|
+
target.tagName === 'SELECT'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (isFilterInput) {
|
|
287
|
+
this.debouncedSubmitFilters()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Use event delegation on the form for both input and change events
|
|
292
|
+
// This handles all filter inputs without needing to attach individual listeners
|
|
293
|
+
form.addEventListener('input', this.boundHandleFilterInput, true)
|
|
294
|
+
form.addEventListener('change', this.boundHandleFilterInput, true)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Debounced filter submission
|
|
298
|
+
debouncedSubmitFilters() {
|
|
299
|
+
// Clear existing timer
|
|
300
|
+
if (this.debounceTimer) {
|
|
301
|
+
clearTimeout(this.debounceTimer)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Set new timer
|
|
305
|
+
this.debounceTimer = setTimeout(() => {
|
|
306
|
+
this.submitFilters(null)
|
|
307
|
+
}, this.debounceDelay)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Clear all filters
|
|
311
|
+
clearFilters(event) {
|
|
312
|
+
event.preventDefault()
|
|
313
|
+
const form = this.contentTarget.querySelector("form")
|
|
314
|
+
if (form) {
|
|
315
|
+
// Clear debounce timer
|
|
316
|
+
if (this.debounceTimer) {
|
|
317
|
+
clearTimeout(this.debounceTimer)
|
|
318
|
+
this.debounceTimer = null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Reset all inputs except hidden ones (sort, per_page)
|
|
322
|
+
const inputs = form.querySelectorAll('input:not([type="hidden"]), select, textarea')
|
|
323
|
+
inputs.forEach(input => {
|
|
324
|
+
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
325
|
+
input.checked = false
|
|
326
|
+
} else {
|
|
327
|
+
input.value = ''
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
this.syncFilterOps()
|
|
332
|
+
|
|
333
|
+
// Submit immediately (no debounce for clear)
|
|
334
|
+
this.submitFilters(null)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Clear a single filter
|
|
339
|
+
clearSingleFilter(event) {
|
|
340
|
+
event.preventDefault()
|
|
341
|
+
const button = event.currentTarget
|
|
342
|
+
const filterName = button.dataset.filterName
|
|
343
|
+
const filterType = button.dataset.filterType
|
|
344
|
+
const form = this.contentTarget.querySelector("form")
|
|
345
|
+
|
|
346
|
+
if (!form || !filterName) return
|
|
347
|
+
|
|
348
|
+
// Controls are found by name inside this table's form, so ids may repeat across tables
|
|
349
|
+
const field = (suffix) => form.querySelector(
|
|
350
|
+
`[name="filters[${filterName}][${suffix}]"], [name="filters[${filterName}][${suffix}][]"]`
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Clear debounce timer
|
|
354
|
+
if (this.debounceTimer) {
|
|
355
|
+
clearTimeout(this.debounceTimer)
|
|
356
|
+
this.debounceTimer = null
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Clear based on filter type
|
|
360
|
+
switch (filterType) {
|
|
361
|
+
case 'text':
|
|
362
|
+
// Clear both operation and value
|
|
363
|
+
const textOp = field('op')
|
|
364
|
+
const textValue = field('value')
|
|
365
|
+
if (textOp) textOp.value = 'contains'
|
|
366
|
+
if (textValue) textValue.value = ''
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
case 'number':
|
|
370
|
+
case 'date':
|
|
371
|
+
// Clear both from and to fields
|
|
372
|
+
const fromInput = field('from')
|
|
373
|
+
const toInput = field('to')
|
|
374
|
+
if (fromInput) fromInput.value = ''
|
|
375
|
+
if (toInput) toInput.value = ''
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
case 'select':
|
|
379
|
+
// Clear select value (single or multi)
|
|
380
|
+
const selectInput = field('value')
|
|
381
|
+
if (selectInput) {
|
|
382
|
+
if (selectInput.multiple) {
|
|
383
|
+
// Clear all selections in multi-select
|
|
384
|
+
Array.from(selectInput.options).forEach(option => {
|
|
385
|
+
option.selected = false
|
|
386
|
+
})
|
|
387
|
+
} else {
|
|
388
|
+
// Reset to empty/default option
|
|
389
|
+
selectInput.value = ''
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
break
|
|
393
|
+
|
|
394
|
+
case 'boolean':
|
|
395
|
+
// Reset to "All" option
|
|
396
|
+
const booleanInput = field('value')
|
|
397
|
+
if (booleanInput) booleanInput.value = ''
|
|
398
|
+
break
|
|
399
|
+
|
|
400
|
+
default:
|
|
401
|
+
// Generic clear for any other type
|
|
402
|
+
const input = field('value')
|
|
403
|
+
if (input) input.value = ''
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.syncFilterOps()
|
|
407
|
+
|
|
408
|
+
// Submit immediately (no debounce for clear)
|
|
409
|
+
this.submitFilters(null)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Hide the value input for operators that take none (is_empty / is_not_empty)
|
|
413
|
+
syncFilterOps() {
|
|
414
|
+
this.element.querySelectorAll("select[data-filter-op]").forEach(select => {
|
|
415
|
+
const container = select.closest("[data-filter-container]")
|
|
416
|
+
const wrapper = container?.querySelector("[data-filter-value-wrapper]")
|
|
417
|
+
if (!container || !wrapper) return
|
|
418
|
+
|
|
419
|
+
const noValue = select.value === "is_empty" || select.value === "is_not_empty"
|
|
420
|
+
if (noValue) wrapper.querySelector("input").value = ""
|
|
421
|
+
wrapper.style.display = noValue ? "none" : "block"
|
|
422
|
+
container.className = noValue ? "grid grid-cols-1 gap-2" : "grid grid-cols-[1fr_2fr] gap-2"
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Tab switching
|
|
427
|
+
switchTab(event) {
|
|
428
|
+
event.preventDefault()
|
|
429
|
+
const tab = event.currentTarget.dataset.tab
|
|
430
|
+
this.currentTabValue = tab
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
switchTabTo(tab) {
|
|
434
|
+
// Update tab buttons
|
|
435
|
+
this.tabButtonTargets.forEach(button => {
|
|
436
|
+
const isActive = button.dataset.tab === tab
|
|
437
|
+
if (isActive) {
|
|
438
|
+
button.classList.add("text-indigo-600", "border-indigo-600")
|
|
439
|
+
button.classList.remove("text-gray-500", "border-transparent")
|
|
440
|
+
} else {
|
|
441
|
+
button.classList.remove("text-indigo-600", "border-indigo-600")
|
|
442
|
+
button.classList.add("text-gray-500", "border-transparent")
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// Update tab content
|
|
447
|
+
this.tabContentTargets.forEach(content => {
|
|
448
|
+
if (content.dataset.tab === tab) {
|
|
449
|
+
content.classList.remove("hidden")
|
|
450
|
+
} else {
|
|
451
|
+
content.classList.add("hidden")
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// Update panel title
|
|
456
|
+
if (this.hasPanelTitleTarget) {
|
|
457
|
+
if (tab === "columns") {
|
|
458
|
+
this.panelTitleTarget.textContent = this.panelTitleTarget.dataset.columnsTitle || "Customize Columns"
|
|
459
|
+
} else if (tab === "filters") {
|
|
460
|
+
this.panelTitleTarget.textContent = this.panelTitleTarget.dataset.filtersTitle || "Filters"
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Setup debounced filters when switching to filters tab
|
|
465
|
+
if (tab === "filters") {
|
|
466
|
+
setTimeout(() => {
|
|
467
|
+
this.setupDebouncedFilters()
|
|
468
|
+
}, 50)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Sync column checkboxes and order with localStorage when switching to columns tab
|
|
472
|
+
if (tab === "columns") {
|
|
473
|
+
setTimeout(() => {
|
|
474
|
+
this.syncColumnCheckboxes()
|
|
475
|
+
// Sync column order in the drawer
|
|
476
|
+
this.syncColumnListOrder()
|
|
477
|
+
// Also update badge for all tables
|
|
478
|
+
this.updateAllColumnBadges()
|
|
479
|
+
}, 50)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Column visibility management
|
|
484
|
+
updateColumnVisibility(event) {
|
|
485
|
+
event.preventDefault()
|
|
486
|
+
const checkbox = event.currentTarget
|
|
487
|
+
const form = checkbox.closest("form")
|
|
488
|
+
if (!form) return
|
|
489
|
+
|
|
490
|
+
const tableName = form.dataset.tableName
|
|
491
|
+
const columnName = checkbox.dataset.columnName
|
|
492
|
+
const isVisible = checkbox.checked
|
|
493
|
+
|
|
494
|
+
if (!tableName || !columnName) {
|
|
495
|
+
console.warn("Missing tableName or columnName", { tableName, columnName })
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Save to localStorage
|
|
500
|
+
this.saveColumnVisibility(tableName, columnName, isVisible)
|
|
501
|
+
|
|
502
|
+
// Apply visibility to table
|
|
503
|
+
this.applyColumnVisibility(tableName)
|
|
504
|
+
|
|
505
|
+
// Update badge
|
|
506
|
+
this.updateColumnBadge(tableName)
|
|
507
|
+
|
|
508
|
+
// Dispatch event to update export link
|
|
509
|
+
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
510
|
+
detail: { tableName }
|
|
511
|
+
}))
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
selectAllColumns(event) {
|
|
515
|
+
event.preventDefault()
|
|
516
|
+
const tableName = event.currentTarget.dataset.tableName
|
|
517
|
+
const form = document.getElementById(`column-visibility-form-${tableName}`)
|
|
518
|
+
|
|
519
|
+
if (form) {
|
|
520
|
+
const checkboxes = form.querySelectorAll('input[type="checkbox"]')
|
|
521
|
+
checkboxes.forEach(checkbox => {
|
|
522
|
+
checkbox.checked = true
|
|
523
|
+
const columnName = checkbox.dataset.columnName
|
|
524
|
+
this.saveColumnVisibility(tableName, columnName, true)
|
|
525
|
+
})
|
|
526
|
+
this.applyColumnVisibility(tableName)
|
|
527
|
+
this.updateColumnBadge(tableName)
|
|
528
|
+
|
|
529
|
+
// Dispatch event to update export link
|
|
530
|
+
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
531
|
+
detail: { tableName }
|
|
532
|
+
}))
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
deselectAllColumns(event) {
|
|
537
|
+
event.preventDefault()
|
|
538
|
+
const tableName = event.currentTarget.dataset.tableName
|
|
539
|
+
const form = document.getElementById(`column-visibility-form-${tableName}`)
|
|
540
|
+
|
|
541
|
+
if (form) {
|
|
542
|
+
const checkboxes = form.querySelectorAll('input[type="checkbox"]')
|
|
543
|
+
checkboxes.forEach(checkbox => {
|
|
544
|
+
checkbox.checked = false
|
|
545
|
+
const columnName = checkbox.dataset.columnName
|
|
546
|
+
this.saveColumnVisibility(tableName, columnName, false)
|
|
547
|
+
})
|
|
548
|
+
this.applyColumnVisibility(tableName)
|
|
549
|
+
this.updateColumnBadge(tableName)
|
|
550
|
+
|
|
551
|
+
// Dispatch event to update export link
|
|
552
|
+
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
553
|
+
detail: { tableName }
|
|
554
|
+
}))
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Save column visibility to localStorage
|
|
559
|
+
saveColumnVisibility(tableName, columnName, isVisible) {
|
|
560
|
+
const key = `infotable_columns_${tableName}`
|
|
561
|
+
let visibility = {}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const stored = localStorage.getItem(key)
|
|
565
|
+
if (stored) {
|
|
566
|
+
visibility = JSON.parse(stored)
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {
|
|
569
|
+
console.warn("Failed to parse column visibility from localStorage", e)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
visibility[columnName] = isVisible
|
|
573
|
+
localStorage.setItem(key, JSON.stringify(visibility))
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Sync checkboxes with localStorage values or defaults
|
|
577
|
+
syncColumnCheckboxes() {
|
|
578
|
+
// Find all column visibility forms
|
|
579
|
+
const forms = this.contentTarget.querySelectorAll('[id^="column-visibility-form-"]')
|
|
580
|
+
forms.forEach(form => {
|
|
581
|
+
const tableName = form.dataset.tableName
|
|
582
|
+
const visibility = this.getColumnVisibility(tableName)
|
|
583
|
+
|
|
584
|
+
// Update checkboxes based on saved preferences or defaults
|
|
585
|
+
const checkboxes = form.querySelectorAll('input[type="checkbox"]')
|
|
586
|
+
checkboxes.forEach(checkbox => {
|
|
587
|
+
const columnName = checkbox.dataset.columnName
|
|
588
|
+
if (visibility && visibility.hasOwnProperty(columnName)) {
|
|
589
|
+
// Use saved preference from localStorage
|
|
590
|
+
checkbox.checked = visibility[columnName]
|
|
591
|
+
} else {
|
|
592
|
+
// Use default visibility from data attribute
|
|
593
|
+
const defaultVisible = checkbox.dataset.defaultVisible === "true"
|
|
594
|
+
checkbox.checked = defaultVisible
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
// Update badge after syncing
|
|
599
|
+
this.updateColumnBadge(tableName)
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Sync column list order in the drawer with localStorage
|
|
604
|
+
syncColumnListOrder() {
|
|
605
|
+
if (!this.hasColumnListTarget) return
|
|
606
|
+
|
|
607
|
+
this.columnListTargets.forEach(list => {
|
|
608
|
+
const tableName = list.dataset.tableName
|
|
609
|
+
const defaultSchema = JSON.parse(list.dataset.columnsSchema || '[]')
|
|
610
|
+
const storedOrder = this.loadColumnOrder(tableName)
|
|
611
|
+
|
|
612
|
+
if (!storedOrder) {
|
|
613
|
+
// No stored order, but still sync pin buttons with saved pinned state
|
|
614
|
+
const pinnedData = this.getAllColumnsPinned(tableName)
|
|
615
|
+
const columnItems = list.querySelectorAll('.column-item')
|
|
616
|
+
columnItems.forEach(item => {
|
|
617
|
+
const columnName = item.dataset.columnName
|
|
618
|
+
const pinned = pinnedData[columnName] || null
|
|
619
|
+
this.updatePinButtons(item, pinned)
|
|
620
|
+
})
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Validate stored order
|
|
625
|
+
if (!this.isSchemaValid(storedOrder, defaultSchema)) {
|
|
626
|
+
console.warn(`Schema mismatch for table ${tableName}, clearing stored order`)
|
|
627
|
+
this.clearColumnOrder(tableName)
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Apply order to the list
|
|
632
|
+
this.applyColumnOrderToList(tableName, storedOrder)
|
|
633
|
+
|
|
634
|
+
// Sync pin button styling with localStorage
|
|
635
|
+
const pinnedData = this.getAllColumnsPinned(tableName)
|
|
636
|
+
const columnItems = list.querySelectorAll('.column-item')
|
|
637
|
+
columnItems.forEach(item => {
|
|
638
|
+
const columnName = item.dataset.columnName
|
|
639
|
+
const pinned = pinnedData[columnName] || null
|
|
640
|
+
this.updatePinButtons(item, pinned)
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Get column visibility from localStorage
|
|
646
|
+
getColumnVisibility(tableName) {
|
|
647
|
+
const key = `infotable_columns_${tableName}`
|
|
648
|
+
try {
|
|
649
|
+
const stored = localStorage.getItem(key)
|
|
650
|
+
return stored ? JSON.parse(stored) : null
|
|
651
|
+
} catch (e) {
|
|
652
|
+
console.warn("Failed to parse column visibility from localStorage", e)
|
|
653
|
+
return null
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Apply column visibility to the table
|
|
658
|
+
applyColumnVisibility(tableName) {
|
|
659
|
+
const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
|
|
660
|
+
if (!tableWrapper) {
|
|
661
|
+
console.warn(`Table wrapper not found for: ${tableName}`)
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Try to find table in the wrapper or in turbo-frame
|
|
666
|
+
let table = tableWrapper.querySelector("table")
|
|
667
|
+
if (!table) {
|
|
668
|
+
// Table might be inside turbo-frame
|
|
669
|
+
const turboFrame = document.getElementById(`${tableName}_content`)
|
|
670
|
+
if (turboFrame) {
|
|
671
|
+
table = turboFrame.querySelector("table")
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (!table) {
|
|
676
|
+
console.warn(`Table not found for: ${tableName}`)
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Find all table headers and cells
|
|
681
|
+
const headers = table.querySelectorAll("thead th[data-column-name]")
|
|
682
|
+
const rows = table.querySelectorAll("tbody tr")
|
|
683
|
+
|
|
684
|
+
if (headers.length === 0) {
|
|
685
|
+
console.warn(`No column headers found for table: ${tableName}`)
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Get saved preferences from localStorage
|
|
690
|
+
const visibility = this.getColumnVisibility(tableName)
|
|
691
|
+
|
|
692
|
+
headers.forEach((header) => {
|
|
693
|
+
const columnName = header.dataset.columnName
|
|
694
|
+
const defaultVisible = header.dataset.defaultVisible === "true"
|
|
695
|
+
|
|
696
|
+
// Determine if column should be visible
|
|
697
|
+
let shouldShow
|
|
698
|
+
if (visibility && visibility.hasOwnProperty(columnName)) {
|
|
699
|
+
// Use saved preference
|
|
700
|
+
shouldShow = visibility[columnName]
|
|
701
|
+
} else {
|
|
702
|
+
// Use default visibility
|
|
703
|
+
shouldShow = defaultVisible
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Apply visibility
|
|
707
|
+
header.style.display = shouldShow ? "" : "none"
|
|
708
|
+
|
|
709
|
+
// Hide/show corresponding cells in all rows
|
|
710
|
+
rows.forEach(row => {
|
|
711
|
+
const cells = row.querySelectorAll("td[data-column-name]")
|
|
712
|
+
const cell = Array.from(cells).find(c => c.dataset.columnName === columnName)
|
|
713
|
+
if (cell) {
|
|
714
|
+
cell.style.display = shouldShow ? "" : "none"
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Update column badge count for a specific table
|
|
721
|
+
updateColumnBadge(tableName) {
|
|
722
|
+
// Find the badge for this table using the data attribute
|
|
723
|
+
const badge = Array.from(this.columnBadgeTargets || []).find(b =>
|
|
724
|
+
b.dataset.badgeTableName === tableName
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
if (!badge) {
|
|
728
|
+
// Fallback: find badge in tab button
|
|
729
|
+
const tabButton = Array.from(this.tabButtonTargets).find(button =>
|
|
730
|
+
button.dataset.tab === "columns" && button.dataset.tableName === tableName
|
|
731
|
+
)
|
|
732
|
+
if (tabButton) {
|
|
733
|
+
const fallbackBadge = tabButton.querySelector('[data-badge-table-name="' + tableName + '"]')
|
|
734
|
+
if (fallbackBadge) {
|
|
735
|
+
this.updateBadgeText(fallbackBadge, tableName)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
this.updateBadgeText(badge, tableName)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Helper method to update badge text
|
|
745
|
+
updateBadgeText(badge, tableName) {
|
|
746
|
+
// Find the form for this table
|
|
747
|
+
const form = document.getElementById(`column-visibility-form-${tableName}`)
|
|
748
|
+
if (!form) return
|
|
749
|
+
|
|
750
|
+
// Count visible and total columns
|
|
751
|
+
const checkboxes = form.querySelectorAll('input[type="checkbox"]')
|
|
752
|
+
const totalColumns = checkboxes.length
|
|
753
|
+
const visibleColumns = Array.from(checkboxes).filter(cb => cb.checked).length
|
|
754
|
+
|
|
755
|
+
// Update badge text
|
|
756
|
+
badge.textContent = `${visibleColumns} / ${totalColumns}`
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Update all column badges (useful when switching tabs)
|
|
760
|
+
updateAllColumnBadges() {
|
|
761
|
+
// Find all column visibility forms
|
|
762
|
+
const forms = this.contentTarget?.querySelectorAll('[id^="column-visibility-form-"]')
|
|
763
|
+
if (!forms) return
|
|
764
|
+
|
|
765
|
+
forms.forEach(form => {
|
|
766
|
+
const tableName = form.dataset.tableName
|
|
767
|
+
if (tableName) {
|
|
768
|
+
this.updateColumnBadge(tableName)
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ========== Column Pinning ==========
|
|
774
|
+
|
|
775
|
+
// Handle pin button click
|
|
776
|
+
pinColumn(event) {
|
|
777
|
+
event.preventDefault()
|
|
778
|
+
event.stopPropagation()
|
|
779
|
+
|
|
780
|
+
const button = event.currentTarget
|
|
781
|
+
const columnName = button.dataset.columnName
|
|
782
|
+
const pinDirection = button.dataset.pinDirection
|
|
783
|
+
const columnItem = button.closest('.column-item')
|
|
784
|
+
const form = button.closest('form')
|
|
785
|
+
|
|
786
|
+
if (!form || !columnName) return
|
|
787
|
+
|
|
788
|
+
const tableName = form.dataset.tableName
|
|
789
|
+
|
|
790
|
+
// Get current pinned state
|
|
791
|
+
const currentPinned = this.getColumnPinned(tableName, columnName)
|
|
792
|
+
|
|
793
|
+
// Toggle: if clicking same direction, unpin; otherwise, pin in that direction
|
|
794
|
+
const newPinned = (currentPinned === pinDirection) ? null : pinDirection
|
|
795
|
+
|
|
796
|
+
// Save to localStorage
|
|
797
|
+
this.saveColumnPinned(tableName, columnName, newPinned)
|
|
798
|
+
|
|
799
|
+
// Update button styling
|
|
800
|
+
this.updatePinButtons(columnItem, newPinned)
|
|
801
|
+
|
|
802
|
+
// Reorder columns in drawer based on pinned state
|
|
803
|
+
this.reorderColumnsWithPinning(tableName)
|
|
804
|
+
|
|
805
|
+
// Get the new order after pinning
|
|
806
|
+
const list = Array.from(this.columnListTargets).find(l => l.dataset.tableName === tableName)
|
|
807
|
+
if (list) {
|
|
808
|
+
const columnItems = list.querySelectorAll('.column-item')
|
|
809
|
+
const newOrder = Array.from(columnItems).map(item => item.dataset.columnName)
|
|
810
|
+
|
|
811
|
+
// Apply the new order to the table (reorders DOM)
|
|
812
|
+
this.applyColumnOrder(tableName, newOrder)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Apply sticky positioning to table immediately
|
|
816
|
+
this.applyColumnPinningToTable(tableName)
|
|
817
|
+
|
|
818
|
+
// Dispatch event for export link update
|
|
819
|
+
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
820
|
+
detail: { tableName }
|
|
821
|
+
}))
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Save pinned state to localStorage
|
|
825
|
+
saveColumnPinned(tableName, columnName, pinned) {
|
|
826
|
+
const key = `infotable_column_pinned_${tableName}`
|
|
827
|
+
let pinnedData = {}
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const stored = localStorage.getItem(key)
|
|
831
|
+
if (stored) {
|
|
832
|
+
pinnedData = JSON.parse(stored)
|
|
833
|
+
}
|
|
834
|
+
} catch (e) {
|
|
835
|
+
console.warn("Failed to parse column pinned from localStorage", e)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (pinned) {
|
|
839
|
+
pinnedData[columnName] = pinned
|
|
840
|
+
} else {
|
|
841
|
+
delete pinnedData[columnName]
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
localStorage.setItem(key, JSON.stringify(pinnedData))
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Get pinned state from localStorage
|
|
848
|
+
getColumnPinned(tableName, columnName) {
|
|
849
|
+
const key = `infotable_column_pinned_${tableName}`
|
|
850
|
+
try {
|
|
851
|
+
const stored = localStorage.getItem(key)
|
|
852
|
+
if (stored) {
|
|
853
|
+
const pinnedData = JSON.parse(stored)
|
|
854
|
+
return pinnedData[columnName] || null
|
|
855
|
+
}
|
|
856
|
+
} catch (e) {
|
|
857
|
+
console.warn("Failed to parse column pinned from localStorage", e)
|
|
858
|
+
}
|
|
859
|
+
return null
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Get all pinned columns
|
|
863
|
+
getAllColumnsPinned(tableName) {
|
|
864
|
+
const key = `infotable_column_pinned_${tableName}`
|
|
865
|
+
try {
|
|
866
|
+
const stored = localStorage.getItem(key)
|
|
867
|
+
return stored ? JSON.parse(stored) : {}
|
|
868
|
+
} catch (e) {
|
|
869
|
+
console.warn("Failed to parse column pinned from localStorage", e)
|
|
870
|
+
return {}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Update pin button styling
|
|
875
|
+
updatePinButtons(columnItem, pinned) {
|
|
876
|
+
const leftBtn = columnItem.querySelector('.pin-left')
|
|
877
|
+
const rightBtn = columnItem.querySelector('.pin-right')
|
|
878
|
+
|
|
879
|
+
if (leftBtn) {
|
|
880
|
+
if (pinned === 'left') {
|
|
881
|
+
leftBtn.classList.add('bg-indigo-100', 'text-indigo-600')
|
|
882
|
+
leftBtn.classList.remove('text-gray-400', 'hover:bg-gray-100', 'hover:text-gray-600')
|
|
883
|
+
} else {
|
|
884
|
+
leftBtn.classList.remove('bg-indigo-100', 'text-indigo-600')
|
|
885
|
+
leftBtn.classList.add('text-gray-400', 'hover:bg-gray-100', 'hover:text-gray-600')
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (rightBtn) {
|
|
890
|
+
if (pinned === 'right') {
|
|
891
|
+
rightBtn.classList.add('bg-indigo-100', 'text-indigo-600')
|
|
892
|
+
rightBtn.classList.remove('text-gray-400', 'hover:bg-gray-100', 'hover:text-gray-600')
|
|
893
|
+
} else {
|
|
894
|
+
rightBtn.classList.remove('bg-indigo-100', 'text-indigo-600')
|
|
895
|
+
rightBtn.classList.add('text-gray-400', 'hover:bg-gray-100', 'hover:text-gray-600')
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Reorder columns in drawer based on pinning (left, unpinned, right)
|
|
901
|
+
reorderColumnsWithPinning(tableName) {
|
|
902
|
+
const list = Array.from(this.columnListTargets).find(l => l.dataset.tableName === tableName)
|
|
903
|
+
if (!list) return
|
|
904
|
+
|
|
905
|
+
const pinnedData = this.getAllColumnsPinned(tableName)
|
|
906
|
+
const columnItems = Array.from(list.querySelectorAll('.column-item'))
|
|
907
|
+
|
|
908
|
+
// Separate columns by pinned state
|
|
909
|
+
const leftPinned = []
|
|
910
|
+
const unpinned = []
|
|
911
|
+
const rightPinned = []
|
|
912
|
+
|
|
913
|
+
columnItems.forEach(item => {
|
|
914
|
+
const columnName = item.dataset.columnName
|
|
915
|
+
const pinned = pinnedData[columnName]
|
|
916
|
+
|
|
917
|
+
if (pinned === 'left') {
|
|
918
|
+
leftPinned.push(item)
|
|
919
|
+
} else if (pinned === 'right') {
|
|
920
|
+
rightPinned.push(item)
|
|
921
|
+
} else {
|
|
922
|
+
unpinned.push(item)
|
|
923
|
+
}
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
// Reorder in DOM: left, unpinned, right
|
|
927
|
+
leftPinned.forEach(item => list.appendChild(item))
|
|
928
|
+
unpinned.forEach(item => list.appendChild(item))
|
|
929
|
+
rightPinned.forEach(item => list.appendChild(item))
|
|
930
|
+
|
|
931
|
+
// Save new order to localStorage
|
|
932
|
+
const newOrder = [...leftPinned, ...unpinned, ...rightPinned].map(item => item.dataset.columnName)
|
|
933
|
+
this.saveColumnOrder(tableName, newOrder)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Apply pinning to table (add sticky positioning)
|
|
937
|
+
applyColumnPinningToTable(tableName) {
|
|
938
|
+
const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
|
|
939
|
+
if (!tableWrapper) return
|
|
940
|
+
|
|
941
|
+
let table = tableWrapper.querySelector("table")
|
|
942
|
+
if (!table) {
|
|
943
|
+
const turboFrame = document.getElementById(`${tableName}_content`)
|
|
944
|
+
if (turboFrame) {
|
|
945
|
+
table = turboFrame.querySelector("table")
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (!table) return
|
|
950
|
+
|
|
951
|
+
const pinnedData = this.getAllColumnsPinned(tableName)
|
|
952
|
+
const headers = Array.from(table.querySelectorAll("thead th[data-column-name]"))
|
|
953
|
+
const rows = table.querySelectorAll("tbody tr")
|
|
954
|
+
|
|
955
|
+
// Calculate positions for pinned columns
|
|
956
|
+
let leftOffset = 0
|
|
957
|
+
let rightOffset = 0
|
|
958
|
+
|
|
959
|
+
// First pass: reset all columns
|
|
960
|
+
headers.forEach(header => {
|
|
961
|
+
header.style.position = ''
|
|
962
|
+
header.style.left = ''
|
|
963
|
+
header.style.right = ''
|
|
964
|
+
header.style.zIndex = ''
|
|
965
|
+
header.style.removeProperty('background-color')
|
|
966
|
+
header.style.removeProperty('background-image')
|
|
967
|
+
header.style.removeProperty('opacity')
|
|
968
|
+
header.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-gray-50', "after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]", "before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
rows.forEach(row => {
|
|
972
|
+
const cells = row.querySelectorAll("td[data-column-name]")
|
|
973
|
+
cells.forEach(cell => {
|
|
974
|
+
cell.style.position = ''
|
|
975
|
+
cell.style.left = ''
|
|
976
|
+
cell.style.right = ''
|
|
977
|
+
cell.style.zIndex = ''
|
|
978
|
+
cell.style.removeProperty('background-color')
|
|
979
|
+
cell.style.removeProperty('background-image')
|
|
980
|
+
cell.style.removeProperty('opacity')
|
|
981
|
+
cell.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-white', "after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]", "before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
|
|
982
|
+
})
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
// Find last left-pinned and first right-pinned columns
|
|
986
|
+
let lastLeftPinned = null
|
|
987
|
+
let firstRightPinned = null
|
|
988
|
+
|
|
989
|
+
headers.forEach(header => {
|
|
990
|
+
const columnName = header.dataset.columnName
|
|
991
|
+
const pinned = pinnedData[columnName]
|
|
992
|
+
if (pinned === 'left') {
|
|
993
|
+
lastLeftPinned = columnName
|
|
994
|
+
}
|
|
995
|
+
if (pinned === 'right' && !firstRightPinned) {
|
|
996
|
+
firstRightPinned = columnName
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
// Apply sticky positioning for pinned columns
|
|
1001
|
+
headers.forEach(header => {
|
|
1002
|
+
const columnName = header.dataset.columnName
|
|
1003
|
+
const pinned = pinnedData[columnName]
|
|
1004
|
+
|
|
1005
|
+
if (pinned === 'left') {
|
|
1006
|
+
// Get existing inline styles
|
|
1007
|
+
const existingStyles = header.getAttribute('style') || ''
|
|
1008
|
+
|
|
1009
|
+
// Build comprehensive style string with all !important flags
|
|
1010
|
+
const newStyles = `
|
|
1011
|
+
position: sticky !important;
|
|
1012
|
+
left: ${leftOffset}px !important;
|
|
1013
|
+
z-index: 30 !important;
|
|
1014
|
+
background: #f9fafb !important;
|
|
1015
|
+
background-color: #f9fafb !important;
|
|
1016
|
+
background-image: none !important;
|
|
1017
|
+
background-position: initial !important;
|
|
1018
|
+
background-size: initial !important;
|
|
1019
|
+
background-repeat: initial !important;
|
|
1020
|
+
background-attachment: initial !important;
|
|
1021
|
+
opacity: 1 !important;
|
|
1022
|
+
${existingStyles}
|
|
1023
|
+
`.replace(/\s+/g, ' ').trim()
|
|
1024
|
+
|
|
1025
|
+
// Set all styles at once via setAttribute
|
|
1026
|
+
header.setAttribute('style', newStyles)
|
|
1027
|
+
header.classList.add('infotable-pinned-left', '!bg-gray-50')
|
|
1028
|
+
|
|
1029
|
+
// Mark last left-pinned column
|
|
1030
|
+
if (columnName === lastLeftPinned) {
|
|
1031
|
+
header.classList.add("after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]")
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Apply to cells in all rows
|
|
1035
|
+
rows.forEach(row => {
|
|
1036
|
+
const cell = row.querySelector(`td[data-column-name="${columnName}"]`)
|
|
1037
|
+
if (cell) {
|
|
1038
|
+
// Get existing inline styles
|
|
1039
|
+
const existingStyles = cell.getAttribute('style') || ''
|
|
1040
|
+
|
|
1041
|
+
// Build comprehensive style string
|
|
1042
|
+
const newStyles = `
|
|
1043
|
+
position: sticky !important;
|
|
1044
|
+
left: ${leftOffset}px !important;
|
|
1045
|
+
z-index: 15 !important;
|
|
1046
|
+
background: #ffffff !important;
|
|
1047
|
+
background-color: #ffffff !important;
|
|
1048
|
+
background-image: none !important;
|
|
1049
|
+
background-position: initial !important;
|
|
1050
|
+
background-size: initial !important;
|
|
1051
|
+
background-repeat: initial !important;
|
|
1052
|
+
background-attachment: initial !important;
|
|
1053
|
+
opacity: 1 !important;
|
|
1054
|
+
${existingStyles}
|
|
1055
|
+
`.replace(/\s+/g, ' ').trim()
|
|
1056
|
+
|
|
1057
|
+
cell.setAttribute('style', newStyles)
|
|
1058
|
+
cell.classList.add('infotable-pinned-left', '!bg-white')
|
|
1059
|
+
|
|
1060
|
+
if (columnName === lastLeftPinned) {
|
|
1061
|
+
cell.classList.add("after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]")
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
leftOffset += header.offsetWidth
|
|
1067
|
+
} else if (pinned === 'right') {
|
|
1068
|
+
// Get existing inline styles
|
|
1069
|
+
const existingStyles = header.getAttribute('style') || ''
|
|
1070
|
+
|
|
1071
|
+
// Build comprehensive style string
|
|
1072
|
+
const newStyles = `
|
|
1073
|
+
position: sticky !important;
|
|
1074
|
+
right: ${rightOffset}px !important;
|
|
1075
|
+
z-index: 30 !important;
|
|
1076
|
+
background: #f9fafb !important;
|
|
1077
|
+
background-color: #f9fafb !important;
|
|
1078
|
+
background-image: none !important;
|
|
1079
|
+
background-position: initial !important;
|
|
1080
|
+
background-size: initial !important;
|
|
1081
|
+
background-repeat: initial !important;
|
|
1082
|
+
background-attachment: initial !important;
|
|
1083
|
+
opacity: 1 !important;
|
|
1084
|
+
${existingStyles}
|
|
1085
|
+
`.replace(/\s+/g, ' ').trim()
|
|
1086
|
+
|
|
1087
|
+
header.setAttribute('style', newStyles)
|
|
1088
|
+
header.classList.add('infotable-pinned-right', '!bg-gray-50')
|
|
1089
|
+
|
|
1090
|
+
// Mark first right-pinned column
|
|
1091
|
+
if (columnName === firstRightPinned) {
|
|
1092
|
+
header.classList.add("before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Apply to cells in all rows
|
|
1096
|
+
rows.forEach(row => {
|
|
1097
|
+
const cell = row.querySelector(`td[data-column-name="${columnName}"]`)
|
|
1098
|
+
if (cell) {
|
|
1099
|
+
// Get existing inline styles
|
|
1100
|
+
const existingStyles = cell.getAttribute('style') || ''
|
|
1101
|
+
|
|
1102
|
+
// Build comprehensive style string
|
|
1103
|
+
const newStyles = `
|
|
1104
|
+
position: sticky !important;
|
|
1105
|
+
right: ${rightOffset}px !important;
|
|
1106
|
+
z-index: 15 !important;
|
|
1107
|
+
background: #ffffff !important;
|
|
1108
|
+
background-color: #ffffff !important;
|
|
1109
|
+
background-image: none !important;
|
|
1110
|
+
background-position: initial !important;
|
|
1111
|
+
background-size: initial !important;
|
|
1112
|
+
background-repeat: initial !important;
|
|
1113
|
+
background-attachment: initial !important;
|
|
1114
|
+
opacity: 1 !important;
|
|
1115
|
+
${existingStyles}
|
|
1116
|
+
`.replace(/\s+/g, ' ').trim()
|
|
1117
|
+
|
|
1118
|
+
cell.setAttribute('style', newStyles)
|
|
1119
|
+
cell.classList.add('infotable-pinned-right', '!bg-white')
|
|
1120
|
+
|
|
1121
|
+
if (columnName === firstRightPinned) {
|
|
1122
|
+
cell.classList.add("before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
rightOffset += header.offsetWidth
|
|
1128
|
+
}
|
|
1129
|
+
})
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ========== Column Ordering with Drag and Drop ==========
|
|
1133
|
+
|
|
1134
|
+
// Initialize Sortable for drag and drop column ordering
|
|
1135
|
+
initializeSortable() {
|
|
1136
|
+
if (!this.hasColumnListTarget) return
|
|
1137
|
+
|
|
1138
|
+
this.columnListTargets.forEach(list => {
|
|
1139
|
+
const tableName = list.dataset.tableName
|
|
1140
|
+
const defaultSchema = JSON.parse(list.dataset.columnsSchema || '[]')
|
|
1141
|
+
|
|
1142
|
+
// Validate stored schema and load column order
|
|
1143
|
+
this.validateAndLoadColumnOrder(tableName, defaultSchema)
|
|
1144
|
+
|
|
1145
|
+
// Initialize Sortable.js
|
|
1146
|
+
const sortable = new Sortable(list, {
|
|
1147
|
+
animation: 150,
|
|
1148
|
+
handle: '.drag-handle',
|
|
1149
|
+
ghostClass: 'opacity-50',
|
|
1150
|
+
dragClass: 'shadow-lg',
|
|
1151
|
+
onEnd: (evt) => {
|
|
1152
|
+
this.handleColumnReorder(tableName, list)
|
|
1153
|
+
}
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
// Store sortable instance for cleanup
|
|
1157
|
+
if (!this.sortableInstances) {
|
|
1158
|
+
this.sortableInstances = new Map()
|
|
1159
|
+
}
|
|
1160
|
+
this.sortableInstances.set(tableName, sortable)
|
|
1161
|
+
})
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Handle column reorder after drag and drop
|
|
1165
|
+
handleColumnReorder(tableName, list) {
|
|
1166
|
+
// Get new order from DOM
|
|
1167
|
+
const columnItems = list.querySelectorAll('.column-item')
|
|
1168
|
+
const newOrder = Array.from(columnItems).map(item => item.dataset.columnName)
|
|
1169
|
+
|
|
1170
|
+
// Unpin all columns when manually reordering
|
|
1171
|
+
const pinnedKey = `infotable_column_pinned_${tableName}`
|
|
1172
|
+
localStorage.removeItem(pinnedKey)
|
|
1173
|
+
|
|
1174
|
+
// Update pin button styling for all items
|
|
1175
|
+
columnItems.forEach(item => {
|
|
1176
|
+
this.updatePinButtons(item, null)
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
// Save to localStorage
|
|
1180
|
+
this.saveColumnOrder(tableName, newOrder)
|
|
1181
|
+
|
|
1182
|
+
// Apply order to table
|
|
1183
|
+
this.applyColumnOrder(tableName, newOrder)
|
|
1184
|
+
|
|
1185
|
+
// Remove sticky positioning since all are unpinned
|
|
1186
|
+
this.applyColumnPinningToTable(tableName)
|
|
1187
|
+
|
|
1188
|
+
// Dispatch event for export link update
|
|
1189
|
+
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
1190
|
+
detail: { tableName }
|
|
1191
|
+
}))
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Save column order to localStorage
|
|
1195
|
+
saveColumnOrder(tableName, order) {
|
|
1196
|
+
const key = `infotable_column_order_${tableName}`
|
|
1197
|
+
localStorage.setItem(key, JSON.stringify(order))
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Load column order from localStorage
|
|
1201
|
+
loadColumnOrder(tableName) {
|
|
1202
|
+
const key = `infotable_column_order_${tableName}`
|
|
1203
|
+
try {
|
|
1204
|
+
const stored = localStorage.getItem(key)
|
|
1205
|
+
return stored ? JSON.parse(stored) : null
|
|
1206
|
+
} catch (e) {
|
|
1207
|
+
console.warn("Failed to parse column order from localStorage", e)
|
|
1208
|
+
return null
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Validate stored schema against backend schema
|
|
1213
|
+
validateAndLoadColumnOrder(tableName, defaultSchema) {
|
|
1214
|
+
const storedOrder = this.loadColumnOrder(tableName)
|
|
1215
|
+
|
|
1216
|
+
if (!storedOrder) {
|
|
1217
|
+
// No stored order, nothing to validate
|
|
1218
|
+
return
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Check if stored order matches backend schema
|
|
1222
|
+
const isValid = this.isSchemaValid(storedOrder, defaultSchema)
|
|
1223
|
+
|
|
1224
|
+
if (!isValid) {
|
|
1225
|
+
// Schema mismatch - clear invalid localStorage
|
|
1226
|
+
console.warn(`Schema mismatch for table ${tableName}, clearing stored order`)
|
|
1227
|
+
this.clearColumnOrder(tableName)
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Valid schema, apply the order
|
|
1232
|
+
this.applyColumnOrderToList(tableName, storedOrder)
|
|
1233
|
+
this.applyColumnOrder(tableName, storedOrder)
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Check if stored order is valid against default schema
|
|
1237
|
+
isSchemaValid(storedOrder, defaultSchema) {
|
|
1238
|
+
// Extract column names from schema (handle both old and new formats)
|
|
1239
|
+
const schemaNames = defaultSchema.map(item =>
|
|
1240
|
+
typeof item === 'string' ? item : item.name
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
// Must have same length
|
|
1244
|
+
if (storedOrder.length !== schemaNames.length) {
|
|
1245
|
+
return false
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Must contain same columns (order can differ)
|
|
1249
|
+
const storedSet = new Set(storedOrder)
|
|
1250
|
+
const defaultSet = new Set(schemaNames)
|
|
1251
|
+
|
|
1252
|
+
if (storedSet.size !== defaultSet.size) {
|
|
1253
|
+
return false
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
for (const col of storedOrder) {
|
|
1257
|
+
if (!defaultSet.has(col)) {
|
|
1258
|
+
return false
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return true
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Apply column order to the column list in the drawer
|
|
1266
|
+
applyColumnOrderToList(tableName, order) {
|
|
1267
|
+
const list = Array.from(this.columnListTargets).find(l => l.dataset.tableName === tableName)
|
|
1268
|
+
if (!list) return
|
|
1269
|
+
|
|
1270
|
+
const columnItems = Array.from(list.querySelectorAll('.column-item'))
|
|
1271
|
+
const itemMap = new Map()
|
|
1272
|
+
|
|
1273
|
+
// Create map of column name to DOM element
|
|
1274
|
+
columnItems.forEach(item => {
|
|
1275
|
+
itemMap.set(item.dataset.columnName, item)
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
// Reorder DOM elements according to order array
|
|
1279
|
+
order.forEach(columnName => {
|
|
1280
|
+
const item = itemMap.get(columnName)
|
|
1281
|
+
if (item) {
|
|
1282
|
+
list.appendChild(item)
|
|
1283
|
+
}
|
|
1284
|
+
})
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Apply column order to the table
|
|
1288
|
+
applyColumnOrder(tableName, order) {
|
|
1289
|
+
const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
|
|
1290
|
+
if (!tableWrapper) return
|
|
1291
|
+
|
|
1292
|
+
// Try to find table in the wrapper or in turbo-frame
|
|
1293
|
+
let table = tableWrapper.querySelector("table")
|
|
1294
|
+
if (!table) {
|
|
1295
|
+
const turboFrame = document.getElementById(`${tableName}_content`)
|
|
1296
|
+
if (turboFrame) {
|
|
1297
|
+
table = turboFrame.querySelector("table")
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (!table) return
|
|
1302
|
+
|
|
1303
|
+
// Get headers and create a map
|
|
1304
|
+
const headerRow = table.querySelector("thead tr")
|
|
1305
|
+
if (!headerRow) return
|
|
1306
|
+
|
|
1307
|
+
const headers = Array.from(headerRow.querySelectorAll("th[data-column-name]"))
|
|
1308
|
+
const headerMap = new Map()
|
|
1309
|
+
|
|
1310
|
+
headers.forEach((header, index) => {
|
|
1311
|
+
headerMap.set(header.dataset.columnName, { element: header, originalIndex: index })
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
// Reorder headers according to order array
|
|
1315
|
+
order.forEach(columnName => {
|
|
1316
|
+
const headerInfo = headerMap.get(columnName)
|
|
1317
|
+
if (headerInfo) {
|
|
1318
|
+
headerRow.appendChild(headerInfo.element)
|
|
1319
|
+
}
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
// Reorder cells in each row
|
|
1323
|
+
const rows = table.querySelectorAll("tbody tr")
|
|
1324
|
+
rows.forEach(row => {
|
|
1325
|
+
const cells = Array.from(row.querySelectorAll("td[data-column-name]"))
|
|
1326
|
+
const cellMap = new Map()
|
|
1327
|
+
|
|
1328
|
+
cells.forEach(cell => {
|
|
1329
|
+
cellMap.set(cell.dataset.columnName, cell)
|
|
1330
|
+
})
|
|
1331
|
+
|
|
1332
|
+
order.forEach(columnName => {
|
|
1333
|
+
const cell = cellMap.get(columnName)
|
|
1334
|
+
if (cell) {
|
|
1335
|
+
row.appendChild(cell)
|
|
1336
|
+
}
|
|
1337
|
+
})
|
|
1338
|
+
})
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Clear column order from localStorage
|
|
1342
|
+
clearColumnOrder(tableName) {
|
|
1343
|
+
const key = `infotable_column_order_${tableName}`
|
|
1344
|
+
localStorage.removeItem(key)
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Reset to defaults - clear all custom settings
|
|
1348
|
+
resetToDefaults(event) {
|
|
1349
|
+
event.preventDefault()
|
|
1350
|
+
const tableName = event.currentTarget.dataset.tableName
|
|
1351
|
+
|
|
1352
|
+
if (!tableName) return
|
|
1353
|
+
|
|
1354
|
+
// Clear column visibility
|
|
1355
|
+
const visibilityKey = `infotable_columns_${tableName}`
|
|
1356
|
+
localStorage.removeItem(visibilityKey)
|
|
1357
|
+
|
|
1358
|
+
// Clear column order
|
|
1359
|
+
this.clearColumnOrder(tableName)
|
|
1360
|
+
|
|
1361
|
+
// Clear column pinning
|
|
1362
|
+
const pinnedKey = `infotable_column_pinned_${tableName}`
|
|
1363
|
+
localStorage.removeItem(pinnedKey)
|
|
1364
|
+
|
|
1365
|
+
// Reset checkboxes to defaults
|
|
1366
|
+
const form = document.getElementById(`column-visibility-form-${tableName}`)
|
|
1367
|
+
if (form) {
|
|
1368
|
+
const checkboxes = form.querySelectorAll('input[type="checkbox"]')
|
|
1369
|
+
checkboxes.forEach(checkbox => {
|
|
1370
|
+
const defaultVisible = checkbox.dataset.defaultVisible === "true"
|
|
1371
|
+
checkbox.checked = defaultVisible
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1374
|
+
// Reset pin buttons to defaults
|
|
1375
|
+
const columnItems = form.querySelectorAll('.column-item')
|
|
1376
|
+
columnItems.forEach(item => {
|
|
1377
|
+
const defaultPinned = item.dataset.defaultPinned
|
|
1378
|
+
this.updatePinButtons(item, defaultPinned === 'left' ? 'left' : defaultPinned === 'right' ? 'right' : null)
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
// Reset column order in the drawer
|
|
1382
|
+
const defaultSchema = JSON.parse(form.dataset.columnsSchema || '[]')
|
|
1383
|
+
const schemaNames = defaultSchema.map(item => typeof item === 'string' ? item : item.name)
|
|
1384
|
+
this.applyColumnOrderToList(tableName, schemaNames)
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Apply defaults to table
|
|
1388
|
+
this.applyColumnVisibility(tableName)
|
|
1389
|
+
|
|
1390
|
+
// Apply default column order to table
|
|
1391
|
+
const list = Array.from(this.columnListTargets).find(l => l.dataset.tableName === tableName)
|
|
1392
|
+
if (list) {
|
|
1393
|
+
const defaultSchema = JSON.parse(list.dataset.columnsSchema || '[]')
|
|
1394
|
+
const schemaNames = defaultSchema.map(item => typeof item === 'string' ? item : item.name)
|
|
1395
|
+
this.applyColumnOrder(tableName, schemaNames)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Remove sticky positioning
|
|
1399
|
+
this.applyColumnPinningToTable(tableName)
|
|
1400
|
+
|
|
1401
|
+
// Update badge
|
|
1402
|
+
this.updateColumnBadge(tableName)
|
|
1403
|
+
|
|
1404
|
+
// Dispatch event for export link update
|
|
1405
|
+
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
1406
|
+
detail: { tableName }
|
|
1407
|
+
}))
|
|
1408
|
+
|
|
1409
|
+
// Show feedback (optional)
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
}
|