1 /**
  2  * @fileOverview Library for easy forms. This library is
  3  * still under development.<br/>
  4  *
  5  * <p>To display a QuickForm, first create one, then add the fields in order,
  6  * then print the QuickForm.  QuickForms take care of basic formatting like
  7  * labeling input fields.</p>
  8  *
  9  * @example
 10 import("quickforms");
 11 var form = new QuickForm({});
 12 form.addHeading("h", raw("What do <em>you</em> like?"));
 13 form.addInputText("who", {label: "you are..."});
 14 form.addInputText("what", {label: "you like..."});
 15 form.addInputTextArea("why", {label: "because..."});
 16 form.addSubmit("That's why!", "That's why!");
 17 print(form);
 18  */
 19 
 20 // TODO: should you be able to create QuickForm and QuickButton without
 21 // the "this" keyword?
 22 
 23 //----------------------------------------------------------------
 24 // globals
 25 //----------------------------------------------------------------
 26 
 27 var _CSS = """
 28 <style>
 29 
 30 form.quickform {
 31   background-color: #fff;
 32   border: 1px solid #ccc;
 33   padding: 0.6em;
 34 }
 35 
 36 /*---- header ----*/
 37 
 38 form.quickform div.qfheader {
 39   font-size: 1.0em;
 40   font-weight: bold;
 41   border-bottom: 1px solid #ccc; 
 42   margin-bottom: 1.0em;
 43   padding: 0.1em 0.2em 0.2em 0;
 44 }
 45 
 46 /*---- label ----*/
 47 form.quickform div.qflabeldiv {
 48   margin-top: 1.0em;
 49 }
 50 form.quickform label.qflabel {
 51   color: #222;
 52   font-weight: bold;
 53   font-size: 76%;
 54 }
 55 form.quickform .qffield {
 56   margin-bottom: 1.0em;
 57 }
 58 /*---- inputText ----*/
 59 form.quickform div.qfinputtextdiv {
 60 }
 61 form.quickform input.qfinputtext, form.quickform textarea.qfinputtextarea {
 62   border: 1px solid #8faec6;
 63 }
 64 
 65 form.quickform input.qfsubmitdiv {
 66 }
 67 
 68 form.quickform input.qfsubmit {
 69   margin-top: 1.0em;
 70   padding: 2px 6px;
 71 }
 72 
 73 form.quickform span.surroundtext {
 74   color: #555;
 75   font-size: 80%;
 76 }
 77 
 78 div.qfclearfloats { clear: both; }
 79 
 80 </style>
 81 """;
 82 
 83 page.head.write(_CSS);
 84 
 85 //----------------------------------------------------------------
 86 // helper functions
 87 //----------------------------------------------------------------
 88 function _clearfloats() {
 89   return DIV({className: 'qfclearfloats'}, raw("<!-- -->"));
 90 }
 91 
 92 //----------------------------------------------------------------
 93 // QuickForm object
 94 //----------------------------------------------------------------
 95 
 96 /**
 97  * <p>Creates a new, empty QuickForm.</p>
 98  *
 99  * <p>Supported options (all optional):</p>
100  * <ul>
101  * <li><strong>method:</strong> "get" or "post" (defaults to "post")</li>
102  * <li><strong>action:</strong> path to submit to, defaults to current path</li>
103  * <li><strong>enctype:</strong> used to change the method of data encoding</li>
104  * </ul>
105  *
106  * @constructor
107  * @param {object} [opts]  an optional dictionary of options
108  */
109 QuickForm = function(opts) {
110   this.method = (opts && opts.method) ? opts.method.toUpperCase() : 'POST';
111   this.action = (opts && opts.action) ? opts.action : request.path;
112   this.enctype = (opts && opts.enctype) ? opts.enctype :
113     "application/x-www-form-urlencoded";
114 
115   this.form = FORM({method: this.method, enctype: this.enctype,
116             action: this.action, className: 'quickform'});
117   this.inputNames = [];
118 };
119 
120 /**
121  * Adds a heading in bold type that introduces a section of the form.
122  *
123  * @param {string} [id] (optional) unique id string, used for CSS rules
124  * @param {string} text the text of the heading
125  * @function
126  */
127 QuickForm.prototype.addHeading = function(id, text) {
128   if (! text) {
129     text = id;
130     id = undefined;
131   }
132   var attribs = {className: 'qfheader'};
133   if (id) attribs.id = id;
134   this.form.push(DIV(attribs, text));
135   return this;
136 };
137 
138 /**
139  * <p>Creates a one-line text box for entering a small amount of text.</p>
140  *
141  * <p>Supported options (all optional):</p>
142  * <ul>
143  * <li><strong>label:</strong> text to label the field with, defaults to id</li>
144  * <li><strong>beforeText:</strong> text to put immediately before the input field</li>
145  * <li><strong>afterText:</strong> text to put immediately after the input field</li>
146  * <li><strong>value:</strong> initial text for the field</li>
147  * <li><strong>size:</strong> width of the field in characters</li>
148  * <li><strong>disabled:</strong> set to "disabled" to disable the field</li>
149  * </ul>
150  *
151  * @param {string} id a unique id string, used as a parameter name for this field
152  * @param {object} [opts]  an optional dictionary of options
153  * @function
154  */
155 QuickForm.prototype.addInputText = function(id, opts) {
156   // remember this name
157   this.inputNames.push(id);
158 
159   // label
160   var labelText = (opts && opts.label) ? opts.label : id;
161   this.form.push(DIV({className: 'qflabeldiv'},
162              LABEL({htmlFor: id,
163                className: 'qflabel'},
164                labelText)));
165 
166   // container
167   var container = DIV({className: 'inputtextdiv qffield'});
168 
169   // beforeText
170   if (opts && opts.beforeText) {
171     container.push(SPAN({className: 'surroundtext'}, opts.beforeText));
172   }
173   
174   // input
175   var attrs = {className: 'qfinputtext',
176            type: 'text',
177            name: id,
178            id: id};
179   var quotestripper = new RegExp("\"", "g");
180   eachProperty(opts, function(k, v) {
181     attrs[k] = String(v).replace(quotestripper, """);
182   });
183   container.push(INPUT(attrs));
184 
185   // afterText
186   if (opts && opts.afterText) {
187     container.push(SPAN({className: 'surroundtext'}, opts.afterText));
188   }
189 
190   this.form.push(container);
191   return this;
192 };
193 
194 /**
195  * <p>Creates a rectangular, multi-line box for entering text.</p>
196  *
197  * <p>Supported options (all optional):</p>
198  * <ul>
199  * <li><strong>label:</strong> text to label the box with, defaults to id</li>
200  * <li><strong>rows:</strong> how many lines high to make the box</li>
201  * <li><strong>cols:</strong> how many characters wide to make the box</li>
202  * <li><strong>value:</strong> initial text for the text area</li>
203  * </ul>
204  *
205  * @param {string} id a unique id string, used as a parameter name for this field
206  * @param {object} [opts]  an optional dictionary of options
207  * @function
208  */
209 QuickForm.prototype.addInputTextArea = function(id, opts) {
210   // remember this name
211   this.inputNames.push(id);
212 
213   // label
214   var labelText = (opts && opts.label) ? opts.label : id;
215   this.form.push(DIV({className: 'qflabeldiv'},
216              LABEL({htmlFor: id,
217                className: 'qflabel'},
218                labelText)));
219 
220   // input
221   var rows = (opts && opts.rows) ? opts.rows : 10;
222   var cols = (opts && opts.cols) ? opts.cols : 40;
223   var value = (opts && opts.value) ? opts.value : '';
224   this.form.push(DIV({className: 'inputtextareadiv qffield'},
225              TEXTAREA({className: 'qfinputtextarea',
226                id: id,
227                name: id,
228                rows: rows,
229                cols: cols}, raw(value))));
230   return this;
231 }
232 
233 /**
234  * <p>Creates a control that allows the user to select a file for upload.
235  * This feature is particularly experimental.</p>
236  *
237  * <p>Supported options (all optional):</p>
238  * <ul>
239  * <li><strong>label:</strong> text to label the control with, defaults to id</li>
240  * </ul>
241  *
242  * @param {string} id a unique id string, used as a parameter name for this control
243  * @param {object} [opts]  an optional dictionary of options
244  * @function
245  */
246 QuickForm.prototype.addInputFile = function(id, opts) {
247   this.inputNames.push(id)
248 
249   var labelText = (opts && opts.label) ? opts.label : id
250   this.form.push(DIV({className: 'qflabeldiv'},
251              LABEL({htmlFor: id,
252                className: 'qflabel'},
253                labelText)));
254 
255   this.form.push(DIV({className: 'inputtextdiv qffield'},
256              INPUT({className: 'qfinputtext',
257                type: 'file',
258                name: id,
259                id: id})));
260   return this;
261 };
262 
263 /**
264  * Adds a hidden field to the form which does not affect display, but
265  * causes an additional parameter to be submitted with the form.
266  * @param {string} id a unique id string, used as a parameter name for
267  * this field
268  * @param {string} val the value of this parameter
269  * @function
270  */
271 QuickForm.prototype.addInputHidden = function(id, val) {
272   this.inputNames.push(id)
273 
274   this.form.push(INPUT({type: 'hidden', name: id, value: val}));
275   return this;
276 };
277 
278 // /**
279 //  * Creates a drop-down menu to the form that allows a user to select
280 //  * from several options.
281 //  * @param {string} id a unique id string, used as a parameter name for
282 //  * this field
283 //  * @param {iterable} base an object that supports the
284 //  * <code>forEach</code> method, and whose elements contain the
285 //  * properties for the option elements.
286 //  * @param {object} keys a dictionary with properties "value",
287 //  * "content", and "selected", that specify the property of
288 //  * <code>base</code>'s objects that should be used as the "value",
289 //  * menu text, and whether that menu item is selected, respecitvely.
290 //  * @param {object} [opts] an optional dictionary of options.
291 //  * @function
292 //  */
293 // QuickForm.prototype.addSelect = function(id, base, keys, opts) {
294 //   // label
295 //   var labelText = (opts && opts.label) ? opts.label : id;
296 //   this.form.push(DIV({className: 'qflabeldiv'},
297 //              LABEL({htmlFor: id,
298 //                className: 'qflabel'},
299 //                labelText)));
300 
301 //   // container
302 //   var container = DIV({className: 'selectdiv qffield'});
303 
304 //   // beforeText
305 //   if (opts && opts.beforeText) {
306 //     container.push(SPAN({className: 'surroundtext'}, opts.beforeText));
307 //   }
308   
309 //   // input
310 //   var attrs = {className: 'qfinputtext',
311 //            type: 'text',
312 //            name: id,
313 //            id: id};
314 //   var quotestripper = new RegExp("\"", "g");
315 //   eachProperty(opts, function(k, v) {
316 //     attrs[k] = String(v).replace(quotestripper, """);
317 //   });
318 //   var select = SELECT(attrs);
319 //   container.push(select);
320 
321 //   base.forEach(function(obj) {
322 //     var attrs = {name: id, 
323 //                  value: String(obj[keys.value]).replace(quotestripper, """)};
324 //     if (obj[keys.selected])
325 //       attrs.selected = "selected";
326 //     select.push(new OPTION(attrs, obj[keys.content]));
327 //   });
328 
329 //   // afterText
330 //   if (opts && opts.afterText) {
331 //     container.push(SPAN({className: 'surroundtext'}, opts.afterText));
332 //   }
333 
334 //   this.form.push(container);
335 //   return this;
336 // }
337   
338 
339 /**
340  * Adds a submit button to this form.
341  * @param {string} id a unique id string, used as a parameter name for this field
342  * @param {string} text the text on the button, and also the value of the parameter
343  * @function
344  */
345 QuickForm.prototype.addSubmit = function(id, text) {
346   this.form.push(DIV({className: 'qfsubmitdiv qffield'},
347              INPUT({type: 'submit',
348                name: id,
349                value: text,
350                className: 'qfsubmit'})));
351   return this;
352 };
353 
354 /**
355  * Does basic checking to determine whether the current request is a valid
356  * submission of this form.
357  *
358  * @function
359  * @return {boolean}
360  */
361 QuickForm.prototype.validate = function() {
362   // what to do here?
363   // eventually:
364   //    run validation functions on form input
365   //  X make sure request method matches form's method
366   //    if returns false, pre-fill existing values with stuff
367   //    make sure request path matches the form's action
368   //  X make sure all the input parameters are present
369   //
370   
371   if (request.method != this.method) {
372     return false;
373   }
374 
375   var hasparams = true;
376   this.inputNames.forEach(function(name) {
377       if (!request.param(name)) {
378     hasparams = false;
379       }
380     });
381   if (!hasparams) {
382     return false;
383   }
384 
385   return true;
386 };
387 
388 /**
389  * Assuming that the current request is a form submission of this QuickForm,
390  * assembles an object with a property for each form field.
391  * @function
392  * @return {object}
393  */
394 QuickForm.prototype.getInput = function() {
395   var input = {};
396   this.inputNames.forEach(function(n) {
397       input[n] = request.param(n);
398     });
399   return input;
400 };
401 
402 /**
403  * Converts this QuickForm to HTML mark-up.  This is called for you
404  * when you print a QuickForm.
405  * @function
406  * @return {string} html-formatted string.
407  */
408 QuickForm.prototype.toHTML = function() {
409   return this.form.toHTML();
410 };
411 
412 
413 
414 //----------------------------------------------------------------
415 // QuickButton
416 //----------------------------------------------------------------
417 
418 /**
419  * Creates a QuickButton, a self-contained button that when pressed
420  * submits a form that contains a bunch of parameter/value pairs.
421  *
422  * <p>Supported options (all optional):</p>
423  * <ul>
424  * <li><strong>method:</strong> "get" or "post" (defaults to "get")</li>
425  * <li><strong>action:</strong> path to submit to, defaults to current path</li>
426  * </ul>
427  *
428  * @param {string} label a label for the button
429  * @param {object} opts a dictionary of options
430  * @param {object} inputs a dictionary of parameter/value mappings to submit
431  * @constructor
432 */
433 function QuickButton(label, opts, inputs) {
434   var method = (opts && opts.method) ? opts.method : 'GET';
435   var action = (opts && opts.action) ? opts.action : request.path;
436   var form = FORM({method: method, action: action, style: 'display: inline;', className: 'quickbutton'});
437   
438   eachProperty(inputs, function(k, v) {
439     form.push(INPUT({type: 'hidden', name: k, value: v}));
440   });
441   form.push(INPUT({type: 'submit', value: label, style: 'display: inline;'}));
442   this.form = form;
443 }
444 
445 /**
446  * Converts this QuickButton to HTML mark-up.  This is called for you
447  * when you print a QuickButton.
448  * @function
449  * @return {string} html-formatted string.
450  */
451 QuickButton.prototype.toHTML = function() {
452   return this.form.toHTML();
453 };
454 
455 
456 
457