1// Included Classes: JavascriptFunction JavaScriptObject JavascriptArray JavascriptNamespace ServerRequest ServerResponse EventSourceEmulator DebuggingOptions ApplicationClient 2 3 4 5function _ensureNamespace(aPathString) { 6 //Global Utility Function 7 var eachName, 8 aPath, 9 currentNS; 10 aPath = aPathString.split("."); 11 //If the AppeX Application framework (plus corecode) is present, use the ensureNamespace function. 12 //This ensures backwards compatibility for classes explicitly referenced via the "$t" root namespace. 13 if (window.$t) { 14 return $t.ensureNamespace(aPathString); 15 } 16 currentNS = window; 17 while (aPath.length > 0) { 18 eachName = aPath.shift(); 19 if (!currentNS[eachName]) { 20 currentNS[eachName] = {} 21 } 22 currentNS = currentNS[eachName]; 23 } 24} 25 26 27/***************************************************************************************** 28* AppeX.JavascriptFunction 29****************************************************************************************** 30 31*****************************************************************************************/ 32 33 34 Function.ClassName = "Function"; 35 36 Object.defineProperty(Function.prototype, 37 "functionName", 38 {value: function functionName() { 39 var aName, 40 aString, 41 afterKeyword; 42 aName = this.name; 43 if (aName) { 44 return aName; 45 } 46 aString = this.toString(); 47 afterKeyword = aString.substr("function ".length); 48 aName = afterKeyword.split("(")[0].trim(); 49 return aName; 50 } 51 }); 52 53 Object.defineProperty(Function.prototype, 54 "bindMethod", 55 {value: function bindMethod(aFunction) { 56 Object.defineProperty( 57 this.prototype, 58 aFunction.functionName(), 59 {value: aFunction, enumerable: false, writable: true}); 60 return aFunction; 61 } 62 }); 63 64 65// -------------------------------------------------------------------------------------- 66Function.bindMethod(function fullName() { 67 68 var prefix = this.Namespace ? (this.Namespace.fullName() + ".") : ""; 69 var className = this.ClassName ? this.ClassName : "<unknown>"; 70 return prefix + className 71}); 72 73 74// -------------------------------------------------------------------------------------- 75Function.bindMethod(function getNamespace() { 76 77 return this.Namespace ? this.Namespace : {} 78}); 79 80 81 82/***************************************************************************************** 83* AppeX.JavaScriptObject 84****************************************************************************************** 85 86*****************************************************************************************/ 87 88 89Object.ClassName = "Object"; 90 91// -------------------------------------------------------------------------------------- 92Object.bindMethod(function callSuper(aFunctionName, arguments) { 93 94 95 //self deprecated: #(#version '9.0' #sunset '9.1'). 96 97 var superclass, 98 aFunction; 99 100 var aString = ' DEPRECATED CODE WARNING!! "callSuper" is deprecated in AppeX. Please rewrite this call as "super.' 101 + aFunctionName 102 + '(' 103 + arguments 104 + ');"' 105 ; 106 console.warn(aString); 107 108 superclass = this.superclass(); 109 //apply the superclass implementation in the context of 'this' 110 aFunction = superclass.prototype[aFunctionName]; 111 return aFunction.apply(this, arguments); 112 113}); 114 115 116// -------------------------------------------------------------------------------------- 117Object.bindMethod(function eventTarget() { 118 119 // Simulate the object's ability to implement EventTarget API. 120 return this._eventTarget = this._eventTarget || document.createTextNode('eventTarget'); 121}); 122 123 124// -------------------------------------------------------------------------------------- 125Object.bindMethod(function getClassName() { 126 127 return this.constructor.name; 128}); 129 130 131// -------------------------------------------------------------------------------------- 132Object.bindMethod(function getFullClassName() { 133 134 return $t.getFullClassName(this); 135}); 136 137 138// -------------------------------------------------------------------------------------- 139Object.bindMethod(function subclassResponsibility(methodName) { 140 141 $t.debug(this, " must implement " + methodName); 142}); 143 144 145// -------------------------------------------------------------------------------------- 146Object.bindMethod(function superclass() { 147 148 return Object.getPrototypeOf(Object.getPrototypeOf(this)).constructor; 149 150 151}); 152 153 154// -------------------------------------------------------------------------------------- 155Object.bindMethod(function triggerEvent(type, detail){ 156 157 var event; 158 if (!this.dispatchEvent) { 159 return; 160 }; 161 event = document.createEvent("CustomEvent"); 162 event.initCustomEvent(type, true, true, detail); 163 this.dispatchEvent(event); 164}); 165 166 167 168/***************************************************************************************** 169* AppeX.JavascriptArray 170****************************************************************************************** 171 172*****************************************************************************************/ 173 174 175 Array.ClassName = "Array"; 176 177 178// -------------------------------------------------------------------------------------- 179Array.bindMethod(function eachDo(aFunction) { 180 181 for (var i = 0; i < this.length; i++) { 182 aFunction(this[i]); 183 } 184 185}); 186 187 188// -------------------------------------------------------------------------------------- 189Array.bindMethod(function includesAnyOf(anArray) { 190 191 var self = this, 192 aBoolean = false;
193 anArray.eachDo( 194 function (eAnArrayElement) { 195 if (self.includes(eAnArrayElement)) { 196 aBoolean = true; 197 } 198 } 199 ); 200 return aBoolean; 201}); 202 203 204 205/***************************************************************************************** 206* JavascriptNamespace 207****************************************************************************************** 208 209*****************************************************************************************/ 210(function () { 211Namespace = class JavascriptNamespace { 212// -------------------------------------------------------------------------------------- 213constructor(aName) { 214 215 this.name = aName; 216 217} 218 219 220// -------------------------------------------------------------------------------------- 221debug() { 222 223 $t._debuggingOptions = $t._debuggingOptions || new AppeX.DebuggingOptions(); 224 if (arguments.length !== 0) { 225 $t._debuggingOptions.applyOptions(null, arguments); 226 } 227 228} 229 230 231// -------------------------------------------------------------------------------------- 232defineNamespace(aName) { 233 234 var namespace = this[aName]; 235 236 if (namespace instanceof $t.Namespace) { 237 return namespace; 238 } 239 namespace = new $t.Namespace(aName); 240 this[aName] = namespace; 241 namespace.parent = this; 242 if (this === $t) { 243 //If a global namespace of some sort already exists (e.g. AppeX), don't just delete it, copy its properties 244 //over to the newly created namespace which will take its place as the global. 245 this.replace(namespace, window[aName]); 246 window[aName] = namespace; 247 } 248 return namespace; 249} 250 251 252// -------------------------------------------------------------------------------------- 253ensureNamespace(aName) { 254 255 var path = aName.split("."), 256 currentNamespace = this; 257 258 while (path.length > 0) { 259 var eachName = path.shift(); 260 currentNamespace = currentNamespace.defineNamespace(eachName); 261 } 262 return currentNamespace; 263} 264 265 266// -------------------------------------------------------------------------------------- 267getFullClassName(anObject) { 268 269 var theName = anObject.constructor.fullName ? anObject.constructor.fullName() : anObject.constructor.name; 270 return theName.replace("$t.", ""); 271 272} 273 274 275// -------------------------------------------------------------------------------------- 276initialize() { 277 278 //Create the root "Smalltalk" Namespace "$t" 279 $t = new Namespace("Smalltalk"); 280 $t.Namespace = Namespace; 281 282} 283 284 285// -------------------------------------------------------------------------------------- 286replace(target, source) { 287 288 // replace the properties in the target with those in the source (as in extjs apply or jquery extend) 289 // return the target 290 if (!source) { 291 return target 292 }; 293 for (var key in source) { 294 target[key] = source[key] 295 }; 296 return target; 297 298} 299 300 301}; 302 303 304 //Create the root "Smalltalk" Namespace "$t" 305 $t = new Namespace("Smalltalk"); 306 $t.Namespace = Namespace; 307 308 309})(); 310/***************************************************************************************** 311* AppeX.ServerRequest 312****************************************************************************************** 313 314*****************************************************************************************/ 315$t.ensureNamespace("AppeX"); 316 317AppeX.ServerRequest = class ServerRequest { 318// -------------------------------------------------------------------------------------- 319constructor(url, data, method, asynchronous) { 320 // Initialize the receiver. Make sure I have valid data and method. 321 // Set up a deliveryMap to map between HTTP methods and functions 322 // that should be invoked when asked to deliver the data. 323 // Set up a container object for callbacks. 324 this.url = url; 325 this.data = data || {}; 326 this.method = method || "POST"; 327 this.method = this.method.toUpperCase(); 328 this.asynchronous = !(asynchronous === false); 329 this._deliveryMap = { 330 "GET" : this.doGetData, 331 "POST" : this.doPostData, 332 "PUT" : this.doPutData, 333 "DELETE" : this.doDeleteData 334 }; 335 this._callbacks = {}; 336 return this; 337} 338 339 340// -------------------------------------------------------------------------------------- 341deliver() { 342 // Deliver the request to the server. 343 // Find which one of the delivery functions should be invoked. 344 var deliveryFunction = this._deliveryMap[this.method]; 345 this.httpRequest = new XMLHttpRequest(); 346 this.response = new $t.AppeX.ServerResponse(this); 347 deliveryFunction.apply(this, [this.url, this.data]); 348 return this.response; 349} 350 351 352// -------------------------------------------------------------------------------------- 353doDeleteData(url, data) {
354 // This is the 'DELETE' delivery function. 355 // Encode my data as a query string to include as part of the URL. 356 var request = this.httpRequest; 357 request.open("DELETE", url + "?" + this.encodeFormData(data), this.asynchronous); 358 if (this.asynchronous) { 359 this.setupCallbacks(request); 360 } 361 this.sendRequest(request, null); 362 if (!this.asynchronous) { 363 this.request_DONE(request); 364 } 365} 366 367 368// -------------------------------------------------------------------------------------- 369doGetData(url, data) { 370 // This is the 'GET' delivery function. 371 // Encode my data as a query string to include as part of the URL. 372 var request = this.httpRequest, 373 query = this.encodeFormData(data); 374 if (query.length > 0) { 375 query = "?" + query; 376 } 377 request.open("GET", url + query, this.asynchronous); 378 if (this.asynchronous) { 379 this.setupCallbacks(request); 380 } 381 this.sendRequest(request, null); 382 if (!this.asynchronous) { 383 this.request_DONE(request); 384 } 385} 386 387 388// -------------------------------------------------------------------------------------- 389doPostData(url, data) { 390 // This is the 'POST' delivery function. 391 // data is sent as x-www-form-urlencoded. 392 var request = this.httpRequest; 393 request.open("POST", url, this.asynchronous); 394 if (this.asynchronous) { 395 this.setupCallbacks(request); 396 } 397 request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); 398 this.sendRequest(request, this.encodeFormData(data)); 399 if (!this.asynchronous) { 400 this.request_DONE(request); 401 } 402} 403 404 405// -------------------------------------------------------------------------------------- 406doPutData(url, data) { 407 // This is the 'PUT' delivery function. 408 // data is sent as x-www-form-urlencoded. 409 var request = this.httpRequest; 410 request.open("PUT", url, this.asynchronous); 411 if (this.asynchronous) { 412 this.setupCallbacks(request); 413 } 414 request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); 415 this.sendRequest(request, this.encodeFormData(data)); 416 if (!this.asynchronous) { 417 this.request_DONE(request); 418 } 419} 420 421 422// -------------------------------------------------------------------------------------- 423encodeFormData(data) { 424 // Encode the properties of an object as if thery were name/value pairs from 425 // an HTML form, using application/x-www-form-urlencoded format. 426 if (!data) return ""; 427 var pairs = []; 428 $t.debug("Encoding Form Data:", data); 429 for (var name in data) { 430 if (!data.hasOwnProperty(name)) continue; 431 if (typeof data[name] === "function") continue; 432 var value = data[name].toString(); 433 name = encodeURIComponent(name).replace("%20", "+"); 434 value = encodeURIComponent(value).replace("%20", "+"); 435 pairs.push(name + "=" + value); 436 }; 437 $t.debug("Encoded Form Data:", pairs); 438 return pairs.join("&"); 439} 440 441 442// -------------------------------------------------------------------------------------- 443ensureCallback(readyState, httpStatus) { 444 var callbacks = (this._callbacks[readyState] = this._callbacks[readyState] || {}); 445 return callbacks[httpStatus] = callbacks[httpStatus] || []; 446} 447 448 449// -------------------------------------------------------------------------------------- 450processCallbacks(readyState, status) { 451 var self = this; 452 // Process the higher-level callbacks that operate on my response data. 453 this.ensureCallback(readyState, status); 454 var readyStateCallbacks = self._callbacks[readyState]; 455 var statusCallbacks = readyStateCallbacks[status] || []; 456 statusCallbacks.forEach( 457 function(callback) { 458 self.processReadyStateChange(callback); 459 } 460 ) 461} 462 463 464// -------------------------------------------------------------------------------------- 465processReadyStateChange(callback) { 466 // At this point, all low level callback processing is done by me. 467 // I let the registered callbacks operate on my response and its data. 468 // It is assumed that callback knows for which readyState/status it has been registered 469 // because I don't care any more what these are. 470 $t.debug(this, "Processing response CALLBACK:", callback); 471 callback(this.response); 472} 473 474 475// -------------------------------------------------------------------------------------- 476request_DONE(httpRequest) { 477 // XMLHttpRequest.DONE has been detected. 478 $t.debug("XMLHttpRequest.DONE. Creating ServerResponse object from XMLHttpRequest:", httpRequest); 479 var response = this.response; /* a ServerResponse */ 480 response.isReceiving = true; 481 response.httpStatus = httpRequest.status; 482 response.contentType = httpRequest.getResponseHeader("Content-Type"); 483 response.data = httpRequest.response; 484 response.isReceiving = false;
485 if (httpRequest.status === 200) { 486 response.isSuccess = true; 487 } else if (httpRequest.status >= 400) { 488 response.isError = true; 489 response.data = { 490 status: httpRequest.status, 491 statusText: httpRequest.statusText, 492 response: httpRequest.response 493 } 494 } 495 response.processData(); 496 response.isDone = true; 497 response.processCallbacks(); 498} 499 500 501// -------------------------------------------------------------------------------------- 502request_HEADERS_RECEIVED(httpRequest) { 503 // Nothing special to do here. Placeholder for future enhancements. 504} 505 506 507// -------------------------------------------------------------------------------------- 508request_LOADING(httpRequest) { 509 // Nothing special to do here. Placeholder for future enhancements. 510} 511 512 513// -------------------------------------------------------------------------------------- 514request_OPENED(httpRequest) { 515 // Nothing special to do here. Placeholder for future enhancements. 516} 517 518 519// -------------------------------------------------------------------------------------- 520request_UNSENT(httpRequest) { 521 // Nothing special to do here. Placeholder for future enhancements. 522} 523 524 525// -------------------------------------------------------------------------------------- 526sendRequest(httpRequest, data) { 527 $t.debug("Sending XMLHttpRequest:", httpRequest, data); 528 httpRequest.send(data); 529} 530 531 532// -------------------------------------------------------------------------------------- 533setupCallbacks(httpRequest) { 534 // save the current context to use inside the callback function. 535 var self = this; 536 httpRequest.onreadystatechange = function() { 537 // First, process the Http request and set some internal values from it. 538 $t.debug("Processing XMLHttpRequest.readyState:", httpRequest.readyState); 539 switch(httpRequest.readyState) { 540 //A commonly used JS library is known to make the properties XMLHttpRequest.UNSENT etc. unavailabe. 541 //Therefore if necessary default to readyState values documented at https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState 542 case XMLHttpRequest.UNSENT? XMLHttpRequest.UNSENT : 0: 543 self.request_UNSENT(httpRequest); 544 break; 545 case XMLHttpRequest.OPENED? XMLHttpRequest.OPENED : 1: 546 self.request_OPENED(httpRequest); 547 break; 548 case XMLHttpRequest.HEADERS_RECEIVED? XMLHttpRequest.HEADERS_RECEIVED : 2: 549 self.request_HEADERS_RECEIVED(httpRequest); 550 break; 551 case XMLHttpRequest.LOADING ? XMLHttpRequest.LOADING : 3: 552 self.request_LOADING(httpRequest); 553 break; 554 case XMLHttpRequest.DONE? XMLHttpRequest.DONE : 4: 555 self.request_DONE(httpRequest); 556 break; 557 }; 558 559 // Next, process the higher-level callbacks that operate on my response data. 560 self.processCallbacks(httpRequest.readyState, httpRequest.status); 561 }; 562 563} 564 565 566}; 567 568/***************************************************************************************** 569* AppeX.ServerResponse 570****************************************************************************************** 571 572*****************************************************************************************/ 573$t.ensureNamespace("AppeX"); 574 575AppeX.ServerResponse = class ServerResponse { 576// -------------------------------------------------------------------------------------- 577addContentHandler(rule, handler) { 578 // Add the rule/handler pair to my content handlers. This would allow cusome content handling 579 // beyond what the standard handlers will be. 580 // @rule: 581 // a function that will receive my contentType in the argument and will return a boolean 582 // indicating whether the rule is a match. 583 // @handler: 584 // a function that will be applied to myself when @rule returns true. 585 this._contentHandlers.push([rule, handler]); 586} 587 588 589// -------------------------------------------------------------------------------------- 590constructor(aRequest) { 591 592 this.object = null; 593 this.request = aRequest; 594 this.callbacks = { 595 "onSuccess": function() {}, 596 "onError": function(error) { 597 console.error(error, aRequest); 598 } 599 }; 600 this.initializeContentHandlers(); 601 this.setupDefaultContentHandlers(); 602} 603 604 605// -------------------------------------------------------------------------------------- 606initializeContentHandlers() { 607 this._contentHandlers = []; 608 return this; 609} 610 611 612// -------------------------------------------------------------------------------------- 613onError(callbackFunction) { 614 // if I am not done, push callbackFunction on the error callbacks queue. 615 // Otherwise, process callbackFunction immediately if I am in error. 616 // Always return myself to allow method chaining. 617 if (!this.isDone) { 618 this.callbacks.onError = callbackFunction; 619 return this; 620 } else if (this.isError) { 621 this.processCallbacks(callbackFunction); 622 } 623 return this; 624} 625 626 627// -------------------------------------------------------------------------------------- 628onSuccess(callbackFunction) { 629 // if I am not done, push callbackFunction on the sucess callbacks queue. 630 // Otherwise, process callbackFunction immediately if isSuccess. 631 // Always return myself to allow method chaining. 632 if (!this.isDone) { 633 this.callbacks.onSuccess = callbackFunction; 634 return this; 635 } else if (this.isSuccess) { 636 this.processCallbacks(callbackFunction); 637 } 638 return this; 639} 640 641 642// -------------------------------------------------------------------------------------- 643processCallbacks(callback) { 644 // depending on the status, process the right callback. 645 if (! this.isDone) { 646 throw new Error("The processCallbacks function must not be called before the response is done."); 647 return; 648 } 649 // if callback is defined, evaluate it. 650 // if callback is not defined, find which callback should be executed and re-enter this function with 651 // the proper callback in argument. 652 if (callback !== undefined) { 653 return callback(this.object); 654 } 655 656 // execute the generic version 657 if (this.isSuccess) { 658 return this.callbacks.onSuccess(this.object); 659 } 660 if (this.isError) { 661 return this.callbacks.onError(this.object); 662 } 663} 664 665 666// -------------------------------------------------------------------------------------- 667processData() { 668 var i, 669 handlers, 670 pair, 671 match; 672 if (this.data === null) { 673 return this; 674 } 675 handlers = this._contentHandlers; 676 for (i = 0; i< handlers.length; i++) { 677 pair = handlers[i]; 678 match = pair[0].call(this,this.contentType); 679 if (match) { 680 pair[1].call(this, this); 681 break; 682 } 683 } 684 //cannot just check 'if (this.object)', since legitimate json responses such as 0 and false are both falsey and would be replaced. 685 if (this.object === null) { 686 this.object = this.data; 687 } 688 this.isDone = true; 689 return this; 690} 691 692 693// -------------------------------------------------------------------------------------- 694setupDefaultContentHandlers() { 695 // Define the default content handlers. 696 // If an application wants to use custom content handlers for a response, 697 // it should call <response>.initializeContentHandlers() 698 // followed by a sequence of <response>.addContentHandler(rule, handler) calls to define its own. 699 // the rules are evaluated on a 'first win' basis, so the order is important here. 700 // For example, if a catch-all rule for text is used but 'text/html' is handled differently, 701 // the rule for 'text/html' must come before 'text/*'. 702 703 this.addContentHandler( 704 function(contentType) { 705 return contentType == undefined 706 }, 707 function(response) { 708 response.object = response.data; 709 return response; 710 } 711 ); 712 this.addContentHandler( 713 function(contentType) { 714 return contentType.match(/^text/); 715 }, 716 function(response) { 717 response.object = response.data; 718 return response; 719 } 720 ); 721 this.addContentHandler( 722 function(contentType) { 723 return contentType == "application/json"; 724 }, 725 function(response) { 726 response.object = JSON.parse(response.data); 727 return response; 728 } 729 ); 730 this.addContentHandler( 731 function(contentType) { 732 return true; 733 }, 734 function(response) { 735 response.object = response.data; 736 return response; 737 } 738 ); 739 740 return this; 741} 742 743 744}; 745 746/***************************************************************************************** 747* AppeX.EventSourceEmulator 748****************************************************************************************** 749 750*****************************************************************************************/ 751$t.ensureNamespace("AppeX"); 752 753AppeX.EventSourceEmulator = class EventSourceEmulator { 754// -------------------------------------------------------------------------------------- 755addEventListener(type, listener, useCapture) { 756// Simulate the EventTarget API 757 758 return this.eventTarget().addEventListener(type, listener, useCapture || false); 759} 760 761 762// -------------------------------------------------------------------------------------- 763close() { 764 this.aborted = true; 765 this.xhr.abort(); 766 this.readyState = EventSource.CLOSED; 767} 768 769 770// -------------------------------------------------------------------------------------- 771connect() { 772 // This is how we establish a connection 773 this.type = null; 774 this.xhr.open("GET", this.url);
775 this.xhr.setRequestHeader("Cache-Control", "no-cache"); 776 if (this.lastEventId) { 777 this.xhr.setRequestHeader("Last-Event-ID", this.lastEventId); 778 } 779 this.xhr.send(); 780} 781 782 783// -------------------------------------------------------------------------------------- 784constructor(url) { 785 786 // Initialize newly created object. 787 this.type = null; // To check property response type. 788 this.lastEventId = ""; // For resyncing with the server 789 this.retryDelay = 3000; // Delay between connection attempts 790 this.aborted = false; // Set true to give up on connecting 791 792 Object.defineProperty( 793 this, 794 "url", 795 { value: url, writable: false, enumerable: true, configurable: false } 796 ); 797 798 // Store this for callbacks: 799 var self = this; 800 801 // Create an XHR object 802 var xhr = this.xhr = new XMLHttpRequest(); 803 this.readyState = EventSource.CONNECTING; // Initial state, 804 805 // Define an event handler for it 806 xhr.onreadystatechange = function() { 807 switch(xhr.readyState) { 808 // It is safe to use EventSource. We only run this code if EventSource exists - i.e., this.constructor. 809 // When the request opens (XHR.OPENED) 810 case 1: 811 self.readyState = EventSource.OPEN; 812 self.charsReceived = 0; //for reconnect after readystate done 813 break; 814 // When a chunk of data arrives (XHR.LOADING) 815 case 3: 816 self.processData(); 817 break; 818 // When the request closes (XHR.DONE) 819 case 4: 820 self.readyState = EventSource.CONNECTING; 821 self.reconnect(); 822 break; 823 } 824 }; 825 826 // And after a slight delay to allow things settle down, establish a long-lived connection through it 827 setTimeout(function() { self.connect() }, 300 /* milliseconds */ ); 828} 829 830 831// -------------------------------------------------------------------------------------- 832createEvent(eventType, data) { 833// Create a new MessageEvent object for eventType and data. 834 835 var event = document.createEvent("MessageEvent"); 836 var origin = this.url; 837 var source = this; 838 var bubbles = false; 839 var cancelable = false; 840 841 event.initMessageEvent( 842 eventType, 843 bubbles, 844 cancelable, 845 data, 846 origin, 847 this.lastEventId, 848 source); 849 850 return event; 851} 852 853 854// -------------------------------------------------------------------------------------- 855dispatchEvent(event) { 856// Simulate the EventTarget API 857 858 if (! this._eventTarget) { return null }; 859 return this._eventTarget.dispatchEvent(event); 860} 861 862 863// -------------------------------------------------------------------------------------- 864processData() { 865// Each time data arrives, process it and trigger the onmessage handler 866// This function handles the details of the Server-Sent Events protocol 867 868 if (!this.type) { 869 // Check the response type if we haven"t already 870 this.type = this.xhr.getResponseHeader("Content-Type"); 871 if (this.type !== "text/event-stream") { 872 return this.close(); 873 } 874 } 875 876 var eventName = "message"; // The default type field of our event objects 877 var data = ""; // Holds message data 878 879 // Keep track of how much we"ve received and get only the 880 // portion of the response that we haven"t already processed. 881 var chunk = this.xhr.responseText.substring(this.charsReceived); 882 this.charsReceived = this.xhr.responseText.length; 883 884 // Break the chunk of text into lines and iterate over them. 885 var lines = chunk.replace(/(\r\n|\r|\n)$/, "").split(/\r\n|\r|\n/); 886 887 // Read lines and process the keys from the data stream. 888 for(var i = 0; i < lines.length; i++) { 889 var line = lines[i]; 890 var pos = line.indexOf(":"); 891 var name; 892 var value=""; 893 if (pos == 0) continue; // Ignore comments 894 if (pos > 0) { // field name:value 895 name = line.substring(0,pos); 896 value = line.substring(pos+1); 897 if (value.charAt(0) == " ") { 898 value = value.substring(1); 899 } 900 } else { 901 name = line; // field name only 902 } 903 904 switch(name) { 905 case "event": eventName = value; break; 906 case "data": data += value + "\n"; break; 907 case "id": this.lastEventId = value; break; 908 case "retry": this.retryDelay = parseInt(value) || 3000; break; 909 default: break; // Ignore any other line 910 } 911 912 // A blank line means send the event 913 if (line === "") { 914 // If no data, try next. 915 if (data === "") continue; 916 917 // Chop trailing newline if there is one 918 if (data.charAt(data.length-1) == "\n") { 919 data = data.substring(0, data.length-1); 920 }; 921 922 // Create a new event and dispatch it. 923 var event = this.createEvent(eventName, data); 924 eventName == 'message' && this.onmessage && this.onmessage(event); 925 this.dispatchEvent(event); 926 data = "";
927 continue; 928 } 929 } 930} 931 932 933// -------------------------------------------------------------------------------------- 934reconnect() { 935// If the connection closes normally, wait a second and try to restart 936 if (this.aborted) return; // Don't reconnect after an abort 937 if (this.xhr.status >= 300) return; // Don't reconnect after an error 938 939 // Wait a bit, then reconnect 940 var self = this; 941 setTimeout( 942 function() { 943 self.connect() 944 }, 945 this.retryDelay 946 ); 947} 948 949 950// -------------------------------------------------------------------------------------- 951removeEventListener(type, listener, useCapture) { 952// Simulate the EventTarget API 953 954 if (! this._eventTarget) { return null }; 955 return this._eventTarget.removeEventListener(type, listener, useCapture || false) 956} 957 958 959}; 960 961 962 $t.AppeX.EventSourceEmulator.CONNECTING = 0; 963 $t.AppeX.EventSourceEmulator.OPEN = 1; 964 $t.AppeX.EventSourceEmulator.CLOSED = 2; 965 966 967// Emulate the EventSource API for browsers that do not support it. 968// Requires an XMLHttpRequest that sends readystatechange events whenever 969// there is new data written to a long-lived HTTP connection. Note that 970// this is not a complete implementation of the API: it does not support the 971// readyState property, the close() method, nor the open and error events. 972// Also event registration for message events is through the onmessage 973// property only--this version does not define an addEventListener method. 974 if (window.EventSource === undefined) { 975 // If EventSource is not defined, 976 window.EventSource = $t.AppeX.EventSourceEmulator; 977 } 978 979 980/***************************************************************************************** 981* AppeX.DebuggingOptions 982****************************************************************************************** 983 984*****************************************************************************************/ 985$t.ensureNamespace("AppeX"); 986 987AppeX.DebuggingOptions = class DebuggingOptions { 988// -------------------------------------------------------------------------------------- 989applyOptions(object, arrayOfArguments) { 990// Use my options to notify the user with the arguments passed. 991 var label; 992 if(object) { 993 label = object.getClassName(); 994 } 995 else { 996 label = "AppeX System" 997 } 998 if (this.log) { 999 console.log("%c %s Log ", "color:darkBlue; background:lightGrey; font-size: larger", label); 1000 if(object) { 1001 console.log(object); 1002 } 1003 for (var i = 0; i < arrayOfArguments.length; i++) { 1004 var argument = arrayOfArguments[i]; 1005 if (typeof argument === 'object') { 1006 console.dir(argument); 1007 } else { 1008 console.log(argument) 1009 } 1010 } 1011 } 1012 1013 if (this.trace) { 1014 console.log("%c %s Trace ", "color:red; background:lightGrey; font-size: larger", label); 1015 if(object) { 1016 console.log(object); 1017 } 1018 if (console.trace) { 1019 console.trace.apply(console, arrayOfArguments); 1020 } else { 1021 console.log.apply(console, arrayOfArguments); 1022 } 1023 } 1024 1025 if (this.debugger) { 1026 debugger; 1027 } 1028 1029} 1030 1031 1032// -------------------------------------------------------------------------------------- 1033constructor() { 1034 1035 this.off(); 1036} 1037 1038 1039// -------------------------------------------------------------------------------------- 1040off() { 1041 this.log = false; 1042 this.trace = false; 1043 this.debugger = false; 1044} 1045 1046 1047// -------------------------------------------------------------------------------------- 1048option(option, boolean) { 1049 1050 // Set or get a debugging option 1051 var index, 1052 levels = ["log", "trace", "debugger"]; 1053 if (boolean === undefined) { 1054 return this[option]; 1055 } 1056 index = levels.indexOf(option); 1057 if (index != -1) { 1058 this[option] = boolean; 1059 } 1060 return this[option]; 1061} 1062 1063 1064}; 1065 1066/***************************************************************************************** 1067* AppeX.ApplicationClient 1068****************************************************************************************** 1069 1070*****************************************************************************************/ 1071$t.ensureNamespace("AppeX"); 1072 1073AppeX.ApplicationClient = class ApplicationClient { 1074// -------------------------------------------------------------------------------------- 1075_debugger(boolean) { 1076 return this._debugging("debugger", boolean); 1077} 1078 1079 1080// -------------------------------------------------------------------------------------- 1081_debugging(option, boolean) { 1082// Get or set a debugging option named @option. 1083// If boolean is undefined, it's a 'get', otherwise 'set'. 1084// If boolean is undefined and I haven't set my debugging options, the return value will be false. 1085 if (boolean === undefined) { 1086 if (!this._debuggingOptions) { 1087 return false;
1088 } 1089 } 1090 this._debuggingOn(); 1091 return this._debuggingOptions.option(option, boolean); 1092} 1093 1094 1095// -------------------------------------------------------------------------------------- 1096_debuggingOff() { 1097 this._debuggingOptions && this._debuggingOptions.off(); 1098} 1099 1100 1101// -------------------------------------------------------------------------------------- 1102_debuggingOn() { 1103 this._debuggingOptions = this._debuggingOptions || new AppeX.DebuggingOptions(); 1104} 1105 1106 1107// -------------------------------------------------------------------------------------- 1108_log(boolean) { 1109 return this._debugging("log", boolean); 1110} 1111 1112 1113// -------------------------------------------------------------------------------------- 1114_setLanguage(aString, aCallback) { 1115// aString is the code of the language to be set, e.g., 'en' 1116// aCallback is an optional anonymous function to be executed after the language has been successfully set. 1117 1118 var self = this, 1119 aCatalogFromStorage, 1120 response; 1121 1122 //first check if the catalog is already present in memory 1123 if ($t._catalogs[aString]) { 1124 this._language = aString; 1125 //put the name of the current language in sesssion storage 1126 this.storeItemInStorage('appex_language', this._language, window.sessionStorage); 1127 aCallback && aCallback(); 1128 return; 1129 } else { 1130 //if the catalog for the language is not in memory, try to retrieve the data from session storage 1131 aCatalogFromStorage = this.getItemFromStorage('appex_' + aString, window.sessionStorage); 1132 if (aCatalogFromStorage) { 1133 this._language = aString; 1134 //cache the catalog data retrieved from storage in memory 1135 $t._catalogs[this._language] = aCatalogFromStorage; 1136 //put the name of the current language in sesssion and local storage 1137 this.storeItemInStorage('appex_language', this._language, window.sessionStorage); 1138 this.storeItemInStorage('appex_language', this._language, window.localStorage); 1139 aCallback && aCallback(); 1140 return; 1141 } 1142 } 1143 1144 response = this.asyncMessageToServer("_appex_catalogForLanguage", aString) 1145 .onError( function (error) { console.log("Error:", error) }) 1146 .onSuccess( function (data) { 1147 self._language = aString; 1148 $t._catalogs[self._language] = data; 1149 //put the current language in session and local storage 1150 self.storeItemInStorage('appex_language', self._language, window.sessionStorage); 1151 self.storeItemInStorage('appex_language', self._language, window.localStorage); 1152 //put the corresponding catalog in session storage 1153 self.storeItemInStorage('appex_' + self._language, data, window.sessionStorage); 1154 aCallback && aCallback(); 1155 }); 1156 1157} 1158 1159 1160// -------------------------------------------------------------------------------------- 1161_trace(boolean) { 1162 return this._debugging("trace", boolean); 1163} 1164 1165 1166// -------------------------------------------------------------------------------------- 1167_translate(key, defaultString, catalogId) { 1168 1169 //The only required parameter is 'key'. 1170 //If there is a catalog value for 'key', then that value will be returned. 1171 //If there is no catalog value for 'key', but a 'defaultString' is supplied, the 'defaultValue' will be returned. 1172 //If there is no catalog entry for 'key', and no 'defaultString' is supplied, then the 'key' itself will be returned. 1173 //If there is no catalog, then the key will be returned. 1174 1175 //In summary, one of the following three possible values will be returned, in order of preference: 1176 //1. value looked up in catalog 2.supplied default string 3.key. 1177 1178 1179 var aCatalog, 1180 aLocale; 1181 1182 aLocale = this._language || 'en'; 1183 1184 //in first cut ignore catalogId 1185 aCatalog = $t._catalogs[aLocale]; 1186 if (!aCatalog) { 1187 console.log('There is no user messages catalog present for locale ' + '"' + this._language + '" or for locale "' + aLocale + '". Adding an empty catalog.' ); 1188 $t._catalogs[aLocale] = {}; 1189 return key; 1190 } 1191 return aCatalog[key] || defaultString || key; 1192 1193} 1194 1195 1196// -------------------------------------------------------------------------------------- 1197addEventListener(type, listener, useCapture) { 1198// Simulate the EventTarget API 1199 1200 return this.eventTarget().addEventListener(type, listener, useCapture || false); 1201} 1202 1203 1204// -------------------------------------------------------------------------------------- 1205asyncMessageToServer(path, messageArguments, messageOptions) { 1206// Force the message to be sent asynchronously. 1207 1208 var self = this, 1209 response, 1210 args, 1211 options; 1212 1213 args = messageArguments || []; 1214 options = messageOptions || {}; 1215 1216 options.async = true; 1217 1218 response = this.messageToServer(path, args, options); 1219 response.onError(function(error) { 1220 self.errorAsyncRequestFailed(path, error); 1221 }); 1222 1223 return response; 1224} 1225 1226 1227// -------------------------------------------------------------------------------------- 1228buildHtml() { 1229// The main function for building the HTML. 1230// Do nothing by default. 1231// Subclasses will want to override. 1232 1233 return this; 1234} 1235 1236 1237// -------------------------------------------------------------------------------------- 1238cancelInterestInAllEvents() { 1239// Cancel interest in all previously registered events. 1240 1241 if (!this.serverEvents) { 1242 return this; 1243 } 1244 1245 var 1246 i, 1247 eventNames = Object.keys(this.serverEvents); 1248 for (var eachEvent in eventNames) { 1249 this.cancelInterestInEvent(eventNames[eachEvent]); 1250 }; 1251 1252 return this; 1253} 1254 1255 1256// -------------------------------------------------------------------------------------- 1257constructor(options){ 1258 1259 var anObject = this; 1260 1261 // Keep my url so I know where to send my message. 1262 // Set the options that may have been passed to the constructor function, 1263 if (document.baseURI) { 1264 anObject.url = document.baseURI; 1265 } else { 1266 //the full baseURI would be as below for IE 1267 anObject.url = document.location.protocol + "//" + document.location.host + document.location.pathname + '/'; 1268 } 1269 anObject.queryParameters = anObject.getQueryParameters(); 1270 anObject.hashPath = anObject.getHashPath(); 1271 $t.replace(anObject, options); 1272 return anObject; 1273 1274 1275} 1276 1277 1278// -------------------------------------------------------------------------------------- 1279cancelInterestInEvent(eventName) { 1280// If I haven't done that before, I will post a client event notifying server 1281// that I am no longer interested in @eventName. 1282// Then I remove @eventName from my serverEvents. 1283 var self = this; 1284 if (this.serverEvents && this.serverEvents[eventName]) { 1285 this.postClientEvent("cancelInterestInEvent", eventName); 1286 this.serverEvents[eventName].forEach(function(listener){ 1287 self.eventChannel.source.removeEventListener (eventName,listener) 1288 }); 1289 delete this.serverEvents[eventName] 1290 } 1291} 1292 1293 1294// -------------------------------------------------------------------------------------- 1295closeEventChannel() { 1296 1297 $t.debug("Closing Event Channel"); 1298 this.eventChannel.source.close(); 1299 this.cancelInterestInAllEvents(); 1300 delete this.eventChannel.source; 1301 delete this.eventChannel; 1302 delete this.serverEvents; 1303 return this; 1304} 1305 1306 1307// -------------------------------------------------------------------------------------- 1308createEventChannel() { 1309// Create the eventChannel object to which a source will be attached later. 1310 this.eventChannel = { 1311 postUrl: "_appex_postClientEvent" 1312 }; 1313 1314 return this; 1315} 1316 1317 1318// -------------------------------------------------------------------------------------- 1319createEventSource(path) { 1320// Create a new EventSource with @url as a base and my sessionId in the query string. 1321 return new EventSource(this.url + path + "?" + this.sessionQuery()); 1322} 1323 1324 1325// -------------------------------------------------------------------------------------- 1326debug() { 1327// If I have set up debugging options, use them to do logging, debugging, etc. 1328 var options = this._debuggingOptions; 1329 if (options) { 1330 options.applyOptions(this, arguments); 1331 } 1332} 1333 1334 1335// -------------------------------------------------------------------------------------- 1336DELETE(path, parameters) { 1337// 'delete' is a Javascript reserved word. I have to be named with capital letters. 1338 return this.asyncMessageToServer( 1339 path, 1340 parameters, 1341 { method: 'DELETE' } 1342 ); 1343} 1344 1345 1346// -------------------------------------------------------------------------------------- 1347dispatchEvent(event) { 1348// Simulate the EventTarget API 1349 1350 if (! this._eventTarget) { return null }; 1351 return this._eventTarget.dispatchEvent(event); 1352} 1353 1354 1355// -------------------------------------------------------------------------------------- 1356errorAsyncRequestFailed(path, error) { 1357 $t.debug("Request Failed. Response:", path, error.response); 1358} 1359 1360 1361// -------------------------------------------------------------------------------------- 1362GET(path, parameters) { 1363 1364 return this.asyncMessageToServer( 1365 path, 1366 parameters, 1367 { method: 'GET' } 1368 ); 1369} 1370 1371 1372// -------------------------------------------------------------------------------------- 1373getHashPath() { 1374// Get the current document location hash string and parse it into an Array that represents the hash. 1375 1376 var hash = document.location.hash, 1377 components; 1378 1379 if(hash.length === 0) { 1380 return []; 1381 } 1382 1383 hash = hash.split("#"); 1384 // Ignore all but the last. The hash string must have only a single hash sign, and we are only interested in what comes after it. 1385 // e.g., "#list/someValue" -> ["", "list/someValue" ] 1386 hash = hash[hash.length - 1]; 1387 // Split the hash into path components, and return it. 1388 // e.g., ["list", "someValue"] 1389 components = hash.split("/"); 1390 for (var i = 0; i < components.length; i++) { 1391 components[i] = decodeURIComponent(components[i]); 1392 } 1393 return components; 1394} 1395 1396 1397// -------------------------------------------------------------------------------------- 1398getItemFromStorage(key, storage) { 1399 // Get an item under the @key from the browser @storage.
1400 // @storage can be sessionStorage or localStorage. 1401 // return an Object or null 1402 var stringInStorage, 1403 returnString; 1404 stringInStorage = storage.getItem(key); 1405 //do not throw an error and halt execution trying to parse a non-json string such as 'hi' 1406 if (this.isJSONString(stringInStorage)) { 1407 return JSON.parse(stringInStorage); 1408 } 1409 return stringInStorage 1410} 1411 1412 1413// -------------------------------------------------------------------------------------- 1414getQueryParameters() { 1415// Get the current document location search string and parse it into an object with keys and values. 1416 1417 var search = document.location.search, 1418 query, 1419 pairs, 1420 eachPair, 1421 eachKey, 1422 eachValue, 1423 parameters = {}; 1424 1425 if(search.length === 0) { 1426 return parameters; 1427 } 1428 1429 query = search.split("?"); 1430 // Ignore all but the last. The query string must have only a single questionmark, and we are only interested in what comes after it. 1431 // e.g., "?id=123&key=value" -> ["", "id=123&key=value" ] 1432 query = query[query.length - 1]; 1433 // Split the query into key/value pairs. 1434 // e.g., ["id=123", "key=value"] 1435 pairs = query.split("&"); 1436 1437 for (var i = 0; i < pairs.length; i++) { 1438 // Split each pair into key and value. 1439 // e.g., ["id", "123"] 1440 eachPair = pairs[i].split("="); 1441 eachKey = decodeURIComponent(eachPair[0]); 1442 eachValue = decodeURIComponent(eachPair[1]); 1443 parameters[eachKey] = eachValue; 1444 } 1445 return parameters; 1446} 1447 1448 1449// -------------------------------------------------------------------------------------- 1450getSessionFromStorage() { 1451 var storedSession = this.getItemFromStorage("appex_session", window.sessionStorage); 1452 $t.debug("Session item:", storedSession); 1453 var session = {}; 1454 if (storedSession) { 1455 $t.replace(session, storedSession); 1456 } 1457 return session; 1458} 1459 1460 1461// -------------------------------------------------------------------------------------- 1462handleEventSourceError(eventSource,event) { 1463// An error occurred on @eventSource. A likely cause would be a network error or server shutdown. 1464// By default, just log it to the console. 1465 console.log("EventSource error occurred: ", eventSource, event); 1466 return this; 1467} 1468 1469 1470// -------------------------------------------------------------------------------------- 1471handleServerMessage(messageEvent) { 1472 // Process a generic server message. No event: has been specified in the response. 1473 // I do nothing but subclasses may override. 1474 $t.debug("Server message event:", messageEvent); 1475} 1476 1477 1478// -------------------------------------------------------------------------------------- 1479handleServerShutdown(duration) { 1480 // this is a default server shutdown handler. 1481 this.closeEventChannel(); 1482 alert ("Server is shutting down in "+ duration + " seconds"); 1483 return this 1484} 1485 1486 1487// -------------------------------------------------------------------------------------- 1488handleSessionExpired() { 1489 // this is a default 'session expired' handler. 1490 // I am no longer interested in 'SessionExpired' event (no point). 1491 // If I establish a new session, I have to re-register interest again. 1492 this.cancelInterestInEvent('SessionExpired'); 1493 this.closeEventChannel(); 1494 this.removeItemFromStorage("appex_session", sessionStorage); 1495 this.showSessionExpired(this.session); 1496 return this 1497} 1498 1499 1500// -------------------------------------------------------------------------------------- 1501handleSessionNotFound(data) { 1502 alert("Session not found: " + data); 1503 this.closeEventChannel(); 1504} 1505 1506 1507// -------------------------------------------------------------------------------------- 1508handleSessionReady() { 1509 this.cancelInterestInEvent("SessionReady"); 1510 this.buildHtml(); 1511} 1512 1513 1514// -------------------------------------------------------------------------------------- 1515initialize(options) { 1516// Keep my url so I know where to send my message. 1517// Set the options that may have been passed to the constructor function, 1518 if (document.baseURI) { 1519 this.url = document.baseURI; 1520 } else { 1521 //the full baseURI would be as below for IE 1522 this.url = document.location.protocol + "//" + document.location.host + document.location.pathname + '/'; 1523 } 1524 this.queryParameters = this.getQueryParameters(); 1525 this.hashPath = this.getHashPath(); 1526 $t.replace(this, options); 1527} 1528 1529 1530// -------------------------------------------------------------------------------------- 1531initializeUrl() { 1532 // Keep my url so I know where to send my message. 1533 var elements = document.getElementsByTagName("base"); 1534 var url = elements.length 1535 ? elements[elements.length - 1].href 1536 : document.URL; 1537 this.url = url; 1538} 1539 1540 1541// -------------------------------------------------------------------------------------- 1542install(options) { 1543// Install the application, then process the installQueue. 1544 this.installApplication(options); 1545 this.buildHtml(); 1546 this.postInstall(); 1547} 1548 1549 1550// -------------------------------------------------------------------------------------- 1551installApplication(options) { 1552// Install myself as the sole application. 1553// Set whatever options I may have received. 1554 $t.application = this; 1555 this.constructor.application = this; 1556 $t.replace(this, options); 1557 return this; 1558} 1559 1560 1561// -------------------------------------------------------------------------------------- 1562installSession(options) { 1563 // If I am linking a session, I need to add session options to myself. 1564 // Typically, that would be "sessionKey" session parameter but the application may add others. 1565 // See Application class>>bodyInstallApplication for sample construction of sender 1566 1567 var self = this, 1568 session, 1569 response; 1570 1571 this.installApplication(options);
1572 session = this.getSessionFromStorage(); 1573 response = this.linkSession(session); 1574 response 1575 .onError(function(error){ 1576 console.error("Session link primitive failed", session, error) 1577 }) 1578 .onSuccess(function() { 1579 $t.debug("Install Session. Server Response for _appex_linkSession:", response); 1580 self.installSessionSuccess(session, response.object); 1581 self.postInstall(); 1582 }); 1583 return this; 1584} 1585 1586 1587// -------------------------------------------------------------------------------------- 1588installSessionSuccess(session, data) { 1589 1590 this.updateSessionData(session, data); 1591 this.createEventChannel(); 1592 this.registerServerEvents(); 1593 this.openEventChannel(); 1594 1595 return this; 1596} 1597 1598 1599// -------------------------------------------------------------------------------------- 1600isJSONString(aString) { 1601 try { 1602 JSON.parse(aString); 1603 } catch (error) { 1604 console.log('WARNING: Handled Error: ' + error); 1605 return false; 1606 } 1607 return true; 1608} 1609 1610 1611// -------------------------------------------------------------------------------------- 1612linkSession(session) { 1613 1614 return this.primitiveToServer("_appex_linkSession", session, {async: true}); 1615 1616} 1617 1618 1619// -------------------------------------------------------------------------------------- 1620messageToServer(path, messageArguments, messageOptions) { 1621 /* Extract the callback options from messageArguments, 1622 * determine the type of message (synchrnous, asynchronous), 1623 * and invoke the function appropriate for the message. 1624 */ 1625 1626 var options = messageOptions || {}; 1627 var argumentArray = messageArguments || []; 1628 var method = options.method || "POST"; 1629 method = method.toUpperCase(); 1630 var asynchronous = (options.async == true); 1631 var data = {}; 1632 if (this.sessionKey) { 1633 var sessionKey = this.sessionKey; 1634 data[sessionKey] = this[sessionKey]; 1635 } 1636 var jsonArgs = JSON.stringify(argumentArray); 1637 $t.debug("Sending Request Path:", path); 1638 $t.debug("Sending Request Arguments:", argumentArray, jsonArgs); 1639 if (jsonArgs != "{}" && jsonArgs != "[]") { 1640 data.arguments = jsonArgs; 1641 } 1642 $t.debug("Sending Request DATA:", data); 1643 1644 return this.serverRequest(method, path, data, asynchronous); 1645} 1646 1647 1648// -------------------------------------------------------------------------------------- 1649onServerEvent(eventName, callback) { 1650// Register interest in @eventName with the server. 1651// Add an event listener to my eventChannel source, and return the listener function. 1652 1653 var self = this, 1654 listener, 1655 addInterestInSource, 1656 sourceReadyHandler; 1657 1658 // First, register interest on the server itself: 1659 this.registerInterestInEvent(eventName); 1660 1661 // Keep track of the listener function for a removal when no longer interested. 1662 listener = function(event) { 1663 callback(JSON.parse(event.data), event); 1664 }; 1665 this.serverEvents[eventName].push(listener); 1666 1667 if(this.eventChannel.source) { 1668 // EventSource has been set up, simply run the function. 1669 this.eventChannel.source.addEventListener (eventName,listener); 1670 } else { 1671 // Create a callback function in which I run the addInterestInSource. 1672 sourceReadyHandler = function(e) { 1673 // Don't forget to remove the event listener itself when invoked, to prevent a possibility of multiple callbacks. 1674 self.eventChannel.eventTarget().removeEventListener("EventSourceReady", sourceReadyHandler); 1675 self.eventChannel.source.addEventListener (eventName,listener); 1676 }; 1677 // Now register the callback function. 1678 this.eventChannel.eventTarget().addEventListener("EventSourceReady", sourceReadyHandler) 1679 } 1680 return listener; 1681} 1682 1683 1684// -------------------------------------------------------------------------------------- 1685openEventChannel() { 1686 var self = this; 1687 1688 var eventSource = this.createEventSource( "_appex_getServerEvents" ); 1689 eventSource.onmessage = function(event) { 1690 self.handleServerMessage(event); 1691 }; 1692 eventSource.onopen = function(event) { 1693 console.log('!!! openEventChannel ', eventSource, event); 1694 //console.log(event); 1695 }; 1696 eventSource.onerror = function(event) { 1697 self.handleEventSourceError(eventSource,event); 1698 }; 1699 this.eventChannel.source = eventSource; 1700 this.eventChannel.eventTarget().triggerEvent("EventSourceReady"); 1701 1702 return this; 1703} 1704 1705 1706// -------------------------------------------------------------------------------------- 1707POST(path, parameters) { 1708 return this.asyncMessageToServer( 1709 path, 1710 parameters, 1711 { method: 'POST' } 1712 ); 1713} 1714 1715 1716// -------------------------------------------------------------------------------------- 1717postClientEvent(eventName, data) { 1718// Post event with @eventName to the server. 1719// This allows the server to notify its other clients about 1720// the event having occurred here. 1721 1722 var self = this, 1723 response; 1724 response = this.asyncMessageToServer( 1725 this.eventChannel.postUrl, 1726 { 1727 eventName: eventName, 1728 data: data 1729 } 1730 ) 1731 .onError(function (error) { 1732 self.postClientEventError(error); 1733 }) 1734 .onSuccess(function () { 1735 self.postClientEventSuccess(response); 1736 }); 1737 return response; 1738} 1739 1740 1741// -------------------------------------------------------------------------------------- 1742postClientEventError(error) { 1743 1744 $t.debug('Log error by default. Override postClientEventError in subclasses. ' + error.response); 1745} 1746 1747 1748// -------------------------------------------------------------------------------------- 1749postClientEventSuccess(response) { 1750 //by default do nothing. Override in subclasses 1751 $t.debug("Client Event Success:", response); 1752} 1753 1754 1755// -------------------------------------------------------------------------------------- 1756postInstall() { 1757// Hook to do post-install actions. Do nothing by default. 1758 var self = this; 1759 document.body.addEventListener( 1760 "unload", 1761 function(event) { 1762 self.uninstall(); 1763 } 1764 ); 1765 return this; 1766} 1767 1768 1769// -------------------------------------------------------------------------------------- 1770primitiveToServer(message, primitiveArguments, optionsArg) { 1771// Primitive are like messages except they go directly to the subclass of ApplicationServer which serves the application. 1772// A primitive will have a string specifying the message, arguments, and ajax options (i.e.,method, sync/async, etc.) 1773// A primitive is session independent (no prior session establishment is necessary) 1774 1775 $t.debug("Sending PRIMITIVE to server:", message, primitiveArguments, optionsArg); 1776 var options, 1777 method; 1778 1779 options = optionsArg || {}; 1780 method = options.method || "POST"; 1781 method = method.toUpperCase(); 1782 var asynchronous = !(options.async == false); 1783 var data = { 1784 "arguments": JSON.stringify(primitiveArguments) 1785 };
1786 return this.serverRequest(method, message, data, asynchronous); 1787} 1788 1789 1790// -------------------------------------------------------------------------------------- 1791PUT(path, parameters) { 1792 return this.asyncMessageToServer( 1793 path, 1794 parameters, 1795 { method: 'PUT' } 1796 ); 1797} 1798 1799 1800// -------------------------------------------------------------------------------------- 1801registerInterestInEvent(eventName) { 1802// If I haven't done that before, I will post a client event notifying server 1803// that I am interested in @eventName. 1804// Then I add @eventName property to my serverEvents with counter set to zero. 1805 1806 this.serverEvents = this.serverEvents || {}; 1807 if (!this.serverEvents[eventName]) { 1808 this.postClientEvent("registerInterestInEvent", eventName); 1809 this.serverEvents[eventName] = []; 1810 } 1811} 1812 1813 1814// -------------------------------------------------------------------------------------- 1815registerServerEvents() { 1816 1817 this.registerSessionReady(); 1818 this.registerSessionExpired(); 1819 this.registerSessionNotFound(); 1820 this.registerServerShutdown(); 1821 return this; 1822} 1823 1824 1825// -------------------------------------------------------------------------------------- 1826registerServerShutdown() { 1827 1828 var self = this; 1829 this.onServerEvent( 1830 'ServerShutdown', 1831 function(data) { 1832 self.handleServerShutdown(data); 1833 } 1834 ); 1835 1836 return this; 1837} 1838 1839 1840// -------------------------------------------------------------------------------------- 1841registerSessionExpired() { 1842 1843 var self = this; 1844 this.onServerEvent( 1845 'SessionExpired', 1846 function(data) { 1847 self.handleSessionExpired(data); 1848 } 1849 ); 1850 1851 return this; 1852} 1853 1854 1855// -------------------------------------------------------------------------------------- 1856registerSessionNotFound() { 1857 1858 var self = this; 1859 this.onServerEvent( 1860 'SessionNotFound', 1861 function(data) { 1862 self.handleSessionNotFound(data); 1863 } 1864 ); 1865 1866 return this; 1867} 1868 1869 1870// -------------------------------------------------------------------------------------- 1871registerSessionReady() { 1872 1873 var self = this; 1874 1875 this.onServerEvent( 1876 "SessionReady", 1877 function(event) { 1878 self.handleSessionReady(); 1879 } 1880 ); 1881 1882 return this; 1883} 1884 1885 1886// -------------------------------------------------------------------------------------- 1887removeEventListener(type, listener, useCapture) { 1888// Simulate the EventTarget API 1889 1890 if (! this._eventTarget) { return null }; 1891 return this._eventTarget.removeEventListener(type, listener, useCapture || false) 1892} 1893 1894 1895// -------------------------------------------------------------------------------------- 1896removeItemFromStorage(key, storage) { 1897 // Get an item under the @key from the browser @storage, then remove it. 1898 // @storage can be sessionStorage or localStorage. 1899 var item = this.getItemFromStorage(key, storage); 1900 storage.removeItem(key); 1901 return item; 1902} 1903 1904 1905// -------------------------------------------------------------------------------------- 1906serverRequest (method, path, data, asynchronous) { 1907 /* Build a generic ajax message that will handle both synchronous and asynchronous requests, 1908 * on any browser(?). Tested on Chrome, Firefox, IE. 1909 */ 1910 1911 var application = this, 1912 url = this.url, 1913 returnData = {}; 1914 var questionMarkIndex = url.indexOf('?'); 1915 var urlWithoutQuery; 1916 //if url contains a query string, insert the extension to the path before the question mark
1917 if(questionMarkIndex > 0) { 1918 urlWithoutQuery = url.slice(0, questionMarkIndex); 1919 //add trailing slash if missing 1920 if (urlWithoutQuery[urlWithoutQuery.length-1] != "/") { urlWithoutQuery += "/" }; 1921 url = urlWithoutQuery + path + url.slice(questionMarkIndex); 1922 } else { 1923 //add trailing slash if missing 1924 if (url[url.length-1] != "/") { url += "/" }; 1925 url += path; 1926 } 1927 var request = new AppeX.ServerRequest(url, data, method, asynchronous); 1928 $t.debug("Sending Server Request",request); 1929 request.deliver(); 1930 $t.debug("Received Server Response", request.response); 1931 return request.response; 1932} 1933 1934 1935// -------------------------------------------------------------------------------------- 1936sessionQuery() { 1937 if (!this.session) return null; 1938 var sessionKey = this.session.sessionKey; 1939 return sessionKey + "=" + this[sessionKey]; 1940} 1941 1942 1943// -------------------------------------------------------------------------------------- 1944setTheme(url) { 1945 // Set the href of the #appex-theme-css element to @url. 1946 // This will cause the theme link CSS to be reloaded and the page re-styled according to the new style sheet. 1947 var link = document.getElementById("appex-theme-css"); 1948 1949 if (!link) { 1950 return; 1951 } 1952 1953 link.href = url; 1954} 1955 1956 1957// -------------------------------------------------------------------------------------- 1958showSessionExpired(session) { 1959 alert ("Session expired."); 1960 return this 1961} 1962 1963 1964// -------------------------------------------------------------------------------------- 1965storeItemInStorage(key, item, storage) { 1966 // Store an item under the @key in the browser @storage. 1967 // @storage can be sessionStorage or localStorage. 1968 storage.setItem(key, JSON.stringify(item)); 1969} 1970 1971 1972// -------------------------------------------------------------------------------------- 1973syncMessageToServer(path, messageArguments, messageOptions) { 1974 /* Force the message to be sent synchronously. */ 1975 1976 var args, 1977 options; 1978 1979 args = messageArguments || []; 1980 options = messageOptions || {}; 1981 1982 options.async = false; 1983 1984 return this.messageToServer(path, args, options).object; 1985} 1986 1987 1988// -------------------------------------------------------------------------------------- 1989uninstall() { 1990// Tear down things that need to be torn down when the window gets closed, reloaded, or navigated away. 1991 1992 this.closeEventChannel(); 1993} 1994 1995 1996// -------------------------------------------------------------------------------------- 1997updateSessionData(session, data) { 1998// Update a @session object from @data. 1999 $t.replace(session, data); 2000 $t.debug("Update Session:", session); 2001 this.storeItemInStorage("appex_session", session, window.sessionStorage); 2002 // For easy access, store the value of 'sessionId' in myself. 2003 this[session.sessionKey] = session[session.sessionKey]; 2004 this.session = session; 2005 return this; 2006} 2007 2008 2009}; 2010
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.