glimmer-dsl-opal 0.5.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/README.md +438 -17
  4. data/VERSION +1 -1
  5. data/app/assets/images/glimmer/images/calendar.gif +0 -0
  6. data/app/assets/images/glimmer/images/ui-icons_222222_256x240.png +0 -0
  7. data/app/assets/images/glimmer/images/ui-icons_444444_256x240.png +0 -0
  8. data/app/assets/images/glimmer/images/ui-icons_555555_256x240.png +0 -0
  9. data/app/assets/images/glimmer/images/ui-icons_777620_256x240.png +0 -0
  10. data/app/assets/images/glimmer/images/ui-icons_777777_256x240.png +0 -0
  11. data/app/assets/images/glimmer/images/ui-icons_cc0000_256x240.png +0 -0
  12. data/app/assets/images/glimmer/images/ui-icons_ffffff_256x240.png +0 -0
  13. data/app/assets/stylesheets/glimmer/glimmer.css +15 -0
  14. data/app/assets/stylesheets/glimmer/jquery-ui.css +1312 -0
  15. data/app/assets/stylesheets/glimmer/jquery-ui.structure.css +886 -0
  16. data/app/assets/stylesheets/glimmer/jquery-ui.theme.css +443 -0
  17. data/app/assets/stylesheets/glimmer/jquery.ui.timepicker.css +57 -0
  18. data/lib/display.rb +31 -0
  19. data/lib/file.rb +29 -0
  20. data/lib/glimmer-dsl-opal.rb +40 -8
  21. data/lib/glimmer-dsl-opal/ext/date.rb +49 -3
  22. data/lib/glimmer-dsl-opal/ext/struct.rb +37 -0
  23. data/lib/glimmer-dsl-opal/samples/elaborate/tic_tac_toe.rb +23 -0
  24. data/lib/glimmer-dsl-opal/samples/hello/hello_date_time.rb +63 -0
  25. data/lib/glimmer-dsl-opal/samples/hello/hello_table.rb +283 -0
  26. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/GPL-LICENSE.txt +278 -0
  27. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/MIT-LICENSE.txt +20 -0
  28. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/jquery.ui.timepicker.css +57 -0
  29. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/jquery.ui.timepicker.js +1496 -0
  30. data/lib/glimmer-dsl-opal/vendor/jquery-ui/AUTHORS.txt +333 -0
  31. data/lib/glimmer-dsl-opal/vendor/jquery-ui/LICENSE.txt +43 -0
  32. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  33. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  34. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  35. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  36. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  37. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  38. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.min.css +7 -0
  39. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.min.js +13 -0
  40. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.structure.min.css +5 -0
  41. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.theme.min.css +5 -0
  42. data/lib/glimmer-dsl-opal/vendor/jquery-ui/package.json +74 -0
  43. data/lib/glimmer-dsl-swt.rb +20 -35
  44. data/lib/glimmer/data_binding/table_items_binding.rb +32 -19
  45. data/lib/glimmer/dsl/opal/block_property_expression.rb +41 -0
  46. data/lib/glimmer/dsl/opal/custom_widget_expression.rb +1 -1
  47. data/lib/glimmer/dsl/opal/dsl.rb +2 -0
  48. data/lib/glimmer/dsl/opal/shell_expression.rb +7 -2
  49. data/lib/glimmer/dsl/opal/widget_expression.rb +10 -1
  50. data/lib/glimmer/engine.rb +9 -0
  51. data/lib/glimmer/swt.rb +3 -3
  52. data/lib/glimmer/swt/button_proxy.rb +5 -5
  53. data/lib/glimmer/swt/checkbox_proxy.rb +1 -0
  54. data/lib/glimmer/swt/color_proxy.rb +45 -45
  55. data/lib/glimmer/swt/combo_proxy.rb +42 -3
  56. data/lib/glimmer/swt/composite_proxy.rb +7 -3
  57. data/lib/glimmer/swt/control_editor.rb +54 -0
  58. data/lib/glimmer/swt/date_time_proxy.rb +209 -0
  59. data/lib/glimmer/swt/display_proxy.rb +6 -2
  60. data/lib/glimmer/swt/fill_layout_proxy.rb +1 -1
  61. data/lib/glimmer/swt/label_proxy.rb +2 -2
  62. data/lib/glimmer/swt/layout_data_proxy.rb +13 -10
  63. data/lib/glimmer/swt/layout_proxy.rb +5 -5
  64. data/lib/glimmer/swt/list_proxy.rb +2 -2
  65. data/lib/glimmer/swt/make_shift_shell_proxy.rb +4 -4
  66. data/lib/glimmer/swt/message_box_proxy.rb +10 -8
  67. data/lib/glimmer/swt/property_owner.rb +2 -2
  68. data/lib/glimmer/swt/radio_proxy.rb +1 -0
  69. data/lib/glimmer/swt/shell_proxy.rb +8 -0
  70. data/lib/glimmer/swt/tab_folder_proxy.rb +5 -5
  71. data/lib/glimmer/swt/tab_item_proxy.rb +7 -7
  72. data/lib/glimmer/swt/table_column_proxy.rb +71 -12
  73. data/lib/glimmer/swt/table_editor.rb +65 -0
  74. data/lib/glimmer/swt/table_item_proxy.rb +50 -7
  75. data/lib/glimmer/swt/table_proxy.rb +595 -22
  76. data/lib/glimmer/swt/text_proxy.rb +51 -3
  77. data/lib/glimmer/swt/widget_proxy.rb +141 -27
  78. data/lib/glimmer/ui/custom_widget.rb +8 -8
  79. data/lib/net/http.rb +1 -5
  80. data/lib/os.rb +36 -0
  81. data/lib/uri.rb +3 -3
  82. metadata +49 -9
  83. data/lib/glimmer/data_binding/ext/observable_model.rb +0 -40
