dom-rb 0.1.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,269 @@
1
+ require 'dom/builder'
2
+ require 'dom/util'
3
+ require 'dom/debug'
4
+
5
+ module CSR
6
+ module DOM
7
+ class Content
8
+ include CSR::DOM::Builder
9
+ include CSR::DOM::Util
10
+ include CSR::DOM::Debug
11
+
12
+ # options
13
+ #
14
+ # :value # object appropriate to type or proc(context) or proc(value) or proc(context, value)
15
+ # :sort_value # object appropriate to type or proc(context) or proc(value) or proc(context, value)
16
+ # :type # :container, :media, :string, :integer, :decimal, :bool, :count, :percent, :sign, :date
17
+ # :css # string with valid css classes or proc(value) or proc(context, value)
18
+ # :style # { } hash of styles per Clearwater or proc(value) or proc(context, value)
19
+ # :format # string or proc(value) or proc(context, value) for :decimal, :percent, :date
20
+ # :comma # boolean for numeric values (default true)
21
+ # :events # { click: ->{ clicked }, ... } - hash or proc(value) or proc(context, value)
22
+ # # event names per 'developer.mozilla.org/en-US/docs/Web/Events'
23
+
24
+ attr_reader :options, :type
25
+
26
+ def initialize(**options)
27
+ # self.debug_level = 1
28
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "options=#{options}" ]}
29
+ @options = options
30
+ @type = @options[:type] ||= :container
31
+ @options[:css] ||= ''
32
+ @options[:style] ||= {}
33
+ @options[:events] ||= {}
34
+ @is_numeric = [:integer, :decimal, :percent, :count].include?(@type)
35
+ @is_text = !(container? || media?)
36
+ @options[:comma] = true if @options[:comma].nil?
37
+ debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
38
+ set_format_defaults
39
+ debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
40
+ set_css_defaults
41
+ debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
42
+ set_style_defaults
43
+ debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
44
+ end
45
+
46
+ def container?
47
+ @type == :container
48
+ end
49
+
50
+ def media?
51
+ @type == :media
52
+ end
53
+
54
+ def text?
55
+ @is_text
56
+ end
57
+
58
+ def date?
59
+ @type == :date
60
+ end
61
+
62
+ def numeric?
63
+ @is_numeric
64
+ end
65
+
66
+ # Returns a DOM element.
67
+ # Context may be anything meaningful to procs.
68
+ def element(tag_name, attributes: {}, context: nil)
69
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "tag_name=#{tag_name} attributes=#{attributes} context=#{context}" ]}
70
+ result = tag(
71
+ tag_name,
72
+ attributes: ->{
73
+ attributes.merge.({
74
+ id: id(context: context, value: value),
75
+ class: css(context: context, value: value),
76
+ style: style(context: context, value: value),
77
+ })
78
+ },
79
+ content: ->{
80
+ formatted_value(context: context)
81
+ },
82
+ model: self
83
+ )
84
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "result=#{result}" ]}
85
+ result
86
+ end
87
+
88
+ # Context may be anything meaningful to procs.
89
+ # Returned value will be formatted (if applicable).
90
+ def value_and_attributes(context: nil)
91
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "context=#{context}" ]}
92
+ value = formatted_value(context: context)
93
+ result = [
94
+ value,
95
+ {
96
+ class: css(context: context, value: value),
97
+ style: style(context: context, value: value),
98
+ }.merge(
99
+ events(context: context, value: value)
100
+ )
101
+ ]
102
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "context=#{context} result=#{result}" ]}
103
+ result
104
+ end
105
+
106
+ def sort_value(context: nil)
107
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "context=#{context.class.name}" ]}
108
+ option(:sort_value, context: context)
109
+ end
110
+
111
+ def value(context: nil)
112
+ option(:value, context: context)
113
+ end
114
+
115
+ def css(context: nil, value: nil)
116
+ option(:css, context: context, value: value)
117
+ end
118
+
119
+ def id(context: nil, value: nil)
120
+ option(:id, context: context, value: value) || hex_id
121
+ end
122
+
123
+ def style(context: nil, value: nil)
124
+ option(:style, context: context, value: value)
125
+ end
126
+
127
+ def events(context: nil, value: nil)
128
+ result = {}
129
+ events = option(:events, context: context, value: value)
130
+ if events && events.size > 0
131
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "events=#{events}" ]}
132
+ events.each do |event, proc|
133
+ event = event[0,2] == 'on' ? event : :"on#{event}"
134
+ result[event] = ->(_) {
135
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "event=#{event} context=#{context} value=#{value}" ]}
136
+ resolve(proc, context: context, value: nil)
137
+ }
138
+ end
139
+ end
140
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "result=#{result}" unless result.empty? ]}
141
+ result
142
+ end
143
+
144
+ def formatted_value(context: nil)
145
+ v = value(context: context)
146
+ if v
147
+ if text?
148
+ format = format(context: context, value: v)
149
+ if numeric?
150
+ comma_numeric(format % v.to_f)
151
+ elsif date?
152
+ parse_date(v).strftime(value)
153
+ else
154
+ v.to_s
155
+ end
156
+ else
157
+ v
158
+ end
159
+ end
160
+ end
161
+
162
+ def format(context: nil, value: nil)
163
+ option(:format, context: context, value: value)
164
+ end
165
+
166
+ def option(name, context: nil, value: nil)
167
+ option = @options[name]
168
+ resolve(option, context: context, value: value)
169
+ end
170
+
171
+ def resolve(thing, context: nil, value: nil)
172
+ if Proc === thing
173
+ if thing.arity == 2
174
+ thing.call(context, value)
175
+ elsif thing.arity == 1
176
+ thing.call(value || context)
177
+ else
178
+ thing.call
179
+ end
180
+ else
181
+ thing
182
+ end
183
+ end
184
+
185
+ def comma_numeric(s)
186
+ @options[comma] ? s.comma_numeric : s
187
+ end
188
+
189
+ def parse_date(d)
190
+ self.class.parse_date(d)
191
+ end
192
+
193
+ def self.parse_date(d)
194
+ return d if d.is_a?(Date)
195
+ # csr Date.parse can't handle strings with named months
196
+ # nor can it parse YYYYMMDD without separators!
197
+ if RUBY_PLATFORM == 'csr'
198
+ t = Time.parse(d)
199
+ Date.new(t.year, t.month, t.day)
200
+ else
201
+ Date.parse(d)
202
+ end
203
+ end
204
+
205
+ private
206
+
207
+ def set_tag_defaults
208
+ @options[:tag] ||= 'div'
209
+ end
210
+
211
+ def set_style_defaults
212
+ if text? && options[:style].nil?
213
+ @options[:style] = case @options[:type]
214
+ when :integer, :decimal, :count, :percent
215
+ { text_align: 'right' }
216
+ else
217
+ { text_align: 'center' }
218
+ end
219
+ end
220
+ end
221
+
222
+ def set_css_defaults
223
+ # ?
224
+ end
225
+
226
+ def set_format_defaults
227
+ if text? && @options[:format].nil?
228
+ @options[:format] = case @options[:type]
229
+ when :decimal
230
+ ->(v) {
231
+ comma_numeric('%0.2f' % v)
232
+ }
233
+ when :integer, :count
234
+ ->(v) {
235
+ comma_numeric(v.to_i.to_s)
236
+ }
237
+ when :percent
238
+ ->(v) {
239
+ comma_numeric((v * 100).round(0).to_s)
240
+ }
241
+ when :sign
242
+ ->(v) {
243
+ if v == 0
244
+ '0'
245
+ else
246
+ v < 0 ? '-' : '+'
247
+ end
248
+ }
249
+ when :date
250
+ ->(v) {
251
+ v.strftime('%Y/%m/%d')
252
+ }
253
+ when :bool
254
+ ->(v) {
255
+ v ? 'T' : 'F'
256
+ }
257
+ else # :text, ...
258
+ ->(v) {
259
+ v.to_s
260
+ }
261
+ end
262
+ end
263
+ end
264
+
265
+ end
266
+
267
+ end
268
+ end
269
+
data/csr/dom/debug.rb ADDED
@@ -0,0 +1,61 @@
1
+ module CSR
2
+ module DOM
3
+ module Debug
4
+ module_function
5
+
6
+ def stack_trace
7
+ callback = ->(stackframes) {
8
+ %x(
9
+ var stringifiedStack = stackframes.map(function(sf) {
10
+ return sf.toString();
11
+ }).join('\n');
12
+ console.log(stringifiedStack);
13
+ )
14
+ }
15
+ errback = ->(err) {
16
+ `console.log(err.message)`
17
+ }
18
+ `StackTrace.get().then(callback).catch(errback)`
19
+ end
20
+
21
+ def debug_level=(l)
22
+ @debug_level = l
23
+ end
24
+
25
+ def debug_level
26
+ @debug_level ||= 0
27
+ end
28
+
29
+ def debug_method_missing=(v)
30
+ @debug_method_missing = v
31
+ end
32
+
33
+ def debug_method_missing?
34
+ !!@debug_method_missing
35
+ end
36
+
37
+ def method_missing(name, *args, &block)
38
+ if true # debug_method_missing?
39
+ debug 0, ->{[__FILE__, __LINE__, __method__, "METHOD_MISSING: self=#{self} method=#{name}"]}
40
+ # debug 0, ->{[__FILE__, __LINE__, __method__, "called by: #{caller.first.to_s}"]}
41
+ stack_trace
42
+ end
43
+ super
44
+ end
45
+
46
+ def debug(level, proc)
47
+ if level == 0 || level <= debug_level
48
+ file, line, method, msg = proc.call
49
+ s = "#{file}[#{line}] #{self.is_a?(Class) ? (self.name + '#') : self.class.name}##{method}"
50
+ s = s + " >> #{msg}" if msg
51
+ if RUBY_PLATFORM == 'opal'
52
+ `console.log(s)`
53
+ else
54
+ puts s
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ require 'opal-browser'
2
+
3
+ module CSR
4
+ module DOM
5
+ module Globals
6
+
7
+ module_function
8
+
9
+ def document
10
+ $document
11
+ end
12
+
13
+ def window
14
+ $window
15
+ end
16
+
17
+ end
18
+ end
19
+ end
data/csr/dom/root.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'dom/globals'
2
+ require 'dom/util'
3
+ require 'dom/debug'
4
+
5
+ module CSR
6
+ module DOM
7
+ module Root
8
+ include CSR::DOM::Globals
9
+ include CSR::DOM::Util
10
+ include CSR::DOM::Debug
11
+
12
+ module_function
13
+
14
+ # TODO: should this be here?
15
+ def dom_root
16
+ unless @dom_root
17
+ if respond_to? :first_element # or container, Volt methods
18
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "setting @dom_root to first_element=#{first_element}" ]}
19
+ self.dom_root = first_element
20
+ else
21
+ # in a separate model?
22
+ fail "#{self.class.name}##{__method__}:#{__LINE_} : dom_root= must be called first"
23
+ end
24
+ end
25
+ @dom_root
26
+ end
27
+
28
+ # TODO: should this be here?
29
+ def dom_root=(element)
30
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "element = #{element}" ]}
31
+ @dom_root = DOM(element)
32
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "element = #{element}" ]}
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,300 @@
1
+ require 'dom/table/caption'
2
+ require 'dom/table/section'
3
+ require 'dom/table/column'
4
+ require 'dom/builder'
5
+ require 'dom/util'
6
+ require 'dom/debug'
7
+
8
+ module CSR
9
+ module DOM
10
+ class Table
11
+ include CSR::DOM::Builder
12
+ include CSR::DOM::Util
13
+ include CSR::DOM::Debug
14
+
15
+ # TODO: other than bootstrap, ...
16
+ DEFAULT_CSS = 'table table-condensed table-bordered table-hover'
17
+ DEFAULT_STYLE = {}
18
+
19
+ # :id, # dom id - optional
20
+ # :css, # table css class string or nil
21
+ # :style, # table style hash or nil
22
+ # :caption_content, # string or ComponentSpec with tag => 'caption'
23
+ # :head_rows, # Proc or array of objects or nil
24
+ # :body_rows, # Proc or array of objects or nil
25
+ # :foot_rows, # Proc or array of objects or nil
26
+ # :sort_column_id, # default sort column or nil
27
+ # :sort_order, # nil, 1 or -1
28
+ # :accordion, # whether table has master column which can be collapsed
29
+ # :context, # something meaningful to column content procs or nil
30
+ # :reactive # is the table reactive, default false
31
+
32
+ attr_reader :id
33
+ attr_reader :sort_column_id, :sort_column, :sort_orders, :sort_order
34
+ attr_reader :accordion
35
+ attr_reader :css
36
+ attr_reader :style
37
+ attr_reader :caption_content
38
+ attr_reader :context
39
+ attr_reader :section_ids
40
+
41
+ def initialize(**options)
42
+ # self.debug_level = 1
43
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "options: #{options}" ]}
44
+ @id = options[:id] || hex_id
45
+ @rooted = false
46
+ init_sections(options)
47
+ @raw_columns = options[:columns]
48
+ @context = options[:context]
49
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
50
+ init_caption(options)
51
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
52
+ init_css_style(options)
53
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
54
+ init_row_sources(options)
55
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
56
+ init_visibility
57
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
58
+ init_sorting(options)
59
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
60
+ init_accordion(options)
61
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
62
+ end
63
+
64
+ def columns
65
+ unless @columns
66
+ @columns = if Proc === @raw_columns
67
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "@raw_columns=#{@raw_columns}"]}
68
+ c = @raw_columns.call
69
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "@columns=#{@columns}"]}
70
+ c
71
+ else
72
+ @raw_columns
73
+ end
74
+ end
75
+ @columns
76
+ end
77
+
78
+ def rooted?
79
+ @rooted
80
+ end
81
+
82
+ def root
83
+ debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
84
+ @root ||= tag(
85
+ :table,
86
+ attributes: attributes,
87
+ content: content
88
+ )
89
+ @rooted = true
90
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "@root=#{@root}" ]}
91
+ @root
92
+ end
93
+
94
+ def attributes
95
+ debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
96
+ result = {
97
+ class: css,
98
+ style: style
99
+ }
100
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "result=#{result}" ]}
101
+ result
102
+ end
103
+
104
+ def content
105
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
106
+ content = arrify(caption, sections)
107
+ result = content.map {|e| e.root }
108
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "result=#{result}" ]}
109
+ result
110
+ end
111
+
112
+ def caption
113
+ debug 1, ->{[ __FILE__, __LINE__, __method__]}
114
+ @caption ||= caption_content ? CSR::DOM::Table::Caption.new(self, caption_content) : nil
115
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "@caption=#{@caption}" ]}
116
+ @caption
117
+ end
118
+
119
+ def sections
120
+ @sections ||= section_ids.map do |id|
121
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "id=#{id}"]}
122
+ section = CSR::DOM::Table::Section.new(self, id)
123
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "section=#{section}"]}
124
+ section
125
+ end.reject do |section|
126
+ section.empty?
127
+ end
128
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "@sections=#{@sections}"]}
129
+ @sections
130
+ end
131
+
132
+ def section(id)
133
+ @sections ? @sections.detect {|e| e.id == id} : nil
134
+ end
135
+
136
+ def visible_columns
137
+ @visible_columns ||= visible_column_indexes.map {|i| columns[i]}
138
+ end
139
+
140
+ def visible_column_indexes
141
+ @visible_column_indexes
142
+ end
143
+
144
+ def column_ids
145
+ columns.map(&:id)
146
+ end
147
+
148
+ def caption_attributes
149
+ # TODO:
150
+ nil
151
+ end
152
+
153
+ def cell_attributes(section_id)
154
+ # TODO:
155
+ nil
156
+ end
157
+
158
+ def section_attributes(section_id)
159
+ nil
160
+ end
161
+
162
+ def sorted?
163
+ !!@sort_column_id
164
+ end
165
+
166
+ # set or toggle the sort order to/of the given column
167
+ def sort!(sort_column_id)
168
+ update_sorting(sort_column_id)
169
+ update_sections
170
+ end
171
+
172
+ def row_source(section_id)
173
+ @row_sources[section_id]
174
+ end
175
+
176
+ def update
177
+ # debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
178
+ @root = nil
179
+ init_sorting
180
+ update_caption
181
+ update_sections
182
+ end
183
+
184
+ def update_caption
185
+ @caption.update if @caption
186
+ end
187
+
188
+ def update_sections(ids = nil)
189
+ ids = ids || section_ids
190
+ ids.each do |id|
191
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "id=#{id} " ]}
192
+ update_section(id)
193
+ end
194
+ end
195
+
196
+ def update_section(id)
197
+ section = section(id)
198
+ if section
199
+ section.update
200
+ end
201
+ end
202
+
203
+ def update_head
204
+ update_section :head
205
+ end
206
+
207
+ def update_body
208
+ update_section :body
209
+ end
210
+
211
+ def update_foot
212
+ update_section :foot
213
+ end
214
+
215
+ # source should be source (unsorted) index or row source (model)
216
+ # columns may be visible column indexes or Column's
217
+ def update_row(section_id, source, columns = nil)
218
+ section = section(section_id)
219
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "section_id=#{section_id} source=#{source} columns=#{columns} section=#{section}" ]}
220
+ if section
221
+ section.update_row(source, columns)
222
+ end
223
+ end
224
+
225
+ # source should be source (unsorted) index or row source (model)
226
+ # column may be visible column index or a Column
227
+ def update_cell(section_id, source, column)
228
+ section = section(section_id)
229
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "section_id=#{section_id} source=#{source} column=#{column} section=#{section}" ]}
230
+ if section
231
+ section.update_cell(source, column)
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ def init_sections(options)
238
+ # TODO: from options
239
+ @section_ids = [:head, :body, :foot]
240
+ end
241
+
242
+
243
+ def init_row_sources(options)
244
+ @row_sources = {}
245
+ section_ids.each do |id|
246
+ @row_sources[id] = options[:"#{id}_rows"]
247
+ end
248
+ debug 1, ->{[ __FILE__, __LINE__, __method__, "@row_sources=#{@row_sources}" ]}
249
+ end
250
+
251
+ def init_accordion(options)
252
+ @accordion = !!options[:accordion]
253
+ end
254
+
255
+ def init_css_style(options)
256
+ @css = options[:css] || DEFAULT_CSS
257
+ @style = options[:style] || DEFAULT_STYLE
258
+ end
259
+
260
+ def init_caption(options)
261
+ @caption_content = options[:caption]
262
+ end
263
+
264
+ def init_sorting(options = nil)
265
+ # debug 1, ->{[ __FILE__, __LINE__, __method__ ]}
266
+ if options
267
+ @initial_sort_column_id = options[:sort_column_id]
268
+ @initial_sort_order = options[:sort_order]
269
+ end
270
+ @sort_column_id = sort_column_id unless
271
+ @sort_column = @sort_column_id ? columns.detect {|c| c.id == @sort_column_id} : columns.detect {|c| c.sort?}
272
+ @sort_column_id = @sort_column ? @sort_column.id : nil
273
+ @sort_order = @initial_sort_order || 1 unless @sort_order
274
+ @sort_orders = {}
275
+ if @sort_column_id
276
+ columns.each { |c| sort_orders[c.id] = c.id == sort_column_id ? @sort_order : 1 }
277
+ end
278
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "@sort_column_id=#{@sort_column_id}" ]}
279
+ end
280
+
281
+ def update_sorting(sort_column_id)
282
+ # debug 1, ->{[ __FILE__, __LINE__, __method__, "column_id=#{column_id}" ]}
283
+ if @sort_column_id == sort_column_id
284
+ sort_orders[@sort_column_id] *= -1
285
+ else
286
+ @sort_column_id = sort_column_id
287
+ @sort_column = columns.detect {|c| c.id == @sort_column_id}
288
+ end
289
+ @sort_order = sort_orders[@sort_column_id]
290
+ end
291
+
292
+ def init_visibility
293
+ @visible_column_indexes = columns.size.times.to_a
294
+ end
295
+
296
+ end
297
+
298
+ end # module DOM
299
+ end # module CSR
300
+