infotable 1.2.0 → 1.3.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 +15 -0
- data/app/assets/javascripts/infotable/controllers/table_controller.js +149 -0
- data/app/views/infotable/tables/_active_filters.html.erb +1 -1
- data/app/views/infotable/tables/_table.html.erb +6 -6
- data/app/views/infotable/tables/show.html.erb +1 -1
- data/config/locales/en.yml +1 -0
- data/config/locales/pl.yml +1 -0
- data/lib/infotable/base.rb +14 -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: f5df1bcdf9da714e75a1f5b2f39681f69de99243954eefa1a936e1895d93370b
|
|
4
|
+
data.tar.gz: 8e0d96b405da00978ceaa3090a30dd310a1d6edc5ae1591ac50e96bede219bd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca2f4b93b865921e6d8b8d62d8fabd3d935cd6db93600e3ecd2934df6d58e17930cb22bbf1f4bddeaa78c28dbedba7a3e6a4ff205320f144488dcffd432c950a
|
|
7
|
+
data.tar.gz: 2d86610f937536dca157b8d99ee9265f77e652ea0772ef3c96fc0306dd7a3f3b3d2712bd8524b427e548f600b63f5694efbc636f818ba1afd4e737b54cc34332
|
data/README.md
CHANGED
|
@@ -372,6 +372,21 @@ Filters, search and sort survive pagination, per-page changes and CSV export.
|
|
|
372
372
|
|
|
373
373
|
Search, filters, sort, page and page size are mirrored in the page's address bar, one namespace per table (`/users?users_table[search]=ann&users_table[page]=2`). Links can be shared and bookmarked, a reload keeps the table as it was, and the Back button steps through the states. Turn it off globally with `config.url_state = false` or per table with `url_state false` (e.g. tables inside modals or tabs).
|
|
374
374
|
|
|
375
|
+
#### Column widths, dark mode, small screens
|
|
376
|
+
|
|
377
|
+
- Every header has a drag handle on its right edge (keyboard: focus it, Left/Right, Shift for bigger steps; double-click resets). Widths are remembered in the browser. `resizable_columns false` removes the handles.
|
|
378
|
+
- Dark mode follows `prefers-color-scheme`. If your app switches with a `.dark` class instead, add `@custom-variant dark (&:where(.dark, .dark *));` to your Tailwind CSS.
|
|
379
|
+
- On narrow screens the toolbar stacks and the table scrolls sideways inside its own box.
|
|
380
|
+
|
|
381
|
+
#### CSV export columns
|
|
382
|
+
|
|
383
|
+
The export contains the columns the user sees, in the order they see them; action columns are never exported. Pin it down per table:
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
export_columns :name, :email, :created_at # default list, and the only columns that can be exported
|
|
387
|
+
column :internal_note, exportable: false # never exported
|
|
388
|
+
```
|
|
389
|
+
|
|
375
390
|
#### Large tables
|
|
376
391
|
|
|
377
392
|
```ruby
|
|
@@ -5,6 +5,7 @@ export default class extends Controller {
|
|
|
5
5
|
static values = {
|
|
6
6
|
name: String,
|
|
7
7
|
urlState: Boolean,
|
|
8
|
+
resizable: { type: Boolean, default: true },
|
|
8
9
|
idsUrl: String
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -24,6 +25,9 @@ export default class extends Controller {
|
|
|
24
25
|
// Apply column visibility from localStorage when table loads
|
|
25
26
|
this.applyColumnVisibility()
|
|
26
27
|
|
|
28
|
+
// Remembered column widths and the drag handles
|
|
29
|
+
this.applyColumnWidths()
|
|
30
|
+
|
|
27
31
|
// Apply column pinning (sticky positioning)
|
|
28
32
|
this.applyColumnPinning()
|
|
29
33
|
|
|
@@ -54,6 +58,7 @@ export default class extends Controller {
|
|
|
54
58
|
applyOrderAndVisibility() {
|
|
55
59
|
this.applyColumnOrder()
|
|
56
60
|
this.applyColumnVisibility()
|
|
61
|
+
this.applyColumnWidths()
|
|
57
62
|
this.applyColumnPinning()
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -780,6 +785,150 @@ export default class extends Controller {
|
|
|
780
785
|
}
|
|
781
786
|
}
|
|
782
787
|
|
|
788
|
+
// ---- Column widths ----------------------------------------------------------
|
|
789
|
+
// Every header gets a drag handle on its right edge (pointer drag, or arrow keys while it is
|
|
790
|
+
// focused). The width is written to the header and each cell of the column and remembered in
|
|
791
|
+
// localStorage as { columnName: px }. Double-click on a handle goes back to the table's width.
|
|
792
|
+
|
|
793
|
+
static MIN_COLUMN_WIDTH = 60
|
|
794
|
+
static MAX_COLUMN_WIDTH = 800
|
|
795
|
+
|
|
796
|
+
loadColumnWidths() {
|
|
797
|
+
try {
|
|
798
|
+
const stored = JSON.parse(localStorage.getItem(`infotable_column_widths_${this.nameValue}`) || "{}")
|
|
799
|
+
return stored && typeof stored === "object" ? stored : {}
|
|
800
|
+
} catch (e) {
|
|
801
|
+
return {}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
saveColumnWidths(widths) {
|
|
806
|
+
try {
|
|
807
|
+
const key = `infotable_column_widths_${this.nameValue}`
|
|
808
|
+
Object.keys(widths).length ? localStorage.setItem(key, JSON.stringify(widths)) : localStorage.removeItem(key)
|
|
809
|
+
} catch (e) {
|
|
810
|
+
// storage unavailable (private window): the width still applies until the next load
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
columnTable() {
|
|
815
|
+
return this.element.querySelector("table") || document.getElementById(`${this.nameValue}_content`)?.querySelector("table")
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Writes one column's width to its header and cells. null restores the width the table was rendered with.
|
|
819
|
+
setColumnWidth(table, columnName, width) {
|
|
820
|
+
const escaped = CSS.escape(columnName)
|
|
821
|
+
const cells = table.querySelectorAll(`th[data-column-name="${escaped}"], td[data-column-name="${escaped}"]`)
|
|
822
|
+
|
|
823
|
+
cells.forEach(cell => {
|
|
824
|
+
if (cell.dataset.defaultWidth === undefined) cell.dataset.defaultWidth = cell.style.width
|
|
825
|
+
const value = width === null ? cell.dataset.defaultWidth : `${width}px`
|
|
826
|
+
cell.style.width = value
|
|
827
|
+
cell.style.maxWidth = value
|
|
828
|
+
cell.style.minWidth = width === null ? "" : value
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
applyColumnWidths() {
|
|
833
|
+
const table = this.columnTable()
|
|
834
|
+
if (!table) return
|
|
835
|
+
|
|
836
|
+
const widths = this.loadColumnWidths()
|
|
837
|
+
Object.entries(widths).forEach(([name, width]) => {
|
|
838
|
+
if (Number.isFinite(width)) this.setColumnWidth(table, name, width)
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
if (this.resizableValue) this.addResizeHandles(table)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
addResizeHandles(table) {
|
|
845
|
+
table.querySelectorAll("thead th[data-column-name]").forEach(header => {
|
|
846
|
+
if (header.querySelector("[data-resize-handle]")) return
|
|
847
|
+
|
|
848
|
+
const handle = document.createElement("div")
|
|
849
|
+
handle.setAttribute("data-resize-handle", "")
|
|
850
|
+
handle.setAttribute("role", "separator")
|
|
851
|
+
handle.setAttribute("aria-orientation", "vertical")
|
|
852
|
+
handle.setAttribute("aria-label", `${header.textContent.trim()}: ${this.resizeLabel()}`)
|
|
853
|
+
handle.tabIndex = 0
|
|
854
|
+
handle.className = "absolute top-0 bottom-0 right-0 w-2 cursor-col-resize touch-none select-none hover:bg-indigo-400/40 focus:bg-indigo-400/60 focus:outline-none"
|
|
855
|
+
|
|
856
|
+
handle.addEventListener("pointerdown", event => this.startResize(event, header, handle))
|
|
857
|
+
handle.addEventListener("keydown", event => this.resizeWithKeys(event, header))
|
|
858
|
+
handle.addEventListener("dblclick", event => { event.stopPropagation(); this.resetColumnWidth(header) })
|
|
859
|
+
handle.addEventListener("click", event => { event.preventDefault(); event.stopPropagation() })
|
|
860
|
+
|
|
861
|
+
header.appendChild(handle)
|
|
862
|
+
})
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
resizeLabel() {
|
|
866
|
+
return this.element.dataset.resizeLabel || "resize column"
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
startResize(event, header, handle) {
|
|
870
|
+
if (event.button !== undefined && event.button !== 0) return
|
|
871
|
+
event.preventDefault()
|
|
872
|
+
event.stopPropagation()
|
|
873
|
+
|
|
874
|
+
const table = header.closest("table")
|
|
875
|
+
const name = header.dataset.columnName
|
|
876
|
+
const startX = event.clientX
|
|
877
|
+
const startWidth = header.getBoundingClientRect().width
|
|
878
|
+
let width = startWidth
|
|
879
|
+
|
|
880
|
+
handle.setPointerCapture?.(event.pointerId)
|
|
881
|
+
|
|
882
|
+
const move = moveEvent => {
|
|
883
|
+
width = this.clampColumnWidth(startWidth + (moveEvent.clientX - startX))
|
|
884
|
+
this.setColumnWidth(table, name, width)
|
|
885
|
+
}
|
|
886
|
+
const stop = () => {
|
|
887
|
+
handle.removeEventListener("pointermove", move)
|
|
888
|
+
handle.removeEventListener("pointerup", stop)
|
|
889
|
+
handle.removeEventListener("pointercancel", stop)
|
|
890
|
+
this.commitColumnWidth(name, width)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
handle.addEventListener("pointermove", move)
|
|
894
|
+
handle.addEventListener("pointerup", stop)
|
|
895
|
+
handle.addEventListener("pointercancel", stop)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
resizeWithKeys(event, header) {
|
|
899
|
+
const step = event.shiftKey ? 50 : 10
|
|
900
|
+
const delta = { ArrowLeft: -step, ArrowRight: step }[event.key]
|
|
901
|
+
if (delta === undefined) return
|
|
902
|
+
|
|
903
|
+
event.preventDefault()
|
|
904
|
+
event.stopPropagation()
|
|
905
|
+
const width = this.clampColumnWidth(header.getBoundingClientRect().width + delta)
|
|
906
|
+
this.setColumnWidth(header.closest("table"), header.dataset.columnName, width)
|
|
907
|
+
this.commitColumnWidth(header.dataset.columnName, width)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
resetColumnWidth(header) {
|
|
911
|
+
const name = header.dataset.columnName
|
|
912
|
+
this.setColumnWidth(header.closest("table"), name, null)
|
|
913
|
+
const widths = this.loadColumnWidths()
|
|
914
|
+
delete widths[name]
|
|
915
|
+
this.saveColumnWidths(widths)
|
|
916
|
+
this.applyColumnPinning()
|
|
917
|
+
this.updateExportLink()
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
clampColumnWidth(width) {
|
|
921
|
+
return Math.round(Math.min(this.constructor.MAX_COLUMN_WIDTH, Math.max(this.constructor.MIN_COLUMN_WIDTH, width)))
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
commitColumnWidth(name, width) {
|
|
925
|
+
const widths = this.loadColumnWidths()
|
|
926
|
+
widths[name] = width
|
|
927
|
+
this.saveColumnWidths(widths)
|
|
928
|
+
// pinned columns are positioned from the widths of the columns before them
|
|
929
|
+
this.applyColumnPinning()
|
|
930
|
+
}
|
|
931
|
+
|
|
783
932
|
// Apply column pinning (sticky positioning) from localStorage
|
|
784
933
|
applyColumnPinning() {
|
|
785
934
|
const tableName = this.nameValue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%# Chips for the filters and search narrowing the table; each one removes itself. Locals: chips, table_name %>
|
|
2
2
|
<% if chips.any? %>
|
|
3
|
-
<div class="flex flex-wrap items-center gap-2 border-b border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900 px-6 py-2" data-active-filters>
|
|
3
|
+
<div class="flex flex-wrap items-center gap-2 border-b border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900 px-4 sm:px-6 py-2" data-active-filters>
|
|
4
4
|
<% chips.each do |chip| %>
|
|
5
5
|
<% remove_action = chip[:search] ? "click->infotable--table#clearSearch" : "click->infotable--drawer#clearSingleFilter" %>
|
|
6
6
|
<% chip_label = chip[:search] ? chip[:text] : "#{chip[:label]}: #{chip[:text]}" %>
|
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
export_params[:search] = initial_state[:search] if initial_state[:search].present?
|
|
35
35
|
export_url = Infotable::Engine.routes.url_helpers.table_export_path(export_params.compact)
|
|
36
36
|
%>
|
|
37
|
-
<div class="w-full relative overflow-hidden dark:[color-scheme:dark]" data-controller="infotable--table infotable--drawer" data-infotable--table-name-value="<%= table_name %>" data-infotable--table-url-state-value="<%= table_class.url_state? %>"<% if table_class.checkbox_enabled? %> data-infotable--table-ids-url-value="<%= Infotable::Engine.routes.url_helpers.table_ids_path(table_name: table_name) %>"<% end %>>
|
|
37
|
+
<div class="w-full relative overflow-hidden dark:[color-scheme:dark]" data-controller="infotable--table infotable--drawer" data-infotable--table-name-value="<%= table_name %>" data-infotable--table-url-state-value="<%= table_class.url_state? %>" data-infotable--table-resizable-value="<%= table_class.resizable_columns? %>" data-resize-label="<%= t('infotable.table.resize_column') %>"<% if table_class.checkbox_enabled? %> data-infotable--table-ids-url-value="<%= Infotable::Engine.routes.url_helpers.table_ids_path(table_name: table_name) %>"<% end %>>
|
|
38
38
|
<div class="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 relative<%= " flex" if rail %>" style="overflow: hidden;">
|
|
39
39
|
<div class="<%= rail ? "flex-1 min-w-0" : "w-full" %>">
|
|
40
40
|
<%# Header section with controls (hidden with `toolbar :rail`) %>
|
|
41
41
|
<% unless rail %>
|
|
42
|
-
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
|
43
|
-
<div class="flex justify-between items-center">
|
|
42
|
+
<div class="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
|
43
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:justify-between sm:items-center">
|
|
44
44
|
<div>
|
|
45
45
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100"><%= title %></h2>
|
|
46
46
|
<% if subtitle.present? %>
|
|
47
47
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= subtitle %></p>
|
|
48
48
|
<% end %>
|
|
49
49
|
</div>
|
|
50
|
-
<div class="flex gap-3">
|
|
50
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
51
51
|
<%# Global search over `searchable: true` columns %>
|
|
52
52
|
<% if table_class.searchable_columns.any? %>
|
|
53
53
|
<input type="search"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
placeholder="<%= t('infotable.search.placeholder') %>"
|
|
57
57
|
aria-label="<%= t('infotable.search.label') %>"
|
|
58
58
|
data-action="input->infotable--table#search"
|
|
59
|
-
class="w-56 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
|
59
|
+
class="w-full sm:w-56 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
|
60
60
|
<% end %>
|
|
61
61
|
<%# Export CSV button %>
|
|
62
62
|
<a href="<%= export_url %>"
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
|
|
90
90
|
<%# Batch action bar: shown while rows are selected %>
|
|
91
91
|
<% if table_class.batch_action_list.any? %>
|
|
92
|
-
<div class="hidden items-center justify-between gap-3 px-6 py-3 border-b border-indigo-100 dark:border-indigo-900 bg-indigo-50 dark:bg-indigo-950"
|
|
92
|
+
<div class="hidden flex-wrap items-center justify-between gap-3 px-4 sm:px-6 py-3 border-b border-indigo-100 dark:border-indigo-900 bg-indigo-50 dark:bg-indigo-950"
|
|
93
93
|
data-infotable--table-target="batchBar">
|
|
94
94
|
<div class="flex items-center gap-3 text-sm text-indigo-900 dark:text-indigo-100">
|
|
95
95
|
<span data-infotable--table-target="batchCount"
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
|
|
115
115
|
<%# Pagination - Only visible when infinite scroll is disabled %>
|
|
116
116
|
<% unless @infinite_scroll_enabled %>
|
|
117
|
-
<div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 py-4">
|
|
117
|
+
<div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 sm:px-6 py-4">
|
|
118
118
|
<%= render "infotable/tables/pagination", pagy: @pagy, table_name: params[:table_name] %>
|
|
119
119
|
</div>
|
|
120
120
|
<% end %>
|
data/config/locales/en.yml
CHANGED
data/config/locales/pl.yml
CHANGED
|
@@ -12,6 +12,7 @@ pl:
|
|
|
12
12
|
initial: "Ładowanie danych..."
|
|
13
13
|
more_items: "Ładowanie kolejnych elementów..."
|
|
14
14
|
all_loaded: "Wszystkie elementy załadowane"
|
|
15
|
+
resize_column: "zmień szerokość kolumny"
|
|
15
16
|
empty_state:
|
|
16
17
|
title: "Nie znaleziono danych"
|
|
17
18
|
description: "Rozpocznij od dodania rekordów, aby zobaczyć je tutaj."
|
data/lib/infotable/base.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Infotable
|
|
2
2
|
class Base
|
|
3
3
|
class << self
|
|
4
|
-
attr_accessor :table_columns, :table_query, :table_query_sql, :pagination_settings, :sort_settings, :height_settings, :infinite_scroll_settings, :row_link_settings, :slim_rows_settings, :row_size_settings, :filters_button_settings, :display_settings, :nil_value_settings, :empty_value_settings, :table_authorize, :checkbox_settings, :batch_action_list, :countless_setting, :url_state_setting
|
|
4
|
+
attr_accessor :table_columns, :table_query, :table_query_sql, :pagination_settings, :sort_settings, :height_settings, :infinite_scroll_settings, :row_link_settings, :slim_rows_settings, :row_size_settings, :filters_button_settings, :display_settings, :nil_value_settings, :empty_value_settings, :table_authorize, :checkbox_settings, :batch_action_list, :countless_setting, :url_state_setting, :resizable_columns_setting
|
|
5
5
|
|
|
6
6
|
def inherited(subclass)
|
|
7
7
|
super
|
|
@@ -45,6 +45,7 @@ module Infotable
|
|
|
45
45
|
subclass.batch_action_list = []
|
|
46
46
|
subclass.countless_setting = false
|
|
47
47
|
subclass.url_state_setting = nil
|
|
48
|
+
subclass.resizable_columns_setting = nil
|
|
48
49
|
|
|
49
50
|
inherit_from_parent(subclass)
|
|
50
51
|
end
|
|
@@ -62,6 +63,7 @@ module Infotable
|
|
|
62
63
|
subclass.batch_action_list = parent.batch_action_list.map(&:dup)
|
|
63
64
|
subclass.countless_setting = parent.countless_setting
|
|
64
65
|
subclass.url_state_setting = parent.url_state_setting
|
|
66
|
+
subclass.resizable_columns_setting = parent.resizable_columns_setting
|
|
65
67
|
%i[pagination_settings sort_settings height_settings infinite_scroll_settings row_link_settings
|
|
66
68
|
slim_rows_settings row_size_settings filters_button_settings display_settings
|
|
67
69
|
nil_value_settings empty_value_settings].each do |attr|
|
|
@@ -88,6 +90,17 @@ module Infotable
|
|
|
88
90
|
@url_state_setting = enabled
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
# Drag handles on the column headers (and arrow keys on the focused handle) to change
|
|
94
|
+
# column widths; widths are remembered per browser. Double-click resets one column.
|
|
95
|
+
# resizable_columns false
|
|
96
|
+
def resizable_columns(enabled = true)
|
|
97
|
+
@resizable_columns_setting = enabled
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resizable_columns?
|
|
101
|
+
@resizable_columns_setting.nil? ? true : @resizable_columns_setting ? true : false
|
|
102
|
+
end
|
|
103
|
+
|
|
91
104
|
def url_state?
|
|
92
105
|
@url_state_setting.nil? ? Infotable.configuration.url_state : @url_state_setting ? true : false
|
|
93
106
|
end
|
data/lib/infotable/version.rb
CHANGED