RT# 82949 - changes section name from fees to pricing, better opiton
[freeside.git] / httemplate / elements / auto-table.html
1 <%doc>
2 (within a form)
3 <table>
4 <tr>
5   <th>Field 1</th>
6   <th>Field 2</th>
7 </tr>
8 <tr id="mytemplate">
9   <td><input type="text" name="field1"></td>
10   <td><select name="field2">...</td>
11   ...
12 </tr>
13 </table>
14 <& /elements/auto-table.html,
15   table => 'mytable',
16   template_row = 'mytemplate',
17   rows => [
18             { field1 => 'foo', field2 => 'CA', ... },
19             { field1 => 'bar', field2 => 'TX', ... }, ...
20           ],
21 &>
22
23   or if you prefer:
24 ...
25   fieldorder => [ 'field1', 'field2', ... ],
26   rows => [
27             [ 'foo', 'CA' ],
28             [ 'bar', 'TX' ],
29           ],
30
31 In the process/ handler, something like:
32 my @rows;
33 my %vars = $cgi->Vars;
34 for my $k ( keys %vars ) {
35   $k =~ /^${pre}magic(\d+)$/ or next;
36   my $rownum = $1;
37   # find all submitted names ending in this rownum
38   my %thisrow = 
39     map { $_ => $vars{$_} } 
40     grep /^(.*[\d])$rownum$/, keys %vars;
41   $thisrow->{num} = delete $thisrow{"${pre}magic$rownum"};
42   push @rows, $thisrow;
43 }
44 </%doc>
45 <tbody id="<%$pre%>autotable"></tbody>
46 <script type="text/javascript">
47 var <%$pre%>template;
48 var <%$pre%>tbody;
49 var <%$pre%>next_rownum;
50 var <%$pre%>set_rownum;
51 var <%$pre%>addRow;
52 var <%$pre%>deleteRow;
53 var <%$pre%>fieldorder = <% encode_json($fieldorder) %>;
54
55 function <%$pre%>possiblyAddRow_factory(obj) {
56   var callback = obj.onchange;
57   return function() {
58     if ( obj.rownum == <%$pre%>tbody.lastChild.rownum ) {
59       // then this is the last row, and it's being changed, so spawn a new row
60       <%$pre%>addRow();
61     }
62     if ( callback ) {
63       callback.apply(obj);
64     }
65   }
66 }
67
68 function <%$pre%>set_rownum(obj, rownum) {
69   obj.rownum = rownum;
70   if ( obj.id ) {
71     obj.id = obj.id + rownum;
72   }
73   if ( obj.getAttribute('name') ) {
74     obj.setAttribute('name', obj.getAttribute('name') + rownum);
75     // also, in this case it's a form field that will be part of the record
76     // so set up an onchange handler
77     obj.onchange = <%$pre%>possiblyAddRow_factory(obj);
78   }
79   for (var i = 0; i < obj.children.length; i++) {
80     if ( obj.children[i] instanceof Node ) {
81       <%$pre%>set_rownum(obj.children[i], rownum);
82     }
83   }
84 }
85
86 function <%$pre%>addRow(data) {
87   // duplicate the node
88   // warning: cloneNode doesn't clone event handlers that were set through 
89   // the DOM
90   // if 'data' is an object, prepopulate the row's fields with the object's
91   // elements
92   // returns the rownum of the new row
93   var row = <%$pre%>template.cloneNode(true);
94   <%$pre%>tbody.appendChild(row);
95   var this_rownum = <%$pre%>next_rownum;
96   <%$pre%>set_rownum(row, this_rownum);
97   if(data instanceof Array) {
98     for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) {
99       var el = document.getElementsByName(<%$pre |js_string%> +
100                                           <%$pre%>fieldorder[i] +
101                                           this_rownum)[0];
102       if (el) {
103         if ( el.tagName.toLowerCase() == 'span' ) {
104           el.innerHTML = data[i];
105         } else if ( el.type == 'checkbox' ) {
106           el.checked = (el.value == data[i]);
107         } else {
108           el.value = data[i];
109         }
110       }
111     }
112   } else if (data instanceof Object) {
113     for (var field in data) {
114       var el = document.getElementsByName(<%$pre |js_string%> +
115                                           field +
116                                           this_rownum)[0];
117       if (el) {
118         if ( el.tagName.toLowerCase() == 'span' ) {
119           el.innerHTML = data[field];
120         } else if ( el.type == 'checkbox' ) {
121           el.checked = (el.value == data[field]);
122         } else {
123           el.value = data[field];
124         }
125       }
126     }
127   } // else nothing
128   <%$pre%>next_rownum++;
129   return this_rownum;
130 }
131
132 function <%$pre%>deleteRow(rownum) {
133   if ( rownum == <%$pre%>tbody.lastChild.rownum ) {
134     // if this is the last row, spawn another one after it
135     <%$pre%>addRow();
136   }
137   var r = document.getElementById('<%$pre%>row' + rownum);
138   <%$pre%>tbody.removeChild(r);
139 }
140
141 function <%$pre%>set_prefix(obj) {
142   if ( obj.id ) {
143     obj.id = <%$pre |js_string%> + obj.id;
144   }
145   if ( obj.getAttribute('name') ) {
146     obj.setAttribute('name', <%$pre |js_string%> + obj.getAttribute('name'));
147   }
148   for (var i = 0; i < obj.children.length; i++) {
149     if ( obj.children[i] instanceof Node ) {
150       <%$pre%>set_prefix(obj.children[i]);
151     }
152   }
153 }
154
155 function <%$pre%>init() {
156   <%$pre%>template = document.getElementById(<% $template_row |js_string%>);
157   <%$pre%>tbody = document.getElementById('<%$pre%>autotable');
158   <%$pre%>next_rownum = <%$pre%>template.sectionRowIndex;
159   // detach the template row
160   var table = <%$pre%>template.parentNode;
161   table.removeChild(<%$pre%>template);
162   // give it an id
163   <%$pre%>template.id = 'row';
164   // prefix the ids and names of the TR object and all its descendants
165   <%$pre%>set_prefix(<%$pre%>template);
166   // add a magic identifier so we know it's been submitted
167   var magic = document.createElement('INPUT');
168   magic.setAttribute('type', 'hidden');
169   magic.setAttribute('name', '<%$pre%>magic');
170   magic.value = '1';
171   // and a delete button
172 %# should this be enclosed in an actual <button> for aesthetics?
173   var delete_button = document.createElement('IMG');
174   delete_button.id = '<%$pre%>delete_button';
175   delete_button.src = '<%$fsurl%>images/cross.png';
176   delete_button.alt = 'X';
177   // use an inline string for this so that it will be cloned properly
178   delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);");
179   // and an error display
180   var error_span = document.createElement('SPAN');
181   error_span.className = 'error';
182   error_span.style.color = '#FF0000';
183   error_span.setAttribute('name', '<%$pre%>error');
184   error_span.style.padding = '5px';
185   var delete_cell = document.createElement('TD');
186   delete_cell.style.textAlign = 'left';
187   delete_cell.appendChild(delete_button);
188   delete_cell.appendChild(magic); // it has to go somewhere
189   delete_cell.appendChild(error_span);
190   <%$pre%>template.appendChild(delete_cell);
191
192   // preload rows
193   var rows = <% encode_json(\@rows) %>;
194   for (var i = 0; i < rows.length; i++) {
195     <%$pre%>addRow(rows[i]);
196   }
197
198   <%$pre%>addRow();
199 }
200
201 <%$pre%>init();
202 </script>
203 <%init>
204 my %opt = @_;
205 my $pre = '';
206 $pre = $opt{'table'} . '_' if $opt{'table'};
207 my $template_row = $opt{'template_row'}
208   or die "auto-table requires template_row\n"; # a DOM id
209
210 # rows that we will preload, as hashrefs of name => value
211 my @rows = @{ $opt{'data'} || [] };
212 foreach (@rows) {
213   # allow an array of FS::Record objects to be passed
214   if ( blessed($_) and $_->isa('FS::Record') ) {
215     $_ = $_->hashref;
216   }
217 }
218 my $fieldorder = $opt{'fieldorder'} || [];
219 </%init>