ruby_ui 1.0.2 → 1.2.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/LICENSE.txt +1 -1
- data/README.md +4 -0
- data/lib/generators/ruby_ui/component_generator.rb +5 -1
- data/lib/generators/ruby_ui/dependencies.yml +10 -0
- data/lib/generators/ruby_ui/install/docs_generator.rb +33 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +4 -0
- data/lib/ruby_ui/accordion/accordion_docs.rb +53 -0
- data/lib/ruby_ui/alert/alert_docs.rb +135 -0
- data/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb +35 -0
- data/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb +64 -0
- data/lib/ruby_ui/avatar/avatar_docs.rb +92 -0
- data/lib/ruby_ui/badge/badge_docs.rb +80 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb +116 -0
- data/lib/ruby_ui/button/button_docs.rb +143 -0
- data/lib/ruby_ui/calendar/calendar_docs.rb +34 -0
- data/lib/ruby_ui/card/card_docs.rb +114 -0
- data/lib/ruby_ui/carousel/carousel_docs.rb +104 -0
- data/lib/ruby_ui/chart/chart_docs.rb +115 -0
- data/lib/ruby_ui/checkbox/checkbox.rb +2 -2
- data/lib/ruby_ui/checkbox/checkbox_docs.rb +41 -0
- data/lib/ruby_ui/clipboard/clipboard_docs.rb +30 -0
- data/lib/ruby_ui/codeblock/codeblock_docs.rb +55 -0
- data/lib/ruby_ui/collapsible/collapsible_docs.rb +96 -0
- data/lib/ruby_ui/combobox/combobox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
- data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +252 -47
- data/lib/ruby_ui/combobox/combobox_docs.rb +286 -0
- data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
- data/lib/ruby_ui/combobox/combobox_item.rb +5 -7
- data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +1 -5
- data/lib/ruby_ui/combobox/combobox_radio.rb +1 -8
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +1 -6
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- data/lib/ruby_ui/command/command_docs.rb +154 -0
- data/lib/ruby_ui/context_menu/context_menu.rb +1 -1
- data/lib/ruby_ui/context_menu/context_menu_docs.rb +85 -0
- data/lib/ruby_ui/data_table/data_table.rb +29 -0
- data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
- data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
- data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
- data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
- data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
- data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
- data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
- data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
- data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
- data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
- data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
- data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
- data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
- data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
- data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
- data/lib/ruby_ui/dialog/dialog_docs.rb +102 -0
- data/lib/ruby_ui/docs/base.rb +90 -0
- data/lib/ruby_ui/docs/component_setup_tabs.rb +15 -0
- data/lib/ruby_ui/docs/components_table.rb +13 -0
- data/lib/ruby_ui/docs/header.rb +17 -0
- data/lib/ruby_ui/docs/sidebar_examples.rb +22 -0
- data/lib/ruby_ui/docs/visual_code_example.rb +22 -0
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +212 -0
- data/lib/ruby_ui/form/form_docs.rb +178 -0
- data/lib/ruby_ui/form/form_field.rb +1 -1
- data/lib/ruby_ui/form/form_field_error.rb +1 -1
- data/lib/ruby_ui/form/form_field_hint.rb +1 -1
- data/lib/ruby_ui/form/form_field_label.rb +1 -1
- data/lib/ruby_ui/hover_card/hover_card_docs.rb +71 -0
- data/lib/ruby_ui/input/input.rb +4 -3
- data/lib/ruby_ui/input/input_docs.rb +68 -0
- data/lib/ruby_ui/link/link_docs.rb +106 -0
- data/lib/ruby_ui/masked_input/masked_input.rb +11 -1
- data/lib/ruby_ui/masked_input/masked_input_controller.js +13 -0
- data/lib/ruby_ui/masked_input/masked_input_docs.rb +47 -0
- data/lib/ruby_ui/native_select/native_select.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
- data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
- data/lib/ruby_ui/pagination/pagination_docs.rb +127 -0
- data/lib/ruby_ui/popover/popover_docs.rb +971 -0
- data/lib/ruby_ui/progress/progress_docs.rb +27 -0
- data/lib/ruby_ui/radio_button/radio_button.rb +1 -1
- data/lib/ruby_ui/radio_button/radio_button_docs.rb +53 -0
- data/lib/ruby_ui/select/select_docs.rb +129 -0
- data/lib/ruby_ui/separator/separator_docs.rb +36 -0
- data/lib/ruby_ui/sheet/sheet_content.rb +1 -1
- data/lib/ruby_ui/sheet/sheet_docs.rb +76 -0
- data/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb +29 -0
- data/lib/ruby_ui/sidebar/sidebar_docs.rb +176 -0
- data/lib/ruby_ui/skeleton/skeleton_docs.rb +29 -0
- data/lib/ruby_ui/switch/switch_docs.rb +46 -0
- data/lib/ruby_ui/table/table_docs.rb +102 -0
- data/lib/ruby_ui/tabs/tabs_docs.rb +211 -0
- data/lib/ruby_ui/textarea/textarea_docs.rb +54 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +71 -0
- data/lib/ruby_ui/tooltip/tooltip_docs.rb +52 -0
- data/lib/ruby_ui/typography/typography_docs.rb +107 -0
- data/lib/ruby_ui.rb +1 -1
- metadata +90 -3
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::DataTable < Views::Base
|
|
4
|
+
Row = Struct.new(:id, :name, :email, :salary, :status, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
SAMPLE_ROWS = [
|
|
7
|
+
Row.new(id: 1, name: "Alice", email: "alice@example.com", salary: 90_000, status: "Active"),
|
|
8
|
+
Row.new(id: 2, name: "Bob", email: "bob@example.com", salary: 75_000, status: "Inactive"),
|
|
9
|
+
Row.new(id: 3, name: "Carol", email: "carol@example.com", salary: 85_000, status: "Active")
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
14
|
+
component = "DataTable"
|
|
15
|
+
render Docs::Header.new(
|
|
16
|
+
title: component,
|
|
17
|
+
description: "A Hotwire-first data table. Every interaction (sort, search, pagination) is a Rails request answered with HTML, swapped via Turbo Frame. Row selection uses form-first submission."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Heading(level: 2) { "Usage" }
|
|
21
|
+
|
|
22
|
+
render Docs::VisualCodeExample.new(title: "Server-driven table", context: self) do
|
|
23
|
+
@@code = <<~RUBY
|
|
24
|
+
DataTable(id: "employees") do
|
|
25
|
+
DataTableToolbar do
|
|
26
|
+
DataTableSearch(path: employees_path, value: @search)
|
|
27
|
+
DataTablePerPageSelect(path: employees_path, value: @per_page)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
div(class: "rounded-md border") do
|
|
31
|
+
Table do
|
|
32
|
+
TableHeader do
|
|
33
|
+
TableRow do
|
|
34
|
+
TableHead { "Name" }
|
|
35
|
+
DataTableSortHead(column_key: :email, label: "Email",
|
|
36
|
+
sort: @sort, direction: @direction,
|
|
37
|
+
path: employees_path)
|
|
38
|
+
TableHead(class: "text-right") { "Salary" }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
TableBody do
|
|
42
|
+
@rows.each do |r|
|
|
43
|
+
TableRow do
|
|
44
|
+
TableCell { r.name }
|
|
45
|
+
TableCell { r.email }
|
|
46
|
+
TableCell(class: "text-right") { r.salary }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
DataTablePaginationBar do
|
|
54
|
+
DataTableSelectionSummary(total_on_page: @rows.size)
|
|
55
|
+
DataTablePagination(page: @page, per_page: @per_page,
|
|
56
|
+
total_count: @total_count, path: employees_path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
RUBY
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
render Docs::VisualCodeExample.new(title: "Selection + bulk actions", context: self) do
|
|
63
|
+
@@code = <<~RUBY
|
|
64
|
+
FORM_ID = "employees_form"
|
|
65
|
+
|
|
66
|
+
DataTable(id: "employees_select") do
|
|
67
|
+
DataTableToolbar do
|
|
68
|
+
DataTableSearch(path: employees_path, value: @search)
|
|
69
|
+
DataTableBulkActions do
|
|
70
|
+
Button(type: "submit", form: FORM_ID,
|
|
71
|
+
formaction: bulk_delete_employees_path,
|
|
72
|
+
formmethod: "post",
|
|
73
|
+
variant: :destructive, size: :sm) { "Delete" }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
DataTableForm(id: FORM_ID, action: "") do
|
|
78
|
+
div(class: "rounded-md border") do
|
|
79
|
+
Table do
|
|
80
|
+
TableHeader do
|
|
81
|
+
TableRow do
|
|
82
|
+
TableHead(class: "w-10") { DataTableSelectAllCheckbox() }
|
|
83
|
+
TableHead { "Name" }
|
|
84
|
+
TableHead { "Email" }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
TableBody do
|
|
88
|
+
@rows.each do |r|
|
|
89
|
+
TableRow do
|
|
90
|
+
TableCell { DataTableRowCheckbox(value: r.id) }
|
|
91
|
+
TableCell { r.name }
|
|
92
|
+
TableCell { r.email }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
DataTablePaginationBar do
|
|
101
|
+
DataTableSelectionSummary(total_on_page: @rows.size)
|
|
102
|
+
DataTablePagination(page: @page, per_page: @per_page,
|
|
103
|
+
total_count: @total_count, path: employees_path)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
RUBY
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
render Docs::VisualCodeExample.new(title: "Column visibility", context: self) do
|
|
110
|
+
@@code = <<~RUBY
|
|
111
|
+
DataTable(id: "employees_cols") do
|
|
112
|
+
DataTableToolbar do
|
|
113
|
+
DataTableColumnToggle(columns: [
|
|
114
|
+
{key: :email, label: "Email"},
|
|
115
|
+
{key: :salary, label: "Salary"}
|
|
116
|
+
])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Table do
|
|
120
|
+
TableHeader do
|
|
121
|
+
TableRow do
|
|
122
|
+
TableHead { "Name" }
|
|
123
|
+
TableHead(data: {column: "email"}) { "Email" }
|
|
124
|
+
TableHead(data: {column: "salary"}) { "Salary" }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
TableBody do
|
|
128
|
+
@rows.each do |r|
|
|
129
|
+
TableRow do
|
|
130
|
+
TableCell { r.name }
|
|
131
|
+
TableCell(data: {column: "email"}) { r.email }
|
|
132
|
+
TableCell(data: {column: "salary"}) { r.salary }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
RUBY
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
render Docs::VisualCodeExample.new(title: "Expandable rows", context: self) do
|
|
142
|
+
@@code = <<~RUBY
|
|
143
|
+
DataTable(id: "employees_expand") do
|
|
144
|
+
Table do
|
|
145
|
+
TableHeader do
|
|
146
|
+
TableRow do
|
|
147
|
+
TableHead(class: "w-10") { }
|
|
148
|
+
TableHead { "Name" }
|
|
149
|
+
TableHead { "Email" }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
TableBody do
|
|
153
|
+
@rows.each do |r|
|
|
154
|
+
detail_id = "row-\#{r.id}-detail"
|
|
155
|
+
TableRow do
|
|
156
|
+
TableCell { DataTableExpandToggle(controls: detail_id, label: "Toggle \#{r.name}") }
|
|
157
|
+
TableCell { r.name }
|
|
158
|
+
TableCell { r.email }
|
|
159
|
+
end
|
|
160
|
+
TableRow(id: detail_id, class: "hidden", role: "region") do
|
|
161
|
+
TableCell(colspan: 3, class: "bg-muted/40") do
|
|
162
|
+
div(class: "p-4") do
|
|
163
|
+
p { "Salary: $\#{r.salary}" }
|
|
164
|
+
p { "Status: \#{r.status}" }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
RUBY
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
176
|
+
|
|
177
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableExpandToggle < Base
|
|
5
|
+
def initialize(controls:, expanded: false, label: "Toggle row details", **attrs)
|
|
6
|
+
@controls = controls
|
|
7
|
+
@expanded = expanded
|
|
8
|
+
@label = label
|
|
9
|
+
super(**attrs)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
button(
|
|
14
|
+
type: "button",
|
|
15
|
+
aria_expanded: @expanded.to_s,
|
|
16
|
+
aria_controls: @controls,
|
|
17
|
+
aria_label: @label,
|
|
18
|
+
data: {
|
|
19
|
+
action: "click->ruby-ui--data-table#toggleRowDetail"
|
|
20
|
+
},
|
|
21
|
+
**attrs
|
|
22
|
+
) do
|
|
23
|
+
render_icon
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def render_icon
|
|
30
|
+
# inline chevron-right SVG (lucide)
|
|
31
|
+
svg(
|
|
32
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
33
|
+
width: "16",
|
|
34
|
+
height: "16",
|
|
35
|
+
viewBox: "0 0 24 24",
|
|
36
|
+
fill: "none",
|
|
37
|
+
stroke: "currentColor",
|
|
38
|
+
stroke_width: "2",
|
|
39
|
+
stroke_linecap: "round",
|
|
40
|
+
stroke_linejoin: "round",
|
|
41
|
+
class: "h-4 w-4 transition-transform duration-150 group-aria-expanded:rotate-90"
|
|
42
|
+
) do |s|
|
|
43
|
+
s.polyline(points: "9 18 15 12 9 6")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def default_attrs
|
|
48
|
+
{
|
|
49
|
+
class: "group inline-flex items-center justify-center h-8 w-8 rounded-md hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableForm < Base
|
|
5
|
+
def initialize(action: "", method: "post", id: nil, **attrs)
|
|
6
|
+
@action = action
|
|
7
|
+
@method = method
|
|
8
|
+
@id = id
|
|
9
|
+
super(**attrs)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def view_template(&block)
|
|
13
|
+
form_attrs = {action: @action, method: @method}
|
|
14
|
+
form_attrs[:id] = @id if @id
|
|
15
|
+
form(**form_attrs, **attrs) do
|
|
16
|
+
input(type: "hidden", name: "authenticity_token", value: csrf_token)
|
|
17
|
+
yield if block
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def csrf_token
|
|
24
|
+
# In a Rails app, view_context provides a real CSRF token.
|
|
25
|
+
# Outside Rails (gem tests), fall back to a placeholder.
|
|
26
|
+
if respond_to?(:helpers, true) && helpers.respond_to?(:form_authenticity_token)
|
|
27
|
+
helpers.form_authenticity_token
|
|
28
|
+
elsif respond_to?(:view_context, true) && view_context.respond_to?(:form_authenticity_token)
|
|
29
|
+
view_context.form_authenticity_token
|
|
30
|
+
else
|
|
31
|
+
"csrf-token-placeholder"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def default_attrs
|
|
36
|
+
{}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableKaminariAdapter
|
|
5
|
+
def initialize(collection)
|
|
6
|
+
@collection = collection
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def current_page = @collection.current_page
|
|
10
|
+
|
|
11
|
+
def total_pages = @collection.total_pages
|
|
12
|
+
|
|
13
|
+
def total_count = @collection.total_count
|
|
14
|
+
|
|
15
|
+
def per_page = @collection.limit_value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableManualAdapter
|
|
5
|
+
attr_reader :current_page, :per_page, :total_count
|
|
6
|
+
|
|
7
|
+
def initialize(page:, per_page:, total_count:)
|
|
8
|
+
@current_page = page.to_i
|
|
9
|
+
@per_page = [per_page.to_i, 1].max
|
|
10
|
+
@total_count = total_count.to_i
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def total_pages
|
|
14
|
+
[(@total_count.to_f / @per_page).ceil, 1].max
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require_relative "data_table_manual_adapter"
|
|
5
|
+
require_relative "data_table_pagy_adapter"
|
|
6
|
+
require_relative "data_table_kaminari_adapter"
|
|
7
|
+
|
|
8
|
+
module RubyUI
|
|
9
|
+
class DataTablePagination < Base
|
|
10
|
+
def initialize(with: nil, pagy: nil, kaminari: nil, page: nil, per_page: nil, total_count: nil, page_param: "page", path: "", query: {}, window: 1, prev_label: "<", next_label: ">", **attrs)
|
|
11
|
+
@adapter = resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:)
|
|
12
|
+
@page_param = page_param
|
|
13
|
+
@path = path
|
|
14
|
+
@query = query.to_h.transform_keys(&:to_s)
|
|
15
|
+
@window = window
|
|
16
|
+
@prev_label = prev_label
|
|
17
|
+
@next_label = next_label
|
|
18
|
+
super(**attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def view_template
|
|
22
|
+
return if total <= 1
|
|
23
|
+
|
|
24
|
+
render RubyUI::Pagination.new(class: "mx-0 w-auto justify-end", **attrs) do
|
|
25
|
+
render RubyUI::PaginationContent.new do
|
|
26
|
+
prev_item
|
|
27
|
+
number_items
|
|
28
|
+
next_item
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:)
|
|
36
|
+
return with if with
|
|
37
|
+
return RubyUI::DataTablePagyAdapter.new(pagy) if pagy
|
|
38
|
+
return RubyUI::DataTableKaminariAdapter.new(kaminari) if kaminari
|
|
39
|
+
if page && per_page && total_count
|
|
40
|
+
return RubyUI::DataTableManualAdapter.new(page:, per_page:, total_count:)
|
|
41
|
+
end
|
|
42
|
+
raise ArgumentError, "DataTablePagination requires one of: with:, pagy:, kaminari:, or page:+per_page:+total_count:"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def current = @adapter.current_page
|
|
46
|
+
|
|
47
|
+
def total = @adapter.total_pages
|
|
48
|
+
|
|
49
|
+
def page_href(p)
|
|
50
|
+
qs = build_query(@query.merge(@page_param => p.to_s))
|
|
51
|
+
qs.empty? ? @path : "#{@path}?#{qs}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_query(hash)
|
|
55
|
+
hash.flat_map { |k, v|
|
|
56
|
+
Array(v).map { |val| "#{CGI.escape(k.to_s)}=#{CGI.escape(val.to_s)}" }
|
|
57
|
+
}.join("&")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prev_item
|
|
61
|
+
if current <= 1
|
|
62
|
+
li do
|
|
63
|
+
span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { @prev_label }
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
render RubyUI::PaginationItem.new(href: page_href(current - 1)) { @prev_label }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def next_item
|
|
71
|
+
if current >= total
|
|
72
|
+
li do
|
|
73
|
+
span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { @next_label }
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
render RubyUI::PaginationItem.new(href: page_href(current + 1)) { @next_label }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def number_items
|
|
81
|
+
windowed_pages.each do |p|
|
|
82
|
+
if p == :gap
|
|
83
|
+
render RubyUI::PaginationEllipsis.new
|
|
84
|
+
else
|
|
85
|
+
render RubyUI::PaginationItem.new(href: page_href(p), active: p == current) { plain p.to_s }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def windowed_pages
|
|
91
|
+
return (1..total).to_a if total <= 7
|
|
92
|
+
pages = [1]
|
|
93
|
+
pages << :gap if current - @window > 2
|
|
94
|
+
((current - @window)..(current + @window)).each { |p| pages << p if p > 1 && p < total }
|
|
95
|
+
pages << :gap if current + @window < total - 1
|
|
96
|
+
pages << total
|
|
97
|
+
pages
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTablePagyAdapter
|
|
5
|
+
def initialize(pagy)
|
|
6
|
+
@pagy = pagy
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def current_page = @pagy.page
|
|
10
|
+
|
|
11
|
+
def total_pages = @pagy.pages
|
|
12
|
+
|
|
13
|
+
def total_count = @pagy.count
|
|
14
|
+
|
|
15
|
+
def per_page = @pagy.items
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTablePerPageSelect < Base
|
|
5
|
+
def initialize(path:, name: "per_page", value: nil, frame_id: nil, options: [5, 10, 25, 50], **attrs)
|
|
6
|
+
@path = path
|
|
7
|
+
@name = name
|
|
8
|
+
@value = value
|
|
9
|
+
@frame_id = frame_id
|
|
10
|
+
@options = options
|
|
11
|
+
super(**attrs)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def view_template
|
|
15
|
+
form_attrs = {action: @path, method: "get"}
|
|
16
|
+
form_attrs[:data] = {turbo_frame: @frame_id} if @frame_id
|
|
17
|
+
|
|
18
|
+
form(**attrs.merge(form_attrs)) do
|
|
19
|
+
render RubyUI::NativeSelect.new(name: @name, onchange: safe("this.form.requestSubmit()")) do
|
|
20
|
+
@options.each do |opt|
|
|
21
|
+
option_attrs = {value: opt.to_s}
|
|
22
|
+
option_attrs[:selected] = true if opt.to_s == @value.to_s
|
|
23
|
+
option(**option_attrs) { plain opt.to_s }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def default_attrs
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableRowCheckbox < Base
|
|
5
|
+
def initialize(value:, name: "ids[]", label: nil, **attrs)
|
|
6
|
+
@value = value
|
|
7
|
+
@name = name
|
|
8
|
+
@label = label
|
|
9
|
+
super(**attrs)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
render RubyUI::Checkbox.new(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_attrs
|
|
19
|
+
{
|
|
20
|
+
name: @name,
|
|
21
|
+
value: @value,
|
|
22
|
+
aria_label: @label || "Select row #{@value}",
|
|
23
|
+
data: {
|
|
24
|
+
"ruby-ui--data-table-target": "rowCheckbox",
|
|
25
|
+
action: "change->ruby-ui--data-table#toggleRow"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableSearch < Base
|
|
5
|
+
def initialize(path:, name: "search", value: nil, frame_id: nil, placeholder: "Search...", debounce: 300, preserved_params: {}, **attrs)
|
|
6
|
+
@path = path
|
|
7
|
+
@name = name
|
|
8
|
+
@value = value
|
|
9
|
+
@frame_id = frame_id
|
|
10
|
+
@placeholder = placeholder
|
|
11
|
+
@debounce = debounce
|
|
12
|
+
@preserved_params = preserved_params
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
form_attrs = {method: "get", action: @path}
|
|
18
|
+
form_attrs[:data] = form_data
|
|
19
|
+
|
|
20
|
+
form(**attrs.merge(form_attrs)) do
|
|
21
|
+
render RubyUI::Input.new(
|
|
22
|
+
type: :search,
|
|
23
|
+
name: @name,
|
|
24
|
+
value: @value,
|
|
25
|
+
placeholder: @placeholder,
|
|
26
|
+
autocomplete: "off"
|
|
27
|
+
)
|
|
28
|
+
@preserved_params.each do |k, v|
|
|
29
|
+
next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
|
30
|
+
next if k.to_s == @name
|
|
31
|
+
input(type: "hidden", name: k.to_s, value: v.to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def debounce_enabled?
|
|
39
|
+
@debounce && @debounce.to_i > 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def form_data
|
|
43
|
+
base = {}
|
|
44
|
+
base[:turbo_frame] = @frame_id if @frame_id
|
|
45
|
+
if debounce_enabled?
|
|
46
|
+
base[:controller] = "ruby-ui--data-table-search"
|
|
47
|
+
base[:"ruby-ui--data-table-search-delay-value"] = @debounce.to_i
|
|
48
|
+
base[:action] = "input->ruby-ui--data-table-search#submit"
|
|
49
|
+
end
|
|
50
|
+
base
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_attrs
|
|
54
|
+
{class: "max-w-sm flex-1"}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Module-level map survives controller disconnect/connect across Turbo Frame swaps.
|
|
4
|
+
// Keyed by the search form's action URL.
|
|
5
|
+
const PENDING_FOCUS = new Map();
|
|
6
|
+
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static values = { delay: { type: Number, default: 300 } };
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.timer = null;
|
|
12
|
+
this.beforeFrameRender = this.captureBeforeRender.bind(this);
|
|
13
|
+
document.addEventListener("turbo:before-frame-render", this.beforeFrameRender);
|
|
14
|
+
// New instance after a Turbo Frame swap — check for captured state.
|
|
15
|
+
this.restoreIfPending();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
clearTimeout(this.timer);
|
|
20
|
+
document.removeEventListener("turbo:before-frame-render", this.beforeFrameRender);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
submit(event) {
|
|
24
|
+
if (event && event.type !== "input") return;
|
|
25
|
+
clearTimeout(this.timer);
|
|
26
|
+
if (this.delayValue <= 0) return;
|
|
27
|
+
this.timer = setTimeout(() => this.element.requestSubmit(), this.delayValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
captureBeforeRender() {
|
|
31
|
+
const input = this.input();
|
|
32
|
+
if (!input || document.activeElement !== input) return;
|
|
33
|
+
PENDING_FOCUS.set(this.key(), {
|
|
34
|
+
selectionStart: input.selectionStart,
|
|
35
|
+
selectionEnd: input.selectionEnd
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
restoreIfPending() {
|
|
40
|
+
const state = PENDING_FOCUS.get(this.key());
|
|
41
|
+
if (!state) return;
|
|
42
|
+
PENDING_FOCUS.delete(this.key());
|
|
43
|
+
const input = this.input();
|
|
44
|
+
if (!input) return;
|
|
45
|
+
input.focus();
|
|
46
|
+
const len = input.value.length;
|
|
47
|
+
try {
|
|
48
|
+
input.setSelectionRange(
|
|
49
|
+
Math.min(state.selectionStart ?? len, len),
|
|
50
|
+
Math.min(state.selectionEnd ?? len, len)
|
|
51
|
+
);
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input() {
|
|
56
|
+
return this.element.querySelector('input[type="search"]');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
key() {
|
|
60
|
+
return this.element.action || "_";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableSelectAllCheckbox < Base
|
|
5
|
+
def view_template
|
|
6
|
+
render RubyUI::Checkbox.new(**attrs)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
aria_label: "Select all",
|
|
14
|
+
data: {
|
|
15
|
+
"ruby-ui--data-table-target": "selectAll",
|
|
16
|
+
action: "change->ruby-ui--data-table#toggleAll"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableSelectionSummary < Base
|
|
5
|
+
def initialize(total_on_page: 0, **attrs)
|
|
6
|
+
@total_on_page = total_on_page
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template
|
|
11
|
+
div(**attrs) do
|
|
12
|
+
plain "0 of #{@total_on_page} row(s) selected."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_attrs
|
|
19
|
+
{
|
|
20
|
+
class: "text-sm text-muted-foreground",
|
|
21
|
+
data: {"ruby-ui--data-table-target": "selectionSummary"}
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|