f_components 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +144 -0
  3. data/Rakefile +20 -0
  4. data/app/components/f_components/avatar/component.html.erb +1 -0
  5. data/app/components/f_components/avatar/component.rb +30 -0
  6. data/app/components/f_components/avatar/component.scss +3 -0
  7. data/app/components/f_components/base.rb +14 -0
  8. data/app/components/f_components/button/component.html.erb +5 -0
  9. data/app/components/f_components/button/component.rb +68 -0
  10. data/app/components/f_components/button/component.scss +9 -0
  11. data/app/components/f_components/collapsible/component.html.erb +6 -0
  12. data/app/components/f_components/collapsible/component.rb +15 -0
  13. data/app/components/f_components/collapsible/component.scss +19 -0
  14. data/app/components/f_components/dropdown/component.html.erb +10 -0
  15. data/app/components/f_components/dropdown/component.rb +19 -0
  16. data/app/components/f_components/dropdown/component.scss +10 -0
  17. data/app/components/f_components/form_field/component.html.erb +25 -0
  18. data/app/components/f_components/form_field/component.rb +124 -0
  19. data/app/components/f_components/resource_table/component.html.erb +17 -0
  20. data/app/components/f_components/resource_table/component.rb +139 -0
  21. data/app/components/f_components/table/component.html.erb +23 -0
  22. data/app/components/f_components/table/component.rb +88 -0
  23. data/app/components/f_components/table/component_controller.js +84 -0
  24. data/app/components/f_components/table/desktop/component.html.erb +25 -0
  25. data/app/components/f_components/table/desktop/component.rb +71 -0
  26. data/app/components/f_components/table/mobile/component.html.erb +8 -0
  27. data/app/components/f_components/table/mobile/component.rb +102 -0
  28. data/app/frontend/f_components/stylesheets/blocks/_button.scss +189 -0
  29. data/app/frontend/f_components/stylesheets/f_components.scss +12 -0
  30. data/app/frontend/f_components/stylesheets/variables/_breakpoints.scss +3 -0
  31. data/app/frontend/f_components/stylesheets/variables/_colors.scss +19 -0
  32. data/app/helpers/f_components/application_helper.rb +13 -0
  33. data/app/helpers/f_components/components_helper.rb +33 -0
  34. data/app/helpers/f_components/icons_helper.rb +34 -0
  35. data/config/webpack/development.js +5 -0
  36. data/config/webpack/environment.js +3 -0
  37. data/config/webpack/production.js +7 -0
  38. data/config/webpacker.yml +89 -0
  39. data/lib/f_components/engine.rb +28 -0
  40. data/lib/f_components/railtie.rb +12 -0
  41. data/lib/f_components/version.rb +5 -0
  42. data/lib/f_components.rb +18 -0
  43. metadata +158 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module ResourceTable