@@ -1,28 +1,300 @@
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
7
- attr_reader :columns, :selection
8
- attr_accessor :column_properties
29
+ class TableProxy < CompositeProxy
30
+ attr_reader :columns, :selection,
31
+ :sort_type, :sort_column, :sort_property, :sort_block, :sort_by_block, :additional_sort_properties,
32
+ :editor, :table_editor
33
+ attr_accessor :column_properties, :item_count, :data
9
34
  alias items children
35
+ alias model_binding data
10
36
 
11
- def initialize(parent, args)
12
- super(parent, args)
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_focus_lost {
132
+ table_proxy.finish_edit!
133
+ }
134
+ on_key_pressed { |key_event|
135
+ if key_event.keyCode == swt(:cr)
136
+ table_proxy.finish_edit!
137
+ elsif key_event.keyCode == swt(:esc)
138
+ table_proxy.cancel_edit!
139
+ end
140
+ }
141
+ }
142
+ end,
143
+ },
144
+ date_drop_down: {
145
+ widget_value_property: :date_time,
146
+ editor_gui: lambda do |args, model, property, table_proxy|
147
+ first_time = true
148
+ table_proxy.table_editor.minimumWidth = 80
149
+ table_proxy.table_editor.minimumHeight = 15
150
+ date_drop_down(*args) {
151
+ date_time model.send(property)
152
+ focus true
153
+ on_widget_selected {
154
+ table_proxy.finish_edit!
155
+ }
156
+ on_focus_lost {
157
+ table_proxy.finish_edit!
158
+ }
159
+ on_key_pressed { |key_event|
160
+ if key_event.keyCode == swt(:cr)
161
+ table_proxy.finish_edit!
162
+ elsif key_event.keyCode == swt(:esc)
163
+ table_proxy.cancel_edit!
164
+ end
165
+ }
166
+ }
167
+ end,
168
+ },
169
+ time: {
170
+ widget_value_property: :date_time,
171
+ editor_gui: lambda do |args, model, property, table_proxy|
172
+ first_time = true
173
+ table_proxy.table_editor.minimumWidth = 80
174
+ table_proxy.table_editor.minimumHeight = 15
175
+ time(*args) {
176
+ date_time model.send(property)
177
+ focus true
178
+ on_widget_selected {
179
+ table_proxy.finish_edit!
180
+ }
181
+ on_focus_lost {
182
+ table_proxy.finish_edit!
183
+ }
184
+ on_key_pressed { |key_event|
185
+ if key_event.keyCode == swt(:cr)
186
+ table_proxy.finish_edit!
187
+ elsif key_event.keyCode == swt(:esc)
188
+ table_proxy.cancel_edit!
189
+ end
190
+ }
191
+ }
192
+ end,
193
+ },
194
+ radio: {
195
+ widget_value_property: :selection,
196
+ editor_gui: lambda do |args, model, property, table_proxy|
197
+ first_time = true
198
+ table_proxy.table_editor.minimumHeight = 25
199
+ radio(*args) {
200
+ selection model.send(property)
201
+ focus true
202
+ on_widget_selected {
203
+ table_proxy.finish_edit!
204
+ }
205
+ on_focus_lost {
206
+ table_proxy.finish_edit!
207
+ }
208
+ on_key_pressed { |key_event|
209
+ if key_event.keyCode == swt(:cr)
210
+ table_proxy.finish_edit!
211
+ elsif key_event.keyCode == swt(:esc)
212
+ table_proxy.cancel_edit!
213
+ end
214
+ }
215
+ }
216
+ end,
217
+ },
218
+ spinner: {
219
+ widget_value_property: :selection,
220
+ editor_gui: lambda do |args, model, property, table_proxy|
221
+ first_time = true
222
+ table_proxy.table_editor.minimumHeight = 25
223
+ table_editor_widget_proxy = spinner(*args) {
224
+ selection model.send(property)
225
+ focus true
226
+ on_focus_lost {
227
+ table_proxy.finish_edit!
228
+ }
229
+ on_key_pressed { |key_event|
230
+ if key_event.keyCode == swt(:cr)
231
+ table_proxy.finish_edit!
232
+ elsif key_event.keyCode == swt(:esc)
233
+ table_proxy.cancel_edit!
234
+ end
235
+ }
236
+ }
237
+ table_editor_widget_proxy
238
+ end,
239
+ },
240
+ }
241
+ end
242
+ end
243
+
244
+ def initialize(parent, args, block)
245
+ super(parent, args, block)
13
246
  @columns = []
