glimmer-dsl-web 0.0.1

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.
@@ -0,0 +1,1058 @@
1
+ # Copyright (c) 2020-2022 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/web/event_listener_proxy'
23
+ require 'glimmer/web/property_owner'
24
+
25
+ # TODO implement menu (which delays building it till render using add_content_on_render)
26
+
27
+ module Glimmer
28
+ module Web
29
+ class ElementProxy
30
+ class << self
31
+ # Factory Method that translates a Glimmer DSL keyword into a ElementProxy object
32
+ def for(keyword, parent, args, block)
33
+ element_type(keyword).new(keyword, parent, args, block)
34
+ end
35
+
36
+ # returns Ruby proxy class (type) that would handle this keyword
37
+ def element_type(keyword)
38
+ class_name_main = "#{keyword.camelcase(:upper)}Proxy"
39
+ Glimmer::Web::ElementProxy.const_get(class_name_main.to_sym)
40
+ rescue NameError => e
41
+ Glimmer::Web::ElementProxy
42
+ end
43
+
44
+ def next_id_number_for(name)
45
+ @max_id_numbers[name] = max_id_number_for(name) + 1
46
+ end
47
+
48
+ def max_id_number_for(name)
49
+ @max_id_numbers[name] = max_id_numbers[name] || 0
50
+ end
51
+
52
+ def max_id_numbers
53
+ @max_id_numbers ||= reset_max_id_numbers!
54
+ end
55
+
56
+ def reset_max_id_numbers!
57
+ @max_id_numbers = {}
58
+ end
59
+
60
+ def underscored_widget_name(widget_proxy)
61
+ widget_proxy.class.name.split(/::|\./).last.sub(/Proxy$/, '').underscore
62
+ end
63
+
64
+ def widget_handling_listener
65
+ @@widget_handling_listener
66
+ end
67
+ end
68
+
69
+ include Glimmer
70
+ include PropertyOwner
71
+
72
+ Event = Struct.new(:widget, keyword_init: true)
73
+
74
+ attr_reader :keyword, :parent, :args, :options, :path, :children, :enabled, :foreground, :background, :focus, :removed?, :rendered
75
+ alias rendered? rendered
76
+
77
+ def initialize(keyword, parent, args, block)
78
+ @keyword = keyword
79
+ @parent = parent
80
+ @options = args.last.is_a?(Hash) ? args.last.symbolize_keys : {}
81
+ @args = args
82
+ @block = block
83
+ @children = []
84
+ @parent&.post_initialize_child(self)
85
+ end
86
+
87
+ # Executes for the parent of a child that just got added
88
+ def post_initialize_child(child)
89
+ @children << child
90
+ child.render
91
+ end
92
+
93
+ # Executes for the parent of a child that just got removed
94
+ def post_remove_child(child)
95
+ @children&.delete(child)
96
+ end
97
+
98
+ # Executes at the closing of a parent widget curly braces after all children/properties have been added/set
99
+ def post_add_content
100
+ # No Op
101
+ end
102
+
103
+ def css_classes
104
+ dom_element.attr('class').to_s.split
105
+ end
106
+
107
+ def remove
108
+ remove_all_listeners
109
+ Document.find(path).remove
110
+ parent&.post_remove_child(self)
111
+ # children.each(:remove) # TODO enable this safely
112
+ @removed = true
113
+ listeners_for('widget_removed').each {|listener| listener.call(Event.new(widget: self))}
114
+ end
115
+
116
+ def remove_all_listeners
117
+ effective_observation_request_to_event_mapping.keys.each do |keyword|
118
+ effective_observation_request_to_event_mapping[keyword].to_collection.each do |mapping|
119
+ observation_requests[keyword].to_a.each do |event_listener|
120
+ event = mapping[:event]
121
+ event_handler = mapping[:event_handler]
122
+ event_element_css_selector = mapping[:event_element_css_selector]
123
+ the_listener_dom_element = event_element_css_selector ? Element[event_element_css_selector] : listener_dom_element
124
+ the_listener_dom_element.off(event, event_listener)
125
+ # TODO improve to precisely remove the listeners that were added, no more no less. (or use the event_listener_proxies method instead or in collaboration)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def path
132
+ "#{parent_path} #{element}##{id}.#{name}"
133
+ end
134
+ alias widget_path path # pure path without subchildren modifications
135
+
136
+ # Root element representing widget. Must be overridden by subclasses if different from div
137
+ def element
138
+ keyword
139
+ end
140
+
141
+ def shell
142
+ current_widget = self
143
+ current_widget = current_widget.parent until current_widget.parent.nil?
144
+ current_widget
145
+ end
146
+
147
+ def parents
148
+ parents_array = []
149
+ current_widget = self
150
+ until current_widget.parent.nil?
151
+ current_widget = current_widget.parent
152
+ parents_array << current_widget
153
+ end
154
+ parents_array
155
+ end
156
+
157
+ def dialog_ancestor
158
+ parents.detect {|p| p.is_a?(DialogProxy)}
159
+ end
160
+
161
+ def print
162
+ `window.print()`
163
+ true
164
+ end
165
+
166
+ def enabled=(value)
167
+ @enabled = value
168
+ dom_element.prop('disabled', !@enabled)
169
+ end
170
+
171
+ def foreground=(value)
172
+ value = ColorProxy.new(value) if value.is_a?(String)
173
+ @foreground = value
174
+ dom_element.css('color', foreground.to_css) unless foreground.nil?
175
+ end
176
+
177
+ def background=(value)
178
+ value = ColorProxy.new(value) if value.is_a?(String)
179
+ @background = value
180
+ dom_element.css('background-color', background.to_css) unless background.nil?
181
+ end
182
+
183
+ def focus=(value)
184
+ @focus = value
185
+ dom_element.focus # TODO consider if a delay or async_exec is needed here
186
+ end
187
+
188
+ def set_focus
189
+ self.focus = true
190
+ end
191
+ alias setFocus set_focus
192
+
193
+ def parent_path
194
+ @parent&.path
195
+ end
196
+
197
+ def parent_dom_element
198
+ if parent_path
199
+ Document.find(parent_path)
200
+ elsif options[:root]
201
+ Document.find(options[:root])
202
+ end
203
+ end
204
+
205
+ def render(custom_parent_dom_element: nil, brand_new: false)
206
+ the_parent_dom_element = custom_parent_dom_element || parent_dom_element
207
+ old_element = dom_element
208
+ brand_new = @dom.nil? || old_element.empty? || brand_new
209
+ build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
210
+ if brand_new
211
+ # TODO make a method attach to allow subclasses to override if needed
212
+ attach(the_parent_dom_element)
213
+ else
214
+ reattach(old_element)
215
+ end
216
+ observation_requests&.each do |keyword, event_listener_set|
217
+ event_listener_set.each do |event_listener|
218
+ handle_observation_request(keyword, event_listener)
219
+ end
220
+ end
221
+ children.each do |child|
222
+ child.render
223
+ end
224
+ @rendered = true
225
+ unless skip_content_on_render_blocks?
226
+ content_on_render_blocks.each do |content_block|
227
+ content(&content_block)
228
+ end
229
+ end
230
+ end
231
+ alias redraw render
232
+
233
+ def attach(the_parent_dom_element)
234
+ the_parent_dom_element.append(@dom)
235
+ end
236
+
237
+ def reattach(old_element)
238
+ old_element.replace_with(@dom)
239
+ end
240
+
241
+ def add_text_content(text)
242
+ dom_element.append(text)
243
+ end
244
+
245
+ def content_on_render_blocks
246
+ @content_on_render_blocks ||= []
247
+ end
248
+
249
+ def skip_content_on_render_blocks?
250
+ false
251
+ end
252
+
253
+ def add_content_on_render(&content_block)
254
+ if rendered?
255
+ content_block.call
256
+ else
257
+ content_on_render_blocks << content_block
258
+ end
259
+ end
260
+
261
+ def build_dom(layout: true)
262
+ # TODO consider passing parent element instead and having table item include a table cell widget only for opal
263
+ @dom = nil
264
+ @dom = dom # TODO unify how to build dom for most widgets based on element, id, and name (class)
265
+ @dom = @parent.get_layout.dom(@dom) if @parent.respond_to?(:layout) && @parent.get_layout
266
+ @dom
267
+ end
268
+
269
+ def dom
270
+ body_id = id
271
+ body_class = ([name] + css_classes.to_a).join(' ')
272
+ html_options = options.dup
273
+ html_options[:id] ||= body_id
274
+ html_options[:class] ||= ''
275
+ html_options[:class] = "#{html_options[:class]} #{body_class}".strip
276
+ @dom ||= html {
277
+ send(keyword, html_options) {
278
+ # TODO consider supporting the idea of dynamic CSS building on close of shell that adds only as much CSS as needed for widgets that were mentioned
279
+ # style(class: 'common-style') {
280
+ # style_dom_css
281
+ # }
282
+ # [LayoutProxy, WidgetProxy].map(&:descendants).reduce(:+).each do |style_class|
283
+ # if style_class.constants.include?('STYLE')
284
+ # style(class: "#{style_class.name.split(':').last.underscore.gsub('_', '-').sub(/-proxy$/, '')}-style") {
285
+ # style_class::STYLE
286
+ # }
287
+ # end
288
+ # end
289
+ }
290
+ }.to_s
291
+ end
292
+
293
+ def content(&block)
294
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ElementExpression.new, keyword, &block)
295
+ end
296
+
297
+ # Subclasses must override with their own mappings
298
+ def observation_request_to_event_mapping
299
+ {}
300
+ end
301
+
302
+ def effective_observation_request_to_event_mapping
303
+ default_observation_request_to_event_mapping.merge(observation_request_to_event_mapping)
304
+ end
305
+
306
+ def default_observation_request_to_event_mapping
307
+ myself = self
308
+ mouse_event_handler = -> (event_listener) {
309
+ -> (event) {
310
+ # TODO generalize this solution to all widgets that support key presses
311
+ event.define_singleton_method(:widget) {myself}
312
+ event.define_singleton_method(:button, &event.method(:which))
313
+ event.define_singleton_method(:count) {1} # TODO support double-click count of 2 in the future by using ondblclick
314
+ event.define_singleton_method(:x, &event.method(:page_x))
315
+ event.define_singleton_method(:y, &event.method(:page_y))
316
+ doit = true
317
+ event.define_singleton_method(:doit=) do |value|
318
+ doit = value
319
+ end
320
+ event.define_singleton_method(:doit) { doit }
321
+
322
+ if event.which == 1
323
+ # event.prevent # TODO consider if this is needed
324
+ event_listener.call(event)
325
+ end
326
+
327
+ # TODO Imlement doit properly for all different kinds of events
328
+ # unless doit
329
+ # event.prevent
330
+ # event.stop
331
+ # event.stop_immediate
332
+ # end
333
+ }
334
+ }
335
+ mouse_move_event_handler = -> (event_listener) {
336
+ -> (event) {
337
+ # TODO generalize this solution to all widgets that support key presses
338
+ event.define_singleton_method(:widget) {myself}
339
+ event.define_singleton_method(:button, &event.method(:which))
340
+ event.define_singleton_method(:count) {1} # TODO support double-click count of 2 in the future by using ondblclick
341
+ event.define_singleton_method(:x, &event.method(:page_x))
342
+ event.define_singleton_method(:y, &event.method(:page_y))
343
+ doit = true
344
+ event.define_singleton_method(:doit=) do |value|
345
+ doit = value
346
+ end
347
+ event.define_singleton_method(:doit) { doit }
348
+
349
+ event_listener.call(event)
350
+
351
+ # TODO Imlement doit properly for all different kinds of events
352
+ # unless doit
353
+ # event.prevent
354
+ # event.stop
355
+ # event.stop_immediate
356
+ # end
357
+ }
358
+ }
359
+ context_menu_handler = -> (event_listener) {
360
+ -> (event) {
361
+ # TODO generalize this solution to all widgets that support key presses
362
+ event.define_singleton_method(:widget) {myself}
363
+ event.define_singleton_method(:button, &event.method(:which))
364
+ event.define_singleton_method(:count) {1} # TODO support double-click count of 2 in the future by using ondblclick
365
+ event.define_singleton_method(:x, &event.method(:page_x))
366
+ event.define_singleton_method(:y, &event.method(:page_y))
367
+ doit = true
368
+ event.define_singleton_method(:doit=) do |value|
369
+ doit = value
370
+ end
371
+ event.define_singleton_method(:doit) { doit }
372
+
373
+ if event.which == 3
374
+ event.prevent
375
+ event_listener.call(event)
376
+ end
377
+ # TODO Imlement doit properly for all different kinds of events
378
+ # unless doit
379
+ # event.prevent
380
+ # event.stop
381
+ # event.stop_immediate
382
+ # end
383
+ }
384
+ }
385
+ {
386
+ 'on_focus_gained' => {
387
+ event: 'focus',
388
+ },
389
+ 'on_focus_lost' => {
390
+ event: 'blur',
391
+ },
392
+ 'on_mouse_move' => [
393
+ {
394
+ event: 'mousemove',
395
+ event_handler: mouse_move_event_handler,
396
+ },
397
+ ],
398
+ 'on_mouse_up' => [
399
+ {
400
+ event: 'mouseup',
401
+ event_handler: mouse_event_handler,
402
+ },
403
+ {
404
+ event: 'contextmenu',
405
+ event_handler: context_menu_handler,
406
+ },
407
+ ],
408
+ 'on_mouse_down' => [
409
+ {
410
+ event: 'mousedown',
411
+ event_handler: mouse_event_handler,
412
+ },
413
+ {
414
+ event: 'contextmenu',
415
+ event_handler: context_menu_handler,
416
+ },
417
+ ],
418
+ 'on_swt_mouseup' => [
419
+ {
420
+ event: 'mouseup',
421
+ event_handler: mouse_event_handler,
422
+ },
423
+ {
424
+ event: 'contextmenu',
425
+ event_handler: context_menu_handler,
426
+ },
427
+ ],
428
+ 'on_swt_mousedown' => [
429
+ {
430
+ event: 'mousedown',
431
+ event_handler: mouse_event_handler,
432
+ },
433
+ {
434
+ event: 'contextmenu',
435
+ event_handler: context_menu_handler,
436
+ },
437
+ ],
438
+ 'on_key_pressed' => {
439
+ event: 'keypress',
440
+ event_handler: -> (event_listener) {
441
+ -> (event) {
442
+ event.define_singleton_method(:widget) {myself}
443
+ event.define_singleton_method(:keyLocation) do
444
+ location = `#{event.to_n}.originalEvent.location`
445
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
446
+ end
447
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
448
+ event.define_singleton_method(:keyCode) {
449
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
450
+ }
451
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
452
+ event.define_singleton_method(:character) {event.which.chr}
453
+ event.define_singleton_method(:stateMask) do
454
+ state_mask = 0
455
+ state_mask |= SWTProxy[:alt] if event.alt_key
456
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
457
+ state_mask |= SWTProxy[:shift] if event.shift_key
458
+ state_mask |= SWTProxy[:command] if event.meta_key
459
+ state_mask
460
+ end
461
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
462
+ doit = true
463
+ event.define_singleton_method(:doit=) do |value|
464
+ doit = value
465
+ end
466
+ event.define_singleton_method(:doit) { doit }
467
+ event_listener.call(event)
468
+
469
+ # TODO Fix doit false, it's not stopping input
470
+ unless doit
471
+ event.prevent
472
+ event.prevent_default
473
+ event.stop_propagation
474
+ event.stop_immediate_propagation
475
+ end
476
+
477
+ doit
478
+ }
479
+ } },
480
+ 'on_key_released' => {
481
+ event: 'keyup',
482
+ event_handler: -> (event_listener) {
483
+ -> (event) {
484
+ event.define_singleton_method(:keyLocation) do
485
+ location = `#{event.to_n}.originalEvent.location`
486
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
487
+ end
488
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
489
+ event.define_singleton_method(:widget) {myself}
490
+ event.define_singleton_method(:keyCode) {
491
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
492
+ }
493
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
494
+ event.define_singleton_method(:character) {event.which.chr}
495
+ event.define_singleton_method(:stateMask) do
496
+ state_mask = 0
497
+ state_mask |= SWTProxy[:alt] if event.alt_key
498
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
499
+ state_mask |= SWTProxy[:shift] if event.shift_key
500
+ state_mask |= SWTProxy[:command] if event.meta_key
501
+ state_mask
502
+ end
503
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
504
+ doit = true
505
+ event.define_singleton_method(:doit=) do |value|
506
+ doit = value
507
+ end
508
+ event.define_singleton_method(:doit) { doit }
509
+ event_listener.call(event)
510
+
511
+ # TODO Fix doit false, it's not stopping input
512
+ unless doit
513
+ event.prevent
514
+ event.prevent_default
515
+ event.stop_propagation
516
+ event.stop_immediate_propagation
517
+ end
518
+
519
+ doit
520
+ }
521
+ }
522
+ },
523
+ 'on_swt_keydown' => [
524
+ {
525
+ event: 'keypress',
526
+ event_handler: -> (event_listener) {
527
+ -> (event) {
528
+ event.define_singleton_method(:keyLocation) do
529
+ location = `#{event.to_n}.originalEvent.location`
530
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
531
+ end
532
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
533
+ event.define_singleton_method(:keyCode) {
534
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
535
+ }
536
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
537
+ event.define_singleton_method(:widget) {myself}
538
+ event.define_singleton_method(:character) {event.which.chr}
539
+ event.define_singleton_method(:stateMask) do
540
+ state_mask = 0
541
+ state_mask |= SWTProxy[:alt] if event.alt_key
542
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
543
+ state_mask |= SWTProxy[:shift] if event.shift_key
544
+ state_mask |= SWTProxy[:command] if event.meta_key
545
+ state_mask
546
+ end
547
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
548
+ doit = true
549
+ event.define_singleton_method(:doit=) do |value|
550
+ doit = value
551
+ end
552
+ event.define_singleton_method(:doit) { doit }
553
+ event_listener.call(event)
554
+
555
+ # TODO Fix doit false, it's not stopping input
556
+ unless doit
557
+ event.prevent
558
+ event.prevent_default
559
+ event.stop_propagation
560
+ event.stop_immediate_propagation
561
+ end
562
+
563
+ doit
564
+ }
565
+ }
566
+ },
567
+ {
568
+ event: 'keydown',
569
+ event_handler: -> (event_listener) {
570
+ -> (event) {
571
+ event.define_singleton_method(:keyLocation) do
572
+ location = `#{event.to_n}.originalEvent.location`
573
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
574
+ end
575
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
576
+ event.define_singleton_method(:keyCode) {
577
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
578
+ }
579
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
580
+ event.define_singleton_method(:widget) {myself}
581
+ event.define_singleton_method(:character) {event.which.chr}
582
+ event.define_singleton_method(:stateMask) do
583
+ state_mask = 0
584
+ state_mask |= SWTProxy[:alt] if event.alt_key
585
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
586
+ state_mask |= SWTProxy[:shift] if event.shift_key
587
+ state_mask |= SWTProxy[:command] if event.meta_key
588
+ state_mask
589
+ end
590
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
591
+ doit = true
592
+ event.define_singleton_method(:doit=) do |value|
593
+ doit = value
594
+ end
595
+ event.define_singleton_method(:doit) { doit }
596
+ event_listener.call(event) if event.which != 13 && (event.which == 127 || event.which <= 40)
597
+
598
+ # TODO Fix doit false, it's not stopping input
599
+ unless doit
600
+ event.prevent
601
+ event.prevent_default
602
+ event.stop_propagation
603
+ event.stop_immediate_propagation
604
+ end
605
+ doit
606
+ }
607
+ }
608
+ }
609
+ ],
610
+ 'on_swt_keyup' => {
611
+ event: 'keyup',
612
+ event_handler: -> (event_listener) {
613
+ -> (event) {
614
+ event.define_singleton_method(:keyLocation) do
615
+ location = `#{event.to_n}.originalEvent.location`
616
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
617
+ end
618
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
619
+ event.define_singleton_method(:widget) {myself}
620
+ event.define_singleton_method(:keyCode) {
621
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
622
+ }
623
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
624
+ event.define_singleton_method(:character) {event.which.chr}
625
+ event.define_singleton_method(:stateMask) do
626
+ state_mask = 0
627
+ state_mask |= SWTProxy[:alt] if event.alt_key
628
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
629
+ state_mask |= SWTProxy[:shift] if event.shift_key
630
+ state_mask |= SWTProxy[:command] if event.meta_key
631
+ state_mask
632
+ end
633
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
634
+ doit = true
635
+ event.define_singleton_method(:doit=) do |value|
636
+ doit = value
637
+ end
638
+ event.define_singleton_method(:doit) { doit }
639
+ event_listener.call(event)
640
+
641
+ # TODO Fix doit false, it's not stopping input
642
+ unless doit
643
+ event.prevent
644
+ event.prevent_default
645
+ event.stop_propagation
646
+ event.stop_immediate_propagation
647
+ end
648
+
649
+ doit
650
+ }
651
+ }
652
+ },
653
+ }
654
+ end
655
+
656
+ def name
657
+ self.class.name.split('::').last.underscore.sub(/_proxy$/, '').gsub('_', '-')
658
+ end
659
+
660
+ def id
661
+ return options[:id] if options.include?(:id)
662
+ @id ||= "#{name}-#{ElementProxy.next_id_number_for(name)}"
663
+ end
664
+
665
+ # Sets id explicitly. Useful in cases of wanting to maintain a stable id
666
+ def id=(value)
667
+ # TODO delete this method if it is not needed and isn't accurate in what it does
668
+ @id = value
669
+ end
670
+
671
+ # Subclasses can override with their own selector
672
+ def selector
673
+ "#{name}##{id}"
674
+ end
675
+
676
+ def add_css_class(css_class)
677
+ dom_element.add_class(css_class)
678
+ end
679
+
680
+ def add_css_classes(css_classes_to_add)
681
+ css_classes_to_add.each {|css_class| add_css_class(css_class)}
682
+ end
683
+
684
+ def remove_css_class(css_class)
685
+ dom_element.remove_class(css_class)
686
+ end
687
+
688
+ def remove_css_classes(css_classes_to_remove)
689
+ css_classes_to_remove.each {|css_class| remove_css_class(css_class)}
690
+ end
691
+
692
+ def clear_css_classes
693
+ css_classes.each {|css_class| remove_css_class(css_class)}
694
+ end
695
+
696
+ def has_style?(symbol)
697
+ @args.include?(symbol) # not a very solid implementation. Bring SWT constants eventually
698
+ end
699
+
700
+ def dom_element
701
+ # TODO consider making this pick an element in relation to its parent, allowing unhooked dom elements to be built if needed (unhooked to the visible page dom)
702
+ Document.find(path)
703
+ end
704
+
705
+ # TODO consider adding a default #dom method implementation for the common case, automatically relying on #element and other methods to build the dom html
706
+
707
+ def style_element
708
+ style_element_id = "#{id}-style"
709
+ style_element_selector = "style##{style_element_id}"
710
+ element = dom_element.find(style_element_selector)
711
+ if element.empty?
712
+ new_element = Element.new(:style)
713
+ new_element.attr('id', style_element_id)
714
+ new_element.attr('class', "#{name.gsub('_', '-')}-instance-style widget-instance-style")
715
+ dom_element.prepend(new_element)
716
+ element = dom_element.find(style_element_selector)
717
+ end
718
+ element
719
+ end
720
+
721
+ def listener_path
722
+ path
723
+ end
724
+
725
+ def listener_dom_element
726
+ Document.find(listener_path)
727
+ end
728
+
729
+ def observation_requests
730
+ @observation_requests ||= {}
731
+ end
732
+
733
+ def event_listener_proxies
734
+ @event_listener_proxies ||= []
735
+ end
736
+
737
+ def suspend_event_handling
738
+ @event_handling_suspended = true
739
+ end
740
+
741
+ def resume_event_handling
742
+ @event_handling_suspended = false
743
+ end
744
+
745
+ def event_handling_suspended?
746
+ @event_handling_suspended
747
+ end
748
+
749
+ def listeners
750
+ @listeners ||= {}
751
+ end
752
+
753
+ def listeners_for(listener_event)
754
+ listeners[listener_event.to_s] ||= []
755
+ end
756
+
757
+ def can_handle_observation_request?(observation_request)
758
+ # TODO sort this out for Opal
759
+ observation_request = observation_request.to_s
760
+ if observation_request.start_with?('on_swt_')
761
+ constant_name = observation_request.sub(/^on_swt_/, '')
762
+ SWTProxy.has_constant?(constant_name)
763
+ elsif observation_request.start_with?('on_')
764
+ # event = observation_request.sub(/^on_/, '')
765
+ # can_add_listener?(event) || can_handle_drag_observation_request?(observation_request) || can_handle_drop_observation_request?(observation_request)
766
+ true # TODO filter by valid listeners only in the future
767
+ end
768
+ end
769
+
770
+ def handle_observation_request(keyword, original_event_listener)
771
+ case keyword
772
+ when 'on_widget_removed'
773
+ listeners_for(keyword.sub(/^on_/, '')) << original_event_listener.to_proc
774
+ else
775
+ handle_javascript_observation_request(keyword, original_event_listener)
776
+ end
777
+ end
778
+
779
+ def handle_javascript_observation_request(keyword, original_event_listener)
780
+ return unless effective_observation_request_to_event_mapping.keys.include?(keyword)
781
+ event = nil
782
+ delegate = nil
783
+ effective_observation_request_to_event_mapping[keyword].to_collection.each do |mapping|
784
+ observation_requests[keyword] ||= Set.new
785
+ observation_requests[keyword] << original_event_listener
786
+ event = mapping[:event]
787
+ event_handler = mapping[:event_handler]
788
+ event_element_css_selector = mapping[:event_element_css_selector]
789
+ potential_event_listener = event_handler&.call(original_event_listener)
790
+ event_listener = potential_event_listener || original_event_listener
791
+ async_event_listener = proc do |event|
792
+ # TODO look into the issue with using async::task.new here. maybe put it in event listener (like not being able to call preventDefault or return false successfully )
793
+ # maybe consider pushing inside the widget classes instead where needed only or implement universal doit support correctly to bypass this issue
794
+ # Async::Task.new do
795
+ @@widget_handling_listener = self
796
+ # TODO also make sure to disable all widgets for suspension
797
+ event_listener.call(event) unless dialog_ancestor&.event_handling_suspended?
798
+ @widget_handling_listener = nil
799
+ # end
800
+ end
801
+ the_listener_dom_element = event_element_css_selector ? Element[event_element_css_selector] : listener_dom_element
802
+ unless the_listener_dom_element.empty?
803
+ the_listener_dom_element.on(event, &async_event_listener)
804
+ # TODO ensure uniqueness of insertion (perhaps adding equals/hash method to event listener proxy)
805
+
806
+ event_listener_proxies << EventListenerProxy.new(element_proxy: self, selector: selector, dom_element: the_listener_dom_element, event: event, listener: async_event_listener, original_event_listener: original_event_listener)
807
+ end
808
+ end
809
+ end
810
+
811
+ def remove_event_listener_proxies
812
+ event_listener_proxies.each do |event_listener_proxy|
813
+ event_listener_proxy.unregister
814
+ end
815
+ event_listener_proxies.clear
816
+ end
817
+
818
+ def add_observer(observer, property_name)
819
+ property_listener_installers = self.class&.ancestors&.to_a.map {|ancestor| widget_property_listener_installers[ancestor]}.compact
820
+ widget_listener_installers = property_listener_installers.map{|installer| installer[property_name.to_s.to_sym]}.compact if !property_listener_installers.empty?
821
+ widget_listener_installers.to_a.each do |widget_listener_installer|
822
+ widget_listener_installer.call(observer)
823
+ end
824
+ end
825
+
826
+ def set_attribute(attribute_name, *args)
827
+ apply_property_type_converters(attribute_name, args)
828
+ super(attribute_name, *args) # PropertyOwner
829
+ end
830
+
831
+ def method_missing(method, *args, &block)
832
+ if method.to_s.start_with?('on_')
833
+ handle_observation_request(method, block)
834
+ else
835
+ super(method, *args, &block)
836
+ end
837
+ end
838
+
839
+ def swt_widget
840
+ # only added for compatibility/adaptibility with Glimmer DSL for SWT
841
+ self
842
+ end
843
+
844
+ def apply_property_type_converters(attribute_name, args)
845
+ if args.count == 1
846
+ value = args.first
847
+ converter = property_type_converters[attribute_name.to_sym]
848
+ args[0] = converter.call(value) if converter
849
+ end
850
+ # if args.count == 1 && args.first.is_a?(ColorProxy)
851
+ # g_color = args.first
852
+ # args[0] = g_color.swt_color
853
+ # end
854
+ end
855
+
856
+ def property_type_converters
857
+ color_converter = proc do |value|
858
+ if value.is_a?(Symbol) || value.is_a?(String)
859
+ ColorProxy.new(value)
860
+ else
861
+ value
862
+ end
863
+ end
864
+ @property_type_converters ||= {
865
+ :background => color_converter,
866
+ # :background_image => proc do |value|
867
+ # if value.is_a?(String)
868
+ # if value.start_with?('uri:classloader')
869
+ # value = value.sub(/^uri\:classloader\:\//, '')
870
+ # object = java.lang.Object.new
871
+ # value = object.java_class.resource_as_stream(value)
872
+ # value = java.io.BufferedInputStream.new(value)
873
+ # end
874
+ # image_data = ImageData.new(value)
875
+ # on_event_Resize do |resize_event|
876
+ # new_image_data = image_data.scaledTo(@swt_widget.getSize.x, @swt_widget.getSize.y)
877
+ # @swt_widget.getBackgroundImage&.remove
878
+ # @swt_widget.setBackgroundImage(Image.new(@swt_widget.getDisplay, new_image_data))
879
+ # end
880
+ # Image.new(@swt_widget.getDisplay, image_data)
881
+ # else
882
+ # value
883
+ # end
884
+ # end,
885
+ :foreground => color_converter,
886
+ # :font => proc do |value|
887
+ # if value.is_a?(Hash)
888
+ # font_properties = value
889
+ # FontProxy.new(self, font_properties).swt_font
890
+ # else
891
+ # value
892
+ # end
893
+ # end,
894
+ :text => proc do |value|
895
+ # if swt_widget.is_a?(Browser)
896
+ # value.to_s
897
+ # else
898
+ value.to_s
899
+ # end
900
+ end,
901
+ # :visible => proc do |value|
902
+ # !!value
903
+ # end,
904
+ }
905
+ end
906
+
907
+ def widget_property_listener_installers
908
+ @swt_widget_property_listener_installers ||= {
909
+ # WidgetProxy => {
910
+ # :focus => proc do |observer|
911
+ # on_focus_gained { |focus_event|
912
+ # observer.call(true)
913
+ # }
914
+ # on_focus_lost { |focus_event|
915
+ # observer.call(false)
916
+ # }
917
+ # end,
918
+ # },
919
+ MenuItemProxy => {
920
+ :selection => proc do |observer|
921
+ on_widget_selected { |selection_event|
922
+ # TODO look into validity of this and perhaps move toggle logic to MenuItemProxy
923
+ if check?
924
+ observer.call(!selection)
925
+ else
926
+ observer.call(selection)
927
+ end
928
+ }
929
+ end
930
+ },
931
+ ScaleProxy => {
932
+ :selection => proc do |observer|
933
+ on_widget_selected { |selection_event|
934
+ observer.call(selection)
935
+ }
936
+ end
937
+ },
938
+ SliderProxy => {
939
+ :selection => proc do |observer|
940
+ on_widget_selected { |selection_event|
941
+ observer.call(selection)
942
+ }
943
+ end
944
+ },
945
+ SpinnerProxy => {
946
+ :selection => proc do |observer|
947
+ on_widget_selected { |selection_event|
948
+ observer.call(selection)
949
+ }
950
+ end
951
+ },
952
+ TextProxy => {
953
+ :text => proc do |observer|
954
+ on_modify_text { |modify_event|
955
+ observer.call(text)
956
+ }
957
+ end,
958
+ # :caret_position => proc do |observer|
959
+ # on_event_keydown { |event|
960
+ # observer.call(getCaretPosition)
961
+ # }
962
+ # on_event_keyup { |event|
963
+ # observer.call(getCaretPosition)
964
+ # }
965
+ # on_event_mousedown { |event|
966
+ # observer.call(getCaretPosition)
967
+ # }
968
+ # on_event_mouseup { |event|
969
+ # observer.call(getCaretPosition)
970
+ # }
971
+ # end,
972
+ # :selection => proc do |observer|
973
+ # on_event_keydown { |event|
974
+ # observer.call(getSelection)
975
+ # }
976
+ # on_event_keyup { |event|
977
+ # observer.call(getSelection)
978
+ # }
979
+ # on_event_mousedown { |event|
980
+ # observer.call(getSelection)
981
+ # }
982
+ # on_event_mouseup { |event|
983
+ # observer.call(getSelection)
984
+ # }
985
+ # end,
986
+ # :selection_count => proc do |observer|
987
+ # on_event_keydown { |event|
988
+ # observer.call(getSelectionCount)
989
+ # }
990
+ # on_event_keyup { |event|
991
+ # observer.call(getSelectionCount)
992
+ # }
993
+ # on_event_mousedown { |event|
994
+ # observer.call(getSelectionCount)
995
+ # }
996
+ # on_event_mouseup { |event|
997
+ # observer.call(getSelectionCount)
998
+ # }
999
+ # end,
1000
+ # :top_index => proc do |observer|
1001
+ # @last_top_index = getTopIndex
1002
+ # on_paint_control { |event|
1003
+ # if getTopIndex != @last_top_index
1004
+ # @last_top_index = getTopIndex
1005
+ # observer.call(@last_top_index)
1006
+ # end
1007
+ # }
1008
+ # end,
1009
+ },
1010
+ # Java::OrgEclipseSwtCustom::StyledText => {
1011
+ # :text => proc do |observer|
1012
+ # on_modify_text { |modify_event|
1013
+ # observer.call(getText)
1014
+ # }
1015
+ # end,
1016
+ # },
1017
+ DateTimeProxy => {
1018
+ :date_time => proc do |observer|
1019
+ on_widget_selected { |selection_event|
1020
+ observer.call(date_time)
1021
+ }
1022
+ end
1023
+ },
1024
+ RadioProxy => { #radio?
1025
+ :selection => proc do |observer|
1026
+ on_widget_selected { |selection_event|
1027
+ observer.call(selection)
1028
+ }
1029
+ end
1030
+ },
1031
+ TableProxy => {
1032
+ :selection => proc do |observer|
1033
+ on_widget_selected { |selection_event|
1034
+ observer.call(selection_event.table_item.get_data) # TODO ensure selection doesn't conflict with editing
1035
+ }
1036
+ end,
1037
+ },
1038
+ # Java::OrgEclipseSwtWidgets::MenuItem => {
1039
+ # :selection => proc do |observer|
1040
+ # on_widget_selected { |selection_event|
1041
+ # observer.call(getSelection)
1042
+ # }
1043
+ # end
1044
+ # },
1045
+ }
1046
+ end
1047
+
1048
+ private
1049
+
1050
+ def css_cursor
1051
+ SWT_CURSOR_TO_CSS_CURSOR_MAP[@cursor]
1052
+ end
1053
+
1054
+ end
1055
+ end
1056
+ end
1057
+
1058
+ require 'glimmer/dsl/web/element_expression'