orbital_ui 0.3.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/app/assets/config/orbital_ui_manifest.js +2 -0
- data/app/assets/javascripts/orbital_ui/index.js +6 -0
- data/app/assets/javascripts/orbital_ui/index.js.map +1 -0
- data/app/assets/stylesheets/orbital_ui/index.css +3 -0
- data/app/assets/stylesheets/orbital_ui/index.css.map +1 -0
- data/app/assets/stylesheets/orbital_ui/theme.css +113 -0
- data/app/assets/stylesheets/orbital_ui/themes/blue.css +43 -0
- data/app/assets/stylesheets/orbital_ui/themes/green.css +43 -0
- data/app/assets/stylesheets/orbital_ui/themes/orange.css +43 -0
- data/app/assets/stylesheets/orbital_ui/themes/rose.css +43 -0
- data/app/assets/stylesheets/orbital_ui/themes/violet.css +43 -0
- data/app/components/orbital_ui/accordion/item.rb +32 -0
- data/app/components/orbital_ui/accordion.rb +23 -0
- data/app/components/orbital_ui/alert.rb +80 -0
- data/app/components/orbital_ui/avatar.rb +87 -0
- data/app/components/orbital_ui/badge.rb +28 -0
- data/app/components/orbital_ui/button.rb +46 -0
- data/app/components/orbital_ui/button_group.rb +20 -0
- data/app/components/orbital_ui/card/action.rb +17 -0
- data/app/components/orbital_ui/card/content.rb +17 -0
- data/app/components/orbital_ui/card/description.rb +17 -0
- data/app/components/orbital_ui/card/footer.rb +17 -0
- data/app/components/orbital_ui/card/header.rb +25 -0
- data/app/components/orbital_ui/card/title.rb +17 -0
- data/app/components/orbital_ui/card.rb +30 -0
- data/app/components/orbital_ui/check_box.rb +59 -0
- data/app/components/orbital_ui/component.rb +12 -0
- data/app/components/orbital_ui/concerns/icon_prop.rb +14 -0
- data/app/components/orbital_ui/confirmation_dialog.rb +142 -0
- data/app/components/orbital_ui/delete_dialog.rb +143 -0
- data/app/components/orbital_ui/dialog.rb +46 -0
- data/app/components/orbital_ui/dialog_frame.rb +17 -0
- data/app/components/orbital_ui/dropdown.rb +58 -0
- data/app/components/orbital_ui/expander.rb +20 -0
- data/app/components/orbital_ui/grid.rb +22 -0
- data/app/components/orbital_ui/heading.rb +65 -0
- data/app/components/orbital_ui/icon.rb +21 -0
- data/app/components/orbital_ui/image.rb +26 -0
- data/app/components/orbital_ui/inline_stack.rb +31 -0
- data/app/components/orbital_ui/kbd.rb +15 -0
- data/app/components/orbital_ui/kbd_group.rb +15 -0
- data/app/components/orbital_ui/menu/item.rb +74 -0
- data/app/components/orbital_ui/menu/label.rb +20 -0
- data/app/components/orbital_ui/menu/separator.rb +21 -0
- data/app/components/orbital_ui/menu/sub.rb +68 -0
- data/app/components/orbital_ui/menu.rb +36 -0
- data/app/components/orbital_ui/modal.rb +36 -0
- data/app/components/orbital_ui/modal_frame.rb +17 -0
- data/app/components/orbital_ui/navigation_menu.rb +51 -0
- data/app/components/orbital_ui/popcard.rb +18 -0
- data/app/components/orbital_ui/popover.rb +50 -0
- data/app/components/orbital_ui/progress_bar.rb +33 -0
- data/app/components/orbital_ui/prose.rb +15 -0
- data/app/components/orbital_ui/select.rb +140 -0
- data/app/components/orbital_ui/separator.rb +22 -0
- data/app/components/orbital_ui/spinner.rb +14 -0
- data/app/components/orbital_ui/stack.rb +29 -0
- data/app/components/orbital_ui/text.rb +41 -0
- data/app/components/orbital_ui/text_field.rb +132 -0
- data/app/components/orbital_ui/tooltip.rb +42 -0
- data/app/components/orbital_ui/typography.rb +38 -0
- data/lib/orbital_ui/attribute_utils.rb +152 -0
- data/lib/orbital_ui/components/attributes.rb +164 -0
- data/lib/orbital_ui/components/styles.rb +40 -0
- data/lib/orbital_ui/engine.rb +7 -0
- data/lib/orbital_ui/icons.rb +17312 -0
- data/lib/orbital_ui/type/any.rb +17 -0
- data/lib/orbital_ui/type/array.rb +27 -0
- data/lib/orbital_ui/type/breakpoints.rb +46 -0
- data/lib/orbital_ui/type/symbol.rb +17 -0
- data/lib/orbital_ui/version.rb +5 -0
- data/lib/orbital_ui.rb +49 -0
- metadata +162 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class OrbitalUi::Text < OrbitalUi::Component
|
|
4
|
+
SIZE_CLASSES = { xs: "text-body-xs", sm: "text-body-sm", lg: "text-body-lg", xl: "text-body-xl" }.freeze
|
|
5
|
+
WEIGHT_CLASSES = { light: "font-light", medium: "font-medium", semibold: "font-semibold", bold: "font-bold" }.freeze
|
|
6
|
+
|
|
7
|
+
attribute :size, :symbol, only: %i[xs sm md lg xl], default: :md
|
|
8
|
+
attribute :weight, :symbol, only: %i[light normal medium semibold bold], default: :normal
|
|
9
|
+
attribute :align, :symbol, only: %i[left center right], default: :left
|
|
10
|
+
attribute :tone, :symbol, only: %i[default subdued success warning danger info magic], default: :default
|
|
11
|
+
attribute :as, :string, default: "span"
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
content_tag @as, **html_attributes do
|
|
15
|
+
content
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def default_attributes
|
|
22
|
+
attrs = super.merge(
|
|
23
|
+
class: class_names(
|
|
24
|
+
'Orbital-Text',
|
|
25
|
+
size_classes,
|
|
26
|
+
weight_classes
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
attrs[:"data-tone"] = @tone unless @tone == :default
|
|
30
|
+
attrs[:"data-align"] = @align unless @align == :left
|
|
31
|
+
attrs
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def size_classes
|
|
35
|
+
SIZE_CLASSES[@size]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def weight_classes
|
|
39
|
+
WEIGHT_CLASSES[@weight]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrbitalUi
|
|
4
|
+
class TextField < Component
|
|
5
|
+
attribute :type, :string, default: "text"
|
|
6
|
+
attribute :disabled, :boolean, default: false
|
|
7
|
+
attribute :readonly, :boolean, default: false
|
|
8
|
+
attribute :name, :string, default: nil
|
|
9
|
+
attribute :value, :string, default: nil
|
|
10
|
+
attribute :label, :string, default: nil
|
|
11
|
+
attribute :labelHidden, :boolean, default: false
|
|
12
|
+
attribute :iconLeft, :any, default: nil
|
|
13
|
+
attribute :iconRight, :any, default: nil
|
|
14
|
+
attribute :prefix, :string, default: nil
|
|
15
|
+
attribute :suffix, :string, default: nil
|
|
16
|
+
attribute :multiline, :integer, default: nil
|
|
17
|
+
attribute :required, :boolean, default: false
|
|
18
|
+
attribute :placeholder, :string, default: nil
|
|
19
|
+
attribute :help, :string, default: nil
|
|
20
|
+
attribute :error, :string, default: nil
|
|
21
|
+
attribute :autofocus, :boolean, default: false
|
|
22
|
+
attribute :fullwidth, :boolean, default: false
|
|
23
|
+
attribute :rightAligned, :boolean, default: false
|
|
24
|
+
attribute :resizable, :boolean, default: false
|
|
25
|
+
|
|
26
|
+
orb_template <<-ORB
|
|
27
|
+
<div class="Orbital-Field" data-fullwidth={@fullwidth || nil}>
|
|
28
|
+
<label for={input_id} class="Orbital-Field-Label" data-hidden={@labelHidden || nil} :if={render_label?}>
|
|
29
|
+
<Text size="sm" weight=medium>{{label_text}}</Text>
|
|
30
|
+
</label>
|
|
31
|
+
<div class="Orbital-Input relative flex items-center">
|
|
32
|
+
<div class="Orbital-Input-Icon" data-position="left" :if={@iconLeft || @prefix}>
|
|
33
|
+
<Icon name={@iconLeft} size=md class="p-1 text-gray-300" :if={@iconLeft} />
|
|
34
|
+
<Text tone="subdued" :if={@prefix}>{{ @prefix }}</Text>
|
|
35
|
+
</div>
|
|
36
|
+
{{ render_input }}
|
|
37
|
+
<div class="Orbital-Input-Icon" data-position="right" :if={@iconRight || @suffix}>
|
|
38
|
+
<Icon name={@iconRight} size=md class="p-1 text-gray-300" :if={@iconRight} />
|
|
39
|
+
<Text tone="subdued" :if={@suffix}>{{ @suffix }}</Text>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="Orbital-Field-Help leading-none" :if={help? || error?}>
|
|
43
|
+
{% if error? %}
|
|
44
|
+
<Text size="sm" weight=medium tone="danger" class="leading-none!">{{ @error }}</Text>
|
|
45
|
+
{% else %}
|
|
46
|
+
<Text size="sm" tone="subdued" class="leading-none!">{{ @help }}</Text>
|
|
47
|
+
{% end %}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
ORB
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def label_classes
|
|
55
|
+
"Orbital-Field-Label"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def label_text
|
|
59
|
+
@label || @name.to_s.humanize
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_label?
|
|
63
|
+
(@label.present? || @name.present?) && !@labelHidden
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def help?
|
|
67
|
+
@help.present?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def error?
|
|
71
|
+
@error.present?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_input
|
|
75
|
+
common_opts = {
|
|
76
|
+
id: input_id,
|
|
77
|
+
name: @name,
|
|
78
|
+
value: @value,
|
|
79
|
+
disabled: @disabled,
|
|
80
|
+
readonly: @readonly,
|
|
81
|
+
required: @required,
|
|
82
|
+
placeholder: @placeholder,
|
|
83
|
+
autofocus: @autofocus,
|
|
84
|
+
autocomplete: "off",
|
|
85
|
+
class: input_classes,
|
|
86
|
+
"data-error": error? || nil,
|
|
87
|
+
"data-align": @rightAligned ? "right" : nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if @multiline
|
|
91
|
+
tag.textarea(**common_opts, rows: @multiline)
|
|
92
|
+
else
|
|
93
|
+
tag.input(type: @type, **common_opts)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def input_padding_left
|
|
98
|
+
if @iconLeft && @prefix
|
|
99
|
+
"pl-11"
|
|
100
|
+
elsif @iconLeft || @prefix
|
|
101
|
+
"pl-8"
|
|
102
|
+
else
|
|
103
|
+
"pl-2"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def input_padding_right
|
|
108
|
+
if @iconRight && @suffix
|
|
109
|
+
"pr-10"
|
|
110
|
+
elsif @iconRight || @suffix
|
|
111
|
+
"pr-8"
|
|
112
|
+
else
|
|
113
|
+
"pr-2"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def input_classes
|
|
118
|
+
padding_left = input_padding_left
|
|
119
|
+
padding_right = input_padding_right
|
|
120
|
+
|
|
121
|
+
class_names(
|
|
122
|
+
padding_left,
|
|
123
|
+
padding_right,
|
|
124
|
+
"noresize" => !@resizable
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def input_id
|
|
129
|
+
@id ||= "input-#{SecureRandom.hex(4)}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrbitalUi
|
|
4
|
+
class Tooltip < Component
|
|
5
|
+
attribute :id, :string, default: -> { "popover-#{SecureRandom.hex(4)}" }
|
|
6
|
+
attribute :on, :symbol, default: :hover, only: [:click, :hover]
|
|
7
|
+
attribute :position, :symbol, default: :n, only: [:auto, :n, :ne, :e, :se, :s, :sw, :w, :nw]
|
|
8
|
+
attribute :text, :string, default: ""
|
|
9
|
+
|
|
10
|
+
orb_template <<-ORB
|
|
11
|
+
<div **html_attributes>
|
|
12
|
+
<div data-orbital-popover-target="trigger">
|
|
13
|
+
{{content}}
|
|
14
|
+
</div>
|
|
15
|
+
<div
|
|
16
|
+
popover
|
|
17
|
+
id={@id}
|
|
18
|
+
data-orbital-popover-target="content"
|
|
19
|
+
class="Orbital-Tooltip-Content"
|
|
20
|
+
role="tooltip"
|
|
21
|
+
data-position={@position}>
|
|
22
|
+
{{@text}}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
ORB
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def default_attributes
|
|
30
|
+
super.merge(
|
|
31
|
+
class: "Orbital-Tooltip",
|
|
32
|
+
data: {
|
|
33
|
+
'controller' => "orbital-popover",
|
|
34
|
+
'orbital-popover-trigger-value' => @on,
|
|
35
|
+
'orbital-popover-position-value' => @position,
|
|
36
|
+
'orbital-popover-show-delay-value' => 200,
|
|
37
|
+
'orbital-popover-hide-delay-value' => 0
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrbitalUi
|
|
4
|
+
class Typography < Component
|
|
5
|
+
TAG_MAPPING = {
|
|
6
|
+
h1: :h1,
|
|
7
|
+
h2: :h2,
|
|
8
|
+
h3: :h3,
|
|
9
|
+
h4: :h4,
|
|
10
|
+
p: :p,
|
|
11
|
+
blockquote: :blockquote,
|
|
12
|
+
list: :ul,
|
|
13
|
+
lead: :p,
|
|
14
|
+
large: :div,
|
|
15
|
+
small: :small,
|
|
16
|
+
muted: :p
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
attribute :variant, :symbol, default: :p, only: TAG_MAPPING.keys
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
content_tag(tag_name, content, **html_attributes)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def tag_name
|
|
28
|
+
TAG_MAPPING[@variant] || :p
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_attributes
|
|
32
|
+
super.merge(
|
|
33
|
+
class: "Orbital-Typography",
|
|
34
|
+
"data-variant": @variant
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/keys"
|
|
4
|
+
|
|
5
|
+
module OrbitalUi
|
|
6
|
+
module AttributeUtils
|
|
7
|
+
# DEFAULT_MERGEABLE_ATTRIBUTES is a list of HTML attributes where the value
|
|
8
|
+
# contain multiple elements separated by spaces. We use it to target values
|
|
9
|
+
# to split so the arrays can be cleanly merged.
|
|
10
|
+
#
|
|
11
|
+
# They are stored as nested arrays so when we're walking multiple
|
|
12
|
+
# levels on the deep merge the structure can be identified. This means
|
|
13
|
+
# the library works with the Rails-preferred format of
|
|
14
|
+
# `aria: { describedby: "xyz" }` rather than `"aria-describedby" => "xyz"`
|
|
15
|
+
DEFAULT_MERGEABLE_ATTRIBUTES = [
|
|
16
|
+
%i[class],
|
|
17
|
+
%i[style],
|
|
18
|
+
%i[aria controls],
|
|
19
|
+
%i[aria describedby],
|
|
20
|
+
%i[aria flowto],
|
|
21
|
+
%i[aria labelledby],
|
|
22
|
+
%i[data aria_controls],
|
|
23
|
+
%i[data action],
|
|
24
|
+
%i[data controller],
|
|
25
|
+
%i[aria owns],
|
|
26
|
+
%i[rel]
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
refine Hash do
|
|
30
|
+
# Merge the incoming hash into the current one in a way that suits
|
|
31
|
+
# HTML attributes.
|
|
32
|
+
#
|
|
33
|
+
# @param custom [Hash] the incoming hash
|
|
34
|
+
# @param parents [Array] used to keep track of the keys that have already been
|
|
35
|
+
# merged, *don't supply this unless the method is called during recursion*
|
|
36
|
+
# @mergeable_attributes [Array<Array<Symbol>>] Mergeable attributes are
|
|
37
|
+
# HTML attributes can contain lists made from space-separated strings. We
|
|
38
|
+
# convert them to arrays so they can be cleanly merged. Rails accepts them
|
|
39
|
+
# as arrays so there's no need to convert back to strings.
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# original = { class: "red", data: { size: "medium", controller: "comment" } }
|
|
43
|
+
# incoming = { class: "blue", data: { controller: "reply" } }
|
|
44
|
+
#
|
|
45
|
+
# original.merge_html_attributes(incoming)
|
|
46
|
+
#
|
|
47
|
+
# => {:class=>["red", "blue"], :data=>{:size=>"medium", :controller=>"reply comment"}}
|
|
48
|
+
#
|
|
49
|
+
def merge_html_attributes(custom, parents = [], mergeable_attributes: DEFAULT_MERGEABLE_ATTRIBUTES)
|
|
50
|
+
return custom unless custom.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
overrides = custom.deep_symbolize_keys
|
|
53
|
+
originals = deep_symbolize_keys
|
|
54
|
+
|
|
55
|
+
originals.each_with_object(originals) do |(key, value), merged|
|
|
56
|
+
next unless overrides.key?(key)
|
|
57
|
+
|
|
58
|
+
merged[key] =
|
|
59
|
+
process_pair(key, value, parents: parents, overrides: overrides, mergeable_attributes: mergeable_attributes)
|
|
60
|
+
|
|
61
|
+
overrides.delete(key)
|
|
62
|
+
end.merge(overrides)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Remove unwanted attributes from a the hash and any values that are hashes
|
|
66
|
+
# recursively. In particular we don't care for empty hashes, arrays,
|
|
67
|
+
# strings that are empty or just contain spaces and nils.
|
|
68
|
+
#
|
|
69
|
+
# It preserves +true+ and +false+.
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] the tidied hash
|
|
72
|
+
# @example
|
|
73
|
+
# { class: "blue", title: nil, lang: "", aria: { describedby: [] } }.deep_tidy
|
|
74
|
+
#
|
|
75
|
+
# => { class: "blue" }
|
|
76
|
+
#
|
|
77
|
+
def deep_tidy
|
|
78
|
+
each_with_object({}) { |(k, v), tidied| (tidied[k] = tidy_value(v)) unless v.nil? }.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def process_pair(key, value, parents:, overrides:, mergeable_attributes:)
|
|
84
|
+
combine_values(
|
|
85
|
+
value,
|
|
86
|
+
overrides[key],
|
|
87
|
+
parents: [*parents, *key],
|
|
88
|
+
mergeable_attributes: mergeable_attributes
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def tidy_value(value)
|
|
93
|
+
case value
|
|
94
|
+
when TrueClass, FalseClass then value
|
|
95
|
+
when Hash then tidy_hash(value)
|
|
96
|
+
when Array then tidy_array(value)
|
|
97
|
+
else tidy_remaining(value)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def tidy_hash(hash)
|
|
102
|
+
return nil if hash.empty?
|
|
103
|
+
|
|
104
|
+
hash.deep_tidy
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def tidy_array(array)
|
|
108
|
+
return nil if array.empty?
|
|
109
|
+
|
|
110
|
+
array.filter_map { |v| tidy_value(v) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def tidy_remaining(value)
|
|
114
|
+
# Strip empty strings - Rails converts them to bare attributes
|
|
115
|
+
# for example:
|
|
116
|
+
# tag.div(hidden: "", data: { turbo_stream: "" })
|
|
117
|
+
# -> <div hidden data-turbo-stream></div>
|
|
118
|
+
value.to_s.strip.presence
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def combine_values(value, override, **kwargs)
|
|
122
|
+
case split_attribute_list(value, **kwargs)
|
|
123
|
+
when Array
|
|
124
|
+
combine_array(value, override, **kwargs)
|
|
125
|
+
when Hash
|
|
126
|
+
combine_hash(value, override, **kwargs)
|
|
127
|
+
else
|
|
128
|
+
split_attribute_list(override, **kwargs)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def combine_array(originals, overrides, parents:, mergeable_attributes:)
|
|
133
|
+
return overrides if overrides.nil?
|
|
134
|
+
return overrides unless mergeable_attributes.include?(parents)
|
|
135
|
+
|
|
136
|
+
(try_split(originals) + try_split(overrides)).uniq
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def try_split(value)
|
|
140
|
+
value.is_a?(String) ? value.split : value
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def combine_hash(originals, overrides, parents:, mergeable_attributes:)
|
|
144
|
+
originals.merge_html_attributes(overrides, parents, mergeable_attributes: mergeable_attributes)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def split_attribute_list(value, parents:, mergeable_attributes:)
|
|
148
|
+
mergeable_attributes.include?(parents) ? try_split(value) : value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrbitalUi
|
|
4
|
+
module Components
|
|
5
|
+
module Attributes
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
using OrbitalUi::AttributeUtils
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
ActiveModel::Type.register(:any, OrbitalUi::Type::Any)
|
|
12
|
+
ActiveModel::Type.register(:array, OrbitalUi::Type::Array)
|
|
13
|
+
ActiveModel::Type.register(:symbol, OrbitalUi::Type::Symbol)
|
|
14
|
+
ActiveModel::Type.register(:breakpoints, OrbitalUi::Type::Breakpoints)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
##
|
|
19
|
+
# Define an attribute for the component
|
|
20
|
+
#
|
|
21
|
+
# Example:
|
|
22
|
+
# attribute :label, :string, default: 'More'
|
|
23
|
+
#
|
|
24
|
+
# @param name [Symbol] the name of the attribute
|
|
25
|
+
# @param type [Symbol] the type of the attribute
|
|
26
|
+
# @param default [Object] the default value of the attribute
|
|
27
|
+
# @param options [Hash] additional options for the attribute
|
|
28
|
+
def attribute(name, type = nil, default: nil, **options)
|
|
29
|
+
# A redefined attribute replaces the inherited one. If both stayed, initialize would
|
|
30
|
+
# process it twice, and the second pass would reset the caller's value to the default.
|
|
31
|
+
attribute_definitions.reject! { |definition| definition[:name] == name }
|
|
32
|
+
attribute_definitions << { name: name, type: type, default: default, **options }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# Used to store the attribute definitions for the component
|
|
37
|
+
def attribute_definitions
|
|
38
|
+
@attribute_definitions ||= if superclass.respond_to?(:attribute_definitions)
|
|
39
|
+
superclass.attribute_definitions.dup
|
|
40
|
+
else
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(kwargs = {})
|
|
47
|
+
# Set the default values for the attributes, or the values passed in the keyword arguments
|
|
48
|
+
self.class.attribute_definitions.each do |definition|
|
|
49
|
+
name = definition[:name]
|
|
50
|
+
default_value = __default_value_for_attribute_definition(definition)
|
|
51
|
+
raw_value = kwargs.delete(name)
|
|
52
|
+
|
|
53
|
+
raise "Missing required attribute :#{name}" if raw_value.nil? && definition[:required]
|
|
54
|
+
|
|
55
|
+
value = ActiveModel::Type.lookup(definition[:type]).cast(raw_value)
|
|
56
|
+
|
|
57
|
+
# Assign the default value to a new instance variable
|
|
58
|
+
instance_variable_set(:"@#{name}", default_value)
|
|
59
|
+
|
|
60
|
+
# Define a getter method for the attribute
|
|
61
|
+
define_singleton_method(:"attr_#{name}") { instance_variable_get(:"@#{name}") }
|
|
62
|
+
|
|
63
|
+
# Assign the value passed in the keyword arguments to the instance variable if possible
|
|
64
|
+
next if value.nil?
|
|
65
|
+
|
|
66
|
+
if definition[:only]&.exclude?(value)
|
|
67
|
+
if definition[:unknown] == :raise
|
|
68
|
+
raise ArgumentError, "Invalid value for attribute #{name}: #{value.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
next
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
instance_variable_set(:"@#{name}", value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# All remaining keyword arguments are stored in the @unmatched_attributes instance variable
|
|
78
|
+
instance_variable_set(:@unmatched_attributes, kwargs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Returns the default attributes of the component as a hash
|
|
83
|
+
#
|
|
84
|
+
def default_attributes
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Returns the HTML attributes of the component as a hash
|
|
90
|
+
def html_attributes
|
|
91
|
+
__flatten_attributes(default_attributes.merge_html_attributes(@unmatched_attributes).deep_tidy)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Returns the attributes of the component as a hash
|
|
96
|
+
#
|
|
97
|
+
# @param include_options [Boolean] whether to include the options in the result
|
|
98
|
+
# @param flattened [Boolean] whether to flatten the result
|
|
99
|
+
# @return [Hash] the attributes of the component
|
|
100
|
+
def attrs(include_options: false, flattened: false)
|
|
101
|
+
result = {}
|
|
102
|
+
self.class.attribute_definitions.each do |definition|
|
|
103
|
+
name = definition[:name]
|
|
104
|
+
value = instance_variable_get(:"@#{name}")
|
|
105
|
+
result[name] = value
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
result.merge!(@unmatched_attributes || {}) if include_options
|
|
109
|
+
if flattened
|
|
110
|
+
__flatten_attributes(result)
|
|
111
|
+
else
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Returns the options of the component as a hash
|
|
118
|
+
#
|
|
119
|
+
# The options are the keyword arguments passed to the component that were not
|
|
120
|
+
# matched against the defined attributes.
|
|
121
|
+
#
|
|
122
|
+
# @param flattened [Boolean] whether to flatten the result
|
|
123
|
+
# @return [Hash] the options of the component
|
|
124
|
+
def options(flattened: true)
|
|
125
|
+
result = @unmatched_attributes || {}
|
|
126
|
+
if flattened
|
|
127
|
+
__flatten_attributes(result)
|
|
128
|
+
else
|
|
129
|
+
result
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def __default_value_for_attribute_definition(definition)
|
|
136
|
+
default_value = definition.fetch(:default, nil)
|
|
137
|
+
return default_value if default_value.nil?
|
|
138
|
+
|
|
139
|
+
default_value = default_value.call if default_value.respond_to?(:call)
|
|
140
|
+
default_value
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def __flatten_attributes(attributes)
|
|
144
|
+
flattened = {}
|
|
145
|
+
|
|
146
|
+
attributes.each do |key, value|
|
|
147
|
+
case value
|
|
148
|
+
when Hash
|
|
149
|
+
__flatten_attributes(value).each do |k, v|
|
|
150
|
+
if k.nil?
|
|
151
|
+
flattened[key] = v
|
|
152
|
+
else
|
|
153
|
+
flattened["#{key}-#{k.to_s.tr('_', '-')}"] = v
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
flattened[key] = value if value
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
flattened
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrbitalUi
|
|
4
|
+
module Components
|
|
5
|
+
module Styles
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Join the given style names together
|
|
10
|
+
#
|
|
11
|
+
# This implementation follows the same pattern as the `class_names` method
|
|
12
|
+
# however, it does not split the values by spaces.
|
|
13
|
+
#
|
|
14
|
+
def style_names(*args)
|
|
15
|
+
styles = build_style_names(*args).flat_map { |value| CGI.unescape_html(value.to_s) }.uniq
|
|
16
|
+
safe_join(styles.push(''), ";")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_style_names(*args)
|
|
22
|
+
values = []
|
|
23
|
+
args.each do |arg|
|
|
24
|
+
case arg
|
|
25
|
+
when Hash
|
|
26
|
+
arg.each do |k, v|
|
|
27
|
+
values << k if v && k.present?
|
|
28
|
+
end
|
|
29
|
+
when Array
|
|
30
|
+
values.concat build_style_names(*arg)
|
|
31
|
+
else
|
|
32
|
+
values << arg.to_s if arg.present?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
values
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|