5
+ class Component < Base
6
+ attributes :attributes, :resources, resource_class: nil, resource_path: nil,
7
+ class: nil, selectable: {}, actions: true
8
+
9
+ private
10
+
11
+ def table_header
12
+ tag.thead do
13
+ tag.tr class: 'text-left border-b-2 border-gray-lt' do
14
+ row = table_headers
15
+ row.prepend(tag.th(class: 'py-3 px-7 w-2') { nil }) if selectable.present?
16
+ row.append(tag.th(class: 'py-4 px-7')) if actions
17
+
18
+ safe_join(row)
19
+ end
20
+ end
21
+ end
22
+
23
+ def table_headers
24
+ attributes.map do |attribute|
25
+ tag.th(class: 'py-4 px-7') { attribute_name(attribute) }
26
+ end
27
+ end
28
+
29
+ def table_row(resource, class:)
30
+ tag.tr class: binding.local_variable_get(:class) do
31
+ row = []
32
+ row.prepend(tag.td(class: 'py-3 px-7') { form_check_box(resource) }) if selectable.present?
33
+ row.append(table_rows(resource))
34
+ row.append(link_to_resource(resource)) if actions
35
+
36
+ safe_join(row)
37
+ end
38
+ end
39
+
40
+ def table_rows(resource)
41
+ attributes.map { |attribute| tag.td(class: 'py-3 px-7') { row_value(resource, attribute) } }
42
+ end
43
+
44
+ def mobile_header(resource, class:, &block)
45
+ collapsible_title = row_value(resource, attributes.first, class: 'font-bold text-sm')
46
+
47
+ if selectable.present?
48
+ check_box = form_check_box(resource)
49
+ collapsible_title = tag.div(class: 'w-full flex space-x-4 items-center') { check_box + collapsible_title }
50
+ end
51
+
52
+ fcomponent(:collapsible, summary: collapsible_title, class: binding.local_variable_get(:class), &block)
53
+ end
54
+
55
+ def mobile_rows(resource, class:)
56
+ tag.div class: binding.local_variable_get(:class) do
57
+ safe_join(other_attributes.map { |attribute| row(resource, attribute) })
58
+ end
59
+ end
60
+
61
+ def link_to_resource(resource, display_as: :link)
62
+ path = resource_path.present? ? resource_path.(resource) : resource
63
+ return tag.td(class: 'py-3 px-7') { link_to('Detalhes', path) } if display_as == :link
64
+
65
+ fcomponent :button, 'Detalhes', path, type: :secondary, mods: [:sm], class: 'mt-7 mx-auto'
66
+ end
67
+
68
+ def other_attributes
69
+ Array(attributes[1..])
70
+ end
71
+
72
+ def row(resource, attribute)
73
+ tag.div(class: 'flex items-center text-sm') do
74
+ row_label(attribute) + row_value(resource, attribute)
75
+ end
76
+ end
77
+
78
+ def row_label(attribute)
79
+ tag.strong(class: 'w-20 min-w-20 mr-8') { attribute_name(attribute) }
80
+ end
81
+
82
+ def row_value(resource, attribute, class: nil)
83
+ if (attr_value = attribute_value(resource, attribute))
84
+ tag.p(class: "max-w-5/6 #{binding.local_variable_get(:class)}") do
85
+ format_attribute_value(attribute, attr_value)
86
+ end
87
+ else
88
+ attribute_fallback
89
+ end
90
+ end
91
+
92
+ def format_attribute_value(attribute, value)
93
+ return value.to_s unless resource_class.defined_enums.has_key?(attribute.to_s)
94
+
95
+ resource_class.human_enum_name(attribute.to_s, value)
96
+ end
97
+
98
+ def attribute_name(attribute)
99
+ resource_class.human_attribute_name(attribute)
100
+ end
101
+
102
+ def attribute_value(resource, attribute)
103
+ resource.public_send(attribute)
104
+ end
105
+
106
+ def attribute_fallback
107
+ @attribute_fallback ||= tag.p(class: 'text-red') { 'Dados insuficientes' }
108
+ end
109
+
110
+ def resource_class
111
+ @resource_class ||=
112
+ case resources.class.to_s
113
+ when 'ActiveRecord::Relation'
114
+ resources.model
115
+ when 'Draper::CollectionDecorator'
116
+ resources.object.model
117
+ else
118
+ resources.first.class
119
+ end
120
+ end
121
+
122
+ def table_layout
123
+ selectable.present? ? 'md:table md:table-fixed' : 'md:table'
124
+ end
125
+
126
+ def resources_class_name
127
+ resource_class.to_s.parameterize(separator: '_')
128
+ end
129
+
130
+ def form
131
+ selectable[:form]
132
+ end
133
+
134
+ def form_check_box(resource)
135
+ form.check_box(resources_class_name, { multiple: true }, resource.id, nil)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,23 @@
1
+ <div data-controller="f-table-component">
2
+ <%=
3
+ desktop_table(
4
+ columns: columns,
5
+ rows: rows,
6
+ actions: @actions,
7
+ check_boxes: @check_boxes,
8
+ **@options
9
+ )
10
+ %>
11
+
12
+ <%=
13
+ mobile_table(
14
+ columns: columns,
15
+ rows: rows,
16
+ main_column: @main_column,
17
+ actions: @actions,
18
+ check_boxes: @check_boxes,
19
+ resources: @resources,
20
+ **@options
21
+ )
22
+ %>
23
+ </div>
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module Table
5
+ class Component < Base
6
+ renders_one :desktop_table, Table::Desktop::Component
7
+ renders_one :mobile_table, Table::Mobile::Component
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @values = {}
12
+ @actions = []
13
+ @check_boxes = []
14
+ @main_column = nil
15
+ @current_resource = nil
16
+ @current_resource_index = 0
17
+ end
18
+
19
+ def self.for(resources, **options, &block)
20
+ table = new(options)
21
+ table.build(resources, &block)
22
+
23
+ table
24
+ end
25
+
26
+ def column(name, id: nil, main: false)
27
+ if main
28
+ @main_column = main.is_a?(Proc) ? main : name
29
+ end
30
+
31
+ @values[name] ||= []
32
+
33
+ return if @current_resource.blank?
34
+
35
+ @values[name] << { id: id, column_index: @current_resource_index, cell_content: yield(@current_resource).to_s }
36
+ end
37
+
38
+ def check_box(name:, value:, checked: false, **options)
39
+ value = value.(@current_resource) if value.respond_to? :call
40
+ checked = checked.(@current_resource) if checked.respond_to? :call
41
+
42
+ parsed_options = options
43
+
44
+ options.each do |k, v|
45
+ parsed_options[k] = v.(@current_resource) if v.respond_to? :call
46
+ end
47
+
48
+ parsed_options[:id] ||= "#{sanitize_to_id(name)}#{value}"
49
+
50
+ @check_boxes << check_box_tag(name, value, checked, **parsed_options)
51
+ end
52
+
53
+ def action
54
+ action = yield(@current_resource)
55
+
56
+ @actions[@current_resource_index] ||= []
57
+ @actions[@current_resource_index] << action
58
+ end
59
+
60
+ def build(resources)
61
+ @resources = resources.presence || [nil]
62
+ @resources.each_with_index do |resource, i|
63
+ @current_resource = resource
64
+ @current_resource_index = i
65
+
66
+ yield(self, @current_resource)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def columns
73
+ @values.keys
74
+ end
75
+
76
+ def rows
77
+ @values.values.transpose
78
+ end
79
+
80
+ # This was copied from Rails, since it's not a public API
81
+ # Source:
82
+ # https://github.com/rails/rails/blob/d305be07428a8e86e2736b57f839680c9e970293/actionview/lib/action_view/helpers/form_tag_helper.rb#L931
83
+ def sanitize_to_id(name)
84
+ name.to_s.delete(']').tr('^-a-zA-Z0-9:.', '_')
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,84 @@
1
+ import { Controller as BaseController } from '@hotwired/stimulus';
2
+ import { findAll } from '@fretadao/f-js-dom';
3
+
4
+ export default class extends BaseController {
5
+ static targets = ['desktopTable', 'mobileTable'];
6
+
7
+ connect() {
8
+ this.toggleDuplicatesHandler = this.toggleDuplicates.bind(this);
9
+
10
+ this.checkboxes().forEach(checkbox => {
11
+ checkbox.addEventListener('change', this.toggleDuplicatesHandler);
12
+ });
13
+
14
+
15
+ this.syncInputHandler = this.syncInput.bind(this)
16
+
17
+ this.inputs().forEach(input => {
18
+ input.addEventListener('input', this.syncInputHandler);
19
+ })
20
+
21
+ document.dispatchEvent(this.#loadedEvent());
22
+ }
23
+
24
+ disconnect() {
25
+ this.checkboxes().forEach(checkbox => {
26
+ checkbox.removeEventListener('change', this.toggleDuplicatesHandler);
27
+ });
28
+
29
+ this.inputs().forEach(input => {
30
+ input.removeEventListener('change', this.syncInputHandler);
31
+ })
32
+ }
33
+
34
+ toggleDuplicates(event) {
35
+ const checkbox = event.currentTarget;
36
+
37
+ this.checkboxes({ id: checkbox.id, value: checkbox.value }).forEach(dup => {
38
+ dup.checked = checkbox.checked;
39
+ });
40
+
41
+ this.element.dispatchEvent(new CustomEvent('checkboxesChanged', { bubbles: true }));
42
+ }
43
+
44
+ syncInput(event) {
45
+ const input = event.currentTarget;
46
+
47
+ this.inputs(input.id).forEach(dup => {
48
+ dup.value = input.value;
49
+ });
50
+
51
+ this.element.dispatchEvent(new CustomEvent('inputsChanged', { bubbles: true }));
52
+ }
53
+
54
+ checkboxes({ id, value } = {}) {
55
+ let query = 'input[type=checkbox]';
56
+
57
+ if(!!id) {
58
+ query = `#${id}`;
59
+ } else if (!!value) {
60
+ query += `[value="${value}"]`;
61
+ }
62
+
63
+ return findAll(query, this.element);
64
+ }
65
+
66
+ inputs(id = null) {
67
+ let query = 'input:not([type=checkbox])';
68
+
69
+ if(!!id) {
70
+ query = `#${id}`;
71
+ }
72
+
73
+ return findAll(query, this.element);
74
+ }
75
+
76
+ #loadedEvent() {
77
+ return new CustomEvent(
78
+ 'f-components-table:loaded',
79
+ {
80
+ detail: { desktopTable: this.desktopTableTarget, mobileTable: this.mobileTableTarget }
81
+ }
82
+ );
83
+ }
84
+ }
@@ -0,0 +1,25 @@
1
+ <%= tag.table(id: @id, class: "hidden md:w-full md:table md:text-sm #{@class}", **@options) do %>
2
+ <thead>
3
+ <tr class="text-left border-b-2 border-gray-lt">
4
+ <% @columns.each do |header| %>
5
+ <th class='px-4 py-3'><%= header %></th>
6
+ <% end %>
7
+ </tr>
8
+ </thead>
9
+
10
+ <tbody>
11
+ <% @rows.each_with_index do |row, i| %>
12
+ <tr class='even:bg-gray-ltr'>
13
+ <%= check_box_for(resource_index: i) %>
14
+
15
+ <% row.each do |cell| %>
16
+ <td class="px-4 py-3">
17
+ <%= format_cell(cell) %>
18
+ </td>
19
+ <% end %>
20
+
21
+ <%= actions(resource_index: i) %>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ <% end %>
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module Table
5
+ module Desktop
6
+ class Component < Base
7
+ def initialize(**options)
8
+ @rows = options.delete(:rows)
9
+ @actions = options.delete(:actions)
10
+ @check_boxes = options.delete(:check_boxes)
11
+ @columns = options.delete(:columns)
12
+ @columns.prepend('') if @check_boxes.present?
13
+ @columns.push('') if @actions.present?
14
+ @id = ['desktop-table', options.delete(:id_suffix)].filter_map(&:presence).join('-')
15
+ @class = options.delete(:class)
16
+ add_target(options)
17
+ @options = options
18
+ end
19
+
20
+ def check_box_for(resource_index:)
21
+ return if @check_boxes.empty?
22
+
23
+ tag.td(class: 'py-3 text-center') { @check_boxes[resource_index] }
24
+ end
25
+
26
+ def actions(resource_index:)
27
+ return if @actions.blank?
28
+
29
+ resource_actions = @actions[resource_index]
30
+
31
+ tag.td(class: 'py-3 px-4') do
32
+ actions_for_resource(resource_actions)
33
+ end
34
+ end
35
+
36
+ def actions_for_resource(resource_actions)
37
+ fcomponent :dropdown, label: 'Opções', padding: false, icon: 'plus', class: 'w-fit mx-auto' do
38
+ safe_join(
39
+ resource_actions.map do |action|
40
+ tag.div(class: 'text-sm py-3 px-5 even:bg-gray-lt') do
41
+ action
42
+ end
43
+ end
44
+ )
45
+ end
46
+ end
47
+
48
+ def format_cell(value)
49
+ cell_content = value.is_a?(Hash) ? value[:cell_content] : value
50
+ return cell_content if cell_content.present? && html?(cell_content)
51
+
52
+ cell_id = value[:id].present? ? "#{value[:id]}-desktop-#{value[:column_index]}" : nil
53
+
54
+ tag.p(id: cell_id, class: 'max-w-5/6') { cell_content }
55
+ end
56
+
57
+ def html?(test_target)
58
+ Nokogiri::XML.parse(test_target.to_s).errors.empty?
59
+ end
60
+
61
+ def add_target(options)
62
+ options ||= {}
63
+ options[:data] ||= {}
64
+ options[:data][:f_table_component_target] = 'desktopTable'
65
+
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,8 @@
1
+ <%= tag.div(id: @id, class: "md:hidden #{@class}", **@options) do %>
2
+ <% rows.each_with_index do |row, i| %>
3
+ <%= table_head(resource_index: i) do %>
4
+ <%= table_body(row) %>
5
+ <%= actions(resource_index: i) %>
6
+ <% end %>
7
+ <% end %>
8
+ <% end %>
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FComponents
4
+ module Table
5
+ module Mobile
6
+ class Component < Base
7
+ def initialize(**options)
8
+ @rows = options.delete(:rows)
9
+ @actions = options.delete(:actions)
10
+ @main_column = options.delete(:main_column)
11
+ @check_boxes = options.delete(:check_boxes)
12
+ @columns = options.delete(:columns)
13
+ @id = ['mobile-table', options.delete(:id_suffix)].filter_map(&:presence).join('-')
14
+ @class = options.delete(:class)
15
+ @resources = options.delete(:resources)
16
+ add_target(options)
17
+ @options = options
18
+ end
19
+
20
+ def table_head(resource_index:, &block)
21
+ current_resource = @resources[resource_index]
22
+
23
+ if @main_column.is_a?(Proc) && current_resource.present?
24
+ main_header = @main_column.call(current_resource)
25
+ else
26
+ main_header_index = @columns.index(@main_column) or invalid_column_error!
27
+ main_header = tag.strong(class: 'text-sm') do
28
+ value = @rows[resource_index][main_header_index]
29
+
30
+ value.is_a?(Hash) ? value[:cell_content] : value
31
+ end
32
+ end
33
+
34
+ title = if @check_boxes.present?
35
+ @check_boxes[resource_index] + main_header
36
+ else
37
+ main_header
38
+ end
39
+
40
+ fcomponent(:collapsible, summary: title, class: 'bg-white border-b border-gray-lt', &block)
41
+ end
42
+
43
+ def rows
44
+ @rows.map do |row|
45
+ row.map { |cell| format_cell(cell) }
46
+ end
47
+ end
48
+
49
+ def table_body(row)
50
+ tag.div class: 'flex flex-col space-y-5 mt-5' do
51
+ safe_join(
52
+ row.each_with_index.map do |value, position|
53
+ tag.div(class: 'flex items-center text-sm', data: { mobile_col: true }) do
54
+ tag.strong(class: 'w-20 min-w-20 mr-8') { @columns[position] } + format_cell(value)
55
+ end
56
+ end
57
+ )
58
+ end
59
+ end
60
+
61
+ def actions(resource_index:)
62
+ return if @actions.blank?
63
+
64
+ fcomponent :dropdown, label: 'Opções', padding: false, icon: 'plus', class: 'mt-5' do
65
+ safe_join(
66
+ @actions[resource_index].map do |action|
67
+ tag.div(class: 'text-sm py-3 px-5 even:bg-gray-lt') do
68
+ action
69
+ end
70
+ end
71
+ )
72
+ end
73
+ end
74
+
75
+ def format_cell(value)
76
+ cell_content = value.is_a?(Hash) ? value[:cell_content] : value
77
+ return cell_content if cell_content.present? && html?(cell_content)
78
+
79
+ cell_id = value[:id].present? ? "#{value[:id]}-mobile-#{value[:column_index]}" : nil
80
+
81
+ tag.p(id: cell_id, class: 'max-w-5/6') { cell_content }
82
+ end
83
+
84
+ def html?(test_target)
85
+ Nokogiri::XML.parse(test_target.to_s).errors.empty?
86
+ end
87
+
88
+ def invalid_column_error!
89
+ raise "Main column '#{@main_column}' not found in #{@columns}. Maybe you forgot to specify a main column?"
90
+ end
91
+
92
+ def add_target(options)
93
+ options ||= {}
94
+ options[:data] ||= {}
95
+ options[:data][:f_table_component_target] = 'mobileTable'
96
+
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end