14
247
  @children = []
248
+ @editors = []
15
249
  @selection = []
250
+ @table_editor = TableEditor.new(self)
251
+ @table_editor.horizontalAlignment = SWTProxy[:left]
252
+ @table_editor.grabHorizontal = true
253
+ @table_editor.minimumWidth = 90
254
+ @table_editor.minimumHeight = 20
255
+ if editable?
256
+ on_mouse_up { |event|
257
+ edit_table_item(event.table_item, event.column_index)
258
+ }
259
+ end
16
260
  end
17
261
 
18
262
  # Only table_columns may be added as children
19
- def add_child(child)
263
+ def post_initialize_child(child)
20
264
  if child.is_a?(TableColumnProxy)
21
265
  @columns << child
266
+ child.render
267
+ elsif child.is_a?(TableItemProxy)
268
+ @children << child
269
+ child.render
22
270
  else
23
- @children << child
271
+ @editors << child
24
272
  end
25
- child.redraw
273
+ end
274
+
275
+ # Executes for the parent of a child that just got disposed
276
+ def post_dispose_child(child)
277
+ if child.is_a?(TableColumnProxy)
278
+ @columns&.delete(child)
279
+ elsif child.is_a?(TableItemProxy)
280
+ @children&.delete(child)
281
+ else
282
+ @editors&.delete(child)
283
+ end
284
+ end
285
+
286
+ def post_add_content
287
+ return if @initially_sorted
288
+ initial_sort!
289
+ @initially_sorted = true
290
+ end
291
+
292
+ def default_layout
293
+ nil
294
+ end
295
+
296
+ def get_data(key=nil)
297
+ data
26
298
  end
