gridify 0.1.0

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