fluxbit_view_components 0.2.0 → 0.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 +4 -4
- data/README.md +10 -0
- data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
- data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
- data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
- data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
- data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
- data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
- data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
- data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
- data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
- data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
- data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
- data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
- data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
- data/app/components/fluxbit/accordion_component.rb +125 -0
- data/app/components/fluxbit/alert_component.rb +8 -8
- data/app/components/fluxbit/avatar_component.rb +11 -12
- data/app/components/fluxbit/avatar_group_component.rb +1 -1
- data/app/components/fluxbit/badge_component.rb +8 -7
- data/app/components/fluxbit/banner_component.rb +139 -0
- data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
- data/app/components/fluxbit/breadcrumb_component.rb +66 -0
- data/app/components/fluxbit/button_component.rb +39 -11
- data/app/components/fluxbit/button_group_component.rb +1 -1
- data/app/components/fluxbit/card_component.rb +26 -23
- data/app/components/fluxbit/carousel_component.rb +154 -0
- data/app/components/fluxbit/component.rb +24 -3
- data/app/components/fluxbit/drawer_component.html.erb +30 -0
- data/app/components/fluxbit/drawer_component.rb +125 -0
- data/app/components/fluxbit/dropdown_component.rb +41 -0
- data/app/components/fluxbit/dropdown_item_component.rb +68 -0
- data/app/components/fluxbit/flex_component.rb +1 -1
- data/app/components/fluxbit/form/check_box_component.rb +56 -0
- data/app/components/fluxbit/form/component.rb +27 -26
- data/app/components/fluxbit/form/dropzone_component.html.erb +39 -0
- data/app/components/fluxbit/form/dropzone_component.rb +39 -0
- data/app/components/fluxbit/form/field_component.rb +28 -0
- data/app/components/fluxbit/form/form_builder_component.rb +1 -1
- data/app/components/fluxbit/form/{helper_text_component.rb → help_text_component.rb} +9 -4
- data/app/components/fluxbit/form/label_component.rb +40 -30
- data/app/components/fluxbit/form/password_component.rb +247 -0
- data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
- data/app/components/fluxbit/form/range_component.rb +52 -0
- data/app/components/fluxbit/form/select_component.rb +185 -0
- data/app/components/fluxbit/form/text_field_component.rb +185 -0
- data/app/components/fluxbit/form/toggle_component.html.erb +23 -0
- data/app/components/fluxbit/form/toggle_component.rb +81 -0
- data/app/components/fluxbit/form/upload_image_component.html.erb +50 -0
- data/app/components/fluxbit/form/upload_image_component.rb +61 -0
- data/app/components/fluxbit/gravatar_component.rb +7 -0
- data/app/components/fluxbit/icon_helpers.rb +167 -0
- data/app/components/fluxbit/link_component.rb +42 -0
- data/app/components/fluxbit/modal_component.rb +28 -31
- data/app/components/fluxbit/pagination_component.rb +206 -0
- data/app/components/fluxbit/popover_component.rb +14 -14
- data/app/components/fluxbit/progress_component.rb +196 -0
- data/app/components/fluxbit/skeleton_component.rb +237 -0
- data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
- data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
- data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
- data/app/components/fluxbit/speed_dial_component.rb +73 -0
- data/app/components/fluxbit/spinner_component.rb +71 -0
- data/app/components/fluxbit/spinner_percent_component.rb +174 -0
- data/app/components/fluxbit/stepper_component.rb +223 -0
- data/app/components/fluxbit/tab_component.rb +44 -25
- data/app/components/fluxbit/table_component.rb +186 -0
- data/app/components/fluxbit/table_group_component.rb +28 -0
- data/app/components/fluxbit/theme_button_component.rb +64 -0
- data/app/components/fluxbit/timeline_component.rb +63 -0
- data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
- data/app/components/fluxbit/timeline_item_component.rb +78 -0
- data/app/components/fluxbit/tooltip_component.rb +2 -2
- data/app/helpers/fluxbit/components_helper.rb +93 -51
- data/app/helpers/fluxbit/form_builder.rb +136 -0
- data/app/helpers/fluxbit/view_helper.rb +71 -0
- data/config/locales/en.yml +37 -4
- data/config/locales/pt-BR.yml +36 -0
- data/lib/fluxbit/config/accordion_component.rb +73 -0
- data/lib/fluxbit/config/avatar_component.rb +11 -11
- data/lib/fluxbit/config/badge_component.rb +14 -11
- data/lib/fluxbit/config/banner_component.rb +60 -0
- data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
- data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
- data/lib/fluxbit/config/button_component.rb +6 -4
- data/lib/fluxbit/config/card_component.rb +23 -12
- data/lib/fluxbit/config/carousel_component.rb +33 -0
- data/lib/fluxbit/config/drawer_component.rb +48 -0
- data/lib/fluxbit/config/dropdown_component.rb +29 -0
- data/lib/fluxbit/config/form/check_box_component.rb +19 -0
- data/lib/fluxbit/config/form/dropzone_component.rb +20 -0
- data/lib/fluxbit/config/form/{helper_text_component.rb → help_text_component.rb} +2 -2
- data/lib/fluxbit/config/form/label_component.rb +31 -0
- data/lib/fluxbit/config/form/password_component.rb +19 -0
- data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
- data/lib/fluxbit/config/form/range_component.rb +15 -0
- data/lib/fluxbit/config/form/text_field_component.rb +76 -0
- data/lib/fluxbit/config/form/toggle_component.rb +79 -0
- data/lib/fluxbit/config/link_component.rb +24 -0
- data/lib/fluxbit/config/modal_component.rb +1 -1
- data/lib/fluxbit/config/pagination_component.rb +31 -0
- data/lib/fluxbit/config/popover_component.rb +1 -1
- data/lib/fluxbit/config/progress_component.rb +63 -0
- data/lib/fluxbit/config/skeleton_component.rb +82 -0
- data/lib/fluxbit/config/speed_dial_component.rb +50 -0
- data/lib/fluxbit/config/spinner_component.rb +30 -0
- data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
- data/lib/fluxbit/config/stepper_component.rb +299 -0
- data/lib/fluxbit/config/tab_component.rb +6 -0
- data/lib/fluxbit/config/table_component.rb +75 -0
- data/lib/fluxbit/config/theme_button_component.rb +19 -0
- data/lib/fluxbit/config/timeline_component.rb +77 -0
- data/lib/fluxbit/view_components/engine.rb +11 -3
- data/lib/fluxbit/view_components/version.rb +1 -1
- data/lib/fluxbit/view_components.rb +27 -1
- data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
- data/lib/generators/fluxbit/pagy_generator.rb +39 -0
- data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
- data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
- data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
- data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
- data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
- data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
- data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
- data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
- data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
- data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
- data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
- data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
- data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
- data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
- data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
- data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
- data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
- data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
- data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
- data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
- data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
- data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
- data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
- data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
- data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
- data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
- data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
- data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
- data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
- data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
- data/lib/install/install.rb +61 -3
- metadata +127 -35
- data/LICENSE.txt +0 -20
- data/app/components/fluxbit/form/checkbox_input_component.rb +0 -61
- data/app/components/fluxbit/form/datepicker_component.rb +0 -7
- data/app/components/fluxbit/form/radio_input_component.rb +0 -21
- data/app/components/fluxbit/form/range_input_component.rb +0 -51
- data/app/components/fluxbit/form/select_free_input_component.rb +0 -77
- data/app/components/fluxbit/form/select_input_component.rb +0 -21
- data/app/components/fluxbit/form/spacer_input_component.rb +0 -12
- data/app/components/fluxbit/form/text_input_component.rb +0 -225
- data/app/components/fluxbit/form/textarea_input_component.rb +0 -57
- data/app/components/fluxbit/form/toggle_input_component.rb +0 -166
- data/app/components/fluxbit/form/upload_image_input_component.html.erb +0 -48
- data/app/components/fluxbit/form/upload_image_input_component.rb +0 -61
- data/app/components/fluxbit/form/upload_input_component.html.erb +0 -12
- data/app/components/fluxbit/form/upload_input_component.rb +0 -47
- data/app/helpers/fluxbit/classes_helper.rb +0 -9
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<%= content_tag :div, **@wrapper_html do %>
|
|
2
|
+
<div id="<%= id %>" class="mt-6 grow lg:mt-0 lg:ml-6 lg:shrink-0 lg:grow-0">
|
|
3
|
+
<%= label %>
|
|
4
|
+
<div class="mt-1 lg:hidden">
|
|
5
|
+
<div class="flex items-center">
|
|
6
|
+
<div class="inline-block h-12 w-12 shrink-0 overflow-hidden <%= container_rounded_class %> relative" aria-hidden="true">
|
|
7
|
+
<%= image_element %>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="ml-5 rounded-md shadow-xs">
|
|
10
|
+
<div class="group relative flex items-center justify-center rounded-md border border-slate-300 py-2 px-3 focus-within:ring-2 focus-within:ring-sky-500 focus-within:ring-offset-2 hover:bg-slate-50">
|
|
11
|
+
<label for="mobile-<%= id %>" class="pointer-events-none relative text-sm font-medium leading-4 text-slate-700">
|
|
12
|
+
<span><%= @title %></span>
|
|
13
|
+
<span class="sr-only"><%= @label %></span>
|
|
14
|
+
</label>
|
|
15
|
+
<%= input_element(input_id: "mobile-#{id}") %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="relative hidden overflow-hidden <%= container_rounded_class %> lg:block w-40">
|
|
22
|
+
<div class="inline-block h-40 w-40 shrink-0 overflow-hidden <%= container_rounded_class %> relative" aria-hidden="true">
|
|
23
|
+
<%= image_element %>
|
|
24
|
+
</div>
|
|
25
|
+
<label for="desktop-<%= id %>" class="absolute inset-0 flex flex-col h-full w-full items-center justify-center bg-blue-800/75 text-sm font-medium text-white opacity-0 hover:opacity-100">
|
|
26
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-white mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
|
28
|
+
</svg>
|
|
29
|
+
<span><%= @title %></span>
|
|
30
|
+
<span class="sr-only"><%= @label %></span>
|
|
31
|
+
<%= input_element(input_id: "desktop-#{id}") %>
|
|
32
|
+
</label>
|
|
33
|
+
</div>
|
|
34
|
+
<%= help_text %>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
function loadFile(event, id) {
|
|
39
|
+
const images = document.querySelectorAll('.img_photo_' + id);
|
|
40
|
+
if (event.target.files && event.target.files[0]) {
|
|
41
|
+
images.forEach(function(img) {
|
|
42
|
+
img.src = URL.createObjectURL(event.target.files[0]);
|
|
43
|
+
img.onload = function() {
|
|
44
|
+
URL.revokeObjectURL(img.src);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
<% end %>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The `Fluxbit::Form::UploadImageComponent` renders a stylized image upload field with live preview,
|
|
4
|
+
# drag-and-drop UI, support for both mobile and desktop layouts, labels, helper text, and integration
|
|
5
|
+
# with Rails form builders and Active Storage attachments. It provides custom title/subtitle, placeholder,
|
|
6
|
+
# and image preview, and is fully configurable via props.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# = render Fluxbit::Form::UploadImageComponent.new(attribute: :avatar, label: "Profile photo")
|
|
10
|
+
#
|
|
11
|
+
# @see docs/03_Forms/UploadImage.md For detailed documentation and examples.
|
|
12
|
+
class Fluxbit::Form::UploadImageComponent < Fluxbit::Form::FieldComponent
|
|
13
|
+
# Initializes the upload image component with the given properties.
|
|
14
|
+
#
|
|
15
|
+
# @param form [ActionView::Helpers::FormBuilder] The form builder (optional, for Rails forms)
|
|
16
|
+
# @param attribute [Symbol] The model attribute to be used in the form (required if using form builder)
|
|
17
|
+
# @param id [String] The id of the input element (optional)
|
|
18
|
+
# @param label [String] The label for the input field (optional)
|
|
19
|
+
# @param help_text [String] Additional help text for the input field (optional)
|
|
20
|
+
# @param helper_popover [String] Content for a popover helper (optional)
|
|
21
|
+
# @param helper_popover_placement [String] Placement of the popover (default: "right")
|
|
22
|
+
# @param image_path [String] Path to the image to be displayed (optional)
|
|
23
|
+
# @param image_placeholder [String] Placeholder image path if no image is attached (optional)
|
|
24
|
+
# @param title [Boolean, String] Whether to show a title (true for default, false to hide, or custom string)
|
|
25
|
+
# @param rounded [Boolean] Whether to show image as circle (true, default) or square with rounded edges (false)
|
|
26
|
+
# @param class [String] Additional CSS classes for the input element
|
|
27
|
+
# @param ... any other HTML attribute supported by file_field_tag
|
|
28
|
+
def initialize(**props)
|
|
29
|
+
super(**props)
|
|
30
|
+
@title = @props.delete(:title) || "Change"
|
|
31
|
+
@rounded = @props.delete(:rounded)
|
|
32
|
+
@rounded = true if @rounded.nil?
|
|
33
|
+
@image_path = @props.delete(:image_path) ||
|
|
34
|
+
(if @object&.send(@attribute).respond_to?(:attached?) && @object&.send(@attribute)&.send("attached?")
|
|
35
|
+
@object&.send(@attribute)&.variant(resize_to_fit: [ 160, 160 ])
|
|
36
|
+
end) || @props.delete(:image_placeholder) || ""
|
|
37
|
+
|
|
38
|
+
@props["class"] = "absolute inset-0 h-full w-full cursor-pointer rounded-md border-gray-300 opacity-0"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def input_element(input_id: nil)
|
|
42
|
+
@props["onchange"] = "loadFile(event, '#{id}')"
|
|
43
|
+
return file_field_tag @name, @props.merge(id: input_id || id) if @form.nil?
|
|
44
|
+
|
|
45
|
+
@form.file_field(@attribute, **@props, id: input_id || id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def image_element
|
|
49
|
+
image_tag @image_path,
|
|
50
|
+
class: "img_photo_#{id} img_photo absolute inset-0 w-full h-full object-cover #{image_rounded_class}",
|
|
51
|
+
alt: @attribute&.to_s&.humanize
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def container_rounded_class
|
|
55
|
+
@rounded ? "rounded-full" : "rounded-lg"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def image_rounded_class
|
|
59
|
+
@rounded ? "rounded-full" : "rounded-lg"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -24,6 +24,7 @@ class Fluxbit::GravatarComponent < Fluxbit::AvatarComponent
|
|
|
24
24
|
# @option props [Symbol] :filetype (:png) The filetype of the Gravatar (:png, :jpg, :gif).
|
|
25
25
|
# @option props [Symbol] :default (:identicon) The default image to use if no Gravatar is found.
|
|
26
26
|
# @option props [Integer] :size (:md) The size of the Gravatar base on the size provided by AvatarComponent.
|
|
27
|
+
# @option props [Boolean] :url_only (false) If true, returns only the Gravatar URL instead of rendering the avatar component.
|
|
27
28
|
# @option props [String] :remove_class ('') Classes to be removed from the default Gravatar class list.
|
|
28
29
|
# @option props [Hash] **props Remaining options declared as HTML attributes, applied to the Gravatar container.
|
|
29
30
|
def initialize(**props)
|
|
@@ -37,10 +38,16 @@ class Fluxbit::GravatarComponent < Fluxbit::AvatarComponent
|
|
|
37
38
|
}
|
|
38
39
|
add class: gravatar_styles[:base], to: @props
|
|
39
40
|
@email = @props.delete(:email)
|
|
41
|
+
@url_only = @props.delete(:url_only)
|
|
40
42
|
src = gravatar_url
|
|
41
43
|
super(src: src, **@props)
|
|
42
44
|
end
|
|
43
45
|
|
|
46
|
+
def call
|
|
47
|
+
return gravatar_url.html_safe if @url_only
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
44
51
|
# The raw MD5 hash of the users' email. Gravatar is particularly tricky as
|
|
45
52
|
# it downcases all emails. This is really the guts of the module,
|
|
46
53
|
# everything else is just convenience.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fluxbit
|
|
4
|
+
module IconHelpers
|
|
5
|
+
def chevron_right(**props)
|
|
6
|
+
add to: props, class: "w-2.5 h-2.5", first_element: true
|
|
7
|
+
remove_class_from_props(props)
|
|
8
|
+
props["aria-hidden"] = "true"
|
|
9
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
10
|
+
props[:fill] = "none"
|
|
11
|
+
props[:viewBox] = "0 0 6 10"
|
|
12
|
+
stroke_width = props.delete(:stroke_width) || 2
|
|
13
|
+
|
|
14
|
+
tag.svg(**props) do
|
|
15
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m1 9 4-4-4-4")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def chevron_left(**props)
|
|
20
|
+
add to: props, class: "w-2.5 h-2.5", first_element: true
|
|
21
|
+
remove_class_from_props(props)
|
|
22
|
+
props["aria-hidden"] = "true"
|
|
23
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
24
|
+
props[:fill] = "none"
|
|
25
|
+
props[:viewBox] = "0 0 6 10"
|
|
26
|
+
stroke_width = props.delete(:stroke_width) || 2
|
|
27
|
+
|
|
28
|
+
tag.svg(**props) do
|
|
29
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m5 1-4 4 4 4")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def chevron_up(**props)
|
|
34
|
+
add to: props, class: "w-2.5 h-2.5", first_element: true
|
|
35
|
+
remove_class_from_props(props)
|
|
36
|
+
props["aria-hidden"] = "true"
|
|
37
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
38
|
+
props[:fill] = "none"
|
|
39
|
+
props[:viewBox] = "0 0 10 6"
|
|
40
|
+
stroke_width = props.delete(:stroke_width) || 2
|
|
41
|
+
|
|
42
|
+
tag.svg(**props) do
|
|
43
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m1 5 4-4 4 4")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def chevron_down(**props)
|
|
48
|
+
add to: props, class: "w-2.5 h-2.5", first_element: true
|
|
49
|
+
remove_class_from_props(props)
|
|
50
|
+
props["aria-hidden"] = "true"
|
|
51
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
52
|
+
props[:fill] = "none"
|
|
53
|
+
props[:viewBox] = "0 0 10 6"
|
|
54
|
+
stroke_width = props.delete(:stroke_width) || 2
|
|
55
|
+
|
|
56
|
+
tag.svg(**props) do
|
|
57
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m1 1 4 4 4-4")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def close_icon(**props)
|
|
62
|
+
props["aria-hidden"] = "true"
|
|
63
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
64
|
+
props[:fill] = "none"
|
|
65
|
+
props[:viewBox] = "0 0 14 14"
|
|
66
|
+
|
|
67
|
+
tag.svg(**props) do
|
|
68
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => 2, d: "m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def plus_icon(**props)
|
|
73
|
+
props["aria-hidden"] = "true"
|
|
74
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
75
|
+
props[:fill] = "currentColor"
|
|
76
|
+
props[:viewBox] = "0 0 24 24"
|
|
77
|
+
|
|
78
|
+
tag.svg(**props) do
|
|
79
|
+
tag.path("fill-rule" => "evenodd", d: "M12 3.75a.75.75 0 0 1 .75.75v6.75h6.75a.75.75 0 0 1 0 1.5h-6.75v6.75a.75.75 0 0 1-1.5 0v-6.75H4.5a.75.75 0 0 1 0-1.5h6.75V4.5a.75.75 0 0 1 .75-.75Z", "clip-rule" => "evenodd")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def chevron_double_left(**props)
|
|
84
|
+
add to: props, class: "w-3 h-2.5", first_element: true
|
|
85
|
+
remove_class_from_props(props)
|
|
86
|
+
props["aria-hidden"] = "true"
|
|
87
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
88
|
+
props[:fill] = "none"
|
|
89
|
+
props[:viewBox] = "0 0 10 10"
|
|
90
|
+
stroke_width = props.delete(:stroke_width) || 2
|
|
91
|
+
|
|
92
|
+
tag.svg(**props) do
|
|
93
|
+
safe_join [
|
|
94
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M4 1l-4 4 4 4"),
|
|
95
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M10 1l-4 4 4 4")
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def chevron_double_right(**props)
|
|
101
|
+
add to: props, class: "w-3 h-2.5", first_element: true
|
|
102
|
+
remove_class_from_props(props)
|
|
103
|
+
props["aria-hidden"] = "true"
|
|
104
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
105
|
+
props[:fill] = "none"
|
|
106
|
+
props[:viewBox] = "0 0 12 10"
|
|
107
|
+
stroke_width = props.delete(:stroke_width) || 2
|
|
108
|
+
|
|
109
|
+
tag.svg(**props) do
|
|
110
|
+
safe_join [
|
|
111
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M2 1l4 4-4 4"),
|
|
112
|
+
tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M8 1l4 4-4 4")
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ellipsis_horizontal(**props)
|
|
118
|
+
add to: props, class: "w-2.5 h-2.5", first_element: true
|
|
119
|
+
remove_class_from_props(props)
|
|
120
|
+
props["aria-hidden"] = "true"
|
|
121
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
122
|
+
props[:fill] = "currentColor"
|
|
123
|
+
props[:viewBox] = "0 0 10 2"
|
|
124
|
+
|
|
125
|
+
tag.svg(**props) do
|
|
126
|
+
safe_join [
|
|
127
|
+
tag.circle(cx: 1, cy: 1, r: 1),
|
|
128
|
+
tag.circle(cx: 5, cy: 1, r: 1),
|
|
129
|
+
tag.circle(cx: 9, cy: 1, r: 1)
|
|
130
|
+
]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def eye_icon(**props)
|
|
135
|
+
add to: props, class: "size-4", first_element: true
|
|
136
|
+
remove_class_from_props(props)
|
|
137
|
+
props["aria-hidden"] = "true"
|
|
138
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
139
|
+
props[:fill] = "none"
|
|
140
|
+
props[:viewBox] = "0 0 24 24"
|
|
141
|
+
props[:"stroke-width"] = "1.5"
|
|
142
|
+
props[:stroke] = "currentColor"
|
|
143
|
+
|
|
144
|
+
tag.svg(**props) do
|
|
145
|
+
safe_join [
|
|
146
|
+
tag.path("stroke-linecap" => "round", "stroke-linejoin" => "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"),
|
|
147
|
+
tag.path("stroke-linecap" => "round", "stroke-linejoin" => "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z")
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def eye_slash_icon(**props)
|
|
153
|
+
add to: props, class: "size-4", first_element: true
|
|
154
|
+
remove_class_from_props(props)
|
|
155
|
+
props["aria-hidden"] = "true"
|
|
156
|
+
props[:xmlns] = "http://www.w3.org/2000/svg"
|
|
157
|
+
props[:fill] = "none"
|
|
158
|
+
props[:viewBox] = "0 0 24 24"
|
|
159
|
+
props[:"stroke-width"] = "1.5"
|
|
160
|
+
props[:stroke] = "currentColor"
|
|
161
|
+
|
|
162
|
+
tag.svg(**props) do
|
|
163
|
+
tag.path("stroke-linecap" => "round", "stroke-linejoin" => "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
##
|
|
5
|
+
# The `Fluxbit::LinkComponent` is a customizable link component that extends {Fluxbit::Component}.
|
|
6
|
+
# It provides a straightforward way to generate anchor elements (`<a>`) with configurable colors
|
|
7
|
+
# and styling options. Additional HTML attributes can be passed to further control the component's
|
|
8
|
+
# behavior and appearance.
|
|
9
|
+
#
|
|
10
|
+
# Example usage:
|
|
11
|
+
# = render Fluxbit::LinkComponent.new(size: 2, spacing: :wider, line_height: :relaxed) do
|
|
12
|
+
# "My Heading"
|
|
13
|
+
#
|
|
14
|
+
class Fluxbit::LinkComponent < Fluxbit::Component
|
|
15
|
+
include Fluxbit::Config::LinkComponent
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Initializes the link component with the provided options.
|
|
19
|
+
#
|
|
20
|
+
# @param [Symbol] color (:default) The color style for the heading. Must be one of the keys defined in +styles[:colors]+.
|
|
21
|
+
# @param [Hash] props Additional HTML attributes to be applied to the heading element, such as +class+, +id+, +data-*, etc.
|
|
22
|
+
#
|
|
23
|
+
# @return [Fluxbit::LinkComponent]
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# = render Fluxbit::LinkComponent.new(color: :primary) do
|
|
27
|
+
# "My Heading"
|
|
28
|
+
#
|
|
29
|
+
def initialize(**props)
|
|
30
|
+
super
|
|
31
|
+
@props = props
|
|
32
|
+
@color = @props.delete(:color) || @@color
|
|
33
|
+
|
|
34
|
+
add to: @props, class: [ styles[:colors][@color], styles[:base] ]
|
|
35
|
+
@props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
|
|
36
|
+
@href = @props.delete(:href) || "#"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call
|
|
40
|
+
link_to content, @href, **@props
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -23,10 +23,10 @@ class Fluxbit::ModalComponent < Fluxbit::Component
|
|
|
23
23
|
# @option props [Boolean] :only_css (false) Determines if the modal can be closed by clicking the backdrop, using a CSS-based approach.
|
|
24
24
|
# @option props [Boolean] :static (false) If true, the modal will not close when clicking the backdrop or pressing the ESC key.
|
|
25
25
|
# @option props [String] :remove_class ('') Classes to be removed from the default modal class list.
|
|
26
|
-
# @option props [Hash] :
|
|
27
|
-
# @option props [Hash] :
|
|
28
|
-
# @option props [Hash] :
|
|
29
|
-
# @option props [Hash] :
|
|
26
|
+
# @option props [Hash] :content_html ({}) Additional HTML attributes and classes for the content wrapper inside the modal.
|
|
27
|
+
# @option props [Hash] :header_html ({}) Additional HTML attributes and classes for the header section.
|
|
28
|
+
# @option props [Hash] :footer_html ({}) Additional HTML attributes and classes for the footer section.
|
|
29
|
+
# @option props [Hash] :close_button_html ({}) Additional HTML attributes and classes for the close button element.
|
|
30
30
|
# @option props [Hash] **props Remaining options declared as HTML attributes, applied to the modal container.
|
|
31
31
|
def initialize(**props)
|
|
32
32
|
super
|
|
@@ -50,39 +50,35 @@ class Fluxbit::ModalComponent < Fluxbit::Component
|
|
|
50
50
|
@props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
|
|
51
51
|
|
|
52
52
|
# Content properties
|
|
53
|
-
@
|
|
54
|
-
add(class: content_classes, to: @
|
|
55
|
-
@
|
|
53
|
+
@content_html = @props.delete(:content_html) || {}
|
|
54
|
+
add(class: content_classes, to: @content_html, first_element: true)
|
|
55
|
+
@content_html[:class] = remove_class(@content_html.delete(:remove_class) || "", @content_html[:class])
|
|
56
56
|
|
|
57
57
|
# Header properties
|
|
58
|
-
@
|
|
59
|
-
add(class: header_classes, to: @
|
|
60
|
-
@
|
|
58
|
+
@header_html = @props.delete(:header_html) || {}
|
|
59
|
+
add(class: header_classes, to: @header_html, first_element: true)
|
|
60
|
+
@header_html[:class] = remove_class(@header_html.delete(:remove_class) || "", @header_html[:class])
|
|
61
61
|
|
|
62
62
|
# Footer properties
|
|
63
|
-
@
|
|
64
|
-
add(class: footer_classes, to: @
|
|
65
|
-
@
|
|
63
|
+
@footer_html = @props.delete(:footer_html) || {}
|
|
64
|
+
add(class: footer_classes, to: @footer_html, first_element: true)
|
|
65
|
+
@footer_html[:class] = remove_class(@footer_html.delete(:remove_class) || "", @footer_html[:class])
|
|
66
66
|
|
|
67
67
|
# Close button properties
|
|
68
|
-
@
|
|
69
|
-
add(class: styles[:header][:close][:base], to: @
|
|
70
|
-
@
|
|
71
|
-
@
|
|
72
|
-
@
|
|
73
|
-
@close_button_props["aria-label"] = "Close"
|
|
68
|
+
@close_button_html = @props.delete(:close_button_html) || {}
|
|
69
|
+
add(class: styles[:header][:close][:base], to: @close_button_html, first_element: true)
|
|
70
|
+
@close_button_html[:class] = remove_class(@close_button_html.delete(:remove_class) || "", @close_button_html[:class])
|
|
71
|
+
@close_button_html[:type] = "button"
|
|
72
|
+
@close_button_html["data-modal-hide"] = @props[:id]
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
def call
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
) do
|
|
81
|
-
content_tag(:div, **@content_props) do
|
|
82
|
-
content_tag(:div, class: styles[:content][:inner]) do
|
|
76
|
+
tag.div(**@props) do
|
|
77
|
+
tag.div(**@content_html) do
|
|
78
|
+
tag.div(class: styles[:content][:inner]) do
|
|
83
79
|
concat(header) if title? || @title.present? || @close_button
|
|
84
|
-
concat(
|
|
85
|
-
concat(
|
|
80
|
+
concat(tag.div(content, class: body_classes))
|
|
81
|
+
concat(tag.div(footer, **@footer_html)) if footer?
|
|
86
82
|
end
|
|
87
83
|
end
|
|
88
84
|
end
|
|
@@ -114,7 +110,7 @@ class Fluxbit::ModalComponent < Fluxbit::Component
|
|
|
114
110
|
def header
|
|
115
111
|
return close_button if @close_button && !title? && !@title.present?
|
|
116
112
|
|
|
117
|
-
content_tag(:div, **@
|
|
113
|
+
content_tag(:div, **@header_html) do
|
|
118
114
|
concat(title) if title?
|
|
119
115
|
concat(content_tag(:h3, @title, class: styles[:header][:title])) if @title.present?
|
|
120
116
|
concat(close_button) if @close_button
|
|
@@ -126,12 +122,13 @@ class Fluxbit::ModalComponent < Fluxbit::Component
|
|
|
126
122
|
end
|
|
127
123
|
|
|
128
124
|
def close_button
|
|
125
|
+
@close_button_html["aria-label"] = t("fluxbit.modal.aria_close")
|
|
129
126
|
content_tag(
|
|
130
127
|
:button,
|
|
131
|
-
**@
|
|
128
|
+
**@close_button_html
|
|
132
129
|
) do
|
|
133
|
-
concat content_tag(:span, "
|
|
134
|
-
concat
|
|
130
|
+
concat content_tag(:span, t("fluxbit.modal.dismiss"), class: "sr-only")
|
|
131
|
+
concat close_icon(class: "size-3")
|
|
135
132
|
end
|
|
136
133
|
end
|
|
137
134
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# The `Fluxbit::PaginationComponent` is a component for rendering customizable pagination controls.
|
|
2
|
+
# It extends `Fluxbit::Component` and provides options for configuring the pagination's
|
|
3
|
+
# appearance, behavior, and content areas. You can control the pagination's layout, item count,
|
|
4
|
+
# and other interactive elements. The pagination is divided into different sections (previous, next, etc.),
|
|
5
|
+
# each of which can be styled or customized through various properties.
|
|
6
|
+
class Fluxbit::PaginationComponent < Fluxbit::Component
|
|
7
|
+
include Fluxbit::Config::PaginationComponent
|
|
8
|
+
|
|
9
|
+
def initialize(pagy = nil, **props)
|
|
10
|
+
@pagy = pagy
|
|
11
|
+
|
|
12
|
+
@props = props
|
|
13
|
+
@count = @props.delete(:count) || 0
|
|
14
|
+
@last = @props.delete(:last) || 1
|
|
15
|
+
@next = @props.delete(:next)
|
|
16
|
+
@page = @props.delete(:page) || 1
|
|
17
|
+
@prev = @props.delete(:prev)
|
|
18
|
+
@size = @props.delete(:size) || :default
|
|
19
|
+
@ends = @props.delete(:ends) || true
|
|
20
|
+
@request_path = @props.delete(:request_path) || nil
|
|
21
|
+
|
|
22
|
+
if @pagy
|
|
23
|
+
@count = @pagy.count
|
|
24
|
+
@last = @pagy.last
|
|
25
|
+
@next = @pagy.next
|
|
26
|
+
@page = @pagy.page
|
|
27
|
+
@prev = @pagy.prev
|
|
28
|
+
@size = @pagy.vars[:size]
|
|
29
|
+
@ends = @pagy.vars[:ends]
|
|
30
|
+
@request_path = @pagy.vars[:request_path]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
unless @size.is_a?(Integer) && @size >= 0
|
|
34
|
+
raise ArgumentError, "expected :size to be an Integer >= 0, got #{@size.inspect} (#{@size.class})"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@show_first_last = options @props.delete(:show_first_last), default: @@show_first_last
|
|
38
|
+
@show_prev_next = options @props.delete(:show_prev_next), default: @@show_prev_next
|
|
39
|
+
@show_pages = options @props.delete(:show_pages), default: @@show_pages
|
|
40
|
+
@show_icons = options @props.delete(:show_icons), default: @@show_icons
|
|
41
|
+
@show_texts = options @props.delete(:show_texts), default: @@show_texts
|
|
42
|
+
@sizing = options @props.delete(:sizing), default: @@sizing
|
|
43
|
+
@aria_label = @props.delete(:aria_label) || translate("aria_label.nav", count: @last)
|
|
44
|
+
@show_texts = true if !@show_icons && !@show_texts
|
|
45
|
+
|
|
46
|
+
add(class: [ styles[:root], styles[:sizes][@sizing][:root] ], to: @props)
|
|
47
|
+
@page_link_style = [ styles[:page_link], styles[:sizes][@sizing][:page_link] ].join(" ")
|
|
48
|
+
@current_style = [ styles[:current], styles[:sizes][@sizing][:page_link] ].join(" ")
|
|
49
|
+
@props[:aria] ||= {}
|
|
50
|
+
@props[:aria][:label] = @aria_label unless @props[:aria][:label]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def call
|
|
54
|
+
tag.nav(**@props) do
|
|
55
|
+
concat first_button if @show_first_last
|
|
56
|
+
concat prev_button if @show_prev_next
|
|
57
|
+
|
|
58
|
+
if @show_pages
|
|
59
|
+
series.each do |item|
|
|
60
|
+
case item
|
|
61
|
+
when Integer
|
|
62
|
+
concat(tag.a(item.to_s, href: url_for(item), role: "link", class: @page_link_style, aria: { label: item.to_s }))
|
|
63
|
+
when String
|
|
64
|
+
concat(tag.a(item.to_s, role: "link", class: @current_style, aria: { disabled: true, current: "page" }))
|
|
65
|
+
when :gap
|
|
66
|
+
concat(tag.a(ellipsis_horizontal, role: "link", class: @page_link_style, aria: { disabled: true }))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
concat next_button if @show_prev_next
|
|
72
|
+
concat last_button if @show_first_last
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def translate(key, options = {})
|
|
79
|
+
I18n.t(key, **options.merge(scope: "fluxbit.pagination")) # , default: Fluxbit::DEFAULT_TRANSLATIONS["fluxbit.pagination.#{key}"]))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def first_button
|
|
83
|
+
props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.first") } }
|
|
84
|
+
if @page != 1
|
|
85
|
+
props[:href] = url_for(1)
|
|
86
|
+
else
|
|
87
|
+
props[:aria][:disabled] = true
|
|
88
|
+
add class: styles[:disabled], to: props, first_element: true
|
|
89
|
+
end
|
|
90
|
+
add class: styles[:previous], to: props
|
|
91
|
+
|
|
92
|
+
tag.a(**props) do
|
|
93
|
+
concat(chevron_double_left) if @show_icons
|
|
94
|
+
concat(tag.span(
|
|
95
|
+
translate("first"),
|
|
96
|
+
class: @show_texts ? (@show_icons ? styles[:text_with_icon_prev] : styles[:only_text]) : styles[:only_icon]
|
|
97
|
+
))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def last_button
|
|
102
|
+
props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.last") } }
|
|
103
|
+
if @page != @last
|
|
104
|
+
props[:href] = url_for(@last)
|
|
105
|
+
else
|
|
106
|
+
props[:aria][:disabled] = true
|
|
107
|
+
add class: styles[:disabled], to: props, first_element: true
|
|
108
|
+
end
|
|
109
|
+
add class: styles[:next], to: props
|
|
110
|
+
|
|
111
|
+
tag.a(**props) do
|
|
112
|
+
concat(tag.span(
|
|
113
|
+
translate("last"),
|
|
114
|
+
class: @show_texts ? (@show_icons ? styles[:text_with_icon_next] : styles[:only_text]) : styles[:only_icon]
|
|
115
|
+
))
|
|
116
|
+
concat(chevron_double_right) if @show_icons
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def prev_button
|
|
121
|
+
props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.prev") } }
|
|
122
|
+
if prev_page = @prev
|
|
123
|
+
props[:href] = url_for(prev_page)
|
|
124
|
+
else
|
|
125
|
+
props[:aria][:disabled] = true
|
|
126
|
+
add class: styles[:disabled], to: props, first_element: true
|
|
127
|
+
end
|
|
128
|
+
add(class: styles[:previous], to: props) unless @show_first_last
|
|
129
|
+
|
|
130
|
+
tag.a(**props) do
|
|
131
|
+
concat(chevron_left) if @show_icons
|
|
132
|
+
concat(tag.span(
|
|
133
|
+
translate("prev"),
|
|
134
|
+
class: @show_texts ? (@show_icons ? styles[:text_with_icon_prev] : styles[:only_text]) : styles[:only_icon]
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def next_button
|
|
141
|
+
props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.next") } }
|
|
142
|
+
if next_page = @next
|
|
143
|
+
props[:href] = url_for(next_page)
|
|
144
|
+
else
|
|
145
|
+
props[:aria][:disabled] = true
|
|
146
|
+
add class: styles[:disabled], to: props, first_element: true
|
|
147
|
+
end
|
|
148
|
+
add(class: styles[:next], to: props) unless @show_first_last
|
|
149
|
+
|
|
150
|
+
tag.a(**props) do
|
|
151
|
+
concat(tag.span(
|
|
152
|
+
translate("next"),
|
|
153
|
+
class: @show_texts ? (@show_icons ? styles[:text_with_icon_next] : styles[:only_text]) : styles[:only_icon]
|
|
154
|
+
))
|
|
155
|
+
concat(chevron_right) if @show_icons
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def series
|
|
160
|
+
return @pagy.series(size: @size) if @pagy && @pagy.respond_to?(:series, true)
|
|
161
|
+
return [] if @size.zero?
|
|
162
|
+
|
|
163
|
+
[].tap do |series|
|
|
164
|
+
if @size >= @last
|
|
165
|
+
series.push(*1..@last)
|
|
166
|
+
else
|
|
167
|
+
left = ((@size - 1) / 2.0).floor # left half might be 1 page shorter for even size
|
|
168
|
+
start = if @page <= left # beginning pages
|
|
169
|
+
1
|
|
170
|
+
elsif @page > (@last - @size + left) # end pages
|
|
171
|
+
@last - @size + 1
|
|
172
|
+
else # intermediate pages
|
|
173
|
+
@page - left
|
|
174
|
+
end
|
|
175
|
+
series.push(*start...start + @size)
|
|
176
|
+
# Set first and last pages plus gaps when needed, respecting the size
|
|
177
|
+
if @ends && @size >= 7
|
|
178
|
+
series[0] = 1
|
|
179
|
+
series[1] = :gap unless series[1] == 2
|
|
180
|
+
series[-2] = :gap unless series[-2] == @last - 1
|
|
181
|
+
series[-1] = @last
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
series[series.index(@page)] = @page.to_s
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def url_for(page)
|
|
189
|
+
vars = @pagy&.vars || {}
|
|
190
|
+
# Use current request parameters as base
|
|
191
|
+
params = (respond_to?(:request) ? request.GET : controller.request.GET).dup
|
|
192
|
+
params.merge!(vars[:params].transform_keys(&:to_s)) if vars[:params].is_a?(Hash)
|
|
193
|
+
# Set page and possibly limit
|
|
194
|
+
params[vars[:page_param].to_s] = page
|
|
195
|
+
params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
|
|
196
|
+
# Apply params proc if given
|
|
197
|
+
params = vars[:params].call(params) if vars[:params].is_a?(Proc)
|
|
198
|
+
|
|
199
|
+
# Build query string
|
|
200
|
+
query_str = params.any? ? "?#{Rack::Utils.build_nested_query(params)}" : ""
|
|
201
|
+
# Base path (use stored request_path or current path)
|
|
202
|
+
base_path = @request_path || (respond_to?(:request) ? request.path : controller.request.path)
|
|
203
|
+
base_path = "#{request.base_url}#{base_path}" if vars[:absolute]
|
|
204
|
+
"#{base_path}#{query_str}#{vars[:fragment] || ''}"
|
|
205
|
+
end
|
|
206
|
+
end
|