27
299
 
28
300
  def remove_all
@@ -30,20 +302,40 @@ module Glimmer
30
302
  redraw
31
303
  end
32
304
 
305
+ def editable?
306
+ args.include?(:editable)
307
+ end
308
+ alias editable editable?
309
+
310
+ def selection
311
+ @selection.to_a
312
+ end
313
+
33
314
  def selection=(new_selection)
34
- changed = (@selection + new_selection) - (@selection & new_selection)
315
+ new_selection = new_selection.to_a
316
+ changed = (selection + new_selection) - (selection & new_selection)
35
317
  @selection = new_selection
36
- changed.each(&:redraw)
318
+ changed.each(&:redraw_selection)
37
319
  end
38
320
 
39
- def items=(new_items)
321
+ def items=(new_items)
40
322
  @children = new_items
323
+ # TODO optimize in the future by sorting elements in DOM directly when no change to elements occur other than sort
41
324
  redraw
42
325
  end
43
326
 
327
+ def item_count=(value)
328
+ @item_count = value
329
+ redraw_empty_items
330
+ end
331
+
332
+ def cells_for(model)
333
+ column_properties.map {|property| model.send(property)}
334
+ end
335
+
44
336
  def search(&condition)
45
337
  items.select {|item| condition.nil? || condition.call(item)}
46
- end
338
+ end
47
339
 
48
340
  def index_of(item)
49
341
  items.index(item)
@@ -61,8 +353,252 @@ module Glimmer
61
353
  self.selection = new_selection
62
354
  end
63
355
 
