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  * @class Library for easy forms.
109  */
110 QuickForm = function(opts) {
111   this.method = (opts && opts.method) ? opts.method.toUpperCase() : 'POST';
112   this.action = (opts && opts.action) ? opts.action : request.path;
113   this.enctype = (opts && opts.enctype) ? opts.enctype :
114     "application/x-www-form-urlencoded";
115 
116   this.form = FORM({method: this.method, enctype: this.enctype,
117             action: this.action, className: 'quickform'});
118   this.inputNames = [];
119 };
120 
121 /**
122  * Adds a heading in bold type that introduces a section of the form.
123  *
124  * @param {string} [id] (optional) unique id string, used for CSS rules
125  * @param {string} text the text of the heading
126  * @function
127  */
128 QuickForm.prototype.addHeading = function(id, text) {
129   if (! text) {
130     text = id;
131     id = undefined;
132   }
133   var attribs = {className: 'qfheader'};
134   if (id) attribs.id = id;
135   this.form.push(DIV(attribs, text));
136   return this;
137 };
138 
139 /**
140  * <p>Creates a one-line text box for entering a small amount of text.</p>
141  *
142  * <p>Supported options (all optional):</p>
143  * <ul>
144  * <li><strong>label:</strong> text to label the field with, defaults to id</li>
145  * <li><strong>beforeText:</strong> text to put immediately before the input field</li>
146  * <li><strong>afterText:</strong> text to put immediately after the input field</li>
147  * <li><strong>value:</strong> initial text for the field</li>
148  * <li><strong>size:</strong> width of the field in characters</li>
149  * <li><strong>disabled:</strong> set to "disabled" to disable the field</li>
150  * </ul>
151  *
152  * @param {string} id a unique id string, used as a parameter name for this field
153  * @param {object} [opts]  an optional dictionary of options
154  * @function
155  */
156 QuickForm.prototype.addInputText = function(id, opts) {
157   // remember this name
158   this.inputNames.push(id);
159 
160   // label
161   var labelText = (opts && opts.label) ? opts.label : id;
162   this.form.push(DIV({className: 'qflabeldiv'},
163              LABEL({htmlFor: id,
164                className: 'qflabel'},
165                labelText)));
166 
167   // container
168   var container = DIV({className: 'inputtextdiv qffield'});
169 
170   // beforeText
171   if (opts && opts.beforeText) {
172     container.push(SPAN({className: 'surroundtext'}, opts.beforeText));
173   }
174   
175   // input
176   var attrs = {className: 'qfinputtext',
177            type: 'text',
178            name: id,
179            id: id};
180   var quotestripper = new RegExp("\"", "g");
181   eachProperty(opts, function(k, v) {
182     attrs[k] = String(v).replace(quotestripper, """);
183   });
184   container.push(INPUT(attrs));
185 
186   // afterText
187   if (opts && opts.afterText) {
188     container.push(SPAN({className: 'surroundtext'}, opts.afterText));
189   }
190 
191   this.form.push(container);
192   return this;
193 };
194 
195 /**
196  * <p>Creates a rectangular, multi-line box for entering text.</p>
197  *
198  * <p>Supported options (all optional):</p>
199  * <ul>
200  * <li><strong>label:</strong> text to label the box with, defaults to id</li>
201  * <li><strong>rows:</strong> how many lines high to make the box</li>
202  * <li><strong>cols:</strong> how many characters wide to make the box</li>
203  * <li><strong>value:</strong> initial text for the text area</li>
204  * </ul>
205  *
206  * @param {string} id a unique id string, used as a parameter name for this field
207  * @param {object} [opts]  an optional dictionary of options
208  * @function
209  */
210 QuickForm.prototype.addInputTextArea = function(id, opts) {
211   // remember this name
212   this.inputNames.push(id);
213 
214   // label
215   var labelText = (opts && opts.label) ? opts.label : id;
216   this.form.push(DIV({className: 'qflabeldiv'},
217              LABEL({htmlFor: id,
218                className: 'qflabel'},
219                labelText)));
220 
221   // input
222   var rows = (opts && opts.rows) ? opts.rows : 10;
223   var cols = (opts && opts.cols) ? opts.cols : 40;
224   var value = (opts && opts.value) ? opts.value : '';
225   this.form.push(DIV({className: 'inputtextareadiv qffield'},
226              TEXTAREA({className: 'qfinputtextarea',
227                id: id,
228                name: id,
229                rows: rows,
230                cols: cols}, raw(value))));
231   return this;
232 }
233 
234 /**
235  * <p>Creates a control that allows the user to select a file for upload.
236  * This feature is particularly experimental.</p>
237  *
238  * <p>Supported options (all optional):</p>
239  * <ul>
240  * <li><strong>label:</strong> text to label the control with, defaults to id</li>
241  * </ul>
242  *
243  * @param {string} id a unique id string, used as a parameter name for this control
244  * @param {object} [opts]  an optional dictionary of options
245  * @function
246  */
247 QuickForm.prototype.addInputFile = function(id, opts) {
248   this.inputNames.push(id)
249 
250   var labelText = (opts && opts.label) ? opts.label : id
251   this.form.push(DIV({className: 'qflabeldiv'},
252              LABEL({htmlFor: id,
253                className: 'qflabel'},
254                labelText)));
255 
256   this.form.push(DIV({className: 'inputtextdiv qffield'},
257              INPUT({className: 'qfinputtext',
258                type: 'file',
259                name: id,
260                id: id})));
261   return this;
262 };
263 
264 /**
265  * Adds a hidden field to the form which does not affect display, but
266  * causes an additional parameter to be submitted with the form.
267  * @param {string} id a unique id string, used as a parameter name for
268  * this field
269  * @param {string} val the value of this parameter
270  * @function
271  */
272 QuickForm.prototype.addInputHidden = function(id, val) {
273   this.inputNames.push(id)
274 
275   this.form.push(INPUT({type: 'hidden', name: id, value: val}));
276   return this;
277 };
278 
279 /**
280  * Creates a drop-down menu to the form that allows a user to select
281  * from several options.
282  * @param {string} id a unique id string, used as a parameter name for
283  * this field
284  * @param {iterable} base an object that supports the
285  * <code>forEach</code> method, and whose elements contain the
286  * properties for the option elements.
287  * @param {object} keys a dictionary with properties "value",
288  * "content", and "selected", that specify the property of
289  * <code>base</code>'s objects that should be used as the "value",
290  * menu text, and whether that menu item is selected, respecitvely.
291  * @param {object} [opts] an optional dictionary of options.
292  * @function
293  */
294 QuickForm.prototype.addSelect = function(id, base, keys, opts) {
295   // label
296   var labelText = (opts && opts.label) ? opts.label : id;
297   this.form.push(DIV({className: 'qflabeldiv'},
298              LABEL({htmlFor: id,
299                className: 'qflabel'},
300                labelText)));
301 
302   // container
303   var container = DIV({className: 'selectdiv qffield'});
304 
305   // beforeText
306   if (opts && opts.beforeText) {
307     container.push(SPAN({className: 'surroundtext'}, opts.beforeText));
308   }
309 
310   // input
311   var attrs = {className: 'qfinputtext',
312            type: 'text',
313            name: id,
314            id: id};
315   var quotestripper = new RegExp("\"", "g");
316   eachProperty(opts, function(k, v) {
317     attrs[k] = String(v).replace(quotestripper, """);
318   });
319   var select = SELECT(attrs);
320   container.push(select);
321 
322   base.forEach(function(obj) {
323     var attrs = {name: id, 
324                  value: String(obj[keys.value]).replace(quotestripper, """)};
325     if (obj[keys.selected])
326       attrs.selected = "selected";
327     select.push(new OPTION(attrs, obj[keys.content]));
328   });
329 
330   // afterText
331   if (opts && opts.afterText) {
332     container.push(SPAN({className: 'surroundtext'}, opts.afterText));
333   }
334 
335   this.form.push(container);
336   return this;
337 }
338   
339 
340 /**
341  * Adds a submit button to this form.
342  * @param {string} id a unique id string, used as a parameter name for this field
343  * @param {string} text the text on the button, and also the value of the parameter
344  * @function
345  */
346 QuickForm.prototype.addSubmit = function(id, text) {
347   this.form.push(DIV({className: 'qfsubmitdiv qffield'},
348              INPUT({type: 'submit',
349                name: id,
350                value: text,
351                className: 'qfsubmit'})));
352   return this;
353 };
354 
355 /**
356  * Does basic checking to determine whether the current request is a valid
357  * submission of this form.
358  *
359  * @function
360  * @return {boolean}
361  */
362 QuickForm.prototype.validate = function() {
363   // what to do here?
364   // eventually:
365   //    run validation functions on form input
366   //  X make sure request method matches form's method
367   //    if returns false, pre-fill existing values with stuff
368   //    make sure request path matches the form's action
369   //  X make sure all the input parameters are present
370   //
371   
372   if (request.method != this.method) {
373     return false;
374   }
375 
376   var hasparams = true;
377   this.inputNames.forEach(function(name) {
378       if (!request.param(name)) {
379     hasparams = false;
380       }
381     });
382   if (!hasparams) {
383     return false;
384   }
385 
386   return true;
387 };
388 
389 /**
390  * Assuming that the current request is a form submission of this QuickForm,
391  * assembles an object with a property for each form field.
392  * @function
393  * @return {object}
394  */
395 QuickForm.prototype.getInput = function() {
396   var input = {};
397   this.inputNames.forEach(function(n) {
398       input[n] = request.param(n);
399     });
400   return input;
401 };
402 
403 /**
404  * Converts this QuickForm to HTML mark-up.  This is called for you
405  * when you print a QuickForm.
406  * @function
407  * @return {string} html-formatted string.
408  */
409 QuickForm.prototype.toHTML = function() {
410   return this.form.toHTML();
411 };
412 
413 
414 
415 //----------------------------------------------------------------
416 // QuickButton
417 //----------------------------------------------------------------
418 
419 /**
420  * Creates a QuickButton, a self-contained button that when pressed
421  * submits a form that contains a bunch of parameter/value pairs.
422  *
423  * <p>Supported options (all optional):</p>
424  * <ul>
425  * <li><strong>method:</strong> "get" or "post" (defaults to "get")</li>
426  * <li><strong>action:</strong> path to submit to, defaults to current path</li>
427  * </ul>
428  *
429  * @param {string} label a label for the button
430  * @param {object} opts a dictionary of options
431  * @param {object} inputs a dictionary of parameter/value mappings to submit
432  * @class Creates a QuickButton, a self-contained button that when pressed submits a form that contains a bunch of parameter/value pairs.
433  * @constructor
434 */
435 function QuickButton(label, opts, inputs) {
436   var method = (opts && opts.method) ? opts.method : 'GET';
437   var action = (opts && opts.action) ? opts.action : request.path;
438   var form = FORM({method: method, action: action, style: 'display: inline;', className: 'quickbutton'});
439   
440   eachProperty(inputs, function(k, v) {
441     form.push(INPUT({type: 'hidden', name: k, value: v}));
442   });
443   form.push(INPUT({type: 'submit', value: label, style: 'display: inline;'}));
444   this.form = form;
445 }
446 
447 /**
448  * Converts this QuickButton to HTML mark-up.  This is called for you
449  * when you print a QuickButton.
450  * @function
451  * @return {string} html-formatted string.
452  */
453 QuickButton.prototype.toHTML = function() {
454   return this.form.toHTML();
455 };
456 
457 
458 
459