phlexi-table 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/phlexi/table/base.rb +356 -74
  3. data/lib/phlexi/table/components/actions_column.rb +43 -0
  4. data/lib/phlexi/table/components/base.rb +22 -18
  5. data/lib/phlexi/table/components/column.rb +14 -0
  6. data/lib/phlexi/table/components/column_group.rb +23 -0
  7. data/lib/phlexi/table/components/concerns/displays_data.rb +27 -0
  8. data/lib/phlexi/table/components/concerns/displays_header.rb +30 -0
  9. data/lib/phlexi/table/components/concerns/groups_columns.rb +36 -0
  10. data/lib/phlexi/table/components/data_column.rb +33 -0
  11. data/lib/phlexi/table/components/header_cell.rb +19 -0
  12. data/lib/phlexi/table/components/options/alignment.rb +31 -0
  13. data/lib/phlexi/table/components/options/labels.rb +30 -0
  14. data/lib/phlexi/table/components/selection_cell.rb +17 -0
  15. data/lib/phlexi/table/components/selection_column.rb +21 -0
  16. data/lib/phlexi/table/components/sortable_header_cell.rb +74 -0
  17. data/lib/phlexi/table/display_theme.rb +6 -0
  18. data/lib/phlexi/table/html.rb +15 -0
  19. data/lib/phlexi/table/options/captions.rb +22 -0
  20. data/lib/phlexi/table/{field_options/description.rb → options/descriptions.rb} +5 -5
  21. data/lib/phlexi/table/theme.rb +25 -0
  22. data/lib/phlexi/table/version.rb +1 -1
  23. data/lib/phlexi/table/wrapped_object.rb +27 -0
  24. data/lib/phlexi/table.rb +5 -6
  25. metadata +49 -25
  26. data/lib/phlexi/table/components/concerns/displays_value.rb +0 -54
  27. data/lib/phlexi/table/components/date_time.rb +0 -49
  28. data/lib/phlexi/table/components/description.rb +0 -21
  29. data/lib/phlexi/table/components/hint.rb +0 -21
  30. data/lib/phlexi/table/components/label.rb +0 -15
  31. data/lib/phlexi/table/components/number.rb +0 -37
  32. data/lib/phlexi/table/components/placeholder.rb +0 -15
  33. data/lib/phlexi/table/components/string.rb +0 -17
  34. data/lib/phlexi/table/components/wrapper.rb +0 -17
  35. data/lib/phlexi/table/field_options/associations.rb +0 -21
  36. data/lib/phlexi/table/field_options/attachments.rb +0 -21
  37. data/lib/phlexi/table/field_options/hints.rb +0 -22
  38. data/lib/phlexi/table/field_options/inferred_types.rb +0 -129
  39. data/lib/phlexi/table/field_options/labels.rb +0 -28
  40. data/lib/phlexi/table/field_options/placeholders.rb +0 -18
  41. data/lib/phlexi/table/field_options/themes.rb +0 -132
  42. data/lib/phlexi/table/structure/dom.rb +0 -42
  43. data/lib/phlexi/table/structure/field_builder.rb +0 -158
  44. data/lib/phlexi/table/structure/field_collection.rb +0 -39
  45. data/lib/phlexi/table/structure/namespace.rb +0 -123
  46. data/lib/phlexi/table/structure/namespace_collection.rb +0 -40
  47. data/lib/phlexi/table/structure/node.rb +0 -24
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module Components
6
- class Label < Base
7
- def view_template
8
- h5(**attributes) {
9
- field.label
10
- }
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/number_helper"
4
-
5
- module Phlexi
6
- module Table
7
- module Components
8
- class Number < Base
9
- include Concerns::TablesValue
10
-
11
- def render_value(value)
12
- p(**attributes) {
13
- format_number(value)
14
- }
15
- end
16
-
17
- protected
18
-
19
- def build_attributes
20
- super
21
-
22
- @options = attributes.delete(:options) || {}
23
- end
24
-
25
- private
26
-
27
- def format_number(value)
28
- ActiveSupport::NumberHelper.number_to_delimited(value, **@options)
29
- end
30
-
31
- def normalize_value(value)
32
- Float(value.to_s)
33
- end
34
- end
35
- end
36
- end
37
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module Components
6
- class Placeholder < Base
7
- def view_template
8
- p(**attributes) {
9
- field.placeholder
10
- }
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module Components
6
- class String < Base
7
- include Concerns::TablesValue
8
-
9
- def render_value(value)
10
- p(**attributes) {
11
- value
12
- }
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module Components
6
- class Wrapper < Base
7
- def view_template
8
- div(**attributes) {
9
- render field.label_tag
10
- yield field if block_given?
11
- render field.description_tag
12
- }
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module FieldOptions
6
- module Associations
7
- protected
8
-
9
- def association_reflection
10
- @association_reflection ||= find_association_reflection
11
- end
12
-
13
- def find_association_reflection
14
- if object.class.respond_to?(:reflect_on_association)
15
- object.class.reflect_on_association(key)
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module FieldOptions
6
- module Attachments
7
- protected
8
-
9
- def attachment_reflection
10
- @attachment_reflection ||= find_attachment_reflection
11
- end
12
-
13
- def find_attachment_reflection
14
- if object.class.respond_to?(:reflect_on_attachment)
15
- object.class.reflect_on_attachment(key)
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module FieldOptions
6
- module Hints
7
- def hint(hint = nil)
8
- if hint.nil?
9
- options[:hint]
10
- else
11
- options[:hint] = hint
12
- self
13
- end
14
- end
15
-
16
- def has_hint?
17
- hint.present?
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bigdecimal"
4
-
5
- module Phlexi
6
- module Table
7
- module FieldOptions
8
- module InferredTypes
9
- def inferred_db_type
10
- @inferred_db_type ||= infer_db_type
11
- end
12
-
13
- def inferred_display_component
14
- @inferred_display_component ||= infer_display_component
15
- end
16
-
17
- private
18
-
19
- def infer_display_component
20
- case inferred_db_type
21
- when :string, :text
22
- infer_string_display_type(key)
23
- when :integer, :float, :decimal
24
- :number
25
- when :date, :datetime, :time
26
- :date
27
- when :boolean
28
- :boolean
29
- when :json, :jsonb, :hstore
30
- :code
31
- else
32
- if association_reflection
33
- :association
34
- elsif attachment_reflection
35
- :attachment
36
- else
37
- :text
38
- end
39
- end
40
- end
41
-
42
- def infer_db_type
43
- if object.class.respond_to?(:columns_hash)
44
- # ActiveRecord object
45
- column = object.class.columns_hash[key.to_s]
46
- return column.type if column
47
- end
48
-
49
- if object.class.respond_to?(:attribute_types)
50
- # ActiveModel::Attributes
51
- custom_type = object.class.attribute_types[key.to_s]
52
- return custom_type.type if custom_type
53
- end
54
-
55
- # Check if object responds to the key
56
- if object.respond_to?(key)
57
- # Fallback to inferring type from the value
58
- return infer_db_type_from_value(object.send(key))
59
- end
60
-
61
- # Default to string if we can't determine the type
62
- :string
63
- end
64
-
65
- def infer_db_type_from_value(value)
66
- case value
67
- when Integer
68
- :integer
69
- when Float
70
- :float
71
- when BigDecimal
72
- :decimal
73
- when TrueClass, FalseClass
74
- :boolean
75
- when Date
76
- :date
77
- when Time, DateTime
78
- :datetime
79
- when Hash
80
- :json
81
- else
82
- :string
83
- end
84
- end
85
-
86
- def infer_string_display_type(key)
87
- key = key.to_s.downcase
88
-
89
- return :password if is_password_field?
90
-
91
- custom_type = custom_string_display_type(key)
92
- return custom_type if custom_type
93
-
94
- :text
95
- end
96
-
97
- def custom_string_display_type(key)
98
- custom_mappings = {
99
- /url$|^link|^site/ => :url,
100
- /^email/ => :email,
101
- /phone|tel(ephone)?/ => :phone,
102
- /^time/ => :time,
103
- /^date/ => :date,
104
- /^number|_count$|_amount$/ => :number,
105
- /^color/ => :color
106
- }
107
-
108
- custom_mappings.each do |pattern, type|
109
- return type if key.match?(pattern)
110
- end
111
-
112
- nil
113
- end
114
-
115
- def is_password_field?
116
- key = self.key.to_s.downcase
117
-
118
- exact_matches = ["password"]
119
- prefixes = ["encrypted_"]
120
- suffixes = ["_password", "_digest", "_hash"]
121
-
122
- exact_matches.include?(key) ||
123
- prefixes.any? { |prefix| key.start_with?(prefix) } ||
124
- suffixes.any? { |suffix| key.end_with?(suffix) }
125
- end
126
- end
127
- end
128
- end
129
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module FieldOptions
6
- module Labels
7
- def label(label = nil)
8
- if label.nil?
9
- options[:label] = options.fetch(:label) { calculate_label }
10
- else
11
- options[:label] = label
12
- self
13
- end
14
- end
15
-
16
- private
17
-
18
- def calculate_label
19
- if object.class.respond_to?(:human_attribute_name)
20
- object.class.human_attribute_name(key.to_s, {base: object})
21
- else
22
- key.to_s.humanize
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module FieldOptions
6
- module Placeholders
7
- def placeholder(placeholder = nil)
8
- if placeholder.nil?
9
- options[:placeholder] || "-"
10
- else
11
- options[:placeholder] = placeholder
12
- self
13
- end
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,132 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module FieldOptions
6
- module Themes
7
- # Resolves theme classes for components based on their type.
8
- #
9
- # This method is responsible for determining the appropriate CSS classes for a given display component.
10
- # It supports a hierarchical theming system, allowing for cascading themes and easy customization.
11
- #
12
- # @param component [Symbol, String] The type of display component (e.g., :text, :date, :boolean)
13
- #
14
- # @return [String, nil] A string of CSS classes for the component, or nil if no theme is applied
15
- #
16
- # @example Basic usage
17
- # themed(:text)
18
- # # => "text-gray-700 text-sm"
19
- #
20
- # @example Cascading themes
21
- # # Assuming email inherits from text in the theme definition
22
- # themed(:email)
23
- # # => "text-gray-700 text-sm text-blue-600 underline"
24
- #
25
- # @note The actual CSS classes returned will depend on the theme definitions in the `theme` hash
26
- # and any overrides specified in the `options` hash.
27
- #
28
- # @see #resolve_theme
29
- # @see #theme
30
- def themed(component)
31
- return unless component
32
-
33
- resolve_theme(component)
34
- end
35
-
36
- protected
37
-
38
- # Recursively resolves the theme for a given property, handling nested symbol references
39
- #
40
- # @param property [Symbol, String] The theme property to resolve
41
- # @param visited [Set] Set of already visited properties to prevent infinite recursion
42
- # @return [String, nil] The resolved theme value or nil if not found
43
- #
44
- # @example Resolving a nested theme
45
- # # Assuming the theme is: { email: :text, text: "text-gray-700" }
46
- # resolve_theme(:email)
47
- # # => "text-gray-700"
48
- def resolve_theme(property, visited = Set.new)
49
- return nil if !property.present? || visited.include?(property)
50
- visited.add(property)
51
-
52
- result = theme[property]
53
- if result.is_a?(Symbol)
54
- resolve_theme(result, visited)
55
- else
56
- result
57
- end
58
- end
59
-
60
- # Retrieves or initializes the theme hash for the display builder.
61
- #
62
- # This method returns a hash containing theme definitions for various display components.
63
- # If a theme has been explicitly set in the options, it returns that. Otherwise, it
64
- # initializes and returns a default theme.
65
- #
66
- # The theme hash defines CSS classes or references to other theme keys for different
67
- # components.
68
- #
69
- # @return [Hash] A hash containing theme definitions for display components
70
- #
71
- # @example Accessing the theme
72
- # theme[:text]
73
- # # => "text-gray-700 text-sm"
74
- #
75
- # @example Theme inheritance
76
- # theme[:email] # Returns :text, indicating email inherits text's theme
77
- #
78
- # @note The actual content of the theme hash depends on the default_theme method
79
- # and any theme overrides specified in the options when initializing the field builder.
80
- #
81
- # @see #default_theme
82
- def theme
83
- @theme ||= options[:theme] || default_theme
84
- end
85
-
86
- # Defines and returns the default theme hash for the display builder.
87
- #
88
- # This method returns a hash containing the base theme definitions for various components.
89
- # It sets up the default styling and relationships between different components.
90
- # The theme uses a combination of explicit CSS classes and symbolic references to other theme keys,
91
- # allowing for a flexible and inheritance-based theming system.
92
- #
93
- # @return [Hash] A frozen hash containing default theme definitions for components
94
- #
95
- # @example Accessing the default theme
96
- # default_theme[:text]
97
- # # => "text-gray-700 text-sm"
98
- #
99
- # @example Theme inheritance
100
- # default_theme[:email]
101
- # # => :text (indicates that :email inherits from :text)
102
- #
103
- # @note This method returns a frozen hash to prevent accidental modifications.
104
- # To customize the theme, users should provide their own theme hash when initializing the display builder.
105
- #
106
- # @see #theme
107
- def default_theme
108
- {
109
- label: "text-base font-bold text-gray-500 dark:text-gray-400 mb-1",
110
- description: "text-sm text-gray-400 dark:text-gray-500",
111
- placeholder: "text-xl font-semibold text-gray-500 dark:text-gray-300 mb-1 italic",
112
- string: "text-xl font-semibold text-gray-900 dark:text-white mb-1",
113
- # text: :string,
114
- number: :string,
115
- datetime: :string,
116
- # boolean: :string,
117
- # code: :string,
118
- # email: :text,
119
- # url: :text,
120
- # phone: :text,
121
- # color: :text,
122
- # search: :text,
123
- # password: :string,
124
- # association: :string,
125
- attachment: :string,
126
- wrapper: nil
127
- }.freeze
128
- end
129
- end
130
- end
131
- end
132
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Table
5
- module Structure
6
- # Generates DOM IDs for a Field, Namespace, or Node based on
7
- # norms that were established by Rails. These can be used outside of Rails in
8
- # other Ruby web frameworks since it has no dependencies on Rails.
9
- class DOM
10
- def initialize(field:)
11
- @field = field
12
- end
13
-
14
- # Converts the value of the field to a String, which is required to work
15
- # with Phlex. Assumes that `Object#to_s` emits a format suitable for display.
16
- def value
17
- @field.value.to_s
18
- end
19
-
20
- # Walks from the current node to the parent node, grabs the names, and separates
21
- # them with a `_` for a DOM ID.
22
- def id
23
- @id ||= begin
24
- root, *rest = lineage
25
- root_key = root.respond_to?(:dom_id) ? root.dom_id : root.key
26
- rest.map(&:key).unshift(root_key).join("_")
27
- end
28
- end
29
-
30
- # One-liner way of walking from the current node all the way up to the parent.
31
- def lineage
32
- @lineage ||= Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
33
- end
34
-
35
- # Emit the id and value in an HTML tag-ish that doesn't have an element.
36
- def inspect
37
- "<#{self.class.name} id=#{id.inspect} value=#{value.inspect}/>"
38
- end
39
- end
40
- end
41
- end
42
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "phlex"
4
-
5
- module Phlexi
6
- module Table
7
- module Structure
8
- # FieldBuilder class is responsible for building display fields with various options and components.
9
- #
10
- # @attr_reader [Structure::DOM] dom The DOM structure for the field.
11
- # @attr_reader [Hash] options Options for the field.
12
- # @attr_reader [Object] object The object associated with the field.
13
- # @attr_reader [Hash] attributes Attributes for the field.
14
- # @attr_accessor [Object] value The value of the field.
15
- class FieldBuilder < Node
16
- include Phlex::Helpers
17
- include FieldOptions::Themes
18
- include FieldOptions::Associations
19
- include FieldOptions::Attachments
20
- include FieldOptions::InferredTypes
21
- include FieldOptions::Labels
22
- include FieldOptions::Placeholders
23
- include FieldOptions::Description
24
- # include FieldOptions::Hints
25
-
26
- attr_reader :dom, :options, :object, :value
27
-
28
- # Initializes a new FieldBuilder instance.
29
- #
30
- # @param key [Symbol, String] The key for the field.
31
- # @param parent [Structure::Namespace] The parent object.
32
- # @param object [Object, nil] The associated object.
33
- # @param value [Object] The initial value for the field.
34
- # @param options [Hash] Additional options for the field.
35
- def initialize(key, parent:, object: nil, value: NIL_VALUE, **options)
36
- super(key, parent: parent)
37
-
38
- @object = object
39
- @value = determine_value(value)
40
- @options = options
41
- @dom = Structure::DOM.new(field: self)
42
- end
43
-
44
- # Creates a label tag for the field.
45
- #
46
- # @param attributes [Hash] Additional attributes for the label.
47
- # @return [Components::Label] The label component.
48
- def label_tag(**, &)
49
- create_component(Components::Label, :label, **, &)
50
- end
51
-
52
- # Creates a Placeholder tag for the field.
53
- #
54
- # @param attributes [Hash] Additional attributes for the placeholder.
55
- # @return [Components::Placeholder] The placeholder component.
56
- def placeholder_tag(**, &)
57
- create_component(Components::Placeholder, :placeholder, **, &)
58
- end
59
-
60
- # Creates a Description tag for the field.
61
- #
62
- # @param attributes [Hash] Additional attributes for the description.
63
- # @return [Components::Description] The description component.
64
- def description_tag(**, &)
65
- create_component(Components::Description, :description, **, &)
66
- end
67
-
68
- # Creates a string display tag for the field.
69
- #
70
- # @param attributes [Hash] Additional attributes for the string display.
71
- # @return [Components::String] The string component.
72
- def string_tag(**, &)
73
- create_component(Components::String, :string, **, &)
74
- end
75
-
76
- # # Creates a text display tag for the field.
77
- # #
78
- # # @param attributes [Hash] Additional attributes for the text display.
79
- # # @return [Components::Text] The text component.
80
- # def text_tag(**, &)
81
- # create_component(Components::Text, :text, **, &)
82
- # end
83
-
84
- # Creates a number display tag for the field.
85
- #
86
- # @param attributes [Hash] Additional attributes for the number display.
87
- # @return [Components::Number] The number component.
88
- def number_tag(**, &)
89
- create_component(Components::Number, :number, **, &)
90
- end
91
-
92
- # Creates a datetime display for the field.
93
- #
94
- # @param attributes [Hash] Additional attributes for the datetime display.
95
- # @return [Components::DateTime] The datetime component.
96
- def datetime_tag(**, &)
97
- create_component(Components::DateTime, :datetime, **, &)
98
- end
99
-
100
- # # Creates a boolean display tag for the field.
101
- # #
102
- # # @param attributes [Hash] Additional attributes for the boolean display.
103
- # # @return [Components::Boolean] The boolean component.
104
- # def boolean_tag(**, &)
105
- # create_component(Components::Boolean, :boolean, **, &)
106
- # end
107
-
108
- # # Creates an association display tag for the field.
109
- # #
110
- # # @param attributes [Hash] Additional attributes for the association display.
111
- # # @return [Components::Association] The association component.
112
- # def association_tag(**, &)
113
- # create_component(Components::Association, :association, **, &)
114
- # end
115
-
116
- # # Creates an attachment display tag for the field.
117
- # #
118
- # # @param attributes [Hash] Additional attributes for the attachment display.
119
- # # @return [Components::Attachment] The attachment component.
120
- # def attachment_tag(**, &)
121
- # create_component(Components::Attachment, :attachment, **, &)
122
- # end
123
-
124
- # Wraps the field with additional markup.
125
- #
126
- # @param attributes [Hash] Additional attributes for the wrapper.
127
- # @yield [block] The block to be executed within the wrapper.
128
- # @return [Components::Wrapper] The wrapper component.
129
- def wrapped(**, &)
130
- create_component(Components::Wrapper, :wrapper, **, &)
131
- end
132
-
133
- # Creates a repeated field collection.
134
- #
135
- # @param range [#each] The collection of items to generate displays for.
136
- # @yield [block] The block to be executed for each item in the collection.
137
- # @return [FieldCollection] The field collection.
138
- def repeated(collection = [], &)
139
- FieldCollection.new(field: self, collection:, &)
140
- end
141
-
142
- protected
143
-
144
- def create_component(component_class, theme_key, **attributes, &)
145
- theme_key = attributes.delete(:theme) || theme_key
146
- attributes = mix({class: themed(theme_key)}, attributes) unless attributes.key?(:class!)
147
- component_class.new(self, **attributes, &)
148
- end
149
-
150
- def determine_value(value)
151
- return value unless value == NIL_VALUE
152
-
153
- object.respond_to?(key) ? object.public_send(key) : nil
154
- end
155
- end
156
- end
157
- end
158
- end