64
- def edit_table_item(table_item, column_index)
65
- table_item.edit(column_index)
356
+ def sort_block=(comparator)
357
+ @sort_block = comparator
358
+ end
359
+
360
+ def sort_by_block=(property_picker)
361
+ @sort_by_block = property_picker
362
+ end
363
+
364
+ def sort_property=(new_sort_property)
365
+ @sort_property = [new_sort_property].flatten.compact
366
+ end
367
+
368
+ def detect_sort_type
369
+ @sort_type = sort_property.size.times.map { String }
370
+ array = model_binding.evaluate_property
371
+ sort_property.each_with_index do |a_sort_property, i|
372
+ values = array.map { |object| object.send(a_sort_property) }
373
+ value_classes = values.map(&:class).uniq
374
+ if value_classes.size == 1
375
+ @sort_type[i] = value_classes.first
376
+ elsif value_classes.include?(Integer)
377
+ @sort_type[i] = Integer
378
+ elsif value_classes.include?(Float)
379
+ @sort_type[i] = Float
380
+ end
381
+ end
382
+ end
383
+
384
+ def column_sort_properties
385
+ column_properties.zip(columns.map(&:sort_property)).map do |pair|
386
+ [pair.compact.last].flatten.compact
387
+ end
388
+ end
389
+
390
+ def sort_direction
391
+ @sort_direction == :ascending ? SWTProxy[:up] : SWTProxy[:down]
392
+ end
393
+
394
+ def sort_direction=(value)
395
+ @sort_direction = value == SWTProxy[:up] ? :ascending : :descending
396
+ end
397
+
398
+ # Sorts by specified TableColumnProxy object. If nil, it uses the table default sort instead.
399
+ def sort_by_column!(table_column_proxy=nil)
400
+ index = columns.to_a.index(table_column_proxy) unless table_column_proxy.nil?
401
+ new_sort_property = table_column_proxy.nil? ? @sort_property : table_column_proxy.sort_property || [column_properties[index]]
402
+
403
+ return if table_column_proxy.nil? && new_sort_property.nil? && @sort_block.nil? && @sort_by_block.nil?
404
+ if new_sort_property && table_column_proxy.nil? && new_sort_property.size == 1 && (index = column_sort_properties.index(new_sort_property))
405
+ table_column_proxy = columns[index]
406
+ end
407
+ if new_sort_property && new_sort_property.size == 1 && !additional_sort_properties.to_a.empty?
408
+ selected_additional_sort_properties = additional_sort_properties.clone
409
+ if selected_additional_sort_properties.include?(new_sort_property.first)
410
+ selected_additional_sort_properties.delete(new_sort_property.first)
411
+ new_sort_property += selected_additional_sort_properties
412
+ else
413
+ new_sort_property += additional_sort_properties
414
+ end
415
+ end
416
+
417
+ new_sort_property = [new_sort_property].flatten.compact unless new_sort_property.is_a?(Array)
418
+ @sort_direction = @sort_direction.nil? || @sort_property.first != new_sort_property.first || @sort_direction == :descending ? :ascending : :descending
419
+
420
+ @sort_property = new_sort_property
421
+ table_column_index = column_properties.index(new_sort_property.to_s.to_sym)
422
+ table_column_proxy ||= columns[table_column_index] if table_column_index
423
+ @sort_column = table_column_proxy if table_column_proxy
424
+
425
+ if table_column_proxy
426
+ @sort_by_block = nil
427
+ @sort_block = nil
428
+ end
429
+ @sort_type = nil
430
+ if table_column_proxy&.sort_by_block
431
+ @sort_by_block = table_column_proxy.sort_by_block
432
+ elsif table_column_proxy&.sort_block
433
+ @sort_block = table_column_proxy.sort_block
434
+ else
435
+ detect_sort_type
436
+ end
437
+
438
+ sort!
439
+ end
440
+
441
+ def initial_sort!
442
+ sort_by_column!
443
+ end
444
+
445
+ def sort!
446
+ return unless sort_property && (sort_type || sort_block || sort_by_block)
447
+ array = model_binding.evaluate_property
448
+ array = array.sort_by(&:hash) # this ensures consistent subsequent sorting in case there are equivalent sorts to avoid an infinite loop
449
+ # Converting value to_s first to handle nil cases. Should work with numeric, boolean, and date fields
450
+ if sort_block
451
+ sorted_array = array.sort(&sort_block)
452
+ elsif sort_by_block
453
+ sorted_array = array.sort_by(&sort_by_block)
454
+ else
455
+ sorted_array = array.sort_by do |object|
456
+ sort_property.each_with_index.map do |a_sort_property, i|
457
+ value = object.send(a_sort_property)
458
+ # handle nil and difficult to compare types gracefully
459
+ if sort_type[i] == Integer
460
+ value = value.to_i
461
+ elsif sort_type[i] == Float
462
+ value = value.to_f
463
+ elsif sort_type[i] == String
464
+ value = value.to_s
465
+ end
466
+ value
467
+ end
468
+ end
469
+ end
470
+ sorted_array = sorted_array.reverse if @sort_direction == :descending
471
+ model_binding.call(sorted_array)
472
+ end
473
+
474
+ def additional_sort_properties=(*args)
475
+ @additional_sort_properties = args unless args.empty?
476
+ end
477
+
478
+ def editor=(args)
479
+ @editor = args
480
+ end
481
+
482
+ # Indicates if table is in edit mode, thus displaying a text widget for a table item cell
483
+ def edit_mode?
484
+ !!@edit_mode
485
+ end
486
+
487
+ def cancel_edit!
488
+ @cancel_edit&.call if @edit_mode
489
+ end
490
+
491
+ def finish_edit!
492
+ @finish_edit&.call if @edit_mode
493
+ end
494
+
495
+ # 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.
496
+ # It is set to false once change is saved to model
497
+ def edit_in_progress?
498
+ !!@edit_in_progress
499
+ end
500
+
501
+ def edit_selected_table_item(column_index, before_write: nil, after_write: nil, after_cancel: nil)
502
+ edit_table_item(selection.first, column_index, before_write: before_write, after_write: after_write, after_cancel: after_cancel)
503
+ end
504
+
505
+ # TODO migrate the following to the next method
506
+ # def edit_table_item(table_item, column_index)
507
+ # table_item&.edit(column_index) unless column_index.nil?
508
+ # end
509
+
510
+ def edit_table_item(table_item, column_index, before_write: nil, after_write: nil, after_cancel: nil)
511
+ return if table_item.nil? || (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
512
+ @edit_column_index = column_index
513
+ @edit_table_item = table_item
514
+ column_index = column_index.to_i
515
+ model = table_item.data
516
+ property = column_properties[column_index]
517
+ cancel_edit!
518
+ return unless columns[column_index].editable?
519
+ action_taken = false
520
+ @edit_mode = true
521
+
522
+ editor_config = columns[column_index].editor || editor
523
+ editor_config = [editor_config].flatten.compact
524
+ editor_widget_options = editor_config.last.is_a?(Hash) ? editor_config.last : {}
525
+ editor_widget_arg_last_index = editor_config.last.is_a?(Hash) ? -2 : -1
526
+ editor_widget = (editor_config[0] || :text).to_sym
527
+ editor_widget_args = editor_config[1..editor_widget_arg_last_index]
528
+ model_editing_property = editor_widget_options[:property] || property
529
+ widget_value_property = TableProxy::editors.symbolize_keys[editor_widget][:widget_value_property]
530
+
531
+ @cancel_edit = lambda do |event=nil|
532
+ @cancel_in_progress = true
533
+ @table_editor.cancel!
534
+ @table_editor_widget_proxy&.dispose
535
+ @table_editor_widget_proxy = nil
536
+ after_cancel&.call
537
+ @edit_in_progress = false
538
+ @cancel_in_progress = false
539
+ @cancel_edit = nil
540
+ @edit_table_item = @edit_column_index = nil if (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
541
+ @edit_mode = false
542
+ end
543
+
544
+ @finish_edit = lambda do |event=nil|
545
+ new_value = @table_editor_widget_proxy&.send(widget_value_property)
546
+ if table_item.disposed?
547
+ @cancel_edit.call
548
+ elsif !new_value.nil? && !action_taken && !@edit_in_progress && !@cancel_in_progress
549
+ action_taken = true
550
+ @edit_in_progress = true
551
+ if new_value == model.send(model_editing_property)
552
+ @cancel_edit.call
553
+ else
554
+ before_write&.call
555
+ @table_editor.save!(widget_value_property: widget_value_property)
556
+ model.send("#{model_editing_property}=", new_value) # makes table update itself, so must search for selected table item again
557
+ # Table refresh happens here because of model update triggering observers, so must retrieve table item again
558
+ edited_table_item = search { |ti| ti.data == model }.first
559
+ show_item(edited_table_item)
560
+ @table_editor_widget_proxy&.dispose
561
+ @table_editor_widget_proxy = nil
562
+ after_write&.call(edited_table_item)
563
+ @edit_in_progress = false
564
+ @edit_table_item = @edit_column_index = nil
565
+ end
566
+ end
567
+ end
568
+
569
+ content {
570
+ @table_editor_widget_proxy = TableProxy::editors.symbolize_keys[editor_widget][:editor_gui].call(editor_widget_args, model, model_editing_property, self)
571
+ }
572
+ @table_editor.set_editor(@table_editor_widget_proxy, table_item, column_index)
573
+ rescue => e
574
+ Glimmer::Config.logger.error {e.full_message}
575
+ raise e
576
+ end
577
+
578
+ def show_item(table_item)
579
+ table_item.dom_element.focus
580
+ end
581
+
582
+ def add_listener(underscored_listener_name, &block)
583
+ enhanced_block = lambda do |event|
584
+ event.extend(TableListenerEvent)
585
+ block.call(event)
586
+ end
587
+ super(underscored_listener_name, &enhanced_block)
588
+ end
589
+
590
+
591
+ def header_visible=(value)
592
+ @header_visible = value
593
+ if @header_visible
594
+ thead_dom_element.remove_class('hide')
595
+ else
596
+ thead_dom_element.add_class('hide')
597
+ end
598
+ end
599
+
600
+ def header_visible
601
+ @header_visible
66
602
  end
67
603
 
68
604
  def selector
@@ -84,9 +620,10 @@ module Glimmer
84
620
  event.singleton_class.send(:define_method, :column_index) do
85
621
  (table_data || event.target).attr('data-column-index')
86
622
  end
87
- event_listener.call(event)
623
+
624
+ event_listener.call(event) unless event.table_item.nil? && event.column_index.nil?
88
625
  }
89
- }
626
+ }
90
627
 
