vdom-rb 0.1.0

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,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
+