vdom-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ puts "#{__FILE__}[#{__LINE__}] requiring vdom stuff"
2
+
3
+ require 'singleton'
4
+ require 'vdom/instance'
5
+
6
+ module VDOM
7
+ class Registry
8
+
9
+ include Singleton
10
+ include Enumerable
11
+
12
+ def initialize
13
+ @instances = Array.new
14
+ end
15
+
16
+ def add(instance)
17
+ # `console.log(#{"#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__}(instance=#{instance})"})`
18
+ if @instances.find_index(instance)
19
+ fail "#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__}: virtual DOM instance with id #{instance.id} already registered"
20
+ end
21
+ @instances << instance
22
+ end
23
+
24
+ def delete(instance)
25
+ @instances ? @instances.delete(instance) : nil
26
+ end
27
+
28
+ def each(&block)
29
+ @instances.each(&block)
30
+ end
31
+
32
+ def size
33
+ @instances.size
34
+ end
35
+
36
+ # Render the DOM with given id, or
37
+ # all DOMs if dom id is nil.
38
+ def render(dom_id = nil)
39
+ result = false
40
+ # `console.log(#{"#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__}(dom_id=#{dom_id}): @instances.size=#{@instances.size} "})`
41
+ each { |e|
42
+ if dom_id.nil? || e.id == dom_id
43
+ # `console.log(#{"#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__}(dom_id=#{dom_id}): calling #{e.class.name}#render"})`
44
+ e.render
45
+ result = true
46
+ end
47
+ }
48
+ # `console.log(#{"#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__}(dom_id=#{dom_id}): @instances.size=#{@instances.size} result=#{result} "})`
49
+ result
50
+ end
51
+
52
+ def shutdown
53
+ each { |e| e.shutdown }
54
+ end
55
+
56
+ end
57
+
58
+ Instances = VDOM::Registry.instance
59
+ end
60
+
@@ -0,0 +1,164 @@
1
+ require 'vdom/component'
2
+
3
+ module VDOM
4
+ module Renderer
5
+
6
+ module_function
7
+
8
+ def node(tag_name, attributes: nil, content: nil, model: nil, dom_id: nil)
9
+ VDOM::Node.new(tag_name, attributes: attributes, content: content, model: model, dom_id: dom_id)
10
+ end
11
+
12
+ def state_component(content_or_state)
13
+ VDOM::StateComponent.new(content_or_state)
14
+ end
15
+
16
+ def icon(name, callback: nil, css: nil, float: nil, margins: nil, style: nil)
17
+ _style = {
18
+ text_align: 'center',
19
+ vertical_align: 'middle',
20
+ color: 'inherit',
21
+ background_color: 'inherit',
22
+ cursor: 'pointer',
23
+ }
24
+ if margins
25
+ _style =_style.merge(margins)
26
+ elsif float
27
+ _style = _style.merge(
28
+ case float.to_sym
29
+ when :left
30
+ { margin_right: '0.5em' } # { margin_top: '0.2em', margin_right: '0.5em' }
31
+ when :right
32
+ { margin_left: '0.5em' } # { margin_top: '0.2em', margin_right: '0.5em' }
33
+ else
34
+ { } # { margin_top: '0.2em', margin_right: '0.5em' }
35
+ end
36
+ )
37
+ end
38
+ # arg style overrides any defaults
39
+ _style = _style.merge(style) if style
40
+ _class = iconify(name)
41
+ _class = "#{_class} #{css}" if css
42
+ _class = "#{_class} pull-#{float}" if float
43
+ attributes = {
44
+ class: _class,
45
+ style: _style
46
+ }.merge(
47
+ callback ? { onclick: callback } : {}
48
+ )
49
+ node(:span, attributes: attributes)
50
+ end
51
+
52
+ def icon_with_anchor(icon_name, href, float: nil, margins: nil, icon_css: nil, anchor_css: nil, icon_style: nil, anchor_style: nil)
53
+ _icon = icon(icon_name, css: icon_css, float: float, margins: margins, style: icon_style)
54
+ node(:a,
55
+ attributes: {
56
+ href: href,
57
+ style: anchor_style,
58
+ class: anchor_css || '',
59
+ },
60
+ content: _icon
61
+ )
62
+ end
63
+
64
+ def add_icon(href)
65
+ icon_with_anchor(
66
+ :plus_sign,
67
+ href,
68
+ icon_style: { color: 'lightgreen'}
69
+ )
70
+ end
71
+
72
+ def delete_icon(callback)
73
+ icon(
74
+ :remove_sign,
75
+ callback: callback,
76
+ style: {color: 'red'}
77
+ )
78
+ end
79
+
80
+ def plain_anchor(content, href)
81
+ node(:a,
82
+ attributes: {
83
+ href: href,
84
+ style: 'color: inherit; background-color: inherit'
85
+ },
86
+ content: content
87
+ )
88
+ end
89
+
90
+ def sortable(callback, direction: 0, content: nil)
91
+ if direction != 0
92
+ node(:div,
93
+ attributes: {
94
+ onclick: callback,
95
+ style: { cursor: 'pointer' }
96
+ },
97
+ content: arrify(content) + [
98
+ node(:span,
99
+ attributes: {
100
+ class: "glyphicon glyphicon-triangle-#{direction > 0 ? 'top' : 'bottom'}",
101
+ style: {
102
+ font_size: 'smaller',
103
+ margin_left: '0.5em',
104
+ vertical_align: 'middle',
105
+ color: 'inherit',
106
+ background_color: 'inherit',
107
+ }
108
+ }
109
+ )
110
+ ]
111
+ )
112
+ else
113
+ node(:div,
114
+ attributes: {
115
+ onclick: callback,
116
+ style: { cursor: 'pointer' }
117
+ },
118
+ content: content
119
+ )
120
+ end
121
+ end
122
+
123
+ def collapsible(callback, collapsed: false, content: nil)
124
+ node(:div,
125
+ attributes: {
126
+ onclick: callback,
127
+ style: { cursor: 'pointer' }
128
+ },
129
+ content: [
130
+ node(:span,
131
+ attributes: {
132
+ class: "glyphicon glyphicon-menu-#{collapsed ? 'down' : 'up'} pull-left",
133
+ style: {
134
+ font_size: 'smaller',
135
+ margin_right: '0.5em',
136
+ vertical_align: 'middle',
137
+ color: 'inherit',
138
+ background_color: 'inherit',
139
+ }
140
+ }
141
+ )
142
+ ] + arrify(content)
143
+ )
144
+ end
145
+
146
+ def arrify(obj)
147
+ if obj
148
+ if Enumerable === obj
149
+ obj.to_a
150
+ else
151
+ [obj]
152
+ end
153
+ else
154
+ []
155
+ end
156
+ end
157
+
158
+ # TODO: generalize from bootstrap
159
+ def iconify(icon_name)
160
+ "glyphicon glyphicon-#{icon_name.to_s.gsub('_', '-')}"
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,500 @@
1
+ require 'vdom/component'
2
+ require 'vdom/renderer'
3
+ require 'vdom/content'
4
+ require 'vdom/util'
5
+
6
+ module VDOM
7
+
8
+ class TableRow < VDOM::StateComponent
9
+ include VDOM::Renderer
10
+ include VDOM::Util
11
+
12
+ def initialize(state)
13
+ super(state)
14
+ end
15
+
16
+ def table; state[:table] end
17
+ def section; state[:section] end
18
+ def index; state[:index] end
19
+
20
+ def render
21
+ # debug __FILE__, __LINE__, __method__, "section=#{section} model_index [#{index}]"
22
+ node(
23
+ :tr,
24
+ attributes: table.row_attributes(section, index),
25
+ content: cells
26
+ )
27
+ end
28
+
29
+ def cells
30
+ _table = table
31
+ _section = section
32
+ _is_head = _section == :head
33
+ # debug __FILE__, __LINE__, __method__, "section=#{section} index=#{index} "
34
+ _row_model = _table.row_model(section, index)
35
+ # debug __FILE__, __LINE__, __method__, "section=#{section} index=#{index} cust=#{_row_model.class} #{_row_model.to_h}"
36
+ _table.visible_columns.map do |column|
37
+ content = column.section_content(_section)
38
+ if Content === content
39
+ content, attributes = content.value_and_attributes(context: _row_model)
40
+ # debug __FILE__, __LINE__, __method__, "section=#{section} column=#{column.id} content=#{content}"
41
+ end
42
+ if _is_head && column.sort?
43
+ content = Renderer.sortable(
44
+ column.sort_callback,
45
+ direction: _table.sort_column_id == column.id ? _table.sort_order : 0,
46
+ content: content
47
+ )
48
+ end
49
+ tag_name = _is_head ? 'th' : 'td'
50
+ node(tag_name, attributes: attributes, content: content)
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ class TableCaption < VDOM::StateComponent
57
+ include VDOM::Renderer
58
+ include VDOM::Util
59
+
60
+ def initialize(state)
61
+ super(state)
62
+ end
63
+
64
+ def table
65
+ state[:table]
66
+ end
67
+
68
+ def render
69
+ # debug __FILE__, __LINE__, __method__, "CAPTION"
70
+ content = table.caption_content
71
+ content = if VDOM::Content === content
72
+ content.node(:caption, attributes: attributes, context: state[:table])
73
+ else
74
+ node(
75
+ :h3,
76
+ attributes: table.caption_attributes,
77
+ content: content
78
+ )
79
+ end
80
+ node(
81
+ :caption,
82
+ attributes: table.caption_attributes,
83
+ content: content
84
+ )
85
+ end
86
+ end
87
+
88
+ class TableSection < VDOM::StateComponent
89
+ include VDOM::Renderer
90
+
91
+ def initialize(state)
92
+ super(state)
93
+ end
94
+
95
+ def table; state[:table] end
96
+ def section; state[:section] end
97
+ def row_states; state[:row_states] end
98
+
99
+ def render
100
+ # debug __FILE__, __LINE__, __method__, "@section=#{section}"
101
+ node(
102
+ "t#{section}",
103
+ attributes: table.section_attributes(section),
104
+ content: rows
105
+ )
106
+ end
107
+
108
+ def rows
109
+ row_states.map do |row_state|
110
+ TableRow.new(row_state)
111
+ end
112
+ end
113
+ end
114
+
115
+ class Table < VDOM::StateComponent
116
+ include VDOM::Util
117
+
118
+ # TODO: other than bootstrap, ...
119
+ DEFAULT_CSS = 'table table-condensed table-bordered table-hover'
120
+ DEFAULT_STYLE = {}
121
+
122
+ # :css, # table css class string or nil
123
+ # :style, # table style hash or nil
124
+ # :caption_content, # string or ComponentSpec with tag => 'caption'
125
+ # :head_rows, # Proc or array of objects or nil
126
+ # :body_rows, # Proc or array of objects or nil
127
+ # :foot_rows, # Proc or array of objects or nil
128
+ # :sort_column_id, # default sort column or nil
129
+ # :sort_order, # nil, 1 or -1
130
+ # :accordion, # whether table has master column which can be collapsed
131
+ # :context, # something meaningful to column content procs or nil
132
+ # :reactive # is the table reactive, default false
133
+
134
+ attr_reader :sort_column_id, :sort_column, :sort_orders, :sort_order
135
+ attr_reader :accordion
136
+ attr_reader :css
137
+ attr_reader :style
138
+ attr_reader :caption_content
139
+ attr_reader :context
140
+ attr_reader :section_ids
141
+ attr_reader :render_count
142
+
143
+ def initialize(**options)
144
+ # debug __FILE__, __LINE__, __method__, "options: #{options.class.name}"
145
+
146
+ init_sections(options)
147
+
148
+ # debug __FILE__, __LINE__, __method__
149
+ @raw_columns = options[:columns]
150
+ @context = options[:context]
151
+
152
+ # debug __FILE__, __LINE__, __method__
153
+ init_caption(options)
154
+
155
+ # debug __FILE__, __LINE__, __method__
156
+ init_css_style(options)
157
+
158
+ # debug __FILE__, __LINE__, __method__
159
+ init_row_sources(options)
160
+ init_row_models
161
+
162
+ # debug __FILE__, __LINE__, __method__
163
+ @initial_sort_column_id = options[:sort_column_id]
164
+ @initial_sort_order = options[:sort_order]
165
+
166
+ # debug __FILE__, __LINE__, __method__
167
+ init_visibility
168
+
169
+ # debug __FILE__, __LINE__, __method__
170
+ init_sorting
171
+
172
+ # debug __FILE__, __LINE__, __method__
173
+ init_accordion(options)
174
+
175
+ # debug __FILE__, __LINE__, __method__
176
+ init_states
177
+
178
+ # debug __FILE__, __LINE__, __method__, "_state=#{_state}"
179
+ super(table_state)
180
+
181
+ @render_count = 0
182
+ end
183
+
184
+ def table_state
185
+ State.new(table: self)
186
+ end
187
+
188
+ def columns
189
+ unless @columns
190
+ @columns = if Proc === @raw_columns
191
+ @raw_columns.call
192
+ else
193
+ @raw_columns
194
+ end
195
+ end
196
+ @columns
197
+ end
198
+
199
+ def render
200
+ @render_count += 1
201
+ node(
202
+ :table,
203
+ attributes: { class: css, style: style },
204
+ content: [caption_component] + section_components
205
+ )
206
+ end
207
+
208
+ def caption_component
209
+ TableCaption.new(caption_state)
210
+ end
211
+
212
+ def section_components
213
+ section_ids.map do |id|
214
+ TableSection.new(section_state(id))
215
+ end
216
+ end
217
+
218
+ def visible_columns
219
+ @visible_columns ||= visible_column_indexes.map {|i| columns[i]}
220
+ end
221
+
222
+ def visible_column_indexes
223
+ @visible_column_indexes
224
+ end
225
+
226
+ def column_ids
227
+ columns.map(&:id)
228
+ end
229
+
230
+ def caption_attributes
231
+ # TODO:
232
+ nil
233
+ end
234
+
235
+ def cell_attributes(section)
236
+ # TODO:
237
+ nil
238
+ end
239
+
240
+ def section_attributes(section)
241
+ # TODO:
242
+ nil
243
+ end
244
+
245
+ def row_attributes(section, index)
246
+ # TODO:
247
+ nil
248
+ end
249
+
250
+ def sorted?
251
+ !!@sort_column_id
252
+ end
253
+
254
+ # set or toggle the sort order to/of the given column
255
+ def sort!(column_id)
256
+ if @sort_column_id == column_id
257
+ sort_orders[@sort_column_id] *= -1
258
+ else
259
+ @sort_column_id = column_id
260
+ @sort_column = columns.detect {|c| c.id == @sort_column_id}
261
+ end
262
+ @sort_order = sort_orders[@sort_column_id]
263
+ @model_indexes_sorted[:body] = nil
264
+ invalidate_sections
265
+ render_dom!
266
+ end
267
+
268
+ def row_models(section)
269
+ @row_models[section]
270
+ end
271
+
272
+ def row_model(section, row_index)
273
+ models = row_models(section)
274
+ models ? models[row_index] : nil
275
+ end
276
+
277
+ def invalidate
278
+ init_row_models
279
+ init_sorting
280
+ invalidate_caption
281
+ invalidate_sections
282
+ render_dom!
283
+ end
284
+
285
+ def invalidate_caption
286
+ @caption_state = nil
287
+ end
288
+
289
+ def invalidate_sections(arg_ids = nil)
290
+ ids = arg_ids || section_ids
291
+ ids.each do |id|
292
+ # debug __FILE__, __LINE__, __method__, "id=#{id} "
293
+ invalidate_section(id, row_states: true)
294
+ end
295
+ end
296
+
297
+ def invalidate_section(section, row_states: true)
298
+ # debug __FILE__, __LINE__, __method__, "section=#{section} row_states=#{row_states}"
299
+ @section_states[section] = nil
300
+ @row_states[section] = nil if row_states
301
+ end
302
+
303
+ def invalidate_row(section, model, columns = nil)
304
+ # debug __FILE__, __LINE__, __method__, "section=#{section} model=#{model} columns=#{columns}"
305
+ index = state_index(section, model)
306
+ if index
307
+ if section == :body && (columns.nil? || columns.include?(@sort_column))
308
+ # debug __FILE__, __LINE__, __method__, "section=#{section} model=#{model.name} invalidating whole section"
309
+ @model_indexes_sorted[section] = nil
310
+ invalidate_section(section, row_states: true)
311
+ else
312
+ # debug __FILE__, __LINE__, __method__, "section=#{section} invalidating row [#{index}] only for #{model.name}"
313
+ invalidate_section(section, row_states: false)
314
+ invalidate_row_state(section, index, columns)
315
+ end
316
+ end
317
+ end
318
+
319
+ def invalidate_cell(section, row, column)
320
+ invalidate_row(section, row, [column])
321
+ end
322
+
323
+ private
324
+
325
+ def init_sections(options)
326
+ # TODO: from options
327
+ @section_ids = [:head, :body, :foot]
328
+ end
329
+
330
+
331
+ def init_row_sources(options)
332
+ @row_sources = {}
333
+ section_ids.each do |section|
334
+ @row_sources[section] = options[:"#{section}_rows"]
335
+ end
336
+ # debug __FILE__, __LINE__, __method__, "@row_sources=#{@row_sources}"
337
+ end
338
+
339
+ def init_row_models
340
+ # debug __FILE__, __LINE__, __method__
341
+ @row_models = {}
342
+ empty_sections = []
343
+ @section_ids.each do |section|
344
+ source = @row_sources[section]
345
+ @row_models[section] = case source
346
+ when Fixnum
347
+ source.times.to_a
348
+ when Proc
349
+ source.call
350
+ else
351
+ if source.nil? || source.size == 0
352
+ empty_sections << section
353
+ end
354
+ source
355
+ end
356
+ end
357
+ empty_sections.each do |section|
358
+ @section_ids.delete(section)
359
+ @row_models[section]
360
+ end
361
+ # debug __FILE__, __LINE__, __method__, '@section_ids=#{@section_ids}'
362
+ end
363
+
364
+ def init_accordion(options)
365
+ @accordion = !!options[:accordion]
366
+ end
367
+
368
+ def init_css_style(options)
369
+ @css = options[:css] || DEFAULT_CSS
370
+ @style = options[:style] || DEFAULT_STYLE
371
+ end
372
+
373
+ def init_caption(options)
374
+ @caption_content = options[:caption]
375
+ end
376
+
377
+ def init_sorting
378
+ @sort_column_id = @initial_sort_column_id unless @sort_column_id
379
+ @sort_column = @sort_column_id ? columns.detect {|c| c.id == @sort_column_id} : columns.detect {|c| c.sort?}
380
+ @sort_column_id = @sort_column ? @sort_column.id : nil
381
+ @sort_order = @initial_sort_order || 1 unless @sort_order
382
+ @sort_orders = {}
383
+ if @sort_column_id
384
+ columns.each { |c| sort_orders[c.id] = c.id == sort_column_id ? @sort_order : 1 }
385
+ end
386
+ @model_indexes_sorted = {}
387
+ # debug __FILE__, __LINE__, __method__, "@sort_column_id=#{@sort_column_id}"
388
+ end
389
+
390
+ def init_states
391
+ @caption_state = nil
392
+ @section_states = {}
393
+ @row_states = {}
394
+ end
395
+
396
+ def init_visibility
397
+ @visible_column_indexes = columns.size.times.to_a
398
+ end
399
+
400
+ def caption_state
401
+ @caption_state ||= State.new(table: self)
402
+ end
403
+
404
+ def section_state(section)
405
+ unless @section_states[section]
406
+ # debug __FILE__, __LINE__, __method__, "#{section} creating new section state"
407
+ @section_states[section] = State.new(
408
+ table: self,
409
+ section: section,
410
+ row_states: row_states(section)
411
+ )
412
+ end
413
+ @section_states[section]
414
+ end
415
+
416
+ def row_states(section)
417
+ sorted_indexes = model_indexes_sorted(section)
418
+ states = (@row_states[section] ||= Array.new(sorted_indexes.size))
419
+ if section == :body
420
+ # debug __FILE__, __LINE__, __method__, "#{section} sorted_indexes=#{sorted_indexes.to_a}"
421
+ # debug __FILE__, __LINE__, __method__, "#{section} sorted_models=#{sorted_indexes.map{|i|row_models(section)[i]}.map(&:code)}"
422
+ end
423
+ sorted_indexes.each_with_index do |model_index, row_index|
424
+ unless states[row_index]
425
+ if section == :body
426
+ # debug __FILE__, __LINE__, __method__, "#{section} creating new row state[#{row_index}] model_index=#{model_index} #{section == :body ? row_models(section)[model_index].name : ''}"
427
+ end
428
+ states[row_index] = State.new(table: self, section: section, index: model_index)
429
+ end
430
+ end
431
+ states
432
+ end
433
+
434
+ def invalidate_row_state(section, index, columns)
435
+ if @row_states[section]
436
+ # debug __FILE__, __LINE__, __method__, "#{section} index=#{index} #{row_models(section)[index].name}"
437
+ @row_states[section][index] = nil
438
+ end
439
+ end
440
+
441
+ def state_index(section, model)
442
+ models = row_models(section)
443
+ model_indexes_sorted(section).each_with_index do |model_index, state_index|
444
+ if model == models[model_index]
445
+ return state_index
446
+ end
447
+ end
448
+ nil
449
+ end
450
+
451
+ def model_indexes_sorted(section)
452
+ unless @model_indexes_sorted[section]
453
+ models = row_models(section)
454
+ @model_indexes_sorted[section] = models.size.times.to_a
455
+ if section == :body && sorted?
456
+ order = sort_order
457
+ @model_indexes_sorted[section] = @model_indexes_sorted[section].sort do |a, b|
458
+ val_a = sort_value(sort_column, models[a])
459
+ val_b = sort_value(sort_column, models[b])
460
+ compare(val_a, val_b) * order
461
+ end
462
+ sorted_models = @model_indexes_sorted[section].map{|i|models[i]}
463
+ sorted_models = sorted_models.map(&:code)
464
+ # debug __FILE__, __LINE__, __method__, "sorted models = #{sorted_models}"
465
+ # debug __FILE__, __LINE__, __method__, "sort_column=#{sort_column.id} model_indexes_sorted=#{@model_indexes_sorted}"
466
+ end
467
+ end
468
+ # debug __FILE__, __LINE__, __method__, "sort_column=#{sort_column.id} model_indexes_sorted=#{@model_indexes_sorted}"
469
+ @model_indexes_sorted[section]
470
+ end
471
+
472
+ def sort_value(column, row_model)
473
+ content = column.section_content(:body)
474
+ result = if VDOM::Content === content
475
+ content.sort_value(context: row_model)
476
+ else
477
+ content
478
+ end
479
+ result
480
+ end
481
+
482
+ def compare(a, b)
483
+ if a && b
484
+ a <=> b
485
+ elsif a
486
+ 1
487
+ elsif b
488
+ -1
489
+ else
490
+ 0
491
+ end
492
+ end
493
+
494
+
495
+ end
496
+
497
+ end # module VDOM
498
+
499
+ require 'vdom/table_column'
500
+