infotable 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5df1bcdf9da714e75a1f5b2f39681f69de99243954eefa1a936e1895d93370b
4
- data.tar.gz: 8e0d96b405da00978ceaa3090a30dd310a1d6edc5ae1591ac50e96bede219bd7
3
+ metadata.gz: 61eaa702c985b223c91d6da00bf84515ba067ad2a7d8dd3b6fde0c5e9eac7603
4
+ data.tar.gz: edda1595d51b8ae913cecd87315a01ebf2f8788df68578ff3b45086b04c942d6
5
5
  SHA512:
6
- metadata.gz: ca2f4b93b865921e6d8b8d62d8fabd3d935cd6db93600e3ecd2934df6d58e17930cb22bbf1f4bddeaa78c28dbedba7a3e6a4ff205320f144488dcffd432c950a
7
- data.tar.gz: 2d86610f937536dca157b8d99ee9265f77e652ea0772ef3c96fc0306dd7a3f3b3d2712bd8524b427e548f600b63f5694efbc636f818ba1afd4e737b54cc34332
6
+ metadata.gz: 7e612ced33027a865095124cad1c1f2798abaab7238cb4c073da90625a7adbfa1068f5ca216e2e753766085ad5e43b6d1723255773ebc9e10dc3acfa63150634
7
+ data.tar.gz: 73c8657018df5e58536b75b27e5624ef4868c5a2fb29f72d87a7a258a8cb813c51184188d1b2b6bc9de576cf94605eab850cee545bf76e71eaf53198b0fe2a32
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)
@@ -258,7 +258,7 @@ module Infotable
258
258
 
259
259
  {
260
260
  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-l-xl' : 'border-l rounded-r-xl'} border-indigo-200 dark:border-indigo-800 shadow-2xl transition-all duration-300 ease-in-out z-50",
261
+ 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
262
  role: "dialog",
263
263
  inert: true, # closed drawer is off-screen: keep it out of the tab order (JS toggles it)
264
264
  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-lg border border-gray-100 dark:border-gray-800 hover:border-indigo-200 dark:hover:border-indigo-700 transition-all duration-150">
33
- <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
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#openFilters"
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#openColumns"
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 %>">
@@ -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? %>
@@ -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 # nil means use global config
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
@@ -1,3 +1,3 @@
1
1
  module Infotable
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: infotable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Cieślar