glimmer-dsl-opal 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +1003 -58
  4. data/VERSION +1 -1
  5. data/lib/glimmer-dsl-opal.rb +26 -8
  6. data/lib/glimmer/data_binding/element_binding.rb +1 -1
  7. data/lib/glimmer/data_binding/ext/observable_model.rb +40 -0
  8. data/lib/glimmer/data_binding/list_selection_binding.rb +1 -1
  9. data/lib/glimmer/data_binding/table_items_binding.rb +70 -0
  10. data/lib/glimmer/dsl/opal/async_exec_expression.rb +17 -0
  11. data/lib/glimmer/dsl/opal/column_properties_expression.rb +22 -0
  12. data/lib/glimmer/dsl/opal/combo_selection_data_binding_expression.rb +2 -2
  13. data/lib/glimmer/dsl/opal/dsl.rb +10 -12
  14. data/lib/glimmer/dsl/opal/layout_data_expression.rb +2 -2
  15. data/lib/glimmer/dsl/opal/{text_expression.rb → layout_expression.rb} +5 -5
  16. data/lib/glimmer/dsl/opal/list_selection_data_binding_expression.rb +2 -3
  17. data/lib/glimmer/dsl/opal/message_box_expression.rb +20 -0
  18. data/lib/glimmer/dsl/opal/observe_expression.rb +32 -0
  19. data/lib/glimmer/dsl/opal/shell_expression.rb +2 -2
  20. data/lib/glimmer/dsl/opal/{composite_expression.rb → table_column_expression.rb} +3 -3
  21. data/lib/glimmer/dsl/opal/{list_expression.rb → table_expression.rb} +3 -3
  22. data/lib/glimmer/dsl/opal/table_items_data_binding_expression.rb +29 -0
  23. data/lib/glimmer/dsl/opal/widget_expression.rb +23 -0
  24. data/lib/glimmer/opal/display_proxy.rb +23 -0
  25. data/lib/glimmer/opal/element_proxy.rb +48 -13
  26. data/lib/glimmer/swt/browser_proxy.rb +27 -0
  27. data/lib/glimmer/swt/button_proxy.rb +40 -0
  28. data/lib/glimmer/{opal/select_proxy.rb → swt/combo_proxy.rb} +15 -11
  29. data/lib/glimmer/swt/composite_proxy.rb +31 -0
  30. data/lib/glimmer/{opal → swt}/event_listener_proxy.rb +1 -1
  31. data/lib/glimmer/{opal → swt}/grid_layout_proxy.rb +7 -18
  32. data/lib/glimmer/swt/label_proxy.rb +30 -0
  33. data/lib/glimmer/swt/layout_data_proxy.rb +52 -0
  34. data/lib/glimmer/swt/layout_proxy.rb +60 -0
  35. data/lib/glimmer/{opal → swt}/list_proxy.rb +18 -15
  36. data/lib/glimmer/swt/message_box_proxy.rb +146 -0
  37. data/lib/glimmer/{opal → swt}/point.rb +1 -1
  38. data/lib/glimmer/{opal → swt}/property_owner.rb +1 -1
  39. data/lib/glimmer/swt/shell_proxy.rb +235 -0
  40. data/lib/glimmer/swt/tab_folder_proxy.rb +52 -0
  41. data/lib/glimmer/swt/tab_item_proxy.rb +101 -0
  42. data/lib/glimmer/swt/table_column_proxy.rb +56 -0
  43. data/lib/glimmer/swt/table_item_proxy.rb +147 -0
  44. data/lib/glimmer/swt/table_proxy.rb +177 -0
  45. data/lib/glimmer/swt/text_proxy.rb +46 -0
  46. data/lib/glimmer/swt/widget_proxy.rb +389 -0
  47. data/lib/jquery.js +2 -0
  48. data/lib/samples/elaborate/contact_manager.rb +2 -3
  49. data/lib/samples/elaborate/login.rb +0 -1
  50. data/lib/samples/elaborate/tic_tac_toe.rb +5 -5
  51. data/lib/samples/hello/hello_computed.rb +19 -19
  52. data/lib/samples/hello/hello_tab.rb +2 -2
  53. metadata +92 -59
  54. data/lib/glimmer/config.rb +0 -22
  55. data/lib/glimmer/dsl/engine.rb +0 -193
  56. data/lib/glimmer/dsl/expression.rb +0 -42
  57. data/lib/glimmer/dsl/expression_handler.rb +0 -48
  58. data/lib/glimmer/dsl/opal/browser_expression.rb +0 -17
  59. data/lib/glimmer/dsl/opal/button_expression.rb +0 -18
  60. data/lib/glimmer/dsl/opal/combo_expression.rb +0 -17
  61. data/lib/glimmer/dsl/opal/grid_layout_expression.rb +0 -17
  62. data/lib/glimmer/dsl/opal/label_expression.rb +0 -17
  63. data/lib/glimmer/dsl/parent_expression.rb +0 -12
  64. data/lib/glimmer/dsl/static_expression.rb +0 -36
  65. data/lib/glimmer/dsl/top_level_expression.rb +0 -7
  66. data/lib/glimmer/error.rb +0 -6
  67. data/lib/glimmer/invalid_keyword_error.rb +0 -6
  68. data/lib/glimmer/opal/div_proxy.rb +0 -20
  69. data/lib/glimmer/opal/document_proxy.rb +0 -90
  70. data/lib/glimmer/opal/iframe_proxy.rb +0 -23
  71. data/lib/glimmer/opal/input_proxy.rb +0 -41
  72. data/lib/glimmer/opal/label_proxy.rb +0 -25
  73. data/lib/glimmer/opal/layout_data_proxy.rb +0 -31
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.5
1
+ 0.1.0
@@ -1,10 +1,28 @@
1
1
  require 'opal'
