1 /* 2 * Copyright (c) 2007-2016 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 7 // NOTE labkey.js should NOT depend on any external libraries like ExtJS 8 9 if (typeof LABKEY == "undefined") 10 { 11 /** 12 * @namespace Namespace used to encapsulate LabKey core API and utilities. 13 */ 14 LABKEY = new function() 15 { 16 var configs = { 17 contextPath: "", 18 DataRegions: {}, 19 devMode: false, 20 demoMode: false, 21 dirty: false, 22 isDocumentClosed: false, 23 extJsRoot: "ext-3.4.1", 24 extJsRoot_42: "ext-4.2.1", 25 extThemeRoot: "labkey-ext-theme", 26 extThemeName_42: "seattle", 27 extThemeRoot_42: "ext-theme", 28 fieldMarker: '@', 29 hash: 0, 30 imagePath: "", 31 requestedCssFiles: {}, 32 requestedScriptFiles: [], 33 submit: false, 34 unloadMessage: "You will lose any changes made to this page.", 35 verbose: false, 36 widget: {} 37 }; 38 39 // private variables not configurable 40 var _requestedCssFiles = {}; 41 42 // private caching mechanism for script loading 43 var ScriptCache = function() 44 { 45 var cache = {}; 46 47 var callbacksOnCache = function(key) 48 { 49 // console.log('calling --', key); 50 var cbs = cache[key]; 51 52 // set the cache to hit 53 cache[key] = true; 54 55 // Tell mothership.js to hook event callbacks 56 if (LABKEY.Mothership) 57 { 58 if (key.indexOf(configs.extJsRoot + "/ext-all") == 0) 59 LABKEY.Mothership.hookExt3(); 60 61 if (key.indexOf(configs.extJsRoot_42 + "/ext-all") == 0) 62 LABKEY.Mothership.hookExt4(); 63 } 64 65 // call on the callbacks who have been waiting for this resource 66 if (isArray(cbs)) 67 { 68 var cb; 69 for (var c=0; c < cbs.length; c++) 70 { 71 cb = cbs[c]; 72 handle(cb.fn, cb.scope); 73 } 74 } 75 }; 76 77 var inCache = function(key) 78 { 79 // console.log('hit --', key); 80 return cache[key] === true; 81 }; 82 83 var inFlightCache = function(key) 84 { 85 return isArray(cache[key]); 86 }; 87 88 var loadCache = function(key, cb, s) 89 { 90 // console.log('miss --', key); 91 // The value as an array denotes the cache resource is in flight 92 if (!cache[key]) 93 cache[key] = []; 94 95 if (isFunction(cb)) 96 cache[key].push({fn: cb, scope: s}); 97 }; 98 99 return { 100 callbacksOnCache: callbacksOnCache, 101 inCache: inCache, 102 inFlightCache: inFlightCache, 103 loadCache: loadCache 104 }; 105 }; 106 107 // instance of scripting cache used by public methods 108 var scriptCache = new ScriptCache(); 109 110 // Public Method Definitions 111 112 var addElemToHead = function(elemName, attributes) 113 { 114 var elem = document.createElement(elemName); 115 for (var a in attributes) { 116 if (attributes.hasOwnProperty(a)) { 117 elem[a] = attributes[a]; 118 } 119 } 120 return document.getElementsByTagName("head")[0].appendChild(elem); 121 }; 122 123 var addMarkup = function(html) 124 { 125 if (configs.isDocumentClosed) 126 { 127 var elem = document.createElement("div"); 128 elem.innerHTML = html; 129 document.body.appendChild(elem.firstChild); 130 } 131 else 132 document.write(html); 133 }; 134 135 //private. used to append additional module context objects for AJAXd views 136 var applyModuleContext = function(ctx) { 137 for (var mn in ctx) { 138 if (ctx.hasOwnProperty(mn)) { 139 LABKEY.moduleContext[mn.toLowerCase()] = ctx[mn]; 140 } 141 } 142 }; 143 144 var beforeunload = function (dirtyCallback, scope, msg) 145 { 146 return function () { 147 if (!getSubmit() && (isDirty() || (dirtyCallback && dirtyCallback.call(scope)))) { 148 return msg || configs.unloadMessage; 149 } 150 }; 151 }; 152 153 var createElement = function(tag, innerHTML, attributes) 154 { 155 var e = document.createElement(tag); 156 if (innerHTML) 157 e.innerHTML = innerHTML; 158 if (attributes) 159 { 160 for (var att in attributes) 161 { 162 if (attributes.hasOwnProperty(att)) 163 { 164 try 165 { 166 e[att] = attributes[att]; 167 } 168 catch (x) 169 { 170 console.log(x); // e['style'] is read-only in old firefox 171 } 172 } 173 } 174 } 175 return e; 176 }; 177 178 var getModuleContext = function(moduleName) { 179 return LABKEY.moduleContext[moduleName.toLowerCase()]; 180 }; 181 182 var getModuleProperty = function(moduleName, property) { 183 var ctx = getModuleContext(moduleName); 184 if (!ctx) { 185 return null; 186 } 187 return ctx[property]; 188 }; 189 190 var getSubmit = function() 191 { 192 return configs.submit; 193 }; 194 195 // simple callback handler that will type check then call with scope 196 var handle = function(callback, scope) 197 { 198 if (isFunction(callback)) 199 { 200 callback.call(scope || this); 201 } 202 }; 203 204 // If we're in demo mode, replace each ID with an equal length string of "*". This code should match DemoMode.id(). 205 var id = function(id) 206 { 207 if (configs.demoMode) 208 { 209 return new Array(id.length + 1 ).join("*"); 210 } 211 else 212 { 213 return id; 214 } 215 }; 216 217 var init = function(config) 218 { 219 for (var p in config) 220 { 221 //TODO: we should be trying to seal some of these objects, or at least wrap them to make them harder to manipulate 222 if (config.hasOwnProperty(p)) { 223 configs[p] = config[p]; 224 LABKEY[p] = config[p]; 225 } 226 } 227 if ("Security" in LABKEY) 228 LABKEY.Security.currentUser = LABKEY.user; 229 }; 230 231 var isArray = function(value) 232 { 233 return Object.prototype.toString.call(value) === "[object Array]"; 234 }; 235 236 var isBoolean = function(value) 237 { 238 return typeof value === "boolean"; 239 }; 240 241 var isDirty = function() 242 { 243 return configs.dirty; 244 }; 245 246 var isFunction = function(value) 247 { 248 return typeof value === "function"; 249 }; 250 251 var isLibrary = function(file) 252 { 253 return file && (file.indexOf('.') === -1 || file.indexOf('.lib.xml') > -1); 254 }; 255 256 var loadScripts = function() 257 { 258 configs.isDocumentClosed = true; 259 }; 260 261 var loadedScripts = function() 262 { 263 for (var i=0; i < arguments.length; i++) 264 { 265 if (isArray(arguments[i])) 266 { 267 for (var j=0; j < arguments[i].length; j++) 268 { 269 scriptCache.callbacksOnCache(arguments[i][j]); 270 } 271 } 272 else 273 { 274 scriptCache.callbacksOnCache(arguments[i]); 275 } 276 } 277 return true; 278 }; 279 280 var qs = function(params) 281 { 282 if (!params) 283 return ''; 284 285 var qs = '', and = '', pv, p; 286 287 for (p in params) 288 { 289 if (params.hasOwnProperty(p)) 290 { 291 pv = params[p]; 292 293 if (pv === null || pv === undefined) 294 pv = ''; 295 296 if (isArray(pv)) 297 { 298 for (var i=0; i < pv.length; i++) 299 { 300 qs += and + encodeURIComponent(p) + '=' + encodeURIComponent(pv[i]); 301 and = '&'; 302 } 303 } 304 else 305 { 306 qs += and + encodeURIComponent(p) + '=' + encodeURIComponent(pv); 307 and = '&'; 308 } 309 } 310 } 311 312 return qs; 313 }; 314 315 // So as not to confuse with native support for fetch() 316 var _fetch = function(url, params, success, failure) 317 { 318 var xhr = new XMLHttpRequest(); 319 var _url = url + (url.indexOf('?') === -1 ? '?' : '&') + qs(params); 320 321 xhr.onreadystatechange = function() 322 { 323 if (xhr.readyState === 4) 324 { 325 var _success = (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304; 326 _success ? success(xhr) : failure(xhr); 327 } 328 }; 329 330 xhr.open('GET', _url, true); 331 332 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 333 if (LABKEY.CSRF) 334 xhr.setRequestHeader('X-LABKEY-CSRF', LABKEY.CSRF); 335 336 xhr.send(null); 337 338 return xhr; 339 }; 340 341 var requiresCss = function(file) 342 { 343 if (isArray(file)) 344 { 345 for (var i=0;i<file.length;i++) 346 requiresCss(file[i]); 347 return; 348 } 349 350 if (file.indexOf('/') == 0) 351 { 352 file = file.substring(1); 353 } 354 355 var key = file, 356 fullPath; 357 358 if (!_requestedCssFiles[key]) 359 { 360 _requestedCssFiles[key] = true; 361 362 // Support both LabKey and external CSS files 363 if (file.substr(0, 4) != "http") 364 { 365 // local files 366 fullPath = configs.contextPath + "/" + file + '?' + configs.hash; 367 } 368 else 369 { 370 // external files 371 fullPath = file; 372 } 373 374 addElemToHead("link", { 375 type: "text/css", 376 rel: "stylesheet", 377 href: fullPath 378 }); 379 } 380 }; 381 382 var requestedCssFiles = function() 383 { 384 var ret = (arguments.length > 0 && _requestedCssFiles[arguments[0]]) ? true : false; 385 for (var i=0; i < arguments.length ; i++) 386 _requestedCssFiles[arguments[i]] = true; 387 return ret; 388 }; 389 390 var requiresClientAPI = function(callback, scope) 391 { 392 // backwards compat for 'immediate' 393 if (arguments.length > 0 && isBoolean(arguments[0])) 394 { 395 callback = arguments[1]; 396 scope = arguments[2]; 397 } 398 399 requiresLib('clientapi', function() 400 { 401 requiresExt3ClientAPI(callback, scope); 402 }); 403 }; 404 405 var requiresExt3 = function(callback, scope) 406 { 407 // backwards compat for 'immediate' 408 if (arguments.length > 0 && isBoolean(arguments[0])) 409 { 410 callback = arguments[1]; 411 scope = arguments[2]; 412 } 413 414 if (window.Ext) 415 { 416 handle(callback, scope); 417 } 418 else 419 { 420 requiresCss(configs.extJsRoot + '/resources/css/ext-all.css'); 421 requiresLib('Ext3', callback, scope); 422 } 423 }; 424 425 var requiresExt3ClientAPI = function(callback, scope) 426 { 427 // backwards compat for 'immediate' 428 if (arguments.length > 0 && isBoolean(arguments[0])) 429 { 430 callback = arguments[1]; 431 scope = arguments[2]; 432 } 433 434 requiresExt3(function() 435 { 436 requiresLib('clientapi/ext3', callback, scope); 437 }); 438 }; 439 440 var requiresExt4ClientAPI = function(callback, scope) 441 { 442 // backwards compat for 'immediate' 443 if (arguments.length > 0 && isBoolean(arguments[0])) 444 { 445 callback = arguments[1]; 446 scope = arguments[2]; 447 } 448 449 requiresExt4Sandbox(function() 450 { 451 requiresLib('Ext4ClientApi', callback, scope); 452 }); 453 }; 454 455 var requiresExt4Sandbox = function(callback, scope) 456 { 457 // backwards compat for 'immediate' 458 if (arguments.length > 0 && isBoolean(arguments[0])) 459 { 460 callback = arguments[1]; 461 scope = arguments[2]; 462 } 463 464 if (window.Ext4) 465 { 466 handle(callback, scope); 467 } 468 else 469 { 470 requiresCss(configs.extThemeRoot_42 + "/" + configs.extThemeName_42 + "/ext-all.css"); 471 requiresLib('Ext4', callback, scope); 472 } 473 }; 474 475 var requiresLib = function(lib, callback, scope) 476 { 477 if (!lib) 478 { 479 handle(callback, scope); 480 return; 481 } 482 483 var _lib = lib.split('.lib.xml')[0]; 484 485 // in case _lib is now empty 486 if (!_lib) 487 { 488 handle(callback, scope); 489 return; 490 } 491 492 if (scriptCache.inCache(_lib)) 493 { 494 handle(callback, scope); 495 return; 496 } 497 else if (scriptCache.inFlightCache(_lib)) 498 { 499 scriptCache.loadCache(_lib, callback, scope); 500 return; 501 } 502 else 503 { 504 scriptCache.loadCache(_lib, callback, scope); 505 } 506 507 var cacheLoader = function() 508 { 509 scriptCache.callbacksOnCache(_lib); 510 }; 511 512 _fetch('core-loadLibrary.api', { 513 library: _lib 514 }, function(data) { 515 // success 516 var json = JSON.parse(data.responseText); 517 var definition = json['libraries'][_lib]; 518 519 if (definition) 520 { 521 var styles = []; 522 var scripts = []; 523 for (var d=0; d < definition.length; d++) 524 { 525 if (definition[d].indexOf('.css') > -1) 526 { 527 styles.push(definition[d]); 528 } 529 else 530 { 531 scripts.push(definition[d]); 532 } 533 } 534 535 LABKEY.requiresCss(styles); 536 LABKEY.requiresScript(scripts, cacheLoader, undefined, true /* inOrder, sadly */); 537 } 538 else 539 { 540 throw new Error('Failed to retrieve library definition \"' + _lib + '\"'); 541 } 542 }, function() { 543 // failure 544 throw new Error('Failed to load library: \"' + _lib + '\"'); 545 }); 546 }; 547 548 var requiresScript = function(file, callback, scope, inOrder) 549 { 550 if (arguments.length === 0) 551 { 552 throw "LABKEY.requiresScript() requires the 'file' parameter."; 553 } 554 else if (!file) 555 { 556 throw "LABKEY.requiresScript() invalid 'file' argument."; 557 } 558 559 // backwards compat for 'immediate' 560 if (arguments.length > 1 && isBoolean(arguments[1])) 561 { 562 callback = arguments[2]; 563 scope = arguments[3]; 564 inOrder = arguments[4]; 565 } 566 567 if (isArray(file)) 568 { 569 var requestedLength = file.length; 570 var loaded = 0; 571 572 if (inOrder) 573 { 574 var chain = function() 575 { 576 loaded++; 577 if (loaded == requestedLength) 578 { 579 handle(callback, scope); 580 } 581 else if (loaded < requestedLength) 582 requiresScript(file[loaded], chain, undefined, true); 583 }; 584 585 if (scriptCache.inCache(file[loaded])) 586 { 587 chain(); 588 } 589 else 590 requiresScript(file[loaded], chain, undefined, true); 591 } 592 else 593 { 594 // request all the scripts (order does not matter) 595 var allDone = function() 596 { 597 loaded++; 598 if (loaded == requestedLength) 599 { 600 handle(callback, scope); 601 } 602 }; 603 604 for (var i = 0; i < file.length; i++) 605 { 606 if (scriptCache.inCache(file[i])) 607 { 608 allDone(); 609 } 610 else 611 requiresScript(file[i], allDone); 612 } 613 } 614 return; 615 } 616 617 if (isLibrary(file)) 618 { 619 if (file === 'Ext3') 620 { 621 requiresExt3(callback, scope); 622 } 623 if (file === 'Ext4') 624 { 625 requiresExt4Sandbox(callback, scope); 626 } 627 else 628 { 629 requiresLib(file, callback, scope); 630 } 631 return; 632 } 633 634 if (file.indexOf('/') == 0) 635 { 636 file = file.substring(1); 637 } 638 639 if (scriptCache.inCache(file)) 640 { 641 // cache hit -- script is loaded and ready to go 642 handle(callback, scope); 643 return; 644 } 645 else if (scriptCache.inFlightCache(file)) 646 { 647 // cache miss -- in flight 648 scriptCache.loadCache(file, callback, scope); 649 return; 650 } 651 else 652 { 653 // cache miss 654 scriptCache.loadCache(file, callback, scope); 655 } 656 657 // although FireFox and Safari allow scripts to use the DOM 658 // during parse time, IE does not. So if the document is 659 // closed, use the DOM to create a script element and append it 660 // to the head element. Otherwise (still parsing), use document.write() 661 662 // Support both LabKey and external JavaScript files 663 var src = file.substr(0, 4) != "http" ? configs.contextPath + "/" + file + '?' + configs.hash : file; 664 665 var cacheLoader = function() 666 { 667 scriptCache.callbacksOnCache(file); 668 }; 669 670 if (configs.isDocumentClosed || callback) 671 { 672 //create a new script element and append it to the head element 673 var script = addElemToHead("script", { 674 src: src, 675 type: "text/javascript" 676 }); 677 678 // IE has a different way of handling <script> loads 679 if (script.readyState) 680 { 681 script.onreadystatechange = function() { 682 if (script.readyState == "loaded" || script.readyState == "complete") { 683 script.onreadystatechange = null; 684 cacheLoader(); 685 } 686 }; 687 } 688 else 689 { 690 script.onload = cacheLoader; 691 } 692 } 693 else 694 { 695 document.write('\n<script type="text/javascript" src="' + src + '"></script>\n'); 696 cacheLoader(); 697 } 698 }; 699 700 var requiresVisualization = function(callback, scope) 701 { 702 requiresLib('vis/vis', callback, scope); 703 }; 704 705 var setDirty = function (dirty) 706 { 707 configs.dirty = (dirty ? true : false); // only set to boolean 708 }; 709 710 var setSubmit = function (submit) 711 { 712 configs.submit = (submit ? true : false); // only set to boolean 713 }; 714 715 var showNavTrail = function() 716 { 717 var elem = document.getElementById("navTrailAncestors"); 718 if(elem) 719 elem.style.visibility = "visible"; 720 elem = document.getElementById("labkey-nav-trail-current-page"); 721 if(elem) 722 elem.style.visibility = "visible"; 723 }; 724 725 return { 726 727 /** 728 * This callback type is called 'requireCallback' and is displayed as a global symbol 729 * 730 * @callback requireCallback 731 */ 732 733 /** 734 * The DataRegion class allows you to interact with LabKey grids, 735 * including querying and modifying selection state, filters, and more. 736 * @field 737 */ 738 DataRegions: configs.DataRegions, 739 740 demoMode: configs.demoMode, 741 devMode: configs.devMode, 742 dirty: configs.dirty, 743 extJsRoot: configs.extJsRoot, 744 extJsRoot_42: configs.extJsRoot_42, 745 extThemeRoot: configs.extThemeRoot, 746 fieldMarker: configs.fieldMarker, 747 hash: configs.hash, 748 imagePath: configs.imagePath, 749 submit: configs.submit, 750 unloadMessage: configs.unloadMessage, 751 verbose: configs.verbose, 752 widget: configs.widget, 753 754 /** @field */ 755 contextPath: configs.contextPath, 756 757 /** 758 * Appends an element to the head of the document 759 * @private 760 * @param {String} elemName First argument for docoument.createElement 761 * @param {Object} [attributes] 762 * @returns {*} 763 */ 764 addElemToHead: addElemToHead, 765 766 // TODO: Eligible for removal after util.js is migrated 767 addMarkup: addMarkup, 768 applyModuleContext: applyModuleContext, 769 beforeunload: beforeunload, 770 createElement: createElement, 771 772 /** 773 * @function 774 * @param {String} moduleName The name of the module 775 * @returns {Object} The context object for this module. The current view must have specifically requested 776 * the context for this module in its view XML 777 */ 778 getModuleContext: getModuleContext, 779 780 /** 781 * @function 782 * @param {String} moduleName The name of the module 783 * @param {String} property The property name to return 784 * @returns {String} The value of the module property. Will return null if the property has not been set. 785 */ 786 getModuleProperty: getModuleProperty, 787 getSubmit: getSubmit, 788 id: id, 789 init: init, 790 isDirty: isDirty, 791 loadScripts: loadScripts, 792 loadedScripts: loadedScripts, 793 794 /** 795 * Loads a CSS file from the server. 796 * @function 797 * @param {(string|string[])} file - The path of the CSS file to load 798 * @example 799 <script type="text/javascript"> 800 LABKEY.requiresCss("myModule/myFile.css"); 801 </script> 802 */ 803 requiresCss: requiresCss, 804 requestedCssFiles: requestedCssFiles, 805 requiresClientAPI: requiresClientAPI, 806 807 /** 808 * This can be added to any LABKEY page in order to load ExtJS 3. This is the preferred method to declare Ext3 usage 809 * from wiki pages. For HTML or JSP pages defined in a module, see our <a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=ext4Development">documentation</a> on declaration of client dependencies. 810 * @function 811 * @param {boolean} [immediate=true] - True to load the script immediately; false will defer script loading until the page has been downloaded. 812 * @param {requireCallback} [callback] - Callback for when all dependencies are loaded. 813 * @param {Object} [scope] - Scope of callback. 814 * @example 815 <script type="text/javascript"> 816 LABKEY.requiresExt3(true, function() { 817 Ext.onReady(function() { 818 // Ext 3 is loaded and ready 819 }); 820 }); 821 </script> 822 */ 823 requiresExt3: requiresExt3, 824 825 /** 826 * This can be added to any LABKEY page in order to load the LabKey ExtJS 3 Client API. 827 * @function 828 * @param {boolean} [immediate=true] - True to load the script immediately; false will defer script loading until the page has been downloaded. 829 * @param {requireCallback} [callback] - Callback for when all dependencies are loaded. 830 * @param {Object} [scope] - Scope of callback. 831 * @example 832 <script type="text/javascript"> 833 LABKEY.requiresExt3ClientAPI(true, function() { 834 // your code here 835 }); 836 </script> 837 */ 838 requiresExt3ClientAPI: requiresExt3ClientAPI, 839 840 /** 841 * This can be added to any LABKEY page in order to load the LabKey ExtJS 4 Client API. This primarily 842 * consists of a set of utility methods {@link LABKEY.ext4.Util} and an extended Ext.data.Store {@link LABKEY.ext4.data.Store}. 843 * It will load ExtJS 4 as a dependency. 844 * @function 845 * @param {boolean} [immediate=true] - True to load the script immediately; false will defer script loading until the page has been downloaded. 846 * @param {requireCallback} [callback] - Callback for when all dependencies are loaded. 847 * @param {Object} [scope] - Scope of callback. 848 * @example 849 <script type="text/javascript"> 850 LABKEY.requiresExt4ClientAPI(true, function() { 851 // your code here 852 }); 853 </script> 854 */ 855 requiresExt4ClientAPI: requiresExt4ClientAPI, 856 857 /** 858 * This can be added to any LABKEY page in order to load ExtJS 4. This is the preferred method to declare Ext4 usage 859 * from wiki pages. For HTML or JSP pages defined in a module, see our <a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=ext4Development">documentation</a> on declaration of client dependencies. 860 * @function 861 * @param {boolean} [immediate=true] - True to load the script immediately; false will defer script loading until the page has been downloaded. 862 * @param {requireCallback} [callback] - Callback for when all dependencies are loaded. 863 * @param {Object} [scope] - Scope of callback. 864 * @example 865 <script type="text/javascript"> 866 LABKEY.requiresExt4Sandbox(true, function() { 867 Ext4.onReady(function(){ 868 // Ext4 is loaded and ready 869 }); 870 }); 871 </script> 872 */ 873 requiresExt4Sandbox: requiresExt4Sandbox, 874 875 /** 876 * Deprecated. Use LABKEY.requiresExt3 instead. 877 * @function 878 * @private 879 */ 880 requiresExtJs: requiresExt3, 881 882 /** 883 * Loads JavaScript file(s) from the server. 884 * @function 885 * @param {(string|string[])} file - A file or Array of files to load. 886 * @param {Function} [callback] - Callback for when all dependencies are loaded. 887 * @param {Object} [scope] - Scope of callback. 888 * @param {boolean} [inOrder=false] - True to load the scripts in the order they are passed in. Default is false. 889 * @example 890 <script type="text/javascript"> 891 LABKEY.requiresScript("myModule/myScript.js", true, function() { 892 // your script is loaded 893 }); 894 </script> 895 */ 896 requiresScript: requiresScript, 897 requiresVisualization: requiresVisualization, 898 setDirty: setDirty, 899 setSubmit: setSubmit, 900 showNavTrail: showNavTrail 901 } 902 }; 903 904 } 905