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