glimmer-dsl-opal 0.7.0 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +290 -53
  4. data/VERSION +1 -1
  5. data/lib/display.rb +31 -0
  6. data/lib/file.rb +29 -0
  7. data/lib/glimmer-dsl-opal.rb +30 -1
  8. data/lib/glimmer-dsl-opal/ext/date.rb +1 -1
  9. data/lib/glimmer-dsl-opal/ext/struct.rb +37 -0
  10. data/lib/glimmer-dsl-opal/samples/elaborate/contact_manager.rb +50 -23
  11. data/lib/glimmer-dsl-opal/samples/elaborate/login.rb +22 -5
  12. data/lib/glimmer-dsl-opal/samples/hello/hello_browser.rb +1 -1
  13. data/lib/glimmer-dsl-opal/samples/hello/hello_button.rb +46 -0
  14. data/lib/glimmer-dsl-opal/samples/hello/hello_custom_shell.rb +7 -7
  15. data/lib/glimmer-dsl-opal/samples/hello/hello_table.rb +5 -5
  16. data/lib/glimmer-dsl-swt.rb +20 -35
  17. data/lib/glimmer/data_binding/table_items_binding.rb +6 -4
  18. data/lib/glimmer/dsl/opal/custom_widget_expression.rb +6 -0
  19. data/lib/glimmer/dsl/opal/widget_expression.rb +6 -2
  20. data/lib/glimmer/swt/combo_proxy.rb +40 -1
  21. data/lib/glimmer/swt/composite_proxy.rb +5 -1
  22. data/lib/glimmer/swt/control_editor.rb +54 -0
  23. data/lib/glimmer/swt/date_time_proxy.rb +66 -1
  24. data/lib/glimmer/swt/font_proxy.rb +4 -4
  25. data/lib/glimmer/swt/grid_layout_proxy.rb +20 -12
  26. data/lib/glimmer/swt/label_proxy.rb +11 -3
  27. data/lib/glimmer/swt/layout_data_proxy.rb +10 -7
  28. data/lib/glimmer/swt/layout_proxy.rb +1 -1
  29. data/lib/glimmer/swt/message_box_proxy.rb +2 -10
  30. data/lib/glimmer/swt/table_column_proxy.rb +9 -0
  31. data/lib/glimmer/swt/table_editor.rb +65 -0
  32. data/lib/glimmer/swt/table_item_proxy.rb +36 -0
  33. data/lib/glimmer/swt/table_proxy.rb +375 -17
  34. data/lib/glimmer/swt/text_proxy.rb +1 -1
  35. data/lib/glimmer/swt/widget_proxy.rb +99 -21
  36. data/lib/glimmer/ui/custom_shell.rb +9 -7
  37. data/lib/os.rb +36 -0
  38. metadata +25 -3
@@ -4,11 +4,10 @@ require 'glimmer/swt/widget_proxy'
4
4
  module Glimmer
5
5
  module SWT
6
6
  class LabelProxy < WidgetProxy
7
- attr_reader :text, :background_image, :image, :alignment
7
+ attr_reader :text, :background_image, :image
8
8
 
9
9
  def initialize(parent, args, block)
10
10
  super(parent, args, block)
11
- self.alignment = [:left, :center, :right].detect {|align| args.detect { |arg| SWTProxy[align] == arg } }
12
11
  end
13
12
 
14
13
  def text=(value)
@@ -32,6 +31,15 @@ module Glimmer
32
31
  def element
33
32
  'label'
34
33
  end
34
+
35
+ def alignment
36
+ if @alignment.nil?
37
+ found_arg = nil
38
+ @alignment = [:left, :center, :right].detect {|align| found_arg = args.detect { |arg| SWTProxy[align] == SWTProxy[arg] } }
39
+ args.delete(found_arg)
40
+ end
41
+ @alignment
42
+ end
35
43
 
36
44
  def alignment=(value)
37
45
  # TODO consider storing swt value in the future instead
@@ -44,7 +52,7 @@ module Glimmer
44
52
  label_id = id
45
53
  label_class = name
