formotion 0.0.3 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile +6 -0
- data/LIST_OF_ROW_TYPES.md +163 -0
- data/NEW_ROW_TYPES.md +73 -0
- data/README.md +11 -14
- data/Rakefile +6 -0
- data/app/app_delegate.rb +22 -0
- data/examples/KitchenSink/Rakefile +1 -1
- data/examples/KitchenSink/app/app_delegate.rb +50 -2
- data/examples/Login/.gitignore +5 -0
- data/examples/Login/Rakefile +9 -0
- data/examples/Login/app/app_delegate.rb +16 -0
- data/examples/Login/app/login_controller.rb +47 -0
- data/examples/Login/app/user_controller.rb +38 -0
- data/examples/Login/spec/main_spec.rb +9 -0
- data/lib/formotion.rb +7 -1
- data/lib/formotion/{form.rb → form/form.rb} +0 -0
- data/lib/formotion/{form_delegate.rb → form/form_delegate.rb} +10 -15
- data/lib/formotion/patch/object.rb +9 -0
- data/lib/formotion/patch/ui_action_sheet.rb +27 -0
- data/lib/formotion/patch/ui_text_view.rb +122 -0
- data/lib/formotion/patch/ui_text_view_placeholder.rb +65 -0
- data/lib/formotion/{row.rb → row/row.rb} +37 -27
- data/lib/formotion/row/row_cell_builder.rb +28 -0
- data/lib/formotion/row_type/base.rb +41 -0
- data/lib/formotion/row_type/check_row.rb +30 -0
- data/lib/formotion/row_type/date_row.rb +61 -0
- data/lib/formotion/row_type/email_row.rb +11 -0
- data/lib/formotion/row_type/image_row.rb +99 -0
- data/lib/formotion/row_type/number_row.rb +11 -0
- data/lib/formotion/row_type/options_row.rb +24 -0
- data/lib/formotion/row_type/phone_row.rb +11 -0
- data/lib/formotion/row_type/row_type.rb +21 -0
- data/lib/formotion/row_type/slider_row.rb +43 -0
- data/lib/formotion/row_type/static_row.rb +6 -0
- data/lib/formotion/row_type/string_row.rb +112 -0
- data/lib/formotion/row_type/submit_row.rb +34 -0
- data/lib/formotion/row_type/switch_row.rb +19 -0
- data/lib/formotion/row_type/text_row.rb +76 -0
- data/lib/formotion/{section.rb → section/section.rb} +3 -0
- data/lib/formotion/version.rb +1 -1
- data/spec/form_spec.rb +5 -4
- data/spec/functional/character_spec.rb +70 -0
- data/spec/functional/check_row_spec.rb +42 -0
- data/spec/functional/date_row_spec.rb +63 -0
- data/spec/functional/image_row_spec.rb +82 -0
- data/spec/functional/options_row_spec.rb +27 -0
- data/spec/functional/slider_row_spec.rb +27 -0
- data/spec/functional/submit_row_spec.rb +30 -0
- data/spec/functional/switch_row_spec.rb +28 -0
- data/spec/functional/text_row_spec.rb +60 -0
- data/spec/row_type/check_spec.rb +27 -0
- data/spec/row_type/date_spec.rb +69 -0
- data/spec/row_type/email_spec.rb +22 -0
- data/spec/row_type/image_spec.rb +43 -0
- data/spec/row_type/number_spec.rb +22 -0
- data/spec/row_type/options_spec.rb +37 -0
- data/spec/row_type/phone_spec.rb +22 -0
- data/spec/row_type/slider_spec.rb +47 -0
- data/spec/row_type/static_spec.rb +15 -0
- data/spec/row_type/string_spec.rb +52 -0
- data/spec/row_type/submit_spec.rb +28 -0
- data/spec/row_type/switch_spec.rb +27 -0
- data/spec/row_type/text_spec.rb +41 -0
- data/spec/support/ui_control_wrap_extension.rb +7 -0
- metadata +88 -9
- data/lib/formotion/row_cell_builder.rb +0 -168
- data/lib/formotion/row_type.rb +0 -33
@@ -0,0 +1,28 @@
|
|
1
|
+
#################
|
2
|
+
#
|
3
|
+
# Formotion::RowCellBuilder
|
4
|
+
# RowCellBuilder handles taking Formotion::Rows
|
5
|
+
# and configuring UITableViewCells based on their properties.
|
6
|
+
#
|
7
|
+
#################
|
8
|
+
module Formotion
|
9
|
+
class RowCellBuilder
|
10
|
+
|
11
|
+
# PARAMS row.is_a? Formotion::Row
|
12
|
+
# RETURNS [cell configured to that row, a UITextField for that row if applicable or nil]
|
13
|
+
def self.make_cell(row)
|
14
|
+
cell, text_field = nil
|
15
|
+
|
16
|
+
cell = UITableViewCell.alloc.initWithStyle(row.object.cell_style, reuseIdentifier:row.reuse_identifier)
|
17
|
+
|
18
|
+
cell.accessoryType = UITableViewCellAccessoryNone
|
19
|
+
cell.textLabel.text = row.title
|
20
|
+
cell.detailTextLabel.text = row.subtitle
|
21
|
+
|
22
|
+
edit_field = row.object.build_cell(cell)
|
23
|
+
|
24
|
+
[cell, edit_field]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class Base
|
4
|
+
attr_accessor :row, :tableView
|
5
|
+
|
6
|
+
def tableView
|
7
|
+
@tableView ||= self.row.form.table
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(row)
|
11
|
+
@row = row
|
12
|
+
end
|
13
|
+
|
14
|
+
def submit_button?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
# RowCellBuilder uses this to instantiate the UITableViewCell.
|
19
|
+
def cell_style
|
20
|
+
UITableViewCellStyleSubtitle
|
21
|
+
end
|
22
|
+
|
23
|
+
# builder method for row cell specific implementation
|
24
|
+
def build_cell(cell)
|
25
|
+
# implement in row class
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# called by the Row after all the setup and connections are made
|
30
|
+
# in #make_cell
|
31
|
+
def after_build(cell)
|
32
|
+
end
|
33
|
+
|
34
|
+
# method gets triggered when tableView(tableView, didSelectRowAtIndexPath:indexPath)
|
35
|
+
# in UITableViewDelegate is executed
|
36
|
+
def on_select(tableView, tableViewDelegate)
|
37
|
+
# implement in row class
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class CheckRow < Base
|
4
|
+
|
5
|
+
# This is actually called whenever again cell is checked/unchecked
|
6
|
+
# in the UITableViewDelegate callbacks. So (for now) don't
|
7
|
+
# instantiate long-lived objects in them.
|
8
|
+
# Maybe that logic should be moved elsewhere?
|
9
|
+
def build_cell(cell)
|
10
|
+
cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_select(tableView, tableViewDelegate)
|
15
|
+
if row.section.select_one and !row.value
|
16
|
+
row.section.rows.each do |other_row|
|
17
|
+
other_row.value = (other_row == row)
|
18
|
+
|
19
|
+
cell = tableView.cellForRowAtIndexPath(other_row.index_path)
|
20
|
+
other_row.object.build_cell(cell) if cell
|
21
|
+
end
|
22
|
+
elsif !row.section.select_one
|
23
|
+
row.value = !row.value
|
24
|
+
build_cell(tableView.cellForRowAtIndexPath(row.index_path))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class DateRow < StringRow
|
4
|
+
# overwrite Character on_change method
|
5
|
+
def on_change(text_field)
|
6
|
+
end
|
7
|
+
|
8
|
+
def update
|
9
|
+
self.row.text_field && self.row.text_field.text = self.formatted_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def date_value
|
13
|
+
value = self.row.value
|
14
|
+
if value.is_a? Numeric
|
15
|
+
NSDate.dateWithTimeIntervalSince1970(value.to_i)
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def formatter
|
22
|
+
@formatter ||= begin
|
23
|
+
formatter = NSDateFormatter.new
|
24
|
+
|
25
|
+
date_style = self.row.format
|
26
|
+
if date_style && date_style.to_s[-5..-1] != "style"
|
27
|
+
date_style = (date_style.to_s + "_style").to_sym
|
28
|
+
end
|
29
|
+
formatter.dateStyle = self.row.send(:const_int_get, "NSDateFormatter", date_style || NSDateFormatterShortStyle)
|
30
|
+
formatter
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def formatted_value
|
35
|
+
return formatter.stringFromDate(self.date_value) if self.date_value
|
36
|
+
self.row.value
|
37
|
+
end
|
38
|
+
|
39
|
+
def after_build(cell)
|
40
|
+
self.row.text_field.inputView = self.picker
|
41
|
+
update
|
42
|
+
end
|
43
|
+
|
44
|
+
def picker
|
45
|
+
@picker ||= begin
|
46
|
+
picker = UIDatePicker.alloc.initWithFrame(CGRectZero)
|
47
|
+
picker.datePickerMode = UIDatePickerModeDate
|
48
|
+
picker.hidden = false
|
49
|
+
picker.date = self.date_value || NSDate.date
|
50
|
+
|
51
|
+
picker.when(UIControlEventValueChanged) do
|
52
|
+
self.row.value = @picker.date.timeIntervalSince1970.to_i
|
53
|
+
update
|
54
|
+
end
|
55
|
+
|
56
|
+
picker
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class ImageRow < Base
|
4
|
+
include BW::KVO
|
5
|
+
|
6
|
+
IMAGE_VIEW_TAG=1100
|
7
|
+
|
8
|
+
def build_cell(cell)
|
9
|
+
add_plus_accessory(cell)
|
10
|
+
|
11
|
+
observe(self.row, "value") do |old_value, new_value|
|
12
|
+
@image_view.image = new_value
|
13
|
+
if new_value
|
14
|
+
self.row.rowHeight = 200
|
15
|
+
cell.accessoryView = nil
|
16
|
+
else
|
17
|
+
self.row.rowHeight = 44
|
18
|
+
add_plus_accessory(cell)
|
19
|
+
end
|
20
|
+
row.form.reload_data
|
21
|
+
end
|
22
|
+
|
23
|
+
@image_view = UIImageView.alloc.init
|
24
|
+
@image_view.image = row.value if row.value
|
25
|
+
@image_view.tag = IMAGE_VIEW_TAG
|
26
|
+
@image_view.contentMode = UIViewContentModeScaleAspectFit
|
27
|
+
@image_view.backgroundColor = UIColor.clearColor
|
28
|
+
cell.addSubview(@image_view)
|
29
|
+
|
30
|
+
cell.swizzle(:layoutSubviews) do
|
31
|
+
def layoutSubviews
|
32
|
+
old_layoutSubviews
|
33
|
+
|
34
|
+
# viewWithTag is terrible, but I think it's ok to use here...
|
35
|
+
formotion_field = self.viewWithTag(IMAGE_VIEW_TAG)
|
36
|
+
|
37
|
+
field_frame = formotion_field.frame
|
38
|
+
field_frame.origin.y = 10
|
39
|
+
field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
|
40
|
+
field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
|
41
|
+
field_frame.size.height = self.frame.size.height - 20
|
42
|
+
formotion_field.frame = field_frame
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_select(tableView, tableViewDelegate)
|
48
|
+
@action_sheet = UIActionSheet.alloc.init
|
49
|
+
@action_sheet.delegate = self
|
50
|
+
|
51
|
+
@action_sheet.destructiveButtonIndex = (@action_sheet.addButtonWithTitle "Delete") if row.value
|
52
|
+
@action_sheet.addButtonWithTitle "Take" if BW::Device.camera.front? or BW::Device.camera.rear?
|
53
|
+
@action_sheet.addButtonWithTitle "Choose"
|
54
|
+
@action_sheet.cancelButtonIndex = (@action_sheet.addButtonWithTitle "Cancel")
|
55
|
+
|
56
|
+
@action_sheet.showInView @image_view
|
57
|
+
end
|
58
|
+
|
59
|
+
def actionSheet actionSheet, clickedButtonAtIndex: index
|
60
|
+
source = nil
|
61
|
+
|
62
|
+
if index == actionSheet.destructiveButtonIndex
|
63
|
+
row.value = nil
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
case actionSheet.buttonTitleAtIndex(index)
|
68
|
+
when "Take"
|
69
|
+
source = :camera
|
70
|
+
when "Choose"
|
71
|
+
source = :photo_library
|
72
|
+
when "Cancel"
|
73
|
+
else
|
74
|
+
p "Unrecognized button title #{actionSheet.buttonTitleAtIndex(index)}"
|
75
|
+
end
|
76
|
+
|
77
|
+
if source
|
78
|
+
@camera = BW::Device.camera.any
|
79
|
+
@camera.picture(source_type: source, media_types: [:image]) do |result|
|
80
|
+
if result[:original_image]
|
81
|
+
row.value = result[:original_image]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_plus_accessory(cell)
|
88
|
+
@add_button ||= begin
|
89
|
+
button = UIButton.buttonWithType(UIButtonTypeContactAdd)
|
90
|
+
button.when(UIControlEventTouchUpInside) do
|
91
|
+
self.on_select(nil, nil)
|
92
|
+
end
|
93
|
+
button
|
94
|
+
end
|
95
|
+
cell.accessoryView = @add_button
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class OptionsRow < Base
|
4
|
+
|
5
|
+
SLIDER_VIEW_TAG = 1200
|
6
|
+
|
7
|
+
def build_cell(cell)
|
8
|
+
cell.selectionStyle = UITableViewCellSelectionStyleNone
|
9
|
+
slideView = UISegmentedControl.alloc.initWithItems(row.items || [])
|
10
|
+
slideView.selectedSegmentIndex = row.items.index(row.value) if row.value
|
11
|
+
slideView.segmentedControlStyle = UISegmentedControlStyleBar
|
12
|
+
cell.accessoryView = slideView
|
13
|
+
|
14
|
+
slideView.when(UIControlEventValueChanged) do
|
15
|
+
row.value = row.items[slideView.selectedSegmentIndex]
|
16
|
+
end
|
17
|
+
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
ROW_TYPES = Formotion::RowType.constants(false).select { |constant_name| constant_name =~ /Row$/ }
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def for(string_or_sym)
|
7
|
+
type = string_or_sym
|
8
|
+
|
9
|
+
if type.is_a?(Symbol) or type.is_a? String
|
10
|
+
string = "#{type.to_s.downcase}_row".camelize
|
11
|
+
if not const_defined? string
|
12
|
+
raise Formotion::InvalidClassError, "Invalid RowType value #{string_or_sym}. Create a class called #{string}"
|
13
|
+
end
|
14
|
+
Formotion::RowType.const_get(string)
|
15
|
+
else
|
16
|
+
raise Formotion::InvalidClassError, "Attempted row type #{type.inspect} is not a valid RowType."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class SliderRow < Base
|
4
|
+
|
5
|
+
SLIDER_VIEW_TAG = 1200
|
6
|
+
|
7
|
+
def build_cell(cell)
|
8
|
+
cell.selectionStyle = UITableViewCellSelectionStyleNone
|
9
|
+
slideView = UISlider.alloc.initWithFrame(CGRectZero)
|
10
|
+
cell.accessoryView = slideView
|
11
|
+
row.range ||= (1..10)
|
12
|
+
slideView.minimumValue = row.range.first
|
13
|
+
slideView.maximumValue = row.range.last
|
14
|
+
slideView.tag = SLIDER_VIEW_TAG
|
15
|
+
slideView.setValue(row.value, animated:true) if row.value
|
16
|
+
slideView.accessibilityLabel = row.title + " Slider"
|
17
|
+
|
18
|
+
slideView.when(UIControlEventValueChanged) do
|
19
|
+
row.value = slideView.value
|
20
|
+
end
|
21
|
+
|
22
|
+
cell.swizzle(:layoutSubviews) do
|
23
|
+
def layoutSubviews
|
24
|
+
old_layoutSubviews
|
25
|
+
|
26
|
+
# viewWithTag is terrible, but I think it's ok to use here...
|
27
|
+
formotion_field = self.viewWithTag(SLIDER_VIEW_TAG)
|
28
|
+
formotion_field.sizeToFit
|
29
|
+
|
30
|
+
field_frame = formotion_field.frame
|
31
|
+
field_frame.origin.y = 10
|
32
|
+
field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
|
33
|
+
field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
|
34
|
+
field_frame.size.height = self.frame.size.height - 20
|
35
|
+
formotion_field.frame = field_frame
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Formotion
|
2
|
+
module RowType
|
3
|
+
class StringRow < Base
|
4
|
+
|
5
|
+
# The new UITextField in a UITableViewCell
|
6
|
+
# will be assigned this tag, if applicable.
|
7
|
+
TEXT_FIELD_TAG=1000
|
8
|
+
|
9
|
+
def keyboardType
|
10
|
+
UIKeyboardTypeDefault
|
11
|
+
end
|
12
|
+
|
13
|
+
# Configures the cell to have a new UITextField
|
14
|
+
# which is used to enter data. Consists of
|
15
|
+
# 1) setting up that field with the appropriate properties
|
16
|
+
# specified by `row` 2) configures the callbacks on the field
|
17
|
+
# to call any callbacks `row` listens for.
|
18
|
+
# Also does the layoutSubviews swizzle trick
|
19
|
+
# to size the UITextField so it won't bump into the titleLabel.
|
20
|
+
def build_cell(cell)
|
21
|
+
field = UITextField.alloc.initWithFrame(CGRectZero)
|
22
|
+
field.tag = TEXT_FIELD_TAG
|
23
|
+
|
24
|
+
field.placeholder = row.placeholder
|
25
|
+
field.text = row.value.to_s
|
26
|
+
|
27
|
+
field.clearButtonMode = UITextFieldViewModeWhileEditing
|
28
|
+
field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter
|
29
|
+
field.textAlignment = UITextAlignmentRight
|
30
|
+
|
31
|
+
field.keyboardType = keyboardType
|
32
|
+
|
33
|
+
field.secureTextEntry = true if row.secure?
|
34
|
+
field.returnKeyType = row.return_key || UIReturnKeyNext
|
35
|
+
field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization
|
36
|
+
field.autocorrectionType = row.auto_correction if row.auto_correction
|
37
|
+
field.clearButtonMode = row.clear_button || UITextFieldViewModeWhileEditing
|
38
|
+
|
39
|
+
add_callbacks(field)
|
40
|
+
|
41
|
+
cell.swizzle(:layoutSubviews) do
|
42
|
+
def layoutSubviews
|
43
|
+
old_layoutSubviews
|
44
|
+
|
45
|
+
# viewWithTag is terrible, but I think it's ok to use here...
|
46
|
+
formotion_field = self.viewWithTag(TEXT_FIELD_TAG)
|
47
|
+
formotion_field.sizeToFit
|
48
|
+
|
49
|
+
field_frame = formotion_field.frame
|
50
|
+
field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
|
51
|
+
field_frame.origin.y = ((self.frame.size.height - field_frame.size.height) / 2.0).round
|
52
|
+
field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
|
53
|
+
formotion_field.frame = field_frame
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
cell.addSubview(field)
|
58
|
+
field
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_callbacks(field)
|
63
|
+
if row.on_enter_callback
|
64
|
+
field.should_return? do |text_field|
|
65
|
+
if row.on_enter_callback.arity == 0
|
66
|
+
row.on_enter_callback.call
|
67
|
+
elsif row.on_enter_callback.arity == 1
|
68
|
+
row.on_enter_callback.call(row)
|
69
|
+
end
|
70
|
+
false
|
71
|
+
end
|
72
|
+
elsif field.returnKeyType == UIReturnKeyDone
|
73
|
+
field.should_return? do |text_field|
|
74
|
+
text_field.resignFirstResponder
|
75
|
+
false
|
76
|
+
end
|
77
|
+
else
|
78
|
+
field.should_return? do |text_field|
|
79
|
+
if row.next_row && row.next_row.text_field
|
80
|
+
row.next_row.text_field.becomeFirstResponder
|
81
|
+
else
|
82
|
+
text_field.resignFirstResponder
|
83
|
+
end
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
field.on_begin do |text_field|
|
89
|
+
row.on_begin_callback && row.on_begin_callback.call
|
90
|
+
end
|
91
|
+
|
92
|
+
field.should_begin? do |text_field|
|
93
|
+
row.section.form.active_row = row
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
field.on_change do |text_field|
|
98
|
+
on_change(text_field)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def on_change(text_field)
|
103
|
+
row.value = text_field.text
|
104
|
+
end
|
105
|
+
|
106
|
+
def on_select(tableView, tableViewDelegate)
|
107
|
+
row.text_field.becomeFirstResponder
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|