infotable 1.4.1 → 1.5.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 +4 -4
- data/README.md +35 -1
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +59 -364
- data/app/assets/javascripts/infotable/controllers/table_controller.js +71 -98
- data/app/assets/javascripts/infotable/vendor/sortable.esm.js +3363 -0
- data/app/controllers/infotable/tables_controller.rb +15 -4
- data/app/helpers/infotable/tables_helper.rb +0 -23
- data/app/views/infotable/tables/_column_visibility.html.erb +4 -4
- data/app/views/infotable/tables/_end_indicator.html.erb +8 -0
- data/app/views/infotable/tables/_next_page_frame.html.erb +1 -9
- data/app/views/infotable/tables/_table_rows.html.erb +9 -2
- data/app/views/infotable/tables/show.html.erb +2 -2
- data/config/importmap.rb +2 -2
- data/lib/generators/infotable/install/templates/initializer.rb +5 -3
- data/lib/infotable/base.rb +61 -4
- data/lib/infotable/configuration.rb +1 -1
- data/lib/infotable/engine.rb +1 -8
- data/lib/infotable/filter_processor.rb +10 -1
- data/lib/infotable/formatters/boolean_formatter.rb +4 -4
- data/lib/infotable/table_resolver.rb +9 -0
- data/lib/infotable/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1a84d197b721ed57ad46be7d91a870d0f7d0d3c6a7bc471e4e1c306aefa9e1f
|
|
4
|
+
data.tar.gz: 19c3fd871c6e5bee6908266fe877774786b8b2e7d7fa289117b05e65684016e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8bcbd636497943daa6fb58cec7a8550f4258c14a7a65a771d10c1f4e2776bed51fe6b8ad2137126131b353fedd91076e6d017697b0f405b3694d257bd436098
|
|
7
|
+
data.tar.gz: c6a4f20ebe7642c1d8d686be6e8fee8226f1076ee93b2daa5de79f0b7a630f17d264eb64cc067d164fb1de5033f1c08c059ca1e46a7b9d5b7666298f7d5b5a47
|
data/README.md
CHANGED
|
@@ -129,6 +129,15 @@ Infotable.configure do |config|
|
|
|
129
129
|
end
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
### Security
|
|
133
|
+
|
|
134
|
+
Tables are served by the engine at `/infotable/tables/:name/{data,export,ids}`. A table without an
|
|
135
|
+
`authorize { |controller| ... }` block is readable (and exportable as CSV) by **everyone who can reach the
|
|
136
|
+
route**, unless the engine controllers inherit your app's authentication. The installer therefore sets
|
|
137
|
+
`config.parent_controller = "ApplicationController"`; keep it (or point it at a controller with your
|
|
138
|
+
`before_action :authenticate_user!`) and add `authorize` to tables with restricted data. The engine's own
|
|
139
|
+
default (`ActionController::Base`) applies no authentication at all.
|
|
140
|
+
|
|
132
141
|
### Available Global Options
|
|
133
142
|
|
|
134
143
|
| Option | Type | Default | Description |
|
|
@@ -137,7 +146,8 @@ end
|
|
|
137
146
|
| `per_page_options` | Array | `[10, 25, 50, 100]` | Available rows per page options |
|
|
138
147
|
| `drawer_position` | Symbol | `:right` | Position of filter/column drawer (`:left` or `:right`) |
|
|
139
148
|
| `drawer_width` | String | `"26rem"` | Width of the drawer (`px`, `rem`, `em`, `vw` or `%`; anything else falls back to the default) |
|
|
140
|
-
| `parent_controller` | String | `"ActionController::Base"` | Controller the engine's controllers inherit from. Set it to `"ApplicationController"` to reuse your authentication |
|
|
149
|
+
| `parent_controller` | String | `"ActionController::Base"` (the installer sets `"ApplicationController"`) | Controller the engine's controllers inherit from. Set it to `"ApplicationController"` to reuse your authentication |
|
|
150
|
+
| `max_select_all` | Integer, nil | `4000` | "Select all N matching" collects at most this many ids (each is sent as `ids[]`; Rack accepts 4096 parameters per request) |
|
|
141
151
|
| `table_min_height` | String | `"400px"` | Minimum table height |
|
|
142
152
|
| `table_max_height` | String | `"600px"` | Maximum table height (enables scrolling) |
|
|
143
153
|
| `autofilter` | Boolean | `true` | Auto-apply filters on change (debounced) vs manual apply |
|
|
@@ -448,6 +458,30 @@ Features:
|
|
|
448
458
|
- Cmd/Ctrl+Click to open in new tab
|
|
449
459
|
- Ignores clicks on buttons, links, and inputs
|
|
450
460
|
|
|
461
|
+
#### Row click, classes and data attributes
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
# Style rows by their data (write the Tailwind classes out in full so they are generated)
|
|
465
|
+
row_class ->(order) { "bg-red-50" if order.overdue? }
|
|
466
|
+
|
|
467
|
+
# data-* attributes on the <tr>, e.g. for your own Stimulus controller
|
|
468
|
+
row_data ->(post) { { post_id: post.id, status: post.status } } # data-post-id, data-status
|
|
469
|
+
|
|
470
|
+
# Rows fire a cancelable `infotable:row-click` event (needs no row_link)
|
|
471
|
+
row_click
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Every row also gets `id="<dom_id>"` (`order_42`), so Turbo Streams can target it (`turbo_stream.replace dom_id(order)`, `broadcasts_to`). Rows without a record id are left without one.
|
|
475
|
+
|
|
476
|
+
Clicking a row (not a link, button or input inside it) first dispatches `infotable:row-click` on the `<tr>`, with `detail: { id, url, data, originalEvent }`. If nothing calls `preventDefault()` and the table has a `row_link`, the click then falls back to following it, so the two compose:
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
document.addEventListener("infotable:row-click", event => {
|
|
480
|
+
if (event.detail.data.status === "locked") event.preventDefault() // no navigation for locked rows
|
|
481
|
+
else openPreview(event.detail.data.postId) // ...or handle it yourself
|
|
482
|
+
})
|
|
483
|
+
```
|
|
484
|
+
|
|
451
485
|
#### Row Size
|
|
452
486
|
|
|
453
487
|
Control table row padding and text size with three options:
|
|
@@ -267,7 +267,7 @@ export default class extends Controller {
|
|
|
267
267
|
// Keep search and sort from the current frame URL
|
|
268
268
|
if (tableFrame?.src) {
|
|
269
269
|
const current = new URL(tableFrame.src, window.location.href).searchParams
|
|
270
|
-
for (const key of ["search", "sort", "sort_column", "sort_direction"]) {
|
|
270
|
+
for (const key of ["search", "per_page", "sort", "sort_column", "sort_direction"]) {
|
|
271
271
|
if (current.has(key) && !params.has(key)) params.set(key, current.get(key))
|
|
272
272
|
}
|
|
273
273
|
}
|
|
@@ -568,7 +568,7 @@ export default class extends Controller {
|
|
|
568
568
|
this.saveColumnVisibility(tableName, columnName, isVisible)
|
|
569
569
|
|
|
570
570
|
// Apply visibility to table
|
|
571
|
-
this.
|
|
571
|
+
this.applyToTable(tableName)
|
|
572
572
|
|
|
573
573
|
// Update badge
|
|
574
574
|
this.updateColumnBadge(tableName)
|
|
@@ -591,7 +591,7 @@ export default class extends Controller {
|
|
|
591
591
|
const columnName = checkbox.dataset.columnName
|
|
592
592
|
this.saveColumnVisibility(tableName, columnName, true)
|
|
593
593
|
})
|
|
594
|
-
this.
|
|
594
|
+
this.applyToTable(tableName)
|
|
595
595
|
this.updateColumnBadge(tableName)
|
|
596
596
|
|
|
597
597
|
// Dispatch event to update export link
|
|
@@ -613,7 +613,7 @@ export default class extends Controller {
|
|
|
613
613
|
const columnName = checkbox.dataset.columnName
|
|
614
614
|
this.saveColumnVisibility(tableName, columnName, false)
|
|
615
615
|
})
|
|
616
|
-
this.
|
|
616
|
+
this.applyToTable(tableName)
|
|
617
617
|
this.updateColumnBadge(tableName)
|
|
618
618
|
|
|
619
619
|
// Dispatch event to update export link
|
|
@@ -638,7 +638,7 @@ export default class extends Controller {
|
|
|
638
638
|
}
|
|
639
639
|
|
|
640
640
|
visibility[columnName] = isVisible
|
|
641
|
-
|
|
641
|
+
this.storeSet(key, JSON.stringify(visibility))
|
|
642
642
|
}
|
|
643
643
|
|
|
644
644
|
// Sync checkboxes with localStorage values or defaults
|
|
@@ -722,67 +722,12 @@ export default class extends Controller {
|
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Try to find table in the wrapper or in turbo-frame
|
|
734
|
-
let table = tableWrapper.querySelector("table")
|
|
735
|
-
if (!table) {
|
|
736
|
-
// Table might be inside turbo-frame
|
|
737
|
-
const turboFrame = document.getElementById(`${tableName}_content`)
|
|
738
|
-
if (turboFrame) {
|
|
739
|
-
table = turboFrame.querySelector("table")
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if (!table) {
|
|
744
|
-
console.warn(`Table not found for: ${tableName}`)
|
|
745
|
-
return
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Find all table headers and cells
|
|
749
|
-
const headers = table.querySelectorAll("thead th[data-column-name]")
|
|
750
|
-
const rows = table.querySelectorAll("tbody tr")
|
|
751
|
-
|
|
752
|
-
if (headers.length === 0) {
|
|
753
|
-
console.warn(`No column headers found for table: ${tableName}`)
|
|
754
|
-
return
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Get saved preferences from localStorage
|
|
758
|
-
const visibility = this.getColumnVisibility(tableName)
|
|
759
|
-
|
|
760
|
-
headers.forEach((header) => {
|
|
761
|
-
const columnName = header.dataset.columnName
|
|
762
|
-
const defaultVisible = header.dataset.defaultVisible === "true"
|
|
763
|
-
|
|
764
|
-
// Determine if column should be visible
|
|
765
|
-
let shouldShow
|
|
766
|
-
if (visibility && visibility.hasOwnProperty(columnName)) {
|
|
767
|
-
// Use saved preference
|
|
768
|
-
shouldShow = visibility[columnName]
|
|
769
|
-
} else {
|
|
770
|
-
// Use default visibility
|
|
771
|
-
shouldShow = defaultVisible
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Apply visibility
|
|
775
|
-
header.style.display = shouldShow ? "" : "none"
|
|
776
|
-
|
|
777
|
-
// Hide/show corresponding cells in all rows
|
|
778
|
-
rows.forEach(row => {
|
|
779
|
-
const cells = row.querySelectorAll("td[data-column-name]")
|
|
780
|
-
const cell = Array.from(cells).find(c => c.dataset.columnName === columnName)
|
|
781
|
-
if (cell) {
|
|
782
|
-
cell.style.display = shouldShow ? "" : "none"
|
|
783
|
-
}
|
|
784
|
-
})
|
|
785
|
-
})
|
|
725
|
+
// The table controller applies order, visibility and pinning (it owns the table DOM); an explicit
|
|
726
|
+
// `order` replaces the stored one (used right after a drag, a pin or a reset)
|
|
727
|
+
applyToTable(tableName, order = null) {
|
|
728
|
+
document.dispatchEvent(new CustomEvent("infotable:apply-columns", {
|
|
729
|
+
detail: { tableName, order }
|
|
730
|
+
}))
|
|
786
731
|
}
|
|
787
732
|
|
|
788
733
|
// Update column badge count for a specific table
|
|
@@ -876,13 +821,10 @@ export default class extends Controller {
|
|
|
876
821
|
const columnItems = list.querySelectorAll('.column-item')
|
|
877
822
|
const newOrder = Array.from(columnItems).map(item => item.dataset.columnName)
|
|
878
823
|
|
|
879
|
-
//
|
|
880
|
-
this.
|
|
824
|
+
// Order and sticky positioning, applied to the table immediately
|
|
825
|
+
this.applyToTable(tableName, newOrder)
|
|
881
826
|
}
|
|
882
827
|
|
|
883
|
-
// Apply sticky positioning to table immediately
|
|
884
|
-
this.applyColumnPinningToTable(tableName)
|
|
885
|
-
|
|
886
828
|
// Dispatch event for export link update
|
|
887
829
|
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
888
830
|
detail: { tableName }
|
|
@@ -903,40 +845,53 @@ export default class extends Controller {
|
|
|
903
845
|
console.warn("Failed to parse column pinned from localStorage", e)
|
|
904
846
|
}
|
|
905
847
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
} else {
|
|
909
|
-
delete pinnedData[columnName]
|
|
910
|
-
}
|
|
848
|
+
// An unpin is stored as null (not removed): a column pinned by default must stay unpinned
|
|
849
|
+
pinnedData[columnName] = pinned || null
|
|
911
850
|
|
|
912
|
-
|
|
851
|
+
this.storeSet(key, JSON.stringify(pinnedData))
|
|
913
852
|
}
|
|
914
853
|
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
const key = `infotable_column_pinned_${tableName}`
|
|
854
|
+
// localStorage is missing or full in some browsers (private mode): the drawer then works for this page only
|
|
855
|
+
storeSet(key, value) {
|
|
918
856
|
try {
|
|
919
|
-
|
|
920
|
-
if (stored) {
|
|
921
|
-
const pinnedData = JSON.parse(stored)
|
|
922
|
-
return pinnedData[columnName] || null
|
|
923
|
-
}
|
|
857
|
+
localStorage.setItem(key, value)
|
|
924
858
|
} catch (e) {
|
|
925
|
-
console.warn("Failed to
|
|
859
|
+
console.warn("Failed to write to localStorage", e)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
storeRemove(key) {
|
|
864
|
+
try {
|
|
865
|
+
localStorage.removeItem(key)
|
|
866
|
+
} catch (e) {
|
|
867
|
+
console.warn("Failed to write to localStorage", e)
|
|
926
868
|
}
|
|
927
|
-
return null
|
|
928
869
|
}
|
|
929
870
|
|
|
930
|
-
// Get
|
|
871
|
+
// Get pinned state from localStorage
|
|
872
|
+
getColumnPinned(tableName, columnName) {
|
|
873
|
+
return this.getAllColumnsPinned(tableName)[columnName] || null
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Pinned columns: what the user chose (localStorage) over the table's `pinned:` defaults.
|
|
877
|
+
// A column the user unpinned is stored as null and stays unpinned.
|
|
931
878
|
getAllColumnsPinned(tableName) {
|
|
932
879
|
const key = `infotable_column_pinned_${tableName}`
|
|
880
|
+
let stored = {}
|
|
933
881
|
try {
|
|
934
|
-
const
|
|
935
|
-
|
|
882
|
+
const raw = localStorage.getItem(key)
|
|
883
|
+
if (raw) stored = JSON.parse(raw)
|
|
936
884
|
} catch (e) {
|
|
937
885
|
console.warn("Failed to parse column pinned from localStorage", e)
|
|
938
|
-
return {}
|
|
939
886
|
}
|
|
887
|
+
|
|
888
|
+
const pinned = {}
|
|
889
|
+
const form = document.getElementById(`column-visibility-form-${tableName}`)
|
|
890
|
+
form?.querySelectorAll('.column-item').forEach(item => {
|
|
891
|
+
const defaultPinned = item.dataset.defaultPinned
|
|
892
|
+
if (defaultPinned) pinned[item.dataset.columnName] = defaultPinned
|
|
893
|
+
})
|
|
894
|
+
return { ...pinned, ...stored }
|
|
940
895
|
}
|
|
941
896
|
|
|
942
897
|
// Update pin button styling
|
|
@@ -1001,202 +956,6 @@ export default class extends Controller {
|
|
|
1001
956
|
this.saveColumnOrder(tableName, newOrder)
|
|
1002
957
|
}
|
|
1003
958
|
|
|
1004
|
-
// Apply pinning to table (add sticky positioning)
|
|
1005
|
-
applyColumnPinningToTable(tableName) {
|
|
1006
|
-
const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
|
|
1007
|
-
if (!tableWrapper) return
|
|
1008
|
-
|
|
1009
|
-
let table = tableWrapper.querySelector("table")
|
|
1010
|
-
if (!table) {
|
|
1011
|
-
const turboFrame = document.getElementById(`${tableName}_content`)
|
|
1012
|
-
if (turboFrame) {
|
|
1013
|
-
table = turboFrame.querySelector("table")
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (!table) return
|
|
1018
|
-
|
|
1019
|
-
const pinnedData = this.getAllColumnsPinned(tableName)
|
|
1020
|
-
const headers = Array.from(table.querySelectorAll("thead th[data-column-name]"))
|
|
1021
|
-
const rows = table.querySelectorAll("tbody tr")
|
|
1022
|
-
|
|
1023
|
-
// Calculate positions for pinned columns
|
|
1024
|
-
let leftOffset = 0
|
|
1025
|
-
let rightOffset = 0
|
|
1026
|
-
|
|
1027
|
-
// First pass: reset all columns
|
|
1028
|
-
headers.forEach(header => {
|
|
1029
|
-
header.style.position = ''
|
|
1030
|
-
header.style.left = ''
|
|
1031
|
-
header.style.right = ''
|
|
1032
|
-
header.style.zIndex = ''
|
|
1033
|
-
header.style.removeProperty('background-color')
|
|
1034
|
-
header.style.removeProperty('background-image')
|
|
1035
|
-
header.style.removeProperty('opacity')
|
|
1036
|
-
header.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-gray-50', 'dark:!bg-gray-800', "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]")
|
|
1037
|
-
})
|
|
1038
|
-
|
|
1039
|
-
rows.forEach(row => {
|
|
1040
|
-
const cells = row.querySelectorAll("td[data-column-name]")
|
|
1041
|
-
cells.forEach(cell => {
|
|
1042
|
-
cell.style.position = ''
|
|
1043
|
-
cell.style.left = ''
|
|
1044
|
-
cell.style.right = ''
|
|
1045
|
-
cell.style.zIndex = ''
|
|
1046
|
-
cell.style.removeProperty('background-color')
|
|
1047
|
-
cell.style.removeProperty('background-image')
|
|
1048
|
-
cell.style.removeProperty('opacity')
|
|
1049
|
-
cell.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900', "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]")
|
|
1050
|
-
})
|
|
1051
|
-
})
|
|
1052
|
-
|
|
1053
|
-
// Find last left-pinned and first right-pinned columns
|
|
1054
|
-
let lastLeftPinned = null
|
|
1055
|
-
let firstRightPinned = null
|
|
1056
|
-
|
|
1057
|
-
headers.forEach(header => {
|
|
1058
|
-
const columnName = header.dataset.columnName
|
|
1059
|
-
const pinned = pinnedData[columnName]
|
|
1060
|
-
if (pinned === 'left') {
|
|
1061
|
-
lastLeftPinned = columnName
|
|
1062
|
-
}
|
|
1063
|
-
if (pinned === 'right' && !firstRightPinned) {
|
|
1064
|
-
firstRightPinned = columnName
|
|
1065
|
-
}
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
// Apply sticky positioning for pinned columns
|
|
1069
|
-
headers.forEach(header => {
|
|
1070
|
-
const columnName = header.dataset.columnName
|
|
1071
|
-
const pinned = pinnedData[columnName]
|
|
1072
|
-
|
|
1073
|
-
if (pinned === 'left') {
|
|
1074
|
-
// Get existing inline styles
|
|
1075
|
-
const existingStyles = header.getAttribute('style') || ''
|
|
1076
|
-
|
|
1077
|
-
// Build comprehensive style string with all !important flags
|
|
1078
|
-
const newStyles = `
|
|
1079
|
-
position: sticky !important;
|
|
1080
|
-
left: ${leftOffset}px !important;
|
|
1081
|
-
z-index: 30 !important;
|
|
1082
|
-
background: #f9fafb !important;
|
|
1083
|
-
background-color: #f9fafb !important;
|
|
1084
|
-
background-image: none !important;
|
|
1085
|
-
background-position: initial !important;
|
|
1086
|
-
background-size: initial !important;
|
|
1087
|
-
background-repeat: initial !important;
|
|
1088
|
-
background-attachment: initial !important;
|
|
1089
|
-
opacity: 1 !important;
|
|
1090
|
-
${existingStyles}
|
|
1091
|
-
`.replace(/\s+/g, ' ').trim()
|
|
1092
|
-
|
|
1093
|
-
// Set all styles at once via setAttribute
|
|
1094
|
-
header.setAttribute('style', newStyles)
|
|
1095
|
-
header.classList.add('infotable-pinned-left', '!bg-gray-50', 'dark:!bg-gray-800')
|
|
1096
|
-
|
|
1097
|
-
// Mark last left-pinned column
|
|
1098
|
-
if (columnName === lastLeftPinned) {
|
|
1099
|
-
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]")
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Apply to cells in all rows
|
|
1103
|
-
rows.forEach(row => {
|
|
1104
|
-
const cell = row.querySelector(`td[data-column-name="${columnName}"]`)
|
|
1105
|
-
if (cell) {
|
|
1106
|
-
// Get existing inline styles
|
|
1107
|
-
const existingStyles = cell.getAttribute('style') || ''
|
|
1108
|
-
|
|
1109
|
-
// Build comprehensive style string
|
|
1110
|
-
const newStyles = `
|
|
1111
|
-
position: sticky !important;
|
|
1112
|
-
left: ${leftOffset}px !important;
|
|
1113
|
-
z-index: 15 !important;
|
|
1114
|
-
background: #ffffff !important;
|
|
1115
|
-
background-color: #ffffff !important;
|
|
1116
|
-
background-image: none !important;
|
|
1117
|
-
background-position: initial !important;
|
|
1118
|
-
background-size: initial !important;
|
|
1119
|
-
background-repeat: initial !important;
|
|
1120
|
-
background-attachment: initial !important;
|
|
1121
|
-
opacity: 1 !important;
|
|
1122
|
-
${existingStyles}
|
|
1123
|
-
`.replace(/\s+/g, ' ').trim()
|
|
1124
|
-
|
|
1125
|
-
cell.setAttribute('style', newStyles)
|
|
1126
|
-
cell.classList.add('infotable-pinned-left', '!bg-white', 'dark:!bg-gray-900')
|
|
1127
|
-
|
|
1128
|
-
if (columnName === lastLeftPinned) {
|
|
1129
|
-
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]")
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
})
|
|
1133
|
-
|
|
1134
|
-
leftOffset += header.offsetWidth
|
|
1135
|
-
} else if (pinned === 'right') {
|
|
1136
|
-
// Get existing inline styles
|
|
1137
|
-
const existingStyles = header.getAttribute('style') || ''
|
|
1138
|
-
|
|
1139
|
-
// Build comprehensive style string
|
|
1140
|
-
const newStyles = `
|
|
1141
|
-
position: sticky !important;
|
|
1142
|
-
right: ${rightOffset}px !important;
|
|
1143
|
-
z-index: 30 !important;
|
|
1144
|
-
background: #f9fafb !important;
|
|
1145
|
-
background-color: #f9fafb !important;
|
|
1146
|
-
background-image: none !important;
|
|
1147
|
-
background-position: initial !important;
|
|
1148
|
-
background-size: initial !important;
|
|
1149
|
-
background-repeat: initial !important;
|
|
1150
|
-
background-attachment: initial !important;
|
|
1151
|
-
opacity: 1 !important;
|
|
1152
|
-
${existingStyles}
|
|
1153
|
-
`.replace(/\s+/g, ' ').trim()
|
|
1154
|
-
|
|
1155
|
-
header.setAttribute('style', newStyles)
|
|
1156
|
-
header.classList.add('infotable-pinned-right', '!bg-gray-50', 'dark:!bg-gray-800')
|
|
1157
|
-
|
|
1158
|
-
// Mark first right-pinned column
|
|
1159
|
-
if (columnName === firstRightPinned) {
|
|
1160
|
-
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]")
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// Apply to cells in all rows
|
|
1164
|
-
rows.forEach(row => {
|
|
1165
|
-
const cell = row.querySelector(`td[data-column-name="${columnName}"]`)
|
|
1166
|
-
if (cell) {
|
|
1167
|
-
// Get existing inline styles
|
|
1168
|
-
const existingStyles = cell.getAttribute('style') || ''
|
|
1169
|
-
|
|
1170
|
-
// Build comprehensive style string
|
|
1171
|
-
const newStyles = `
|
|
1172
|
-
position: sticky !important;
|
|
1173
|
-
right: ${rightOffset}px !important;
|
|
1174
|
-
z-index: 15 !important;
|
|
1175
|
-
background: #ffffff !important;
|
|
1176
|
-
background-color: #ffffff !important;
|
|
1177
|
-
background-image: none !important;
|
|
1178
|
-
background-position: initial !important;
|
|
1179
|
-
background-size: initial !important;
|
|
1180
|
-
background-repeat: initial !important;
|
|
1181
|
-
background-attachment: initial !important;
|
|
1182
|
-
opacity: 1 !important;
|
|
1183
|
-
${existingStyles}
|
|
1184
|
-
`.replace(/\s+/g, ' ').trim()
|
|
1185
|
-
|
|
1186
|
-
cell.setAttribute('style', newStyles)
|
|
1187
|
-
cell.classList.add('infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900')
|
|
1188
|
-
|
|
1189
|
-
if (columnName === firstRightPinned) {
|
|
1190
|
-
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]")
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
})
|
|
1194
|
-
|
|
1195
|
-
rightOffset += header.offsetWidth
|
|
1196
|
-
}
|
|
1197
|
-
})
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
959
|
// ========== Column Ordering with Drag and Drop ==========
|
|
1201
960
|
|
|
1202
961
|
// Initialize Sortable for drag and drop column ordering
|
|
@@ -1237,7 +996,9 @@ export default class extends Controller {
|
|
|
1237
996
|
|
|
1238
997
|
// Unpin all columns when manually reordering
|
|
1239
998
|
const pinnedKey = `infotable_column_pinned_${tableName}`
|
|
1240
|
-
|
|
999
|
+
const unpinned = {}
|
|
1000
|
+
columnItems.forEach(item => { unpinned[item.dataset.columnName] = null })
|
|
1001
|
+
this.storeSet(pinnedKey, JSON.stringify(unpinned))
|
|
1241
1002
|
|
|
1242
1003
|
// Update pin button styling for all items
|
|
1243
1004
|
columnItems.forEach(item => {
|
|
@@ -1247,11 +1008,8 @@ export default class extends Controller {
|
|
|
1247
1008
|
// Save to localStorage
|
|
1248
1009
|
this.saveColumnOrder(tableName, newOrder)
|
|
1249
1010
|
|
|
1250
|
-
// Apply order to table
|
|
1251
|
-
this.
|
|
1252
|
-
|
|
1253
|
-
// Remove sticky positioning since all are unpinned
|
|
1254
|
-
this.applyColumnPinningToTable(tableName)
|
|
1011
|
+
// Apply order to the table; nothing is pinned any more
|
|
1012
|
+
this.applyToTable(tableName, newOrder)
|
|
1255
1013
|
|
|
1256
1014
|
// Dispatch event for export link update
|
|
1257
1015
|
document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
|
|
@@ -1262,7 +1020,7 @@ export default class extends Controller {
|
|
|
1262
1020
|
// Save column order to localStorage
|
|
1263
1021
|
saveColumnOrder(tableName, order) {
|
|
1264
1022
|
const key = `infotable_column_order_${tableName}`
|
|
1265
|
-
|
|
1023
|
+
this.storeSet(key, JSON.stringify(order))
|
|
1266
1024
|
}
|
|
1267
1025
|
|
|
1268
1026
|
// Load column order from localStorage
|
|
@@ -1298,7 +1056,7 @@ export default class extends Controller {
|
|
|
1298
1056
|
|
|
1299
1057
|
// Valid schema, apply the order
|
|
1300
1058
|
this.applyColumnOrderToList(tableName, storedOrder)
|
|
1301
|
-
this.
|
|
1059
|
+
this.applyToTable(tableName, storedOrder)
|
|
1302
1060
|
}
|
|
1303
1061
|
|
|
1304
1062
|
// Check if stored order is valid against default schema
|
|
@@ -1352,64 +1110,10 @@ export default class extends Controller {
|
|
|
1352
1110
|
})
|
|
1353
1111
|
}
|
|
1354
1112
|
|
|
1355
|
-
// Apply column order to the table
|
|
1356
|
-
applyColumnOrder(tableName, order) {
|
|
1357
|
-
const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
|
|
1358
|
-
if (!tableWrapper) return
|
|
1359
|
-
|
|
1360
|
-
// Try to find table in the wrapper or in turbo-frame
|
|
1361
|
-
let table = tableWrapper.querySelector("table")
|
|
1362
|
-
if (!table) {
|
|
1363
|
-
const turboFrame = document.getElementById(`${tableName}_content`)
|
|
1364
|
-
if (turboFrame) {
|
|
1365
|
-
table = turboFrame.querySelector("table")
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
if (!table) return
|
|
1370
|
-
|
|
1371
|
-
// Get headers and create a map
|
|
1372
|
-
const headerRow = table.querySelector("thead tr")
|
|
1373
|
-
if (!headerRow) return
|
|
1374
|
-
|
|
1375
|
-
const headers = Array.from(headerRow.querySelectorAll("th[data-column-name]"))
|
|
1376
|
-
const headerMap = new Map()
|
|
1377
|
-
|
|
1378
|
-
headers.forEach((header, index) => {
|
|
1379
|
-
headerMap.set(header.dataset.columnName, { element: header, originalIndex: index })
|
|
1380
|
-
})
|
|
1381
|
-
|
|
1382
|
-
// Reorder headers according to order array
|
|
1383
|
-
order.forEach(columnName => {
|
|
1384
|
-
const headerInfo = headerMap.get(columnName)
|
|
1385
|
-
if (headerInfo) {
|
|
1386
|
-
headerRow.appendChild(headerInfo.element)
|
|
1387
|
-
}
|
|
1388
|
-
})
|
|
1389
|
-
|
|
1390
|
-
// Reorder cells in each row
|
|
1391
|
-
const rows = table.querySelectorAll("tbody tr")
|
|
1392
|
-
rows.forEach(row => {
|
|
1393
|
-
const cells = Array.from(row.querySelectorAll("td[data-column-name]"))
|
|
1394
|
-
const cellMap = new Map()
|
|
1395
|
-
|
|
1396
|
-
cells.forEach(cell => {
|
|
1397
|
-
cellMap.set(cell.dataset.columnName, cell)
|
|
1398
|
-
})
|
|
1399
|
-
|
|
1400
|
-
order.forEach(columnName => {
|
|
1401
|
-
const cell = cellMap.get(columnName)
|
|
1402
|
-
if (cell) {
|
|
1403
|
-
row.appendChild(cell)
|
|
1404
|
-
}
|
|
1405
|
-
})
|
|
1406
|
-
})
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
1113
|
// Clear column order from localStorage
|
|
1410
1114
|
clearColumnOrder(tableName) {
|
|
1411
1115
|
const key = `infotable_column_order_${tableName}`
|
|
1412
|
-
|
|
1116
|
+
this.storeRemove(key)
|
|
1413
1117
|
}
|
|
1414
1118
|
|
|
1415
1119
|
// Reset to defaults - clear all custom settings
|
|
@@ -1421,14 +1125,14 @@ export default class extends Controller {
|
|
|
1421
1125
|
|
|
1422
1126
|
// Clear column visibility
|
|
1423
1127
|
const visibilityKey = `infotable_columns_${tableName}`
|
|
1424
|
-
|
|
1128
|
+
this.storeRemove(visibilityKey)
|
|
1425
1129
|
|
|
1426
1130
|
// Clear column order
|
|
1427
1131
|
this.clearColumnOrder(tableName)
|
|
1428
1132
|
|
|
1429
1133
|
// Clear column pinning
|
|
1430
1134
|
const pinnedKey = `infotable_column_pinned_${tableName}`
|
|
1431
|
-
|
|
1135
|
+
this.storeRemove(pinnedKey)
|
|
1432
1136
|
|
|
1433
1137
|
// Reset checkboxes to defaults
|
|
1434
1138
|
const form = document.getElementById(`column-visibility-form-${tableName}`)
|
|
@@ -1452,19 +1156,10 @@ export default class extends Controller {
|
|
|
1452
1156
|
this.applyColumnOrderToList(tableName, schemaNames)
|
|
1453
1157
|
}
|
|
1454
1158
|
|
|
1455
|
-
// Apply defaults to table
|
|
1456
|
-
this.applyColumnVisibility(tableName)
|
|
1457
|
-
|
|
1458
|
-
// Apply default column order to table
|
|
1159
|
+
// Apply the defaults (visibility, default order, default pinning) to the table
|
|
1459
1160
|
const list = Array.from(this.columnListTargets).find(l => l.dataset.tableName === tableName)
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
const schemaNames = defaultSchema.map(item => typeof item === 'string' ? item : item.name)
|
|
1463
|
-
this.applyColumnOrder(tableName, schemaNames)
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Remove sticky positioning
|
|
1467
|
-
this.applyColumnPinningToTable(tableName)
|
|
1161
|
+
const defaultSchema = JSON.parse(list?.dataset.columnsSchema || '[]')
|
|
1162
|
+
this.applyToTable(tableName, list ? defaultSchema.map(item => typeof item === 'string' ? item : item.name) : null)
|
|
1468
1163
|
|
|
1469
1164
|
// Update badge
|
|
1470
1165
|
this.updateColumnBadge(tableName)
|