coprl 3.0.0.beta.1 → 3.0.0.beta.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +9 -15
- data/CHANGELOG.md +105 -13
- data/Gemfile +12 -1
- data/Gemfile.lock +105 -32
- data/README.md +10 -5
- data/app/demo/components/dialogs.pom +1 -1
- data/app/demo/components/nav/menu.pom +0 -1
- data/app/demo/components/snackbar.pom +9 -3
- data/app/demo/events/content_as_form.pom +3 -3
- data/app/demo/events/halted.pom +23 -0
- data/app/demo/events/nav/drawer.pom +1 -1
- data/app/demo/events/tagged_input.pom +2 -2
- data/app/demo/patterns/search_select.pom +1 -1
- data/app/demo/plugins/animate.pom +144 -0
- data/app/demo/plugins/cacheable.pom +64 -0
- data/app/demo/plugins/clipboard.pom +21 -0
- data/app/demo/plugins/color_picker.pom +17 -0
- data/app/demo/plugins/google_maps.pom +24 -0
- data/app/demo/plugins/iframe.pom +14 -0
- data/app/demo/plugins/image_crop.pom +1 -1
- data/app/demo/plugins/markup.pom +14 -0
- data/app/demo/plugins/nav/drawer.pom +1 -1
- data/app/demo/plugins/script.pom +17 -0
- data/app/demo/plugins/scroll_to.pom +22 -0
- data/app/demo/plugins/timer.pom +24 -0
- data/app/demo/shared/context_list.pom +1 -1
- data/config.ru +15 -1
- data/coprl.gemspec +2 -2
- data/lib/coprl/presenters/cli.rb +10 -0
- data/lib/coprl/presenters/dsl/components/actions/base.rb +5 -1
- data/lib/coprl/presenters/dsl/components/base.rb +6 -3
- data/lib/coprl/presenters/dsl/components/event.rb +6 -1
- data/lib/coprl/presenters/dsl/components/multi_select.rb +3 -3
- data/lib/coprl/presenters/dsl/components/table.rb +2 -2
- data/lib/coprl/presenters/generators/plugin.rb +21 -6
- data/lib/coprl/presenters/generators/templates/plugin/.github/workflows/semantic-release.yml +41 -0
- data/lib/coprl/presenters/generators/templates/plugin/.releaserc +15 -0
- data/lib/coprl/presenters/generators/templates/plugin/.ruby-version +1 -0
- data/lib/coprl/presenters/generators/templates/plugin/README.md.tt +34 -0
- data/lib/coprl/presenters/generators/templates/plugin/demo/plugin.pom.tt +15 -0
- data/lib/coprl/presenters/generators/templates/plugin/lib/coprl/presenters/plugins/components/actions/dsl.rb.tt +1 -1
- data/lib/coprl/presenters/generators/templates/plugin/lib/coprl/presenters/plugins/version.rb.tt +3 -0
- data/lib/coprl/presenters/generators/templates/plugin/presenter_plugin.gemspec.tt +8 -7
- data/lib/coprl/presenters/generators/templates/plugin/views/components/application/component.erb.tt +1 -1
- data/lib/coprl/presenters/settings.rb +1 -1
- data/lib/coprl/presenters/version.rb +1 -1
- data/lib/coprl/presenters/web_client/helpers/headers.rb +8 -6
- data/lib/coprl/presenters/web_client/helpers/rails.rb +1 -0
- data/lib/coprl/presenters/web_client/helpers/rails/template_helper.rb +10 -0
- data/lib/coprl/presenters/web_client/helpers/sinatra.rb +1 -0
- data/lib/coprl/presenters/web_client/helpers/sinatra/template_helper.rb +20 -0
- data/public/bundle.js +10 -4
- data/public/wc.js +10 -4
- data/rails-engine/app/controllers/coprl_controller.rb +20 -1
- data/rails-engine/app/views/layouts/coprl.html.erb +4 -4
- data/rails-engine/config/initializers/session.rb +8 -0
- data/rails-engine/config/routes.rb +2 -1
- data/views/mdc/assets/js/components/events/posts.js +10 -6
- data/views/mdc/body/{_preamble.erb → _wrapper.erb} +16 -5
- data/views/mdc/components/_card.erb +2 -2
- data/views/mdc/components/_checkbox.erb +1 -1
- data/views/mdc/components/_chip.erb +2 -2
- data/views/mdc/components/_content.erb +2 -2
- data/views/mdc/components/_datetime.erb +1 -1
- data/views/mdc/components/_dialog.erb +2 -2
- data/views/mdc/components/_form.erb +2 -2
- data/views/mdc/components/_grid.erb +2 -2
- data/views/mdc/components/_hidden_field.erb +1 -1
- data/views/mdc/components/_multi_select.erb +1 -1
- data/views/mdc/components/_number_field.erb +1 -1
- data/views/mdc/components/_radio_button.erb +1 -1
- data/views/mdc/components/_rich_text_area.erb +1 -1
- data/views/mdc/components/_select.erb +1 -1
- data/views/mdc/components/_slider.erb +2 -2
- data/views/mdc/components/_stepper.erb +4 -4
- data/views/mdc/components/_switch.erb +1 -1
- data/views/mdc/components/_text_area.erb +1 -1
- data/views/mdc/components/_text_field.erb +1 -1
- data/views/mdc/components/buttons/_image.erb +1 -1
- data/views/mdc/components/unordered_list/_list_item.erb +3 -3
- data/views/mdc/layout.erb +4 -4
- metadata +31 -17
- data/app/demo/components/google_maps.pom +0 -22
- data/app/demo/components/snackbar_attached.pom +0 -6
- data/lib/coprl/presenters/generators/templates/plugin/README.md +0 -253
- data/lib/coprl/presenters/plugins/google_maps.rb +0 -24
- data/lib/coprl/presenters/plugins/google_maps/google_map.erb +0 -10
- data/lib/coprl/presenters/plugins/google_maps/google_map.rb +0 -41
- data/views/mdc/body/_postamble.erb +0 -17
data/README.md
CHANGED
@@ -64,7 +64,7 @@ To see the POM:
|
|
64
64
|
### Rails
|
65
65
|
Presenters are a view templating language in Rails.
|
66
66
|
You can mix and match presenters with your existing views,
|
67
|
-
use them as new views, or call them as
|
67
|
+
use them as new views, or call them as partials in existing views.
|
68
68
|
|
69
69
|
#### 1) Add presenters to Gemfile
|
70
70
|
gem 'coprl'
|
@@ -86,7 +86,12 @@ Navigate to [locahost:3000/hello_world](http://127.0.0.1:3000/hello_world)
|
|
86
86
|
|
87
87
|
Use the [Demo] to get example code to drop into your presenters.
|
88
88
|
|
89
|
-
####
|
89
|
+
#### Optional — Setting the root route
|
90
|
+
If you name a presenter :index and want the root of the app to serve up that presenter add the following to your `config/initializers/routes.rb`
|
91
|
+
|
92
|
+
root 'coprl#show'
|
93
|
+
|
94
|
+
#### Optionally — use presenters as partials from ERB/HAML
|
90
95
|
You can render a presenter as a partial from other templating laguages (erb, haml):
|
91
96
|
|
92
97
|
<%= render 'show', presenter: 'hello_world' %>
|
@@ -96,13 +101,13 @@ You need to add the following to your layout to use presenters as a partial alon
|
|
96
101
|
##### Inside the <head> tag add the following:
|
97
102
|
|
98
103
|
<title><%= @pom.page.title if @pom.page %></title>
|
99
|
-
<%= coprl_headers
|
104
|
+
<%= coprl_headers %>
|
100
105
|
|
101
106
|
##### Inside the <body> tag, around you existing yield add the following:
|
102
107
|
|
103
|
-
<%=
|
108
|
+
<%= with_presenters_wrapper do %>
|
104
109
|
<%= yield %>
|
105
|
-
<%=
|
110
|
+
<%= end %>
|
106
111
|
|
107
112
|
### Rack
|
108
113
|
#### 1) To use it, add this line to your Gemfile:
|
@@ -170,7 +170,7 @@ Coprl::Presenters.define(:dialogs) do
|
|
170
170
|
end
|
171
171
|
end
|
172
172
|
separator
|
173
|
-
content
|
173
|
+
content input_tag: :second_form_input do
|
174
174
|
dlg_form
|
175
175
|
end
|
176
176
|
subtitle 'The buttons below are Dialog Action buttons. When clicked, they will process any configured actions, then automatically clsoe the dialog.'
|
@@ -8,10 +8,10 @@ Coprl::Presenters.define(:snackbar) do
|
|
8
8
|
|
9
9
|
indented_grid do
|
10
10
|
title 'On Page'
|
11
|
-
body 'You can attach a snackbar on the server side and will render after load.'
|
11
|
+
body 'You can invoke or attach a snackbar on the server side and will render after load.'
|
12
12
|
snackbar 'Top Level Important Information!'
|
13
|
-
|
14
|
-
attach :snackbar_attached
|
13
|
+
# This is the same as above -- see POM at end of file
|
14
|
+
# attach :snackbar_attached
|
15
15
|
|
16
16
|
title 'As Event'
|
17
17
|
button 'Show Snackbar', id: :show_snackbar do
|
@@ -33,3 +33,9 @@ Coprl::Presenters.define(:snackbar) do
|
|
33
33
|
|
34
34
|
attach :code, file: __FILE__
|
35
35
|
end
|
36
|
+
|
37
|
+
# Coprl::Presenters.define(:snackbar_attached) do
|
38
|
+
# title 'In Attached'
|
39
|
+
# body 'This can be done in an attached presenter as well'
|
40
|
+
# snackbar 'Attached Snackbar Displayed!'
|
41
|
+
# end
|
@@ -118,7 +118,7 @@ Coprl::Presenters.define(:content_as_form) do
|
|
118
118
|
|
119
119
|
grid do
|
120
120
|
column 12 do
|
121
|
-
text_field id: :input_tag_extra, name: :whatever,
|
121
|
+
text_field id: :input_tag_extra, name: :whatever, input_tag: :test_input_tag do
|
122
122
|
value 'foo bar'
|
123
123
|
end
|
124
124
|
end
|
@@ -127,7 +127,7 @@ Coprl::Presenters.define(:content_as_form) do
|
|
127
127
|
indented_grid do
|
128
128
|
card do
|
129
129
|
title 'Content with input_tag'
|
130
|
-
content id: :content_2,
|
130
|
+
content id: :content_2, input_tag: :test_input_tag do
|
131
131
|
text_field name: :text1 do
|
132
132
|
label 'Text 1'
|
133
133
|
end
|
@@ -275,7 +275,7 @@ end
|
|
275
275
|
|
276
276
|
Coprl::Presenters.define(:dialog_c) do
|
277
277
|
dialog id: :dialog_c do
|
278
|
-
content
|
278
|
+
content input_tag: :some_dialog_tag do
|
279
279
|
text_field name: :dialog_text_field do
|
280
280
|
value 'whatever'
|
281
281
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Coprl::Presenters.define(:halted) do
|
2
|
+
helpers Demo::Helpers::IndentedGrid
|
3
|
+
attach :top_nav
|
4
|
+
attach :events_drawer
|
5
|
+
|
6
|
+
indented_grid do
|
7
|
+
display 'Halted Events'
|
8
|
+
body 'This demonstrates how to respond to failed, or halted events'
|
9
|
+
|
10
|
+
content do
|
11
|
+
button :failed_post do
|
12
|
+
event :click do
|
13
|
+
posts '_echo_', status: 500
|
14
|
+
end
|
15
|
+
event 'V:eventsHalted' do
|
16
|
+
snackbar 'That did not work! Please try again later.'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
attach :code, file: __FILE__
|
23
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Coprl::Presenters.define(:events_drawer) do
|
2
2
|
helpers Coprl::Presenters::Helpers::Inflector
|
3
3
|
|
4
|
-
events = %i[field_level_events form_level_events tagged_input parallel_events].sort
|
4
|
+
events = %i[field_level_events form_level_events tagged_input parallel_events halted].sort
|
5
5
|
actions = %i[dialog replaces loads toggle_visibility snackbar updates deletes posts clear last_response prompt_if_dirty].sort
|
6
6
|
|
7
7
|
drawer 'Events' do
|
@@ -18,11 +18,11 @@ Coprl::Presenters.define(:tagged_input) do
|
|
18
18
|
|
19
19
|
title 'Example'
|
20
20
|
|
21
|
-
content
|
21
|
+
content input_tag: :somefields do
|
22
22
|
radio_button text: :on, name: :check_me, value: :on, checked: true
|
23
23
|
radio_button text: :off, name: :check_me, value: :off
|
24
24
|
end
|
25
|
-
text_field name: :myfield2,
|
25
|
+
text_field name: :myfield2, input_tag: :somefields do
|
26
26
|
label 'More Data to post'
|
27
27
|
end
|
28
28
|
|
@@ -0,0 +1,144 @@
|
|
1
|
+
Coprl::Presenters.define(:animate, namespace: :plugins) do
|
2
|
+
helpers Demo::Helpers::IndentedGrid
|
3
|
+
attach :top_nav
|
4
|
+
attach :plugin_drawer
|
5
|
+
plugin :animate, :scroll_to
|
6
|
+
page_title 'Animate', id: :page_title
|
7
|
+
|
8
|
+
indented_grid do
|
9
|
+
grid do
|
10
|
+
column 4 do
|
11
|
+
heading "Animations plugin", id: :animate_heading
|
12
|
+
subheading "Just-add-POM CSS animations"
|
13
|
+
body "It is based on [animate.css](https://animate.style/)."
|
14
|
+
end
|
15
|
+
column 8 do
|
16
|
+
[["Attention seekers",
|
17
|
+
%i( bounce
|
18
|
+
flash
|
19
|
+
pulse
|
20
|
+
rubber_band
|
21
|
+
shake_x
|
22
|
+
shake_y
|
23
|
+
head_shake
|
24
|
+
swing
|
25
|
+
tada
|
26
|
+
wobble
|
27
|
+
jello
|
28
|
+
heart_beat)],
|
29
|
+
["Back entrances",
|
30
|
+
%i( back_in_down
|
31
|
+
back_in_left
|
32
|
+
back_in_right
|
33
|
+
back_in_up)],
|
34
|
+
["Back exits",
|
35
|
+
%i( back_out_down
|
36
|
+
back_out_left
|
37
|
+
back_out_right
|
38
|
+
back_out_up)],
|
39
|
+
["Bouncing entrances",
|
40
|
+
%i( bounce_in
|
41
|
+
bounce_in_down
|
42
|
+
bounce_in_left
|
43
|
+
bounce_in_right
|
44
|
+
bounce_in_up)],
|
45
|
+
["Bouncing exits",
|
46
|
+
%i( bounce_out
|
47
|
+
bounce_out_down
|
48
|
+
bounce_out_left
|
49
|
+
bounce_out_right
|
50
|
+
bounce_out_up)],
|
51
|
+
["Fading entrances",
|
52
|
+
%i( fade_in
|
53
|
+
fade_in_down
|
54
|
+
fade_in_down_big
|
55
|
+
fade_in_left
|
56
|
+
fade_in_left_big
|
57
|
+
fade_in_right
|
58
|
+
fade_in_right_big
|
59
|
+
fade_in_up
|
60
|
+
fade_in_up_big
|
61
|
+
fade_in_top_left
|
62
|
+
fade_in_top_right
|
63
|
+
fade_in_bottom_left
|
64
|
+
fade_in_bottom_right)],
|
65
|
+
["Fading exits",
|
66
|
+
%i( fade_out
|
67
|
+
fade_out_down
|
68
|
+
fade_out_down_big
|
69
|
+
fade_out_left
|
70
|
+
fade_out_left_big
|
71
|
+
fade_out_right
|
72
|
+
fade_out_right_big
|
73
|
+
fade_out_up
|
74
|
+
fade_out_up_big
|
75
|
+
fade_out_top_left
|
76
|
+
fade_out_top_right
|
77
|
+
fade_out_bottom_right
|
78
|
+
fade_out_bottom_left)],
|
79
|
+
["Flippers",
|
80
|
+
%i( flip
|
81
|
+
flip_in_x
|
82
|
+
flip_in_y
|
83
|
+
flip_out_x
|
84
|
+
flip_out_y)],
|
85
|
+
["lightspeed",
|
86
|
+
%i( light_speed_in_right
|
87
|
+
light_speed_in_left
|
88
|
+
light_speed_out_right
|
89
|
+
light_speed_out_left)],
|
90
|
+
["Rotating entrances",
|
91
|
+
%i( rotate_in
|
92
|
+
rotate_in_down_left
|
93
|
+
rotate_in_down_right
|
94
|
+
rotate_in_up_left
|
95
|
+
rotate_in_up_right)],
|
96
|
+
["Rotating exits",
|
97
|
+
%i( rotate_out
|
98
|
+
rotate_out_down_left
|
99
|
+
rotate_out_down_right
|
100
|
+
rotate_out_up_left
|
101
|
+
rotate_out_up_right)],
|
102
|
+
["Specials",
|
103
|
+
%i( hinge
|
104
|
+
jack_in_the_box
|
105
|
+
roll_in
|
106
|
+
roll_out)],
|
107
|
+
["Zooming entrances",
|
108
|
+
%i( zoom_in
|
109
|
+
zoom_in_down
|
110
|
+
zoom_in_left
|
111
|
+
zoom_in_right
|
112
|
+
zoom_in_up)],
|
113
|
+
["Zooming exits",
|
114
|
+
%i( zoom_out
|
115
|
+
zoom_out_down
|
116
|
+
zoom_out_left
|
117
|
+
zoom_out_right
|
118
|
+
zoom_out_up)],
|
119
|
+
["Sliding entrances",
|
120
|
+
%i( slide_in_down
|
121
|
+
slide_in_left
|
122
|
+
slide_in_right
|
123
|
+
slide_in_up)],
|
124
|
+
["Sliding exits",
|
125
|
+
%i( slide_out_down
|
126
|
+
slide_out_left
|
127
|
+
slide_out_right
|
128
|
+
slide_out_up)]].each do |animiation_title, animiations|
|
129
|
+
subtitle animiation_title
|
130
|
+
animiations.each do |animation|
|
131
|
+
button animation do
|
132
|
+
event :click do
|
133
|
+
scroll_to :page_title
|
134
|
+
animate :animate_heading, animation, wait: true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
attach :code, file: __FILE__
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
Coprl::Presenters.define(:cacheable, namespace: :plugins) do
|
4
|
+
helpers Demo::Helpers::IndentedGrid
|
5
|
+
attach :top_nav
|
6
|
+
attach :plugin_drawer
|
7
|
+
plugin :cacheable
|
8
|
+
|
9
|
+
page_title 'Cacheable'
|
10
|
+
|
11
|
+
helpers do
|
12
|
+
def cache_object
|
13
|
+
OpenStruct.new(cache_key: 'wild')
|
14
|
+
end
|
15
|
+
|
16
|
+
def cache_objects
|
17
|
+
[
|
18
|
+
OpenStruct.new(cache_key: 'abc'),
|
19
|
+
OpenStruct.new(cache_key: '123'),
|
20
|
+
]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
indented_grid do
|
25
|
+
body <<~DOC
|
26
|
+
The caching plugin provides russian doll caching support.
|
27
|
+
Declare the plugin in your pom, `plugin :cacheable`, or configure it globally:
|
28
|
+
|
29
|
+
```
|
30
|
+
Coprl::Presenters::Settings.configure do |config|
|
31
|
+
config.presenters.plugins.push(:cacheable)
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
For Rails it automatically uses the Rails::Cache. For other Rack apps you need to configure it with a block:
|
36
|
+
|
37
|
+
```
|
38
|
+
Coprl::Presenters::Plugins::Cacheable::Settings.configure do |config|
|
39
|
+
# A cache needs to respond to fetch(key, &block) and exist?(key) or has_key?(key)
|
40
|
+
config.cache=cache_store
|
41
|
+
end
|
42
|
+
```
|
43
|
+
Simply wrap your POM with the cache directive.
|
44
|
+
Complex keys are supported. If an object responds to `cache_key` then that will be used for it.
|
45
|
+
An object that responds to `map` will be expanded. As a last resort `to_s` is used.
|
46
|
+
|
47
|
+
Examples (refresh your browser to observe cached values):
|
48
|
+
DOC
|
49
|
+
title "Not cached at #{DateTime.now}"
|
50
|
+
cache :remember_me do
|
51
|
+
title "Simple Cache Key - cached at #{DateTime.now}"
|
52
|
+
end
|
53
|
+
|
54
|
+
cache [:title, cache_object, cache_objects] do
|
55
|
+
title "Complex Cache Key - cached at #{DateTime.now}"
|
56
|
+
end
|
57
|
+
blank
|
58
|
+
body <<~DOC
|
59
|
+
Note: Confusion alert ... if you are using shotgun for the demo, it **will not cache**, since the demo uses an in memory cache and shotgun reloads its entire process with each refresh.
|
60
|
+
DOC
|
61
|
+
|
62
|
+
end
|
63
|
+
attach :code, file: __FILE__
|
64
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Coprl::Presenters.define(:clipboard, namespace: :plugins) do
|
2
|
+
helpers Demo::Helpers::IndentedGrid
|
3
|
+
attach :top_nav
|
4
|
+
attach :plugin_drawer
|
5
|
+
plugin :clipboard
|
6
|
+
page_title 'Clipboard'
|
7
|
+
|
8
|
+
indented_grid do
|
9
|
+
headline 'Copy'
|
10
|
+
text_field id: :copy_me do
|
11
|
+
value "Don't copy that floppy!"
|
12
|
+
end
|
13
|
+
button :copy, type: :raised do
|
14
|
+
event :click do
|
15
|
+
clipboard copy: :copy_me
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attach :code, file: __FILE__
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Coprl::Presenters.define(:color_picker, namespace: :plugins) do
|
2
|
+
helpers Demo::Helpers::IndentedGrid
|
3
|
+
attach :top_nav
|
4
|
+
attach :plugin_drawer
|
5
|
+
plugin :color_picker
|
6
|
+
page_title 'Color picker'
|
7
|
+
|
8
|
+
indented_grid do
|
9
|
+
subheading 'Add a Color picker to your page'
|
10
|
+
color_picker 'color',
|
11
|
+
value: '#f44336',
|
12
|
+
color_per_row: 7,
|
13
|
+
color_size: 25
|
14
|
+
|
15
|
+
attach :code, file: __FILE__
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Coprl::Presenters.define(:google_maps, namespace: :plugins) do
|
2
|
+
helpers Demo::Helpers::IndentedGrid
|
3
|
+
attach :top_nav
|
4
|
+
attach :plugin_drawer
|
5
|
+
plugin :google_maps
|
6
|
+
page_title 'Maps'
|
7
|
+
|
8
|
+
indented_grid do
|
9
|
+
unless ENV['GOOGLE_API_KEY']
|
10
|
+
headline 'You must define the ENV variable GOOGLE_API_KEY for this to render a map'
|
11
|
+
subtitle 'Dev hint: Create an .env file with a `GOOGLE_API_KEY=yourkey` and restart. Goto [Google Using API Keys](https://developers.google.com/maps/documentation/javascript/get-api-key) to create a key.'
|
12
|
+
end
|
13
|
+
subheading 'Static Maps'
|
14
|
+
address = '125 Park Street, Traverse City, MI'
|
15
|
+
google_map address: address, height: "300px", width: "400px" do
|
16
|
+
event :click do
|
17
|
+
loads "https://www.google.com/maps/place/#{address}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attach :code, file: __FILE__
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|