glimmer-dsl-opal 0.0.3 → 0.0.8
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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +1049 -35
- data/VERSION +1 -1
- data/lib/glimmer-dsl-opal.rb +5 -2
- data/lib/glimmer/data_binding/ext/observable_model.rb +40 -0
- data/lib/glimmer/data_binding/list_selection_binding.rb +51 -0
- data/lib/glimmer/data_binding/table_items_binding.rb +67 -0
- data/lib/glimmer/dsl/opal/async_exec_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/browser_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/column_properties_expression.rb +22 -0
- data/lib/glimmer/dsl/opal/dsl.rb +14 -0
- data/lib/glimmer/dsl/opal/list_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/list_selection_data_binding_expression.rb +42 -0
- data/lib/glimmer/dsl/opal/message_box_expression.rb +20 -0
- data/lib/glimmer/dsl/opal/observe_expression.rb +32 -0
- data/lib/glimmer/dsl/opal/tab_folder_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/tab_item_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/table_column_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/table_expression.rb +17 -0
- data/lib/glimmer/dsl/opal/table_items_data_binding_expression.rb +29 -0
- data/lib/glimmer/opal/display_proxy.rb +23 -0
- data/lib/glimmer/opal/div_proxy.rb +11 -2
- data/lib/glimmer/opal/document_proxy.rb +141 -11
- data/lib/glimmer/opal/element_proxy.rb +38 -15
- data/lib/glimmer/opal/grid_layout_proxy.rb +3 -1
- data/lib/glimmer/opal/iframe_proxy.rb +23 -0
- data/lib/glimmer/opal/input_proxy.rb +8 -4
- data/lib/glimmer/opal/label_proxy.rb +1 -1
- data/lib/glimmer/opal/layout_data_proxy.rb +23 -2
- data/lib/glimmer/opal/list_proxy.rb +80 -0
- data/lib/glimmer/opal/modal.rb +94 -0
- data/lib/glimmer/opal/point.rb +5 -0
- data/lib/glimmer/opal/select_proxy.rb +1 -1
- data/lib/glimmer/opal/tab_folder.rb +53 -0
- data/lib/glimmer/opal/tab_item.rb +98 -0
- data/lib/glimmer/opal/table_column.rb +50 -0
- data/lib/glimmer/opal/table_item.rb +136 -0
- data/lib/glimmer/opal/table_proxy.rb +149 -0
- data/lib/samples/elaborate/contact_manager.rb +1 -2
- data/lib/samples/elaborate/login.rb +0 -1
- data/lib/samples/elaborate/tic_tac_toe.rb +5 -5
- data/lib/samples/hello/hello_tab.rb +2 -2
- metadata +30 -14
- data/lib/glimmer/config.rb +0 -22
- data/lib/glimmer/dsl/engine.rb +0 -193
- data/lib/glimmer/dsl/expression.rb +0 -42
- data/lib/glimmer/dsl/expression_handler.rb +0 -48
- data/lib/glimmer/dsl/parent_expression.rb +0 -12
- data/lib/glimmer/dsl/static_expression.rb +0 -36
- data/lib/glimmer/dsl/top_level_expression.rb +0 -7
- data/lib/glimmer/error.rb +0 -6
- data/lib/glimmer/invalid_keyword_error.rb +0 -6
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.8
|
data/lib/glimmer-dsl-opal.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'opal'
|
2
|
-
require 'opal-
|
2
|
+
require 'opal-async'
|
3
|
+
require 'browser'
|
4
|
+
require 'browser/effects'
|
3
5
|
require 'glimmer'
|
4
6
|
|
5
7
|
GLIMMER_DSL_OPAL_ROOT = File.expand_path('../..', __FILE__)
|
@@ -7,4 +9,5 @@ GLIMMER_DSL_OPAL_LIB = File.join(GLIMMER_DSL_OPAL_ROOT, 'lib')
|
|
7
9
|
|
8
10
|
$LOAD_PATH.unshift(GLIMMER_DSL_OPAL_LIB)
|
9
11
|
|
10
|
-
require 'glimmer/dsl/opal/dsl'
|
12
|
+
require 'glimmer/dsl/opal/dsl'
|
13
|
+
require 'glimmer/data_binding/ext/observable_model'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'glimmer/data_binding/observable'
|
2
|
+
require 'glimmer/data_binding/observer'
|
3
|
+
require 'glimmer/opal/display_proxy'
|
4
|
+
|
5
|
+
# This ensures all data-binding events happen async and block on modal display
|
6
|
+
|
7
|
+
module Glimmer
|
8
|
+
module DataBinding
|
9
|
+
# TODO prefix utility methods with double-underscore
|
10
|
+
module ObservableModel
|
11
|
+
include Observable
|
12
|
+
# include Glimmer
|
13
|
+
|
14
|
+
def add_property_writer_observers(property_name)
|
15
|
+
property_writer_name = "#{property_name}="
|
16
|
+
method(property_writer_name)
|
17
|
+
ensure_array_object_observer(property_name, send(property_name))
|
18
|
+
begin
|
19
|
+
method("__original_#{property_writer_name}")
|
20
|
+
rescue
|
21
|
+
old_method = self.class.instance_method(property_writer_name)
|
22
|
+
define_singleton_method("__original_#{property_writer_name}", old_method)
|
23
|
+
define_singleton_method(property_writer_name) do |value|
|
24
|
+
old_value = self.send(property_name)
|
25
|
+
unregister_dependent_observers(property_name, old_value)
|
26
|
+
self.send("__original_#{property_writer_name}", value)
|
27
|
+
Glimmer::Opal::DisplayProxy.instance.async_exec do
|
28
|
+
notify_observers(property_name)
|
29
|
+
ensure_array_object_observer(property_name, value, old_value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue => e
|
34
|
+
# ignore writing if no property writer exists
|
35
|
+
Glimmer::Config.logger&.debug "No need to observe property writer: #{property_writer_name}\n#{e.message}\n#{e.backtrace.join("\n")}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'glimmer/data_binding/observable'
|
2
|
+
require 'glimmer/data_binding/observer'
|
3
|
+
|
4
|
+
module Glimmer
|
5
|
+
module DataBinding
|
6
|
+
# SWT List element selection binding
|
7
|
+
class ListSelectionBinding
|
8
|
+
include Glimmer
|
9
|
+
include Observable
|
10
|
+
include Observer
|
11
|
+
|
12
|
+
attr_reader :element_proxy
|
13
|
+
|
14
|
+
PROPERTY_TYPE_UPDATERS = {
|
15
|
+
:string => lambda { |element_proxy, value| element_proxy.select(element_proxy.index_of(value.to_s)) },
|
16
|
+
:array => lambda { |element_proxy, value| element_proxy.selection=(value || []) }
|
17
|
+
}
|
18
|
+
|
19
|
+
PROPERTY_EVALUATORS = {
|
20
|
+
:string => lambda do |selection_array|
|
21
|
+
return nil if selection_array.empty?
|
22
|
+
selection_array[0]
|
23
|
+
end,
|
24
|
+
:array => lambda do |selection_array|
|
25
|
+
selection_array
|
26
|
+
end
|
27
|
+
}
|
28
|
+
|
29
|
+
# Initialize with list element and property_type
|
30
|
+
# property_type :string represents default list single selection
|
31
|
+
# property_type :array represents list multi selection
|
32
|
+
def initialize(element_proxy, property_type)
|
33
|
+
property_type = :string if property_type.nil? or property_type == :undefined
|
34
|
+
@element_proxy = element_proxy
|
35
|
+
@property_type = property_type
|
36
|
+
@element_proxy.on_widget_disposed do |dispose_event|
|
37
|
+
unregister_all_observables
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def call(value)
|
42
|
+
PROPERTY_TYPE_UPDATERS[@property_type].call(@element_proxy, value) unless evaluate_property == value
|
43
|
+
end
|
44
|
+
|
45
|
+
def evaluate_property
|
46
|
+
selection_array = @element_proxy.selection.to_a #TODO refactor send('selection') into proper method invocation
|
47
|
+
PROPERTY_EVALUATORS[@property_type].call(selection_array)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'glimmer/data_binding/observable_array'
|
2
|
+
require 'glimmer/data_binding/observable_model'
|
3
|
+
require 'glimmer/data_binding/observable'
|
4
|
+
require 'glimmer/data_binding/observer'
|
5
|
+
require 'glimmer/opal/table_proxy'
|
6
|
+
require 'glimmer/opal/table_item'
|
7
|
+
|
8
|
+
module Glimmer
|
9
|
+
module DataBinding
|
10
|
+
class TableItemsBinding
|
11
|
+
include DataBinding::Observable
|
12
|
+
include DataBinding::Observer
|
13
|
+
|
14
|
+
def initialize(parent, model_binding, column_properties)
|
15
|
+
@last_model_collection = nil
|
16
|
+
@table = parent
|
17
|
+
@model_binding = model_binding
|
18
|
+
@column_properties = column_properties
|
19
|
+
if @table.respond_to?(:column_properties=)
|
20
|
+
@table.column_properties = @column_properties
|
21
|
+
##else # assume custom widget
|
22
|
+
## @table.body_root.column_properties = @column_properties
|
23
|
+
end
|
24
|
+
call(@model_binding.evaluate_property)
|
25
|
+
model = model_binding.base_model
|
26
|
+
observe(model, model_binding.property_name_expression)
|
27
|
+
##@table.on_widget_disposed do |dispose_event| # doesn't seem needed within Opal
|
28
|
+
## unregister_all_observables
|
29
|
+
##end
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(new_model_collection=nil)
|
33
|
+
if new_model_collection and new_model_collection.is_a?(Array)
|
34
|
+
observe(new_model_collection, @column_properties)
|
35
|
+
@model_collection = new_model_collection
|
36
|
+
end
|
37
|
+
populate_table(@model_collection, @table, @column_properties)
|
38
|
+
sort_table(@model_collection, @table, @column_properties)
|
39
|
+
end
|
40
|
+
|
41
|
+
def populate_table(model_collection, parent, column_properties)
|
42
|
+
return if model_collection&.sort_by(&:hash) == @last_model_collection&.sort_by(&:hash)
|
43
|
+
@last_model_collection = model_collection
|
44
|
+
# TODO improve performance
|
45
|
+
selected_table_item_models = parent.selection.map(&:get_data)
|
46
|
+
parent.remove_all
|
47
|
+
model_collection.each do |model|
|
48
|
+
table_item = Glimmer::Opal::TableItem.new(parent)
|
49
|
+
for index in 0..(column_properties.size-1)
|
50
|
+
table_item.set_text(index, model.send(column_properties[index]).to_s)
|
51
|
+
end
|
52
|
+
table_item.set_data(model)
|
53
|
+
end
|
54
|
+
selected_table_items = parent.search {|item| selected_table_item_models.include?(item.get_data) }
|
55
|
+
selected_table_items = [parent.items.first] if selected_table_items.empty? && !parent.items.empty?
|
56
|
+
parent.selection = selected_table_items unless selected_table_items.empty?
|
57
|
+
parent.redraw
|
58
|
+
end
|
59
|
+
|
60
|
+
def sort_table(model_collection, parent, column_properties)
|
61
|
+
return if model_collection == @last_model_collection
|
62
|
+
parent.items = parent.items.sort_by { |item| model_collection.index(item.get_data) }
|
63
|
+
@last_model_collection = model_collection
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/top_level_expression'
|
3
|
+
require 'glimmer/opal/display_proxy'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DSL
|
7
|
+
module Opal
|
8
|
+
class AsyncExecExpression < StaticExpression
|
9
|
+
include TopLevelExpression
|
10
|
+
|
11
|
+
def interpret(parent, keyword, *args, &block)
|
12
|
+
Glimmer::Opal::DisplayProxy.instance.async_exec(&block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/parent_expression'
|
3
|
+
require 'glimmer/opal/iframe_proxy'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DSL
|
7
|
+
module Opal
|
8
|
+
class BrowserExpression < StaticExpression
|
9
|
+
include ParentExpression
|
10
|
+
|
11
|
+
def interpret(parent, keyword, *args, &block)
|
12
|
+
Glimmer::Opal::IframeProxy.new(parent, args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/opal/table_proxy'
|
3
|
+
|
4
|
+
module Glimmer
|
5
|
+
module DSL
|
6
|
+
module Opal
|
7
|
+
# Responsible for providing a readable keyword (command symbol) to capture
|
8
|
+
# and return column properties for use in TreeItemsDataBindingCommandHandler
|
9
|
+
class ColumnPropertiesExpression < StaticExpression
|
10
|
+
def can_interpret?(parent, keyword, *args, &block)
|
11
|
+
keyword == 'column_properties' and
|
12
|
+
block.nil? and
|
13
|
+
parent.is_a?(Glimmer::Opal::TableProxy)
|
14
|
+
end
|
15
|
+
|
16
|
+
def interpret(parent, keyword, *args, &block)
|
17
|
+
args
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/glimmer/dsl/opal/dsl.rb
CHANGED
@@ -15,7 +15,19 @@ require 'glimmer/dsl/opal/combo_selection_data_binding_expression'
|
|
15
15
|
require 'glimmer/dsl/opal/widget_listener_expression'
|
16
16
|
require 'glimmer/dsl/opal/grid_layout_expression'
|
17
17
|
require 'glimmer/dsl/opal/text_expression'
|
18
|
+
require 'glimmer/dsl/opal/list_expression'
|
19
|
+
require 'glimmer/dsl/opal/browser_expression'
|
20
|
+
require 'glimmer/dsl/opal/tab_folder_expression'
|
21
|
+
require 'glimmer/dsl/opal/tab_item_expression'
|
22
|
+
require 'glimmer/dsl/opal/message_box_expression'
|
23
|
+
require 'glimmer/dsl/opal/async_exec_expression'
|
24
|
+
require 'glimmer/dsl/opal/observe_expression'
|
18
25
|
require 'glimmer/dsl/opal/layout_data_expression'
|
26
|
+
require 'glimmer/dsl/opal/list_selection_data_binding_expression'
|
27
|
+
require 'glimmer/dsl/opal/table_expression'
|
28
|
+
require 'glimmer/dsl/opal/table_column_expression'
|
29
|
+
require 'glimmer/dsl/opal/table_items_data_binding_expression'
|
30
|
+
require 'glimmer/dsl/opal/column_properties_expression'
|
19
31
|
|
20
32
|
module Glimmer
|
21
33
|
module DSL
|
@@ -24,7 +36,9 @@ module Glimmer
|
|
24
36
|
Opal,
|
25
37
|
%w[
|
26
38
|
widget_listener
|
39
|
+
table_items_data_binding
|
27
40
|
combo_selection_data_binding
|
41
|
+
list_selection_data_binding
|
28
42
|
data_binding
|
29
43
|
text
|
30
44
|
property
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/parent_expression'
|
3
|
+
require 'glimmer/opal/list_proxy'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DSL
|
7
|
+
module Opal
|
8
|
+
class ListExpression < StaticExpression
|
9
|
+
include ParentExpression
|
10
|
+
|
11
|
+
def interpret(parent, keyword, *args, &block)
|
12
|
+
Glimmer::Opal::ListProxy.new(parent, args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'glimmer/dsl/expression'
|
2
|
+
require 'glimmer/data_binding/model_binding'
|
3
|
+
require 'glimmer/data_binding/element_binding'
|
4
|
+
require 'glimmer/data_binding/list_selection_binding'
|
5
|
+
require 'glimmer/opal/list_proxy'
|
6
|
+
|
7
|
+
module Glimmer
|
8
|
+
module DSL
|
9
|
+
module Opal
|
10
|
+
class ListSelectionDataBindingExpression < Expression
|
11
|
+
def can_interpret?(parent, keyword, *args, &block)
|
12
|
+
keyword == 'selection' and
|
13
|
+
block.nil? and
|
14
|
+
parent.is_a?(Glimmer::Opal::ListProxy) and
|
15
|
+
args.size == 1 and
|
16
|
+
args[0].is_a?(DataBinding::ModelBinding) and
|
17
|
+
args[0].evaluate_options_property.is_a?(Array)
|
18
|
+
end
|
19
|
+
|
20
|
+
def interpret(parent, keyword, *args, &block)
|
21
|
+
model_binding = args[0]
|
22
|
+
element_binding = DataBinding::ElementBinding.new(parent, 'items')
|
23
|
+
element_binding.call(model_binding.evaluate_options_property)
|
24
|
+
model = model_binding.base_model
|
25
|
+
#TODO make this options observer dependent and all similar observers in widget specific data binding interpretrs
|
26
|
+
element_binding.observe(model, model_binding.options_property_name)
|
27
|
+
|
28
|
+
property_type = :string
|
29
|
+
property_type = :array if parent.has_style?(:multi)
|
30
|
+
list_selection_binding = DataBinding::ListSelectionBinding.new(parent, property_type)
|
31
|
+
list_selection_binding.call(model_binding.evaluate_property)
|
32
|
+
#TODO check if nested data binding works for list widget and other widgets that need custom data binding
|
33
|
+
list_selection_binding.observe(model, model_binding.property_name_expression)
|
34
|
+
|
35
|
+
parent.on_widget_selected do
|
36
|
+
model_binding.call(list_selection_binding.evaluate_property)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/top_level_expression'
|
3
|
+
require 'glimmer/dsl/parent_expression'
|
4
|
+
require 'glimmer/opal/modal'
|
5
|
+
|
6
|
+
module Glimmer
|
7
|
+
module DSL
|
8
|
+
module Opal
|
9
|
+
class MessageBoxExpression < StaticExpression
|
10
|
+
include TopLevelExpression
|
11
|
+
include ParentExpression
|
12
|
+
|
13
|
+
def interpret(parent, keyword, *args, &block)
|
14
|
+
parent = args.delete_at(0)
|
15
|
+
Glimmer::Opal::Modal.new(parent, args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/top_level_expression'
|
3
|
+
require 'glimmer/data_binding/observer'
|
4
|
+
require 'glimmer/data_binding/model_binding'
|
5
|
+
|
6
|
+
module Glimmer
|
7
|
+
module DSL
|
8
|
+
module Opal
|
9
|
+
class ObserveExpression < StaticExpression
|
10
|
+
include TopLevelExpression
|
11
|
+
|
12
|
+
REGEX_NESTED_OR_INDEXED_PROPERTY = /([^\[]+)(\[[^\]]+\])?/
|
13
|
+
|
14
|
+
def can_interpret?(parent, keyword, *args, &block)
|
15
|
+
keyword == 'observe' and
|
16
|
+
block_given? and
|
17
|
+
(args.size == 2) and
|
18
|
+
textual?(args[1])
|
19
|
+
end
|
20
|
+
|
21
|
+
def interpret(parent, keyword, *args, &block)
|
22
|
+
observer = DataBinding::Observer.proc(&block)
|
23
|
+
if args[1].to_s.match(REGEX_NESTED_OR_INDEXED_PROPERTY)
|
24
|
+
observer.observe(DataBinding::ModelBinding.new(args[0], args[1]))
|
25
|
+
else
|
26
|
+
observer.observe(args[0], args[1])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/parent_expression'
|
3
|
+
require 'glimmer/opal/tab_folder'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DSL
|
7
|
+
module Opal
|
8
|
+
class TabFolderExpression < StaticExpression
|
9
|
+
include ParentExpression
|
10
|
+
|
11
|
+
def interpret(parent, keyword, *args, &block)
|
12
|
+
Glimmer::Opal::TabFolder.new(parent, args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/parent_expression'
|
3
|
+
require 'glimmer/opal/tab_item'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DSL
|
7
|
+
module Opal
|
8
|
+
class TabItemExpression < StaticExpression
|
9
|
+
include ParentExpression
|
10
|
+
|
11
|
+
def interpret(parent, keyword, *args, &block)
|
12
|
+
Glimmer::Opal::TabItem.new(parent, args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'glimmer/dsl/static_expression'
|
2
|
+
require 'glimmer/dsl/parent_expression'
|
3
|
+
require 'glimmer/opal/table_column'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DSL
|
7
|
+
module Opal
|
8
|
+
class TableColumnExpression < StaticExpression
|
9
|
+
include ParentExpression
|
10
|
+
|
11
|
+
def interpret(parent, keyword, *args, &block)
|
12
|
+
Glimmer::Opal::TableColumn.new(parent, args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|