91
628
  {
92
629
  'on_mouse_down' => {
@@ -96,13 +633,27 @@ module Glimmer
96
633
  'on_mouse_up' => {
97
634
  event: 'mouseup',
98
635
  event_handler: mouse_handler,
99
- }
636
+ },
637
+ 'on_widget_selected' => {
638
+ event: 'mouseup',
639
+ event_handler: mouse_handler,
640
+ },
100
641
  }
101
642
  end
102
643
 
103
644
  def redraw
104
645
  super()
105
- @columns.to_a.each(&:redraw)
646
+ @columns.to_a.each(&:redraw)
647
+ redraw_empty_items
648
+ end
649
+
650
+ def redraw_empty_items
651
+ if @children&.size.to_i < item_count.to_i
652
+ item_count.to_i.times do
653
+ empty_columns = column_properties&.size.to_i.times.map { |i| "<td data-column-index='#{i}'></td>" }
654
+ items_dom_element.append("<tr class='table-item empty-table-item'>#{empty_columns}</tr>")
655
+ end
656
+ end
106
657
  end
107
658
 
108
659
  def element
@@ -125,7 +676,7 @@ module Glimmer
125
676
  Document.find(items_path)
126
677
  end
127
678
 
128
- def columns_dom
679
+ def columns_dom
129
680
  tr {
130
681
  }