46
54
  @dom ||= html {
47
- label(id: label_id, class: label_class) {
55
+ label(id: label_id, class: label_class, style: "text-align: #{alignment};") {
48
56
  label_text
49
57
  }
50
58
  }.to_s
@@ -3,6 +3,7 @@ require 'glimmer/swt/property_owner'
3
3
  module Glimmer
4
4
  module SWT
5
5
  class LayoutDataProxy
6
+ # TODO make this polymorphic as GridData or RowData subclasses
6
7
  include Glimmer::SWT::PropertyOwner
7
8
  attr_reader :parent,
8
9
  :args,
@@ -44,10 +45,11 @@ module Glimmer
44
45
  def horizontal_alignment=(horizontal_alignment)
45
46
  @horizontal_alignment = horizontal_alignment
46
47
  return if @horizontal_alignment.nil?
47
- if @horizontal_alignment == 'fill'
48
- @parent.dom_element.css('width', '100%') if width_hint.nil?
49
- else
50
- @parent.dom_element.css('text-align', @horizontal_alignment)
48
+ if @horizontal_alignment != 'fill'
49
+ @parent.dom_element.css('text-align', @horizontal_alignment.to_s)
50
+ @parent.dom_element.css('place-self', @horizontal_alignment.to_s)
51
+ @parent.dom_element.css('margin-left', 'auto') if ['right', 'center'].include?(@horizontal_alignment.to_s)
52
+ @parent.dom_element.css('margin-right', 'auto') if ['left', 'center'].include?(@horizontal_alignment.to_s)
51
53
  end
52
54
  # TODO
53
55
  # reapply
@@ -85,18 +87,19 @@ module Glimmer
85
87
 
86
88
  def grab_excess_horizontal_space=(grab_excess_horizontal_space)
87
89
  @grab_excess_horizontal_space = grab_excess_horizontal_space
88
- @parent.dom_element.css('width', "100%") if @grab_excess_horizontal_space && width_hint.nil?
90
+ @parent.dom_element.css('justify-self', @horizontal_alignment) if @grab_excess_horizontal_space && @horizontal_alignment != 'fill' && width_hint.nil?
91
+ @parent.parent.dom_element.css('justify-content', "normal") if @grab_excess_horizontal_space
89
92
  # reapply
90
93
  end
91
94
 
92
95
  def grab_excess_vertical_space=(grab_excess_vertical_space)
93
96
  @grab_excess_vertical_space = grab_excess_vertical_space
94
- @parent.dom_element.css('height', "100%") if @grab_excess_vertical_space && height_hint.nil?
95
- # TODO
97
+ @parent.dom_element.css('height', "100%") if @grab_excess_vertical_space && @vertical_alignment == 'fill' && height_hint.nil?
96
98
  # reapply
97
99
  end
98
100
 
99
101
  def reapply
102
+ # TODO remove reapply method
100
103
  # @parent.css = <<~CSS
101
104
  # CSS
102
105
  end
@@ -18,7 +18,7 @@ module Glimmer
18
18
  a_layout_class = Glimmer::SWT.const_get(class_name_main.to_sym) rescue Glimmer::SWT.const_get(class_name_alternative.to_sym)
19
19
  a_layout_class if a_layout_class.ancestors.include?(Glimmer::SWT::LayoutProxy)
20
20
  rescue => e
21
- puts "Layout #{keyword} was not found!"
21
+ Glimmer::Config.logger.debug "Layout #{keyword} was not found!"
22
22
  nil
23
23
  end
24
24
 
@@ -8,7 +8,7 @@ module Glimmer
8
8
 
9
9
  def initialize(parent, args, block)
10
10
  i = 0
11
- @parent = parent || DisplayProxy.instance.shells.first
11
+ @parent = parent || DisplayProxy.instance.shells.last
12
12
  @args = args
13
13
  @block = block
14
14
  @children = Set.new
@@ -30,16 +30,8 @@ module Glimmer
30
30
  dom_element.find('.modal-content .message').html(@text)
31
31
  end
32
32
 
33
- def document
34
- element = self
35
- begin
36
- element = element.parent
37
- end while(element.parent)
38
- element
39
- end
40
-
41
33
  def open
42
- document.post_initialize_child(self)
34
+ parent.post_initialize_child(self)
43
35
  end
44
36
 
45
37
  def hide
@@ -74,6 +74,15 @@ module Glimmer
74
74
  dom_element.find('.sort-direction')
75
75
  end
76
76
 
77
+ # Sets editor (e.g. combo)
78
+ def editor=(*args)
79
+ @editor = args
80
+ end
81
+
82
+ def editable?
83
+ !@editor&.include?(:none)
84
+ end
85
+
77
86
  def observation_request_to_event_mapping
78
87
  {
79
88
  'on_widget_selected' => {
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer/swt/control_editor'
23
+
24
+ module Glimmer
25
+ module SWT
26
+ # Emulates SWT's native org.eclipse.swt.custom.TableEditor
27
+ class TableEditor < ControlEditor
28
+ alias table composite
29
+
30
+ def editor=(editor_widget, table_item, table_column_index)
31
+ # TODO consider making editor not gain an ID or gain a separate set of IDs to avoid clashing with standard widget predictability of ID
32
+ @table_item = table_item
33
+ @table_column_index = table_column_index
34
+ @editor_widget = editor_widget
35
+ @old_value = table_item.cell_dom_element(table_column_index).html
36
+ table_item.cell_dom_element(table_column_index).html('')
37
+ editor_widget.render(table_item.cell_dom_element(table_column_index))
38
+ # TODO tweak the width perfectly so it doesn't expand the table cell
39
+ # editor_widget.dom_element.css('width', 'calc(100% - 20px)')
40
+ editor_widget.dom_element.css('width', "#{minimumWidth}%") # TODO implement property with pixels (and perhaps derive percentage separately from pixels)
41
+ editor_widget.dom_element.css('height', "#{minimumHeight}px")
42
+ editor_widget.dom_element.add_class('table-editor')
43
+ # TODO consider relying on autofocus instead
44
+ editor_widget.dom_element.focus
45
+ # TODO consider doing the following line only for :text editor
46
+ editor_widget.dom_element.select
47
+ end
48
+ alias set_editor editor=
49
+ alias setEditor editor=
50
+
51
+ def cancel!
52
+ done!
53
+ end
54
+
55
+ def save!
56
+ done!
57
+ end
58
+
59
+ def done!
60
+ @table_item.cell_dom_element(@table_column_index).html(@old_value) unless @old_value.nil?
61
+ @old_value = nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,9 +1,33 @@
1
+ # Copyright (c) 2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
1
22
  require 'glimmer/swt/widget_proxy'
2
23
 
3
24
  module Glimmer
4
25
  module SWT
5
26
  class TableItemProxy < WidgetProxy
6
27
  STYLE = <<~CSS
28
+ tr.table-item td {
29
+ padding-bottom: 0;
30
+ }
7
31
  tr.table-item:nth-child(even):not(.selected) {
8
32
  background: rgb(243, 244, 246);
9
33
  }
@@ -60,6 +84,10 @@ module Glimmer
60
84
  'tr'
61
85
  end
62
86
 
87
+ def cell_dom_element(column_index)
88
+ dom_element.find("td:nth-child(#{column_index + 1})")
89
+ end
90
+
63
91
  def redraw
64
92
  super() #TODO re-enable and remove below lines
65
93
 
@@ -111,6 +139,14 @@ module Glimmer
111
139
  redraw
112
140
  end
113
141
 
142
+ def redraw_selection
143
+ if parent.selection.include?(self)
144
+ dom_element.add_class('selected')
145
+ else
146
+ dom_element.remove_class('selected')
147
+ end
148
+ end
149
+
114
150
  def on_widget_selected(&block)
115
151
  event = 'click'
116
152
  delegate = $document.on(event, selector, &block)
@@ -1,20 +1,251 @@
1
+ # Copyright (c) 2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
1
22
  require 'glimmer/swt/widget_proxy'
2
23
  require 'glimmer/swt/table_column_proxy'
24
+ require 'glimmer/swt/table_item_proxy'
25
+ require 'glimmer/swt/table_editor'
3
26
 
4
27
  module Glimmer
5
28
  module SWT
6
- class TableProxy < WidgetProxy
29
+ class TableProxy < CompositeProxy
7
30
  attr_reader :columns, :selection,
8
- :sort_type, :sort_column, :sort_property, :sort_block, :sort_by_block, :additional_sort_properties
31
+ :sort_type, :sort_column, :sort_property, :sort_block, :sort_by_block, :additional_sort_properties,
32
+ :editor, :table_editor
9
33
  attr_accessor :column_properties, :item_count, :data
10
34
  alias items children
11
35
  alias model_binding data
12
36
 
37
+ class << self
38
+ include Glimmer
39
+
40
+ def editors
41
+ @editors ||= {
42
+ # ensure editor can work with string keys not just symbols (leave one string in for testing)
43
+ text: {
44
+ widget_value_property: :text,
45
+ editor_gui: lambda do |args, model, property, table_proxy|
46
+ table_proxy.table_editor.minimumWidth = 90
47
+ table_proxy.table_editor.minimumHeight = 10
48
+ table_editor_widget_proxy = text(*args) {
49
+ text model.send(property)
50
+ focus true
51
+ on_focus_lost {
52
+ table_proxy.finish_edit!
53
+ }
54
+ on_key_pressed { |key_event|
55
+ if key_event.keyCode == swt(:cr)
56
+ table_proxy.finish_edit!
57
+ elsif key_event.keyCode == swt(:esc)
58
+ table_proxy.cancel_edit!
59
+ end
60
+ }
61
+ }
62
+ # table_editor_widget_proxy.swt_widget.selectAll # TODO select all
63
+ table_editor_widget_proxy
64
+ end,
65
+ },
66
+ combo: {
67
+ widget_value_property: :text,
68
+ editor_gui: lambda do |args, model, property, table_proxy|
69
+ first_time = true
70
+ table_proxy.table_editor.minimumWidth = 90
71
+ table_proxy.table_editor.minimumHeight = 18
72
+ table_editor_widget_proxy = combo(*args) {
73
+ items model.send("#{property}_options")
74
+ text model.send(property)
75
+ focus true
76
+ on_focus_lost {
77
+ table_proxy.finish_edit!
78
+ }
79
+ on_key_pressed { |key_event|
80
+ if key_event.keyCode == swt(:cr)
81
+ table_proxy.finish_edit!
82
+ elsif key_event.keyCode == swt(:esc)
83
+ table_proxy.cancel_edit!
84
+ end
85
+ }
86
+ on_widget_selected {
87
+ if !OS.windows? || !first_time || first_time && model.send(property) != table_editor_widget_proxy.text
88
+ table_proxy.finish_edit!
89
+ end
90
+ }
91
+ }
92
+ table_editor_widget_proxy
93
+ end,
94
+ },
95
+ checkbox: {
96
+ widget_value_property: :selection,
97
+ editor_gui: lambda do |args, model, property, table_proxy|
98
+ first_time = true
99
+ table_proxy.table_editor.minimumHeight = 25
100
+ checkbox(*args) {
101
+ selection model.send(property)
102
+ focus true
103
+ on_widget_selected {
104
+ table_proxy.finish_edit!
105
+ }
106
+ on_focus_lost {
107
+ table_proxy.finish_edit!
108
+ }
109
+ on_key_pressed { |key_event|
110
+ if key_event.keyCode == swt(:cr)
111
+ table_proxy.finish_edit!
112
+ elsif key_event.keyCode == swt(:esc)
113
+ table_proxy.cancel_edit!
114
+ end
115
+ }
116
+ }
117
+ end,
118
+ },
119
+ date: {
120
+ widget_value_property: :date_time,
121
+ editor_gui: lambda do |args, model, property, table_proxy|
122
+ first_time = true
123
+ table_proxy.table_editor.minimumWidth = 90
124
+ table_proxy.table_editor.minimumHeight = 15
125
+ date(*args) {
126
+ date_time model.send(property)
127
+ focus true
128
+ on_widget_selected {
129
+ table_proxy.finish_edit!
130
+ }
131
+ on_key_pressed { |key_event|
132
+ if key_event.keyCode == swt(:cr)
133
+ table_proxy.finish_edit!
134
+ elsif key_event.keyCode == swt(:esc)
135
+ table_proxy.cancel_edit!
136
+ end
137
+ }
138
+ }
139
+ end,
140
+ },
141
+ date_drop_down: {
142
+ widget_value_property: :date_time,
143
+ editor_gui: lambda do |args, model, property, table_proxy|
144
+ first_time = true
145
+ table_proxy.table_editor.minimumWidth = 80
146
+ table_proxy.table_editor.minimumHeight = 15
147
+ date_drop_down(*args) {
148
+ date_time model.send(property)
149
+ focus true
150
+ on_widget_selected {
151
+ table_proxy.finish_edit!
152
+ }
153
+ on_key_pressed { |key_event|
154
+ if key_event.keyCode == swt(:cr)
155
+ table_proxy.finish_edit!
156
+ elsif key_event.keyCode == swt(:esc)
157
+ table_proxy.cancel_edit!
158
+ end
159
+ }
160
+ }
161
+ end,
162
+ },
163
+ time: {
164
+ widget_value_property: :date_time,
165
+ editor_gui: lambda do |args, model, property, table_proxy|
166
+ first_time = true
167
+ table_proxy.table_editor.minimumWidth = 80
168
+ table_proxy.table_editor.minimumHeight = 15
169
+ time(*args) {
170
+ date_time model.send(property)
171
+ focus true
172
+ on_widget_selected {
173
+ table_proxy.finish_edit!
174
+ }
175
+ on_focus_lost {
176
+ table_proxy.finish_edit!
177
+ }
178
+ on_key_pressed { |key_event|
179
+ if key_event.keyCode == swt(:cr)
180
+ table_proxy.finish_edit!
181
+ elsif key_event.keyCode == swt(:esc)
182
+ table_proxy.cancel_edit!
183
+ end
184
+ }
185
+ }
186
+ end,
187
+ },
188
+ radio: {
189
+ widget_value_property: :selection,
190
+ editor_gui: lambda do |args, model, property, table_proxy|
191
+ first_time = true
192
+ table_proxy.table_editor.minimumHeight = 25
193
+ radio(*args) {
194
+ selection model.send(property)
195
+ focus true
196
+ on_widget_selected {
197
+ table_proxy.finish_edit!
198
+ }
199
+ on_focus_lost {
200
+ table_proxy.finish_edit!
201
+ }
202
+ on_key_pressed { |key_event|
203
+ if key_event.keyCode == swt(:cr)
204
+ table_proxy.finish_edit!
205
+ elsif key_event.keyCode == swt(:esc)
206
+ table_proxy.cancel_edit!
207
+ end
208
+ }
209
+ }
210
+ end,
211
+ },
212
+ spinner: {
213
+ widget_value_property: :selection,
214
+ editor_gui: lambda do |args, model, property, table_proxy|
215
+ first_time = true
216
+ table_proxy.table_editor.minimumHeight = 25
217
+ table_editor_widget_proxy = spinner(*args) {
218
+ selection model.send(property)
219
+ focus true
220
+ on_focus_lost {
221
+ table_proxy.finish_edit!
222
+ }
223
+ on_key_pressed { |key_event|
224
+ if key_event.keyCode == swt(:cr)
225
+ table_proxy.finish_edit!
226
+ elsif key_event.keyCode == swt(:esc)
227
+ table_proxy.cancel_edit!
228
+ end
229
+ }
230
+ }
231
+ table_editor_widget_proxy
232
+ end,
233
+ },
234
+ }
235
+ end
236
+ end
237
+
13
238
  def initialize(parent, args, block)
14
239
  super(parent, args, block)
15
240
  @columns = []
16
241
  @children = []
242
+ @editors = []
17
243
  @selection = []
244
+ @table_editor = TableEditor.new(self)
245
+ @table_editor.horizontalAlignment = SWTProxy[:left]
246
+ @table_editor.grabHorizontal = true
247
+ @table_editor.minimumWidth = 90
248
+ @table_editor.minimumHeight = 20
18
249
  if editable?
19
250
  on_mouse_up { |event|
20
251
  edit_table_item(event.table_item, event.column_index)
@@ -26,10 +257,24 @@ module Glimmer
26
257
  def post_initialize_child(child)
27
258
  if child.is_a?(TableColumnProxy)
28
259
  @columns << child
29
- else
260
+ child.render
261
+ elsif child.is_a?(TableItemProxy)
30
262
  @children << child
263
+ child.render
264
+ else
265
+ @editors << child
266
+ end
267
+ end
268
+
269
+ # Executes for the parent of a child that just got disposed
270
+ def post_dispose_child(child)
271
+ if child.is_a?(TableColumnProxy)
272
+ @columns&.delete(child)
273
+ elsif child.is_a?(TableItemProxy)
274
+ @children&.delete(child)
275
+ else
276
+ @editors&.delete(child)
31
277
  end
32
- child.redraw
33
278
  end
34
279
 
35
280
  def post_add_content
@@ -38,6 +283,10 @@ module Glimmer
38
283
  @initially_sorted = true
39
284
  end
40
285
 
286
+ def default_layout
287
+ nil
288
+ end
289
+
41
290
  def get_data(key=nil)
42
291
  data
43
292
  end
@@ -60,11 +309,12 @@ module Glimmer
60
309
  new_selection = new_selection.to_a
61
310
  changed = (selection + new_selection) - (selection & new_selection)
62
311
  @selection = new_selection
63
- changed.each(&:redraw)
312
+ changed.each(&:redraw_selection)
64
313
  end
65
314
 
66
315
  def items=(new_items)
67
316
  @children = new_items
317
+ # TODO optimize in the future by sorting elements in DOM directly when no change to elements occur other than sort
68
318
  redraw
69
319
  end
70
320
 
@@ -97,10 +347,6 @@ module Glimmer
97
347
  self.selection = new_selection
98
348
  end
99
349
 
100
- def search(&condition)
101
- items.select {|item| condition.nil? || condition.call(item)}
102
- end
103
-
104
350
  def sort_block=(comparator)
105
351
  @sort_block = comparator
106
352
  end
@@ -110,7 +356,7 @@ module Glimmer
110
356
  end
111
357
 
112
358
  def sort_property=(new_sort_property)
113
- @sort_property = [new_sort_property].flatten.compact
359
+ @sort_property = new_sort_property.to_collection
114
360
  end
115
361
 
116
362
  def detect_sort_type
@@ -131,7 +377,7 @@ module Glimmer
131
377
 
132
378
  def column_sort_properties
133
379
  column_properties.zip(columns.map(&:sort_property)).map do |pair|
134
- [pair.compact.last].flatten.compact
380
+ pair.compact.last.to_collection
135
381
  end
136
382
  end
137
383
 
@@ -162,7 +408,7 @@ module Glimmer
162
408
  end
163
409
  end
164
410
 
165
- new_sort_property = [new_sort_property].flatten.compact unless new_sort_property.is_a?(Array)
411
+ new_sort_property = new_sort_property.to_collection unless new_sort_property.is_a?(Array)
166
412
  @sort_direction = @sort_direction.nil? || @sort_property.first != new_sort_property.first || @sort_direction == :descending ? :ascending : :descending
167
413
 
168
414
  @sort_property = new_sort_property
@@ -223,9 +469,118 @@ module Glimmer
223
469
  @additional_sort_properties = args unless args.empty?
224
470
  end
225
471
 
226
- def edit_table_item(table_item, column_index)
227
- table_item&.edit(column_index) unless column_index.nil?
472
+ def editor=(args)
473
+ @editor = args
474
+ end
475
+
476
+ # Indicates if table is in edit mode, thus displaying a text widget for a table item cell
477
+ def edit_mode?
478
+ !!@edit_mode
479
+ end
480
+
481
+ def cancel_edit!
482
+ @cancel_edit&.call if @edit_mode
483
+ end
484
+
485
+ def finish_edit!
486
+ @finish_edit&.call if @edit_mode
487
+ end
488
+
489
+ # Indicates if table is editing a table item because the user hit ENTER or focused out after making a change in edit mode to a table item cell.
490
+ # It is set to false once change is saved to model
491
+ def edit_in_progress?
492
+ !!@edit_in_progress
493
+ end
494
+
495
+ def edit_selected_table_item(column_index, before_write: nil, after_write: nil, after_cancel: nil)
496
+ edit_table_item(selection.first, column_index, before_write: before_write, after_write: after_write, after_cancel: after_cancel)
228
497
  end
498
+
499
+ # TODO migrate the following to the next method
500
+ # def edit_table_item(table_item, column_index)
501
+ # table_item&.edit(column_index) unless column_index.nil?
502
+ # end
503
+
504
+ def edit_table_item(table_item, column_index, before_write: nil, after_write: nil, after_cancel: nil)
505
+ return if table_item.nil? || (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
506
+ @edit_column_index = column_index
507
+ @edit_table_item = table_item
508
+ column_index = column_index.to_i
509
+ model = table_item.data
510
+ property = column_properties[column_index]
511
+ cancel_edit!
512
+ return unless columns[column_index].editable?
513
+ action_taken = false
514
+ @edit_mode = true
515
+
516
+ editor_config = columns[column_index].editor || editor
517
+ editor_config = editor_config.to_collection
518
+ editor_widget_options = editor_config.last.is_a?(Hash) ? editor_config.last : {}
519
+ editor_widget_arg_last_index = editor_config.last.is_a?(Hash) ? -2 : -1
520
+ editor_widget = (editor_config[0] || :text).to_sym
521
+ editor_widget_args = editor_config[1..editor_widget_arg_last_index]
522
+ model_editing_property = editor_widget_options[:property] || property
523
+ widget_value_property = TableProxy::editors.symbolize_keys[editor_widget][:widget_value_property]
524
+
525
+ @cancel_edit = lambda do |event=nil|
526
+ @cancel_in_progress = true
527
+ @table_editor.cancel!
528
+ @table_editor_widget_proxy&.dispose
529
+ @table_editor_widget_proxy = nil
530
+ after_cancel&.call
531
+ @edit_in_progress = false
532
+ @cancel_in_progress = false
533
+ @cancel_edit = nil
534
+ @edit_table_item = @edit_column_index = nil if (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
535
+ @edit_mode = false
536
+ end
537
+
538
+ @finish_edit = lambda do |event=nil|
539
+ new_value = @table_editor_widget_proxy&.send(widget_value_property)
540
+ if table_item.disposed?
541
+ @cancel_edit.call
542
+ elsif !new_value.nil? && !action_taken && !@edit_in_progress && !@cancel_in_progress
543
+ action_taken = true
544
+ @edit_in_progress = true
545
+ if new_value == model.send(model_editing_property)
546
+ @cancel_edit.call
547
+ else
548
+ before_write&.call
549
+ @table_editor.save!(widget_value_property: widget_value_property)
550
+ model.send("#{model_editing_property}=", new_value) # makes table update itself, so must search for selected table item again
551
+ # Table refresh happens here because of model update triggering observers, so must retrieve table item again
552
+ edited_table_item = search { |ti| ti.data == model }.first
553
+ show_item(edited_table_item)
554
+ @table_editor_widget_proxy&.dispose
555
+ @table_editor_widget_proxy = nil
556
+ after_write&.call(edited_table_item)
557
+ @edit_in_progress = false
558
+ @edit_table_item = @edit_column_index = nil
559
+ end
560
+ end
561
+ end
562
+
563
+ content {
564
+ @table_editor_widget_proxy = TableProxy::editors.symbolize_keys[editor_widget][:editor_gui].call(editor_widget_args, model, model_editing_property, self)
565
+ }
566
+ @table_editor.set_editor(@table_editor_widget_proxy, table_item, column_index)
567
+ rescue => e
568
+ Glimmer::Config.logger.error {e.full_message}
569
+ raise e
570
+ end
571
+
572
+ def show_item(table_item)
573
+ table_item.dom_element.focus
574
+ end
575
+
576
+ def add_listener(underscored_listener_name, &block)
577
+ enhanced_block = lambda do |event|
578
+ event.extend(TableListenerEvent)
579
+ block.call(event)
580
+ end
581
+ super(underscored_listener_name, &enhanced_block)
582
+ end
583
+
229
584
 
230
585
  def header_visible=(value)
231
586
  @header_visible = value
@@ -259,7 +614,8 @@ module Glimmer
259
614
  event.singleton_class.send(:define_method, :column_index) do
260
615
  (table_data || event.target).attr('data-column-index')
261
616
  end
262
- event_listener.call(event)
617
+
618
+ event_listener.call(event) unless event.table_item.nil? && event.column_index.nil?
263
619
  }
264
620
  }
265
621
 
@@ -274,7 +630,8 @@ module Glimmer
274
630
  },
275
631
  'on_widget_selected' => {
276
632
  event: 'mouseup',
277
- }
633
+ event_handler: mouse_handler,
634
+ },
278
635
  }
279
636
  end
280
637
 
@@ -337,7 +694,8 @@ module Glimmer
337
694
  table_id = id
338
695
  table_id_style = css
339
696
  table_id_css_classes = css_classes
340
- table_id_css_classes << 'table'
697
+ table_id_css_classes << 'table' unless table_id_css_classes.include?('table')
698
+ table_id_css_classes << 'editable' if editable? && !table_id_css_classes.include?('editable')
341
699
  table_id_css_classes_string = table_id_css_classes.to_a.join(' ')
342
700
  @dom ||= html {
343
701
  table(id: table_id, style: table_id_style, class: table_id_css_classes_string) {