infotable 1.4.1 → 1.6.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 +179 -11
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +74 -378
- data/app/assets/javascripts/infotable/controllers/table_controller.js +112 -120
- data/app/assets/javascripts/infotable/vendor/sortable.esm.js +3363 -0
- data/app/controllers/infotable/tables_controller.rb +16 -5
- data/app/helpers/infotable/tables_helper.rb +10 -48
- 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/_filter_drawer.html.erb +6 -6
- data/app/views/infotable/tables/_next_page_frame.html.erb +1 -9
- data/app/views/infotable/tables/_pagination.html.erb +4 -7
- data/app/views/infotable/tables/_table.html.erb +1 -1
- data/app/views/infotable/tables/_table_rows.html.erb +9 -2
- data/app/views/infotable/tables/filters/_boolean.html.erb +1 -1
- data/app/views/infotable/tables/filters/_range.html.erb +1 -1
- data/app/views/infotable/tables/filters/_select.html.erb +2 -2
- data/app/views/infotable/tables/filters/_text.html.erb +2 -2
- data/app/views/infotable/tables/show.html.erb +2 -2
- data/config/importmap.rb +2 -2
- data/config/locales/de.yml +126 -0
- data/config/locales/el.yml +126 -0
- data/config/locales/en.yml +18 -4
- data/config/locales/es.yml +126 -0
- data/config/locales/fr.yml +126 -0
- data/config/locales/it.yml +126 -0
- data/config/locales/pl.yml +18 -4
- data/exe/infotable-mcp +13 -0
- data/lib/generators/infotable/install/templates/initializer.rb +5 -3
- data/lib/generators/infotable/table/table_generator.rb +23 -1
- data/lib/generators/infotable/table/templates/table.rb.tt +8 -1
- data/lib/infotable/base.rb +62 -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/formatters/currency_formatter.rb +4 -12
- data/lib/infotable/formatters/date_formatter.rb +3 -9
- data/lib/infotable/formatters/duration_formatter.rb +51 -0
- data/lib/infotable/formatters/filesize_formatter.rb +14 -0
- data/lib/infotable/formatters/image_formatter.rb +31 -0
- data/lib/infotable/formatters/number_formatter.rb +46 -0
- data/lib/infotable/formatters/tags_formatter.rb +44 -0
- data/lib/infotable/formatters/truncate_formatter.rb +19 -0
- data/lib/infotable/mcp/introspection.rb +94 -0
- data/lib/infotable/mcp/server.rb +26 -0
- data/lib/infotable/mcp/table_checker.rb +80 -0
- data/lib/infotable/mcp/table_writer.rb +66 -0
- data/lib/infotable/mcp/tools.rb +122 -0
- data/lib/infotable/mcp.rb +30 -0
- data/lib/infotable/table_resolver.rb +9 -0
- data/lib/infotable/version.rb +1 -1
- data/lib/infotable.rb +22 -0
- metadata +53 -4
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
+
// Innermost pinned column on each side: an inset 1px line. A border would stay behind when the
|
|
4
|
+
// cell sticks (collapsed borders belong to the table), a box-shadow moves with the cell
|
|
5
|
+
const LEFT_EDGE = 'inset -1px 0 0 0 rgba(128, 128, 128, 0.45)'
|
|
6
|
+
const RIGHT_EDGE = 'inset 1px 0 0 0 rgba(128, 128, 128, 0.45)'
|
|
7
|
+
|
|
3
8
|
// Main table controller that coordinates table interactions
|
|
4
9
|
export default class extends Controller {
|
|
5
10
|
static values = {
|
|
@@ -48,6 +53,8 @@ export default class extends Controller {
|
|
|
48
53
|
|
|
49
54
|
// Listen for column visibility changes to update export link
|
|
50
55
|
this.boundUpdateExportLink = this.updateExportLink.bind(this)
|
|
56
|
+
this.boundApplyColumns = this.applyColumnsFromDrawer.bind(this)
|
|
57
|
+
document.addEventListener("infotable:apply-columns", this.boundApplyColumns)
|
|
51
58
|
document.addEventListener("infotable:columns-changed", this.boundUpdateExportLink)
|
|
52
59
|
document.addEventListener("infotable:filters-changed", this.boundUpdateExportLink)
|
|
53
60
|
}
|
|
@@ -55,6 +62,7 @@ export default class extends Controller {
|
|
|
55
62
|
disconnect() {
|
|
56
63
|
this.element.removeEventListener("turbo:frame-load", this.boundApplyOrderAndVisibility)
|
|
57
64
|
document.removeEventListener("infotable:columns-changed", this.boundUpdateExportLink)
|
|
65
|
+
document.removeEventListener("infotable:apply-columns", this.boundApplyColumns)
|
|
58
66
|
document.removeEventListener("infotable:filters-changed", this.boundUpdateExportLink)
|
|
59
67
|
window.removeEventListener("resize", this.boundFitHeight)
|
|
60
68
|
cancelAnimationFrame(this.fitFrame)
|
|
@@ -88,22 +96,16 @@ export default class extends Controller {
|
|
|
88
96
|
this.applyColumnPinning()
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
// Handle loading states
|
|
92
|
-
beforeFetchRequest(event) {
|
|
93
|
-
this.showLoading()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
afterFetchResponse(event) {
|
|
97
|
-
this.hideLoading()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
99
|
// Handle turbo frame load
|
|
101
100
|
handleFrameLoad(event) {
|
|
102
101
|
this.fitHeight()
|
|
103
102
|
this.updateFilterCount(event.target)
|
|
104
103
|
// rows matching the current filters, when the server knows it (not with countless pagination)
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// (a nested frame, e.g. the infinite-scroll "next page" one, carries no count: keep the last one)
|
|
105
|
+
if (event.target.id === `${this.nameValue}_content`) {
|
|
106
|
+
const total = parseInt(event.target.querySelector("[data-total-count]")?.dataset.totalCount, 10)
|
|
107
|
+
this.totalCount = Number.isFinite(total) ? total : null
|
|
108
|
+
}
|
|
107
109
|
this.syncSelectionScope(event.target)
|
|
108
110
|
if (this.urlStateValue && event.target.id === `${this.nameValue}_content`) this.syncUrl(event.target)
|
|
109
111
|
}
|
|
@@ -179,7 +181,7 @@ export default class extends Controller {
|
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
// The first change on a fresh page replaces the entry, later ones add entries for Back
|
|
182
|
-
const state = { turbo: { restorationIdentifier: crypto.randomUUID() } }
|
|
184
|
+
const state = { turbo: { restorationIdentifier: (crypto.randomUUID?.() ?? `infotable-${Date.now()}-${Math.random().toString(36).slice(2)}`) } }
|
|
183
185
|
if (this.urlStateStarted) {
|
|
184
186
|
window.history.pushState(state, "", next)
|
|
185
187
|
} else {
|
|
@@ -240,7 +242,17 @@ export default class extends Controller {
|
|
|
240
242
|
// Fires for rows added later (next page, infinite scroll) so they pick up the selection
|
|
241
243
|
rowCheckboxTargetConnected(checkbox) {
|
|
242
244
|
checkbox.checked = this.selected.has(checkbox.value)
|
|
243
|
-
this.
|
|
245
|
+
this.scheduleRenderSelection()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A page connects up to 500 checkboxes at once: render the selection once for all of them
|
|
249
|
+
scheduleRenderSelection() {
|
|
250
|
+
if (this.renderScheduled) return
|
|
251
|
+
this.renderScheduled = true
|
|
252
|
+
queueMicrotask(() => {
|
|
253
|
+
this.renderScheduled = false
|
|
254
|
+
this.renderSelection()
|
|
255
|
+
})
|
|
244
256
|
}
|
|
245
257
|
|
|
246
258
|
toggleRow(event) {
|
|
@@ -379,15 +391,6 @@ export default class extends Controller {
|
|
|
379
391
|
form.remove()
|
|
380
392
|
}
|
|
381
393
|
|
|
382
|
-
showLoading() {
|
|
383
|
-
// Add loading indicator (to be enhanced in later phases)
|
|
384
|
-
this.element.style.opacity = "0.6"
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
hideLoading() {
|
|
388
|
-
this.element.style.opacity = "1"
|
|
389
|
-
}
|
|
390
|
-
|
|
391
394
|
// Shift+click on a sortable header adds/toggles the column in the multi-sort list
|
|
392
395
|
// (sort=name:asc,age:desc). A plain click keeps the link's single-column behaviour.
|
|
393
396
|
sortClick(event) {
|
|
@@ -491,8 +494,18 @@ export default class extends Controller {
|
|
|
491
494
|
}
|
|
492
495
|
}
|
|
493
496
|
|
|
494
|
-
//
|
|
495
|
-
|
|
497
|
+
// The columns drawer changed order, visibility or pinning (it stored them in localStorage)
|
|
498
|
+
applyColumnsFromDrawer(event) {
|
|
499
|
+
const { tableName, order } = event.detail
|
|
500
|
+
if (tableName !== this.nameValue) return
|
|
501
|
+
|
|
502
|
+
this.applyColumnOrder(order)
|
|
503
|
+
this.applyColumnVisibility()
|
|
504
|
+
this.applyColumnPinning()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Apply column order: the given one, else the one in localStorage
|
|
508
|
+
applyColumnOrder(explicitOrder = null) {
|
|
496
509
|
const tableName = this.nameValue
|
|
497
510
|
const orderKey = `infotable_column_order_${tableName}`
|
|
498
511
|
|
|
@@ -520,11 +533,11 @@ export default class extends Controller {
|
|
|
520
533
|
const defaultSchema = headers.map(h => h.dataset.columnName)
|
|
521
534
|
|
|
522
535
|
// Load stored order from localStorage
|
|
523
|
-
const storedOrder = this.loadColumnOrder(tableName)
|
|
536
|
+
const storedOrder = explicitOrder || this.loadColumnOrder(tableName)
|
|
524
537
|
if (!storedOrder) return
|
|
525
538
|
|
|
526
539
|
// Validate stored order against default schema
|
|
527
|
-
if (!this.isSchemaValid(storedOrder, defaultSchema)) {
|
|
540
|
+
if (!explicitOrder && !this.isSchemaValid(storedOrder, defaultSchema)) {
|
|
528
541
|
console.warn(`Schema mismatch for table ${tableName}, clearing stored order`)
|
|
529
542
|
this.clearColumnOrder(tableName)
|
|
530
543
|
return
|
|
@@ -628,7 +641,6 @@ export default class extends Controller {
|
|
|
628
641
|
}
|
|
629
642
|
|
|
630
643
|
const headers = table.querySelectorAll("thead th[data-column-name]")
|
|
631
|
-
const rows = table.querySelectorAll("tbody tr")
|
|
632
644
|
|
|
633
645
|
if (headers.length === 0) {
|
|
634
646
|
console.warn(`No column headers found for table: ${tableName}`)
|
|
@@ -642,31 +654,18 @@ export default class extends Controller {
|
|
|
642
654
|
visibility = JSON.parse(stored)
|
|
643
655
|
}
|
|
644
656
|
|
|
657
|
+
const shown = new Map()
|
|
645
658
|
headers.forEach((header) => {
|
|
646
659
|
const columnName = header.dataset.columnName
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
let shouldShow
|
|
651
|
-
if (visibility && visibility.hasOwnProperty(columnName)) {
|
|
652
|
-
// Use saved preference
|
|
653
|
-
shouldShow = visibility[columnName]
|
|
654
|
-
} else {
|
|
655
|
-
// Use default visibility
|
|
656
|
-
shouldShow = defaultVisible
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Apply visibility
|
|
660
|
+
const saved = visibility && Object.prototype.hasOwnProperty.call(visibility, columnName)
|
|
661
|
+
const shouldShow = saved ? visibility[columnName] : header.dataset.defaultVisible === "true"
|
|
662
|
+
shown.set(columnName, shouldShow)
|
|
660
663
|
header.style.display = shouldShow ? "" : "none"
|
|
664
|
+
})
|
|
661
665
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const cell = Array.from(cells).find(c => c.dataset.columnName === columnName)
|
|
666
|
-
if (cell) {
|
|
667
|
-
cell.style.display = shouldShow ? "" : "none"
|
|
668
|
-
}
|
|
669
|
-
})
|
|
666
|
+
// One pass over the cells: hide/show each by its column
|
|
667
|
+
table.querySelectorAll("tbody td[data-column-name]").forEach(cell => {
|
|
668
|
+
if (shown.has(cell.dataset.columnName)) cell.style.display = shown.get(cell.dataset.columnName) ? "" : "none"
|
|
670
669
|
})
|
|
671
670
|
} catch (e) {
|
|
672
671
|
console.warn("Failed to apply column visibility", e)
|
|
@@ -781,34 +780,31 @@ export default class extends Controller {
|
|
|
781
780
|
// The data-turbo="false" ensures Turbo doesn't intercept this
|
|
782
781
|
}
|
|
783
782
|
|
|
784
|
-
//
|
|
783
|
+
// Row click (`row_link` and/or `row_click`). Fires a cancelable `infotable:row-click` on the row;
|
|
784
|
+
// when nothing calls preventDefault() the click falls back to following the row's link.
|
|
785
785
|
handleRowClick(event) {
|
|
786
786
|
const row = event.currentTarget
|
|
787
|
-
const url = row.dataset.rowLink
|
|
788
787
|
|
|
789
|
-
|
|
788
|
+
// Clicks on controls inside the row belong to those controls
|
|
789
|
+
if (event.target.closest("a, button, summary, details, label, input, select, textarea")) return
|
|
790
790
|
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
791
|
+
const url = row.dataset.rowLink || null
|
|
792
|
+
const data = { ...row.dataset }
|
|
793
|
+
delete data.rowLink
|
|
794
|
+
delete data.action
|
|
794
795
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Ignore clicks on elements inside buttons or links
|
|
802
|
-
if (target.closest("a") || target.closest("button")) {
|
|
803
|
-
return
|
|
804
|
-
}
|
|
796
|
+
const click = new CustomEvent("infotable:row-click", {
|
|
797
|
+
bubbles: true,
|
|
798
|
+
cancelable: true,
|
|
799
|
+
detail: { id: row.id || null, url, data, originalEvent: event }
|
|
800
|
+
})
|
|
801
|
+
if (!row.dispatchEvent(click) || !url) return
|
|
805
802
|
|
|
806
|
-
//
|
|
807
|
-
// Check if meta key (Cmd on Mac, Ctrl on Windows) is pressed for new tab
|
|
803
|
+
// Cmd on Mac, Ctrl elsewhere: new tab
|
|
808
804
|
if (event.metaKey || event.ctrlKey) {
|
|
809
805
|
window.open(url, "_blank")
|
|
810
806
|
} else {
|
|
811
|
-
window.location.href = url
|
|
807
|
+
window.Turbo ? window.Turbo.visit(url) : (window.location.href = url)
|
|
812
808
|
}
|
|
813
809
|
}
|
|
814
810
|
|
|
@@ -966,6 +962,12 @@ export default class extends Controller {
|
|
|
966
962
|
}
|
|
967
963
|
|
|
968
964
|
// Apply column pinning (sticky positioning) from localStorage
|
|
965
|
+
// Small pushpin before a pinned column's label, tilted toward the side it is pinned to
|
|
966
|
+
addPinIcon(header, side) {
|
|
967
|
+
const tilt = side === "left" ? "-rotate-45" : "rotate-45"
|
|
968
|
+
header.insertAdjacentHTML("afterbegin", `<svg data-pin-icon aria-hidden="true" class="inline-block w-3 h-3 mr-1 -mt-0.5 text-indigo-500 dark:text-indigo-400 ${tilt}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>`)
|
|
969
|
+
}
|
|
970
|
+
|
|
969
971
|
applyColumnPinning() {
|
|
970
972
|
const tableName = this.nameValue
|
|
971
973
|
const pinnedKey = `infotable_column_pinned_${tableName}`
|
|
@@ -992,7 +994,7 @@ export default class extends Controller {
|
|
|
992
994
|
return
|
|
993
995
|
}
|
|
994
996
|
|
|
995
|
-
|
|
997
|
+
let headers = Array.from(table.querySelectorAll("thead th[data-column-name]"))
|
|
996
998
|
|
|
997
999
|
// Columns the user never pinned/unpinned fall back to the table's `pinned:` default
|
|
998
1000
|
headers.forEach(header => {
|
|
@@ -1003,6 +1005,32 @@ export default class extends Controller {
|
|
|
1003
1005
|
})
|
|
1004
1006
|
const rows = table.querySelectorAll("tbody tr")
|
|
1005
1007
|
|
|
1008
|
+
// Pinned columns jump to their edge, keeping their relative order, so they sit side by side
|
|
1009
|
+
// with no gap; unpinning restores the base order (stored order, else the server's)
|
|
1010
|
+
const names = headers.map(h => h.dataset.columnName)
|
|
1011
|
+
headers.forEach((h, i) => { if (h.dataset.pinBase === undefined) h.dataset.pinBase = String(i) })
|
|
1012
|
+
const storedOrder = this.loadColumnOrder(tableName)
|
|
1013
|
+
const base = storedOrder && this.isSchemaValid(storedOrder, names)
|
|
1014
|
+
? storedOrder
|
|
1015
|
+
: [...headers].sort((a, b) => a.dataset.pinBase - b.dataset.pinBase).map(h => h.dataset.columnName)
|
|
1016
|
+
const leftNames = base.filter(n => pinnedData[n] === 'left')
|
|
1017
|
+
const rightNames = base.filter(n => pinnedData[n] === 'right')
|
|
1018
|
+
const pinOrder = [...leftNames, ...base.filter(n => !leftNames.includes(n) && !rightNames.includes(n)), ...rightNames]
|
|
1019
|
+
// Re-appending a node drops its focus, so only touch rows that are out of order
|
|
1020
|
+
const inOrder = els => els.every((el, i) => el.dataset.columnName === pinOrder[i])
|
|
1021
|
+
const headerRow = headers[0]?.parentElement
|
|
1022
|
+
if (headerRow && !inOrder(headers)) {
|
|
1023
|
+
const byName = new Map(headers.map(h => [h.dataset.columnName, h]))
|
|
1024
|
+
pinOrder.forEach(n => byName.has(n) && headerRow.appendChild(byName.get(n)))
|
|
1025
|
+
headers = Array.from(table.querySelectorAll("thead th[data-column-name]"))
|
|
1026
|
+
}
|
|
1027
|
+
rows.forEach(row => {
|
|
1028
|
+
const cells = Array.from(row.querySelectorAll("td[data-column-name]"))
|
|
1029
|
+
if (inOrder(cells)) return
|
|
1030
|
+
const cellByName = new Map(cells.map(c => [c.dataset.columnName, c]))
|
|
1031
|
+
pinOrder.forEach(n => cellByName.has(n) && row.appendChild(cellByName.get(n)))
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1006
1034
|
|
|
1007
1035
|
// Calculate positions for pinned columns
|
|
1008
1036
|
let leftOffset = 0
|
|
@@ -1011,6 +1039,7 @@ export default class extends Controller {
|
|
|
1011
1039
|
// First pass: reset all columns
|
|
1012
1040
|
headers.forEach(header => {
|
|
1013
1041
|
header.style.position = ''
|
|
1042
|
+
header.style.top = ''
|
|
1014
1043
|
header.style.left = ''
|
|
1015
1044
|
header.style.right = ''
|
|
1016
1045
|
header.style.zIndex = ''
|
|
@@ -1024,7 +1053,8 @@ export default class extends Controller {
|
|
|
1024
1053
|
header.style.removeProperty('opacity')
|
|
1025
1054
|
header.style.backgroundClip = ''
|
|
1026
1055
|
header.style.boxShadow = ''
|
|
1027
|
-
header.
|
|
1056
|
+
header.querySelectorAll('[data-pin-icon]').forEach(icon => icon.remove())
|
|
1057
|
+
header.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-gray-50', 'dark:!bg-gray-800')
|
|
1028
1058
|
})
|
|
1029
1059
|
|
|
1030
1060
|
rows.forEach(row => {
|
|
@@ -1044,7 +1074,7 @@ export default class extends Controller {
|
|
|
1044
1074
|
cell.style.removeProperty('opacity')
|
|
1045
1075
|
cell.style.backgroundClip = ''
|
|
1046
1076
|
cell.style.boxShadow = ''
|
|
1047
|
-
cell.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900',
|
|
1077
|
+
cell.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900', 'group-hover:!bg-gray-50', 'dark:group-hover:!bg-gray-800')
|
|
1048
1078
|
})
|
|
1049
1079
|
})
|
|
1050
1080
|
|
|
@@ -1063,8 +1093,10 @@ export default class extends Controller {
|
|
|
1063
1093
|
}
|
|
1064
1094
|
})
|
|
1065
1095
|
|
|
1066
|
-
// Apply sticky positioning for pinned columns
|
|
1067
|
-
headers.
|
|
1096
|
+
// Apply sticky positioning for pinned columns (right ones from the outer edge inwards)
|
|
1097
|
+
const rightPinnedHeaders = headers.filter(h => pinnedData[h.dataset.columnName] === 'right')
|
|
1098
|
+
const applyOrder = [...headers.filter(h => pinnedData[h.dataset.columnName] === 'left'), ...rightPinnedHeaders.reverse()]
|
|
1099
|
+
applyOrder.forEach(header => {
|
|
1068
1100
|
const columnName = header.dataset.columnName
|
|
1069
1101
|
const pinned = pinnedData[columnName]
|
|
1070
1102
|
|
|
@@ -1078,27 +1110,17 @@ export default class extends Controller {
|
|
|
1078
1110
|
top: 0px !important;
|
|
1079
1111
|
left: ${leftOffset}px !important;
|
|
1080
1112
|
z-index: 30 !important;
|
|
1081
|
-
background: #f9fafb !important;
|
|
1082
|
-
background-color: #f9fafb !important;
|
|
1083
|
-
background-image: none !important;
|
|
1084
|
-
background-position: initial !important;
|
|
1085
|
-
background-size: initial !important;
|
|
1086
|
-
background-repeat: initial !important;
|
|
1087
|
-
background-attachment: initial !important;
|
|
1088
1113
|
opacity: 1 !important;
|
|
1089
1114
|
background-clip: padding-box !important;
|
|
1090
|
-
box-shadow:
|
|
1115
|
+
box-shadow: ${columnName === lastLeftPinned ? LEFT_EDGE : 'none'} !important;
|
|
1091
1116
|
${existingStyles}
|
|
1092
1117
|
`.replace(/\s+/g, ' ').trim()
|
|
1093
1118
|
|
|
1094
1119
|
// Set all styles at once via setAttribute (most aggressive method)
|
|
1095
1120
|
header.setAttribute('style', newStyles)
|
|
1096
1121
|
header.classList.add('infotable-pinned-left', '!bg-gray-50', 'dark:!bg-gray-800')
|
|
1122
|
+
this.addPinIcon(header, 'left')
|
|
1097
1123
|
|
|
1098
|
-
// Mark last left-pinned column
|
|
1099
|
-
if (columnName === lastLeftPinned) {
|
|
1100
|
-
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]")
|
|
1101
|
-
}
|
|
1102
1124
|
|
|
1103
1125
|
// Apply to cells in all rows
|
|
1104
1126
|
rows.forEach(row => {
|
|
@@ -1112,25 +1134,15 @@ export default class extends Controller {
|
|
|
1112
1134
|
position: sticky !important;
|
|
1113
1135
|
left: ${leftOffset}px !important;
|
|
1114
1136
|
z-index: 15 !important;
|
|
1115
|
-
background: #ffffff !important;
|
|
1116
|
-
background-color: #ffffff !important;
|
|
1117
|
-
background-image: none !important;
|
|
1118
|
-
background-position: initial !important;
|
|
1119
|
-
background-size: initial !important;
|
|
1120
|
-
background-repeat: initial !important;
|
|
1121
|
-
background-attachment: initial !important;
|
|
1122
1137
|
opacity: 1 !important;
|
|
1123
1138
|
background-clip: padding-box !important;
|
|
1124
|
-
box-shadow:
|
|
1139
|
+
box-shadow: ${columnName === lastLeftPinned ? LEFT_EDGE : 'none'} !important;
|
|
1125
1140
|
${existingStyles}
|
|
1126
1141
|
`.replace(/\s+/g, ' ').trim()
|
|
1127
1142
|
|
|
1128
1143
|
cell.setAttribute('style', newStyles)
|
|
1129
|
-
cell.classList.add('infotable-pinned-left', '!bg-white', 'dark:!bg-gray-900')
|
|
1144
|
+
cell.classList.add('infotable-pinned-left', '!bg-white', 'dark:!bg-gray-900', 'group-hover:!bg-gray-50', 'dark:group-hover:!bg-gray-800')
|
|
1130
1145
|
|
|
1131
|
-
if (columnName === lastLeftPinned) {
|
|
1132
|
-
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]")
|
|
1133
|
-
}
|
|
1134
1146
|
}
|
|
1135
1147
|
})
|
|
1136
1148
|
|
|
@@ -1145,26 +1157,16 @@ export default class extends Controller {
|
|
|
1145
1157
|
top: 0px !important;
|
|
1146
1158
|
right: ${rightOffset}px !important;
|
|
1147
1159
|
z-index: 30 !important;
|
|
1148
|
-
background: #f9fafb !important;
|
|
1149
|
-
background-color: #f9fafb !important;
|
|
1150
|
-
background-image: none !important;
|
|
1151
|
-
background-position: initial !important;
|
|
1152
|
-
background-size: initial !important;
|
|
1153
|
-
background-repeat: initial !important;
|
|
1154
|
-
background-attachment: initial !important;
|
|
1155
1160
|
opacity: 1 !important;
|
|
1156
1161
|
background-clip: padding-box !important;
|
|
1157
|
-
box-shadow:
|
|
1162
|
+
box-shadow: ${columnName === firstRightPinned ? RIGHT_EDGE : 'none'} !important;
|
|
1158
1163
|
${existingStyles}
|
|
1159
1164
|
`.replace(/\s+/g, ' ').trim()
|
|
1160
1165
|
|
|
1161
1166
|
header.setAttribute('style', newStyles)
|
|
1162
1167
|
header.classList.add('infotable-pinned-right', '!bg-gray-50', 'dark:!bg-gray-800')
|
|
1168
|
+
this.addPinIcon(header, 'right')
|
|
1163
1169
|
|
|
1164
|
-
// Mark first right-pinned column
|
|
1165
|
-
if (columnName === firstRightPinned) {
|
|
1166
|
-
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]")
|
|
1167
|
-
}
|
|
1168
1170
|
|
|
1169
1171
|
// Apply to cells in all rows
|
|
1170
1172
|
rows.forEach(row => {
|
|
@@ -1178,25 +1180,15 @@ export default class extends Controller {
|
|
|
1178
1180
|
position: sticky !important;
|
|
1179
1181
|
right: ${rightOffset}px !important;
|
|
1180
1182
|
z-index: 15 !important;
|
|
1181
|
-
background: #ffffff !important;
|
|
1182
|
-
background-color: #ffffff !important;
|
|
1183
|
-
background-image: none !important;
|
|
1184
|
-
background-position: initial !important;
|
|
1185
|
-
background-size: initial !important;
|
|
1186
|
-
background-repeat: initial !important;
|
|
1187
|
-
background-attachment: initial !important;
|
|
1188
1183
|
opacity: 1 !important;
|
|
1189
1184
|
background-clip: padding-box !important;
|
|
1190
|
-
box-shadow:
|
|
1185
|
+
box-shadow: ${columnName === firstRightPinned ? RIGHT_EDGE : 'none'} !important;
|
|
1191
1186
|
${existingStyles}
|
|
1192
1187
|
`.replace(/\s+/g, ' ').trim()
|
|
1193
1188
|
|
|
1194
1189
|
cell.setAttribute('style', newStyles)
|
|
1195
|
-
cell.classList.add('infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900')
|
|
1190
|
+
cell.classList.add('infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900', 'group-hover:!bg-gray-50', 'dark:group-hover:!bg-gray-800')
|
|
1196
1191
|
|
|
1197
|
-
if (columnName === firstRightPinned) {
|
|
1198
|
-
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]")
|
|
1199
|
-
}
|
|
1200
1192
|
}
|
|
1201
1193
|
})
|
|
1202
1194
|
|