131
682
  end
@@ -134,9 +685,13 @@ module Glimmer
134
685
  thead {
135
686
  columns_dom
136
687
  }
137
- end
688
+ end
138
689
 
139
- def items_dom
690
+ def thead_dom_element
691
+ dom_element.find('thead')
692
+ end
693
+
694
+ def items_dom
140
695
  tbody {
141
696
  }
142
697
  end
@@ -145,7 +700,8 @@ module Glimmer
145
700
  table_id = id
146
701
  table_id_style = css
147
702
  table_id_css_classes = css_classes
148
- table_id_css_classes << 'table'
703
+ table_id_css_classes << 'table' unless table_id_css_classes.include?('table')
704
+ table_id_css_classes << 'editable' if editable? && !table_id_css_classes.include?('editable')
149
705
  table_id_css_classes_string = table_id_css_classes.to_a.join(' ')
150
706
  @dom ||= html {
151
707
  table(id: table_id, style: table_id_style, class: table_id_css_classes_string) {
@@ -154,6 +710,23 @@ module Glimmer
154
710
  }
155
711
  }.to_s
156
712
  end
713
+
714
+ private
715
+
716
+ def property_type_converters
717
+ super.merge({
718
+ selection: lambda do |value|
719
+ if value.is_a?(Array)
720
+ search {|ti| value.include?(ti.get_data) }
721
+ else
722
+ search {|ti| ti.get_data == value}
723
+ end
724
+ end,
725
+ })
726
+ end
727
+
157
728
  end
729
+
158
730
  end
731
+
159
732
  end