2
- require 'opal-parser'
3
- require 'glimmer'
2
+ if RUBY_PLATFORM == 'opal'
3
+ require 'opal-async'
4
+ require 'jquery' # included in glimmer-dsl-opal
5
+ require 'opal-jquery'
6
+ require 'glimmer'
4
7
 
5
- GLIMMER_DSL_OPAL_ROOT = File.expand_path('../..', __FILE__)
6
- GLIMMER_DSL_OPAL_LIB = File.join(GLIMMER_DSL_OPAL_ROOT, 'lib')
7
-
8
- $LOAD_PATH.unshift(GLIMMER_DSL_OPAL_LIB)
9
-
10
- require 'glimmer/dsl/opal/dsl'
8
+ # Spiking async logging
9
+ # logger = Glimmer::Config.logger
10
+ # original_add_method = logger.class.instance_method(:add)
11
+ # logger.define_singleton_method("__original_add", original_add_method)
12
+ # logger.singleton_class.send(:define_method, :add) do |*args|
13
+ # Async::Timeout.new 10000 do
14
+ # __original_add(*args)
15
+ # end
16
+ # end
17
+
18
+ GLIMMER_DSL_OPAL_ROOT = File.expand_path('../..', __FILE__)
19
+ GLIMMER_DSL_OPAL_LIB = File.join(GLIMMER_DSL_OPAL_ROOT, 'lib')
20
+
21
+ $LOAD_PATH.unshift(GLIMMER_DSL_OPAL_LIB)
22
+
23
+ require 'glimmer/dsl/opal/dsl'
24
+ require 'glimmer/data_binding/ext/observable_model'
25
+
26
+ require 'glimmer-dsl-xml'
27
+ Element.alias_native :replace_with, :replaceWith
28
+ end
@@ -27,7 +27,7 @@ module Glimmer
27
27
  @element.set_attribute(@property, converted_value) unless evaluate_property == converted_value
28
28
  end
29
29
 
30
- def evaluate_property
30
+ def evaluate_property
31
31
  @element.send(@property)
32
32
  end
33
33
  end
@@ -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
@@ -39,7 +39,7 @@ module Glimmer
39
39
  end
40
40
 
41
41
  def call(value)
42
- PROPERTY_TYPE_UPDATERS[@property_type].call(@element_proxy, value) unless evaluate_property == value
42
+ PROPERTY_TYPE_UPDATERS[@property_type].call(@element_proxy, value) unless !evaluate_property.is_a?(Array) && evaluate_property == value
43
43
  end
44
44
 
45
45
  def evaluate_property
@@ -0,0 +1,70 @@
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/swt/table_proxy'
6
+ require 'glimmer/swt/table_item_proxy'
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
+ old_items = parent.items
47
+ old_item_ids_per_model = old_items.reduce({}) {|hash, item| hash.merge(item.get_data.hash => item.id) }
48
+ parent.remove_all
49
+ model_collection.each do |model|
50
+ table_item = Glimmer::SWT::TableItemProxy.new(parent)
51
+ for index in 0..(column_properties.size-1)
52
+ table_item.set_text(index, model.send(column_properties[index]).to_s)
53
+ end
54
+ table_item.set_data(model)
55
+ table_item.id = old_item_ids_per_model[model.hash] if old_item_ids_per_model[model.hash]
56
+ end
57
+ selected_table_items = parent.search {|item| selected_table_item_models.include?(item.get_data) }
58
+ selected_table_items = [parent.items.first] if selected_table_items.empty? && !parent.items.empty?
59
+ parent.selection = selected_table_items unless selected_table_items.empty?
60
+ parent.redraw
61
+ end
62
+
63
+ def sort_table(model_collection, parent, column_properties)
64
+ return if model_collection == @last_model_collection
65
+ parent.items = parent.items.sort_by { |item| model_collection.index(item.get_data) }
66
+ @last_model_collection = model_collection
67
+ end
68
+ end
69
+ end
70
+ 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,22 @@
1
+ require 'glimmer/dsl/static_expression'
2
+ require 'glimmer/swt/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::SWT::TableProxy)
14
+ end
15
+
16
+ def interpret(parent, keyword, *args, &block)
17
+ args
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,7 +1,7 @@
1
1
  require 'glimmer/dsl/expression'
