1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2012-2019 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 Ext.namespace("LABKEY","LABKEY.ext");
 20 
 21 
 22 /**
 23  * Constructs a new LabKey FormPanel using the supplied configuration.
 24  * @class <p><font color="red">DEPRECATED - </font> Consider using
 25  * <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.form.FormPanel">Ext.form.FormPanel</a> instead. </p>
 26  * <p>LabKey extension to the
 27  * <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.form.FormPanel">Ext.form.FormPanel</a>.
 28  * This class understands various LabKey metadata formats and can simplify generating basic forms.
 29  * When a LABKEY.ext.FormPanel is created with additional metadata, it will try to intelligently construct fields
 30  * of the appropriate type.</p>
 31  * <p>If you use any of the LabKey APIs that extend Ext APIs, you must either make your code open source or
 32  * <a href="https://www.labkey.org/Documentation/wiki-page.view?name=extDevelopment">purchase an Ext license</a>.</p>
 33  *            <p>Additional Documentation:
 34  *              <ul>
 35  *                  <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=javascriptTutorial">Tutorial: Create Applications with the JavaScript API</a></li>
 36  *              </ul>
 37  *           </p>
 38  * @constructor
 39  * @augments Ext.form.FormPanel
 40  * @param config Configuration properties. This may contain any of the configuration properties supported
 41  * by the <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.form.FormPanel">Ext.form.FormPanel</a>,
 42  * plus those listed here.
 43  * Also, items may specify a ToolTip config in the helpPopup property to display a LabKey-style "?" help tip.
 44  * Note that the selectRowsResults object (see {@link LABKEY.Query.SelectRowsResults}) includes both columnModel and metaData, so you don't need to specify all three.
 45  * @param {object} [config.metaData] as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}.
 46  * @param {object} [config.columnModel] as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}.
 47  * @param {LABKEY.Query.SelectRowsResults} [config.selectRowsResults] as returned by {@link LABKEY.Query.selectRows}.
 48  * @param {boolean} [config.addAllFields='false'] If true, all fields specified in the metaData are automatically created.
 49  * @param {object} [config.values] Provides initial values to populate the form.
 50  * @param {object} [config.errorEl] If specified, form errors will be written to this element; otherwise, a MsgBox will be used.
 51  * @param {string} [config.containerPath] Alternate default container for queries (e.g. for lookups)
 52  * @param {boolean} [config.lazyCreateStore] If false, any lookup stores will be created immediately.  If true, any lookup stores will be created when the component is created. (default true)
 53  *
 54  * @example
 55 <script type="text/javascript">
 56     function onSuccess(data) // e.g. callback from Query.selectRows
 57     {
 58         function submitHandler(formPanel)
 59         {
 60             var form = formPanel.getForm();
 61             if (form.isValid())
 62             {
 63                 var rows = formPanel.getFormValues();
 64                 save(rows);
 65             }
 66             else
 67             {
 68                 Ext.MessageBox.alert("Error Saving", "There are errors in the form.");
 69             }
 70         }
 71 
 72         function cancelHandler()
 73         {
 74             // Replace with real handler code
 75             Ext.MessageBox.alert("Cancelled", "The submission was cancelled.");
 76         }
 77 
 78         var formPanel = new LABKEY.ext.FormPanel(
 79         {
 80             selectRowsResults:data,
 81             addAllFields:true,
 82             buttons:[{text:"Submit", handler: function (b, e) { submitHandler(formPanel); }}, {text: "Cancel", handler: cancelHandler}],
 83             items:[{name:'myField', fieldLabel:'My Field', helpPopup:{title:'help', html:'read the manual'}}]
 84         });
 85         formPanel.render('formDiv');
 86     }
 87 </script>
 88 <div id='formDiv'/>
 89  */
 90 
 91 LABKEY.ext.FormPanel = Ext.extend(Ext.form.FormPanel,
 92 {
 93     constructor : function(config)
 94     {
 95         this.allFields = this.initFieldDefaults(config);
 96         if (Ext.isArray(config.items))
 97             config.items.push({ xtype: 'hidden', name: 'X-LABKEY-CSRF', value: LABKEY.CSRF });
 98         return LABKEY.ext.FormPanel.superclass.constructor.call(this, config);
 99     },
100 
101     defaultType : 'textfield',
102     allFields : [],
103 
104     initComponent : function()
105     {
106         LABKEY.ext.FormPanel.superclass.initComponent.call(this);
107         this.addEvents(
108             'beforeapplydefaults',
109             'applydefaults'
110         );
111 
112         // add all fields that we're not added explicitly
113         if (this.addAllFields && this.allFields.length)
114         {
115             // get a list of fields that were already constructed
116             var existing = {};
117             if (this.items)
118             {
119                 this.items.each(function(c)
120                 {
121                     if (c.isFormField)
122                     {
123                         var name = c.hiddenName || c.name;
124                         existing[name] = name;
125                     }
126                 });
127             }
128             for (var i=0;i<this.allFields.length;i++)
129             {
130                 var c = this.allFields[i];
131                 var name = c.hiddenName || c.name;
132                 // Don't render URL values as a separate input field
133                 if (!existing[name] && name.indexOf(LABKEY.Query.URL_COLUMN_PREFIX) != 0)
134                     this.add(c);
135             }
136         }
137     },
138 
139 
140     /* called from Ext.Container.initComponent() when adding items, before onRender() */
141     applyDefaults : function(c)
142     {
143         this.fireEvent('beforeapplydefaults', this, c);
144         if (this.fieldDefaults)
145         {
146             if (typeof c == 'string')
147             {
148             }
149             else if (!c.events)
150             {
151                 var name = c.name;
152                 if (name && this.fieldDefaults[name])
153                     Ext.applyIf(c, this.fieldDefaults[name]);
154             }
155             else
156             {
157             }
158         }
159         var applied = LABKEY.ext.FormPanel.superclass.applyDefaults.call(this, c);
160         this.fireEvent('applydefaults', this, applied);
161         return applied;
162     },
163 
164     /* gets called before doLayout() */
165     onRender : function(ct, position)
166     {
167         LABKEY.ext.FormPanel.superclass.onRender.call(this, ct, position);
168         this.el.addClass('extContainer');
169     },
170 
171     // private
172     initFieldDefaults : function(config)
173     {
174         var columnModel = config.columnModel;
175         var metaData = config.metaData;
176         var properties = config.properties;
177 
178         if (config.selectRowsResults)
179         {
180             if (!columnModel)
181                 columnModel = config.selectRowsResults.columnModel;
182             if (!metaData)
183                 config.metaData = config.selectRowsResults.metaData;
184             if (config.selectRowsResults.rowCount)
185                 config.values = config.selectRowsResults.rows;
186         }
187         var fields = config.metaData ? config.metaData.fields : null;
188 
189         var defaults = config.fieldDefaults = config.fieldDefaults || {};
190         var items = [], i;
191 
192         function findColumn(id) {
193             if (columnModel)
194                 for (var i = 0; i < columnModel.length; i++)
195                     if (columnModel[i].dataIndex == id)
196                         return columnModel[i];
197             return null;
198         }
199 
200         if (fields || properties)
201         {
202             var count = fields ? fields.length : properties.length;
203             for (i=0 ; i<count ; i++)
204             {
205                 var field = this.getFieldEditorConfig(
206                         {
207                             containerPath: (config.containerPath || LABKEY.container.path),
208                             lazyCreateStore: config.lazyCreateStore
209                         },
210                         fields?fields[i]:{},
211                         properties?properties[i]:{},
212                         columnModel?columnModel[i]:{}
213                         );
214                 var name = field.originalConfig.name;
215                 var d = defaults[name];
216                 defaults[name] = Ext.applyIf(defaults[name] || {}, field);
217 
218                 items.push({name:name});
219             }
220         }
221 
222         if (config.values)
223         {
224             if (!Ext.isArray(config.values))
225                 config.values = [ config.values ];
226 
227             var values = config.values;
228 
229             // UNDONE: primary keys should be readonly when editing multiple rows.
230 
231             var multiRowEdit = values.length > 1;
232             for (i = 0; i < values.length; i++)
233             {
234                 var vals = values[i];
235                 for (var id in vals)
236                 {
237                     if (!(id in defaults))
238                         defaults[id] = {};
239 
240                     // In multi-row edit case: skip if we've already discovered the values for this id are different across rows.
241                     if (multiRowEdit && defaults[id].allRowsSameValue === false)
242                         continue;
243 
244                     var v = vals[id];
245                     if (typeof v == 'function')
246                         continue;
247                     if (v && typeof v == 'object' && 'value' in v)
248                         v = v.value;
249 
250                     if ('xtype' in defaults[id] && defaults[id].xtype == 'checkbox')
251                     {
252                         var checked = v ? true : false;
253                         if (v == "false")
254                             v = false;
255                         if (multiRowEdit && i > 0 && v != defaults[id].checked)
256                         {
257                             defaults[id].checked = false;
258                             defaults[id].allRowsSameValue = false;
259 
260                             // UNDONE: Ext checkboxes don't have an 'unset' state
261                             // Don't require a value for this field.
262                             defaults[id].required = false;
263                             defaults[id].allowBlank = true;
264                         }
265                         else
266                             defaults[id].checked = v;
267                     }
268                     else
269                     {
270                         if (multiRowEdit && i > 0 && v != defaults[id].value)
271                         {
272                             defaults[id].value = undefined;
273                             defaults[id].allRowsSameValue = false;
274                             defaults[id].emptyText = "Selected rows have different values for this field.";
275 
276                             // Don't require a value for this field. Allows a '[none]' entry for ComboBox and empty text fields.
277                             defaults[id].required = false;
278                             defaults[id].allowBlank = true;
279                         }
280                         else
281                             defaults[id].value = v;
282                     }
283                 }
284             }
285         }
286 
287         return items;
288     },
289 
290     getFieldEditorConfig : function ()
291     {
292         return LABKEY.ext.FormHelper.getFieldEditorConfig.apply(null, arguments);
293     },
294 
295     // we want to hook error handling to provide a mechanism to show form errors (vs field errors)
296     // form errors are reported on imaginary field named "_form"
297     createForm : function()
298     {
299         var f = LABKEY.ext.FormPanel.superclass.createForm.call(this);
300         f.formPanel = this;
301         f.findField = function(id)
302         {
303             if (id == "_form")
304                 return this.formPanel;
305             return Ext.form.BasicForm.prototype.findField.call(this,id);
306         };
307         return f;
308     },
309 
310     // Look for form level errors that BasicForm won't handle
311     // CONSIDER: move implementation to getForm().markInvalid()
312     // CONSIDER: find 'unbound' errors and move to form level
313     markInvalid : function(errors)
314     {
315         var formMessage;
316 
317         if (typeof errors == "string")
318         {
319             formMessage = errors;
320             errors = null;
321         }
322         else if (Ext.isArray(errors))
323         {
324            for(var i = 0, len = errors.length; i < len; i++)
325            {
326                var fieldError = errors[i];
327                if (!("id" in fieldError) || "_form" == fieldError.id)
328                    formMessage = fieldError.msg;
329            }
330         }
331         else if (typeof errors == "object" && "_form" in errors)
332         {
333             formMessage = errors._form;
334         }
335 
336         if (errors)
337         {
338             this.getForm().markInvalid(errors);
339         }
340 
341         if (formMessage)
342         {
343             if (this.errorEl)
344                 Ext.get(this.errorEl).update(Ext.util.Format.htmlEncode(formMessage));
345             else
346                Ext.Msg.alert("Error", formMessage);
347         }
348     },
349 
350     /**
351      * Returns an Array of form value Objects.  The returned values will first be populated with
352      * with {@link #values} or {@link #selectRowsResponse.values} then with the form's values.
353      * If the form was not initially populated with {@link #values}, a signle element Array with
354      * just the form's values will be returned.
355      */
356     getFormValues : function ()
357     {
358         // First, get all dirty form field values
359         var fieldValues = this.getForm().getFieldValues(true);
360         for (var key in fieldValues)
361         {
362             if (typeof fieldValues[key] == "string")
363                 fieldValues[key] = fieldValues[key].trim();
364         }
365 
366         // 10887: Include checkboxes that weren't included in the call to .getFieldValues(true).
367         this.getForm().items.each(function (f) {
368             if (f instanceof Ext.form.Checkbox && !f.isDirty())
369             {
370                 var name = f.getName();
371                 var key = fieldValues[name];
372                 var val = f.getValue();
373                 if (Ext.isDefined(key)) {
374                     if (Ext.isArray(key)) {
375                         fieldValues[name].push(val);
376                     } else {
377                         fieldValues[name] = [key, val];
378                     }
379                 } else {
380                     fieldValues[name] = val;
381                 }
382             }
383         });
384 
385         // Finally, populate the data array with form values overriding the initial values.
386         var initialValues = this.initialConfig.values || [];
387         var len = initialValues.length || 1;
388         var result = [];
389         for (var i = 0; i < len; i++)
390         {
391             var data = {};
392             var initialVals = initialValues[i];
393             if (initialVals)
394             {
395                 for (var key in initialVals)
396                 {
397                     var v = initialVals[key];
398                     if (v && typeof v == 'object' && 'value' in v)
399                         v = v.value;
400                     data[key] = v;
401                 }
402             }
403             Ext.apply(data, fieldValues);
404             result.push(data);
405         }
406         return result;
407     }
408 });
409 
410 LABKEY.ext.FormHelper =
411 {
412     _textMeasure : null,
413 
414     /**
415      * Uses the given meta-data to generate a field config object.
416      *
417      * This function accepts a mish-mash of config parameters to be easily adapted to
418      * various different metadata formats.
419      *
420      * @param {string} [config.type] e.g. 'string','int','boolean','float', or 'date'
421      * @param {object} [config.editable]
422      * @param {object} [config.required]
423      * @param {string} [config.label] used to generate fieldLabel
424      * @param {string} [config.name] used to generate fieldLabel (if header is null)
425      * @param {string} [config.caption] used to generate fieldLabel (if label is null)
426      * @param {integer} [config.cols] if input is a textarea, sets the width (style:width is better)
427      * @param {integer} [config.rows] if input is a textarea, sets the height (style:height is better)
428      * @param {string} [config.lookup.schemaName] the schema used for the lookup.  schemaName also supported
429      * @param {string} [config.lookup.queryName] the query used for the lookup.  queryName also supported
430      * @param {Array} [config.lookup.columns] The columns used by the lookup store.  If not set, the <code>[keyColumn, displayColumn]</code> will be used.
431      * @param {string} [config.lookup.keyColumn]
432      * @param {string} [config.lookup.displayColumn]
433      * @param {string} [config.lookup.sort] The sort used by the lookup store.
434      * @param {boolean} [config.lookups] use lookups=false to prevent creating default combobox for lookup columns
435      * @param {object}  [config.ext] is a standard Ext config object that will be merged with the computed field config
436      *      e.g. ext:{width:120, tpl:new Ext.Template(...)}
437      * @param {object} [config.lookup.store] advanced! Pass in your own custom store for a lookup field
438      * @param {boolean} [config.lazyCreateStore] If false, the store will be created immediately.  If true, the store will be created when the component is created. (default true)
439      *
440      * Will accept multiple config parameters which will be combined.
441      *
442      * @private
443      */
444     getFieldEditorConfig: function(c)
445     {
446         /* CONSIDER for 10.3, new prototype (with backward compatibility check)
447          * getFieldEditorConfig(extConfig, [metadata,...])
448          */
449 
450         // Combine the metadata provided into one config object
451         var config = {editable:true, required:false, ext:{}};
452         for (var i=arguments.length-1 ; i>= 0 ; --i)
453         {
454             var ext = config.ext;
455             if (arguments[i])
456             {
457                 ext = Ext.apply(ext, arguments[i].ext);
458                 Ext.apply(config, arguments[i]);
459             }
460             config.ext = ext;
461         }
462 
463         var h = Ext.util.Format.htmlEncode;
464         var lc = function(s){return !s?s:Ext.util.Format.lowercase(s);};
465 
466         config.type = lc(config.jsonType) || lc(config.type) || lc(config.typeName) || 'string';
467 
468         var field =
469         {
470             //added 'caption' for assay support
471             fieldLabel: h(config.label) || h(config.caption) || h(config.header) || h(config.name),
472             name: config.name,
473             originalConfig: config,
474             allowBlank: !config.required,
475             disabled: !config.editable
476         };
477 
478         if (config.tooltip && !config.helpPopup)
479             field.helpPopup = config.tooltip;
480 
481         if (config.lookup && false !== config.lookups)
482         {
483             var l = config.lookup;
484             var store = config.lookup.store || config.store;
485             if (store && store == config.store)
486                 console.debug("use config.lookup.store");
487 
488             if (Ext.isObject(store) && store.events)
489                 field.store = store;
490             else
491                 field.store = LABKEY.ext.FormHelper.getLookupStoreConfig(config);
492 
493             if (field.store && config.lazyCreateStore === false)
494                 field.store = LABKEY.ext.FormHelper.getLookupStore(field);
495 
496             Ext.apply(field, {
497                 xtype: 'combo',
498                 forceSelection:true,
499                 typeAhead: false,
500                 hiddenName: config.name,
501                 hiddenId : (new Ext.Component()).getId(),
502                 triggerAction: 'all',
503                 displayField: l.displayColumn,
504                 valueField: l.keyColumn,
505                 tpl : '<tpl for="."><div class="x-combo-list-item">{[values["' + l.displayColumn + '"]]}</div></tpl>', //FIX: 5860
506                 listClass: 'labkey-grid-editor'
507             });
508         }
509         else if (config.hidden)
510         {
511             field.xtype = 'hidden';
512         }
513         else
514         {
515             switch (config.type)
516             {
517                 case "boolean":
518                     field.xtype = 'checkbox';
519                     break;
520                 case "int":
521                     field.xtype = 'numberfield';
522                     field.allowDecimals = false;
523                     break;
524                 case "float":
525                     field.xtype = 'numberfield';
526                     field.allowDecimals = true;
527                     break;
528                 case "date":
529                     field.xtype = 'datefield';
530                     field.format = Date.patterns.ISO8601Long;
531                     field.altFormats = LABKEY.Utils.getDateAltFormats();
532                     break;
533                 case "string":
534                     if (config.inputType == 'file')
535                     {
536                         field.xtype = 'textfield';
537                         field.inputType = 'file';
538                         break;
539                     }
540                     else if (config.inputType=='textarea')
541                     {
542                         field.xtype = 'textarea';
543                         field.width = 500;
544                         field.height = 60;
545                         if (!this._textMeasure)
546                         {
547                             this._textMeasure = {};
548                             var ta = Ext.DomHelper.append(document.body,{tag:'textarea', rows:10, cols:80, id:'_hiddenTextArea', style:{display:'none'}});
549                             this._textMeasure.height = Math.ceil(Ext.util.TextMetrics.measure(ta,"GgYyJjZ==").height * 1.2);
550                             this._textMeasure.width  = Math.ceil(Ext.util.TextMetrics.measure(ta,"ABCXYZ").width / 6.0);
551                         }
552                         if (config.rows)
553                         {
554                             if (config.rows == 1)
555                                 field.height = undefined;
556                             else
557                             {
558                                 // estimate at best!
559                                 var textHeight =  this._textMeasure.height * config.rows;
560                                 if (textHeight)
561                                     field.height = textHeight;
562                             }
563                         }
564                         if (config.cols)
565                         {
566                             var textWidth = this._textMeasure.width * config.cols;
567                             if (textWidth)
568                                 field.width = textWidth;
569                         }
570 
571                     }
572                     break;
573                 default:
574                     field.xtype = 'textfield';
575             }
576 
577         }
578 
579         if (config.ext)
580             Ext.apply(field,config.ext);
581         
582         // Ext.form.ComboBox defaults to mode=='remote', however, we often want to default to 'local'
583         // We don't want the combo cause a requery (see Combo.doQuery()) when we expect the store
584         // to be loaded exactly once.  Just treat like a local store in this case.
585         // NOTE: if the user over-rides the field.store, they may have to explicitly set the mode to 'remote', even
586         // though 'remote' is the Ext.form.ComboBox default
587         if (field.xtype == 'combo' && Ext.isDefined(field.store) && field.store.autoLoad && field.triggerAction != 'query' && !Ext.isDefined(field.mode))
588             field.mode = 'local';
589 
590         return field;
591     },
592 
593     /**
594      * same as getFieldEditorConfig, but actually constructs the editor
595      */
596     getFieldEditor : function(config, defaultType)
597     {
598         var field = LABKEY.ext.FormHelper.getFieldEditorConfig(config);
599         return Ext.ComponentMgr.create(field, defaultType || 'textfield');
600     },
601 
602     // private
603     getLookupStore : function(storeId, c)
604     {
605         if (typeof(storeId) != 'string')
606         {
607             c = storeId;
608             storeId = LABKEY.ext.FormHelper.getLookupStoreId(c);
609         }
610 
611         // Check if store has already been created.
612         if (Ext.isObject(c.store) && c.store.events)
613             return c.store;
614 
615         var store = Ext.StoreMgr.lookup(storeId);
616         if (!store)
617         {
618             var config = c.store || LABKEY.ext.FormHelper.getLookupStoreConfig(c);
619             config.storeId = storeId;
620             store = Ext.create(config, 'labkey-store');
621         }
622         return store;
623     },
624 
625     // private
626     // Ext.StoreMgr uses 'storeId' to lookup stores.  A store will add itself to the Ext.StoreMgr when constructed.
627     getLookupStoreId : function (c)
628     {
629         if (c.store && c.store.storeId)
630             return c.store.storeId;
631 
632         if (c.lookup)
633             return [c.lookup.schemaName || c.lookup.schema , c.lookup.queryName || c.lookup.table, c.lookup.keyColumn, c.lookup.displayColumn].join('||');
634 
635         return c.name;
636     },
637 
638     // private
639     getLookupStoreConfig : function(c)
640     {
641         // UNDONE: avoid self-joins
642         // UNDONE: core.UsersData
643         // UNDONE: container column
644         var l = c.lookup;
645         // normalize lookup
646         l.queryName = l.queryName || l.table;
647         l.schemaName = l.schemaName || l.schema;
648 
649         if (l.schemaName == 'core' && l.queryName =='UsersData')
650             l.queryName = 'Users';
651         
652         var config = {
653             xtype: "labkey-store",
654             storeId: LABKEY.ext.FormHelper.getLookupStoreId(c),
655             schemaName: l.schemaName,
656             queryName: l.queryName,
657             containerPath: l.container || l.containerPath || c.containerPath || LABKEY.container.path,
658             autoLoad: true
659         };
660 
661         if (l.viewName)
662             config.viewName = l.viewName;
663 
664         if (l.columns)
665             config.columns = l.columns;
666         else
667         {
668             var columns = [];
669             if (l.keyColumn)
670                 columns.push(l.keyColumn);
671             if (l.displayColumn && l.displayColumn != l.keyColumn)
672                 columns.push(l.displayColumn);
673             if (columns.length == 0)
674                 columns = ['*'];
675             config.columns = columns;
676         }
677 
678         if (l.sort)
679             config.sort = l.sort;
680         else
681             config.sort = l.displayColumn;
682         
683         if (!c.required)
684         {
685             config.nullRecord = {
686                 displayColumn: l.displayColumn,
687                 nullCaption: c.lookupNullCaption || "[none]"
688             };
689         }
690 
691         return config;
692     }
693 };
694 
695 
696 LABKEY.ext.Checkbox = Ext.extend(Ext.form.Checkbox,
697 {
698     onRender : function(ct, position)
699     {
700         LABKEY.ext.Checkbox.superclass.onRender.call(this, ct, position);
701         if (this.name)
702         {
703             var marker = LABKEY.fieldMarker + this.name;
704             Ext.DomHelper.insertAfter(this.el, {tag:"input", type:"hidden", name:marker});
705         }
706     }
707 });
708 
709 
710 LABKEY.ext.DatePicker = Ext.extend(Ext.DatePicker, { });
711 
712 
713 LABKEY.ext.DateField = Ext.extend(Ext.form.DateField,
714 {
715     onTriggerClick : function(){
716         if(this.disabled)
717         {
718             return;
719         }
720         if(this.menu == null)
721         {
722             this.menu = new Ext.menu.DateMenu({
723                 cls:'extContainer',     // NOTE change from super.onTriggerClick()
724                 hideOnClick: false,
725                 focusOnSelect: false
726             });
727         }
728         this.onFocus();
729         Ext.apply(this.menu.picker,
730         {
731             minDate : this.minValue,
732             maxDate : this.maxValue,
733             disabledDatesRE : this.disabledDatesRE,
734             disabledDatesText : this.disabledDatesText,
735             disabledDays : this.disabledDays,
736             disabledDaysText : this.disabledDaysText,
737             format : this.format,
738             showToday : this.showToday,
739             minText : String.format(this.minText, this.formatDate(this.minValue)),
740             maxText : String.format(this.maxText, this.formatDate(this.maxValue))
741         });
742         this.menu.picker.setValue(this.getValue() || new Date());
743         this.menu.show(this.el, "tl-bl?");
744         this.menuEvents('on');
745     }
746 });
747 
748 
749 LABKEY.ext.ComboPlugin = function () {
750     var combo = null;
751 
752     return {
753         init : function (combo) {
754             this.combo = combo;
755             if (this.combo.store)
756             {
757                 this.combo.mon(this.combo.store, {
758                     load: this.resizeList,
759                     // fired when the store is filtered or sorted
760                     //datachanged: this.resizeList,
761                     add: this.resizeList,
762                     remove: this.resizeList,
763                     update: this.resizeList,
764                     buffer: 100,
765                     scope: this
766                 });
767             }
768 
769             if (Ext.isObject(this.combo.store) && this.combo.store.events)
770             {
771                 this.combo.initialValue = this.combo.value;
772                 if (this.combo.store.getCount())
773                 {
774                     this.initialLoad();
775                     this.resizeList();
776                 }
777                 else
778                 {
779                     this.combo.mon(this.combo.store, 'load', this.initialLoad, this, {single: true});
780                 }
781             }
782         },
783 
784         initialLoad : function()
785         {
786             if (this.combo.initialValue)
787             {
788                 this.combo.setValue(this.combo.initialValue);
789             }
790         },
791 
792         resizeList : function ()
793         {
794             // bail early if ComboBox was set to an explicit width
795             if (Ext.isDefined(this.combo.listWidth))
796                 return;
797 
798             // CONSIDER: set maxListWidth or listWidth instead of calling .doResize(w) below?
799             var w = this.measureList();
800 
801             // NOTE: same as Ext.form.ComboBox.onResize except doesn't call super.
802             if(!isNaN(w) && this.combo.isVisible() && this.combo.list){
803                 this.combo.doResize(w);
804             }else{
805                 this.combo.bufferSize = w;
806             }
807         },
808 
809         measureList : function ()
810         {
811             if (!this.tm)
812             {
813                 // XXX: should we share a TextMetrics instance across ComboBoxen using a hidden span?
814                 var el = this.combo.el ? this.combo.el : Ext.DomHelper.append(document.body, {tag:'span', style:{display:'none'}});
815                 this.tm = Ext.util.TextMetrics.createInstance(el);
816             }
817 
818             var w = this.combo.el ? this.combo.el.getWidth(true) : 0;
819             var tpl = null;
820             if (this.combo.rendered)
821             {
822                 if (this.combo.view && this.combo.view.tpl instanceof Ext.Template)
823                     tpl = this.combo.view.tpl;
824                 else if (this.combo.tpl instanceof Ext.Template)
825                     tpl = this.combo.tpl;
826             }
827 
828             this.combo.store.each(function (r) {
829                 var html;
830                 if (tpl)
831                     html = tpl.apply(r.data);
832                 else
833                     html = r.get(this.combo.displayField);
834                 w = Math.max(w, Math.ceil(this.tm.getWidth(html)));
835             }, this);
836 
837             if (this.combo.list)
838                 w += this.combo.list.getFrameWidth('lr');
839 
840             // for vertical scrollbar
841             w += 20;
842 
843             return w;
844         }
845     }
846 };
847 Ext.preg('labkey-combo', LABKEY.ext.ComboPlugin);
848 
849 LABKEY.ext.ComboBox = Ext.extend(Ext.form.ComboBox, {
850     constructor: function (config) {
851         config.plugins = config.plugins || [];
852         config.plugins.push(LABKEY.ext.ComboPlugin);
853 
854         LABKEY.ext.ComboBox.superclass.constructor.call(this, config);
855     },
856 
857     initList : function () {
858         // Issue 18401: Customize view folder picker truncates long folder paths
859         // Add displayField as the qtip text for item that are likely to be truncated.
860         if (!this.tpl) {
861             var cls = 'x-combo-list';
862             this.tpl = '<tpl for="."><div ext:qtip="{[values[\'' + this.displayField + '\'] && values[\'' + this.displayField + '\'].length > 50 ? values[\'' + this.displayField + '\'] : \'\']}" class="'+cls+'-item">{' + this.displayField + ':htmlEncode}</div></tpl>';
863         }
864 
865         LABKEY.ext.ComboBox.superclass.initList.call(this);
866     }
867 });
868 
869 /**
870  * The following overwrite allows tooltips on labels within form layouts.
871  * The field have to be a property named "gtip" in the corresponding
872  * config object.
873  */
874 Ext.override(Ext.layout.FormLayout, {
875     setContainer: Ext.layout.FormLayout.prototype.setContainer.createSequence(function(ct) {
876         // the default field template used by all form layouts
877         var t = new Ext.Template(
878             '<div class="x-form-item {itemCls}" tabIndex="-1">',
879                 '<label for="{id}" style="{labelStyle}" class="x-form-item-label {guidedCls}"><span {guidedTip}>{label}{labelSeparator}</span></label>',
880                 '<div class="x-form-element" id="x-form-el-{id}" style="{elementStyle}">',
881                 '</div><div class="{clearCls}"></div>',
882             '</div>'
883         );
884         t.disableFormats = true;
885         t.compile();
886         Ext.layout.FormLayout.prototype.fieldTpl = t;
887     }),
888 
889     getTemplateArgs : function(field) {
890         var noLabelSep = !field.fieldLabel || field.hideLabel,
891                 itemCls = (field.itemCls || this.container.itemCls || '') + (field.hideLabel ? ' x-hide-label' : '');
892 
893         // IE9 quirks needs an extra, identifying class on wrappers of TextFields
894         if (Ext.isIE9 && Ext.isIEQuirks && field instanceof Ext.form.TextField) {
895             itemCls += ' x-input-wrapper';
896         }
897 
898         return {
899             id            : field.id,
900             label         : field.fieldLabel,
901             itemCls       : itemCls,
902             clearCls      : field.clearCls || 'x-form-clear-left',
903             labelStyle    : this.getLabelStyle(field.labelStyle),
904             elementStyle  : this.elementStyle||'',
905             labelSeparator: noLabelSep ? '' : (Ext.isDefined(field.labelSeparator) ? field.labelSeparator : this.labelSeparator),
906             guidedTip     : (field.gtip === undefined ? '' : ' ext:gtip="'+field.gtip+'"'),
907             guidedCls     : (field.gtip === undefined ? '' : 'g-tip-label')
908         };
909     }
910 });
911 
912 Ext.reg('checkbox', LABKEY.ext.Checkbox);
913 Ext.reg('combo', LABKEY.ext.ComboBox);
914 Ext.reg('datefield',  LABKEY.ext.DateField);
915 Ext.reg('labkey-form', LABKEY.ext.FormPanel);
916 
917