fluxbit_view_components 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/fluxbit_view_components/index.js +4 -0
- data/app/assets/javascripts/fluxbit_view_components/telephone_controller.js +98 -0
- data/app/assets/javascripts/fluxbit_view_components.js +90 -1
- data/app/components/fluxbit/accordion_component.rb +4 -1
- data/app/components/fluxbit/form/telephone_component.rb +197 -0
- data/app/components/fluxbit/form/text_field_component.rb +1 -1
- data/app/helpers/fluxbit/form_builder.rb +1 -1
- data/lib/fluxbit/config/form/telephone_component.rb +72 -0
- data/lib/fluxbit/view_components/version.rb +1 -1
- data/lib/fluxbit/view_components.rb +1 -0
- data/lib/generators/fluxbit/scaffold_generator.rb +3 -0
- data/lib/generators/fluxbit/templates/_flash.html.erb.tt +8 -6
- data/lib/generators/fluxbit/templates/controller.rb.tt +3 -2
- data/lib/generators/fluxbit/templates/i18n.general.en.yml.tt +4 -0
- data/lib/generators/fluxbit/templates/i18n.general.pt-BR.yml.tt +0 -0
- data/lib/generators/fluxbit/templates/sortable.rb.tt +23 -0
- metadata +8 -2
- /data/lib/generators/fluxbit/templates/{send_alert_via_drawer.erb.tt → send_alert_via_drawer.html.erb.tt} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ff7c36152723efea00fcb464ac64ca6462b5f2a0f80cd7eab5298f5129bab1a
|
|
4
|
+
data.tar.gz: aa51123027b44b8e2a268275c15ed192bcb8bc111f5ec82d1d9ad74597b3f0c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2051c7d64576f3a0f8996475e184b164adc767241c52e33837472b7a28f011a2fe5e67fc86ad122599a7679dd252a019834489a795fae7b4bb558f4a444200d5
|
|
7
|
+
data.tar.gz: 5d62cc33db45e466341d46fbef8c35b128c4c16d8311a66ec0052a7133e4d7a344527577c66df08a609a48fa142e051a952dda15f041bada8c2ecc54919706a0
|
|
@@ -8,6 +8,7 @@ import FxProgress from './progress_controller'
|
|
|
8
8
|
import FxRowClick from './row_click_controller'
|
|
9
9
|
import FxSelectAll from './select_all_controller'
|
|
10
10
|
import FxSpinnerPercent from './spinner_percent_controller'
|
|
11
|
+
import FxTelephone from './telephone_controller'
|
|
11
12
|
import FxThemeButton from './theme_button_controller'
|
|
12
13
|
|
|
13
14
|
export {
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
FxRowClick,
|
|
22
23
|
FxSelectAll,
|
|
23
24
|
FxSpinnerPercent,
|
|
25
|
+
FxTelephone,
|
|
24
26
|
FxThemeButton
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -35,6 +37,7 @@ export function registerFluxbitControllers(application) {
|
|
|
35
37
|
application.register('fx-row-click', FxRowClick)
|
|
36
38
|
application.register('fx-select-all', FxSelectAll)
|
|
37
39
|
application.register('fx-spinner-percent', FxSpinnerPercent)
|
|
40
|
+
application.register('fx-telephone', FxTelephone)
|
|
38
41
|
application.register('fx-theme-button', FxThemeButton)
|
|
39
42
|
|
|
40
43
|
// Make controllers globally accessible for vanilla JS
|
|
@@ -50,6 +53,7 @@ export function registerFluxbitControllers(application) {
|
|
|
50
53
|
FxRowClick,
|
|
51
54
|
FxSelectAll,
|
|
52
55
|
FxSpinnerPercent,
|
|
56
|
+
FxTelephone,
|
|
53
57
|
FxThemeButton
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["countrySelect"]
|
|
5
|
+
static values = {
|
|
6
|
+
mask: { type: String, default: "(##) #####-####" }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.input = this.element.querySelector('input[type="tel"]')
|
|
11
|
+
if (this.input) {
|
|
12
|
+
this.input.addEventListener('input', this.applyMask.bind(this))
|
|
13
|
+
this.input.addEventListener('keydown', this.handleBackspace.bind(this))
|
|
14
|
+
|
|
15
|
+
// Apply mask to existing value on load
|
|
16
|
+
if (this.input.value) {
|
|
17
|
+
this.applyMask({ target: this.input })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnect() {
|
|
23
|
+
if (this.input) {
|
|
24
|
+
this.input.removeEventListener('input', this.applyMask.bind(this))
|
|
25
|
+
this.input.removeEventListener('keydown', this.handleBackspace.bind(this))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
updateMask(event) {
|
|
30
|
+
const selectedOption = event.target.options[event.target.selectedIndex]
|
|
31
|
+
const newMask = selectedOption.dataset.mask
|
|
32
|
+
|
|
33
|
+
if (newMask) {
|
|
34
|
+
this.maskValue = newMask
|
|
35
|
+
// Clear the current value and reapply mask
|
|
36
|
+
const currentValue = this.input.value
|
|
37
|
+
this.input.value = ''
|
|
38
|
+
this.input.value = this.getCleanValue(currentValue)
|
|
39
|
+
this.applyMask({ target: this.input })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handleBackspace(event) {
|
|
44
|
+
if (event.key === 'Backspace' || event.keyCode === 8) {
|
|
45
|
+
const cursorPosition = this.input.selectionStart
|
|
46
|
+
const value = this.input.value
|
|
47
|
+
|
|
48
|
+
// Check if the character before cursor is a mask character
|
|
49
|
+
if (cursorPosition > 0) {
|
|
50
|
+
const charBefore = value.charAt(cursorPosition - 1)
|
|
51
|
+
if (this.isMaskCharacter(charBefore)) {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
// Find the previous digit and remove it
|
|
54
|
+
let newPosition = cursorPosition - 1
|
|
55
|
+
while (newPosition > 0 && this.isMaskCharacter(value.charAt(newPosition))) {
|
|
56
|
+
newPosition--
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (newPosition >= 0) {
|
|
60
|
+
const cleanValue = this.getCleanValue(value.substring(0, newPosition) + value.substring(cursorPosition))
|
|
61
|
+
this.input.value = cleanValue
|
|
62
|
+
this.applyMask({ target: this.input })
|
|
63
|
+
this.input.setSelectionRange(newPosition, newPosition)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
applyMask(event) {
|
|
71
|
+
const input = event.target
|
|
72
|
+
let value = this.getCleanValue(input.value)
|
|
73
|
+
let maskedValue = ''
|
|
74
|
+
let valueIndex = 0
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < this.maskValue.length && valueIndex < value.length; i++) {
|
|
77
|
+
const maskChar = this.maskValue.charAt(i)
|
|
78
|
+
|
|
79
|
+
if (maskChar === '#') {
|
|
80
|
+
maskedValue += value.charAt(valueIndex)
|
|
81
|
+
valueIndex++
|
|
82
|
+
} else {
|
|
83
|
+
maskedValue += maskChar
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
input.value = maskedValue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getCleanValue(value) {
|
|
91
|
+
// Remove all non-numeric characters
|
|
92
|
+
return value.replace(/\D/g, '')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isMaskCharacter(char) {
|
|
96
|
+
return /[\s\-\(\)\/\.]/.test(char)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1067,6 +1067,93 @@ class FxSpinnerPercent extends Controller {
|
|
|
1067
1067
|
}
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
|
+
class FxTelephone extends Controller {
|
|
1071
|
+
static targets=[ "countrySelect" ];
|
|
1072
|
+
static values={
|
|
1073
|
+
mask: {
|
|
1074
|
+
type: String,
|
|
1075
|
+
default: "(##) #####-####"
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
connect() {
|
|
1079
|
+
this.input = this.element.querySelector('input[type="tel"]');
|
|
1080
|
+
if (this.input) {
|
|
1081
|
+
this.input.addEventListener("input", this.applyMask.bind(this));
|
|
1082
|
+
this.input.addEventListener("keydown", this.handleBackspace.bind(this));
|
|
1083
|
+
if (this.input.value) {
|
|
1084
|
+
this.applyMask({
|
|
1085
|
+
target: this.input
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
disconnect() {
|
|
1091
|
+
if (this.input) {
|
|
1092
|
+
this.input.removeEventListener("input", this.applyMask.bind(this));
|
|
1093
|
+
this.input.removeEventListener("keydown", this.handleBackspace.bind(this));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
updateMask(event) {
|
|
1097
|
+
const selectedOption = event.target.options[event.target.selectedIndex];
|
|
1098
|
+
const newMask = selectedOption.dataset.mask;
|
|
1099
|
+
if (newMask) {
|
|
1100
|
+
this.maskValue = newMask;
|
|
1101
|
+
const currentValue = this.input.value;
|
|
1102
|
+
this.input.value = "";
|
|
1103
|
+
this.input.value = this.getCleanValue(currentValue);
|
|
1104
|
+
this.applyMask({
|
|
1105
|
+
target: this.input
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
handleBackspace(event) {
|
|
1110
|
+
if (event.key === "Backspace" || event.keyCode === 8) {
|
|
1111
|
+
const cursorPosition = this.input.selectionStart;
|
|
1112
|
+
const value = this.input.value;
|
|
1113
|
+
if (cursorPosition > 0) {
|
|
1114
|
+
const charBefore = value.charAt(cursorPosition - 1);
|
|
1115
|
+
if (this.isMaskCharacter(charBefore)) {
|
|
1116
|
+
event.preventDefault();
|
|
1117
|
+
let newPosition = cursorPosition - 1;
|
|
1118
|
+
while (newPosition > 0 && this.isMaskCharacter(value.charAt(newPosition))) {
|
|
1119
|
+
newPosition--;
|
|
1120
|
+
}
|
|
1121
|
+
if (newPosition >= 0) {
|
|
1122
|
+
const cleanValue = this.getCleanValue(value.substring(0, newPosition) + value.substring(cursorPosition));
|
|
1123
|
+
this.input.value = cleanValue;
|
|
1124
|
+
this.applyMask({
|
|
1125
|
+
target: this.input
|
|
1126
|
+
});
|
|
1127
|
+
this.input.setSelectionRange(newPosition, newPosition);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
applyMask(event) {
|
|
1134
|
+
const input = event.target;
|
|
1135
|
+
let value = this.getCleanValue(input.value);
|
|
1136
|
+
let maskedValue = "";
|
|
1137
|
+
let valueIndex = 0;
|
|
1138
|
+
for (let i = 0; i < this.maskValue.length && valueIndex < value.length; i++) {
|
|
1139
|
+
const maskChar = this.maskValue.charAt(i);
|
|
1140
|
+
if (maskChar === "#") {
|
|
1141
|
+
maskedValue += value.charAt(valueIndex);
|
|
1142
|
+
valueIndex++;
|
|
1143
|
+
} else {
|
|
1144
|
+
maskedValue += maskChar;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
input.value = maskedValue;
|
|
1148
|
+
}
|
|
1149
|
+
getCleanValue(value) {
|
|
1150
|
+
return value.replace(/\D/g, "");
|
|
1151
|
+
}
|
|
1152
|
+
isMaskCharacter(char) {
|
|
1153
|
+
return /[\s\-\(\)\/\.]/.test(char);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1070
1157
|
class FxThemeButton extends Controller {
|
|
1071
1158
|
static targets=[ "lightIcon", "darkIcon", "systemIcon" ];
|
|
1072
1159
|
static values={
|
|
@@ -1154,6 +1241,7 @@ function registerFluxbitControllers(application) {
|
|
|
1154
1241
|
application.register("fx-row-click", FxRowClick);
|
|
1155
1242
|
application.register("fx-select-all", FxSelectAll);
|
|
1156
1243
|
application.register("fx-spinner-percent", FxSpinnerPercent);
|
|
1244
|
+
application.register("fx-telephone", FxTelephone);
|
|
1157
1245
|
application.register("fx-theme-button", FxThemeButton);
|
|
1158
1246
|
if (typeof window !== "undefined") {
|
|
1159
1247
|
window.FluxbitControllers = {
|
|
@@ -1167,9 +1255,10 @@ function registerFluxbitControllers(application) {
|
|
|
1167
1255
|
FxRowClick: FxRowClick,
|
|
1168
1256
|
FxSelectAll: FxSelectAll,
|
|
1169
1257
|
FxSpinnerPercent: FxSpinnerPercent,
|
|
1258
|
+
FxTelephone: FxTelephone,
|
|
1170
1259
|
FxThemeButton: FxThemeButton
|
|
1171
1260
|
};
|
|
1172
1261
|
}
|
|
1173
1262
|
}
|
|
1174
1263
|
|
|
1175
|
-
export { FxAssigner, FxAutoSubmit, FxDrawer, FxMethodLink, FxModal, FxPassword, FxProgress, FxRowClick, FxSelectAll, FxSpinnerPercent, FxThemeButton, registerFluxbitControllers };
|
|
1264
|
+
export { FxAssigner, FxAutoSubmit, FxDrawer, FxMethodLink, FxModal, FxPassword, FxProgress, FxRowClick, FxSelectAll, FxSpinnerPercent, FxTelephone, FxThemeButton, registerFluxbitControllers };
|
|
@@ -6,7 +6,10 @@ class Fluxbit::AccordionComponent < Fluxbit::Component
|
|
|
6
6
|
include Fluxbit::Config::AccordionComponent
|
|
7
7
|
|
|
8
8
|
renders_many :panels, lambda { |**attrs, &block|
|
|
9
|
+
@panel_index ||= 0
|
|
10
|
+
attrs[:index] = @panel_index unless attrs.key?(:index)
|
|
9
11
|
panel = Panel.new(accordion_id: fx_id, flush: @flush, color: @color, **attrs)
|
|
12
|
+
@panel_index += 1
|
|
10
13
|
block.call(panel) if block_given?
|
|
11
14
|
panel
|
|
12
15
|
}
|
|
@@ -58,7 +61,7 @@ class Fluxbit::AccordionComponent < Fluxbit::Component
|
|
|
58
61
|
# @param [Boolean] flush (false) Whether the panel should use flush styling.
|
|
59
62
|
# @param [Symbol, String] color (:default) The color theme for this panel.
|
|
60
63
|
# @param [Boolean] open (false) Whether the panel should start in an expanded state.
|
|
61
|
-
# @param [Integer] index
|
|
64
|
+
# @param [Integer] index The panel's position index for proper styling (first, middle, last). Automatically increments for each panel if not specified.
|
|
62
65
|
# @param [Hash] **props Additional HTML attributes for the panel container.
|
|
63
66
|
#
|
|
64
67
|
# @return [Fluxbit::AccordionComponent::Panel]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The `Fluxbit::Form::TelephoneComponent` is a telephone input component that extends `Fluxbit::Form::TextFieldComponent`.
|
|
4
|
+
# It provides a styled telephone input with an integrated country code selector showing country flags and dialing codes.
|
|
5
|
+
# The input includes automatic masking for phone numbers.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# = render Fluxbit::Form::TelephoneComponent.new(name: :phone)
|
|
9
|
+
#
|
|
10
|
+
# @example With default country
|
|
11
|
+
# = render Fluxbit::Form::TelephoneComponent.new(name: :phone, default_country: "BR")
|
|
12
|
+
#
|
|
13
|
+
class Fluxbit::Form::TelephoneComponent < Fluxbit::Form::TextFieldComponent
|
|
14
|
+
include Fluxbit::Config::Form::TelephoneComponent
|
|
15
|
+
|
|
16
|
+
# Initializes the telephone component with the given properties.
|
|
17
|
+
#
|
|
18
|
+
# @param default_country [String] The default country code (ISO 3166-1 alpha-2, e.g., "BR", "US")
|
|
19
|
+
# @param country_field_name [String] Name for the hidden country code field (optional, deprecated in favor of :country)
|
|
20
|
+
# @param country [Symbol] Attribute name for the country field when using form builder (e.g., :phone_country)
|
|
21
|
+
# @param ... all other parameters from TextFieldComponent
|
|
22
|
+
def initialize(**props)
|
|
23
|
+
@default_country = props.delete(:default_country) || @@default_country
|
|
24
|
+
@country_field_name = props.delete(:country_field_name)
|
|
25
|
+
@country_attribute = props.delete(:country)
|
|
26
|
+
|
|
27
|
+
# Set default sizing from config if not specified
|
|
28
|
+
props[:sizing] = @@default_sizing unless props.key?(:sizing)
|
|
29
|
+
|
|
30
|
+
# Force type to tel
|
|
31
|
+
props[:type] = :tel
|
|
32
|
+
|
|
33
|
+
super(**props)
|
|
34
|
+
|
|
35
|
+
# Override the input classes to match our custom sizing
|
|
36
|
+
override_input_sizing
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call
|
|
40
|
+
content_tag :div, **@wrapper_html do
|
|
41
|
+
safe_join [
|
|
42
|
+
label,
|
|
43
|
+
telephone_input_container,
|
|
44
|
+
help_text
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def override_input_sizing
|
|
52
|
+
# Remove the original size classes
|
|
53
|
+
current_classes = @props[:class].to_s
|
|
54
|
+
|
|
55
|
+
# Get size class from config
|
|
56
|
+
size_index = [@sizing, 0].max
|
|
57
|
+
size_index = [size_index, @@telephone_styles[:input][:sizes].length - 1].min
|
|
58
|
+
custom_size_class = @@telephone_styles[:input][:sizes][size_index]
|
|
59
|
+
|
|
60
|
+
# Remove the old size class and add our custom one
|
|
61
|
+
# First remove common padding/text classes that might conflict
|
|
62
|
+
current_classes = current_classes.gsub(/\bp-[\d.]+\b/, "")
|
|
63
|
+
.gsub(/\btext-(xs|sm|base|md|lg|xl)\b/, "")
|
|
64
|
+
.gsub(/\brounded-lg\b/, "")
|
|
65
|
+
.strip
|
|
66
|
+
|
|
67
|
+
@props[:class] = "#{current_classes} #{custom_size_class}".strip
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def current_country
|
|
71
|
+
@@telephone_countries.find { |c| c[:code] == @default_country } || @@telephone_countries.first
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def country_select
|
|
75
|
+
content_tag :div, class: "relative flex-shrink-0" do
|
|
76
|
+
if @form.present? && @country_attribute.present?
|
|
77
|
+
# Use form builder's select to get proper name attribute
|
|
78
|
+
@form.select(
|
|
79
|
+
@country_attribute,
|
|
80
|
+
options_for_select(
|
|
81
|
+
@@telephone_countries.map { |c| [ "#{c[:flag]} #{c[:dial_code]}", c[:code], { "data-dial-code": c[:dial_code], "data-mask": c[:mask] } ] },
|
|
82
|
+
country_select_value
|
|
83
|
+
),
|
|
84
|
+
{},
|
|
85
|
+
{
|
|
86
|
+
id: country_select_id,
|
|
87
|
+
class: country_select_classes,
|
|
88
|
+
data: {
|
|
89
|
+
fx_telephone_target: "countrySelect",
|
|
90
|
+
action: "change->fx-telephone#updateMask"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
else
|
|
95
|
+
# Standalone select (no form builder)
|
|
96
|
+
select_tag(
|
|
97
|
+
country_select_name,
|
|
98
|
+
country_options_html,
|
|
99
|
+
id: country_select_id,
|
|
100
|
+
class: country_select_classes,
|
|
101
|
+
data: {
|
|
102
|
+
fx_telephone_target: "countrySelect",
|
|
103
|
+
action: "change->fx-telephone#updateMask"
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def country_options_html
|
|
111
|
+
safe_join(
|
|
112
|
+
@@telephone_countries.map do |country|
|
|
113
|
+
content_tag :option,
|
|
114
|
+
"#{country[:flag]} #{country[:dial_code]}",
|
|
115
|
+
value: country[:code],
|
|
116
|
+
selected: country[:code] == country_select_value,
|
|
117
|
+
data: {
|
|
118
|
+
dial_code: country[:dial_code],
|
|
119
|
+
mask: country[:mask]
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def country_select_name
|
|
126
|
+
# Only used in standalone mode
|
|
127
|
+
@country_field_name || "#{@name}_country"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def country_select_value
|
|
131
|
+
# Priority:
|
|
132
|
+
# 1. Value from object attribute (when using form builder with country attribute)
|
|
133
|
+
# 2. Default country
|
|
134
|
+
if @form.present? && @country_attribute.present? && @object.present?
|
|
135
|
+
@object.public_send(@country_attribute) rescue @default_country
|
|
136
|
+
else
|
|
137
|
+
@default_country
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def country_select_id
|
|
142
|
+
"#{@props[:id] || @name}_country"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def country_select_classes
|
|
146
|
+
# Get size from config
|
|
147
|
+
size_index = [@sizing, 0].max
|
|
148
|
+
size_index = [size_index, @@telephone_styles[:country_select][:sizes].length - 1].min
|
|
149
|
+
size_config = @@telephone_styles[:country_select][:sizes][size_index]
|
|
150
|
+
|
|
151
|
+
# Get color from config
|
|
152
|
+
color = @color || :default
|
|
153
|
+
color_classes = @@telephone_styles[:country_select][:colors][color] || @@telephone_styles[:country_select][:colors][:default]
|
|
154
|
+
|
|
155
|
+
[
|
|
156
|
+
@@telephone_styles[:country_select][:base],
|
|
157
|
+
@@telephone_styles[:country_select][:width],
|
|
158
|
+
size_config[:padding],
|
|
159
|
+
size_config[:text],
|
|
160
|
+
color_classes
|
|
161
|
+
].join(" ")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def telephone_input
|
|
165
|
+
# Override the props to adjust styling for the telephone input
|
|
166
|
+
input_props = @props.dup
|
|
167
|
+
|
|
168
|
+
# Remove left border radius since it connects to the country select
|
|
169
|
+
current_classes = input_props[:class] || ""
|
|
170
|
+
input_props[:class] = current_classes.gsub("rounded-lg", "rounded-r-lg")
|
|
171
|
+
|
|
172
|
+
# Store original props temporarily
|
|
173
|
+
original_props = @props
|
|
174
|
+
@props = input_props
|
|
175
|
+
|
|
176
|
+
result = input
|
|
177
|
+
|
|
178
|
+
# Restore original props
|
|
179
|
+
@props = original_props
|
|
180
|
+
|
|
181
|
+
result
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def telephone_input_container
|
|
185
|
+
content_tag :div,
|
|
186
|
+
class: "flex w-full",
|
|
187
|
+
data: {
|
|
188
|
+
controller: "fx-telephone",
|
|
189
|
+
fx_telephone_mask_value: current_country[:mask]
|
|
190
|
+
} do
|
|
191
|
+
safe_join([
|
|
192
|
+
country_select,
|
|
193
|
+
content_tag(:div, telephone_input, class: "relative flex-1")
|
|
194
|
+
])
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -99,7 +99,7 @@ class Fluxbit::Form::TextFieldComponent < Fluxbit::Form::FieldComponent
|
|
|
99
99
|
first_element: true,
|
|
100
100
|
class: [
|
|
101
101
|
styles[:default],
|
|
102
|
-
(@props.key?(:readonly) || @props.key?(:disabled) ? styles[:text][@color] :
|
|
102
|
+
(@props.key?(:readonly) || @props.key?(:disabled) ? styles[:text][@color] : styles[:text][:default]),
|
|
103
103
|
styles[:ring][@color],
|
|
104
104
|
styles[:bg][@color],
|
|
105
105
|
styles[:placeholder][@color],
|
|
@@ -68,7 +68,7 @@ module Fluxbit
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
[ :range, :toggle, :upload_image, :dropzone, :password ].each do |component|
|
|
71
|
+
[ :range, :toggle, :upload_image, :dropzone, :password, :telephone ].each do |component|
|
|
72
72
|
define_method("fx_#{component}") do |method, **options, &block|
|
|
73
73
|
options[:error] ||= error_for(method)
|
|
74
74
|
options[:error] = !!options[:error] if options[:error_hidden] && options[:error]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fluxbit::Config::Form::TelephoneComponent
|
|
4
|
+
mattr_accessor :default_country, default: "BR"
|
|
5
|
+
mattr_accessor :default_sizing, default: 1
|
|
6
|
+
|
|
7
|
+
# rubocop: disable Layout/LineLength, Metrics/BlockLength
|
|
8
|
+
mattr_accessor :telephone_countries do
|
|
9
|
+
[
|
|
10
|
+
{ code: "BR", name: "Brasil", dial_code: "+55", flag: "🇧🇷", mask: "(##) #####-####" },
|
|
11
|
+
{ code: "AR", name: "Argentina", dial_code: "+54", flag: "🇦🇷", mask: "## ####-####" },
|
|
12
|
+
{ code: "MX", name: "México", dial_code: "+52", flag: "🇲🇽", mask: "## #### ####" },
|
|
13
|
+
{ code: "CO", name: "Colombia", dial_code: "+57", flag: "🇨🇴", mask: "### ### ####" },
|
|
14
|
+
{ code: "CL", name: "Chile", dial_code: "+56", flag: "🇨🇱", mask: "# #### ####" },
|
|
15
|
+
{ code: "PE", name: "Perú", dial_code: "+51", flag: "🇵🇪", mask: "### ### ###" },
|
|
16
|
+
{ code: "VE", name: "Venezuela", dial_code: "+58", flag: "🇻🇪", mask: "###-###-####" },
|
|
17
|
+
{ code: "EC", name: "Ecuador", dial_code: "+593", flag: "🇪🇨", mask: "## ### ####" },
|
|
18
|
+
{ code: "BO", name: "Bolivia", dial_code: "+591", flag: "🇧🇴", mask: "# ### ####" },
|
|
19
|
+
{ code: "PY", name: "Paraguay", dial_code: "+595", flag: "🇵🇾", mask: "### ### ###" },
|
|
20
|
+
{ code: "UY", name: "Uruguay", dial_code: "+598", flag: "🇺🇾", mask: "# ### ## ##" },
|
|
21
|
+
{ code: "CR", name: "Costa Rica", dial_code: "+506", flag: "🇨🇷", mask: "#### ####" },
|
|
22
|
+
{ code: "PA", name: "Panamá", dial_code: "+507", flag: "🇵🇦", mask: "####-####" },
|
|
23
|
+
{ code: "CU", name: "Cuba", dial_code: "+53", flag: "🇨🇺", mask: "# ### ####" },
|
|
24
|
+
{ code: "DO", name: "República Dominicana", dial_code: "+1", flag: "🇩🇴", mask: "(###) ###-####" },
|
|
25
|
+
{ code: "GT", name: "Guatemala", dial_code: "+502", flag: "🇬🇹", mask: "#### ####" },
|
|
26
|
+
{ code: "HN", name: "Honduras", dial_code: "+504", flag: "🇭🇳", mask: "####-####" },
|
|
27
|
+
{ code: "SV", name: "El Salvador", dial_code: "+503", flag: "🇸🇻", mask: "####-####" },
|
|
28
|
+
{ code: "NI", name: "Nicaragua", dial_code: "+505", flag: "🇳🇮", mask: "#### ####" },
|
|
29
|
+
{ code: "US", name: "United States", dial_code: "+1", flag: "🇺🇸", mask: "(###) ###-####" },
|
|
30
|
+
{ code: "CA", name: "Canada", dial_code: "+1", flag: "🇨🇦", mask: "(###) ###-####" },
|
|
31
|
+
{ code: "ES", name: "España", dial_code: "+34", flag: "🇪🇸", mask: "### ## ## ##" },
|
|
32
|
+
{ code: "PT", name: "Portugal", dial_code: "+351", flag: "🇵🇹", mask: "### ### ###" },
|
|
33
|
+
{ code: "GB", name: "United Kingdom", dial_code: "+44", flag: "🇬🇧", mask: "#### ### ####" },
|
|
34
|
+
{ code: "DE", name: "Germany", dial_code: "+49", flag: "🇩🇪", mask: "### #########" },
|
|
35
|
+
{ code: "FR", name: "France", dial_code: "+33", flag: "🇫🇷", mask: "# ## ## ## ##" },
|
|
36
|
+
{ code: "IT", name: "Italy", dial_code: "+39", flag: "🇮🇹", mask: "### ### ####" },
|
|
37
|
+
{ code: "JP", name: "Japan", dial_code: "+81", flag: "🇯🇵", mask: "##-####-####" },
|
|
38
|
+
{ code: "CN", name: "China", dial_code: "+86", flag: "🇨🇳", mask: "### #### ####" },
|
|
39
|
+
{ code: "IN", name: "India", dial_code: "+91", flag: "🇮🇳", mask: "##### #####" },
|
|
40
|
+
{ code: "AU", name: "Australia", dial_code: "+61", flag: "🇦🇺", mask: "### ### ###" }
|
|
41
|
+
]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
mattr_accessor :telephone_styles do
|
|
45
|
+
{
|
|
46
|
+
country_select: {
|
|
47
|
+
base: "mt-1 block border border-r-0 rounded-l-lg focus:ring-blue-500 focus:border-blue-500",
|
|
48
|
+
colors: {
|
|
49
|
+
default: "text-slate-900 bg-slate-50 border-slate-300 dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500",
|
|
50
|
+
success: "text-green-900 bg-green-50 border-green-300 dark:bg-green-700 dark:border-green-600 dark:text-white",
|
|
51
|
+
danger: "text-red-900 bg-red-50 border-red-300 dark:bg-red-700 dark:border-red-600 dark:text-white",
|
|
52
|
+
warning: "text-yellow-900 bg-yellow-50 border-yellow-300 dark:bg-yellow-700 dark:border-yellow-600 dark:text-white",
|
|
53
|
+
info: "text-cyan-900 bg-cyan-50 border-cyan-300 dark:bg-cyan-700 dark:border-cyan-600 dark:text-white"
|
|
54
|
+
},
|
|
55
|
+
width: "w-24",
|
|
56
|
+
sizes: [
|
|
57
|
+
{ padding: "p-2", text: "text-xs" }, # Small (0)
|
|
58
|
+
{ padding: "p-2.5", text: "text-sm" }, # Medium (1)
|
|
59
|
+
{ padding: "p-4", text: "text-base" } # Large (2)
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
input: {
|
|
63
|
+
sizes: [
|
|
64
|
+
"p-2 text-xs rounded-lg", # Small (0)
|
|
65
|
+
"p-2.5 text-sm rounded-lg", # Medium (1)
|
|
66
|
+
"p-4 text-base rounded-lg" # Large (2)
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
# rubocop: enable Layout/LineLength, Metrics/BlockLength
|
|
72
|
+
end
|
|
@@ -16,6 +16,7 @@ module Fluxbit
|
|
|
16
16
|
require "fluxbit/config/form/password_component"
|
|
17
17
|
require "fluxbit/config/form/radio_group_button_component"
|
|
18
18
|
require "fluxbit/config/form/range_component"
|
|
19
|
+
require "fluxbit/config/form/telephone_component"
|
|
19
20
|
require "fluxbit/config/form/text_field_component"
|
|
20
21
|
require "fluxbit/config/form/toggle_component"
|
|
21
22
|
end
|
|
@@ -89,6 +89,8 @@ module Fluxbit
|
|
|
89
89
|
# Generate i18n
|
|
90
90
|
template "i18n.en.yml.tt", File.join("config", "locales", "#{file_name.pluralize}.en.yml")
|
|
91
91
|
template "i18n.pt-BR.yml.tt", File.join("config", "locales", "#{file_name.pluralize}.pt-BR.yml")
|
|
92
|
+
template "i18n.general.en.yml.tt", File.join("config", "locales", "general.en.yml")
|
|
93
|
+
template "i18n.general.pt-BR.yml.tt", File.join("config", "locales", "general.pt-BR.yml")
|
|
92
94
|
|
|
93
95
|
# Generate Policy
|
|
94
96
|
template "policy.rb.tt", File.join("app/policies", "#{file_name.singularize}_policy.rb")
|
|
@@ -97,6 +99,7 @@ module Fluxbit
|
|
|
97
99
|
template "_alert.html.erb.tt", File.join("app/views/shared", "_alert.html.erb")
|
|
98
100
|
template "send_alert_via_drawer.html.erb.tt", File.join("app/views/shared", "send_alert_via_drawer_alert.html.erb")
|
|
99
101
|
template "_flash.html.erb.tt", File.join("app/views/shared", "_flash.html.erb")
|
|
102
|
+
template "sortable.rb.tt", File.join("app/controllers/concerns", "sortable.rb")
|
|
100
103
|
end
|
|
101
104
|
|
|
102
105
|
private
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
"warning" => :warning
|
|
7
7
|
} %>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
<%%
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<%%=
|
|
9
|
+
<div id="notice" class="fixed top-4 right-4 z-50 max-w-md w-auto">
|
|
10
|
+
<%% flash.each do |type, msg| %>
|
|
11
|
+
<%% next if msg.blank? %>
|
|
12
|
+
<%% color = mapping[type.to_s] || :info %>
|
|
13
|
+
<%%= fx_alert(color: color) do %>
|
|
14
|
+
<%%= safe_join(Array(msg).map { |m| sanitize(m.to_s, tags: [], attributes: []) }, tag.br) %>
|
|
15
|
+
<%% end %>
|
|
14
16
|
<%% end %>
|
|
15
|
-
|
|
17
|
+
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
class <%= namespaced? ? "#{namespace_module}::" : "" %><%= class_name.pluralize %>Controller < ApplicationController
|
|
5
|
+
include Sortable
|
|
5
6
|
include Pundit::Authorization
|
|
6
7
|
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
|
7
8
|
|
|
@@ -241,7 +242,7 @@ class <%= namespaced? ? "#{namespace_module}::" : "" %><%= class_name.pluralize
|
|
|
241
242
|
private
|
|
242
243
|
|
|
243
244
|
def set_<%= singular %>
|
|
244
|
-
@<%= singular %> = <%= class_name.singularize
|
|
245
|
+
@<%= singular %> = policy_scope(<%= class_name.singularize %>).find(params[:id])
|
|
245
246
|
# Note: Pundit authorization for @<%= singular %> happens in each action
|
|
246
247
|
rescue ActiveRecord::RecordNotFound
|
|
247
248
|
@message = t("<%= plural %>.messages.not_found")
|
|
@@ -264,7 +265,7 @@ class <%= namespaced? ? "#{namespace_module}::" : "" %><%= class_name.pluralize
|
|
|
264
265
|
|
|
265
266
|
def set_<%= plural %>_for_bulk_actions
|
|
266
267
|
<%= singular %>_ids = Array(params[:<%= singular %>_ids]).reject(&:blank?)
|
|
267
|
-
@<%= plural %> = <%= class_name.singularize
|
|
268
|
+
@<%= plural %> = policy_scope(<%= class_name.singularize %>).where(id: <%= singular %>_ids)
|
|
268
269
|
|
|
269
270
|
if @<%= plural %>.empty?
|
|
270
271
|
@message = t("<%= plural %>.messages.not_selected_for_action")
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Sortable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
helper_method :apply_sorting
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def apply_sorting(resource_class, order_param, order_options = %i[created_at updated_at], default_order = { created_at: :desc })
|
|
11
|
+
if order_param.present?
|
|
12
|
+
order = order_param.rpartition("_")
|
|
13
|
+
if order.length == 3 && %w[asc desc].include?(order.last) && order_options.include?(order.first.to_sym)
|
|
14
|
+
resource_class.order(order.first.to_sym => order.last.to_sym)
|
|
15
|
+
else
|
|
16
|
+
flash[:error] = t("general.messages.order_error")
|
|
17
|
+
resource_class.order(default_order)
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
resource_class.order(default_order) # Default order
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluxbit_view_components
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Arthur Molina
|
|
@@ -146,6 +146,7 @@ files:
|
|
|
146
146
|
- app/assets/javascripts/fluxbit_view_components/row_click_controller.js
|
|
147
147
|
- app/assets/javascripts/fluxbit_view_components/select_all_controller.js
|
|
148
148
|
- app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js
|
|
149
|
+
- app/assets/javascripts/fluxbit_view_components/telephone_controller.js
|
|
149
150
|
- app/assets/javascripts/fluxbit_view_components/theme_button_controller.js
|
|
150
151
|
- app/components/fluxbit/accordion_component.rb
|
|
151
152
|
- app/components/fluxbit/alert_component.rb
|
|
@@ -177,6 +178,7 @@ files:
|
|
|
177
178
|
- app/components/fluxbit/form/radio_group_button_component.rb
|
|
178
179
|
- app/components/fluxbit/form/range_component.rb
|
|
179
180
|
- app/components/fluxbit/form/select_component.rb
|
|
181
|
+
- app/components/fluxbit/form/telephone_component.rb
|
|
180
182
|
- app/components/fluxbit/form/text_field_component.rb
|
|
181
183
|
- app/components/fluxbit/form/toggle_component.html.erb
|
|
182
184
|
- app/components/fluxbit/form/toggle_component.rb
|
|
@@ -233,6 +235,7 @@ files:
|
|
|
233
235
|
- lib/fluxbit/config/form/password_component.rb
|
|
234
236
|
- lib/fluxbit/config/form/radio_group_button_component.rb
|
|
235
237
|
- lib/fluxbit/config/form/range_component.rb
|
|
238
|
+
- lib/fluxbit/config/form/telephone_component.rb
|
|
236
239
|
- lib/fluxbit/config/form/text_field_component.rb
|
|
237
240
|
- lib/fluxbit/config/form/toggle_component.rb
|
|
238
241
|
- lib/fluxbit/config/gravatar_component.rb
|
|
@@ -289,15 +292,18 @@ files:
|
|
|
289
292
|
- lib/generators/fluxbit/templates/edit.html.erb.tt
|
|
290
293
|
- lib/generators/fluxbit/templates/fluxbit_pagy.css
|
|
291
294
|
- lib/generators/fluxbit/templates/i18n.en.yml.tt
|
|
295
|
+
- lib/generators/fluxbit/templates/i18n.general.en.yml.tt
|
|
296
|
+
- lib/generators/fluxbit/templates/i18n.general.pt-BR.yml.tt
|
|
292
297
|
- lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt
|
|
293
298
|
- lib/generators/fluxbit/templates/index.html.erb.tt
|
|
294
299
|
- lib/generators/fluxbit/templates/index.json.jbuilder.tt
|
|
295
300
|
- lib/generators/fluxbit/templates/new.html.erb.tt
|
|
296
301
|
- lib/generators/fluxbit/templates/partial.html.erb.tt
|
|
297
302
|
- lib/generators/fluxbit/templates/policy.rb.tt
|
|
298
|
-
- lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt
|
|
303
|
+
- lib/generators/fluxbit/templates/send_alert_via_drawer.html.erb.tt
|
|
299
304
|
- lib/generators/fluxbit/templates/show.html.erb.tt
|
|
300
305
|
- lib/generators/fluxbit/templates/show.json.jbuilder.tt
|
|
306
|
+
- lib/generators/fluxbit/templates/sortable.rb.tt
|
|
301
307
|
- lib/generators/fluxbit/templates/update.turbo_stream.erb.tt
|
|
302
308
|
- lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt
|
|
303
309
|
- lib/install/install.rb
|
|
File without changes
|