2
2
  require 'glimmer/data_binding/model_binding'
3
3
  require 'glimmer/data_binding/element_binding'
4
- require 'glimmer/opal/select_proxy'
4
+ require 'glimmer/swt/combo_proxy'
5
5
 
6
6
  module Glimmer
7
7
  module DSL
@@ -10,7 +10,7 @@ module Glimmer
10
10
  def can_interpret?(parent, keyword, *args, &block)
11
11
  keyword == 'selection' and
12
12
  block.nil? and
13
- parent.is_a?(Glimmer::Opal::SelectProxy) and
13
+ parent.is_a?(Glimmer::SWT::ComboProxy) and
14
14
  args.size == 1 and
15
15
  args[0].is_a?(DataBinding::ModelBinding) and
16
16
  args[0].evaluate_options_property.is_a?(Array)
@@ -1,24 +1,20 @@
1
- require 'opal'
2
- require 'browser'
3
-
4
1
  require 'glimmer/dsl/engine'
5
2
  # Dir[File.expand_path('../*_expression.rb', __FILE__)].each {|f| require f}
6
3
  require 'glimmer/dsl/opal/shell_expression'
7
- require 'glimmer/dsl/opal/label_expression'
4
+ require 'glimmer/dsl/opal/widget_expression'
8
5
  require 'glimmer/dsl/opal/property_expression'
9
- require 'glimmer/dsl/opal/combo_expression'
10
- require 'glimmer/dsl/opal/composite_expression'
11
- require 'glimmer/dsl/opal/button_expression'
12
6
  require 'glimmer/dsl/opal/bind_expression'
13
7
  require 'glimmer/dsl/opal/data_binding_expression'
14
8
  require 'glimmer/dsl/opal/combo_selection_data_binding_expression'
15
9
  require 'glimmer/dsl/opal/widget_listener_expression'
16
- require 'glimmer/dsl/opal/grid_layout_expression'
17
- require 'glimmer/dsl/opal/text_expression'
18
- require 'glimmer/dsl/opal/list_expression'
19
- require 'glimmer/dsl/opal/browser_expression'
10
+ require 'glimmer/dsl/opal/message_box_expression'
11
+ require 'glimmer/dsl/opal/async_exec_expression'
12
+ require 'glimmer/dsl/opal/observe_expression'
13
+ require 'glimmer/dsl/opal/layout_expression'
20
14
  require 'glimmer/dsl/opal/layout_data_expression'
21
15
  require 'glimmer/dsl/opal/list_selection_data_binding_expression'
16
+ require 'glimmer/dsl/opal/table_items_data_binding_expression'
17
+ require 'glimmer/dsl/opal/column_properties_expression'
22
18
 
23
19
  module Glimmer
24
20
  module DSL
@@ -27,11 +23,13 @@ module Glimmer
27
23
  Opal,
28
24
  %w[
29
25
  widget_listener
26
+ table_items_data_binding
30
27
  combo_selection_data_binding
31
28
  list_selection_data_binding
32
29
  data_binding
33
- text
30
+ layout
34
31
  property
32
+ widget
35
33
  ]
36
34
  )
37
35
  end
@@ -1,6 +1,6 @@
1
1
  require 'glimmer/dsl/static_expression'
2
2
  require 'glimmer/dsl/parent_expression'
3
- require 'glimmer/opal/layout_data_proxy'
3
+ require 'glimmer/swt/layout_data_proxy'
4
4
 
5
5
  module Glimmer
6
6
  module DSL
@@ -9,7 +9,7 @@ module Glimmer
9
9
  include ParentExpression
10
10
 
11
11
  def interpret(parent, keyword, *args, &block)
12
- Glimmer::Opal::LayoutDataProxy.new(parent, args)
12
+ Glimmer::SWT::LayoutDataProxy.new(parent, args)
13
13
  end
14
14
  end
15
15
  end
@@ -1,20 +1,20 @@
1
1
  require 'glimmer/dsl/expression'
2
2
  require 'glimmer/dsl/parent_expression'
