infotable 1.3.0 → 1.4.1
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 +5 -0
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +43 -1
- data/app/assets/javascripts/infotable/controllers/table_controller.js +41 -5
- data/app/helpers/infotable/tables_helper.rb +12 -1
- data/app/views/infotable/tables/_filter_drawer.html.erb +3 -3
- data/app/views/infotable/tables/_rail.html.erb +3 -3
- data/app/views/infotable/tables/_table_rows.html.erb +1 -1
- data/app/views/infotable/tables/show.html.erb +2 -1
- data/lib/infotable/base.rb +26 -1
- data/lib/infotable/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f00cd7591b4bc03f8f7443771dc1be2abc2e022602a2a2b14a61090d6eea76aa
|
|
4
|
+
data.tar.gz: 1c48ace8adb272674a1b0138b7aa1744dfe5154695b87ea5e1773cc918d912fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ed45ecaf4c41b844dd6a7df34e35bcecefe593fa146fbe352ec9c6e4b20501f7661464a383f6a6ae386190e13d6a4cb395765c6ea4450f1d2637f442256c148
|
|
7
|
+
data.tar.gz: 79fae3ddc36f3c98c826eb18fcb9856f810e5d8fd2681eb9244ba3577efa7a397b384eb8bb170c791cd1668c5c5f5e444f546553d55cffb0cd099813555366bd
|
data/README.md
CHANGED
|
@@ -417,6 +417,11 @@ checkbox_column
|
|
|
417
417
|
# Override global height settings for this table
|
|
418
418
|
table_min_height "500px"
|
|
419
419
|
table_max_height "700px"
|
|
420
|
+
|
|
421
|
+
# Or size the table to the window: it grows until its bottom (with the pagination) sits
|
|
422
|
+
# 16px above the bottom of the viewport, and follows window resizes. table_min_height is the floor.
|
|
423
|
+
table_height :fill
|
|
424
|
+
table_height :fill, offset: 32 # keep 32px free below
|
|
420
425
|
```
|
|
421
426
|
|
|
422
427
|
#### Infinite Scroll
|
|
@@ -4,7 +4,7 @@ import Sortable from "sortablejs"
|
|
|
4
4
|
// Drawer controller for the filter drawer and column visibility
|
|
5
5
|
// Handles opening/closing the right-side drawer with smooth animations
|
|
6
6
|
export default class extends Controller {
|
|
7
|
-
static targets = ["drawer", "content", "tabButton", "tabContent", "panelTitle", "columnBadge", "columnList"]
|
|
7
|
+
static targets = ["rail", "drawer", "content", "tabButton", "tabContent", "panelTitle", "columnBadge", "columnList"]
|
|
8
8
|
static values = {
|
|
9
9
|
open: { type: Boolean, default: false },
|
|
10
10
|
currentTab: { type: String, default: "filters" }
|
|
@@ -146,12 +146,40 @@ export default class extends Controller {
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
// Rail buttons: open the tab, or collapse the drawer when that tab is already showing
|
|
150
|
+
toggleFilters(event) {
|
|
151
|
+
this.toggleTab(event, "filters")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
toggleColumns(event) {
|
|
155
|
+
this.toggleTab(event, "columns")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Click on the rail itself (outside its buttons): toggle the drawer on its default tab
|
|
159
|
+
toggleRail(event) {
|
|
160
|
+
if (event.target.closest("button, a")) return
|
|
161
|
+
|
|
162
|
+
const hasFiltersTab = this.tabContentTargets.some(content => content.dataset.tab === "filters")
|
|
163
|
+
this.toggleTab(event, hasFiltersTab ? "filters" : "columns")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toggleTab(event, tab) {
|
|
167
|
+
event?.preventDefault()
|
|
168
|
+
if (this.openValue && this.currentTabValue === tab) {
|
|
169
|
+
this.openValue = false
|
|
170
|
+
} else {
|
|
171
|
+
this.currentTabValue = tab
|
|
172
|
+
this.openValue = true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
showDrawer() {
|
|
150
177
|
// Slide in drawer from its side using inline transform
|
|
151
178
|
this.previouslyFocused = document.activeElement
|
|
152
179
|
this.drawerTarget.inert = false
|
|
153
180
|
this.drawerTarget.removeAttribute("aria-hidden")
|
|
154
181
|
this.drawerTarget.style.transform = "translateX(0)"
|
|
182
|
+
this.shiftRail(true)
|
|
155
183
|
this.drawerTarget.querySelector("input:not([type=hidden]), select, button")?.focus({ preventScroll: true })
|
|
156
184
|
}
|
|
157
185
|
|
|
@@ -163,6 +191,15 @@ export default class extends Controller {
|
|
|
163
191
|
|
|
164
192
|
// Slide out drawer to its side (data-drawer-side) using inline transform
|
|
165
193
|
this.drawerTarget.style.transform = this.drawerTarget.dataset.drawerSide === "left" ? "translateX(-100%)" : "translateX(100%)"
|
|
194
|
+
this.shiftRail(false)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// `toolbar :rail`: the rail slides left so it stays visible next to a right-hand drawer
|
|
198
|
+
shiftRail(open) {
|
|
199
|
+
if (!this.hasRailTarget || this.drawerTarget.dataset.drawerSide === "left") return
|
|
200
|
+
|
|
201
|
+
this.railTarget.style.transform = open ? `translateX(-${this.drawerTarget.offsetWidth}px)` : ""
|
|
202
|
+
this.railTarget.classList.toggle("shadow-lg", open)
|
|
166
203
|
}
|
|
167
204
|
|
|
168
205
|
closeOnEscape(event) {
|
|
@@ -187,6 +224,11 @@ export default class extends Controller {
|
|
|
187
224
|
return
|
|
188
225
|
}
|
|
189
226
|
|
|
227
|
+
// The rail's buttons toggle the drawer themselves
|
|
228
|
+
if (this.hasRailTarget && this.railTarget.contains(event.target)) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
// Don't close if clicking on buttons that open the drawer
|
|
191
233
|
const clickedElement = event.target.closest('button[data-action*="infotable--drawer#open"]')
|
|
192
234
|
if (clickedElement) {
|
|
@@ -37,6 +37,11 @@ export default class extends Controller {
|
|
|
37
37
|
// Fill the search box and the filter drawer from the page URL (the server already loaded the frame with it)
|
|
38
38
|
if (this.urlStateValue) this.applyUrlStateToControls()
|
|
39
39
|
|
|
40
|
+
// `table_height :fill`: size the rows area to the window, now and on resize
|
|
41
|
+
this.boundFitHeight = this.fitHeight.bind(this)
|
|
42
|
+
window.addEventListener("resize", this.boundFitHeight)
|
|
43
|
+
this.fitHeight()
|
|
44
|
+
|
|
40
45
|
// Listen for turbo frame loads to reapply order and visibility
|
|
41
46
|
this.boundApplyOrderAndVisibility = this.applyOrderAndVisibility.bind(this)
|
|
42
47
|
this.element.addEventListener("turbo:frame-load", this.boundApplyOrderAndVisibility)
|
|
@@ -51,9 +56,30 @@ export default class extends Controller {
|
|
|
51
56
|
this.element.removeEventListener("turbo:frame-load", this.boundApplyOrderAndVisibility)
|
|
52
57
|
document.removeEventListener("infotable:columns-changed", this.boundUpdateExportLink)
|
|
53
58
|
document.removeEventListener("infotable:filters-changed", this.boundUpdateExportLink)
|
|
59
|
+
window.removeEventListener("resize", this.boundFitHeight)
|
|
60
|
+
cancelAnimationFrame(this.fitFrame)
|
|
54
61
|
clearTimeout(this.searchTimeout)
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
// Cap the rows area (`[data-fill-height]`) so that it, plus whatever follows it in the frame
|
|
65
|
+
// (pagination), ends `data-fill-offset` px above the bottom of the window. Measured from the
|
|
66
|
+
// top of the page, so it does not depend on the current scroll position. min-height still wins.
|
|
67
|
+
fitHeight() {
|
|
68
|
+
cancelAnimationFrame(this.fitFrame)
|
|
69
|
+
this.fitFrame = requestAnimationFrame(() => {
|
|
70
|
+
this.element.querySelectorAll("[data-fill-height]").forEach(area => {
|
|
71
|
+
let below = 0
|
|
72
|
+
for (let sibling = area.nextElementSibling; sibling; sibling = sibling.nextElementSibling) {
|
|
73
|
+
below += sibling.offsetHeight
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const top = area.getBoundingClientRect().top + window.scrollY
|
|
77
|
+
const offset = parseInt(area.dataset.fillOffset, 10) || 0
|
|
78
|
+
area.style.maxHeight = `${Math.max(window.innerHeight - top - below - offset, 0)}px`
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
// Apply both order and visibility (used for turbo frame loads)
|
|
58
84
|
applyOrderAndVisibility() {
|
|
59
85
|
this.applyColumnOrder()
|
|
@@ -73,6 +99,7 @@ export default class extends Controller {
|
|
|
73
99
|
|
|
74
100
|
// Handle turbo frame load
|
|
75
101
|
handleFrameLoad(event) {
|
|
102
|
+
this.fitHeight()
|
|
76
103
|
this.updateFilterCount(event.target)
|
|
77
104
|
// rows matching the current filters, when the server knows it (not with countless pagination)
|
|
78
105
|
const total = parseInt(event.target.querySelector("[data-total-count]")?.dataset.totalCount, 10)
|
|
@@ -821,11 +848,20 @@ export default class extends Controller {
|
|
|
821
848
|
const cells = table.querySelectorAll(`th[data-column-name="${escaped}"], td[data-column-name="${escaped}"]`)
|
|
822
849
|
|
|
823
850
|
cells.forEach(cell => {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
851
|
+
// Remember how the server rendered the cell (action columns have width/min-width but no max-width)
|
|
852
|
+
if (cell.dataset.defaultWidth === undefined) {
|
|
853
|
+
cell.dataset.defaultWidth = cell.style.width
|
|
854
|
+
cell.dataset.defaultMaxWidth = cell.style.maxWidth
|
|
855
|
+
cell.dataset.defaultMinWidth = cell.style.minWidth
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (width === null) {
|
|
859
|
+
cell.style.width = cell.dataset.defaultWidth
|
|
860
|
+
cell.style.maxWidth = cell.dataset.defaultMaxWidth
|
|
861
|
+
cell.style.minWidth = cell.dataset.defaultMinWidth
|
|
862
|
+
} else {
|
|
863
|
+
cell.style.width = cell.style.maxWidth = cell.style.minWidth = `${width}px`
|
|
864
|
+
}
|
|
829
865
|
})
|
|
830
866
|
}
|
|
831
867
|
|
|
@@ -28,6 +28,17 @@ module Infotable
|
|
|
28
28
|
render partial: "infotable/tables/table", locals: locals
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Inline width style for a header/body cell. Data columns are clipped to their width (ellipsis);
|
|
32
|
+
# action columns hold buttons, so their width is only a minimum and the column grows to fit all
|
|
33
|
+
# of them (a clipped "Delete" button is worse than a wider column).
|
|
34
|
+
def infotable_cell_width_style(column, ellipsis: false)
|
|
35
|
+
if %w[action actions].include?(column[:type].to_s)
|
|
36
|
+
"width: 1%; min-width: #{column[:width]}; white-space: nowrap;"
|
|
37
|
+
else
|
|
38
|
+
"width: #{column[:width]}; max-width: #{column[:width]}; overflow: hidden;#{' text-overflow: ellipsis;' if ellipsis}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
# Format cell value based on column definition
|
|
32
43
|
def format_cell_value(record, column, table_class = nil)
|
|
33
44
|
value = (table_class || Infotable::Base).cell_value(record, column)
|
|
@@ -258,7 +269,7 @@ module Infotable
|
|
|
258
269
|
|
|
259
270
|
{
|
|
260
271
|
style: "#{side}: 0; width: #{width}; max-width: 90%; transform: translateX(#{hidden});",
|
|
261
|
-
class: "absolute top-0 bottom-0 bg-white dark:bg-gray-900 #{side == 'left' ? 'border-r rounded-
|
|
272
|
+
class: "absolute top-0 bottom-0 bg-white dark:bg-gray-900 #{side == 'left' ? 'border-r rounded-r-xl' : 'border-l rounded-l-xl'} border-gray-200 dark:border-gray-700 shadow-xl transition-all duration-300 ease-in-out z-50",
|
|
262
273
|
role: "dialog",
|
|
263
274
|
inert: true, # closed drawer is off-screen: keep it out of the tab order (JS toggles it)
|
|
264
275
|
aria: { label: I18n.t("infotable.drawer.label"), hidden: true },
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
end %>
|
|
7
7
|
<div class="flex flex-col h-full">
|
|
8
8
|
<%# Drawer Content - Scrollable %>
|
|
9
|
-
<div class="flex-1 overflow-y-auto px-4 py-4">
|
|
9
|
+
<div class="flex-1 overflow-y-auto px-4 py-4 bg-gray-50 dark:bg-gray-950">
|
|
10
10
|
<form id="filter-form-<%= table_name %>"
|
|
11
11
|
action="<%= infotable_table_data_path(table_name: table_name) %>"
|
|
12
12
|
method="get"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
|
|
30
30
|
<%# Render filter for each filterable column %>
|
|
31
31
|
<% table_class.filterable_columns.each do |column| %>
|
|
32
|
-
<div class="group p-4 bg-white dark:bg-gray-900 rounded-
|
|
33
|
-
<label class="block text-
|
|
32
|
+
<div class="group p-4 bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm hover:border-indigo-300 dark:hover:border-indigo-700 transition-all duration-150">
|
|
33
|
+
<label class="block text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2">
|
|
34
34
|
<%= column[:label] %>
|
|
35
35
|
</label>
|
|
36
36
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<% button = "relative flex items-center justify-center w-10 h-10 rounded-lg text-gray-500 dark:text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-950 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors" %>
|
|
3
3
|
<% icon = "w-5 h-5" %>
|
|
4
4
|
<% filter_count = initial_state[:filters].respond_to?(:keys) ? initial_state[:filters].keys.size : 0 %>
|
|
5
|
-
<aside class="flex flex-col items-center shrink-0 w-14 py-3 gap-1 border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900" aria-label="<%= t('infotable.drawer.label') %>">
|
|
5
|
+
<aside data-infotable--drawer-target="rail" data-action="click->infotable--drawer#toggleRail" class="cursor-pointer relative z-40 flex flex-col items-center shrink-0 w-14 py-3 gap-1 transition-all duration-300 ease-in-out border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900" aria-label="<%= t('infotable.drawer.label') %>">
|
|
6
6
|
<% if table_class.filterable_columns.any? && table_class.filters_button_visible? %>
|
|
7
7
|
<button type="button"
|
|
8
|
-
data-action="click->infotable--drawer#
|
|
8
|
+
data-action="click->infotable--drawer#toggleFilters"
|
|
9
9
|
title="<%= t('infotable.table.buttons.filters') %>"
|
|
10
10
|
aria-label="<%= t('infotable.table.buttons.filters') %>"
|
|
11
11
|
class="<%= button %>">
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<% end %>
|
|
19
19
|
|
|
20
20
|
<button type="button"
|
|
21
|
-
data-action="click->infotable--drawer#
|
|
21
|
+
data-action="click->infotable--drawer#toggleColumns"
|
|
22
22
|
title="<%= t('infotable.table.buttons.columns') %>"
|
|
23
23
|
aria-label="<%= t('infotable.table.buttons.columns') %>"
|
|
24
24
|
class="<%= button %>">
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<% table_class.columns.each do |column| %>
|
|
30
30
|
<td data-column-name="<%= column[:name] %>"
|
|
31
31
|
data-default-visible="<%= column[:visible] %>"
|
|
32
|
-
style="
|
|
32
|
+
style="<%= infotable_cell_width_style(column, ellipsis: true) %>"
|
|
33
33
|
class="<%= cell_padding %> whitespace-nowrap <%= text_size %> text-gray-700 dark:text-gray-300">
|
|
34
34
|
<%= format_cell_value(record, column, table_class) %>
|
|
35
35
|
</td>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<% active_filters = infotable_active_filters(@table_class, @filters, @search) %>
|
|
5
5
|
<%= render "infotable/tables/active_filters", chips: active_filters, table_name: params[:table_name] %>
|
|
6
6
|
<div<% if @pagy && !@table_class.countless? %> data-total-count="<%= @pagy.count %>"<% end %> class="relative overflow-y-auto overflow-x-auto scroll-smooth [scrollbar-width:thin] [scrollbar-color:#cbd5e1_#f1f5f9] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar-track]:bg-slate-100 [&::-webkit-scrollbar-thumb]:bg-slate-300 [&::-webkit-scrollbar-thumb]:rounded [&::-webkit-scrollbar-thumb:hover]:bg-slate-400 dark:[scrollbar-color:#475569_#1e293b] dark:[&::-webkit-scrollbar-track]:bg-slate-800 dark:[&::-webkit-scrollbar-thumb]:bg-slate-600"
|
|
7
|
+
<% if @table_class.fill_height? %>data-fill-height data-fill-offset="<%= @table_class.fill_offset %>"<% end %>
|
|
7
8
|
style="min-height: <%= @table_class.min_height %>; max-height: <%= @table_class.max_height %>;">
|
|
8
9
|
|
|
9
10
|
<% if @records.any? %>
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
data-column-name="<%= column[:name] %>"
|
|
34
35
|
data-default-visible="<%= column[:visible] %>"
|
|
35
36
|
<%= tag.attributes(data: { default_pinned: column[:pinned] }) if column[:pinned] %>
|
|
36
|
-
style="
|
|
37
|
+
style="<%= infotable_cell_width_style(column) %>"
|
|
37
38
|
class="<%= header_padding %> text-left text-xs font-semibold text-gray-900 dark:text-gray-100 tracking-wider select-none bg-gray-50 dark:bg-gray-800 sticky top-0 z-20">
|
|
38
39
|
<% if column[:sortable] %>
|
|
39
40
|
<% sort_index = current_sorts.index { |name, _| name == column[:name].to_s } %>
|
data/lib/infotable/base.rb
CHANGED
|
@@ -13,7 +13,8 @@ module Infotable
|
|
|
13
13
|
subclass.sort_settings = {}
|
|
14
14
|
subclass.height_settings = {
|
|
15
15
|
min_height: nil, # nil means use global config
|
|
16
|
-
max_height: nil
|
|
16
|
+
max_height: nil, # nil means use global config
|
|
17
|
+
fill_offset: nil # Integer when the table fills the viewport (table_height :fill)
|
|
17
18
|
}
|
|
18
19
|
subclass.infinite_scroll_settings = {
|
|
19
20
|
enabled: false
|
|
@@ -257,6 +258,30 @@ module Infotable
|
|
|
257
258
|
@height_settings[:max_height] = value
|
|
258
259
|
end
|
|
259
260
|
|
|
261
|
+
# Size the table to the viewport: the rows area grows until the bottom of the table (and its
|
|
262
|
+
# pagination) sits `offset` pixels above the bottom of the window, and follows window resizes.
|
|
263
|
+
# table_min_height still applies as a floor. :fixed (default) uses table_max_height.
|
|
264
|
+
# @example
|
|
265
|
+
# table_height :fill
|
|
266
|
+
# table_height :fill, offset: 32
|
|
267
|
+
def table_height(mode, offset: 16)
|
|
268
|
+
unless [ :fill, :fixed ].include?(mode)
|
|
269
|
+
raise ArgumentError, "Invalid table height: #{mode}. Must be one of fill, fixed"
|
|
270
|
+
end
|
|
271
|
+
raise ArgumentError, "offset must be a non-negative Integer" unless offset.is_a?(Integer) && offset >= 0
|
|
272
|
+
|
|
273
|
+
@height_settings[:fill_offset] = mode == :fill ? offset : nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Pixels to keep free below the table when it fills the viewport, nil for a fixed height
|
|
277
|
+
def fill_offset
|
|
278
|
+
@height_settings[:fill_offset]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def fill_height?
|
|
282
|
+
!fill_offset.nil?
|
|
283
|
+
end
|
|
284
|
+
|
|
260
285
|
# Enable/disable infinite scroll
|
|
261
286
|
def infinite_scroll(enabled)
|
|
262
287
|
@infinite_scroll_settings[:enabled] = enabled
|
data/lib/infotable/version.rb
CHANGED