1 /* 2 * Copyright (c) 2012-2018 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 (function() { 7 8 /** 9 * @name LABKEY.ext4.Util 10 * @class 11 * Ext4 utilities, contains functions to return an Ext config object to create an Ext field based on 12 * the supplied metadata. 13 */ 14 Ext4.ns('LABKEY.ext4.Util'); 15 16 // TODO: Get these off the global 'Date' object 17 Ext4.ns('Date.patterns'); 18 Ext4.applyIf(Date.patterns, { 19 ISO8601Long:"Y-m-d H:i:s", 20 ISO8601Short:"Y-m-d" 21 }); 22 23 var warned = {}; 24 25 var Util = LABKEY.ext4.Util; 26 27 var caseInsensitiveEquals = function(a, b) { 28 a = String(a); 29 b = String(b); 30 return a.toLowerCase() == b.toLowerCase(); 31 }; 32 33 /** 34 * This method takes an object that is/extends an Ext4.Container (e.g. Panels, Toolbars, Viewports, Menus) and 35 * resizes it so the Container fits inside the its parent container. 36 * @param extContainer - (Required) outer container which is the target to be resized 37 * @param options - a set of options 38 * @param options.skipWidth - true to skip updating width, default false 39 * @param options.skipHeight - true to skip updating height, default false 40 * @param options.paddingWidth - total width padding 41 * @param options.paddingHeight - total height padding 42 * @param options.offsetY - distance between bottom of page to bottom of component 43 * @param options.overrideMinWidth - true to set ext container's minWidth value to calculated width, default false 44 */ 45 // resizeToContainer -- defined in Ext4/ext-patches.js 46 47 Ext4.apply(Util, { 48 49 /** 50 * A map to convert between jsonType and Ext type 51 */ 52 EXT_TYPE_MAP: { 53 'string': 'STRING', 54 'int': 'INT', 55 'float': 'FLOAT', 56 'date': 'DATE', 57 'boolean': 'BOOL' 58 }, 59 /** 60 * @lends LABKEY.ext4.Util 61 */ 62 63 /** 64 * @private 65 * @param config 66 */ 67 buildQtip: function(config) { 68 var qtip = []; 69 //NOTE: returned in the 9.1 API format 70 if(config.record && config.record.raw && config.record.raw[config.meta.name] && config.record.raw[config.meta.name].mvValue){ 71 var mvValue = config.record.raw[config.meta.name].mvValue; 72 73 //get corresponding message from qcInfo section of JSON and set up a qtip 74 if(config.record.store && config.record.store.reader.rawData && config.record.store.reader.rawData.qcInfo && config.record.store.reader.rawData.qcInfo[mvValue]) 75 { 76 qtip.push(config.record.store.reader.rawData.qcInfo[mvValue]); 77 config.cellMetaData.css = "labkey-mv"; 78 } 79 qtip.push(mvValue); 80 } 81 82 if (!config.record.isValid()){ 83 var errors = config.record.validate().getByField(config.meta.name); 84 if (errors.length){ 85 Ext4.Array.forEach(errors, function(e){ 86 qtip.push(e.message); 87 }, this); 88 } 89 } 90 91 if (config.meta.buildQtip) { 92 config.meta.buildQtip({ 93 qtip: config.qtip, 94 value: config.value, 95 cellMetaData: config.cellMetaData, 96 meta: config.meta, 97 record: config.record 98 }); 99 } 100 101 if (qtip.length) { 102 qtip = Ext4.Array.unique(qtip); 103 config.cellMetaData.tdAttr = config.cellMetaData.tdAttr || ''; 104 config.cellMetaData.tdAttr += " data-qtip=\"" + Ext4.util.Format.htmlEncode(qtip.join('<br>')) + "\""; 105 } 106 }, 107 108 /** 109 * @private 110 * @param store 111 * @param fieldName 112 */ 113 findFieldMetadata: function(store, fieldName) { 114 var fields = store.model.prototype.fields; 115 if(!fields) 116 return null; 117 118 return fields.get(fieldName); 119 }, 120 121 /** 122 * @private 123 * @param fieldObj 124 */ 125 findJsonType: function(fieldObj) { 126 var type = fieldObj.type || fieldObj.typeName; 127 128 if (type === 'DateTime') 129 return 'date'; 130 else if (type === 'Double') 131 return 'float'; 132 else if (type === 'Integer' || type === 'int') 133 return 'int'; 134 return 'string'; 135 }, 136 137 /** 138 * @private 139 * @param store 140 * @param col 141 * @param config 142 * @param grid 143 */ 144 getColumnConfig: function(store, col, config, grid) { 145 col = col || {}; 146 col.dataIndex = col.dataIndex || col.name; 147 col.header = col.header || col.caption || col.label || col.name; 148 149 var meta = Util.findFieldMetadata(store, col.dataIndex); 150 if(!meta){ 151 return; 152 } 153 154 col.customized = true; 155 156 col.hidden = meta.hidden; 157 col.format = meta.extFormat; 158 159 //this.updatable can override col.editable 160 col.editable = config.editable && col.editable && meta.userEditable; 161 162 if(col.editable && !col.editor) 163 col.editor = Util.getGridEditorConfig(meta); 164 165 col.renderer = Util.getDefaultRenderer(col, meta, grid); 166 167 //HTML-encode the column header 168 col.text = Ext4.util.Format.htmlEncode(meta.label || meta.name || col.header); 169 170 if(meta.ignoreColWidths) 171 delete col.width; 172 173 //allow override of defaults 174 if(meta.columnConfig) 175 Ext4.Object.merge(col, meta.columnConfig); 176 if(config && config[col.dataIndex]) 177 Ext4.Object.merge(col, config[col.dataIndex]); 178 179 return col; 180 }, 181 182 /** 183 * @private 184 * @param store 185 * @param grid 186 * @param config 187 */ 188 getColumnsConfig: function(store, grid, config) { 189 config = config || {}; 190 191 var fields = store.model.getFields(); 192 var columns = store.getColumns(); 193 var cols = []; 194 195 Ext4.each(fields, function(field) { 196 var col; 197 198 if (field.shownInGrid === false) 199 return; 200 201 for (var i=0;i<columns.length;i++){ 202 var c = columns[i]; 203 if (c.dataIndex == field.dataIndex) { 204 col = c; 205 break; 206 } 207 } 208 209 if (!col) 210 col = {dataIndex: field.dataIndex}; 211 212 //NOTE: In Ext4.1 if your store does not provide a key field, Ext will create a new column called 'id' 213 //this is somewhat of a problem, since it is difficult to differentiate this as automatically generated 214 var cfg = Util.getColumnConfig(store, col, config, grid); 215 if (cfg) 216 cols.push(cfg); 217 }, this); 218 219 return cols; 220 }, 221 222 /** 223 * @private 224 * @param displayValue 225 * @param value 226 * @param col 227 * @param meta 228 * @param record 229 */ 230 getColumnUrl: function(displayValue, value, col, meta, record) { 231 //wrap in <a> if url is present in the record's original JSON 232 var url; 233 if (meta.buildUrl) { 234 url = meta.buildUrl({ 235 displayValue: displayValue, 236 value: value, 237 col: col, 238 meta: meta, 239 record: record 240 }); 241 } 242 else if (record.raw && record.raw[meta.name] && record.raw[meta.name].url) { 243 url = record.raw[meta.name].url; 244 } 245 return Ext4.util.Format.htmlEncode(url); 246 }, 247 248 /** 249 * This is designed to be called through either .getFormEditorConfig or .getFormEditor. 250 * Uses the given meta-data to generate a field config object. 251 * 252 * This function accepts a collection of config parameters to be easily adapted to 253 * various different metadata formats. 254 * 255 * Note: you can provide any Ext config options using the editorConfig or formEditorConfig objects 256 * These config options can also be used to pass arbitrary config options used by your specific Ext component 257 * 258 * @param {string} [config.type] e.g. 'string','int','boolean','float', or 'date'. for consistency this will be translated into the property jsonType 259 * @param {object} [config.editable] 260 * @param {object} [config.required] 261 * @param {string} [config.label] used to generate fieldLabel 262 * @param {string} [config.name] used to generate fieldLabel (if header is null) 263 * @param {string} [config.caption] used to generate fieldLabel (if label is null) 264 * @param {integer} [config.cols] if input is a textarea, sets the width (style:width is better) 265 * @param {integer} [config.rows] if input is a textarea, sets the height (style:height is better) 266 * @param {string} [config.lookup.schemaName] the schema used for the lookup. schemaName also supported 267 * @param {string} [config.lookup.queryName] the query used for the lookup. queryName also supported 268 * @param {Array} [config.lookup.columns] The columns used by the lookup store. If not set, the <code>[keyColumn, displayColumn]</code> will be used. 269 * @param {string} [config.lookup.keyColumn] 270 * @param {string} [config.lookup.displayColumn] 271 * @param {string} [config.lookup.sort] The sort used by the lookup store. 272 * @param {boolean} [config.lookups] use lookups=false to prevent creating default combobox for lookup columns 273 * @param {object} [config.editorConfig] is a standard Ext config object (although it can contain any properties) that will be merged with the computed field config 274 * e.g. editorConfig:{width:120, tpl:new Ext.Template(...), arbitraryOtherProperty: 'this will be applied to the editor'} 275 * this will be merged will all form or grid editors 276 * @param {object} [config.formEditorConfig] Similar to editorConfig; however, it will only be merged when getFormEditor() or getFormEditorConfig() are called. 277 * The intention is to provide a mechanism so the same metadata object can be used to generate editors in both a form or a grid (or other contexts). 278 * @param {object} [config.gridEditorConfig] similar to formEditorConfig; however, it will only be merged when getGridEditor() or getGridEditorConfig() are called. 279 * @param {object} [config.columnConfig] similar to formEditorConfig; however, it will only be merged when getColumnConfig() is getColumnsConfig() called. 280 * @param {object} [config.lookup.store] advanced! Pass in your own custom store for a lookup field 281 * @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) 282 * @param {boolean} [config.createIfDoesNotExist] If true, this field will be created in the store, even if it does not otherwise exist on the server. Can be used to force custom fields to appear in a grid or form or to pass additional information to the server at time of import 283 * @param {function} [config.buildQtip] This function will be used to generate the qTip for the field when it appears in a grid instead of the default function. It will be passed a single object as an argument. This object has the following properties: qtip, data, cellMetaData, meta, record, store. Qtip is an array which will be merged to form the contents of the tooltip. Your code should modify the array to alter the tooltip. For example: 284 * buildQtip: function(config){ 285 * qtip.push('I have a tooltip!'); 286 * qtip.push('This is my value: ' + config.value); 287 * } 288 * @param {function} [config.buildDisplayString] This function will be used to generate the display string for the field when it appears in a grid instead of the default function. It will be passed the same argument as buildQtip() 289 * @param {function} [config.buildUrl] This function will be used to generate the URL encapsulating the field 290 * @param {string} [config.urlTarget] If the value is rendered in a LABKEY.ext4.EditorGridPanel (or any other component using this pathway), and it contains a URL, this will be used as the target of <a> tag. For example, use _blank for a new window. 291 * @param {boolean} [config.setValueOnLoad] If true, the store will attempt to set a value for this field on load. This is determined by the defaultValue or getInitialValue function, if either is defined 292 * @param {function} [config.getInitialValue] When a new record is added to this store, this function will be called on that field. If setValueOnLoad is true, this will also occur on load. It will be passed the record and metadata. The advantage of using a function over defaultValue is that more complex and dynamic initial values can be created. For example: 293 * //sets the value to the current date 294 * getInitialValue(val, rec, meta){ 295 * return val || new Date() 296 * } 297 * @param {boolean} [config.wordWrap] If true, when displayed in an Ext grid the contents of the cell will use word wrapping, as opposed to being forced to a single line 298 * 299 * Note: the follow Ext params are automatically defined based on the specified Labkey metadata property: 300 * dataIndex -> name 301 * editable -> userEditable && readOnly 302 * header -> caption 303 * xtype -> set within getDefaultEditorConfig() based on jsonType, unless otherwise provided 304 */ 305 getDefaultEditorConfig: function(meta) { 306 var field = { 307 //added 'caption' for assay support 308 fieldLabel : Ext4.util.Format.htmlEncode(meta.label || meta.caption || meta.caption || meta.header || meta.name), 309 originalConfig : meta, 310 //we assume the store's translateMeta() will handle this 311 allowBlank : (meta.allowBlank === true) || (meta.required !==true), 312 //disabled: meta.editable===false, 313 name : meta.name, 314 dataIndex : meta.dataIndex || meta.name, 315 value : meta.value || meta.defaultValue, 316 width : meta.width, 317 height : meta.height, 318 msgTarget : 'qtip', 319 validateOnChange : true 320 }; 321 322 var helpPopup = meta.helpPopup || (function() { 323 var array = []; 324 325 if (meta.friendlyType) 326 if (!(meta.lookup && meta.lookup['public'] !== false && meta.lookups !== false)) 327 array.push(meta.friendlyType); 328 329 if (meta.description) 330 array.push(Ext4.util.Format.htmlEncode(meta.description)); 331 332 if (!field.allowBlank) 333 array.push("This field is required."); 334 335 return array; 336 }()); 337 338 if (Ext4.isArray(helpPopup)) 339 helpPopup = helpPopup.join('<br>'); 340 field.helpPopup = helpPopup; 341 342 if (meta.hidden) { 343 field.xtype = 'hidden'; 344 field.hidden = true; 345 } 346 else if (meta.editable === false) { 347 field.xtype = 'displayfield'; 348 } 349 else if (meta.lookup && meta.lookup['public'] !== false && meta.lookups !== false && meta.facetingBehaviorType != 'ALWAYS_OFF') { 350 var l = meta.lookup; 351 352 //test whether the store has been created. create if necessary 353 if (Ext4.isObject(meta.store) && meta.store.events) { 354 field.store = meta.store; 355 } 356 else { 357 field.store = Util.getLookupStore(meta); 358 } 359 360 Ext4.apply(field, { 361 // the purpose of this is to allow other editors like multiselect, checkboxGroup, etc. 362 xtype : (meta.xtype || 'labkey-combo'), 363 forceSelection : true, 364 typeAhead : true, 365 queryMode : 'local', 366 displayField : l.displayColumn, 367 valueField : l.keyColumn, 368 //NOTE: supported for non-combo components 369 initialValue : field.value, 370 showValueInList : meta.showValueInList, 371 nullCaption : meta.nullCaption 372 }); 373 } 374 else { 375 switch (meta.jsonType) { 376 case "boolean": 377 field.xtype = meta.xtype || 'checkbox'; 378 if (field.value === true){ 379 field.checked = true; 380 } 381 break; 382 case "int": 383 field.xtype = meta.xtype || 'numberfield'; 384 field.allowDecimals = false; 385 break; 386 case "float": 387 field.xtype = meta.xtype || 'numberfield'; 388 field.allowDecimals = true; 389 break; 390 case "date": 391 field.xtype = meta.xtype || 'datefield'; 392 field.format = meta.extFormat || Date.patterns.ISO8601Long; 393 field.altFormats = LABKEY.Utils.getDateAltFormats(); 394 break; 395 case "string": 396 if (meta.inputType=='textarea') { 397 field.xtype = meta.xtype || 'textarea'; 398 field.width = meta.width; 399 field.height = meta.height; 400 if (!this._textMeasure) { 401 this._textMeasure = {}; 402 var ta = Ext4.DomHelper.append(document.body,{tag:'textarea', rows:10, cols:80, id:'_hiddenTextArea', style:{display:'none'}}); 403 this._textMeasure.height = Math.ceil(Ext4.util.TextMetrics.measure(ta,"GgYyJjZ==").height * 1.2); 404 this._textMeasure.width = Math.ceil(Ext4.util.TextMetrics.measure(ta,"ABCXYZ").width / 6.0); 405 } 406 if (meta.rows && !meta.height) { 407 if (meta.rows == 1) { 408 field.height = undefined; 409 } 410 else { 411 // estimate at best! 412 var textHeight = this._textMeasure.height * meta.rows; 413 if (textHeight) { 414 field.height = textHeight; 415 } 416 } 417 } 418 if (meta.cols && !meta.width) { 419 var textWidth = this._textMeasure.width * meta.cols; 420 if (textWidth) { 421 field.width = textWidth; 422 } 423 } 424 } 425 else { 426 field.xtype = meta.xtype || 'textfield'; 427 } 428 break; 429 default: 430 field.xtype = meta.xtype || 'textfield'; 431 } 432 } 433 434 return field; 435 }, 436 437 /** 438 * @private 439 * @param col 440 * @param meta 441 * @param grid 442 */ 443 getDefaultRenderer: function(col, meta, grid) { 444 return function(value, cellMetaData, record, rowIndex, colIndex, store) { 445 var displayValue = value; 446 var cellStyles = []; 447 var tdCls = []; 448 449 //format value into a string 450 if(!Ext4.isEmpty(value)) 451 displayValue = Util.getDisplayString(value, meta, record, store); 452 else 453 displayValue = value; 454 455 displayValue = Ext4.util.Format.htmlEncode(displayValue); 456 457 if(meta.buildDisplayString){ 458 displayValue = meta.buildDisplayString({ 459 displayValue: displayValue, 460 value: value, 461 col: col, 462 meta: meta, 463 cellMetaData: cellMetaData, 464 record: record, 465 store: store 466 }); 467 } 468 469 //if meta.file is true, add an <img> for the file icon 470 if (meta.file) { 471 displayValue = "<img src=\"" + LABKEY.Utils.getFileIconUrl(value) + "\" alt=\"icon\" title=\"Click to download file\"/> " + displayValue; 472 //since the icons are 16x16, cut the default padding down to just 1px 473 cellStyles.push('padding: 1px 1px 1px 1px'); 474 } 475 476 //build the URL 477 if(col.showLink !== false){ 478 var url = Util.getColumnUrl(displayValue, value, col, meta, record); 479 if(url){ 480 displayValue = "<a " + (meta.urlTarget ? "target=\""+meta.urlTarget+"\"" : "") + " href=\"" + url + "\">" + displayValue + "</a>"; 481 } 482 } 483 484 if(meta.wordWrap && !col.hidden){ 485 cellStyles.push('white-space:normal !important'); 486 } 487 488 489 if (!record.isValid()){ 490 var errs = record.validate().getByField(meta.name); 491 if (errs.length) 492 tdCls.push('labkey-grid-cell-invalid'); 493 } 494 495 if ((meta.allowBlank === false || meta.nullable === false) && Ext4.isEmpty(value)){ 496 tdCls.push('labkey-grid-cell-invalid'); 497 } 498 499 if(cellStyles.length){ 500 cellMetaData.style = cellMetaData.style ? cellMetaData.style + ';' : ''; 501 cellMetaData.style += (cellStyles.join(';')); 502 } 503 504 if (tdCls.length){ 505 cellMetaData.tdCls = cellMetaData.tdCls ? cellMetaData.tdCls + ' ' : ''; 506 cellMetaData.tdCls += tdCls.join(' '); 507 } 508 509 if (!meta.hasOwnProperty('showTooltip') || meta.showTooltip === true) { 510 Util.buildQtip({ 511 displayValue: displayValue, 512 value: value, 513 meta: meta, 514 col: col, 515 record: record, 516 store: store, 517 cellMetaData: cellMetaData 518 }); 519 } 520 521 return displayValue; 522 } 523 }, 524 525 /** 526 * @private 527 * @param value 528 * @param meta 529 * @param record 530 * @param store 531 */ 532 getDisplayString: function(value, meta, record, store) { 533 var displayType = meta.displayFieldJsonType || meta.jsonType; 534 var displayValue = value; 535 var shouldCache; 536 537 //NOTE: the labkey 9.1 API returns both the value of the field and the display value 538 //the server is already doing the work, so we should rely on this 539 //this does have a few problems: 540 //if the displayValue equals the value, the API omits displayValue. because we cant 541 // count on the server returning the right value unless explicitly providing a displayValue, 542 // we only attempt to use that 543 if(record && record.raw && record.raw[meta.name]){ 544 if(Ext4.isDefined(record.raw[meta.name].displayValue)) 545 return record.raw[meta.name].displayValue; 546 // TODO: this needs testing before enabling. would be nice if we could rely on this, 547 // TODO: but i dont think we will be able to (dates, for example) 548 // perhaps only try this for lookups? 549 //else if(Ext4.isDefined(record.raw[meta.name].value)) 550 // return record.raw[meta.name].value; 551 } 552 553 //NOTE: this is substantially changed over LABKEY.ext.FormHelper 554 if(meta.lookup && meta.lookup['public'] !== false && meta.lookups!==false){ 555 //dont both w/ special renderer if the raw value is the same as the displayColumn 556 if (meta.lookup.keyColumn != meta.lookup.displayColumn){ 557 displayValue = Util.getLookupDisplayValue(meta, displayValue, record, store); 558 meta.usingLookup = true; 559 shouldCache = false; 560 displayType = 'string'; 561 } 562 } 563 564 if(meta.extFormatFn && Ext4.isFunction(meta.extFormatFn)){ 565 displayValue = meta.extFormatFn(displayValue); 566 } 567 else { 568 if(!Ext4.isDefined(displayValue)) 569 displayValue = ''; 570 switch (displayType){ 571 case "date": 572 var date = new Date(displayValue); 573 //NOTE: java formats differ from ext 574 var format = meta.extFormat; 575 if(!format){ 576 if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0) 577 format = "Y-m-d"; 578 else 579 format = "Y-m-d H:i:s"; 580 } 581 displayValue = Ext4.Date.format(date, format); 582 break; 583 case "int": 584 displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0'))(displayValue); 585 break; 586 case "boolean": 587 588 var t = meta.editorConfig && meta.editorConfig.trueText ? meta.editorConfig.trueText : (this.trueText || 'true'); 589 var f = meta.editorConfig && meta.editorConfig.falseText ? meta.editorConfig.falseText : (this.falseText || 'false'); 590 var u = meta.editorConfig && meta.editorConfig.undefinedText ? meta.editorConfig.undefinedText : (this.undefinedText || ' '); 591 592 if(displayValue === undefined){ 593 displayValue = u; 594 } 595 else if(!displayValue || displayValue === 'false'){ 596 displayValue = f; 597 } 598 else { 599 displayValue = t; 600 } 601 break; 602 case "float": 603 displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0,000.00'))(displayValue); 604 break; 605 case "string": 606 default: 607 displayValue = !Ext4.isEmpty(displayValue) ? displayValue.toString() : ""; 608 } 609 } 610 611 // Experimental. cache the calculated value, so we dont need to recalculate each time. 612 // This should get cleared by the store on update like any server-generated value 613 if (shouldCache !== false) { 614 record.raw = record.raw || {}; 615 if(!record.raw[meta.name]) 616 record.raw[meta.name] = {}; 617 record.raw[meta.name].displayValue = displayValue; 618 } 619 620 return displayValue; 621 }, 622 623 /** 624 * Constructs an ext field component based on the supplied metadata. Same as getFormEditorConfig, but actually constructs the editor. 625 * The resulting editor is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata 626 * contains a formEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 627 * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 628 * @param {Object} config as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 629 * @return {Object} An Ext field component 630 */ 631 getFormEditor: function(meta, config) { 632 var editorConfig = Util.getFormEditorConfig(meta, config); 633 return Ext4.ComponentMgr.create(editorConfig); 634 }, 635 636 /** 637 * Return an Ext config object to create an Ext field based on the supplied metadata. 638 * The resulting config object is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata 639 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 640 * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 641 * @param {Object} [config] as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 642 * @returns {Object} An Ext 4.x config object 643 */ 644 getFormEditorConfig: function(meta, config) { 645 var editor = Util.getDefaultEditorConfig(meta); 646 647 // now we allow overrides of default behavior, in order of precedence 648 if (meta.editorConfig) 649 Ext4.Object.merge(editor, meta.editorConfig); 650 if (meta.formEditorConfig) 651 Ext4.Object.merge(editor, meta.formEditorConfig); 652 if (config) 653 Ext4.Object.merge(editor, config); 654 655 return editor; 656 }, 657 658 /** 659 * Constructs an ext field component based on the supplied metadata. Same as getFormEditorConfig, but actually constructs the editor. 660 * The resulting editor is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata 661 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 662 * @private 663 * @param meta 664 * @param config 665 * @return {Object} An Ext field component 666 */ 667 getGridEditor: function(meta, config) { 668 var editorConfig = Util.getGridEditorConfig(meta, config); 669 return Ext4.ComponentMgr.create(editorConfig); 670 }, 671 672 /** 673 * @private 674 * Return an Ext config object to create an Ext field based on the supplied metadata. 675 * The resulting config object is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata 676 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 677 * 678 * @name getGridEditorConfig 679 * @function 680 * @returns {object} Returns an Ext config object 681 */ 682 getGridEditorConfig: function(meta, config) { 683 //this produces a generic editor 684 var editor = Util.getDefaultEditorConfig(meta); 685 686 //now we allow overrides of default behavior, in order of precedence 687 if (meta.editorConfig) { 688 Ext4.Object.merge(editor, meta.editorConfig); 689 } 690 691 //note: this will screw up cell editors 692 delete editor.fieldLabel; 693 694 if (meta.gridEditorConfig) { 695 Ext4.Object.merge(editor, meta.gridEditorConfig); 696 } 697 if (config) { 698 Ext4.Object.merge(editor, config); 699 } 700 701 return editor; 702 }, 703 704 /** 705 * @private 706 * NOTE: it would be far better if we did not need to pass the store. This is done b/c we need to fire the 707 * 'datachanged' event once the lookup store loads. A better idea would be to force the store/grid to listen 708 * for event fired by the lookupStore or somehow get the metadata to fire events itself. 709 * @param meta 710 * @param data 711 * @param record 712 * @param store 713 */ 714 getLookupDisplayValue: function(meta, data, record, store) { 715 var lookupStore = Util.getLookupStore(meta); 716 if(!lookupStore){ 717 return ''; 718 } 719 720 meta.lookupStore = lookupStore; 721 var lookupRecord; 722 723 //NOTE: preferentially used snapshot instead of data to allow us to find the record even if the store is currently filtered 724 var records = lookupStore.snapshot || lookupStore.data; 725 var matcher = records.createValueMatcher((data == null ? '' : data), false, true, true); 726 var property = meta.lookup.keyColumn; 727 var recIdx = records.findIndexBy(function(o){ 728 return o && matcher.test(o.get(property)); 729 }, null); 730 731 if (recIdx != -1) 732 lookupRecord = records.getAt(recIdx); 733 734 if (lookupRecord){ 735 return lookupRecord.get(meta.lookup.displayColumn); 736 } 737 else { 738 if (data!==null){ 739 return "[" + data + "]"; 740 } 741 else { 742 return Ext4.isDefined(meta.nullCaption) ? meta.nullCaption : "[none]"; 743 } 744 } 745 }, 746 747 /** 748 * @private 749 * @param storeId 750 * @param c 751 */ 752 getLookupStore: function(storeId, c) { 753 if (!Ext4.isString(storeId)) { 754 c = storeId; 755 storeId = Util.getLookupStoreId(c); 756 } 757 758 if (Ext4.isObject(c.store) && c.store.events) { 759 return c.store; 760 } 761 762 var store = Ext4.StoreMgr.lookup(storeId); 763 if (!store) { 764 var config = c.store || Util.getLookupStoreConfig(c); 765 config.storeId = storeId; 766 store = Ext4.create('LABKEY.ext4.data.Store', config); 767 } 768 return store; 769 }, 770 771 /** 772 * @private 773 * @param c 774 */ 775 getLookupStoreConfig: function(c) { 776 var l = c.lookup; 777 778 // normalize lookup 779 l.queryName = l.queryName || l.table; 780 l.schemaName = l.schemaName || l.schema; 781 782 if (l.schemaName == 'core' && l.queryName =='UsersData') { 783 l.queryName = 'Users'; 784 } 785 786 var config = { 787 xtype: "labkeystore", 788 storeId: Util.getLookupStoreId(c), 789 containerFilter: 'CurrentOrParentAndWorkbooks', 790 schemaName: l.schemaName, 791 queryName: l.queryName, 792 containerPath: l.container || l.containerPath || LABKEY.container.path, 793 autoLoad: true 794 }; 795 796 if (l.viewName) { 797 config.viewName = l.viewName; 798 } 799 800 if (l.filterArray) { 801 config.filterArray = l.filterArray; 802 } 803 804 if (l.columns) { 805 config.columns = l.columns; 806 } 807 else { 808 var columns = []; 809 if (l.keyColumn) { 810 columns.push(l.keyColumn); 811 } 812 if (l.displayColumn && l.displayColumn != l.keyColumn) { 813 columns.push(l.displayColumn); 814 } 815 if (columns.length == 0) { 816 columns = ['*']; 817 } 818 config.columns = columns; 819 } 820 821 if (l.sort) { 822 config.sort = l.sort; 823 } 824 else if (l.sort !== false) { 825 config.sort = l.displayColumn; 826 } 827 828 return config; 829 }, 830 831 /** 832 * @private 833 * @param c 834 */ 835 getLookupStoreId: function(c) { 836 if (c.store && c.store.storeId) { 837 return c.store.storeId; 838 } 839 840 if (c.lookup) { 841 return c.lookup.storeId || [ 842 c.lookup.schemaName || c.lookup.schema, 843 c.lookup.queryName || c.lookup.table, 844 c.lookup.keyColumn, 845 c.lookup.displayColumn 846 ].join('||'); 847 } 848 849 return c.name; 850 }, 851 852 /** 853 * @private 854 * EXPERIMENTAL. Returns the fields from the passed store 855 * @param store 856 * @returns {Ext.util.MixedCollection} The fields associated with this store 857 */ 858 getStoreFields: function(store) { 859 return store.proxy.reader.model.prototype.fields; 860 }, 861 862 /** 863 * @private 864 * @param store 865 * @return {boolean} Whether the store has loaded 866 */ 867 hasStoreLoaded: function(store) { 868 return store.proxy && store.proxy.reader && store.proxy.reader.rawData; 869 }, 870 871 /** 872 * @private 873 * Identify the proper name of a field using an input string such as an excel column label. This helper will 874 * perform a case-insensitive comparison of the field name, label, caption, shortCaption and aliases. 875 * @param {string} fieldName The string to search 876 * @param {Array/Ext.util.MixedCollection} metadata The fields to search 877 * @return {string} The normalized field name or null if not found 878 */ 879 resolveFieldNameFromLabel: function(fieldName, meta) { 880 var fnMatch = []; 881 var aliasMatch = []; 882 883 var testField = function(fieldMeta) { 884 if (caseInsensitiveEquals(fieldName, fieldMeta.name) 885 || caseInsensitiveEquals(fieldName, fieldMeta.caption) 886 || caseInsensitiveEquals(fieldName, fieldMeta.shortCaption) 887 || caseInsensitiveEquals(fieldName, fieldMeta.label) 888 ){ 889 fnMatch.push(fieldMeta.name); 890 return false; //exit here because it should only match 1 name 891 } 892 893 if (fieldMeta.importAliases) { 894 var aliases; 895 if(Ext4.isArray(fieldMeta.importAliases)) 896 aliases = fieldMeta.importAliases; 897 else 898 aliases = fieldMeta.importAliases.split(','); 899 900 Ext4.each(aliases, function(alias){ 901 if (caseInsensitiveEquals(fieldName, alias)) 902 aliasMatch.push(fieldMeta.name); //continue iterating over fields in case a fieldName matches 903 }, this); 904 } 905 }; 906 907 if (meta.hasOwnProperty('each')) { 908 meta.each(testField, this); 909 } 910 else { 911 Ext4.each(meta, testField, this); 912 } 913 914 if (fnMatch.length==1) { 915 return fnMatch[0]; 916 } 917 else if (fnMatch.length > 1) { 918 return null; 919 } 920 else if (aliasMatch.length==1) { 921 return aliasMatch[0]; 922 } 923 return null; 924 }, 925 926 /** 927 * @private 928 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in a details view. 929 * If any of the following are true, it will not appear: hidden, isHidden 930 * If shownInDetailsView is defined, it will take priority 931 * @param {Object} metadata The field metadata object 932 * @return {boolean} Whether the field should appear in the default details view 933 */ 934 shouldShowInDetailsView: function(metadata){ 935 return Ext4.isDefined(metadata.shownInDetailsView) ? metadata.shownInDetailsView : 936 (!metadata.isHidden && !metadata.hidden && metadata.shownInDetailsView!==false); 937 }, 938 939 /** 940 * @private 941 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in an insert view. 942 * If any of the following are false, it will not appear: userEditable and autoIncrement 943 * If any of the follow are true, it will not appear: hidden, isHidden 944 * If shownInInsertView is defined, this will take priority over all 945 * @param {Object} metadata The field metadata object 946 * @return {boolean} Whether the field should appear in the default insert view 947 */ 948 shouldShowInInsertView: function(metadata){ 949 return Ext4.isDefined(metadata.shownInInsertView) ? metadata.shownInInsertView : 950 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement); 951 }, 952 953 /** 954 * @private 955 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in an update view. 956 * If any of the following are false, it will not appear: userEditable and autoIncrement 957 * If any of the follow are true, it will not appear: hidden, isHidden, readOnly 958 * If shownInUpdateView is defined, this will take priority over all 959 * @param {Object} metadata The field metadata object 960 * @return {boolean} Whether the field should appear 961 */ 962 shouldShowInUpdateView: function(metadata) { 963 return Ext4.isDefined(metadata.shownInUpdateView) ? metadata.shownInUpdateView : 964 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement && metadata.readOnly!==false) 965 }, 966 967 /** 968 * @private 969 * Shortcut for LABKEY.ext4.Util.getLookupStore that doesn't require as complex a config object 970 * @param {Object} config Configuration object for an Ext.data.Store 971 * @return {Ext.data.Store} The store 972 */ 973 simpleLookupStore: function(config) { 974 config.lookup = { 975 containerPath : config.containerPath, 976 schemaName : config.schemaName, 977 queryName : config.queryName, 978 viewName : config.viewName, 979 displayColumn : config.displayColumn, 980 keyColumn : config.keyColumn 981 }; 982 983 return Util.getLookupStore(config); 984 }, 985 986 /** 987 * @private 988 * The intention of this method is to provide a standard, low-level way to translating Labkey metadata names into ext ones. 989 * @param field 990 */ 991 translateMetadata: function(field) { 992 field.fieldLabel = Ext4.util.Format.htmlEncode(field.label || field.caption || field.header || field.name); 993 field.dataIndex = field.dataIndex || field.name; 994 field.editable = (field.userEditable!==false && !field.readOnly && !field.autoIncrement && !field.calculated); 995 field.allowBlank = (field.nullable === true) || (field.required !== true); 996 field.jsonType = field.jsonType || Util.findJsonType(field); 997 998 //this will convert values from strings to the correct type (such as booleans) 999 if (!Ext4.isEmpty(field.defaultValue)){ 1000 var type = Ext4.data.Types[LABKEY.ext4.Util.EXT_TYPE_MAP[field.jsonType]]; 1001 if (type){ 1002 field.defaultValue = type.convert(field.defaultValue); 1003 } 1004 } 1005 }, 1006 1007 /** 1008 * This method takes an object that is/extends an Ext4.Container (e.g. Panels, Toolbars, Viewports, Menus) and 1009 * resizes it so the Container fits inside the viewable region of the window. This is generally used in the case 1010 * where the Container is not rendered to a webpart but rather displayed on the page itself (e.g. SchemaBrowser, 1011 * manageFolders, etc). 1012 * @param extContainer - (Required) outer container which is the target to be resized 1013 * @param width - (Required) width of the viewport. In many cases, the window width. If a negative width is passed than 1014 * the width will not be set. 1015 * @param height - (Required) height of the viewport. In many cases, the window height. If a negative height is passed than 1016 * the height will not be set. 1017 * @param paddingX - distance from the right edge of the viewport. Defaults to 35. 1018 * @param paddingY - distance from the bottom edge of the viewport. Defaults to 35. 1019 */ 1020 resizeToViewport: function(extContainer, width, height, paddingX, paddingY, offsetX, offsetY) 1021 { 1022 LABKEY.ext4.Util.resizeToContainer.apply(this, arguments); 1023 } 1024 }); 1025 }()); 1026