bootstrap5_helper 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,145 @@
1
+ module Bootstrap5Helper
2
+ # Builds a Nav Component that can be used in other components.
3
+ #
4
+ #
5
+ class Nav < Component
6
+ # Class constructor
7
+ #
8
+ # @param [ActionView] template
9
+ # @param [Symbol|Hash] tag_or_options
10
+ # @param [Hash] opts
11
+ # @option opts [String] :id
12
+ # @option opts [String] :class
13
+ # @option opts [Hash] :data
14
+ # @option opts [Hash] :child
15
+ #
16
+ def initialize(template, tag_or_options = nil, opts = {}, &block)
17
+ super(template)
18
+
19
+ @tag, args = parse_tag_or_options(tag_or_options, opts)
20
+ @tag ||= config({ navs: :base }, :ul)
21
+
22
+ @id = args.fetch(:id, uuid)
23
+ @class = args.fetch(:class, '')
24
+ @data = args.fetch(:data, {})
25
+ @child = args.fetch(:child, {})
26
+ @content = block || proc { '' }
27
+
28
+ @dropdown = Dropdown.new(@template)
29
+ end
30
+
31
+ # rubocop:disable Metrics/MethodLength
32
+
33
+ # Adds an nav-item to the nav component. this method gets used when the nav-item
34
+ # links to content in a tab or something.
35
+ #
36
+ # @param [Symbol|String] target
37
+ # @param [Hash] opts
38
+ # @option opts [String] :id
39
+ # @option opts [String] :class
40
+ # @option opts [Hash] :data
41
+ # @option opts [Hash] :aria
42
+ # @return [String]
43
+ #
44
+ def item(target, opts = {})
45
+ id = opts.fetch(:id, nil)
46
+ klass = opts.fetch(:class, '')
47
+ data = opts.fetch(:data, {})
48
+ aria = opts.fetch(:aria, {})
49
+
50
+ nav_item_wrapper id: id, data: data do
51
+ content_tag(
52
+ :a,
53
+ class: "nav-link #{klass}",
54
+ href: "##{target}",
55
+ tabindex: -1,
56
+ data: @child[:data],
57
+ aria: aria
58
+ ) do
59
+ block_given? ? yield : target.to_s.titleize
60
+ end
61
+ end
62
+ end
63
+ # rubocop:enable Metrics/MethodLength
64
+
65
+ # Use this when the nav item is nothing more than a hyperlink.
66
+ #
67
+ # @param [String|NilClass] name
68
+ # @param [Hash|NilClass] options
69
+ # @param [Hash|NilClass] html_options
70
+ # @return [String]
71
+ #
72
+ def link(name = nil, options = nil, html_options = nil, &block)
73
+ html_options ||= {}
74
+ html_options[:class] = (html_options[:class] || '') << ' nav-link'
75
+
76
+ nav_item_wrapper do
77
+ @template.link_to(name, options, html_options, &block)
78
+ end
79
+ end
80
+
81
+ # Creates a dropdown menu for the nav.
82
+ #
83
+ # @param [NilClass|Symbol|String] name
84
+ # @param [Hash] opts
85
+ # @option opts [String] :id
86
+ # @option opts [String] :class
87
+ # @option opts [Hash] :data
88
+ # @option opts [Hash] :aria
89
+ # @return [String]
90
+ #
91
+ def dropdown(name, opts = {}, &block)
92
+ id = opts.fetch(:id, nil)
93
+ klass = opts.fetch(:class, '')
94
+ data = opts.fetch(:data, {})
95
+ aria = opts.fetch(:aria, {}).merge(haspopup: true, expanded: false)
96
+
97
+ nav_item_wrapper id: id, class: 'dropdown', data: data do
98
+ content_tag(
99
+ :a,
100
+ name,
101
+ class: "nav-link dropdown-toggle #{klass}",
102
+ href: '#',
103
+ data: { 'bs-toggle' => 'dropdown' },
104
+ role: 'button',
105
+ aria: aria
106
+ ) + @dropdown.menu(opts, &block).to_s.html_safe
107
+ end
108
+ end
109
+
110
+ # String representation of the object.
111
+ #
112
+ # @return [String]
113
+ #
114
+ def to_s
115
+ content_tag(@tag, id: @id, class: "nav #{@class}") do
116
+ @content.call(self)
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ # Decorator for elements require a wrapper component.
123
+ #
124
+ # @return [String]
125
+ #
126
+ def nav_item_wrapper(opts = {}, &block)
127
+ id = opts.fetch(:id, '')
128
+ klass = opts.fetch(:class, '')
129
+ data = opts.fetch(:data, {})
130
+
131
+ case @tag
132
+ when :nav
133
+ block.call
134
+ when :ul
135
+ content_tag(
136
+ :li,
137
+ id: id,
138
+ class: "nav-item #{klass}",
139
+ data: data,
140
+ &block
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,127 @@
1
+ module Bootstrap5Helper
2
+ class Offcanvas
3
+ # Builds a Content component for use in offcanvas.
4
+ #
5
+ #
6
+ class Content < Component
7
+ # Constructor description...
8
+ #
9
+ #
10
+ # @param [Hash] opts
11
+ # @return [ClassName]
12
+ #
13
+ def initialize(template, opts = {}, &block)
14
+ super(template)
15
+
16
+ @id = opts.fetch(:id, uuid)
17
+ @class = opts.fetch(:class, '')
18
+ @data = opts.fetch(:data, {})
19
+ @aria = opts.fetch(:aria, {})
20
+ @scrollable = opts.fetch(:scrollable, false)
21
+ @position = opts.fetch(:position, 'start')
22
+ @content = block || proc { '' }
23
+ end
24
+
25
+ # @todo
26
+ #
27
+ #
28
+ def header(opts = {}, &block)
29
+ id = opts.fetch(:id, nil)
30
+ klass = opts.fetch(:class, '')
31
+ data = opts.fetch(:data, {})
32
+
33
+ content_tag(
34
+ config({ offcanvas: :header }, :div),
35
+ id: id,
36
+ class: "offcanvas-header #{klass}",
37
+ data: data,
38
+ &block
39
+ )
40
+ end
41
+
42
+ # @todo
43
+ #
44
+ #
45
+ def title(text_or_options = nil, opts = {}, &block)
46
+ text, args = parse_text_or_options(text_or_options, opts)
47
+
48
+ id = args.fetch(:id, nil)
49
+ klass = args.fetch(:class, '')
50
+ data = args.fetch(:data, {})
51
+
52
+ content_tag(
53
+ config({ offcanvas: :title }, :h6),
54
+ text,
55
+ id: id,
56
+ class: "offcanvas-title #{klass}",
57
+ data: data,
58
+ &block
59
+ )
60
+ end
61
+
62
+ # @todo
63
+ #
64
+ #
65
+ def close_button(opts = {})
66
+ klass = opts.fetch(:class, '')
67
+
68
+ content_tag(
69
+ config({ offcanvas: :close }, :button),
70
+ class: block_given? ? klass : 'btn-close',
71
+ data: { 'bs-dismiss': 'offcanvas' },
72
+ aria: { label: 'Close' }
73
+ ) do
74
+ block_given? ? yield : xbutton
75
+ end
76
+ end
77
+
78
+ # @todo
79
+ #
80
+ #
81
+ def body(opts = {}, &block)
82
+ id = opts.fetch(:id, nil)
83
+ klass = opts.fetch(:class, '')
84
+ data = opts.fetch(:data, {})
85
+ aria = opts.fetch(:aria, {})
86
+
87
+ content_tag(
88
+ :div,
89
+ id: id,
90
+ class: "offcanvas-body #{klass}",
91
+ data: data,
92
+ aria: aria,
93
+ &block
94
+ )
95
+ end
96
+
97
+ # @todo
98
+ #
99
+ #
100
+ def to_s
101
+ @data.merge!('bs-scroll': @scrollable)
102
+ @data.merge!('bs-backdrop': @backdrop)
103
+
104
+ content_tag(
105
+ :div,
106
+ id: @id,
107
+ class: "offcanvas offcanvas-#{@position} #{@class}",
108
+ tabindex: -1,
109
+ data: @data,
110
+ aria: { labelledby: 'offcanvasExampleLabel' }
111
+ ) do
112
+ @content.call(self)
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ # Builds the `x` button normally used in the header.
119
+ #
120
+ # @return [String]
121
+ #
122
+ def xbutton
123
+ content_tag :span, '&times;'.html_safe, class: 'visually-hidden', aria: { hidden: true }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,169 @@
1
+ module Bootstrap5Helper
2
+ # Builds a Offcanvas component.
3
+ #
4
+ #
5
+ class Offcanvas < Component
6
+ # Class constructor
7
+ #
8
+ # @param [ActionView]
9
+ # @param [NilClass|Symbol|Hash] position_or_options - :start, :end, :top, :bottom
10
+ # @param [Hash] opts
11
+ # @option opts [String] :id
12
+ # @option opts [String] :class
13
+ # @option opts [Hash] :data
14
+ # @option opts [Hash] :aria
15
+ # @option opts [Boolean] :scrollable
16
+ # @option opts [Boolean|String] :backdrop - true, false, static
17
+ # @return [Offcanvas]
18
+ #
19
+ def initialize(template, position_or_options = nil, opts = {}, &block)
20
+ super(template)
21
+ @pos, args = parse_position_or_options(position_or_options, opts)
22
+ @id = args.fetch(:id, uuid)
23
+ @class = args.fetch(:class, '')
24
+ @data = args.fetch(:data, {})
25
+ @aria = args.fetch(:aria, {})
26
+ @scrollable = args.fetch(:scrollable, false)
27
+ @backdrop = args.fetch(:backdrop, true)
28
+ @content = block || proc { '' }
29
+ end
30
+ # rubocop:disable Metrics/MethodLength
31
+
32
+ # Creates a button element to act as the trigger for the offcanvas component.
33
+ #
34
+ # @param [NilClass|String|Hash] text_or_options
35
+ # @param [Hash] opts
36
+ # @option opts [String] :id
37
+ # @option opts [String] :class
38
+ # @option opts [Hash] :data
39
+ # @option opts [Hash] :aria
40
+ # @return [String]
41
+ #
42
+ def button(text_or_options = nil, opts = {}, &block)
43
+ text, args = parse_text_or_options(text_or_options, opts)
44
+
45
+ id = args.fetch(:id, nil)
46
+ klass = args.fetch(:class, '')
47
+
48
+ data = args.fetch(:data, {}).merge!(
49
+ 'bs-toggle' => 'offcanvas',
50
+ 'bs-target' => "##{@id}"
51
+ )
52
+
53
+ aria = args.fetch(:aria, {}).merge!(
54
+ 'controls' => @id
55
+ )
56
+
57
+ content_tag(
58
+ :button,
59
+ text,
60
+ type: :button,
61
+ id: id,
62
+ class: klass,
63
+ data: data,
64
+ aria: aria,
65
+ &block
66
+ )
67
+ end
68
+ # rubocop:enable Metrics/MethodLength
69
+
70
+ # Creates a simple link to toggle the offcanvas content.
71
+ #
72
+ # @param [String|Hash|NilClass] text_or_options
73
+ # @param [Hash] opts
74
+ # @option opts [String] :id
75
+ # @option opts [String] :class
76
+ # @option opts [Hash] :data
77
+ # @option opts [Hash] :aria
78
+ # @return [String]
79
+ #
80
+ def link(text_or_options = nil, opts = {}, &block)
81
+ text, args = parse_text_or_options(text_or_options, opts)
82
+ args[:data] = args.fetch(:data, {}).merge!({ 'bs-toggle': 'offcanvas' })
83
+ args[:aria] = args.fetch(:aria, {}).merge!({ 'controls': @id })
84
+
85
+ options = ["##{@id}", args]
86
+ options.prepend(text) if text.present?
87
+
88
+ @template.link_to(*options, &block)
89
+ end
90
+
91
+ # rubocop:disable Metrics/MethodLength
92
+
93
+ # Used to make a custom dom element for a trigger. Use this when the
94
+ # trigger isnt a link or button.
95
+ #
96
+ # @param [Symbol|Hash|NilClass] tag_or_options
97
+ # @param [Hash] opts
98
+ # @option opts [String] :id
99
+ # @option opts [String] :class
100
+ # @option opts [Hash] :data
101
+ # @option opts [Hash] :aria
102
+ # @return [String]
103
+ #
104
+ def trigger(tag_or_options = nil, opts = {}, &block)
105
+ tag, args = parse_tag_or_options(tag_or_options, opts)
106
+
107
+ id = args.fetch(:id, nil)
108
+ klass = args.fetch(:class, '')
109
+ aria = args.fetch(:aria, {}).merge!({ 'controls': @id })
110
+ data = args.fetch(:data, {}).merge!(
111
+ 'bs-toggle': 'offcanvas',
112
+ 'bs-target': "##{@id}"
113
+ )
114
+
115
+ content_tag(
116
+ tag || config({ offcanvas: :trigger }, :div),
117
+ id: id,
118
+ class: klass,
119
+ data: data,
120
+ aria: aria,
121
+ &block
122
+ )
123
+ end
124
+ # rubocop:enable Metrics/MethodLength
125
+
126
+ # Used to generate the main component. This class serves as a wrapper, so
127
+ # that buttons and links have reference to the content component.
128
+ #
129
+ # @return [String]
130
+ #
131
+ def content(&block)
132
+ Content.new(
133
+ @template,
134
+ {
135
+ id: @id,
136
+ class: @class,
137
+ data: @data,
138
+ aria: @aria,
139
+ scrollable: @scrollable,
140
+ backdrop: @backdrop,
141
+ position: @pos
142
+ },
143
+ &block
144
+ )
145
+ end
146
+
147
+ # Renders the component as a String, but only to the output bugger.
148
+ #
149
+ # @return [NilClass]
150
+ #
151
+ def to_s
152
+ @content.call(self)
153
+
154
+ nil
155
+ end
156
+
157
+ private
158
+
159
+ # Because this options parser is only going to be used here, I figured
160
+ # we would just define it here. That way other components don't have
161
+ # to inherit it.
162
+ #
163
+ # @return [Array]
164
+ #
165
+ def parse_position_or_options(*args)
166
+ parse_arguments(*args, 'start')
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,42 @@
1
+ module Bootstrap5Helper
2
+ # Builds a simple CSS spinner component.
3
+ #
4
+ #
5
+ class PageHeader < Component
6
+ # Class constructor
7
+ #
8
+ # @param [ActionView] template
9
+ # @param [Symbol|Hash] tag_or_options
10
+ # @param [Hash] opts
11
+ # @option opts [String] :id
12
+ # @option opts [String] :class
13
+ # @option opts [Hash] :data
14
+ #
15
+ def initialize(template, tag_or_options = nil, opts = {}, &block)
16
+ super(template)
17
+
18
+ @tag, args = parse_tag_or_options(tag_or_options, opts)
19
+ @tag ||= config(:page_header, :h1)
20
+
21
+ @id = args.fetch(:id, uuid)
22
+ @class = args.fetch(:class, '')
23
+ @data = args.fetch(:data, {})
24
+ @content = block || proc { '' }
25
+ end
26
+
27
+ # String representation of the object.
28
+ #
29
+ # @return [String]
30
+ #
31
+ def to_s
32
+ content_tag(
33
+ @tag,
34
+ id: @id,
35
+ class: "pb-2 mt-4 mb-2 border-bottom #{@class}",
36
+ data: @data
37
+ ) do
38
+ @content.call(self)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ module Bootstrap5Helper
2
+ # Simple Railtie to hook out module into ActionView.
3
+ #
4
+ #
5
+ class Railtie < ::Rails::Railtie
6
+ config.after_initialize do
7
+ ActiveSupport.on_load(:action_view) do
8
+ include Bootstrap5Helper if Bootstrap5Helper.config.autoload_in_views?
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,44 @@
1
+ module Bootstrap5Helper
2
+ # Builds a simple CSS spinner component.
3
+ #
4
+ #
5
+ class Spinner < Component
6
+ # Class constructor
7
+ #
8
+ # @note The different support types are: `:border` and `:grow`
9
+ #
10
+ # @param [ActionView] template
11
+ # @param [Hash] opts
12
+ # @option opts [Symbol] :type
13
+ # @option opts [String] :id
14
+ # @option opts [String] :class
15
+ # @option opts [Hash] :data
16
+ #
17
+ def initialize(template, opts = {}, &block)
18
+ super(template)
19
+
20
+ @type = opts.fetch(:type, :border)
21
+ @id = opts.fetch(:id, uuid)
22
+ @class = opts.fetch(:class, '')
23
+ @data = opts.fetch(:data, {})
24
+ @content = block || proc { '' }
25
+ end
26
+
27
+ # String representation of the object.
28
+ #
29
+ # @return [String]
30
+ #
31
+ def to_s
32
+ content_tag(
33
+ :span,
34
+ id: @id,
35
+ class: "spinner-#{@type} #{@class}",
36
+ role: 'status',
37
+ aria: { hidden: true },
38
+ data: @data
39
+ ) do
40
+ content_tag :span, 'Loading', class: 'visually-hidden'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ module Bootstrap5Helper
2
+ class Tab
3
+ # Build a Content component to be used with Tabs
4
+ #
5
+ #
6
+ class Content < Component
7
+ # Class constructor
8
+ #
9
+ # @param [ActionView] template
10
+ # @param [Hash] opts
11
+ # @option opts [String] :id
12
+ # @option opts [String] :class
13
+ # @option opts [Hash] :data
14
+ #
15
+ def initialize(template, opts = {}, &block)
16
+ super(template)
17
+
18
+ @id = opts.fetch(:id, uuid)
19
+ @class = opts.fetch(:class, '')
20
+ @data = opts.fetch(:data, {})
21
+ @content = block || proc { '' }
22
+ end
23
+
24
+ # Builds the pane for the tab.
25
+ #
26
+ # @param [Symbol] source
27
+ # @param [Hash] opts
28
+ # @option opts [String] :class
29
+ # @option opts [Hash] :data
30
+ # @return [String]
31
+ #
32
+ def pane(source, opts = {}, &block)
33
+ id = opts.fetch(:id, source)
34
+ klass = opts.fetch(:class, '')
35
+ data = opts.fetch(:data, {})
36
+
37
+ content_tag(
38
+ :div,
39
+ id: id,
40
+ class: "tab-pane #{klass}",
41
+ role: 'tabpanel',
42
+ data: data,
43
+ &block
44
+ )
45
+ end
46
+
47
+ # String representation of the object.
48
+ #
49
+ # @return [String]
50
+ #
51
+ def to_s
52
+ content_tag :div, id: @id, class: "tab-content #{@class}" do
53
+ @content.call(self)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,72 @@
1
+ module Bootstrap5Helper
2
+ # Builds a Tab component.
3
+ #
4
+ #
5
+ class Tab < Component
6
+ # Class constructor
7
+ #
8
+ # @param [ActionView] template
9
+ # @param [Symbol|String|Hash] type_or_options
10
+ # @param [Hash] opts
11
+ # @option opts [String] :id
12
+ # @option opts [String] :class
13
+ # @option opts [Hash] :data
14
+ #
15
+ def initialize(template, type_or_options = nil, opts = {}, &block)
16
+ super(template)
17
+ @type, args = type_or_options(type_or_options, opts)
18
+
19
+ @id = args.fetch(:id, uuid)
20
+ @class = args.fetch(:class, '')
21
+ @data = args.fetch(:data, {})
22
+ @content = block || proc { '' }
23
+ end
24
+
25
+ # Builds a custom Nav component for the tabs.
26
+ #
27
+ # @param [Symbol|Hash] tag_or_options
28
+ # @param [Hash] opts
29
+ # @option opts [String] :class
30
+ # @option opts [Hash] :data
31
+ # @return [Nav]
32
+ #
33
+ def nav(tag_or_options = nil, opts = {}, &block)
34
+ tag, args = parse_tag_or_options(tag_or_options, opts)
35
+
36
+ args[:class] = (args[:class] || '') << " nav-#{@type}"
37
+ args[:data] = (args[:data] || {}).merge('bs-toggle' => 'tab')
38
+ args[:child] = { data: { 'bs-toggle' => 'tab' } }
39
+
40
+ Nav.new(@template, tag, args, &block)
41
+ end
42
+
43
+ # Builds the Content object for the Tab.
44
+ #
45
+ # @param [Hash] opts
46
+ # @option opts [String] :id
47
+ # @option opts [String] :class
48
+ # @option opts [Hash] :data
49
+ # @return [Tab::Content]
50
+ #
51
+ def content(opts = {}, &block)
52
+ Content.new(@template, opts, &block)
53
+ end
54
+
55
+ # @note This has a weird interaction. Because this object doesn't actually return any wrapping
56
+ # string or DOM element, we want to return nil, so that only the output buffer on the sub components are
57
+ # returned.
58
+ #
59
+ # If we return the return value of the block, we will get the last element added to the input
60
+ # buffer as an unescaped string.
61
+ #
62
+ def to_s
63
+ @content.call(self)
64
+
65
+ nil
66
+ end
67
+
68
+ def type_or_options(*args)
69
+ parse_arguments(*args, :tabs)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,7 @@
1
+ module Bootstrap5Helper
2
+ # Builds a Toast component.
3
+ #
4
+ #
5
+ class Toast < Component
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Bootstrap5Helper
2
+ VERSION = '1.0.0'.freeze
3
+ end