gridify 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,261 @@
1
+ module Gridify
2
+ class Grid
3
+ # non persistent options:
4
+ # :build_model
5
+ # :only
6
+ # :except
7
+
8
+ attr_accessor :name, # name of the table (required)
9
+ :resource, # based on AR model class (assume tableized, plural, string)
10
+ # used as basis for all RESTful requests and data format
11
+
12
+ # model
13
+ :columns, # incoming: hash of presets (native jqGrid); internally: array of GridColumn objects
14
+ # { :body => { "title" => {"width" => 98} }}
15
+
16
+ #:widths, # hash of column width (key = data type)
17
+ :searchable, # default: true (used in generating columns, changing has no effect on existing cols)
18
+ :sortable, # default: true (used in generating columns, changing has no effect on existing cols)
19
+ :editable, # default: false (used in generating columns, changing has no effect on existing cols)
20
+
21
+ # grid
22
+ :dom_id, # defaults to #{resource}_#{name} eg "notes_grid"
23
+
24
+ :jqgrid_options, # hash of additional jqGrid options that override any other settings
25
+
26
+
27
+ # grid layout options
28
+ :width, # in pixels, or nil (nil means calculated based on overflow setting)
29
+ :width_fit, # :fluid, :scroll, or :visible
30
+ # :fluid will always fit container (presently ignores width option)
31
+ # :scroll uses horizontal scrollbars
32
+ # :fitted scales columns to fit in width, not fluid
33
+
34
+ :height, # in pixels, '100%', or :auto (150)
35
+ # :auto means makes it as tall as needed per number of rows
36
+
37
+ :resizable, # allow gride resize with mouse, (true) (or {}) for default options;
38
+ # nil or false for disabled; or hash of jqUI options
39
+ # see http://jqueryui.com/demos/resizable/
40
+ # defaults (differ from jqUI ones) "minWidth" => 150, "minHeight" => 80
41
+ # when overflow is fluid, "handles" => 's', otherwise 'e, s, se'
42
+
43
+ :arranger, # :sortable, :hide_show, :chooser, or nil for none (nil) ,
44
+ # can combine with array of options
45
+ # or can be a hash with options
46
+ # see http://www.trirand.com/jqgridwiki/doku.php?id=wiki:show_hide_columns
47
+
48
+ # rows
49
+ :alt_rows, # true for odd/even row classes, or odd row style name string (nil)
50
+
51
+ :row_numbers, # true to display row numbers in left column; or numeric width in pixels (nil)
52
+
53
+ :select_rows, # true for rows are selectable (eg for pager buttons); or js function when row is selected, false disables hover (true if pager buttons else false)
54
+
55
+ # header layer
56
+ :title, # title string (aka caption), or true for resource.titleize, nil for no title (nil)
57
+
58
+ :collapsible, # when true generates collapse icon (false)
59
+ :collapsed, # when true initial state is collapsed (false)
60
+
61
+ # pager layer
62
+ :pager, # id of the pager, or true => dom_id+"_pager", false or nil for no pager (nil)
63
+ :paging_controls, # false to disable all (true); or hash with native jqGrid options
64
+ :paging_choices, # array of rows_per_page choices ([10,25,50,100])
65
+
66
+ # nav buttons
67
+ :view_button, # true, or hash of native jqGrid parameters and events for the action
68
+ :add_button,
69
+ :edit_button,
70
+ :delete_button,
71
+
72
+ :search_button, # enable search button and dialog
73
+ :search_advanced, # instead of search_button
74
+ :search_toolbar, # toggleable search bar, true or :visible, :hidden (other options?) (nil)
75
+
76
+ :refresh_button,
77
+ :jqgrid_nav_options, # native jqGrid button options (added to 2nd arg in navGrid method)
78
+
79
+
80
+ # data
81
+ :restful, # use restful url and mtype (true) for all actions
82
+ :finder, # default: :find
83
+ :url, # request url (required unless table_to_grid or derived from resource)
84
+ # if nil, uses "/#{resource}" eg "/notes"
85
+ # note, to force "editurl" use jqgrid_options
86
+
87
+ :data_type, # :xml, :json, and other defined in jqGrid options doc (xml)
88
+ :data_format, # (defaults to rails conventin based on resource) <chickens><chicken><title><body> format
89
+ # set false for jqGrid default <rows><records><row><cell> format
90
+
91
+ :sort_by, # name of sort column of next request
92
+ :sort_order, # sort direction of next request, 'asc' or 'desc' ('asc')
93
+ :case_sensitive, # sort and search are case sensitive (false)
94
+
95
+ :current_page, # current page requested
96
+ :rows_per_page, # number of items to be requested in the next request (paging_choices.first or -1 if pager false)
97
+
98
+ :table_to_grid, # when true generates tableToGrid (false) from html table, then use as local data
99
+ # note, we assume table rows are not selectable.
100
+ # (tableToGrid sets multiselect when first col has checkboxes or radio buttons,
101
+ # we dont know to preserve this so you also need to set in options)
102
+
103
+ :load_once, # true to use local data after first load (false)
104
+ :error_handler, # javacript: method for crud error handling (default to "after_submit")
105
+ :error_container, # selector for posting error/flash messages (.errorExplanation)
106
+
107
+
108
+ :z
109
+
110
+ # ----------------------
111
+ # attribute defaults and special value handling
112
+ # (sure it'd be easier to initialize defaults using a hash but we want nil to mean the jqGrid default - might be true - and not pass a value at all)
113
+
114
+ def restful
115
+ @restful==false ? false : true
116
+ end
117
+
118
+ def finder
119
+ @finder || :find
120
+ end
121
+
122
+ def searchable
123
+ @searchable==false ? false : true
124
+ end
125
+
126
+ def sortable
127
+ @sortable==false ? false : true
128
+ end
129
+
130
+
131
+ def dom_id
132
+ @dom_id || "#{resource}_#{name}"
133
+ end
134
+
135
+ def jqgrid_options
136
+ @jqgrid_options || {}
137
+ end
138
+
139
+ def width_fit
140
+ @width_fit || :fluid
141
+ end
142
+
143
+ def resizable
144
+ return false if @resizable == false
145
+ rs = { "minWidth" => 150, "minHeight" => 80 }
146
+ rs.merge!({ "handles" => 's' }) if width_fit == :fluid
147
+ rs.merge!( @resizable ) if @resizable.is_a?(Hash)
148
+ rs
149
+ end
150
+
151
+ def arranger_type #read-only
152
+ if arranger.is_a?(Hash)
153
+ arranger.keys
154
+ else
155
+ Array(arranger)
156
+ end
157
+ end
158
+
159
+ def arranger_options(type) #read-only
160
+ (arranger[type] if arranger.is_a?(Hash)) || {}
161
+ end
162
+
163
+ def select_rows
164
+ if @select_rows
165
+ @select_rows
166
+ else
167
+ pager && (view_button || edit_button || delete_button)
168
+ end
169
+ end
170
+
171
+ # header layer
172
+ def title
173
+ case @title
174
+ when String: @title
175
+ when true: resource.titleize
176
+ else
177
+ ('&nbsp;' if collapsible || collapsed) #show header with blank title
178
+ end
179
+ end
180
+
181
+ def collapsible
182
+ @collapsible || @collapsed
183
+ end
184
+
185
+ # pager layer
186
+ def pager
187
+ case @pager
188
+ when String: @pager
189
+ when true: dom_id+'_pager'
190
+ end
191
+ end
192
+
193
+ def paging_controls
194
+ @paging_controls.nil? ? true : @paging_controls
195
+ end
196
+
197
+ def paging_choices
198
+ @paging_choices || [10,25,50,100]
199
+ end
200
+
201
+ # data
202
+ def url
203
+ @url || "/#{resource}"
204
+ end
205
+
206
+ def rows_per_page
207
+ #debugger
208
+ # all rows when pager controls or rows per page are off
209
+ if !pager || paging_controls==false || @rows_per_page==false || @rows_per_page==-1
210
+ -1
211
+ elsif @rows_per_page.nil?
212
+ paging_choices.first
213
+ else
214
+ @rows_per_page
215
+ end
216
+ end
217
+
218
+ def data_type
219
+ @data_type || :xml
220
+ end
221
+
222
+ def data_format
223
+ if @data_format || @data_format==false #explicit false for no param
224
+ @data_format
225
+ elsif resource && data_type == :xml
226
+ {
227
+ :root => resource,
228
+ :page => resource+'>page',
229
+ :total => resource+'>total_pages',
230
+ :records => resource+'>total_records',
231
+ :row => resource.singularize,
232
+ :repeatitems => false,
233
+ :id => :id
234
+ }
235
+ elsif resource && data_type == :json
236
+ {
237
+ :root => resource,
238
+ :page => 'page',
239
+ :total => 'total_pages',
240
+ :records => 'total_records',
241
+ :row => resource.singularize,
242
+ :repeatitems => false,
243
+ :id => :id
244
+ }
245
+ end
246
+ end
247
+
248
+ def error_handler
249
+ @error_handler || 'gridify_action_error_handler'
250
+ end
251
+
252
+ def error_handler_return_value
253
+ error_handler ? error_handler : 'true;'
254
+ end
255
+
256
+ def error_container
257
+ @error_container || '.errorExplanation'
258
+ end
259
+
260
+ end
261
+ end
@@ -0,0 +1,346 @@
1
+ # reference: http://github.com/ahe/2dc_jqgrid
2
+
3
+ module Gridify
4
+ class Grid
5
+
6
+ # ----------------------
7
+ # generate the grid javascript for a view
8
+ # options:
9
+ # :script => true generates <script> tag (true)
10
+ # :ready => true generates jquery ready function (true)
11
+ def to_javascript( options={} )
12
+ options = { :script => true, :ready => true }.merge(options)
13
+
14
+ s = ''
15
+ if options[:script]
16
+ s << %Q^<script type="text/javascript">^
17
+ end
18
+
19
+ s << js_helpers
20
+
21
+ if options[:ready]
22
+ s << %Q^jQuery(document).ready(function(){^
23
+ end
24
+
25
+ s << jqgrid_javascript(options)
26
+
27
+ if options[:ready]
28
+ s << %Q^});^
29
+ end
30
+ if options[:script]
31
+ s << %Q^</script>^
32
+ end
33
+ s
34
+ end
35
+
36
+
37
+ def to_json
38
+ jqgrid_properties.to_json_with_js
39
+ end
40
+
41
+ # alias :to_s, :to_javascript
42
+ def to_s( options={} )
43
+ to_javascript( options )
44
+ end
45
+
46
+ # ------------------
47
+ protected
48
+ # ------------------
49
+
50
+ # //{ url: '/notes/{id}', mtype: 'PUT', reloadAfterSubmit: false, closeAfterEdit: true }, // edit settings
51
+ # //{ url: '/notes', mtype: 'POST', reloadAfterSubmit: false, closeAfterEdit: true }, // add settings
52
+ # //{ url: '/notes/{id}', mtype: 'DELETE', reloadAfterSubmit: false }, // delete settings
53
+
54
+ # get the button options
55
+ def edit_button_options
56
+ # 'url' => '/notes/{id}', 'mtype' => 'PUT'
57
+ # {afterSubmit:function(r,data){return #{options[:error_handler_return_value]}(r,data,'edit');}},
58
+
59
+ # note, closeAfterEdit will not close if response returns a non-empty string (even if "success" message)
60
+ merge_options_defaults( edit_button,
61
+ 'reloadAfterSubmit' => false,
62
+ 'closeAfterEdit' => true,
63
+ 'afterSubmit' => "javascript: function(r,data){return #{error_handler_return_value}(r,data,'edit');}"
64
+ )
65
+ end
66
+
67
+ def add_button_options
68
+ # 'url' => '/notes', 'mtype' => 'POST'
69
+ merge_options_defaults( add_button,
70
+ 'reloadAfterSubmit' => false,
71
+ 'closeAfterEdit' => true,
72
+ 'afterSubmit' => "javascript: function(r,data){return #{error_handler_return_value}(r,data,'add');}"
73
+ )
74
+ end
75
+
76
+ def delete_button_options
77
+ # 'url' => '/notes/{id}', 'mtype' => 'DELETE'
78
+ merge_options_defaults( delete_button,
79
+ 'reloadAfterSubmit' => false,
80
+ 'afterSubmit' => "javascript: function(r,data){return #{error_handler_return_value}(r,data,'delete');}"
81
+ )
82
+ end
83
+
84
+ def search_button_options
85
+ if search_advanced
86
+ merge_options_defaults( search_advanced, 'multipleSearch' => true)
87
+ else
88
+ merge_options_defaults( search_button)
89
+ end
90
+ end
91
+
92
+ def view_button_options
93
+ merge_options_defaults( view_button)
94
+ end
95
+
96
+
97
+ # generate the jqGrid initial values in json
98
+ # maps our attributes to jqGrid options; omit values when same as jqGrid defaults
99
+ def jqgrid_properties
100
+ vals = {}
101
+
102
+ # data and request options
103
+ vals['url'] = url if url
104
+ vals['restful'] = true if restful
105
+ vals['postData'] = { :grid => name } #identify which grid making the request
106
+ # vals['colNames'] = column_names if columns.present?
107
+ vals['colModel'] = column_model if columns.present?
108
+ vals['datatype'] = data_type if data_type
109
+ if data_format.present?
110
+ if data_type == :xml
111
+ vals['xmlReader'] = data_format
112
+ elsif data_type == :json
113
+ vals['jsonReader'] = data_format
114
+ end
115
+ end
116
+
117
+ vals['loadonce'] = load_once if load_once
118
+
119
+ vals['sortname'] = sort_by if sort_by
120
+ vals['sortorder'] = sort_order if sort_order && sort_by
121
+ vals['rowNum'] = rows_per_page if rows_per_page
122
+ vals['page'] = current_page if current_page
123
+
124
+ # grid options
125
+ vals['height'] = height if height
126
+ vals['gridview'] = true # faster views, NOTE theres cases when this needs to be disabled
127
+
128
+ case width_fit
129
+ when :fitted
130
+ #vals[:autowidth] = false #default
131
+ #vals[:shrinkToFit] = true #default
132
+ vals['forceFit'] = true
133
+ vals['width'] = width if width
134
+
135
+ when :scroll
136
+ #vals[:autowidth] = false #default
137
+ vals['shrinkToFit'] = false
138
+ #vals['forceFit'] = #ignored by jqGrid
139
+ vals['width'] = width if width
140
+
141
+ else #when :fluid
142
+ vals['autowidth'] = true
143
+ #vals['shrinkToFit'] = true #default
144
+ vals['forceFit'] = true
145
+ #vals['width'] = is ignored
146
+ vals['resizeStop'] = 'javascript: gridify_fluid_recalc_width'
147
+ end
148
+
149
+ vals['sortable'] = true if arranger_type.include?(:sortable)
150
+
151
+ # header layer
152
+ vals['caption'] = title if title
153
+ vals['hidegrid'] = false unless collapsible
154
+ vals['hiddengrid'] = true if collapsed
155
+
156
+ # row formatting
157
+ vals['altrows'] = true if alt_rows
158
+ vals['altclass'] = alt_rows if alt_rows.is_a?(String)
159
+
160
+ vals['rowNumbers'] = true if row_numbers
161
+ vals['rownumWidth'] = row_numbers if row_numbers.is_a?(Numeric)
162
+
163
+ if select_rows.present?
164
+ vals['scrollrows'] = true
165
+ #handler...
166
+ else
167
+ vals['hoverrows'] = false
168
+ vals['beforeSelectRow'] = "javascript: function(){ false; }"
169
+ end
170
+
171
+ # pager layer
172
+ if pager
173
+ vals['pager'] = "##{pager}"
174
+ vals['viewrecords'] = true # display total records in the query (eg "1 - 10 of 25")
175
+ vals['rowList'] = paging_choices
176
+ if paging_controls.is_a?(Hash)
177
+ # allow override of jqGrid pager options
178
+ vals.merge!(paging_controls)
179
+ elsif !paging_controls
180
+ vals['rowList'] = []
181
+ vals['pgbuttons'] = false
182
+ vals['pginput'] = false
183
+ vals['recordtext'] = "{2} records"
184
+ end
185
+ end
186
+
187
+ # allow override of native jqGrid options
188
+ vals.merge(jqgrid_options)
189
+ end
190
+
191
+ # -----------------
192
+ def jqgrid_javascript( options={} )
193
+ s = ''
194
+
195
+ if table_to_grid
196
+ s << %Q^ tableToGrid("##{dom_id}", #{to_json});^
197
+ s << %Q^ grid = jQuery("##{dom_id}") ^
198
+ else
199
+ s << %Q^ grid = jQuery("##{dom_id}").jqGrid(#{to_json})^
200
+ end
201
+
202
+ # tag the grid as fluid so we can find it on resize events
203
+ if width_fit == :fluid
204
+ s << %Q^ .addClass("fluid")^
205
+ end
206
+
207
+ # override tableToGrid colmodel options as needed (sortable)
208
+ #s << %Q^ .jqGrid('setColProp','Title',{sortable: false})^
209
+
210
+ # resize method
211
+ if resizable
212
+ s << %Q^ .jqGrid('gridResize', #{resizable.to_json})^
213
+ end
214
+
215
+ # pager buttons (navGrid)
216
+ if pager
217
+ nav_params = {
218
+ 'edit' => edit_button.present?,
219
+ 'add' => add_button.present?,
220
+ 'del' => delete_button.present?,
221
+ 'search' => search_button.present? || search_advanced.present?,
222
+ 'view' => view_button.present?,
223
+ 'refresh' => refresh_button.present?
224
+ }.merge(jqgrid_nav_options||{})
225
+
226
+ s << %Q^ .navGrid('##{pager}',
227
+ #{nav_params.to_json},
228
+ #{edit_button_options.to_json_with_js},
229
+ #{add_button_options.to_json_with_js},
230
+ #{delete_button_options.to_json_with_js},
231
+ #{search_button_options.to_json_with_js},
232
+ #{view_button_options.to_json_with_js}
233
+ )^
234
+ end
235
+
236
+ if arranger_type.include?(:hide_show)
237
+ s << %Q^ .jqGrid('navButtonAdd','##{pager}',{
238
+ caption: "Columns",
239
+ title: "Hide/Show Columns",
240
+ onClickButton : function (){ jQuery("##{dom_id}").jqGrid('setColumns',
241
+ #{arranger_options(:hide_show).to_json_with_js} );
242
+ }
243
+ })^
244
+ end
245
+ if arranger_type.include?(:chooser)
246
+ # hackey way to build the string but gets it done
247
+ chooser_code = %Q^ function (){ jQuery('##{dom_id}').jqGrid('columnChooser', {
248
+ done : function (perm) {
249
+ if (perm) {
250
+ this.jqGrid('remapColumns', perm, true);
251
+ var gwdth = this.jqGrid('getGridParam','width');
252
+ this.jqGrid('setGridWidth',gwdth);
253
+ }
254
+ } })}^
255
+ chooser_opts = {
256
+ 'caption' => 'Columns',
257
+ 'title' => 'Arrange Columns',
258
+ 'onClickButton' => 'chooser_code'
259
+ }.merge(arranger_options(:chooser))
260
+ s << %Q^ .jqGrid('navButtonAdd','##{pager}', #{chooser_opts.to_json.gsub('"chooser_code"', chooser_code)} )^
261
+ end
262
+
263
+ if search_toolbar
264
+ # I wish we could put this in the header rather than the pager
265
+ s << %Q^ .jqGrid('navButtonAdd',"##{pager}", { caption:"Toggle", title:"Toggle Search Toolbar", buttonicon: 'ui-icon-pin-s', onClickButton: function(){ grid[0].toggleToolbar() } })
266
+ .jqGrid('navButtonAdd',"##{pager}", { caption:"Clear", title:"Clear Search", buttonicon: 'ui-icon-refresh', onClickButton: function(){ grid[0].clearToolbar() } })
267
+ .jqGrid('filterToolbar')^
268
+ end
269
+
270
+ # TODO: built in event handlers, eg
271
+ # loadError
272
+ # onSelectRow, onDblClickRow, onRightClickRow etc
273
+
274
+ s << '; '
275
+
276
+ unless search_toolbar == :visible
277
+ s << %Q^ grid[0].toggleToolbar(); ^
278
+ end
279
+
280
+ # # keep page controls centered (jqgrid bug) [eg appears when :width_fit => :scroll]
281
+ # s << %Q^ $("##{pager}_left").css("width", "auto"); ^
282
+
283
+ s
284
+ end
285
+
286
+ # ------------------
287
+ def js_helpers
288
+ # just move this into appliaction.js?
289
+
290
+ # gridify_fluid_recalc_width: allow grid resize on window resize events
291
+ # recalculate width of grid based on parent container; handles multiple grids
292
+ # ref: http://www.trirand.com/blog/?page_id=393/feature-request/Resizable%20grid/
293
+
294
+ # afterSubmit: display error message in response
295
+
296
+ %Q^ function gridify_fluid_recalc_width(){
297
+ if (grids = jQuery('.fluid.ui-jqgrid-btable:visible')) {
298
+ grids.each(function(index) {
299
+ gridId = jQuery(this).attr('id');
300
+ gridParentWidth = jQuery('#gbox_' + gridId).parent().width();
301
+ jQuery('#' + gridId).setGridWidth(gridParentWidth);
302
+ });
303
+ }
304
+ };
305
+
306
+ jQuery(window).bind('resize', gridify_fluid_recalc_width);
307
+
308
+ function gridify_action_error_handler(r, data, action){
309
+ if (r.responseText != '') {
310
+ return [false, r.responseText];
311
+ } else {
312
+ return true;
313
+ }
314
+ }
315
+ ^
316
+ end
317
+
318
+
319
+ # if(r.responseText != "") {
320
+ # $('#{error_container}').html(r.responseText);
321
+ # $('#{error_container}').slideDown();
322
+ # //window.setTimeout(function() { // Hide error div after 6 seconds
323
+ # // $('#{error_container}').slideUp();
324
+ # //}, 6000);
325
+ # return false;
326
+ # }
327
+ # return true;
328
+
329
+ # lets options be true or a hash, merges into defaults and returns a hash
330
+ def merge_options_defaults( options, defaults={} )
331
+ if options
332
+ defaults.merge( options==true ? {} : options)
333
+ else
334
+ {}
335
+ end
336
+ end
337
+
338
+ end
339
+ end
340
+
341
+ class Hash
342
+ # replace embedded '"javascript: foo"' with 'foo'
343
+ def to_json_with_js
344
+ self.to_json.gsub(/\"javascript: ([^"]*)\"/) {|string| string[1..-2].gsub('javascript:','') }
345
+ end
346
+ end