3
- require 'glimmer/opal/input_proxy'
3
+ require 'glimmer/swt/layout_proxy'
4
4
 
5
5
  module Glimmer
6
6
  module DSL
7
7
  module Opal
8
- class TextExpression < Expression
8
+ class LayoutExpression < Expression
9
9
  include ParentExpression
10
10
 
11
11
  def can_interpret?(parent, keyword, *args, &block)
12
- keyword == 'text' and parent and block_given?
12
+ parent.is_a?(Glimmer::SWT::CompositeProxy) &&
13
+ Glimmer::SWT::LayoutProxy.layout_exists?(keyword)
13
14
  end
14
15
 
15
16
  def interpret(parent, keyword, *args, &block)
16
- args << {type: 'text'}
17
- Glimmer::Opal::InputProxy.new(parent, args)
17
+ Glimmer::SWT::LayoutProxy.for(keyword, parent, args)
18
18
  end
19
19
  end
20
20
  end
@@ -2,7 +2,7 @@ require 'glimmer/dsl/expression'
2
2
  require 'glimmer/data_binding/model_binding'
3
3
  require 'glimmer/data_binding/element_binding'
4
4
  require 'glimmer/data_binding/list_selection_binding'
5
- require 'glimmer/opal/list_proxy'
5
+ require 'glimmer/swt/list_proxy'
6
6
 
7
7
  module Glimmer
8
8
  module DSL
@@ -11,7 +11,7 @@ module Glimmer
11
11
  def can_interpret?(parent, keyword, *args, &block)
12
12
  keyword == 'selection' and
13
13
  block.nil? and
14
- parent.is_a?(Glimmer::Opal::ListProxy) and
14
+ parent.is_a?(Glimmer::SWT::ListProxy) and
15
15
  args.size == 1 and
16
16
  args[0].is_a?(DataBinding::ModelBinding) and
17
17
  args[0].evaluate_options_property.is_a?(Array)
@@ -31,7 +31,6 @@ module Glimmer
31
31
  list_selection_binding.call(model_binding.evaluate_property)
32
32
  #TODO check if nested data binding works for list widget and other widgets that need custom data binding
33
33
  list_selection_binding.observe(model, model_binding.property_name_expression)
34
-
35
34
  parent.on_widget_selected do
36
35
  model_binding.call(list_selection_binding.evaluate_property)
37
36
  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/swt/message_box_proxy'
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::SWT::MessageBoxProxy.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
@@ -1,7 +1,7 @@
1
1
  require 'glimmer/dsl/static_expression'
2
2
  require 'glimmer/dsl/top_level_expression'
3
3
  require 'glimmer/dsl/parent_expression'
4
- require 'glimmer/opal/document_proxy'
4
+ require 'glimmer/swt/shell_proxy'
5
5
 
6
6
  module Glimmer
7
7
  module DSL
@@ -11,7 +11,7 @@ module Glimmer
11
11
  include ParentExpression
12
12
 
13
13
  def interpret(parent, keyword, *args, &block)
14
- Glimmer::Opal::DocumentProxy.new(args)
14
+ Glimmer::SWT::ShellProxy.new(*args)
15
15
  end
16
16
  end
17
17
  end
@@ -1,15 +1,15 @@
1
1
  require 'glimmer/dsl/static_expression'
2
2
  require 'glimmer/dsl/parent_expression'
3
- require 'glimmer/opal/div_proxy'
3
+ require 'glimmer/opal/table_column'
4
4
 
5
5
  module Glimmer
6
6
  module DSL
7
7
  module Opal
8
- class CompositeExpression < StaticExpression
8
+ class TableColumnExpression < StaticExpression
9
9
  include ParentExpression
10
10
 
11
11
  def interpret(parent, keyword, *args, &block)
12
- Glimmer::Opal::DivProxy.new(parent, args)
12
+ Glimmer::Opal::TableColumn.new(parent, args)
13
13
  end
14
14
  end
15
15
  end
@@ -1,15 +1,15 @@
1
1
  require 'glimmer/dsl/static_expression'
2
2
  require 'glimmer/dsl/parent_expression'
3
- require 'glimmer/opal/list_proxy'
3
+ require 'glimmer/swt/table_proxy'
4
4
 
5
5
  module Glimmer
6
6
  module DSL
7
7
  module Opal
8
- class ListExpression < StaticExpression
8
+ class TableExpression < StaticExpression
9
9
  include ParentExpression
10
10
 
11
11
  def interpret(parent, keyword, *args, &block)
12
- Glimmer::Opal::ListProxy.new(parent, args)
12
+ Glimmer::SWT::TableProxy.new(parent, args)
13
13
  end
14
14
  end
15
15
  end