1 /* 2 * Copyright (c) 2012-2017 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 Util.buildQtip({ 510 displayValue: displayValue, 511 value: value, 512 meta: meta, 513 col: col, 514 record: record, 515 store: store, 516 cellMetaData: cellMetaData 517 }); 518 519 return displayValue; 520 } 521 }, 522 523 /** 524 * @private 525 * @param value 526 * @param meta 527 * @param record 528 * @param store 529 */ 530 getDisplayString: function(value, meta, record, store) { 531 var displayType = meta.displayFieldJsonType || meta.jsonType; 532 var displayValue = value; 533 var shouldCache; 534 535 //NOTE: the labkey 9.1 API returns both the value of the field and the display value 536 //the server is already doing the work, so we should rely on this 537 //this does have a few problems: 538 //if the displayValue equals the value, the API omits displayValue. because we cant 539 // count on the server returning the right value unless explicitly providing a displayValue, 540 // we only attempt to use that 541 if(record && record.raw && record.raw[meta.name]){ 542 if(Ext4.isDefined(record.raw[meta.name].displayValue)) 543 return record.raw[meta.name].displayValue; 544 // TODO: this needs testing before enabling. would be nice if we could rely on this, 545 // TODO: but i dont think we will be able to (dates, for example) 546 // perhaps only try this for lookups? 547 //else if(Ext4.isDefined(record.raw[meta.name].value)) 548 // return record.raw[meta.name].value; 549 } 550 551 //NOTE: this is substantially changed over LABKEY.ext.FormHelper 552 if(meta.lookup && meta.lookup['public'] !== false && meta.lookups!==false){ 553 //dont both w/ special renderer if the raw value is the same as the displayColumn 554 if (meta.lookup.keyColumn != meta.lookup.displayColumn){ 555 displayValue = Util.getLookupDisplayValue(meta, displayValue, record, store); 556 meta.usingLookup = true; 557 shouldCache = false; 558 displayType = 'string'; 559 } 560 } 561 562 if(meta.extFormatFn && Ext4.isFunction(meta.extFormatFn)){ 563 displayValue = meta.extFormatFn(displayValue); 564 } 565 else { 566 if(!Ext4.isDefined(displayValue)) 567 displayValue = ''; 568 switch (displayType){ 569 case "date": 570 var date = new Date(displayValue); 571 //NOTE: java formats differ from ext 572 var format = meta.extFormat; 573 if(!format){ 574 if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0) 575 format = "Y-m-d"; 576 else 577 format = "Y-m-d H:i:s"; 578 } 579 displayValue = Ext4.Date.format(date, format); 580 break; 581 case "int": 582 displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0'))(displayValue); 583 break; 584 case "boolean": 585 586 var t = meta.editorConfig && meta.editorConfig.trueText ? meta.editorConfig.trueText : (this.trueText || 'true'); 587 var f = meta.editorConfig && meta.editorConfig.falseText ? meta.editorConfig.falseText : (this.falseText || 'false'); 588 var u = meta.editorConfig && meta.editorConfig.undefinedText ? meta.editorConfig.undefinedText : (this.undefinedText || ' '); 589 590 if(displayValue === undefined){ 591 displayValue = u; 592 } 593 else if(!displayValue || displayValue === 'false'){ 594 displayValue = f; 595 } 596 else { 597 displayValue = t; 598 } 599 break; 600 case "float": 601 displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0,000.00'))(displayValue); 602 break; 603 case "string": 604 default: 605 displayValue = !Ext4.isEmpty(displayValue) ? displayValue.toString() : ""; 606 } 607 } 608 609 // Experimental. cache the calculated value, so we dont need to recalculate each time. 610 // This should get cleared by the store on update like any server-generated value 611 if (shouldCache !== false) { 612 record.raw = record.raw || {}; 613 if(!record.raw[meta.name]) 614 record.raw[meta.name] = {}; 615 record.raw[meta.name].displayValue = displayValue; 616 } 617 618 return displayValue; 619 }, 620 621 /** 622 * Constructs an ext field component based on the supplied metadata. Same as getFormEditorConfig, but actually constructs the editor. 623 * The resulting editor is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata 624 * contains a formEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 625 * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 626 * @param {Object} config as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 627 * @return {Object} An Ext field component 628 */ 629 getFormEditor: function(meta, config) { 630 var editorConfig = Util.getFormEditorConfig(meta, config); 631 return Ext4.ComponentMgr.create(editorConfig); 632 }, 633 634 /** 635 * Return an Ext config object to create an Ext field based on the supplied metadata. 636 * The resulting config object is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata 637 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 638 * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 639 * @param {Object} [config] as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 640 * @returns {Object} An Ext 4.x config object 641 */ 642 getFormEditorConfig: function(meta, config) { 643 var editor = Util.getDefaultEditorConfig(meta); 644 645 // now we allow overrides of default behavior, in order of precedence 646 if (meta.editorConfig) 647 Ext4.Object.merge(editor, meta.editorConfig); 648 if (meta.formEditorConfig) 649 Ext4.Object.merge(editor, meta.formEditorConfig); 650 if (config) 651 Ext4.Object.merge(editor, config); 652 653 return editor; 654 }, 655 656 /** 657 * Constructs an ext field component based on the supplied metadata. Same as getFormEditorConfig, but actually constructs the editor. 658 * The resulting editor is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata 659 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 660 * @private 661 * @param meta 662 * @param config 663 * @return {Object} An Ext field component 664 */ 665 getGridEditor: function(meta, config) { 666 var editorConfig = Util.getGridEditorConfig(meta, config); 667 return Ext4.ComponentMgr.create(editorConfig); 668 }, 669 670 /** 671 * @private 672 * Return an Ext config object to create an Ext field based on the supplied metadata. 673 * The resulting config object is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata 674 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 675 * 676 * @name getGridEditorConfig 677 * @function 678 * @returns {object} Returns an Ext config object 679 */ 680 getGridEditorConfig: function(meta, config) { 681 //this produces a generic editor 682 var editor = Util.getDefaultEditorConfig(meta); 683 684 //now we allow overrides of default behavior, in order of precedence 685 if (meta.editorConfig) { 686 Ext4.Object.merge(editor, meta.editorConfig); 687 } 688 689 //note: this will screw up cell editors 690 delete editor.fieldLabel; 691 692 if (meta.gridEditorConfig) { 693 Ext4.Object.merge(editor, meta.gridEditorConfig); 694 } 695 if (config) { 696 Ext4.Object.merge(editor, config); 697 } 698 699 return editor; 700 }, 701 702 /** 703 * @private 704 * 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 705 * 'datachanged' event once the lookup store loads. A better idea would be to force the store/grid to listen 706 * for event fired by the lookupStore or somehow get the metadata to fire events itself. 707 * @param meta 708 * @param data 709 * @param record 710 * @param store 711 */ 712 getLookupDisplayValue: function(meta, data, record, store) { 713 var lookupStore = Util.getLookupStore(meta); 714 if(!lookupStore){ 715 return ''; 716 } 717 718 meta.lookupStore = lookupStore; 719 var lookupRecord; 720 721 //NOTE: preferentially used snapshot instead of data to allow us to find the record even if the store is currently filtered 722 var records = lookupStore.snapshot || lookupStore.data; 723 var matcher = records.createValueMatcher((data == null ? '' : data), false, true, true); 724 var property = meta.lookup.keyColumn; 725 var recIdx = records.findIndexBy(function(o){ 726 return o && matcher.test(o.get(property)); 727 }, null); 728 729 if (recIdx != -1) 730 lookupRecord = records.getAt(recIdx); 731 732 if (lookupRecord){ 733 return lookupRecord.get(meta.lookup.displayColumn); 734 } 735 else { 736 if (data!==null){ 737 return "[" + data + "]"; 738 } 739 else { 740 return Ext4.isDefined(meta.nullCaption) ? meta.nullCaption : "[none]"; 741 } 742 } 743 }, 744 745 /** 746 * @private 747 * @param storeId 748 * @param c 749 */ 750 getLookupStore: function(storeId, c) { 751 if (!Ext4.isString(storeId)) { 752 c = storeId; 753 storeId = Util.getLookupStoreId(c); 754 } 755 756 if (Ext4.isObject(c.store) && c.store.events) { 757 return c.store; 758 } 759 760 var store = Ext4.StoreMgr.lookup(storeId); 761 if (!store) { 762 var config = c.store || Util.getLookupStoreConfig(c); 763 config.storeId = storeId; 764 store = Ext4.create('LABKEY.ext4.data.Store', config); 765 } 766 return store; 767 }, 768 769 /** 770 * @private 771 * @param c 772 */ 773 getLookupStoreConfig: function(c) { 774 var l = c.lookup; 775 776 // normalize lookup 777 l.queryName = l.queryName || l.table; 778 l.schemaName = l.schemaName || l.schema; 779 780 if (l.schemaName == 'core' && l.queryName =='UsersData') { 781 l.queryName = 'Users'; 782 } 783 784 var config = { 785 xtype: "labkeystore", 786 storeId: Util.getLookupStoreId(c), 787 containerFilter: 'CurrentOrParentAndWorkbooks', 788 schemaName: l.schemaName, 789 queryName: l.queryName, 790 containerPath: l.container || l.containerPath || LABKEY.container.path, 791 autoLoad: true 792 }; 793 794 if (l.viewName) { 795 config.viewName = l.viewName; 796 } 797 798 if (l.filterArray) { 799 config.filterArray = l.filterArray; 800 } 801 802 if (l.columns) { 803 config.columns = l.columns; 804 } 805 else { 806 var columns = []; 807 if (l.keyColumn) { 808 columns.push(l.keyColumn); 809 } 810 if (l.displayColumn && l.displayColumn != l.keyColumn) { 811 columns.push(l.displayColumn); 812 } 813 if (columns.length == 0) { 814 columns = ['*']; 815 } 816 config.columns = columns; 817 } 818 819 if (l.sort) { 820 config.sort = l.sort; 821 } 822 else if (l.sort !== false) { 823 config.sort = l.displayColumn; 824 } 825 826 return config; 827 }, 828 829 /** 830 * @private 831 * @param c 832 */ 833 getLookupStoreId: function(c) { 834 if (c.store && c.store.storeId) { 835 return c.store.storeId; 836 } 837 838 if (c.lookup) { 839 return c.lookup.storeId || [ 840 c.lookup.schemaName || c.lookup.schema, 841 c.lookup.queryName || c.lookup.table, 842 c.lookup.keyColumn, 843 c.lookup.displayColumn 844 ].join('||'); 845 } 846 847 return c.name; 848 }, 849 850 /** 851 * @private 852 * EXPERIMENTAL. Returns the fields from the passed store 853 * @param store 854 * @returns {Ext.util.MixedCollection} The fields associated with this store 855 */ 856 getStoreFields: function(store) { 857 return store.proxy.reader.model.prototype.fields; 858 }, 859 860 /** 861 * @private 862 * @param store 863 * @return {boolean} Whether the store has loaded 864 */ 865 hasStoreLoaded: function(store) { 866 return store.proxy && store.proxy.reader && store.proxy.reader.rawData; 867 }, 868 869 /** 870 * @private 871 * Identify the proper name of a field using an input string such as an excel column label. This helper will 872 * perform a case-insensitive comparison of the field name, label, caption, shortCaption and aliases. 873 * @param {string} fieldName The string to search 874 * @param {Array/Ext.util.MixedCollection} metadata The fields to search 875 * @return {string} The normalized field name or null if not found 876 */ 877 resolveFieldNameFromLabel: function(fieldName, meta) { 878 var fnMatch = []; 879 var aliasMatch = []; 880 881 var testField = function(fieldMeta) { 882 if (caseInsensitiveEquals(fieldName, fieldMeta.name) 883 || caseInsensitiveEquals(fieldName, fieldMeta.caption) 884 || caseInsensitiveEquals(fieldName, fieldMeta.shortCaption) 885 || caseInsensitiveEquals(fieldName, fieldMeta.label) 886 ){ 887 fnMatch.push(fieldMeta.name); 888 return false; //exit here because it should only match 1 name 889 } 890 891 if (fieldMeta.importAliases) { 892 var aliases; 893 if(Ext4.isArray(fieldMeta.importAliases)) 894 aliases = fieldMeta.importAliases; 895 else 896 aliases = fieldMeta.importAliases.split(','); 897 898 Ext4.each(aliases, function(alias){ 899 if (caseInsensitiveEquals(fieldName, alias)) 900 aliasMatch.push(fieldMeta.name); //continue iterating over fields in case a fieldName matches 901 }, this); 902 } 903 }; 904 905 if (meta.hasOwnProperty('each')) { 906 meta.each(testField, this); 907 } 908 else { 909 Ext4.each(meta, testField, this); 910 } 911 912 if (fnMatch.length==1) { 913 return fnMatch[0]; 914 } 915 else if (fnMatch.length > 1) { 916 return null; 917 } 918 else if (aliasMatch.length==1) { 919 return aliasMatch[0]; 920 } 921 return null; 922 }, 923 924 /** 925 * @private 926 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in a details view. 927 * If any of the following are true, it will not appear: hidden, isHidden 928 * If shownInDetailsView is defined, it will take priority 929 * @param {Object} metadata The field metadata object 930 * @return {boolean} Whether the field should appear in the default details view 931 */ 932 shouldShowInDetailsView: function(metadata){ 933 return Ext4.isDefined(metadata.shownInDetailsView) ? metadata.shownInDetailsView : 934 (!metadata.isHidden && !metadata.hidden && metadata.shownInDetailsView!==false); 935 }, 936 937 /** 938 * @private 939 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in an insert view. 940 * If any of the following are false, it will not appear: userEditable and autoIncrement 941 * If any of the follow are true, it will not appear: hidden, isHidden 942 * If shownInInsertView is defined, this will take priority over all 943 * @param {Object} metadata The field metadata object 944 * @return {boolean} Whether the field should appear in the default insert view 945 */ 946 shouldShowInInsertView: function(metadata){ 947 return Ext4.isDefined(metadata.shownInInsertView) ? metadata.shownInInsertView : 948 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement); 949 }, 950 951 /** 952 * @private 953 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in an update view. 954 * If any of the following are false, it will not appear: userEditable and autoIncrement 955 * If any of the follow are true, it will not appear: hidden, isHidden, readOnly 956 * If shownInUpdateView is defined, this will take priority over all 957 * @param {Object} metadata The field metadata object 958 * @return {boolean} Whether the field should appear 959 */ 960 shouldShowInUpdateView: function(metadata) { 961 return Ext4.isDefined(metadata.shownInUpdateView) ? metadata.shownInUpdateView : 962 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement && metadata.readOnly!==false) 963 }, 964 965 /** 966 * @private 967 * Shortcut for LABKEY.ext4.Util.getLookupStore that doesn't require as complex a config object 968 * @param {Object} config Configuration object for an Ext.data.Store 969 * @return {Ext.data.Store} The store 970 */ 971 simpleLookupStore: function(config) { 972 config.lookup = { 973 containerPath : config.containerPath, 974 schemaName : config.schemaName, 975 queryName : config.queryName, 976 viewName : config.viewName, 977 displayColumn : config.displayColumn, 978 keyColumn : config.keyColumn 979 }; 980 981 return Util.getLookupStore(config); 982 }, 983 984 /** 985 * @private 986 * The intention of this method is to provide a standard, low-level way to translating Labkey metadata names into ext ones. 987 * @param field 988 */ 989 translateMetadata: function(field) { 990 field.fieldLabel = Ext4.util.Format.htmlEncode(field.label || field.caption || field.header || field.name); 991 field.dataIndex = field.dataIndex || field.name; 992 field.editable = (field.userEditable!==false && !field.readOnly && !field.autoIncrement && !field.calculated); 993 field.allowBlank = (field.nullable === true) || (field.required !== true); 994 field.jsonType = field.jsonType || Util.findJsonType(field); 995 996 //this will convert values from strings to the correct type (such as booleans) 997 if (!Ext4.isEmpty(field.defaultValue)){ 998 var type = Ext4.data.Types[LABKEY.ext4.Util.EXT_TYPE_MAP[field.jsonType]]; 999 if (type){ 1000 field.defaultValue = type.convert(field.defaultValue); 1001 } 1002 } 1003 }, 1004 1005 /** 1006 * This method takes an object that is/extends an Ext4.Container (e.g. Panels, Toolbars, Viewports, Menus) and 1007 * resizes it so the Container fits inside the viewable region of the window. This is generally used in the case 1008 * where the Container is not rendered to a webpart but rather displayed on the page itself (e.g. SchemaBrowser, 1009 * manageFolders, etc). 1010 * @param extContainer - (Required) outer container which is the target to be resized 1011 * @param width - (Required) width of the viewport. In many cases, the window width. If a negative width is passed than 1012 * the width will not be set. 1013 * @param height - (Required) height of the viewport. In many cases, the window height. If a negative height is passed than 1014 * the height will not be set. 1015 * @param paddingX - distance from the right edge of the viewport. Defaults to 35. 1016 * @param paddingY - distance from the bottom edge of the viewport. Defaults to 35. 1017 */ 1018 resizeToViewport: function(extContainer, width, height, paddingX, paddingY, offsetX, offsetY) 1019 { 1020 LABKEY.ext4.Util.resizeToContainer.apply(this, arguments); 1021 } 1022 }); 1023 }()); 1024