dom-rb 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+