/**
* @preserve
* LIGHTSTREAMER - www.lightstreamer.com
* Lightstreamer Web Client
* Version 8.1.0-beta3 build 1791.8
* Copyright (c) Lightstreamer Srl. All Rights Reserved.
* Licensed under the Apache License, Version 2.0
* See http://www.apache.org/licenses/LICENSE-2.0
* Contains: Chart, DynaGrid, SimpleChartListener, StaticGrid,
* StatusWidget
*/
;(function() {
var lightstreamerExports = (function () {
'use strict';
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var IllegalStateException = /*@__PURE__*/(function() {
/**
* Constructs an IllegalStateException with the specified detail message.
* @constructor
*
* @param {String} message short description of the error.
*
* @exports IllegalStateException
* @class Thrown to indicate that a method has been invoked at an illegal or
* inappropriate time or that the internal state of an object is incompatible
* with the call.
*
Use toString to extract details on the error occurred.
*/
var IllegalStateException = function(message) {
/**
* Name of the error, contains the "IllegalStateException" String.
*
* @type String
*/
this.name = "IllegalStateException";
/**
* Human-readable description of the error.
*
* @type String
*/
this.message = message;
};
IllegalStateException.prototype = {
toString: function() {
return ["[",this.name,this.message,"]"].join("|");
}
};
return IllegalStateException;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Inheritance = /*@__PURE__*/(function() {
/**
* @method
*
* @param {Object} o
* @param {Function} tc
* @param {Object[]} params
*
* @private
*/
function doCall(o,tc,params) {
if (tc) {
if (params) {
return tc.apply(o,params);
} else {
return tc.apply(o);
}
}
}
/**
* @private
*/
function searchAlias(proto,extendedName) {
for (var i in proto) {
if (proto[extendedName] == proto[i] && extendedName != i) {
return i;
}
}
return null;
}
/**
* This module introduce a "classic" inheritance mechanism as well as an helper to
* copy methods from one class to another. See the Inheritance method documentation below for details.
* @exports Inheritance
*/
var Inheritance = {
/**
* This method extends a class with the methods of another class preserving super
* methods and super constructor. This method should be called on a class only
* after its prototype is already filled, otherwise
* super methods may not work as expected.
* The _super_, _callSuperMethod and _callSuperConstructor names are reserved: extending and
* extended classes' prototypes must not define properties with such names.
* Once extended it is possible to call the super constructor calling the _callSuperConstructor
* method and the super methods calling the _callSuperMethod method
*
Note that this function is the module itself (see the example)
*
* @throws {IllegalStateException} if checkAliases is true and an alias of the super class
* collides with a different method on the subclass.
*
* @param {Function} subClass the class that will extend the superClass
* @param {Function} superClass the class to be extended
* @param {boolean} [lightExtension] if true constructor and colliding methods of the
* super class are not ported on the subclass hence only non-colliding methods will be copied
* on the subclass (this kind of extension is also known as mixin)
* @param {boolean} [checkAliases] if true aliases of colliding methods will be searched on the
* super class prototype and, if found, the same alias will be created on the subclass. This is
* especially useful when extending a class that was minified using the Google Closure Compiler.
* Note however that collisions can still occur, between a property and a method and between methods
* when the subclass is minified too. The only way to prevent collisions is to minify super and sub
* classes together.
* @function Inheritance
* @static
*
* @example
* require(["Inheritance"],function(Inheritance) {
* function Class1() {
* }
*
* Class1.prototype = {
* method1: function(a) {
* return a+1;
* }
* };
*
* function Class2() {
* this._callSuperConstructor(Class2);
* }
*
* Class2.prototype = {
* method1: function(a,b) {
* return this._callSuperMethod(Class2,"method1",[a])+b;
* }
* };
*
* Inheritance(Class2,Class1);
*
* var class2Instance = new Class2();
* class2Instance.method1(1,2); //returns 4
*
* });
*/
Inheritance: function(subClass, superClass, lightExtension, checkAliases){
//iterate all of superClass's methods
for (var i in superClass.prototype) {
if (!subClass.prototype[i]) {
//copy non-colliding methods directly
subClass.prototype[i] = superClass.prototype[i];
} else if(checkAliases) {
//in case of collision search in the super prototype if the method has an alias
//and create the alias here too
var name = searchAlias(superClass.prototype,i);
if (name) {
//we want to copy superClass.method to superClass.prototype[i], but subClass.prototype[i] already exists
//name is an alias of i --> superClass.prototype[name] == superClass.prototype[i]
//if subClass has a name method thay is different not an alias of i (subClass.prototype[name] != subClass.prototype[i]) there is a collision problem
if (subClass.prototype[name] && subClass.prototype[name] !== subClass.prototype[i]) {
//unless the alias name was previously copied from superClass ( superClass.prototype[name] == superClass.prototype[name] )
if (superClass.prototype[name] !== superClass.prototype[name]) {
throw new IllegalStateException("Can't solve alias collision, try to minify the classes again (" + name + ", " + i + ")");
}
}
subClass.prototype[name] = subClass.prototype[i];
}
}
}
if (!lightExtension) {
//setup the extended class for super calls (square brakets used to support google closure)
subClass.prototype["_super_"] = superClass;
subClass.prototype["_callSuperConstructor"] = Inheritance._callSuperConstructor;
subClass.prototype["_callSuperMethod"] = Inheritance._callSuperMethod;
}
return subClass;
},
/**
* This method is attached to the prototype of each extended class as _callSuperMethod to make it possible to
* call super methods.
*
Note that it is not actually visible in this module.
*
* @param {Function} ownerClass the class that calls this method.
* @param {String} toCall the name of the super function to be called.
* @param {Object[]} [params] array of parameters to be used to call the super method.
* @static
*/
_callSuperMethod: function(ownerClass, toCall, params){
return doCall(this,ownerClass.prototype["_super_"].prototype[toCall],params);
},
/**
* This method is attached to the
* prototype of each extended class as _callSuperConstructor to make it possible
* to call the super constructor.
*
Note that it is not actually visible in this module.
*
* @param {Function} ownerClass the class that calls this method.
* @param {Object[]} [params] array of parameters to be used to call the super constructor.
* @static
*/
_callSuperConstructor: function(ownerClass, params){
doCall(this,ownerClass.prototype["_super_"], params);
}
};
//the way this is handled may look weird, well it is, I had to put
//things this way with the only purpose to let JSDoc document the module
//as I wanted to (and that didn't even turned out perfectly)
return Inheritance.Inheritance;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Matrix = /*@__PURE__*/(function() {
/**
* Creates a Matrix instance; if specified the matrix is initialized with the given object.
* @constructor
*
* @param {Object} inputMatrix the matrix to initialize this object with.
*
* @exports Matrix
* @class Very simple object-backed bi-dimensional Matrix implementation.
*/
var Matrix = function(inputMatrix) {
/**
* @private
*/
this.matrix = inputMatrix || {};
};
Matrix.prototype = {
/**
* Inserts an element in the matrix. If another element is already present in the
* specified position it is overwritten.
*
* @param insObject {Object} the element to be added.
* @param row {String|Number} the row in the matrix where the element is placed.
* @param column {String|Number} the column in the row where the element is placed.
*/
insert: function(insObject,row,column) {
if (!(row in this.matrix)) {
this.matrix[row] = {};
}
this.matrix[row][column] = insObject;
},
/**
* Gets the element at the specified position in the matrix. If the position is empty null is returned.
* @param row {String|Number} the row in the matrix where the element is located.
* @param column {String|Number} the column in the row where the element is located.
* @returns {Object} the element at the specified location or null.
*/
get: function(row,column) {
if (row in this.matrix && column in this.matrix[row]) {
return this.matrix[row][column];
}
return null;
},
/**
* Removes the element at the specified position in the matrix.
* @param row {String|Number} the row in the matrix where the element is located.
* @param column {String|Number} the column in the row where the element is located.
*/
del: function(row, column) {
if (!row in this.matrix) {
return;
}
if (column in this.matrix[row]) {
delete this.matrix[row][column];
}
for (var i in this.matrix[row]) {
//at least a cell in the row
return;
}
//row is empty, get rid of it
delete this.matrix[row];
},
/**
* Inserts a full row in the matrix. If another row is already present in the
* specified position it is overwritten.
*
* @param insRow {Object} the row to be added.
* @param row {String|Number} the row position.
*/
insertRow: function(insRow, row) {
this.matrix[row] = insRow;
},
/**
* @deprecated
*/
getRow: function(row) {
if (row in this.matrix) {
return this.matrix[row];
}
return null;
},
/**
* Removes the row at the specified position in the matrix.
* @param row {String|Number} the row position.
*/
delRow: function(row) {
if (row in this.matrix) {
delete this.matrix[row];
}
},
/**
* @deprecated
*/
getEntireMatrix: function() {
return this.matrix;
},
/**
* Verify if there are elements in the grid
* @returns true if the matrix is empty, false otherwise
*/
isEmpty: function() {
for (var row in this.matrix) {
return false;
}
return true;
},
/**
* Executes a given callback passing each element of the Matrix. The callback
* receives the element together with its coordinates.
* Callbacks are executed synchronously before the method returns: calling
* insert or delete methods during callback execution may result in
* a wrong iteration.
*
* @param {ForEachCallback} callback The callback to be called.
*/
forEachElement: function(callback) {
for (var row in this.matrix) {
this.forEachElementInRow(row,callback);
}
/*this.forEachRow(function(row) {
that.forEachElementInRow(row,callback);
});*/
},
/**
* Executes a given callback passing the key of each row containing at least one element.
* @param {RowsCallback} callback The callback to be called.
*/
forEachRow: function(callback) {
for (var row in this.matrix) {
callback(row);
}
},
/**
* Executes a given callback passing each element of the specified row. The callback
* receives the element together with its coordinates.
* Callbacks are executed synchronously before the method returns: calling
* insert or delete methods during callback execution may result in
* a wrong iteration.
*
* @param {ForEachCallback} callback The callback to be called.
*/
forEachElementInRow: function(row,callback) {
var rowElements = this.matrix[row];
for (var col in rowElements) {
callback(rowElements[col],row,col);
}
}
};
/**
* Callback for {@link Matrix#forEachElement} and {@link Matrix#forEachElementInRow}
* @callback ForEachCallback
* @param {Object} value the element.
* @param {String|Number} row the row where the element is located
* @return {String|Number} col the column in the row where the element is located
*/
/**
* Callback for {@link Matrix#forEachRow}
* @callback RowsCallback
* @param {String|Number} row a non-empty row in the Matrix.
*/
Matrix.prototype["insert"] = Matrix.prototype.insert;
Matrix.prototype["get"] = Matrix.prototype.get;
Matrix.prototype["del"] = Matrix.prototype.del;
Matrix.prototype["insertRow"] = Matrix.prototype.insertRow;
Matrix.prototype["getRow"] = Matrix.prototype.getRow;
Matrix.prototype["delRow"] = Matrix.prototype.delRow;
Matrix.prototype["getEntireMatrix"] = Matrix.prototype.getEntireMatrix;
Matrix.prototype["forEachElement"] = Matrix.prototype.forEachElement;
Matrix.prototype["forEachElementInRow"] = Matrix.prototype.forEachElementInRow;
Matrix.prototype["forEachRow"] = Matrix.prototype.forEachRow;
Matrix.prototype["isEmpty"] = Matrix.prototype.isEmpty;
return Matrix;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Environment = /*@__PURE__*/(function() {
var isBrowserDocumentVar = (typeof window !== "undefined" && typeof navigator !== "undefined" && typeof document !== "undefined");
var isWebWorkerVar = typeof importScripts !== "undefined"; //potentially WebWorkers may appear on node.js
var isNodeJSVar = typeof process == "object" && (/node(\.exe)?$/.test(process.execPath) || (process.node && process.v8) || (process.versions && process.versions.node && process.versions.v8 ));
if (isBrowserDocumentVar && !document.getElementById) {
throw new IllegalStateException("Not supported browser");
}
/**
* @exports Environment
*/
var Environment = {
/**
* Checks if the code is running inside an HTML document.
*
Note that browsers not supporting DOM Level 2 (i.e.: document.getElementById)
* are not recognized by this method
*
* @returns {Boolean} true if the code is running inside a Browser document, false otherwise.
*
* @static
*/
isBrowserDocument: function() {
return isBrowserDocumentVar;
},
/**
* Checks if the code is running inside a Browser. The code might be either running inside a
* HTML page or inside a WebWorker.
*
Note that browsers not supporting DOM Level 2 (i.e.: document.getElementById)
* are not recognized by this method
*
* @returns {Boolean} true if the code is running inside a Browser, false otherwise.
*
* @static
*/
isBrowser: function() {
return !isNodeJSVar && (isBrowserDocumentVar || isWebWorkerVar);
},
/**
* Checks if the code is running inside Node.js.
*
* @returns {Boolean} true if the code is running inside Node.js, false otherwise.
*
* @static
*/
isNodeJS: function() {
return !isBrowserDocumentVar && isNodeJSVar;
},
/**
* Checks if the code is running inside a WebWorker.
*
* @returns {Boolean} true if the code is running inside a WebWorker, false otherwise.
*
* @static
*/
isWebWorker: function() {
return !isBrowserDocumentVar && !isNodeJSVar && isWebWorkerVar;
},
/**
* Checks if the code is not running on a known environment
* @returns {boolean} true if the code not running on a known environment, false otherwise.
*/
isOther: function() {
return !isBrowserDocumentVar && !isNodeJSVar && !isWebWorkerVar;
},
/**
* Helper method that will throw an IllegalStateException if the return value of isBrowserDocument is false.
* This method is supposedly called as first thing in a module definition.
*
* @throws {IllegalStateException} if this function is not called inside a HTML page. The message of the error
* is the following: "Trying to load a browser-only module on non-browser environment".
*
* @static
*
* @example
* define(["Environment"],function(Environment) {
* Environment.browserDocumentOrDie();
*
* //module definition here
* });
*/
browserDocumentOrDie: function() {
if(!this.isBrowserDocument()) {
throw new IllegalStateException("Trying to load a browser-only module on non-browser environment");
}
}
};
Environment["isBrowserDocument"] = Environment.isBrowserDocument;
Environment["isBrowser"] = Environment.isBrowser;
Environment["isNodeJS"] = Environment.isNodeJS;
Environment["isWebWorker"] = Environment.isWebWorker;
Environment["browserDocumentOrDie"] = Environment.browserDocumentOrDie;
return Environment;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Helpers = /*@__PURE__*/(function() {
var TRIM_REGEXP = new RegExp("^\\s*([\\s\\S]*?)\\s*$");
var COMMA = new RegExp(",","g");
var DOT = new RegExp("\\.","g");
/**
* This module is a motley collection of simple "shortcut" methods
* @exports Helpers
*/
var Helpers = {
/**
* Shortcut for new Date().getTime();
*
* @returns the current timestamp
*/
getTimeStamp: function() {
return new Date().getTime();
},
/**
* Shortcut for Math.round( Math.random() * max );
* @param {Number} [max=1000] The max value to be returned
* @returns the current timestamp
*/
randomG: function(max) {
max = max || 1000;
return Math.round( Math.random() * max );
},
/**
* Trims a string
* @param {String} str the string to be trimmed
* @returns {String} the trimmed string
*/
trim: function(str) {
return str.replace(TRIM_REGEXP,"$1");
},
/**
* Gets a string and interpret it as a Number. The given string may contain dots or commas to separate decimals
* @param {String} val the string to be converted
* @param {Boolean} [commaAsDecimalSeparator=false] true to interpret the commas as decimal separators, false to interpret dots as decimal separators
* @returns {Number} the interpreted number
* @example
* Helpers.getNumber("3.432.771,201",true) == 3432771.201
*/
getNumber: function(val, commaAsDecimalSeparator) {
if (val) {
if (!val.replace) {
return val;
}
if (commaAsDecimalSeparator) {
val = val.replace(DOT, "");
val = val.replace(COMMA, ".");
} else {
val = val.replace(COMMA, "");
}
return new Number(val);
}
return 0;
},
/**
* Shortcut for val.join && typeof(val.join) == "function"
* @param {Object} val the object to be verified
* @returns {Boolean} true if val is an array, false otherwise
*/
isArray: function(val) {
return val && val.join && typeof(val.join) == "function";
},
/**
* Adds a handler for a browser event. The capture flag is set to false.
* @param {Object} obj the element to be listened to.
* @param {String} evnt the event to be listened to.
* @param {Function} handler the function to be called
* @returns {Boolean} true if the event was registered, false otherwise.
* @example
* Helpers.addEvent(window, "load", function(){});
*/
addEvent: function(obj, evnt, handler){
if (!Environment.isBrowserDocument()) {
return false;
}
if (typeof obj.addEventListener != "undefined") {
obj.addEventListener(evnt, handler, false);
} else if (typeof obj.attachEvent != "undefined") { //old IE
obj.attachEvent("on" + evnt, handler);
}
return true;
},
/**
* Removes a handler for a browser event.
* @param {Object} obj the element that is listened to.
* @param {String} evnt the event that is listened to.
* @param {Function} handler the function that is called
* @returns {Boolean} true if the event was removed, false otherwise.
*/
removeEvent: function(obj, evnt, handler){
if (!Environment.isBrowserDocument()) {
return false;
}
if (typeof obj.removeEventListener != "undefined") {
obj.removeEventListener(evnt, handler, false);
} else if (typeof obj.detachEvent != "undefined") { //old IE
obj.detachEvent("on" + evnt, handler);
}
return true;
}
};
Helpers["getTimeStamp"] = Helpers.getTimeStamp;
Helpers["randomG"] = Helpers.randomG;
Helpers["trim"] = Helpers.trim;
Helpers["getNumber"] = Helpers.getNumber;
Helpers["isArray"] = Helpers.isArray;
Helpers["addEvent"] = Helpers.addEvent;
Helpers["removeEvent"] = Helpers.removeEvent;
return Helpers;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var LoggerProxy = /*@__PURE__*/(function() {
//used as default is*Enabled spares arguments handling
var emptyFun = function() {
return false;
};
//implements Logger :)
var placeholder = {
"error":emptyFun,
"warn":emptyFun,
"info":emptyFun,
"debug":emptyFun,
"fatal":emptyFun,
"isDebugEnabled":emptyFun,
"isInfoEnabled":emptyFun,
"isWarnEnabled":emptyFun,
"isErrorEnabled":emptyFun,
"isFatalEnabled":emptyFun
};
/**
* This constructor is used internally by {@link module:LoggerManager} and should not
* be called manually. Use {@link module:LoggerManager.getLoggerProxy} to obtain a
* LoggerProxy instance.
* @constructor
*
* @exports LoggerProxy
*
* @class Offers a simple proxy to {@link Logger} instances. The proxied instance can be
* switched at any time with a different one.
*
Other than the proxied methods it offers some utility methods that will join
* together all of the specified parameters in a single string before passing it to
* the proxied instance.
*/
var LoggerProxy = function(toWrap) {//called simply Log on the original .NET implementation
this.setWrappedInstance(toWrap);
};
LoggerProxy.prototype = {
/**
* Called by LoggerManager to redirect the log to a different Logger
* @param {Logger} [toWrap]
*/
setWrappedInstance: function(toWrap) {
this.wrappedLogger = toWrap || placeholder;
},
//fatal
/**
* Joins all the specified parameters and calls {@link Logger#fatal} on
* the proxied instance.
* @param {...*} mex The string or object to be logged.
*/
logFatal: function(mex) {
if (!this.isFatalLogEnabled()) {
return;
}
mex += this.logArguments(arguments,1);
this.fatal(mex);
},
/**
* Proxies the call to the underling {@link Logger}
*
* @param {String} message The message to be logged.
* @param {Error} [exception] An Exception instance related to the current log message.
*/
fatal: function(mex,exc) {
this.wrappedLogger.fatal(mex,exc);
},
/**
* Proxies the call to the underling {@link Logger}
*/
isFatalLogEnabled: function() {
return !this.wrappedLogger.isFatalEnabled || this.wrappedLogger.isFatalEnabled();
},
//error
/**
* Joins all the specified parameters and calls {@link Logger#error} on
* the proxied instance.
* @param {...*} mex The string or object to be logged.
*/
logError: function(mex) {
if (!this.isErrorLogEnabled()) {
return;
}
mex += this.logArguments(arguments,1);
this.error(mex);
},
/**
* Joins all the specified parameters and calls {@link Logger#error} on
* the proxied instance.
* @param {...*} mex The string or object to be logged.
*/
logErrorExc: function(exc,mex) {
if (!this.isErrorLogEnabled()) {
return;
}
mex = this.logArguments([mex || "", exc],0);
this.error(mex,exc);
},
/**
* Proxies the call to the underling {@link Logger}
*
* @param {String} message The message to be logged.
* @param {Error} [exception] An Exception instance related to the current log message.
*/
error: function(mex,exc) {
this.wrappedLogger.error(mex,exc);
},
/**
* Proxies the call to the underling {@link Logger}
*/
isErrorLogEnabled: function() {
return !this.wrappedLogger.isErrorEnabled || this.wrappedLogger.isErrorEnabled();
},
//warn
/**
* Joins all the specified parameters and calls {@link Logger#warn} on
* the proxied instance.
* @param {...*} mex The string or object to be logged.
*/
logWarn: function(mex) {
if (!this.isWarnLogEnabled()) {
return;
}
mex += this.logArguments(arguments,1);
this.warn(mex);
},
/**
* Proxies the call to the underling {@link Logger}
*
* @param {String} message The message to be logged.
* @param {Error} [exception] An Exception instance related to the current log message.
*/
warn: function(mex,exc) {
this.wrappedLogger.warn(mex,exc);
},
/**
* Proxies the call to the underling {@link Logger}
*/
isWarnLogEnabled: function() {
return !this.wrappedLogger.isWarnEnabled || this.wrappedLogger.isWarnEnabled();
},
//info
/**
* Joins all the specified parameters and calls {@link Logger#info} on
* the proxied instance.
* @param {...*} mex The string or object to be logged.
*/
logInfo: function(mex) {
if (!this.isInfoLogEnabled()) {
return;
}
mex += this.logArguments(arguments,1);
this.info(mex);
},
/**
* Proxies the call to the underling {@link Logger}
*
* @param {String} message The message to be logged.
* @param {Error} [exception] An Exception instance related to the current log message.
*/
info: function(mex,exc) {
this.wrappedLogger.info(mex,exc);
},
/**
* Proxies the call to the underling {@link Logger}
*/
isInfoLogEnabled: function() {
return !this.wrappedLogger.isInfoEnabled || this.wrappedLogger.isInfoEnabled();
},
//debug
/**
* Joins all the specified parameters and calls {@link Logger#debug} on
* the proxied instance.
* @param {...*} mex The string or object to be logged.
*/
logDebug: function(mex) {
if (!this.isDebugLogEnabled()) {
return;
}
mex += this.logArguments(arguments,1);
this.debug(mex);
},
/**
* Proxies the call to the underling {@link Logger}
*
* @param {String} message The message to be logged.
* @param {Error} [exception] An Exception instance related to the current log message.
*/
debug: function(mex,exc) {
this.wrappedLogger.debug(mex,exc);
},
/**
* Proxies the call to the underling {@link Logger}
*/
isDebugLogEnabled: function() {
return !this.wrappedLogger.isDebugEnabled || this.wrappedLogger.isDebugEnabled();
},
/**
* @private
*/
logArguments: function(args, _start){ //Joins together all of the elements of an arguments array in a custom string
_start = _start ? _start : 0;
var _line = " ";
for (var i = _start; i < args.length; i++) {
try {
var element = args[i];
if (element === null) {
_line += "NULL";
} else if (element.length < 0) {
_line += "*";
} else if (element.charAt != null) {
_line += element;
} else if (element.message !== undefined) {
_line += element.message;
if (element.stack) {
_line += "\n"+element.stack+"\n";
}
} else if (element[0] == element) {
// seen on Firefox?
_line += element;
} else if (Helpers.isArray(element)) {
_line += "(";
_line += this.logArguments(element);
_line += ")";
} else {
_line += element;
}
_line += " ";
} catch (_e) {
_line += "??? ";
}
}
return _line;
}
};
//closure exports
LoggerProxy.prototype["debug"] = LoggerProxy.prototype.debug;
LoggerProxy.prototype["isDebugLogEnabled"] = LoggerProxy.prototype.isDebugLogEnabled;
LoggerProxy.prototype["logDebug"] = LoggerProxy.prototype.logDebug;
LoggerProxy.prototype["info"] = LoggerProxy.prototype.info;
LoggerProxy.prototype["isInfoLogEnabled"] = LoggerProxy.prototype.isInfoLogEnabled;
LoggerProxy.prototype["logInfo"] = LoggerProxy.prototype.logInfo;
LoggerProxy.prototype["warn"] = LoggerProxy.prototype.warn;
LoggerProxy.prototype["isWarnEnabled"] = LoggerProxy.prototype.isWarnEnabled;
LoggerProxy.prototype["logWarn"] = LoggerProxy.prototype.logWarn;
LoggerProxy.prototype["error"] = LoggerProxy.prototype.error;
LoggerProxy.prototype["isErrorEnabled"] = LoggerProxy.prototype.isErrorEnabled;
LoggerProxy.prototype["logError"] = LoggerProxy.prototype.logError;
LoggerProxy.prototype["logErrorExc"] = LoggerProxy.prototype.logErrorExc;
LoggerProxy.prototype["fatal"] = LoggerProxy.prototype.fatal;
LoggerProxy.prototype["isFatalEnabled"] = LoggerProxy.prototype.isFatalEnabled;
LoggerProxy.prototype["logFatal"] = LoggerProxy.prototype.logFatal;
return LoggerProxy;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var IllegalArgumentException = /*@__PURE__*/(function() {
/**
* Constructs an IllegalArgumentException with the specified detail message.
* @constructor
*
* @param {String} message short description of the error.
*
* @exports IllegalArgumentException
* @class Thrown to indicate that a method has been passed an illegal
* or inappropriate argument.
*
Use toString to extract details on the error occurred.
*/
var IllegalArgumentException = function(message) {
/**
* Name of the error, contains the "IllegalArgumentException" String.
*
* @type String
*/
this.name = "IllegalArgumentException";
/**
* Human-readable description of the error.
*
* @type String
*/
this.message = message;
};
IllegalArgumentException.prototype = {
toString: function() {
return ["[",this.name,this.message,"]"].join("|");
}
};
return IllegalArgumentException;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var LoggerManager = /*@__PURE__*/(function() {
var logInstances = {};
var currentLoggerProvider = null;
var NOT_LOGGER_PROVIDER = "The given object is not a LoggerProvider";
/**
* This singleton should be used to obtain {@link Logger} instances to
* be used to produce the log.
* @exports LoggerManager
*
* @example
* define(["LoggerManager"],function(LoggerManager) {
*
* //stuff
* var logger = LoggerManager.getLoggerProxy("myCategory");
*
* var MyClass = function() {
* //more stuff
* logger.info("my info log");
* //stuff
* logger.error("my error log");
* };
*
* return MyClass;
*
* });
*
* //elsewhere
* require(["MyClass","LoggerManager","MyLoggerProvider"],
* function(MyClass,LoggerManager,MyLoggerProvider) {
*
* LoggerManager.setLoggerProvider(new MyLoggerProvider());
* var m = new MyClass(); //m will send log to the MyLoggerProvider instance
*
* });
*
*
*/
var LoggerManager = {
/**
* Sets the provider that will be used to get the Logger instances; it can
* be set and changed at any time. All the new log will be sent to the
* {@link Logger} instances obtained from the latest LoggerProvider set.
* Log produced before any provider is set is discarded.
*
* @param {LoggerProvider} [newLoggerProvider] the provider to be used; if
* missing or null all the log will be discarded until a new provider is provided.
*/
setLoggerProvider: function(newLoggerProvider) {
if (newLoggerProvider && !newLoggerProvider.getLogger) {
//null is a valid value
throw new IllegalArgumentException(NOT_LOGGER_PROVIDER);
}
currentLoggerProvider = newLoggerProvider;
//updates the alive proxies
for (var cat in logInstances) {
if (!currentLoggerProvider) {
logInstances[cat].setWrappedInstance(null);
} else {
logInstances[cat].setWrappedInstance(currentLoggerProvider.getLogger(cat));
}
}
},
/**
* Gets a LoggerProxy to be used to produce the log bound to a defined category.
* One single LoggerProxy instance will be created per each category: if the method
* is called twice for the same category then the same instance will be returned.
* On the other hand this method can potentially cause a memory leak as once a Logger
* is created it will never be dismissed. It is expected that the number of
* categories within a single application is somewhat limited and in any case
* not growing with time.
*
* @param {String} cat The category the Logger will be bound to.
* @returns {LoggerProxy} the instance to be used to produce the log
* for a certain category.
*/
getLoggerProxy: function(cat) {
if (!logInstances[cat]) {
if (!currentLoggerProvider) {
logInstances[cat] = new LoggerProxy();
} else {
logInstances[cat] = new LoggerProxy(currentLoggerProvider.getLogger(cat));
}
}
return logInstances[cat];
},
/**
* @private
*/
resolve: function(id){ //if LogMessages is included this method will be replaced
return id;
}
};
LoggerManager["setLoggerProvider"] = LoggerManager.setLoggerProvider;
LoggerManager["getLoggerProxy"] = LoggerManager.getLoggerProxy;
LoggerManager["resolve"] = LoggerManager.resolve;
return LoggerManager;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Setter = /*@__PURE__*/(function() {
//This is an abstract class; it could have been a module with a couple of static methods but I find it handier this way.
/**
* @private
* @constant
* @static
*/
var VALUE_NOT_VALID = "The given value is not valid. ";
/**
* @private
* @constant
* @static
*/
var USE_NUMBER = "Use a number";
/**
* @private
* @constant
* @static
*/
var USE_INTEGER = "Use an integer";
/**
* @private
* @constant
* @static
*/
var USE_POSITIVE = "Use a positive number";
/**
* @private
* @constant
* @static
*/
var OR_ZERO = " or 0";
/**
* @private
* @constant
* @static
*/
var USE_TRUE_OR_FALSE = "Use true or false";
/**
* Fake constructor. This abstract class is supposed to be extended using {@link module:Inheritance}
* light extension.
* @constructor
*
* @exports Setter
* @abstract
* @class Abstract class to be extended to gain input validation for a class' setter methods.
*
* @example
* define(["Inheritance","Setter"],function(Inheritance,Setter) {
*
* var MyClass = function() {
* this.exampleProperty = 1;
* //do stuff
* }
*
* MyClass.prototype = {
* setExampleProperty: function(newVal) {
* this.exampleProperty = this.checkPositiveNumber(newVal,false,false);
* },
*
* //declare more stuff
* };
*
* Inheritance(MyClass,Setter,true);
* return MyClass;
* });
*
*/
var Setter = function() {
};
/**
* Checks the given value to be a valid number. The input value is first explicitly converted into
* a Number (e.g. null would become 0) then is checked to be a valid number and finally it is verified
* against the input configurations.
*
* @throws {IllegalArgumentException} if the input value is not a number or does not respect one of the given constraint.
*
* @param newVal {Number|String|Object} the value to be verified.
* @param canBeZero {Boolean} specifies if the given value can be 0 or not.
* @param canBeDecimal {Boolean} specifies if the given value can be a decimal number or not.
* @returns {Number} The explicitly converted Number is returned.
*
* @protected
* @memberof Setter.prototype
*/
function checkPositiveNumber(newVal,canBeZero,canBeDecimal) {
var tmp = new Number(newVal);
if (isNaN(tmp)) {
throw new IllegalArgumentException(VALUE_NOT_VALID+USE_NUMBER);
}
if(!canBeDecimal && tmp != Math.round(tmp)) {
throw new IllegalArgumentException(VALUE_NOT_VALID+USE_INTEGER);
}
if (canBeZero) {
if (newVal < 0) {
throw new IllegalArgumentException(VALUE_NOT_VALID+USE_POSITIVE+OR_ZERO);
}
} else if(newVal <= 0) {
throw new IllegalArgumentException(VALUE_NOT_VALID+USE_POSITIVE);
}
return tmp;
}
/**
* Checks the given value to be a boolean. The check is performed with the strict equal operactor (i.e.: ===)
* If specified, empty strings, nulls, NaN, 0s, negative numbers
* and undefineds are also admitted values and are interpreted as false.
*
* @param newVal the value to be verified.
* @param notStrictFalse if true anything that would not-strictly equal (==) false is considered as
* a valid false.
* @returns {Boolean}
*
* @throws {IllegalArgumentException} if the input value is not a valid boolean.
*
* @protected
* @memberof Setter.prototype
*/
function checkBool(newVal,notStrictFalse) {
if (newVal === true || newVal === false || (notStrictFalse && !newVal)) {
return newVal === true;
}
throw new IllegalArgumentException(VALUE_NOT_VALID+USE_TRUE_OR_FALSE);
}
Setter.prototype.checkPositiveNumber = checkPositiveNumber;
Setter.prototype.checkBool = checkBool;
Setter.prototype["checkPositiveNumber"] = checkPositiveNumber;
Setter.prototype["checkBool"] = checkBool;
return Setter;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var BrowserDetection = /*@__PURE__*/(function() {
//You'll find strange comments near the methods declarations; We use such comments to keep track of why are we using such browser sniffing
/*
stuff we never used or used in the past
var is_icab = (window.ScriptEngine && (ScriptEngine().indexOf("InScript") > -1));
var is_icab2down = (is_icab && !document.createElement);
var is_icab3up = (is_icab && document.createElement);
var is_konqueror = (navigator.vendor == "KDE")||(document.childNodes)&&(!document.all)&&(!navigator.taintEnabled);
var is_safari = (document.childNodes)&&(!document.all)&&(!navigator.taintEnabled)&&(!navigator.accentColorName);
var is_omniweb45plus = (document.childNodes)&&(!document.all)&&(!navigator.taintEnabled)&&(navigator.accentColorName);
var is_nn6up = (navigator.product == "Gecko");
var is_nn6 = (navigator.product == "Gecko" && !window.find);
var is_nn7 = (navigator.product == "Gecko" && window.find);
*/
//20110909 removed isOldKHTML and isIE5 detections.
//20130813 removed isProbablyWinPhone7 detection
//null means "not yet checked", while false means that we are not on such browser; if we're not on a browser at all
//we can directly set everything to false
var INIT_VALUE = Environment.isBrowser() ? null : false;
var LOW_UA = Environment.isBrowser() ? navigator.userAgent.toLowerCase() : null;
function getSolution(myVer,reqVer,less) {
if(!reqVer || !myVer) {
return true;
} else if (less === true) {
// the given version or less
return myVer <= reqVer;
} else if (less === false) {
//the given version or more
return myVer >= reqVer;
} else {
//exactly the given version
return myVer == reqVer;
}
}
function doUACheck(checkString) {
return LOW_UA.indexOf(checkString) > -1;
}
function getSimpleUACheckFunction(checkString) {
var resultVar = INIT_VALUE;
return function() {
if (resultVar === null) {
resultVar = doUACheck(checkString);
}
return resultVar;
};
}
function getChainedANDFunction(funs) {
var resultVar = INIT_VALUE;
return function() {
if (resultVar === null) {
resultVar = true;
for (var i=0; i= 2) {
return res[1];
}
}
return null;
};
}
function getOperaVersion() {
if (!opera.version) {
//pre 7.6
//we do not need to detect Opera 6 so we're cool like this
return 7;
} else {
//> 7.6
var verStr = opera.version();
verStr = verStr.replace(new RegExp("[^0-9.]+","g"), "");
return parseInt(verStr);
}
//side NOTE: opera 7 mobile does not have opera.postError
}
function hasOperaGlobal() {
return typeof opera != "undefined";
}
function getNotFunction(f) {
return function() {
return !f();
};
}
var khtmlVar = INIT_VALUE;
/**
* Simple module that can be used to try to detect the browser in use.If possible the use of this module should be avoided:
* it should only be used if the behavior can't be guessed using feature detection. The module does not contain an extensive list
* of browsers, new method were added only when needed in the Lightstreamer JavaScript Client library.
*
There are two kinds of methods, one does simply recognize the browsers, the other can also discern the browser version.
* As most of the methods are based on User Agent inspections all the method names contain the "probably" word to recall their
* intrinsic weakness.
* @exports BrowserDetection
*/
var BrowserDetection = {
/**
* Check if the browser in use is probably a rekonq or not
* @method
* @return {Boolean} true if probably a rekonq, false if probably not.
*/
isProbablyRekonq: getSimpleUACheckFunction("rekonq"), //used by isProbablyApple thus "spin fix"
/**
* Check if the browser in use is probably a WebKit based browser or not
* @method
* @return {Boolean} true if probably a WebKit based browser, false if probably not.
*/
isProbablyAWebkit: getSimpleUACheckFunction("webkit"),//iframe generation
/**
* Check if the browser in use is probably a Playstation 3 browser or not
* @method
* @return {Boolean} true if probably a Playstation 3 browser, false if probably not.
*/
isProbablyPlaystation: getSimpleUACheckFunction("playstation 3"), //expected streaming behavior
/**
* Check if the browser in use is probably a Chrome (or Chrome tab) or not. A specific version or version range can be requested.
* @method
* @param {Number=} requestedVersion The version to be checked. If not specified any version will do.
* @param {Boolean=} orLowerFlag true to check versions up to the specified one, false to check for greater versions; the specified version
* is always included. If missing only the specified version is considered.
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
isProbablyChrome: getVersionedFunction(
getSimpleUACheckFunction("chrome/"),
getExtractVersioByRegexpFunction(new RegExp("chrome/([0-9]+)","g"))
), // iframe content generation / used by isProbablyApple / used by isProbablyAndroid / windows communication
/**
* Check if the browser in use is probably a KHTML browser or not
* @method
* @return {Boolean} true if probably a KHTML browser, false if probably not.
*/
isProbablyAKhtml: function() {
if (khtmlVar === null) {
khtmlVar = (document.childNodes) && (!document.all) && (!navigator.taintEnabled) && (!navigator.accentColorName);
}
return khtmlVar;
}, //hourglass trick
/**
* Check if the browser in use is probably a Konqueror or not. A specific version or version range can be requested.
* @method
* @param {Number=} requestedVersion The version to be checked. If not specified any version will do.
* @param {Boolean=} orLowerFlag true to check versions up to the specified one, false to check for greater versions; the specified version
* is always included. If missing only the specified version is considered.
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
isProbablyKonqueror: getVersionedFunction(
getSimpleUACheckFunction("konqueror"),
getExtractVersioByRegexpFunction(new RegExp("konqueror/([0-9.]+)","g"))
), //iframe communications / iframe content generation
/**
* Check if the browser in use is probably an Internet Explorer or not. A specific version or version range can be requested.
* @method
* @param {Number=} requestedVersion The version to be checked. If not specified any version will do.
* @param {Boolean=} orLowerFlag true to check versions up to the specified one, false to check for greater versions; the specified version
* is always included. If missing only the specified version is considered.
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
isProbablyIE: function(requestedVersion,orLowerFlag){
if (
getVersionedFunction(
getSimpleUACheckFunction("msie"),
getExtractVersioByRegexpFunction(new RegExp("msie\\s"+"("+"[0-9]+"+")"+"[.;]","g"))
)(requestedVersion,orLowerFlag)
||
getVersionedFunction(
getSimpleUACheckFunction("rv:11.0"),
function() { return "11"; }
)(requestedVersion,orLowerFlag)
) {
return true;
} else {
return false;
}
}, //color name resolution / eval isolation / hourglass trick / expected streaming behavior / iframe communication / iframe domain handling / iframe creation
/**
* Check if the browser in use is probably an Internet Explorer 11 or not.
* @method
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
isProbablyEdge: getSimpleUACheckFunction("edge"), // expected streaming behavior
/**
* Check if the browser in use is probably a Firefox or not. A specific version or version range can be requested.
* @method
* @param {Number=} requestedVersion The version to be checked. If not specified any version will do.
* @param {Boolean=} orLowerFlag true to check versions up to the specified one, false to check for greater versions; the specified version
* is always included. If missing only the specified version is considered.
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
isProbablyFX: getVersionedFunction(
getSimpleUACheckFunction("firefox"),
getExtractVersioByRegexpFunction(new RegExp("firefox\\/(\\d+\\.?\\d*)"))
), //mad check
/**
* Check if the browser in use is probably an old Opera (i.e.: up to the WebKit switch) or not. A specific version or version range can be requested.
* @method
* @param {Number=} requestedVersion The version to be checked. If not specified any version will do.
* @param {Boolean=} orLowerFlag true to check versions up to the specified one, false to check for greater versions; the specified version
* is always included. If missing only the specified version is considered.
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
isProbablyOldOpera: getVersionedFunction(hasOperaGlobal,getOperaVersion) //autoscroll / expected streaming behavior / windows communication / onload expectations / iframe communications / iframe content generation / iframe generation
};
/**
* Check if the browser in use is probably an Android stock browser or not
* @method
* @return {Boolean} true if probably an Android stock browser, false if probably not.
*/
BrowserDetection.isProbablyAndroidBrowser = getChainedANDFunction([
getSimpleUACheckFunction("android"),
BrowserDetection.isProbablyAWebkit,
getNotFunction(BrowserDetection.isProbablyChrome)
]);//spin fix / connection behavior handling
/**
* Check if the browser in use is probably an Opera Mobile or not
* @method
* @return {Boolean} true if probably a an Opera Mobile, false if probably not.
*/
BrowserDetection.isProbablyOperaMobile = getChainedANDFunction([
BrowserDetection.isProbablyOldOpera,
getSimpleUACheckFunction("opera mobi")
]); //expected test results
/**
* Check if the browser in use is probably an Apple Browser (i.e. Safari or Safari Mobile) or not. A specific version or version range can be requested.
* @method
* @param {Number=} requestedVersion The version to be checked. If not specified any version will do.
* @param {Boolean=} orLowerFlag true to check versions up to the specified one, false to check for greater versions; the specified version
* is always included. If missing only the specified version is considered.
* @return {Boolean} true if the browser is probably the correct one, false if probably not.
*/
BrowserDetection.isProbablyApple = getVersionedFunction(
getChainedANDFunction([ // safari + (ipad || iphone || ipod || (!android+!chrome+!rekonq))
getSimpleUACheckFunction("safari"),
getChainedORFunction([
getSimpleUACheckFunction("ipad"),
getSimpleUACheckFunction("iphone"),
getSimpleUACheckFunction("ipod"),
getChainedANDFunction([
getNotFunction(BrowserDetection.isProbablyAndroidBrowser),
getNotFunction(BrowserDetection.isProbablyChrome),
getNotFunction(BrowserDetection.isProbablyRekonq)])
])
]),
getExtractVersioByRegexpFunction(new RegExp("version\\/(\\d+\\.?\\d*)"))
); //spin fix / windows communication
BrowserDetection["isProbablyRekonq"] = BrowserDetection.isProbablyRekonq;
BrowserDetection["isProbablyChrome"] = BrowserDetection.isProbablyChrome;
BrowserDetection["isProbablyAWebkit"] = BrowserDetection.isProbablyAWebkit;
BrowserDetection["isProbablyPlaystation"] = BrowserDetection.isProbablyPlaystation;
BrowserDetection["isProbablyAndroidBrowser"] = BrowserDetection.isProbablyAndroidBrowser;
BrowserDetection["isProbablyOperaMobile"] = BrowserDetection.isProbablyOperaMobile;
BrowserDetection["isProbablyApple"] = BrowserDetection.isProbablyApple;
BrowserDetection["isProbablyAKhtml"] = BrowserDetection.isProbablyAKhtml;
BrowserDetection["isProbablyKonqueror"] = BrowserDetection.isProbablyKonqueror;
BrowserDetection["isProbablyIE"] = BrowserDetection.isProbablyIE;
BrowserDetection["isProbablyEdge"] = BrowserDetection.isProbablyEdge;
BrowserDetection["isProbablyFX"] = BrowserDetection.isProbablyFX;
BrowserDetection["isProbablyOldOpera"] = BrowserDetection.isProbablyOldOpera;
return BrowserDetection;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var List = /*@__PURE__*/(function() {
/**
* Creates an empty List instance
* @constructor
*
* @exports List
* @class Very simple Array-backed List implementation.
* It is discouraged the use of this class to handle big lists.
*/
var List = function() {
this.data = [];
};
List.prototype = {
/**
* Adds the element to the end of the list (using Array.push).
* Each element can be added multiple times; in such case it will be added to the list multiple times
*
* @param {Object} newEl The element to be added
*/
add: function(newEl) {
this.data.push(newEl);
},
/**
* Removes the first occurrence of the specified object in the List.
* A linear search is performed to find the element; a non-strict comparison ( == )
* is performed to identify the element.
*
* @param {Object} newEl The element to be removed
*
* @returns {Boolean} true if element was found and deleted, false otherwise
*/
remove: function(remEl) {
var i = this.find(remEl);
if (i < 0) {
return false;
}
this.data.splice(i,1);
return true;
},
/**
* Checks if an element is in the list
*
* @param {Object} el the element to be searched
*
* @returns {Boolean} true if element was found, false otherwise
*/
contains: function(el) {
return this.find(el) >= 0;
},
/**
* @private
*/
find: function(el) {
for (var i=0; i
* Callbacks are executed synchronously before the method returns: calling
* {@link List#add} or {@link List#remove} during callback execution may result in
* a wrong iteration.
*
* @param {Function} cb The callback to be called.
*/
forEach: function(cb) {
for (var i=0; i
var names = {
onloadDone : "onloadDone",
onloadInprogress : "onloadInprogress",
unloaded : "unloaded",
unloading : "unloading",
preunloading : "preunloading"
};
var reverse = {};
for (var i in names) {
reverse[names[i]] = i;
}
//<-- closure compiler trick
function getOnloadClosure(that) {
return getEventClosure(that,reverse['onloadDone'],reverse['onloadInprogress'],onloadFunctions,'onloadEvent');
}
function getUnloadClosure(that) {
return getEventClosure(that,reverse['unloaded'],reverse['unloading'],onunloadFunctions,'unloadEvent');
}
function getBeforeUnloadClosure(that) {
return getEventClosure(that,reverse['preunloading'],reverse['preunloading'],onbeforeunloadFunctions,'preUnloadEvent');
}
function getEventClosure(that,toCheck,toSet,toExe,methodName) {
return function() {
if (that[toCheck]) {
return;
}
that[toSet] = true;
toExe.forEach(function(elToExe) {
try {
singleEventExecution(elToExe,methodName);
} catch (_e) {
}
});
if (toCheck != 'preunloading') {
toExe.clean();
}
that[toCheck] = true;
that[toSet] = false;
};
}
function singleEventExecution(elToExe,methodName) {
if (elToExe[methodName]) {
elToExe[methodName]();
} else {
elToExe();
}
}
function asynchSingleEventExecution(elToExe,methodName) {
setTimeout(function() {
singleEventExecution(elToExe,methodName);
},0);
}
function executeLater(what,when,who,how) {
setTimeout(function() {
if (who) {
if (how) {
what.apply(who,how);
} else {
what.apply(who);
}
} else if (how) {
what.apply(null, how);
} else {
what();
}
},when);
}
function DOMloaded() {
isDOMLoaded = true; //no need to initialize it anywhere
}
/**
* Tries to track the loading status of the page. It may fallback to using timeouts or DOMContentLoaded events to address browser compatibilities: in such
* cases there is a chance that the registered onload handlers are fired before the actual onload is. Also unload and beforeunload may not fire at all.
* @exports EnvironmentStatus
*/
var EnvironmentStatus = {
/**
* @private
*/
onloadDone: false,
/**
* @private
*/
onloadInprogress: false,
/**
* @private
*/
unloaded: false,
/**
* @private
*/
unloading: false,
/**
* @private
*/
preunloading: false,
/**
* Checks if the load event has been fired.
* @returns {Boolean} true if the load event has already been fired, false otherwise.
*/
isLoaded: function() {
return this.onloadDone;
},
/**
* Checks if the unload event has been fired.
* @returns {Boolean} true if the unload event has already been fired, false otherwise.
*/
isUnloaded : function() {
return this.unloaded;
},
/**
* Checks if the unload event is currently being handled.
* @returns {Boolean} true if the unload event is currently being handled, false otherwise.
*/
isUnloading: function() {
return this.unloading;
},
/**
* Adds a handler for the load event. If the event was already fired the handler is sent in a setTimeout (with a 0 timeout).
* @param {Function|EnvironmentStatusListener} the function to be executed or an object containing the onloadEvent function to be executed.
*/
addOnloadHandler: function(f) {
if (this.isPreOnload()) {
onloadFunctions.add(f);
} else {
asynchSingleEventExecution(f,'onloadEvent');
}
},
/**
* Adds a handler for the unload event. If the event was already fired the handler is sent in a setTimeout (with a 0 timeout).
* @param {Function|EnvironmentStatusListener} the function to be executed or an object containing the unloadEvent function to be executed.
*/
addUnloadHandler: function(f) {
if (this.isPreUnload()) {
onunloadFunctions.add(f);
} else {
asynchSingleEventExecution(f,'unloadEvent');
}
},
/**
* Adds a handler for the onbeforeunload event.
* @param {Function|EnvironmentStatusListener} the function to be executed or an object containing the preUnloadEvent function to be executed.
*/
addBeforeUnloadHandler: function(f) {
onbeforeunloadFunctions.add(f);
if (this.preunloading) {
asynchSingleEventExecution(f,'preUnloadEvent');
}
},
/**
* Removes the specified load handler if present, otherwise it does nothing.
* @param {Function|EnvironmentStatusListener} the function or object to be removed
*/
removeOnloadHandler: function(f) {
onloadFunctions.remove(f);
},
/**
* Removes the specified unload handler if present, otherwise it does nothing.
* @param {Function|EnvironmentStatusListener} the function or object to be removed
*/
removeUnloadHandler: function(f) {
onunloadFunctions.remove(f);
},
/**
* Removes the specified onbeforeunload handler if present, otherwise it does nothing.
* @param {Function|EnvironmentStatusListener} the function or object to be removed.
*/
removeBeforeUnloadHandler: function(f) {
onbeforeunloadFunctions.remove(f);
},
/**
* @private
*/
isPreOnload: function() {
return !(this.onloadDone || this.onloadInprogress);
},
/**
* @private
*/
isPreUnload: function() {
return !(this.unloaded || this.unloading);
},
/**
* @private
*/
attachToWindow: function() {
Helpers.addEvent(window,"unload",this.closeFun);
Helpers.addEvent(window,"beforeunload",this.beforeCloseFun);
//EXPERIMENTAL
if (document && typeof document.readyState != "undefined") {
var strState = document.readyState;
if (strState.toUpperCase() == "COMPLETE") {
//already loaded
this.asynchExecution();
return;
} else {
//It may happen that readyState is not "completed" but the onload
//was already fired. We fire a timeout to check the case
executeLater(this.controlReadyStateLoad,controlLoadTimeout,this);
}
} else if(this.isInBody()) {
//already loaded
this.asynchExecution();
return;
}
//EXPERIMENTAL end
var done = Helpers.addEvent(window,"load",this.readyFun);
if (!done) {
//Can't append an event listener to the onload event (webworker / nodejs)
//Let's launch a timeout
this.asynchExecution(); //should not happen since we did the check on the module setup, why did we keep it?
} else if (BrowserDetection.isProbablyOldOpera()) {
//Old Opera did not fire the onload event on a page wrapped
//in an iFrame if a brother iFrame is still loading (in the
//worst case the second iFrame is a forever-frame)
//To prevent the case we will fire a fake onload
var checkDOM = false;
//on Opera < 9 DOMContentLoaded does not exist, so we can't wait for it
if (BrowserDetection.isProbablyOldOpera(9, false)) {
checkDOM = true;
//DOMContentLoaded did not fire yet or we should have not reach this point
//as per the readyState/isInBody checks
Helpers.addEvent(document,"DOMContentLoaded",DOMloaded);
}
executeLater(this.controlOperaLoad,controlLoadTimeout,this,[checkDOM]);
}
},
/**
* @private
*/
asynchExecution: function() {
executeLater(this.readyFun,0);
},
/**
* @private
*/
controlReadyStateLoad: function() {
if (!this.onloadDone) {
//onload not yet fired
var strState = document.readyState;
if (strState.toUpperCase() == "COMPLETE") {
this.readyFun();
} else {
executeLater(this.controlReadyStateLoad,controlLoadTimeout,this);
}
}
},
/**
* @private
*/
controlOperaLoad: function(checkDOM) {
if (!this.onloadDone) {
//onload not yet fired
if (isDOMLoaded || !checkDOM && this.isInBody()) {
//DOM is there
//let's fake the onload event
this.readyFun();
} else {
//body is still missing
executeLater(this.controlOperaLoad,controlLoadTimeout,this,[checkDOM]);
}
}
},
/**
* @private
*/
isInBody: function() {
return (typeof document.getElementsByTagName != "undefined" && typeof document.getElementById != "undefined" && ( document.getElementsByTagName("body")[0] != null || document.body != null ) );
}
};
EnvironmentStatus.readyFun = getOnloadClosure(EnvironmentStatus);
EnvironmentStatus.closeFun = getUnloadClosure(EnvironmentStatus);
EnvironmentStatus.beforeCloseFun = getBeforeUnloadClosure(EnvironmentStatus);
if (Environment.isBrowserDocument()) {
EnvironmentStatus.attachToWindow();
} else {
EnvironmentStatus.asynchExecution();
}
EnvironmentStatus["addOnloadHandler"] = EnvironmentStatus.addOnloadHandler;
EnvironmentStatus["addUnloadHandler"] = EnvironmentStatus.addUnloadHandler;
EnvironmentStatus["addBeforeUnloadHandler"] = EnvironmentStatus.addBeforeUnloadHandler;
EnvironmentStatus["removeOnloadHandler"] = EnvironmentStatus.removeOnloadHandler;
EnvironmentStatus["removeUnloadHandler"] = EnvironmentStatus.removeUnloadHandler;
EnvironmentStatus["removeBeforeUnloadHandler"] = EnvironmentStatus.removeBeforeUnloadHandler;
EnvironmentStatus["isLoaded"] = EnvironmentStatus.isLoaded;
EnvironmentStatus["isUnloaded"] = EnvironmentStatus.isUnloaded;
EnvironmentStatus["isUnloading"] = EnvironmentStatus.isUnloading;
return EnvironmentStatus;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Executor = /*@__PURE__*/(function() {
var step = 50;
var newStuffFlag = false;
var toBeExecuted = [];
var now = Helpers.getTimeStamp();
var RESET_TIME = 3*60*60*1000; //3 hours
var resetAt = now+RESET_TIME; //workaround for Safari 5 windows: after several hours the toBeExecuted array becomes unusable (OOM)
var toBeRepeated = [];
var timer = null;
var nextId = 0;
//var goodWakeups = 0;
function sortFun(a,b) {
if (a.time === b.time) {
return a.orderId - b.orderId;
}
return a.time-b.time;
}
//TICK handling stuff
var origin = Environment.isBrowserDocument() && (document.location.protocol == "http:" || document.location.protocol == "https:") ? (document.location.protocol+"//"+document.location.hostname+(document.location.port?":"+document.location.port:"")) : "*";
var DEFAULT_GENERATE_TICK = function() { /*setTimeout(doTick,0); */ };
var generateTickExecution = DEFAULT_GENERATE_TICK;
var pendingGeneratedTick = false;
function doTick() {
pendingGeneratedTick = false;
execute();
}
//we need to call this for urgent task as on FX5 and CH11 setInterval/setTimeout calls
//are made 1 per second on background pages (and our 50ms tick is based on a setInterval)
function generateTick() {
if (!pendingGeneratedTick) {
pendingGeneratedTick = true;
generateTickExecution();
}
}
function doInit() {
if (!timer) {
//set up the method to generate the tick
// on recent browsers we send a post message and trigger the doTick when we receive such message
if (Environment.isBrowserDocument() && typeof postMessage != "undefined") {
generateTickExecution = function() {
try {
window.postMessage("Lightstreamer.run",origin);
} catch (e) {
// sometimes on IE postMessage fails mysteriously but, if repeated, works
try {
window.postMessage("Lightstreamer.run",origin);
} catch (e) {
// await next tick (at most 50ms on foreground page and 1s in background pages)
}
}
};
var postMessageHandler = function(event){
if (event.data == "Lightstreamer.run" && origin == "*" || event.origin == origin) {
doTick();
}
};
Helpers.addEvent(window,"message", postMessageHandler);
///verify if postMessage can be used
generateTick();
if (pendingGeneratedTick == false) {
//post message can't be used, rollback
Helpers.removeEvent(window,"message", postMessageHandler);
generateTickExecution = DEFAULT_GENERATE_TICK;
}
} else if (Environment.isNodeJS() && typeof process != "undefined" && process.nextTick) {
// on node versions having the nextTick method we rely on that
generateTickExecution = function() {
process.nextTick(doTick);
};
} // other cases will use the default implementation that's currently empty
} else {
clearInterval(timer);
}
//for "normal" tasks we use an interval
timer = setInterval(execute,step);
}
//main execution method, the core of the Executor
function execute() {
if (EnvironmentStatus.unloaded) {
clearInterval(timer);
return;
}
var last = now;
now = Helpers.getTimeStamp();
if (now < last) {
// not to be expected, but let's protect from this, because, otherwise,
// the order of the events might not be respected
now = last;
}
//adjustTimer(last, now);
if (toBeExecuted.length > 0) {
if (newStuffFlag) {
toBeExecuted.sort(sortFun);
newStuffFlag = false;
} //no new tasks = no need to sort
var exeNow;
while (toBeExecuted.length > 0 && toBeExecuted[0].time <= now && !EnvironmentStatus.unloaded) {
exeNow = toBeExecuted.shift();
if (exeNow.fun) {
Executor.executeTask(exeNow);
//prepare to re-enqueue repetetive tasks
if (exeNow.step) {
toBeRepeated.push(exeNow);
}
}
}
}
if (toBeExecuted.length <= 0) { //if queue is empty reset the index
nextId = 0;
}
// re-enqueue repetetive tasks
var t;
while(toBeRepeated.length > 0) {
t = toBeRepeated.shift();
if (t.step) { //a task might have called stopRepetitiveTask on this task
t.orderId = nextId++;
Executor.addPackedTimedTask(t,t.step,true);
}
}
if (now >= resetAt) {
resetAt = now+RESET_TIME;
toBeExecuted = [].concat(toBeExecuted);
}
}
/**
* An Executor based on a single setInterval that is triggered every 50ms to dequeue expired tasks.
* When 0ms tasks are enqueued a postMessage call is used to trigger an immediate execution; on nodejs
* the process.nextTick method is used in place of the postMessage; on older browser where postMessage
* is not supported no action is taken.
*
* @exports Executor
* @extends module:ExecutorInterface
*/
var Executor = {
toString: function() {
return ["[","Executor",step,toBeExecuted.length,/*this.goodWakeups,*/"]"].join("|");
},
getQueueLength: function() {
return toBeExecuted.length;
},
packTask: function(fun,context,params) {
return {fun:fun,context:context||null,params:params||null,orderId:nextId++};
},
addPackedTimedTask: function(task,time,repetitive) {
task.step = repetitive ? time : null;
task.time = now + parseInt(time);
// WARNING: "now" has not been refreshed;
// hence, with this implementation, the order of the events is guaranteed
// only when "time" is the same (or growing);
// we assume that sequences of tasks to be kept ordered will have a the same "time"
if (isNaN(task.time)) {
try {
throw new Error();
} catch(e) {
var err = "Executor error for time: " + time;
if (e.stack) {
err+= " " +e.stack;
}
throw err;
}
}
toBeExecuted.push(task);
newStuffFlag = true;
},
addRepetitiveTask: function(fun,interval,context,params) {
return this.addTimedTask(fun,interval,context,params,true);
},
stopRepetitiveTask: function(task) {
if (!task) {
return;
}
task.fun = null;
task.step = null;
},
addTimedTask: function(fun,time,context,params,repetitive) {
var task = this.packTask(fun,context,params);
this.addPackedTimedTask(task,time,repetitive);
if (time == 0) {
generateTick();
}
return task;
},
modifyTaskParam: function(task,index,newParam) {
task.params[index] = newParam;
},
modifyAllTaskParams: function(task,extParams) {
task.params = extParams;
},
delayTask: function(task,delay) {
task.time += delay;
newStuffFlag = true;
},
executeTask: function(task,extParams) {
try {
//IE doesn't like the simple form when useParams is null:
//task.fun.apply(task.context, task.params);
//if we leave the above code instead of using the below code, we fall into IE weird problem, where
//the execution fails in exception, task.fun results not null nor undefined, but if we try to print it
//(toString) or call it results as undefined (exception "Object expected").
var useParams = extParams || task.params;
if (task.context) {
if (useParams) {
task.fun.apply(task.context, useParams);
} else {
task.fun.apply(task.context);
}
} else if (useParams) {
task.fun.apply(null, useParams);
} else {
task.fun();
}
} catch (_e) {
var sendName = null;
try {
sendName = task.fun.name || task.fun.toString();
} catch(_x) {
}
//TODO report sendName
}
}
};
if (Environment.isWebWorker()) {
//we need this workaround otherwise on firefox 10 the Executor may not run as expected.
//I wasn't able to create a simple test case as it seems that the number of classes involved
//and the loading order have an impact on the issue (so that it is possible that once built the
//issue will not be there)
//I don't want to include BrowserDetection here so that I apply the workaround on all browsers
setTimeout(doInit,1);
//other possible workarounds (referring to the failing test)
//that make the Executor run correctly:
// *do not include Subscription
// *do not include the descriptor classes (inside the library code)
// *set the step value to a higher value (75 and 100 are suggested values that seem to work)
} else {
doInit();
}
Executor["getQueueLength"] = Executor.getQueueLength;
Executor["packTask"] = Executor.packTask;
Executor["addPackedTimedTask"] = Executor.addPackedTimedTask;
Executor["addRepetitiveTask"] = Executor.addRepetitiveTask;
Executor["stopRepetitiveTask"] = Executor.stopRepetitiveTask;
Executor["addTimedTask"] = Executor.addTimedTask;
Executor["modifyTaskParam"] = Executor.modifyTaskParam;
Executor["modifyAllTaskParams"] = Executor.modifyAllTaskParams;
Executor["delayTask"] = Executor.delayTask;
Executor["executeTask"] = Executor.executeTask;
return Executor;
})();
/*
function adjustTimer(last, now) {
var diff = now - last;
if (diff <= step) {
goodWakeups++;
} else {
goodWakeups--;
}
if (goodWakeups >= 10) {
changeStep(step+1);
goodWakeups = 0;
} else if (goodWakeups < 0) {
if (step >= 2) {
changeStep(Math.round(step / 2));
goodWakeups = 0;
} else {
goodWakeups = 0;
}
}
}
function changeStep (newStep) {
step = newStep;
doInit();
}
*/
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var EventDispatcher = /*@__PURE__*/(function() {
//var actionsLogger = LoggerManager.getLoggerProxy(LoggerManager.ACTIONS);
/**
* This constructor simply calls the {@link EventDispatcher#initDispatcher initDispatcher} method. This class is supposed
* to be extended using {@link module:Inheritance} extension.
* It can be either light extended or fully extended. When light extension is performed
* the {@link EventDispatcher#initDispatcher initDispatcher} method should be called in the extended class constructor.
* @constructor
*
* @exports EventDispatcher
* @class Class to be extended by classes requiring multiple listeners support.
* The dispatcher can act in two different ways, either synchronously (all listeners are triggered
* before the dispatching method exits) or asynchonously (all the listeners are triggered only when
* the currently running code has been executed).
* When extending this class is a good idea to also prepare an empty fake class to act as interface
* to keep track of the events that will be generated.
*
*
*
*
* @example
* //using light extension
* define(["Inheritance","EventDispatcher"],function(Inheritance,EventDispatcher) {
*
* var MyClass = function() {
* this.initDispatcher();
* //do stuff
* }
*
* MyClass.prototype = {
* foo: function() {
* //still doing stuff
*
* //send an eventName event to the listeners (their eventName method will be called)
* this.dispatchEvent("eventName",[paramForHandlers,otherParamForHandlers]);
*
* //do more stuff
* }
* };
*
* Inheritance(MyClass,EventDispatcher,true);
* return MyClass;
* });
*
* define([],function() {
*
* var MyClassListener = function() {
* //do stuff
* }
*
* MyClassListener = {
* eventName: function(param1,param2) {
* //handle event
* }
* };
*
* return MyClassListener;
* });
*/
var EventDispatcher = function() {
this.initDispatcher();
};
EventDispatcher.prototype = {
/**
* Initializes the required internal structures and configures the dispatcher
* for sending asynchronous events.
*
If called more than once it will reset the status of the instance.
*
This method MUST be called at least once before event dispatching can
* be exploited, otherwise most methods will fail either silently or with unexpected
* exceptions as no init-checks are performed by them.
* @protected
*
* @see EventDispatcher#useSynchEvents
*/
initDispatcher: function() {
this.theListeners = new AsymList();
this.synchEvents = false;
},
/**
* Adds a listener and fires the onListenStart event to it sending itself as only parameter.
* Note that the onListenStart event is only fired on the involved listener not on previously
* registered listeners.
*
* @param {EventDispatcherListener} aListener a listener to receive events notifications. The listener does not need
* to implement all of the possible events: missing events will not be fired on it.
*/
addListener: function(aListener) {
if (aListener && !this.theListeners.contains(aListener)) {
var obj = {handler:aListener,listening:true};
this.theListeners.add(obj);
this.dispatchToOneListener("onListenStart",[this],obj,true);
}
},
/**
* Removes a listener and fires the onListenEnd event to it sending itself as only parameter.
* Note that the onListenEnd event is only fired on the involved listener not on previously
* registered listeners.
*
* @param {EventDispatcherListener} aListener the listener to be removed.
*/
removeListener: function(aListener) {
if (!aListener) {
return;
}
var obj = this.theListeners.remove(aListener);
if (obj) {
this.dispatchToOneListener("onListenEnd",[this],obj,true);
}
},
/**
* Returns an array containing the currently active listeners.
*
* @returns {Array.} an array of listeners.
*/
getListeners: function() {
return this.theListeners.asArray();
},
/**
* Configures the EventDispatcher to send either synchronous or asynchronous events.
*
Synchronous events are fired on listeners before the {@link EventDispatcher#dispatchEvent} call
* of the related event is returned.
* Asynchronous events are fired after the current code block is completed and possibly
* after more code blocks are executed. Can be considered as if the calls are performed
* inside setTimeout with timeout 0.
*
* @param {Boolean} [useSynch=false] true to fire events synchronously, any other value to fire them
* asynchronously.
*
* @see EventDispatcher#initDispatcher
*/
useSynchEvents: function(useSynch) {
this.synchEvents = useSynch === true;
},
/**
* @private
* @param evt
* @param params
* @param listener
* @param forced
*/
dispatchToOneListener: function(evt,params,listener,forced) {
if (this.synchEvents) {
this.dispatchToOneListenerExecution(evt,params,listener,true);
} else {
Executor.addTimedTask(this.dispatchToOneListenerExecution,0,this,[evt,params,listener,forced]);
}
},
/**
* @private
* @param evt
* @param params
* @param listener
* @param forced
*/
dispatchToOneListenerExecution: function(evt,params,listener,forced) {
if (listener && listener.handler[evt] && (forced || listener.listening)) {
try {
//if we don't distinguish the two cases we will have problems on IE
if (params) {
listener.handler[evt].apply(listener.handler,params);
} else {
listener.handler[evt].apply(listener.handler);
}
} catch(_e) {
//actionsLogger.logError(LoggerManager.resolve(63),evt,_e);
}
}
},
/**
* Fires an event on all the listeners.
* @param {String} evt The name of the event to be fired. A method with this name will be called on the listeners.
* @param {Array} [params] An array of objects to be used as parameters for the functions handling the event.
* @see EventDispatcher#useSynchEvents
*/
dispatchEvent: function(evt,params) {
/*if (actionsLogger.isDebugLogEnabled()) {
actionsLogger.logDebug(LoggerManager.resolve(64),evt);
}*/
var that = this;
this.theListeners.forEach(function(el) {
that.dispatchToOneListener(evt,params,el,false);
});
}
};
//closure compiler exports
EventDispatcher.prototype["initDispatcher"] = EventDispatcher.prototype.initDispatcher;
EventDispatcher.prototype["addListener"] = EventDispatcher.prototype.addListener;
EventDispatcher.prototype["removeListener"] = EventDispatcher.prototype.removeListener;
EventDispatcher.prototype["getListeners"] = EventDispatcher.prototype.getListeners;
EventDispatcher.prototype["useSynchEvents"] = EventDispatcher.prototype.useSynchEvents;
EventDispatcher.prototype["dispatchEvent"] = EventDispatcher.prototype.dispatchEvent;
/**
* extend the List class to power up the remove method:
* as we get from outside object but we want to wrap them
* before adding to the list we need a way to remove
* the wrapped object given the original one
* we also change the return value
* @private
*/
var AsymList = function() {
this._callSuperConstructor(AsymList);
};
AsymList.prototype = {
remove: function(remEl) {
var i = this.find(remEl);
if (i < 0) {
return false;
}
var toRet = this.data[i];
toRet.listening = false;
this.data.splice(i,1);
return toRet;
},
find: function(el) {
for (var i=0; iThe class offers some management methods to modify/poll the model behind
* the view but also implements the {@link SubscriptionListener} interface to be
* automatically fed by listening on a {@link Subscription}.
*
When listening for Subscription events the widget will choose what to use
* as key for its model based on the Subscription mode of the first Subscription
* it was added to as a listener:
*
* - If the Subscription mode is MERGE or RAW, the widget will use the item as key:
* each subscribed item will generate a row in the model. The key name will be
* the item name when available, otherwise the 1-based item position within the
* Subscription.
* - If the Subscription mode is COMMAND, the widget will use the value of the
* "key" field as key: each row in the COMMAND subscription will generate a row
* in the model. More precisely, the key value will be expressed as "<item> <key>"
* where <item> is the item name when available, otherwise the 1-based item position
* within the Subscription.
*
Note that this behavior is naturally extended to two-level subscriptions.
* - If the Subscription mode is DISTINCT, the widget will use a progressive
* number as key: each update will generate a row in the model.
*
* For each update received, all the included fields will be integrated into
* the row related to the update key. The field name will be the one specified on
* the related Subscription, when available; otherwise, it will be the 1-based field
* position within the related Subscription.
*
Note that if the Subscription contains the same item name or field name multiple
* times, their updates will not be distinguished in the model and the last value
* processed by the library for that name will be assigned to the model.
* You should ensure that item name or field name collisions cannot occur if the
* colliding names are used to represent different entities; for instance, this holds for
* collisions between first-level and second-level fields in a two-level Subscription.
* Collisions are also possible if the widget is added as a listener to
* other Subscription instances. In this case, also note that the new updates will be
* processed and integrated in the model in the way already determined for the first
* Subscription associated; so, you should ensure that the various Subscriptions yield
* compatible updates.
*
For each {@link SubscriptionListener#onClearSnapshot} event received from any
* of the Subscription the widget is listening to, all the rows internally associated
* to the cleared item are removed. In case of collisions between different items feeding
* the same row the row will be considered pertaining to the first item that fed it.
*
*
Note that methods from the SubscriptionListener should not be called by
* custom code.
*
Note that before any change to the internal model can be made, and
* thus before an instance of this class can be used as listener for a
* Subscription, the {@link AbstractWidget#parseHtml} method has to be called to prepare the view.
*
The class is not meant as a base class for the creation of custom widgets.
* The class constructor and its prototype should never be used directly.
*
* @extends SubscriptionListener
*
* @see Chart
* @see AbstractGrid
*/
var AbstractWidget = function() {
this._callSuperConstructor(AbstractWidget);
this.kind = ITEM_IS_KEY;
this.keyField = null;
this.commandField = null;
this.updateCount = 0;
this.fieldPosBased = null;
this.values = new Matrix();
this.parsed = false;
this.id = arguments[0];
this.useSynchEvents(true);
this.cleanOnFirstSubscribe = false;
this.cleanOnLastUnsubscribe = false;
this.activeSubscriptions = 0;
this.masterSubscription = null;
this.forcedInterpretation = false;
this.updateInProgress = null;
this.suspendedUpdates = [];
this.fifoKeys = [];
this.fifoMap = {};
this.fifoHoles = 0;
this.fifoHead = 0;
this.itemKeyMap = new DoubleKeyMatrix();
};
/**
* @protected
* @ignore
*/
AbstractWidget.ITEM_IS_KEY = ITEM_IS_KEY;
/**
* @protected
* @ignore
*/
AbstractWidget.UPDATE_IS_KEY = UPDATE_IS_KEY;
AbstractWidget.prototype = {
/**
* @ignore
*/
getId: function() {
return this.id;
},
/**
* @protected
* @ignore
*/
checkParsed: function() {
if (!this.parsed) {
throw new IllegalStateException(PARSE_FIRST);
}
},
/**
* From SubscriptionListener
* @inheritdoc
*/
onItemUpdate: function(updateInfo) {
var itemName = updateInfo.getItemName();
var itemPos = updateInfo.getItemPos();
this.updateCount++;
var updateKey;
var itemId = itemName == null ? itemPos : itemName;
if (this.itemIsKey()) {
updateKey = itemId;
} else if (this.updateIsKey()) {
updateKey = this.updateCount;
} else { //this.keyIsKey()
updateKey = itemId+" "+updateInfo.getValue(this.keyField);
}
var updated = {}; //server values
if (this.updateIsKey()) {
updateInfo.forEachField(this.getForeachHandler(updated));
} else {
updateInfo.forEachChangedField(this.getForeachHandler(updated));
}
if (this.keyIsKey() && updated[this.commandField] == "DELETE") {
this.removeRow(updateKey);
//remove from itemKeyMap in any case (inside removeRow)
} else {
this.updateRow(updateKey,updated); //add and update collapsed
this.itemKeyMap.insert(true,itemId,updateKey); //add to the itemKeyMap only in the onItemUpdate case
//in case a key is associated to two different items the first one to reach the map rules
}
},
/**
* From SubscriptionListener
* @inheritdoc
*/
onClearSnapshot: function(itemName, itemPos) {
//get the associated row and remove them
var itemId = itemName == null ? itemPos : itemName;
var associatedRows = this.itemKeyMap.getRow(itemId);
this.itemKeyMap.delRow(itemId);
for (var key in associatedRows) {
this.removeRow(key);
}
},
/*
UNUSED SubscriptionListener methods
onItemLostUpdates: function(itemName, itemPos, lostUpdates) {
},
onCommandSecondLevelItemLostUpdates: function(howMany, key) {
},
onEndOfSnapshot: function(itemName, itemPos) {
},
onSubscriptionError: function(code, message) {
return;
},
onCommandSecondLevelSubscriptionError: function(code, message, key) {
},
*/
/**
* From SubscriptionListener
* @inheritdoc
*/
onSubscription: function() {
if (this.activeSubscriptions == 0 && this.cleanOnFirstSubscribe) {
this.clean();
}
if (this.keyIsKey() && !this.keyField) {
//masterSubscription may be not yet subscribed and I don't have info about this subscription...
//should I keep all the subscriptions in an array and then loop on them to find one that had already been subscribed?
this.keyField = this.masterSubscription.getKeyPosition();
this.commandField = this.masterSubscription.getCommandPosition();
}
this.activeSubscriptions++;
},
/**
* From SubscriptionListener
* @inheritdoc
*/
onUnsubscription: function() {
this.activeSubscriptions--;
if (this.activeSubscriptions == 0 && this.cleanOnLastUnsubscribe) {
this.clean();
}
},
/**
* From SubscriptionListener
* @inheritdoc
*/
onListenStart: function(sub) {
if (!this.masterSubscription) {
this.masterSubscription = sub;
if (!this.forcedInterpretation) {
this.chooseInterpretation();
}
}
if (sub.isSubscribed()) {
this.onSubscription();
}
},
/**
* From SubscriptionListener
* @inheritdoc
*/
onListenEnd: function(sub) {
if (sub.isSubscribed()) {
this.onUnsubscription();
}
},
/**
* @protected
* @ignore
*/
chooseInterpretation: function() {
if (!this.masterSubscription) {
this.kind = ITEM_IS_KEY;
return;
}
var sub = this.masterSubscription;
if (sub.getMode() == LightstreamerConstants.MERGE || sub.getMode() == LightstreamerConstants.RAW) {
this.kind = ITEM_IS_KEY;
} else if (sub.getMode() == LightstreamerConstants.DISTINCT) {
this.kind = UPDATE_IS_KEY;
} else { //LightstreamerConstants.COMMAND
this.kind = KEY_IS_KEY;
try {
sub.getFields();
//field names
this.keyField = "key";
this.commandField = "command";
} catch (e) {
//field position
}
}
},
/**
* @private
*/
getForeachHandler: function(updated) {
var that = this;
return function(name,pos,_new) {
if (that.fieldPosBased === null) {
that.fieldPosBased = name == null;
}
var index = that.fieldPosBased ? pos : name;
updated[index] = _new;
};
},
/**
* @protected
* @ignore
*/
itemIsKey: function() {
return this.kind == ITEM_IS_KEY;
},
/**
* @protected
* @ignore
*/
updateIsKey: function() {
return this.kind == UPDATE_IS_KEY;
},
/**
* @protected
* @ignore
*/
keyIsKey: function() {
return this.kind == KEY_IS_KEY;
},
/**
* @protected
* @ignore
*/
getOldestKey: function() {
if (this.fifoHead >= this.fifoKeys.length) {
return null;
}
return this.fifoKeys[this.fifoHead];
},
/**
* @protected
* @ignore
*/
removeFromFifo: function(key) {
var pos = this.fifoMap[key];
delete(this.fifoMap[key]);
this.fifoKeys[pos] = null;
this.fifoHoles++;
if (pos == this.fifoHead) {
while (this.fifoKeys[this.fifoHead] === null && this.fifoHead < this.fifoKeys.length) {
this.fifoHead++;
}
if (this.fifoHead >= this.fifoKeys.length) {
this.fifoKeys = [];
this.fifoMap = {};
this.fifoHoles = 0;
this.fifoHead = 0;
return;
}
}
if (this.fifoHoles >= MAX_FIFO_HOLES) {
this.fifoMap = {};
var oldArray = this.fifoKeys;
this.fifoKeys = [];
this.fifoHead = 0;
this.fifoHoles = 0;
for (var i=0; iLifecycle: once the {@link AbstractWidget#parseHtml} method has been called,
* this method can be used at any time.
*
* @throws {IllegalStateException} if parseHtml has not been executed yet.
*
* @param {String} key The key associated with the row to be removed.
*/
removeRow: function(key) {
this.checkParsed();
if (this.updateInProgress) {
this.deleteLater(key);
return;
}
if (!this.values.getRow(key)) {
gridsLogger.logWarn(LoggerManager.resolve(65),key,this);
return;
}
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(66),key,this);
}
this.updateInProgress = {}; //cannot be merged with anything but still have to pass the if (this.updateInProgress) check
var exeExc = null;
try {
this.removeRowExecution(key);
this.values.delRow(key);
this.itemKeyMap.delReverse(key);
if (this.updateIsKey()) {
this.removeFromFifo(key);
}
} catch(e) {
exeExc = e;
}
this.updateInProgress = null;
this.dequeuePostponedUpdates();
if (exeExc !== null) {
throw(exeExc);
}
},
/**
* @protected
* @ignore
*/
updateLater: function(key,newValues) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(67),this);
}
//postpone the update
this.suspendedUpdates.push({type:UPDATE,key:key,obj:newValues});
},
/**
* @protected
* @ignore
*/
deleteLater: function(key) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(68),this);
}
//postpone the update
this.suspendedUpdates.push({type:REMOVE,key:key});
},
/**
* @private
*/
dequeuePostponedUpdates: function() {
while (this.suspendedUpdates.length > 0) {
var otherUpdate = this.suspendedUpdates.shift();
if (otherUpdate.type == REMOVE) {
this.removeRow(otherUpdate.key);
} else {
this.updateRow(otherUpdate.key,otherUpdate.obj);
}
}
},
/**
* Updates a row in the internal model and reflects the change on the view.
* If no row associated with the given key is found then a new row is
* created.
*
Example usage:
*
myWidget.updateRow("key1", {field1:"val1",field2:"val2"});
*
* Lifecycle: once the {@link AbstractWidget#parseHtml} method has been called,
* this method can be used at any time. If called while an updateRow on the same
* internal model is still executing (e.g. if called while handling an onVisualUpdate
* callback), then the new update:
*
* - if pertaining to a different key and/or if called on a {@link Chart} instance,
* will be postponed until the first updateRow execution terminates;
* - if pertaining to the same key and if called on a {@link StaticGrid} / {@link DynaGrid}
* instance, will be merged with the current one.
*
*
*
* @throws {IllegalStateException} if parseHtml has not been executed yet.
*
* @param {String} key The key associated with the row to be updated/added.
* @param {Object} newValues A JavaScript object containing name/value pairs
* to fill the row in the mode.
*
Note that the internal model does not have a fixed number of fields;
* each update can add new fields to the model by simply specifying them.
* Also, an update having fewer fields than the current model will have its
* missing fields considered as unchanged.
*/
updateRow: function(key,newValues) {
this.checkParsed();
if (this.updateInProgress) {
//method called from the visualUpdate callback
if (key == this.updateInProgress) {
this.mergeUpdate(key,newValues);
} else {
this.updateLater(key,newValues);
}
return;
}
this.updateInProgress = key;
var exeExc = null;
try {
this.updateRowExecution(key,newValues);
if (!this.values.getRow(key)) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(69),key,this);
}
if (this.updateIsKey()) {
this.newKey(key);
}
this.values.insertRow(newValues,key);
} else {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(70),key,this);
}
for (var i in newValues) {
this.values.insert(newValues[i],key,i);
}
}
} catch(e) {
exeExc = e;
}
this.updateInProgress = null;
this.dequeuePostponedUpdates();
if (exeExc !== null) {
throw exeExc;
}
},
/**
* Removes all the rows from the model and reflects the change on the view.
*
* Lifecycle: once the {@link AbstractWidget#parseHtml} method has been called,
* this method can be used at any time.
*
* @throws {IllegalStateException} if parseHtml has not been executed yet.
*/
clean: function() {
gridsLogger.logInfo(LoggerManager.resolve(71),this);
var keys = [];
this.values.forEachRow(function(row) {
keys.push(row);
});
for (var i=0; iLifecycle: This method can be called at any time.
*
* @param {String} key The key associated with the row to be read.
* @param {String} field The field to be read from the row.
*
* @return {String} The current value for the specified field of the specified row,
* possibly null. If the value for the specified field has never been
* assigned in the model, the method also returns null.
*/
getValue: function(key,field) {
return this.values.get(key,field);
},
/**
* Utility method that can be used to control part of the behavior of
* the widget in case it is used as a listener for one or more
* {@link Subscription} instances.
*
Specifying the two flags it is possible to decide to clean the model and
* view based on the status (subscribed or not) of the Subscriptions this
* instance is listening to.
*
* Lifecycle: This method can be called at any time.
*
* @param {boolean} onFirstSubscribe If true a {@link AbstractWidget#clean} call will be
* automatically performed if in the list of Subscriptions this instance is
* listening to there is no Subscription in the subscribed status and an
* onSubscription is fired by one of such Subscriptions.
*
As a special case, if in the list of Subscriptions this instance is
* listening to there is no Subscription in the subscribed status and this
* instance starts listening to a new Subscription that is already in the
* subscribed status, then it will be considered as if an onSubscription
* event was fired and thus a clean() call will be performed.
*
* @param {boolean} onLastUnsubscribe If true a {@link AbstractWidget#clean} call will be
* automatically performed if in the list of Subscriptions this instance is
* listening to there is only one Subscription in the subscribed status and the
* onUnsubscription for such Subscription is fired.
*
As a special case, if in the list of Subscriptions this instance is
* listening to there is only one Subscription in the subscribed status and
* this instance stops listening to such Subscription then it will be
* considered as if the onUnsubscription event for that Subscription was fired
* and thus a clean() call will be performed.
*
* @see Subscription#isSubscribed
*/
setAutoCleanBehavior: function(onFirstSubscribe, onLastUnsubscribe) {
this.cleanOnFirstSubscribe = this.checkBool(onFirstSubscribe);
this.cleanOnLastUnsubscribe = this.checkBool(onLastUnsubscribe);
},
/**
* Abstract method. See subclasses descriptions for details.
*/
parseHtml: function() {},
/**
* abstract method
* @protected
* @ignore
*/
updateRowExecution: function(key,serverValues) {},
/**
* abstract method
* @protected
* @ignore
*/
removeRowExecution: function(key) {},
/**
* abstract method
* @protected
* @ignore
*/
mergeUpdate: function(key,newValues) {}
};
//closure compiler exports
AbstractWidget.prototype["onItemUpdate"] = AbstractWidget.prototype.onItemUpdate;
AbstractWidget.prototype["onClearSnapshot"] = AbstractWidget.prototype.onClearSnapshot;
AbstractWidget.prototype["onSubscription"] = AbstractWidget.prototype.onSubscription;
AbstractWidget.prototype["onUnsubscription"] = AbstractWidget.prototype.onUnsubscription;
AbstractWidget.prototype["onListenStart"] = AbstractWidget.prototype.onListenStart;
AbstractWidget.prototype["onListenEnd"] = AbstractWidget.prototype.onListenEnd;
AbstractWidget.prototype["removeRow"] = AbstractWidget.prototype.removeRow;
AbstractWidget.prototype["updateRow"] = AbstractWidget.prototype.updateRow;
AbstractWidget.prototype["clean"] = AbstractWidget.prototype.clean;
AbstractWidget.prototype["getValue"] = AbstractWidget.prototype.getValue;
AbstractWidget.prototype["setAutoCleanBehavior"] = AbstractWidget.prototype.setAutoCleanBehavior;
AbstractWidget.prototype["parseHtml"] = AbstractWidget.prototype.parseHtml;
AbstractWidget.prototype["updateRowExecution"] = AbstractWidget.prototype.updateRowExecution;
AbstractWidget.prototype["removeRowExecution"] = AbstractWidget.prototype.removeRowExecution;
AbstractWidget.prototype["mergeUpdate"] = AbstractWidget.prototype.mergeUpdate;
Inheritance(AbstractWidget,EventDispatcher,false,true);
Inheritance(AbstractWidget,Setter,true,true);
return AbstractWidget;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Cell = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
var FIELD = "field";
var TABLE = "grid";
var ROW = "row";
var ITEM = "item";
var UPDATE = "update";
var VALUE = "value";
var IDENTIFICATION_NUMBER = "replica";
var TYPE = "fieldtype";
var VALID_TYPES = {
"extra": true,
"first-level": true,
"second-level": true
};
var FIRST_LEVEL = "first-level";
var SECOND_LEVEL = "second-level";
var HOT = 1;
var COLD = 2;
var VOID_VAL = "\u00A0"; // Constant to be inserted into cells in the DOM to empty them.
//var VOID_HTML = " "; //?
var SOURCE_ATTR_NAME = "source";
var SOURCE_ATTR_VALUE = "lightstreamer";
var valueType= {
"input": true,
"textarea": true};
//reads an attribute from an element
function getLSAttribute(el,name) {
if (Cell.useOldNames === false) {
return getNewAttribute(el,name);
} else if (Cell.useOldNames === true) {
return getOldAttribute(el,name);
} else {
var res = getNewAttribute(el,name);
if (res) {
Cell.useOldNames = false;
return res;
}
res = getOldAttribute(el,name);
if (res) {
Cell.useOldNames = true;
}
return res;
}
}
function getNewAttribute(el,name) {
if (el.dataset) {
if (el.dataset[name]) {
return el.dataset[name];
} else {
return el.getAttribute("data-"+name);
}
} else {
return el.getAttribute("data-"+name);
}
}
function getOldAttribute(el,name) {
return el.getAttribute(name);
}
/**
* Merges two arrays of css attributes
*/
function mergeStyleArrays(localAttr, rowAttr) {
if (!localAttr) {
return rowAttr;
}
for (var type in rowAttr) {
if (!localAttr[type]) {
if (localAttr[type] === null || localAttr[type] === ""){
continue;
}
localAttr[type] = rowAttr[type];
}
}
return localAttr;
}
/**
* verifies that the source="lightstreamer" property is present
*/
function verifyTag (node) {
var str = getLSAttribute(node,SOURCE_ATTR_NAME);
return str && str.toLowerCase() == SOURCE_ATTR_VALUE;
}
/*
* Update-What functions
* depending on the attribute to be updated
* a different write/read method will be attached to the cell
*/
////////////////updateWhat -> style.something
function getUpdateStyleFunction(updateWhat) {
return function(val) {
this.el.style[updateWhat] = val === VOID_VAL ? null : val;
};
}
function getRetrieveStyleFunction(retrieveWhat) {
return function(){
return this.el.style[retrieveWhat] || "";
};
}
////////////////updateWhat -> form.value
function updateFormValue(val) {
if (!val || val === VOID_VAL) {
this.el.value="";
} else {
this.el.value=val;
}
}
function retrieveFormValue() {
return this.el.value;
}
////////////////updateWhat -> content <- this is the "normal" case
function updateContent(val,useInner) {
if (useInner) {
this.el.innerHTML = val;
} else {
if (this.el.childNodes.length != 1 || this.el.firstChild.nodeType != 3) {
// we pass by here if
// * in the cell there is html (more than one child indicates at least one tag; one child but not
// nodeType 3 is not a text node): clean it
// * or the cell is completely empty (no firstChild):
// it was created by LS_cell and its contents was empty
if (this.el.firstChild != null) {
this.el.innerHTML = "";
}
this.el.appendChild(document.createTextNode(val));
} else {
// if the cell was taken over by convertHtmlStructures
// it is definitely a TextNode;
// if the cell has already been updated once in life
// it is certainly a TextNode
// in any case the test means that here there is necessarily a TextNode
this.el.firstChild.nodeValue = val;
}
}
}
function retrieveContent(getInner) {
if (getInner) {
return this.el.innerHTML;
} else if(this.el.firstChild) {
return this.el.firstChild.nodeValue;
}
return "";
}
////////////////updateWhat -> other attribute
function getUpdateAttributeFunction(updateWhat) {
if (updateWhat === VALUE) {
return updateFormValue;
}
return function(val) {
if (!val || val === VOID_VAL) {
this.el.removeAttribute(updateWhat);
} else {
this.el.setAttribute(updateWhat, val);
}
};
}
function getRetrieveAttributeFunction(retrieveWhat) {
if (retrieveWhat === VALUE) {
return retrieveFormValue;
}
return function(){
return this.el.getAttribute(retrieveWhat);
};
}
var nextId = 0;
/**
* @private
*/
var Cell = function(domCell,updateWhat) {
this.el = domCell;
/*public*/ this.isCell = true;
/*public*/ this.fadePhase = 0;
if (!updateWhat) {
//reads what is to update from the HTML
updateWhat = this.getUpdateWhat();
}
//setup read and write methods for this instance
if (updateWhat) {
if (updateWhat.toLowerCase().indexOf("style.") == 0) {
var styleToUpdate = updateWhat.slice(6);
this.updateValue = getUpdateStyleFunction(styleToUpdate);
this.retrieveValue = getRetrieveStyleFunction(styleToUpdate);
} else {
this.updateValue = getUpdateAttributeFunction(updateWhat);
this.retrieveValue = getRetrieveAttributeFunction(updateWhat);
}
} else {
var nodeName = domCell.nodeName.toLowerCase();
if (nodeName in valueType) {
this.updateValue = updateFormValue;
this.retrieveValue = retrieveFormValue;
} else {
this.updateValue = updateContent;
this.retrieveValue = retrieveContent;
}
}
this.cellId = nextId++;
this.updateCount = 0;
//will hold updates before being set on cell
this.nextFormattedValue = null;
this.newHotArray = null;
this.newColdArray = null;
//reads from the cell the current status for future cleaning
this.initialValue = this.retrieveValue(true);
this.initialClass = this.extractCSSClass();
this.initialStyles = this.extractStyles();
};
//expose constants
Cell.HOT = HOT;
Cell.COLD = COLD;
Cell.FIRST_LEVEL = FIRST_LEVEL;
Cell.SECOND_LEVEL = SECOND_LEVEL;
//both old and new names are accepted (i.e.: with or without the data- prefix)
//we will not admit a mix of old and new though
Cell.useOldNames = null; //this is only exposed for test purposes
/**
* Extract elements from the DOM
* @static
*/
Cell.getLSTags = function(root,nodeTypes) {
var lsTags = [];
if (!nodeTypes) {
nodeTypes = ["*"]; //gets all the tags
}
//TODO use selectors if available
for (var i = 0; i < nodeTypes.length; i++) {
var tempTags = root.getElementsByTagName(nodeTypes[i]);
for (var v = 0; v < tempTags.length; v++) {
if (verifyTag(tempTags[v])) {
lsTags.push(new Cell(tempTags[v]));
}
}
}
return lsTags;
};
Cell.verifyTag = verifyTag;
Cell.isAttachedToDOM = function(toFindNode) {
var prevCurrCell = null;
var currCell = toFindNode;
while (currCell != null && currCell != document) {
prevCurrCell = currCell;
currCell = currCell.parentNode;
}
if (currCell == null) {
if (prevCurrCell != null && prevCurrCell.nodeName == "HTML") {
return true;
} else {
return false;
}
} else {
return true;
}
};
Cell.prototype = {
//methods attached during creation
//updateValue: function(value,useInner) {}
//retrieveValue: function() {}
scrollHere: function(otherCell,useInner) {
this.updateValue(otherCell.retrieveValue(),useInner); //from cell to cell, using inner is safe
this.nextFormattedValue = otherCell.nextFormattedValue;
this.newHotArray = otherCell.newHotArray;
this.newColdArray = otherCell.newColdArray;
this.updateCount = otherCell.updateCount;
this.setAttributes(otherCell.extractStyles());
this.updateCSSClass(otherCell.extractCSSClass());
this.fadePhase = otherCell.fadePhase;
},
getEl: function() {
return this.el;
},
extractStyles: function() {
var res = {};
for (var s in this.el.style) {
res[s] = this.el.style[s];
}
return res;
},
extractCSSClass: function() {
return this.el.className;
},
updateCSSClass: function(writeClass) {
if (writeClass !== null && this.el.className != writeClass) {
this.el.className = writeClass;
}
},
setAttributes: function(attrArr) {
if (!attrArr) {
return;
}
for (var attrName in attrArr) {
if (attrName == "CLASS") {
this.updateCSSClass(attrArr[attrName]);
}
try {
if (attrArr[attrName] !== null) {
this.el.style[attrName] = attrArr[attrName];
}
} catch(e) {
//old browsers (FX2 IE6) may pass from here
}
}
},
asynchUpdateStyles: function(ph,applyWhat) {
if (ph != this.updateCount) {
return;
}
if (applyWhat == HOT) {
this.setAttributes(this.newHotArray);
this.newHotArray = null;
} else { //if (applyWhat == COLD)
this.setAttributes(this.newColdArray);
this.newColdArray = null;
}
},
asynchUpdateValue: function(ph,useInner) {
if (ph != this.updateCount) {
return;
}
this.updateValue(this.nextFormattedValue,useInner);
this.nextFormattedValue = null;
this.asynchUpdateStyles(ph,HOT);
},
setUpdating: function() {
this.updateCount++;
return this.updateCount;
},
getField: function() {
var fieldName = getLSAttribute(this.el,FIELD);
if (!fieldName) {
return null;
}
return fieldName;
},
getNum: function() {
var identificationNum = getLSAttribute(this.el,IDENTIFICATION_NUMBER);
if (!identificationNum) {
return null;
}
return identificationNum;
},
getFieldType: function() {
var fieldType = getLSAttribute(this.el,TYPE);
if (!fieldType) {
return FIRST_LEVEL;
}
fieldType = fieldType.toLowerCase();
return VALID_TYPES[fieldType] ? fieldType : FIRST_LEVEL;
},
getTable: function() {
return getLSAttribute(this.el,TABLE);
},
getRow: function() {
var r1 = getLSAttribute(this.el,ITEM);
if (!r1) {
r1 = getLSAttribute(this.el,ROW);
}
return r1;
},
getUpdateWhat: function() {
return getLSAttribute(this.el,UPDATE);
},
incFadePhase: function() {
return ++this.fadePhase;
},
getFadePhase: function() {
return this.fadePhase;
},
getCellId: function() {
return this.cellId;
},
isSameEl: function(otherCell) {
return otherCell.el === this.el;
},
getTagName: function() {
return this.el.tagName;
},
isAttachedToDOM: function() {
return Cell.isAttachedToDOM(this.el);
},
isAttachedToDOMById: function() {
if (!this.el.id) {
return this.isAttachedToDOM(this.el);
}
var found = document.getElementById(this.el.id);
return (found === this.el);
},
setNextFormattedValue: function(fValue) {
this.nextFormattedValue = fValue === "" ? VOID_VAL : fValue;
},
getNextFormattedValue: function() {
return this.nextFormattedValue;
},
addStyle: function(hotValue, coldValue, type) {
if (!this.newHotArray) {
this.newHotArray = {};
}
if (!this.newColdArray) {
this.newColdArray = {};
}
this.newHotArray[type] = hotValue || "";
this.newColdArray[type] = coldValue || "";
},
getNextHotArray: function(mergeWith) {
if (mergeWith) {
this.newHotArray = mergeStyleArrays(this.newHotArray,mergeWith);
}
return this.newHotArray;
},
getNextColdArray: function(mergeWith) {
if (mergeWith) {
this.newColdArray = mergeStyleArrays(this.newColdArray,mergeWith);
}
return this.newColdArray;
},
clean: function() {
this.updateValue(this.initialValue,true);
this.updateCSSClass(this.initialClass);
this.setAttributes(this.initialStyles);
}
};
return Cell;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var ChartLine = /*@__PURE__*/(function() {
function getRelativeValue(value, min, unit) {
var val = new Number(value);
var n = (val - min) / unit;
return Math.round(n);
}
var BLACK = "black";
var gridsLogger = LoggerManager.getLoggerProxy("lightstreamer.charts");
var nextId = 0;
/**
* Used by Lightstreamer to provide a ChartLine object to each call of the
* {@link ChartListener#onNewLine} event.
* This constructor is not supposed to be used by custom code.
* @constructor
*
* @exports ChartLine
* @class Object that describes a single line of a multi-line
* chart. Instances of this class are automatically generated by a {@link Chart}
* instance based on the {@link Chart#setXAxis} and {@link Chart#addYAxis}
* configurations and can be customized during the {@link ChartListener#onNewLine}
* event.
*/
var ChartLine = function() {
//ChartLine(key,owner,xField,yField)
this.parent = arguments[1];
this.yField = arguments[3];
this.pointColor = BLACK;
this.lineColor = BLACK;
this.pointSize = 1;
this.lineSize = 1;
this.yMin = null;
this.yMax = null;
this.yUnit = null;
this.numYLabels = 0;
this.labelsFormatter = null;
this.xArray = [];
this.yArray = [];
this.labels = [];
this.chartId = nextId++;
gridsLogger.logDebug(LoggerManager.resolve(72),this);
};
ChartLine.prototype = {
/**
* @ignore
*/
toString: function() {
return ["[","ChartLine",this.parent,"]"].join("|");
},
/**
* @ignore
*/
getId: function() {
return this.chartId;
},
/**
* @ignore
*/
emptyHistory: function() {
gridsLogger.logDebug(LoggerManager.resolve(73),this);
this.yArray = [];
this.xArray = [];
},
/**
* @ignore
*/
isEmpty: function() {
return this.xArray.length <= 0;
},
/**
* @ignore
*/
reset: function() {
this.emptyHistory();
this.parent.clearLine(this);
},
/**
* @ignore
*/
repaint: function() {
gridsLogger.logDebug(LoggerManager.resolve(74),this);
var xArr = this.xArray;
var yArr = this.yArray;
this.reset();
//ASSERT.ensureValue(xArr.length,yArr.length);
while (xArr.length > 0) {
if ((xArr.length > 1 && xArr[1] >= this.parent.xMin) || xArr[0] >= this.parent.xMin) {
this.addPoint(xArr[0],yArr[0]);
}
xArr.shift();
yArr.shift();
}
gridsLogger.logDebug(LoggerManager.resolve(75),this);
},
/**
* @ignore
*/
addPoint: function(xVal,yVal) {
this.xArray.push(xVal);
this.yArray.push(yVal);
this.parent.drawLine(xVal,yVal,this);
},
/**
* @ignore
*/
calcYUnit: function() {
this.yUnit = (this.yMax - this.yMin) / this.parent.screenY;
gridsLogger.logDebug(LoggerManager.resolve(76),this,this.yUnit);
},
/**
* @ignore
*/
isYAxisPositioned: function() {
return this.yMax !== null;
},
/**
* @ignore
*/
isPointInRange: function(yCoordinate) {
return yCoordinate < this.yMax && yCoordinate > this.yMin;
},
/**
* @ignore
*/
getMin: function() {
return this.yMin;
},
/**
* @ignore
*/
getMax: function() {
return this.yMax;
},
/**
* @ignore
*/
paintYLabels: function() {
this.clearLabels();
var lblVal = "";
var pos = -1;
if (this.numYLabels <= 0) {
return;
}
if (this.numYLabels > 0) {
lblVal = this.labelsFormatter ? this.labelsFormatter(this.yMin) : this.yMin;
pos = this.getRelativeY(this.yMin);
this.labels[this.labels.length] = this.parent.createLabel(this.classYLabels, lblVal, pos, "Y");
}
if (this.numYLabels > 1) {
lblVal = this.labelsFormatter ? this.labelsFormatter(this.yMax) : this.yMax;
pos = this.getRelativeY(this.yMax);
this.labels[this.labels.length] = this.parent.createLabel(this.classYLabels, lblVal, pos, "Y");
}
if (this.numYLabels > 2) {
var divider = this.numYLabels - 1;
var step = (this.yMax - this.yMin) / divider;
var numVal = this.yMin;
for (var w = 1; w < divider; w++) {
numVal += step;
lblVal = this.labelsFormatter ? this.labelsFormatter(numVal) : numVal;
pos = this.getRelativeY(numVal);
this.labels[this.labels.length] = this.parent.createLabel(this.classYLabels, lblVal, pos, "Y");
}
}
gridsLogger.logDebug(LoggerManager.resolve(77),this);
},
/**
* @ignore
*/
getRelativeY: function(value) {
return getRelativeValue(value, this.yMin, this.yUnit);
},
/**
* @ignore
*/
clearLabels: function() {
for (var l = 0; l < this.labels.length; l++) {
if (this.labels[l] && Cell.isAttachedToDOM(this.labels[l])) {
this.labels[l].parentNode.removeChild(this.labels[l]);
}
}
this.labels = [];
gridsLogger.logDebug(LoggerManager.resolve(78),this);
},
/**
* Setter method that configures the legend for the Y axis. The legend
* consists of a specified number of labels for the values in the Y axis.
* The labels values are determined based on the axis limits; the labels
* appearance is controlled by supplying a stylesheet and a formatter
* function.
*
Note that the room for the Y axis labels on the page is not provided
* by the library; it should be provided by specifying a chart width
* smaller then the container element width and displaced on the right,
* through the {@link Chart#configureArea} setting.
* Moreover, as the upmost and lowest labels are centered on the chart
* area borders, a little space should be provided also over and under
* the chart area, through the same method.
*
* Lifecycle: Labels can be configured at any time.
* If not set, no labels are displayed relative to the Y axis.
* If set for different ChartLine instances on the same Chart
* then more sets of labels will be printed.
*
* @throws {IllegalArgumentException} if labelsNum is not a valid
* positive integer number.
*
* @param {Number} labelsNum the number of labels to be spread on the
* Y axis; it should be 1 or greater.
* @param {String} [labelsClass] the name of an existing stylesheet, to be
* applied to the Y axis label HTML elements. The parameter is optional;
* if missing or null, then no specific stylesheet is applied.
* @param {LabelsFormatter} [labelsFormatter] a Function instance
* used to format the Y axis values designated for the labels.
*
The function will be invoked with a Number argument and should return a String.
* If the function is not supplied, then the value will be used with no further formatting.
*/
setYLabels: function(labelsNum, labelsClass, labelsFormatter) {
this.numYLabels = this.checkPositiveNumber(labelsNum,true);
this.classYLabels = labelsClass;
this.labelsFormatter = labelsFormatter || null;
gridsLogger.logDebug(LoggerManager.resolve(79),this);
if (this.yUnit != null && this.parent && this.parent.painter) {
//l'asse Y � gi� configurato, disegno le labels
this.paintYLabels(); //screenY e yMax (yUnit) - chartArea.parentNode
}
},
/**
* Setter method that sets the style to be applied to the points
* drawn on the chart area. Colors of the points,
* and lines can be customized using valid CSS colors while size is specified
* in pixels.
*
* @throws {IllegalArgumentException} if pointSize or lineSize are not
* valid positive integer numbers.
*
* @param {String} [pointColor=black]the color use to draw the points on the chart.
* A point is drawn per each new value in the model. Any valid CSS color can
* be used. By default "black" is used.
* @param {String} [lineColor=black] the color use to draw the lines on the chart.
* A line is to connect two consecutive points for the same line.
* Any valid CSS color can be used. By default "black" is used.
* @param {Number} [pointSize=1] the size in pixel of the drawn points.
* By default 1 is used.
* @param {Number} [lineSize=1] the size in pixel of the drawn lines.
* By default 1 is used.
*/
setStyle: function(pointColor,lineColor,pointSize,lineSize) {
this.pointColor = pointColor;
this.lineColor = lineColor;
this.pointSize = this.checkPositiveNumber(pointSize);
this.lineSize = this.checkPositiveNumber(lineSize);
gridsLogger.logDebug(LoggerManager.resolve(80),this);
},
/**
* Operation method that sets or changes the limits for the visible part
* of the Y axis of the chart (that is, the minimum and maximum Y-coordinates
* shown in the chart for this line).
* When these limits are changed a full repaint of the line is performed.
*
* Lifecycle: The Y axis limits can be set at any time.
*
* @throws {IllegalArgumentException} if the min parameter is greater
* than the max one.
*
* @param {Number} min lower limit for the visible part of the Y axis.
* @param {Number} max higher limit for the visible part of the Y axis.
*/
positionYAxis: function(min, max) {
this.yMax = Number(max);
this.yMin = Number(min);
if (isNaN(this.yMax) || isNaN(this.yMin)) {
throw new IllegalArgumentException("Min and max must be numbers");
} if (this.yMin > this.yMax) {
throw new IllegalArgumentException("The maximum value must be greater than the minimum value");
}
if (this.parent && this.parent.screenY != null && this.parent.painter) {
this.calcYUnit();
this.paintYLabels(); //sreenY - yMax - chartArea.parentNode
if (!this.isEmpty()) {
this.repaint();
}
}
gridsLogger.logDebug(LoggerManager.resolve(81),this);
},
/**
* Inquiry method that retrieves the field in the Chart internal model
* representing the Y axis to which this ChartLine pertains.
*
* @return {Number} the field representing the Y axis.
*
* @see Chart#addYAxis
*/
getYField: function() {
return this.yField;
}
};
ChartLine.prototype["setYLabels"] = ChartLine.prototype.setYLabels;
ChartLine.prototype["setStyle"] = ChartLine.prototype.setStyle;
ChartLine.prototype["positionYAxis"] = ChartLine.prototype.positionYAxis;
ChartLine.prototype["getYField"] = ChartLine.prototype.getYField;
Inheritance(ChartLine,Setter,"O");
return ChartLine;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var ASSERT = /*@__PURE__*/(function() {
var logger = LoggerManager.getLoggerProxy("weswit.test");
var failures = 0;
var VOID = {};
/**
* you can use method of ASSERT to verify conditions
* If a conditions is not met ASSERT.failures is increased
* and an error log line is printed on the ASSERT category
* @exports ASSERT
*/
var ASSERT = {
/**
* The VOID property to be used in various calls
*/
"VOID": VOID,
/**
* Gets the number of failures. A failure is added
* each time any of the other methods return false.
* @returns {Number}
*/
getFailures: function() {
return failures;
},
/**
* Checks that two arrays contain the same elements
* the sameOrder flag can be used to specify if the
* elements must be in the same order in both arrays.
*
* @param {Array} arr1 the first array to be compared
* @param {Array} expected the second array to be compared
* @param {Boolean} sameOrder true if the elements in the
* arrays must be placed in the same order, false otherwise.
* In the latter case duplicated entries will be considered
* as one.
*
* @return true if the test pass, false otherwise.
*/
compareArrays: function(arr1,expected,sameOrder) {
if (arr1.length != expected.length) {
this.failInternal();
logger.logError(LoggerManager.resolve(82),arr1,expected);
return false;
}
if (!sameOrder) {
var genMap = {};
for (var i=0; i=dyabs) {
slope=dy/dx;
end = dx;
move = dx >= 0 ? 1 : -1;
} else {
slope=dx/dy;
end = dy;
move = dy >= 0 ? 1 : -1;
}
var pixDimX = 0;
var pixDimY = 0;
var pixRoundDimX = null;
var pixRoundDimY = null;
var getDim = true;
var moreHorizontal = true;
if (dxabs 0) {
py -= toAddDim;
}
}
}
px -= Math.floor(pixRoundDimX / 2);
py -= Math.floor(pixRoundDimY / 2);
pix.style.left=px + "px";
pix.style.top=py + "px";
pix.style.width=pw + "px";
pix.style.height=ph + "px";
}
this.drawPoint(xval,yval);
},
drawPoint: function(xval,yval) {
//in the div case we skip painting the initial point,
//other points are drawn by the main method
//DRAW POINT
this.lastX = xval;
this.lastY = yval;
},
clear: function() {
if (this.pointArray[0] && Cell.isAttachedToDOM(this.pointArray[0])) {
for (var p = 0; p < this.pointArray.length; p++) {
this.pointArray[p].parentNode.removeChild(this.pointArray[p]);
}
}
this.pointArray = [];
this.lastX = null;
this.lastY = null;
},
remove:function() {
this.clear();
}
};
return ChartPainter;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var Chart = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
var NO_ANCHOR = "A DOM element must be provided as an anchor for the chart";
function getRelativeValue(value, min, unit) {
var val = new Number(value);
var n = (val - min) / unit;
return Math.round(n);
}
var gridsLogger = LoggerManager.getLoggerProxy("lightstreamer.charts");
/*
______________________________
| _anchor |
| ____________________ |
| | caContainer | |
| | ______________ | |
| | | chartarea | | |
| | |____________| | |
| | _____________ | |
| | | label1 | | |
| | |___________| | |
| | _____________ | |
| | | label2 | | |
| | |___________| | |
| | _____________ | |
| | | labelN | | |
| | |___________| | |
| | | |
| |___________________| |
|____________________________|
caContainer -> pos relative - overflow visible
chartArea -> pos absolute - overflow hidden
*/
/**
* Creates an object that extends {@link AbstractWidget} displaying its values
* as a multiline chart.
*
Note that the {@link AbstractWidget#parseHtml} method is automatically called by this
* constructor, hence the container element should have already been prepared on the page DOM.
* However, preparing the element later and then invoking {@link AbstractWidget#parseHtml}
* manually is also supported.
* @constructor
*
* @param {String} id The HTML "id" attribute of a DOM Element to which the chart will be attached.
*
* @exports Chart
* @class A widget displaying the data from its model as a multiline chart.
* As with all the classes extending {@link AbstractWidget} the internal model
* can be automatically updated by listening to one or more {@link Subscription}
* instances.
*
In short, once both X and Y axis have been associated to a field through
* {@link Chart#setXAxis} and {@link Chart#addYAxis},
* each row in the model will be represented as a line in the chart,
* connecting all the X,Y points corresponding to the subsequent values assumed
* by the related fields and dynamically extending with new values. Actually,
* it is possible to associate more fields to the Y axis so that it is possible to
* have more than one line per row.
*
According to the axis settings, every time a row enters the model,
* one or more lines will be added to the chart and corresponding instances of
* {@link ChartLine} will be generated
* and passed to the {@link ChartListener#onNewLine} event to be better
* configured.
*
The behavior of the underlying model is described in {@link AbstractWidget},
* but there is one exception: if this instance is used to listen to events from
* {@link Subscription} instance(s), and the first Subscription it listens to is
* a DISTINCT Subscription, then the base behavior is overridden and the same
* behavior defined for MERGE and RAW modes is adopted.
*
*
Note that, in order to create a chart, the X axis should be associated
* with a field whose values are increasing. Anyway for both X and Y axis
* the used value must be a numeric value. If the original field values are not
* compliant with such restriction, they can be customized before being used by
* means of a parser Function.
*
Also, even if the Chart instance is listening to {@link Subscription} events,
* it is not mandatory to use server-sent fields to plot the chart.
*
The multiline chart for the visualization of model values is
* dynamically maintained by an instance of this class inside a container HTML
* element.
* The container element must be prepared on the page in the form of any HTML
* element owning the "data-source='Lightstreamer'" special attribute, together
* with an HTML "id" attribute that has to be specified in the constructor of this class.
*
* @extends AbstractWidget
*/
var Chart = function(id) {
this._callSuperConstructor(Chart,arguments);
this.caContainer = document.createElement("div");
this.caContainer.style.position = "relative";
this.caContainer.style.overflow = "visible";
this.areaClass = "";
this.offsetY = 0; //distance from top
this.offsetX = 0; //distance from left
this.screenX = null; //area width in pixel
this.screenY = null; //area height in pixel
this.labels = [];
this.labelsFormatter = null;
this.numXLabels = 0;
this.xFieldCode = null;
this.xMin = null; //the 0 value
this.xMax = null; //the maximum value
this.xUnit = null; //value of a pixel
this.xParser = null;
//by field
this.chartArray = {};
this.yParsers = {};
this.forcedDivPainting = false;
this.painter = null;
this.parseHtml();
};
Chart.prototype = {
/**
* @ignore
*/
toString: function() {
return ["[","Chart",this.id,"]"].join("|");
},
/**
* @ignore
*/
forceDivPainting: function(forced) {
//only for test purposes, disable the use of canvas methods
this.forcedDivPainting = forced === true;
},
/**
* @private
*/
initPainter: function() {
this.painter = new ChartPainter(this.forcedDivPainting);
this.painter.setContainer(this.caContainer);
this.configurePainter();
},
/**
* @private
*/
configurePainter: function() {
if (this.painter) {
this.painter.setSize(this.screenX,this.screenY);
this.painter.setOffset(this.offsetX,this.offsetY);
this.painter.setAreaStyle(this.areaClass);
gridsLogger.logDebug(LoggerManager.resolve(93));
}
},
/**
* @private
*/
setChartAnchor: function(anchor,cleaning) {
if (this.painter) {
return;
}
if (anchor && anchor.appendChild) {
anchor.appendChild(this.caContainer);
if (this.screenX == null) {
this.screenX = anchor.offsetWidth;
}
if (this.screenY == null) {
this.screenY = anchor.offsetHeight;
}
this.initPainter();
if (this.xMax != null) {
this.calcXUnit();
this.paintXLabels(); //screenX - xMax - chartArea.parentNode
}
for (var yField in this.chartArray) {
for (var line in this.chartArray[yField]) {
var cLine = this.chartArray[yField][line];
if (cLine && cLine.isYAxisPositioned()) {
cLine.calcYUnit();
cLine.paintYLabels(); //screenY - yMax - chartArea.parentNode
}
}
}
gridsLogger.logInfo(LoggerManager.resolve(94),this,anchor);
} else if (!cleaning) {
//should never pass from here
gridsLogger.logError(LoggerManager.resolve(95),this);
}
},
/**
* @private
*/
createLabel: function(lblClass, text, pos, axis) {
gridsLogger.logDebug(LoggerManager.resolve(96),this);
var lbl=document.createElement("div");
if (lblClass != null) {
lbl.className = lblClass;
}
lbl.style.position = "absolute";
var txt = document.createTextNode(text);
lbl.appendChild(txt);
this.caContainer.appendChild(lbl);
var labelWidth = lbl.offsetWidth;
if (axis.toUpperCase() == "X") {
lbl.style.top = (this.screenY + 5 + this.offsetY) + "px";
lbl.style.left = (pos - (lbl.offsetWidth / 2) + this.offsetX) + "px";
} else if (axis.toUpperCase() == "Y") {
lbl.style.left = (this.offsetX - labelWidth) + "px";
lbl.style.top = ((this.screenY - pos) - (lbl.offsetHeight / 2) + this.offsetY) + "px";
}
return lbl;
},
/**
* @ignore
*/
clearLine: function(currLine) {
this.painter.clearLine(currLine);
},
/**
* @ignore
*/
drawLine: function(xvalo, yvalo, currLine) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(97),this);
}
//from values to pixels
var xval=this.getRelativeX(xvalo);
var yval=currLine.getRelativeY(yvalo);
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(98),xval,yval);
}
this.painter.paintLine(currLine,xval,yval);
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(99));
}
},
/**
* @ignore
*/
repaintAll: function(xvalo, yvalo, currLine) {
gridsLogger.logDebug(LoggerManager.resolve(100));
for (var yField in this.chartArray) {
for (var line in this.chartArray[yField]) {
var cLine = this.chartArray[yField][line];
if (cLine && !cLine.isEmpty()) {
cLine.repaint();
}
}
}
},
/**
* @private
*/
calcXUnit: function() {
this.xUnit = (this.xMax - this.xMin) / this.screenX;
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(101),this,this.xUnit);
}
},
/**
* @private
*/
paintXLabels: function() {
this.clearLabels();
var lblVal = "";
var pos = -1;
if (this.numXLabels <= 0) {
return;
}
if (this.numXLabels > 0) {
lblVal = this.labelsFormatter ? this.labelsFormatter(this.xMin) : this.xMin;
pos = this.getRelativeX(this.xMin);
this.labels[this.labels.length] = this.createLabel(this.classXLabels, lblVal, pos, "X");
}
if (this.numXLabels > 1) {
lblVal = this.labelsFormatter ? this.labelsFormatter(this.xMax) : this.xMax;
pos = this.getRelativeX(this.xMax);
this.labels[this.labels.length] = this.createLabel(this.classXLabels, lblVal, pos, "X");
}
if (this.numXLabels > 2) {
var divider = this.numXLabels - 1;
var step = (this.xMax - this.xMin) / divider;
var numVal = this.xMin;
for (var w = 1; w < divider; w++) {
numVal += step;
lblVal = this.labelsFormatter ? this.labelsFormatter(numVal) : numVal;
pos = this.getRelativeX(numVal);
this.labels[this.labels.length] = this.createLabel(this.classXLabels, lblVal, pos, "X");
}
}
gridsLogger.logDebug(LoggerManager.resolve(102),this);
},
/**
* @private
*/
getRelativeX: function(value) {
return getRelativeValue(value, this.xMin,this.xUnit);
},
/**
* @private
*/
clearLabels: function() {
for (var l = 0; l < this.labels.length; l++) {
if (this.labels[l] && Cell.isAttachedToDOM(this.labels[l])) {
this.labels[l].parentNode.removeChild(this.labels[l]);
}
}
this.labels = [];
gridsLogger.logDebug(LoggerManager.resolve(103),this);
},
/**
* @inheritdoc
*/
onListenStart: function(sub) {
this._callSuperMethod(Chart,"onListenStart",[sub]);
if (this.updateIsKey()) {
//do not permit update is key on charts
this.kind = AbstractWidget.ITEM_IS_KEY;
}
},
/**
* @private
*/
parseValue: function(key,update,field,parser) {
var val = update[field] === null || typeof(update[field]) == "undefined" ? this.values.get(key,field) : update[field];
val = parser ? parser(val,key) : val;
return val === null ? null : Helpers.getNumber(val);
},
/**
* @ignore
*/
mergeUpdate: function(key,newValues) {
this.updateLater(key,newValues);
},
/**
* @ignore
*/
updateRowExecution: function(key,serverValues) {
/*
NOTE: null is like 0 but != 0
isNaN(null) -> false
null > 1 -> false
null < 1 -> true
null > -1 -> true
null == 0 -> false
Also:
xMax > 0 -> always
*/
//fix X
var newPosX = this.parseValue(key,serverValues,this.xFieldCode,this.xParser);
if (newPosX === null) {
//no old, no new, exit
return;
}
if (isNaN(newPosX) || (newPosX !== null && newPosX < this.xMin)) {
return;
}
//notify X overflows
if (newPosX > this.xMax) {
this.dispatchEvent("onXOverflow", [key, newPosX, this.xMin, this.xMax]);
}
for(var yFieldCode in this.chartArray) {
//fix Y
var newPosY = this.parseValue(key,serverValues,yFieldCode,this.yParsers[yFieldCode]);
if (isNaN(newPosY)) {
continue;
}
var chartLine = this.chartArray[yFieldCode][key];
if (newPosX == null || newPosY == null) {
if (chartLine && newPosX == null && newPosY == null) {
gridsLogger.logInfo(LoggerManager.resolve(104),this,chartLine);
//both nulls means clear
chartLine.reset();
continue;
} else {
gridsLogger.logDebug(LoggerManager.resolve(105),this,chartLine);
//if there are nulls we do not paint the point
continue;
}
}
if (!chartLine) {
//this is an add
chartLine = new ChartLine(key, this, this.xFieldCode, yFieldCode);
this.dispatchEvent("onNewLine",[key,chartLine,newPosX,newPosY]);
if (!chartLine.isYAxisPositioned()) {
gridsLogger.logError(LoggerManager.resolve(106),this);
return;
}
chartLine.calcYUnit();
chartLine.paintYLabels(); //screenY - yMax - chartArea.parentNode
this.chartArray[yFieldCode][key] = chartLine;
}
if (!chartLine.isPointInRange(newPosY)) {
this.dispatchEvent("onYOverflow", [key,chartLine,newPosY,chartLine.getMin(),chartLine.getMax()]);
}
chartLine.addPoint(newPosX,newPosY);
}
},
/**
* @ignore
*/
removeRowExecution: function(key) {
for (var yField in this.chartArray) {
this.deleteChartLine(key,yField);
}
},
/**
* @private
*/
deleteChartLine: function(key,field) {
if (!this.chartArray[field]) {
return;
}
var cLine = this.chartArray[field][key];
cLine.reset();//this removes the painting and the history
cLine.clearLabels();//this reemoves the labels
this.painter.removeLine(cLine); //this removes the handling
delete(this.chartArray[field][key]);
this.dispatchEvent("onRemovedLine",[key,cLine]);
gridsLogger.logDebug(LoggerManager.resolve(107),this,key,field);
},
/**
* @inheritdoc
*/
clean: function() {
this._callSuperMethod(Chart,"clean");
//this will call removeRowExecution per each "row"
//tolgo le label sulla X
this.clearLabels();
//tolgo il grafico
if (this.painter) {
this.painter.clean();
}
delete (this.painter);
this.setChartAnchor(this.caContainer.parentNode,true);
gridsLogger.logDebug(LoggerManager.resolve(108),this);
},
/**
* This method is automatically called by the constructor of this class.
* It will bind the current instance with the HTML element having the id
* specified in the constructor.
*/
parseHtml: function() {
gridsLogger.logInfo(LoggerManager.resolve(109),this);
var cAnchor = document.getElementById(this.id);
if (!cAnchor) {
//waiting for a valid call
return;
}
if (!Cell.verifyTag(cAnchor)) {
throw new IllegalStateException(NO_ANCHOR);
}
this.setChartAnchor(cAnchor);
this.parsed = true;
},
/**
* Setter method that sets the stylesheet and positioning to be applied to
* the chart area.
*
* Lifecycle: The chart area stylesheet and position attributes
* can be set and changed at any time.
*
* @throws {IllegalArgumentException} if one of the numeric values is not
* valid.
*
* @param {String} [chartCss] the name of an existing stylesheet to be applied to
* the chart. If not set, the stylesheet is inherited from
* the DOM element containing the chart.
*
* @param {Number} [chartHeight] the height in pixels of the chart area.
* Such height may be set as smaller than the height of the container
* HTML element in order to make room for the X axis labels. If not set,
* the whole height of the container HTML element is used.
*
* @param {Number} [chartWidth] the width in pixels of the chart area.
* Such width may be set as smaller than the width of the container HTML
* element in order to make room for the Y axis labels. If not set,
* the whole width of the container HTML element is used.
*
* @param {Number} [chartTop=0] the distance in pixels between the top margin of the
* chart area and the top margin of the container HTML element.
* Such distance may be set as a nonzero value in order to make room for
* the first Y axis label. If not set, 0 is used.
*
* @param {Number} [chartLeft=0] the distance in pixels between the left margin of
* the chart area and the left margin of the container HTML element.
* Such distance may be set as a nonzero value in order to make room for the
* Y axis labels. If not set, 0 is used.
*/
configureArea: function(chartCss,chartHeight,chartWidth,chartTop,chartLeft) {
if (chartCss) {
this.areaClass = chartCss;
}
if (chartTop) {
this.offsetY = this.checkPositiveNumber(chartTop, true);
}
if (chartLeft) {
this.offsetX = this.checkPositiveNumber(chartLeft, true);
}
if (chartHeight) {
this.screenY = this.checkPositiveNumber(chartHeight, true);
}
if (chartWidth) {
this.screenX = this.checkPositiveNumber(chartWidth, true);
}
this.configurePainter();
if (chartWidth || chartHeight) {
if (chartWidth && this.xMax != null) {
this.calcXUnit();
this.paintXLabels(); //screenX - xMax - chartArea.parentNode
}
for (var yField in this.chartArray) {
for (var line in this.chartArray[yField]) {
var cLine = this.chartArray[yField][line];
if (cLine && cLine.isYAxisPositioned() && this.xMax != null) {
if (chartHeight) {
cLine.calcYUnit();
cLine.paintYLabels(); //screenY - yMax - chartArea.parentNode
}
if (cLine && !cLine.isEmpty()) {
cLine.repaint();
}
}
}
}
}
},
/**
* Setter method that sets the field to be used as the source of the
* X-coordinate for each update. An optional parser can be passed to normalize
* the value before it is used to plot the chart.
* The resulting values should be in the limits posed by the
* {@link Chart#positionXAxis} method, otherwise a
* {@link ChartListener#onXOverflow} event is fired to handle the situation.
* null can also be specified, in which case, if the associated Y value is null
* the chart will be cleared, otherwise the update will be ignored.
*
* Lifecycle: The X axis field can be set at any time.
* Until set, no chart will be printed. If already set, the new setting only
* affects the new points while all the previously plotted points are cleaned.
*
* @param {String} field A field name representing the X axis.
* @param {CustomParserFunction} [xParser] A parser function that can be used to normalize
* the value of the X field before using it to plot the chart.
* If the function is not supplied,
* then the field values should represent valid numbers in JavaScript or be null.
*/
setXAxis: function(field, xParser) {
this.xFieldCode = field;
this.xParser = xParser;
this.clean();
gridsLogger.logDebug(LoggerManager.resolve(110),field,this);
},
/**
* Adds field(s) to be used as the source of the Y-coordinate for each update
* An optional parser can be passed to normalize the value before it is used to
* plot the chart.
* The resulting values should be in the limits posed by the
* {@link ChartLine#positionYAxis} related to the involved line, otherwise a
* {@link ChartListener#onYOverflow} event is fired to handle the situation.
* null can also be specified, in which case, if the associated X value is null
* the chart line will be cleared, otherwise the update will be ignored.
*
It is possible to specify an array of fields instead of specifying a
* single field. If that's the case multiple chart lines will be generated
* per each row in the model.
*
Note that for each field in the underlying model it is possible to associate
* only one line. If multiple lines based on the same fields are needed, dedicated
* fields should be added to the model, through {@link AbstractWidget#updateRow}.
* In case this instance is used to listen to events from {@link Subscription}
* instance(s), updateRow() can be invoked from within {@link SubscriptionListener#onItemUpdate}.
*
* Lifecycle: The method can be invoked at any time, in order to
* add fields to be plotted or in order to change the parser associated to
* fields already being plotted.
* Until invoked for the first time, no chart will be printed.
*
* @param {String} field A field name representing the Y axis. An array
* of field names can also be passed. Each field will generate its own line.
* @param {(CustomParserFunction|CustomParserFunction[])} [yParser] A parser function that can be used to normalize
* the value of the Y field before using it to plot the chart.
* If the function
* is not supplied, then the field values should represent valid numbers in JavaScript or be null.
*
If an array has been specified for the field parameter, then an array of parser functions can
* also be passed. Each parser will be executed on the field having the same index
* in the array. On the other hand, if an array of fields is passed but only one
* parser has been specified, then the parser will be applied to all of the fields.
*
* @see Chart#removeYAxis
*/
addYAxis: function(field, yParser) {
if (Helpers.isArray(field)) {
gridsLogger.logDebug(LoggerManager.resolve(111),this);
for (var i = 0; iIt is possible to specify an array of fields instead of specifying a
* single field. If that's the case all the specified fields and related chart lines
* will be removed.
*
* Lifecycle: The method can be invoked at any time, in order to
* remove plotted fields.
*
* @param {String} field A field name representing the Y axis. An array
* of field names can also be passed.
*
* @see Chart#addYAxis
*/
removeYAxis: function(field) {
if (Helpers.isArray(field)) {
gridsLogger.logDebug(LoggerManager.resolve(113),this);
for (var i = 0; iNote that rising the minimum X value shown also clears from
* the memory all the points whose X value becomes lower. So, those points
* will not be displayed again after lowering again the minimum X value.
*
* Lifecycle: The X axis limits can be set at any time.
*
* @throws {IllegalArgumentException} if the min parameter is greater
* than the max one.
*
* @param {Number} min lower limit for the visible part of the X axis.
* @param {Number} max higher limit for the visible part of the X axis.
*/
positionXAxis: function(min, max) {
this.xMax = Number(max);
this.xMin = Number(min);
if (isNaN(this.xMax) || isNaN(this.xMin)) {
throw new IllegalArgumentException("Min and max must be numbers");
} else if (this.xMin > this.xMax) {
throw new IllegalArgumentException("The maximum value must be greater than the minimum value");
}
if (this.screenX != null) {
this.calcXUnit();
this.paintXLabels(); //screenX - xMax - chartArea.parentNode
}
this.repaintAll();
gridsLogger.logDebug(LoggerManager.resolve(115),this);
},
/**
* Setter method that configures the legend for the X axis. The legend
* consists of a specified number of labels for the values in the X axis.
* The labels values are determined based on the axis limits; the labels
* appearance is controlled by supplying a stylesheet and a formatter
* function.
*
Note that the room for the X axis labels on the page is not provided
* by the library; it should be provided by specifying a chart height
* smaller than the container element height, through the
* {@link Chart#configureArea} setting. Moreover, as the first and last labels
* are centered on the chart area borders, a suitable space should be
* provided also on the left and right of the chart area, through the
* same method.
*
* Lifecycle: Labels can be configured at any time.
* If not set, no labels are displayed relative to the X axis.
*
* @throws {IllegalArgumentException} if labelsNum is not a valid
* poisitive integer number.
*
* @param {Number} labelsNum the number of labels to be spread on the
* X axis; it should be 1 or greater.
* @param {String} [labelsClass] the name of an existing stylesheet, to be
* applied to the X axis label HTML elements. The parameter is optional;
* if missing or null, then no specific stylesheet will be applied.
* @param {LabelsFormatter} [labelsFormatter] a Function instance
* used to format the X axis values designated for the labels.
* If the function is not supplied, then the value will be used with no further formatting.
*
*/
setXLabels: function(labelsNum, labelsClass, labelsFormatter) {
this.numXLabels = this.checkPositiveNumber(labelsNum,true);
this.classXLabels = labelsClass;
this.labelsFormatter = labelsFormatter || null;
if (this.xUnit != null) {
// the X axis is already configured, I design the labels
this.paintXLabels(); //screenX & xMax (xUnit) - chartArea.parentNode
}
gridsLogger.logDebug(LoggerManager.resolve(116),this);
},
/**
* Adds a listener that will receive events from the Chart
* instance.
*
The same listener can be added to several different Chart
* instances.
*
* Lifecycle: a listener can be added at any time.
*
* @param {ChartListener} listener An object that will receive the events
* as shown in the {@link ChartListener} interface.
*
Note that the given instance does not have to implement all of the
* methods of the ChartListener interface. In fact it may also
* implement none of the interface methods and still be considered a valid
* listener. In the latter case it will obviously receive no events.
*/
addListener: function(listener) {
this._callSuperMethod(Chart,"addListener",[listener]);
},
/**
* Removes a listener from the Chart instance so that it
* will not receive events anymore.
*
* Lifecycle: a listener can be removed at any time.
*
* @param {ChartListener} listener The listener to be removed.
*/
removeListener: function(listener) {
this._callSuperMethod(Chart,"removeListener",[listener]);
},
/**
* Returns an array containing the {@link ChartListener} instances that
* were added to this client.
*
* @return {ChartListener[]} an array containing the listeners that were added to this instance.
* Listeners added multiple times are included multiple times in the array.
*/
getListeners: function() {
return this._callSuperMethod(Chart,"getListeners");
}
};
//closure compiler exports
Chart.prototype["parseHtml"] = Chart.prototype.parseHtml;
Chart.prototype["configureArea"] = Chart.prototype.configureArea;
Chart.prototype["setXAxis"] = Chart.prototype.setXAxis;
Chart.prototype["addYAxis"] = Chart.prototype.addYAxis;
Chart.prototype["removeYAxis"] = Chart.prototype.removeYAxis;
Chart.prototype["positionXAxis"] = Chart.prototype.positionXAxis;
Chart.prototype["setXLabels"] = Chart.prototype.setXLabels;
Chart.prototype["addListener"] = Chart.prototype.addListener;
Chart.prototype["removeListener"] = Chart.prototype.removeListener;
Chart.prototype["getListeners"] = Chart.prototype.getListeners;
Chart.prototype["clean"] = Chart.prototype.clean;
Chart.prototype["onListenStart"] = Chart.prototype.onListenStart;
Chart.prototype["updateRowExecution"] = Chart.prototype.updateRowExecution;
Chart.prototype["removeRowExecution"] = Chart.prototype.removeRowExecution;
//<---- Listener Interface
Inheritance(Chart,AbstractWidget);
return Chart;
})();
/**
* Callback for {@link Chart#setXAxis} and {@link Chart#addYAxis}
* @callback CustomParserFunction
* @param {String} fieldValue the field value to be parsed.
* @param {String} key the key associated with the given value
* @return {Number} a valid number to be plotted or null if the value has to be considered unchanged
*/
/**
* Callback for {@link Chart#setXLabels} and {@link ChartLine#setYLabels}
* @callback LabelsFormatter
* @param {Number} value the value to be formatted before being print in a label.
* @return {String} the String to be set as content for the label.
*/
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var CellMatrix = /*@__PURE__*/(function() {
function rowGarbageCollection(currRow) {
var somethingInRow = false;
for (var field in currRow) {
if (!singleCellGarbageCollection(currRow[field])) {
delete(currRow[field]);
} else {
somethingInRow = true;
}
}
return somethingInRow;
}
function singleCellGarbageCollection(currCell) {
if (currCell.isCell) {
return currCell.isAttachedToDOMById();
} else {
var foundGroup = false;
for(var i=0; i [255,255,255]
* "FFFFFF" -> [255,255,255]
* "#FFF" -> [255,255,255]
* @private
*/
function hexColorToRGBArray(hexColor) {
if (hexColor.indexOf("#") == 0) {
hexColor = hexColor.substring(1,hexColor.length);
}
if (hexColor.length == 3) {
hexColor = hexColor.charAt(0)+hexColor.charAt(0)+hexColor.charAt(1)+hexColor.charAt(1)+hexColor.charAt(2)+hexColor.charAt(2);
} else if (hexColor.length != 6) {
gridsLogger.warn( "A hexadecimal color value must be 3 or 6 character long. An invalid value was specified, will be ignored");
return null;
}
var strR = hexColor.substring(0,2);
var strG = hexColor.substring(2,4);
var strB = hexColor.substring(4,6);
var numR = hexToDec(strR);
var numG = hexToDec(strG);
var numB = hexToDec(strB);
if (numR == null || numG == null || numB == null) {
return null;
}
return [numR,numG,numB];
}
var hexMap = {"A":10,"B":11,"C":12,"D":13,"E":14,"F":15};
function makeNum(str) {
if ((str >= 0) && (str <= 9)) {
return new Number(str);
}
str = str.toUpperCase();
if (hexMap[str]) {
return hexMap[str];
} else {
gridsLogger.warn("A hexadecimal number must contain numbers between 0 and 9 and letters between A and F. An invalid value was specified, will be ignored");
return null;
}
}
function hexToDec(hex) {
//return parseInt(hex,16); <- why not?
var res = 0;
var cicle = 0;
var i;
for (i = hex.length; i >= 1; i--) {
var tmp = makeNum(hex.substring(i-1,i));
if (tmp == null) {
return null;
}
var x;
for (x = 1; x <= cicle; x++) {
tmp *= 16;
}
cicle++;
res += tmp;
}
return res;
}
function ifPercToNum(num) {
if (num.indexOf("%") == num.length-1) {
num = parseFloat(num.substring(0,num.length-1));
if (num > 100 || num < 0) {
gridsLogger.warn("A RGB element must be a number >=0 and <=255 or a percentile >=0 and <=100. An invalid value was specified, will be ignored");
return null;
}
num = 2.55*num;
}
return num;
}
function isOkColor(foundColor, notExpectedColor) {
if (!foundColor || foundColor == "") {
return false;
} else if (!notExpectedColor) {
return true;
} else if (foundColor != notExpectedColor) {
return true;
} else {
return false;
}
}
function translateColorNameByDIV(colorName) {
var elem = document.createElement("DIV");
elem.style.backgroundColor = colorName;
var val = ColorConverter.getStyle(elem,bgJS, colorName);
if (val == null) {
// admitted by getStyle; but possible?
return null;
}
if (val[0] == 255 && val[1] == 255 && val[2] == 255) {
if (colorName.toUpperCase() != "WHITE" ) {
//I failed. I try to hang firstly
// the son on the page
var bodyEl = document.getElementsByTagName("BODY")[0];
if (bodyEl) {
bodyEl.appendChild(elem);
val = ColorConverter.getStyle(elem,bgJS, colorName);
bodyEl.removeChild(elem);
}
}
}
colorMap[colorName] = val;
return colorMap[colorName];
}
/**
* "white" -> [255,255,255]
*/
function nameToRGBArray(colorName) {
var res = "";
/* on IE we notice the double change of background, on FX no
* on FX using the engine to change the background does not work, on IE yes
* for Opera we create a div, we assign the color and we make a getStyle
*/
if (colorMap[colorName]) {
return colorMap[colorName];
}
//if (BrowserDetection.isProbablyOldOpera()) {
//if (window.getComputedStyle || (document.defaultView && document.defaultView.getComputedStyle)) {
//20110616: IE9 is still like this
if(!BrowserDetection.isProbablyIE()) {
return translateColorNameByDIV(colorName); //will fill colorMap itself
} else {
try {
colorFrame = IFrameHandler.getFrameWindow("weswit__ColorFrame",true);
if (colorFrame) {
colorFrame.document.bgColor = colorName;
res = colorFrame.document.bgColor;
}
} catch(_e) {
res=null;
}
}
//may still fail
if (!res || res == colorName) {
var initBG = document.bgColor;
document.bgColor = colorName;
res = document.bgColor;
document.bgColor = initBG;
}
if (!res || res == colorName) {
return translateColorNameByDIV(colorName); //will fill colorMap itself
} //else is in the hex form
colorMap[colorName] = hexColorToRGBArray(res);
return colorMap[colorName];
}
function RGBStringToRGBArray(rgbColor) {
var lenPre;
var thirdLimit;
if (rgbColor.indexOf("rgb(") == 0) {
lenPre = 4;
thirdLimit = ")";
} else if (rgbColor.indexOf("rgba(") == 0) {
lenPre = 5;
thirdLimit = ",";
} else{
gridsLogger.warn("A RGB color value must be in the form 'rgb(x, y, z)' or 'rgba(x, y, z, a)'. An invalid value was specified, will be ignored");
return null;
}
rgbColor = rgbColor.substring(lenPre,rgbColor.length);
var v1 = rgbColor.indexOf(",");
var numR = ifPercToNum(rgbColor.substring(0,v1));
var v2 = rgbColor.indexOf(",",v1+1);
var numG = ifPercToNum(rgbColor.substring(v1+1,v2));
var v3 = rgbColor.indexOf(thirdLimit,v2+1);
var numB = ifPercToNum(rgbColor.substring(v2+1,v3));
if (numR == null || numG == null || numB == null) {
return null;
}
return [numR,numG,numB];
}
///////
var bgCSS = "background-color";
var bgJS = "backgroundColor";
var transp = "transparent";
var colorMap = {};
var colorFrame=null;
/**
* @private
*/
var ColorConverter = {
/**
* Translates a color from any (string) form in an RGB array
*/
translateToRGBArray: function(val) {
if (val.indexOf("rgb") == 0) {
//"rgb(0, 255, 0)"
return RGBStringToRGBArray(val);
} else if (val.indexOf("#") == 0) {
//"#00FF00"
return hexColorToRGBArray(val);
} else {
//"green"
return nameToRGBArray(val);
}
},
/*public*/ getStyle: function(elem,styleProp,notExpectedColor) {
if (elem == null) {
//no element? let's say white
return [255,255,255];
}
var val = "";
try {
//do not move if conditions or opera will hate you
if (window.getComputedStyle || (document.defaultView && document.defaultView.getComputedStyle)) {
//try with getComputedStyle
var styleObj = document.defaultView.getComputedStyle(elem, null);
if (styleObj) {
var compProp = styleProp == bgJS ? bgCSS : styleProp;
val = styleObj.getPropertyValue(compProp);
}
}
}catch(e){}
try {
if (!isOkColor(val,notExpectedColor) && elem.currentStyle) {
//try with currentStyle
var compProp = styleProp == bgCSS ? bgJS : styleProp;
val = elem.currentStyle[compProp];
}
}catch(e){}
try {
if (!isOkColor(val,notExpectedColor)) {
//so far so bad... let's read from css, finger crossed
var upProp = styleProp == bgCSS ? bgJS : styleProp;
if (elem.style[upProp] != "") {
val = elem.style[upProp];
} else {
return [255,255,255];
}
}
}catch(e){}
if (val == transp && elem.parentNode) {
//trasparent color, let's check the parent
return this.getStyle(elem.parentNode,styleProp);
} else if (val == transp) {
return [255,255,255];
}
if (!isOkColor(val,notExpectedColor)) {
return [255,255,255];
}
return this.translateToRGBArray(val);
}
};
return ColorConverter;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var FadersHandler = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
//may convert into a full animation engine
/**
* @private
*/
var FadersHandler = function(interval){
this.fadeInterval = interval;
this.freeFaders = new FaderPile();
this.faderId = 0;
this.faderList = {};
this.fadeThread = false;
this.runningFaders = {};
};
FadersHandler.prototype = {
/**
* @private
* @param {Cell} cell
* @param goingDown
* @param endBack
* @param endFore
* @param millis
* @param endCommand
* @returns {String} id
*/
/*public*/ getNewFaderId: function(cell, goingDown, endBack, endFore, millis, endCommand) {
var steps = this.getHowManySteps(millis);
var actPhase = cell.incFadePhase();
if (!actPhase) {
return;
}
var retId = this.freeFaders.get();
if (retId == null) {
this.faderList[this.faderId] = new Fader(cell, goingDown, endBack, endFore, steps, actPhase, endCommand);
return this.faderId++;
} else {
this.faderList[retId].init(cell, goingDown, endBack, endFore, steps, actPhase, endCommand);
return retId;
}
},
/**
* @private
*/
getHowManySteps: function(time) {
var steps = time/this.fadeInterval;
return (steps > 1) ? steps : 1;
},
/*public*/ launchFader: function(id) {
var fader = this.faderList[id];
var currentPhase = fader.cell.getFadePhase();
if (!currentPhase) {
this.stopFader(fader.cell);
return;
}
if (fader.phase < currentPhase) {
return;
}
var oldId = this.runningFaders[fader.cell.getCellId()];
var oldFader = this.faderList[oldId];
if (oldFader) {
if (!oldFader.goingDown) {
if (fader.goingDown) {
if(oldFader.endCommand) {
Executor.executeTask(oldFader.endCommand);
}
} else {
fader.actStep = oldFader.actStep;
if (fader.steps < oldFader.steps) {
fader.steps = oldFader.steps;
}
}
}
this.freeFaders.put(oldId);
}
this.runningFaders[fader.cell.getCellId()] = id;
if (fader.endBack) {
fader.startBack = ColorConverter.getStyle(fader.cell.getEl(),"backgroundColor");
}
if (fader.endFore) {
fader.startFore = ColorConverter.getStyle(fader.cell.getEl(),"color");
}
if (!this.fadeThread) {
this.fadeThreadStart(this.fadeInterval);
}
},
/*public*/ stopFader: function(cell) {
var oldId = this.runningFaders[cell.getCellId()];
if (oldId || oldId == 0) {
delete(this.runningFaders[cell.getCellId()]);
this.freeFaders.put(oldId);
}
},
/**
* @private
*/
doFade: function(lastEndTime) {
var startTime = Helpers.getTimeStamp();
var lostWaiting = 0;
if (lastEndTime) {
lostWaiting = startTime - (lastEndTime + this.fadeInterval);
}
var atLeast1 = false;
for (var ind in this.runningFaders) {
var localFaderId = this.runningFaders[ind];
var fader = this.faderList[localFaderId];
if (fader.actStep > fader.steps) {
this.freeFaders.put(localFaderId);
delete (this.runningFaders[ind]);
if (fader.endCommand) {
Executor.addPackedTimedTask(fader.endCommand,0);
}
} else {
var upEl = fader.cell.getEl();
if (!upEl) {
this.stopFader(fader.cell);
continue;
}
if (fader.endBack == "transparent") {
try {
upEl.style.backgroundColor = "rgba(" +
fader.startBack[0] + "," +
fader.startBack[1] + "," +
fader.startBack[2] + "," +
this.easeInOut(100, 0, fader.steps, fader.actStep) / 100 + ")";
} catch(e) {
var timeToEnd = (fader.steps-fader.actStep)*this.fadeInterval;
Executor.addTimedTask(toTransparent(upEl),timeToEnd);
if (fader.endCommand) {
Executor.addPackedTimedTask(fader.endCommand,timeToEnd);
}
this.stopFader(fader.cell);
continue;
}
} else if (fader.endBack) {
upEl.style.backgroundColor = "rgb("+
this.easeInOut(fader.startBack[0],fader.endBack[0],fader.steps,fader.actStep) +","+
this.easeInOut(fader.startBack[1],fader.endBack[1],fader.steps,fader.actStep) +","+
this.easeInOut(fader.startBack[2],fader.endBack[2],fader.steps,fader.actStep) +")";
}
if (fader.endFore) {
upEl.style.color = "rgb("+
this.easeInOut(fader.startFore[0],fader.endFore[0],fader.steps,fader.actStep)+","+
this.easeInOut(fader.startFore[1],fader.endFore[1],fader.steps,fader.actStep)+","+
this.easeInOut(fader.startFore[2],fader.endFore[2],fader.steps,fader.actStep)+")";
}
atLeast1 = true;
}
fader.actStep++;
}
if (!atLeast1) {
this.fadeThread = false;
} else {
var endTime = Helpers.getTimeStamp();
var lostTime = (endTime - startTime);
var gainedDelay = lostTime + lostWaiting;
if (gainedDelay > this.fadeInterval) {
//the delay is greater than the step size, we need
//to skip steps
//steps to skip
var lostCycles = gainedDelay/this.fadeInterval;
var lostCyclesNorm = Math.floor(lostCycles);
var factionalCycles = lostCycles - lostCyclesNorm;
this.recoverCycles(lostCyclesNorm);
gainedDelay = this.fadeInterval * factionalCycles;
}
this.nextFade(this.fadeInterval-gainedDelay,endTime);
}
},
/**
* @private
*/
nextFade: function(pause,lastEndTime) {
Executor.addTimedTask(this.doFade,pause,this,[lastEndTime]);
},
/**
* @private
*/
recoverCycles: function(lostCyclesNorm) {
for (var ind in this.runningFaders) {
var localFaderId = this.runningFaders[ind];
var fader = this.faderList[localFaderId];
if (fader.actStep > fader.steps) ; else if (fader.actStep + lostCyclesNorm < fader.steps) {
//let's skip some steps
fader.actStep += lostCyclesNorm;
} else {
//move directly to the end
fader.actStep = fader.steps;
}
}
},
/**
* @private
*/
fadeThreadStart: function(timeout) {
if (this.fadeThread == true) {
return;
}
this.fadeThread = true;
this.nextFade(timeout);
},
/**
* @private
*/
easeInOut: function(minValue,maxValue,totalSteps,actualStep) {
minValue = new Number(minValue);
maxValue = new Number(maxValue);
var delta = maxValue - minValue;
var stepp = minValue+(((1 / totalSteps)*actualStep)*delta);
return Math.ceil(stepp);
},
/**
* may be exposed to offer fade functionality to non-Cell elements
* @private
*/
fadeCell: function(theTag, bgColor, textColor, fadeMillis, endCommand){
//then make it a task
var endCommandTask = Executor.packTask(endCommand);
var cell = new Cell(theTag);
var fadeId = this.getNewFaderId(cell, false, bgColor, textColor, fadeMillis, endCommandTask);
this.launchFader(fadeId);
return fadeId;
}
};
function toTransparent(upEl) {
return function() {
upEl.style.backgroundColor = "transparent";
};
}
/**
* @private
*/
var FaderPile = function() {
this.length = 0;
this.pile = {};
};
FaderPile.prototype = {
put: function(id) {
this.pile[this.length] = id;
this.length++;
},
get: function() {
if (this.length <= 0) {
return null;
}
this.length--;
return this.pile[this.length];
}
};
/**
* @private
*/
var Fader = function(cell, goingDown, endBack, endFore, steps, phase, endCommand) {
this.init(cell, goingDown, endBack, endFore, steps, phase, endCommand);
};
Fader.prototype = {
init: function(cell, goingDown, endBack, endFore, steps, phase, endCommand) {
this.endCommand = (endCommand) ? endCommand : null;
this.goingDown = goingDown;
this.cell = cell;
if (endBack === "" || endBack == "transparent") {
this.endBack = "transparent";
} else {
this.endBack = (endBack) ? ColorConverter.translateToRGBArray(endBack) : null;
}
this.endFore = (endFore) ? ColorConverter.translateToRGBArray(endFore) : null;
this.steps = steps;
this.phase = phase;
this.actStep = 0;
}
};
return FadersHandler;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var AbstractGrid = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
var BG_ATTR = "backgroundColor";
var COLOR_ATTR = "color";
var NO_TYPES = "The given array is not valid or empty";
var WRONG_INTERPRETATION = "The given value is not valid, use UPDATE_IS_KEY or ITEM_IS_KEY.";
var NON_EMPTY_ERROR = "This method can only be called while the grid is empty.";
var defaultTags = ["div","span","input"];
function preformatValue(val) {
return val === null ? "" : val;
}
var gridsLogger = LoggerManager.getLoggerProxy("lightstreamer.grids");
/**
* This is an abstract class; no instances of this class should be created.
* @constructor
*
* @exports AbstractGrid
* @class The base class for the hierarchy of the *Grid classes.
* Extends {@link AbstractWidget} to abstract a representation of the
* internal tabular model as a visible grid made of HTML elements.
* A specialized (derived) object, rather than an AbstractGrid instance, should
* be created and used. Two of such classes are available with the library:
* {@link StaticGrid} and {@link DynaGrid}.
* This class is used by the mentioned specialized grid objects to inherit the
* support for their basic configurations and common utilities.
*
The class is not meant as a base class for the creation of custom grids.
* The class constructor and its prototype should never be used directly.
*
* @extends AbstractWidget
*/
var AbstractGrid = function() {
//AbstractGrid(id)
this._callSuperConstructor(AbstractGrid,arguments);
//AbstractGrid
this.useInner = false;
this.fieldSymbols = null;
//ScreenTableHelper
this.tagsToCheck = defaultTags;
//Scroll - DynaScroll
this.addOnTop = false;
//Sorting
this.sortField = null;
this.descendingSort = false;
this.numericSort = false;
this.commaAsDecimalSeparator = false;
this.fader = new FadersHandler(50);
this.currentUpdateKey = null;
this.currentUpdateValues = null;
this.rowCount = 0;
this.maxRow = 0;
this.grid = new CellMatrix();
};
AbstractGrid.prototype = {
/**
* @protected
* @param key
* @param newValues
* @ignore
*/
mergeUpdate: function(key,newValues) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(117),this);
}
for (var i in newValues) {
this.currentUpdateValues[i] = newValues[i];
}
this.fillFormattedValues(this.currentUpdateKey,newValues);
},
/**
* @protected
* @param gridKey
* @param updated
* @ignore
*/
fillFormattedValues: function(gridKey,updated) {
for (var i in updated) {
this.grid.forEachCellInPosition(gridKey,i, function(upCell) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(118),gridKey,i);
}
upCell.setNextFormattedValue(preformatValue(updated[i]));
});
}
},
/**
* Sort comparison method
* @protected
* @ignore
*/
isBefore: function(val1, val2) {
if (val1 == null || val2 == null) {
if (val1 != val2) {
//only one value is null
if (val1 == null) {
//consider null smaller than anything else
return !this.descendingSort;
} else {
return this.descendingSort;
}
}
}
if (this.descendingSort) {
return val1 > val2;
} else {
return val1 < val2;
}
},
/**
* Setter method that enables or disables the interpretation of the
* values in the model as HTML code.
* For instance, if the value "<a href='news03.htm'>Click here</a>"
* is placed in the internal model (either by manual call of the
* {@link AbstractWidget#updateRow} method or by listening on a
* {@link SubscriptionListener#onItemUpdate} event)
* and HTML interpretation is enabled, then the target cell
* will contain a link; otherwise it will contain that bare text.
* Note that the setting applies to all the cells in the associated grid.
* Anyway if it's not the content of a cell that is going to be updated,
* but one of its properties, then this setting is irrelevant for such cell.
*
WARNING: When turning HTML interpretation on, make sure that
* no malicious code may reach the internal model (for example
* through the injection of undesired JavaScript code from the Data Adapter).
*
* Default value: false.
*
* Lifecycle: this setting can be changed at any time.
*
Note that values that have already been placed in the grid cells will not
* be updated to reflect the new setting.
*
* @throws {IllegalStateException} if parseHtml has not been executed yet.
* @throws {IllegalArgumentException} if the given value is not a valid
* boolean value.
*
* @param {boolean} enable true/false to enable/disable HTML interpretation
* for the pushed values.
*/
setHtmlInterpretationEnabled: function(enable) {
this.useInner = this.checkBool(enable);
},
/**
* Inquiry method that gets the type of interpretation to be applied for
* the pushed values for this grid. In fact, the values can be
* put in the target cells as HTML code or as text.
*
* @return {boolean} true if pushed values are interpreted as HTML code, false
* otherwise.
*
* @see AbstractGrid#setHtmlInterpretationEnabled
*/
isHtmlInterpretationEnabled: function() {
return this.useInner;
},
/**
* Setter method that specifies a list of HTML element types to be searched for
* during the mapping of the grid to the HTML made by {@link AbstractGrid#parseHtml}.
*
* Default value: an array containing DIV SPAN and INPUT.
*
* Lifecycle: Node types can be specified at any time.
* However, if the list is changed after the execution of the {@link AbstractGrid#parseHtml}
* method then it will not be used until a new call to such method is performed.
*
*
* @param {String[]} nodeTypes an array of Strings representing the names of the node
* types to be searched for. If the array contains an asterisk (*) then all the
* node types will be checked.
*
* @see AbstractGrid#parseHtml
*/
setNodeTypes: function(nodeTypes) {
if (nodeTypes && nodeTypes.length > 0) {
this.tagsToCheck = nodeTypes;
} else {
throw new IllegalArgumentException(NO_TYPES);
}
},
/**
* Inquiry method that gets the list of node of types that would be searched
* in case of a call to {@link AbstractGrid#parseHtml}.
*
* @return {String[]} a list of node type names.
*
* @see AbstractGrid#setNodeTypes
*/
getNodeTypes: function() {
return this.tagsToCheck;
},
/**
* Setter method that decides whenever new rows entering the model will be
* placed at the top of the grid or at the bottom.
*
Note that if the sort is enabled on the Grid through {@link AbstractGrid#setSort}
* then this setting is ignored as new rows will be placed on their right
* position based on the sort configuration.
*
Also note that the sort/add policy may be ignored depending on the grid
* configuration; see the use of the "data-item" cell attribute in {@link StaticGrid}.
*
* Default value: false.
*
* Lifecycle: this setting can be changed at any time.
*
Note anyway that changing this setting while the internal model
* is not empty may result in a incosistent view.
*
* @throws {IllegalArgumentException} if the given value is not a valid
* boolean value.
*
* @param {boolean} isAddOnTop true/false to place new rows entering the model
* as the first/last row of the grid.
*/
setAddOnTop: function(isAddOnTop) {
if (this.sortField != null) {
gridsLogger.logWarn(LoggerManager.resolve(119));
}
this.addOnTop = this.checkBool(isAddOnTop);
},
/**
* Inquiry method that gets true/false depending on how new rows
* entering the grid are treated. If true is returned, new rows will be placed on top of
* the grid. Viceversa, if false is returned, new rows are placed at the
* bottom.
*
* @return {boolean} true if new rows are added on top, false otherwise.
*
* @see AbstractGrid#setAddOnTop
*/
isAddOnTop: function() {
return this.addOnTop;
},
/**
* Setter method that configures the sort policy of the grid. If no
* sorting policy is set, new rows are always added according with the
* {@link AbstractGrid#setAddOnTop} setting.
* If, on the other hand, sorting is enabled, then new
* rows are positioned according to the sort criteria.
* Sorting is also maintained upon update of an existing row; this may cause the row to be
* repositioned.
*
If asynchronous row repositioning is undesired, it is possible to
* set the sort and immediately disable it with two consecutive calls
* to just enforce grid sorting based on the current contents.
*
The sort can also be performed on fields that are part of the model
* but not part of the grid view.
*
Note that the sort/add policy may be ignored depending on the grid
* configuration; see the use of the "data-item" cell attribute in {@link StaticGrid}.
*
* Default value: no sort is performed.
*
* Lifecycle: The sort configuration can be set and changed
* at any time.
*
* @throws {IllegalArgumentException} if one of the boolean parameters is neither
* missing, null, nor a valid boolean value.
*
* @param {String} sortField The name of the field to be used as sort field,
* or null to disable sorting.
* @param {boolean} [descendingSort=false] true or false to perform descending or
* ascending sort. This parameter is optional; if missing or null,
* then ascending sort is performed.
* @param {boolean} [numericSort=false] true or false to perform numeric or
* alphabetical sort. This parameter is optional; if missing or null, then
* alphabetical sort is performed.
* @param {boolean} [commaAsDecimalSeparator=false] true to specify that sort
* field values are decimal numbers in which the decimal separator is
* a comma; false to specify it is a dot. This setting is used only if
* numericSort is true, in which case it is optional, with false as its
* default value.
*/
setSort: function(sortField, descendingSort, numericSort, commaAsDecimalSeparator) {
if (!sortField) {
this.sortField = null;
return;
}
this.sortField = sortField;
this.descendingSort = this.checkBool(descendingSort,true);
this.numericSort = this.checkBool(numericSort,true);
this.commaAsDecimalSeparator = this.checkBool(commaAsDecimalSeparator,true);
this.sortTable();
},
/**
* Inquiry method that gets the name of the field currently used as sort
* field, if available.
*
* @return {Number} The name of a field, or null if sorting is not currently
* enabled.
*
* @see AbstractGrid#setSort
*/
getSortField: function() {
return this.sortField;
},
/**
* Inquiry method that gets the sort direction currently configured.
*
* @return {boolean} true if descending sort is being performed, false if ascending
* sort is, or null if sorting is not currently enabled.
*
* @see AbstractGrid#setSort
*/
isDescendingSort: function() {
return this.sortField === null ? null : this.descendingSort;
},
/**
* Inquiry method that gets the type of sort currently configured.
*
* @return {boolean} true if numeric sort is being performed, false if alphabetical
* sort is, or null if sorting is not currently enabled.
*
* @see AbstractGrid#setSort
*/
isNumericSort: function() {
return this.sortField === null ? null : this.numericSort;
},
/**
* Inquiry method that gets the type of interpretation to be used to
* parse the sort field values in order to perform numeric sort.
*
* @return {boolean} true if comma is the decimal separator, false if it is a dot;
* returns null if sorting is not currently enabled or numeric sorting
* is not currently configured.
*
* @see AbstractGrid#setSort
*/
isCommaAsDecimalSeparator: function() {
return this.sortField === null || !this.numericSort ? null : this.commaAsDecimalSeparator;
},
/**
* Creates an array containing all the unique values of the "data-field"
* properties in all of the HTML elements associated to this grid during the
* {@link AbstractGrid#parseHtml} execution. The result of this method is supposed to be
* used as "Field List" of a Subscription.
*
Execution of this method is pointless if HTML elements associated to this
* grid specify a field position instead of a field name in their "data-field"
* property.
*
Note that elements specifying the "data-fieldtype" property set to "extra" or "second-level",
* will be ignored by this method. This permits to distinguish fields that are part
* of the main subscription (not specifying any "data-fieldtype" or specifying "first-level"), part of a
* second-level Subscription (specifying "second-level") and not part of a Subscription at all,
* but still manageable in a direct way (specifying "extra").
*
* @return {String[]} The list of unique values found in the "data-field" properties
* of HTML element of this grid.
*
* @see Subscription#setFields
*/
extractFieldList: function() {
return this.extractTypedFieldList(Cell.FIRST_LEVEL);
},
/**
* Creates an array containing all the unique values, of the "data-field" properties
* in all of the HTML elements, having the "data-fieldtype" property set to "second-level",
* associated to this grid during the {@link AbstractGrid#parseHtml} execution.
*
The result of this method is supposed to be
* used as "Field List" of a second-level Subscription.
*
Execution of this method is pointless if HTML elements associated to this
* grid specify a field position instead of a field name in their "data-field"
* property.
*
* @return {String[]} The list of unique values found in the "data-field" properties
* of HTML element of this grid.
*
* @see AbstractGrid#extractFieldList
* @see Subscription#setCommandSecondLevelFields
*/
extractCommandSecondLevelFieldList: function() {
return this.extractTypedFieldList(Cell.SECOND_LEVEL);
},
/**
* Operation method that is used to authorize and execute the binding of the
* widget with the HTML of the page.
*
* Lifecycle: This method can only be called once the HTML structure
* the instance is expecting to find are ready in the DOM.
* That said, it can be invoked at any time and subsequent invocations will update
* the binding to the current state of the page DOM. Anyway, newly found cells
* will be left empty until the next update involving them.
*
* @see Chart
* @see DynaGrid
* @see StaticGrid
*/
parseHtml: function() {
},
/**
* Operation method that is used to force the choice of what to use
* as key for the integration in the internal model, when receiving
* an update from a Subscription this grid is listening to.
*
Specifying "ITEM_IS_KEY" tells the widget to use the item as key;
* this is the behavior that is already the default one when the Subscription
* is in "MERGE" or "RAW" mode (see {@link AbstractWidget} for details).
*
Specifying "UPDATE_IS_KEY" tells the widget to use a progressive number
* as key; this is the behavior that is already the default one when the
* Subscription is in "DISTINCT" mode (see {@link AbstractWidget} for details).
*
Note that when listening to different Subscriptions the default behavior
* is set when the grid is added as listener for the first one and then applied
* to all the others regardless of their mode.
*
* Lifecycle:this method can only be called
* while the internal model is empty.
*
* @throws {IllegalArgumentException} if the given value is not valid.
* @throws {IllegalStateException} if called while the grid is not empty.
*
* @param {String} interpretation either "ITEM_IS_KEY" or "UPDATE_IS_KEY",
* or null to restore the default behavior.
*/
forceSubscriptionInterpretation: function(interpretation) {
if (this.rowCount > 0) {
throw new IllegalStateException(NON_EMPTY_ERROR);
}
if (!interpretation) {
this.forcedInterpretation = false;
this.chooseInterpretation();
} else {
if (interpretation != AbstractWidget.UPDATE_IS_KEY && interpretation != AbstractWidget.ITEM_IS_KEY) {
throw new IllegalArgumentException(WRONG_INTERPRETATION);
}
this.kind = interpretation;
this.forcedInterpretation = true;
}
},
/**
* @private
*/
extractTypedFieldList: function(type) {
var fieldSymbolsSet = this.computeFieldSymbolsSet(type);
var incrementalSchema = [];
for (var fieldSymbol in fieldSymbolsSet) {
incrementalSchema.push(fieldSymbol);
}
return incrementalSchema;
},
/**
* @protected
* @ignore
*/
makeSortValue: function(val) {
if (this.numericSort) {
return Helpers.getNumber(val, this.commaAsDecimalSeparator);
} else if (val === null) {
return val;
} else {
return (new String(val)).toUpperCase();
}
},
/**
* @protected
* @ignore
*/
visualUpdateExecution: function(key,vUpdateInfo,updateKey) {
updateKey = updateKey || key;
var cold2HotTime = vUpdateInfo.coldToHotTime;
var toColdFadeTime = cold2HotTime + vUpdateInfo.hotTime;
var hot2ColdTime = toColdFadeTime + vUpdateInfo.hotToColdTime;
var hotRowStyle = vUpdateInfo.hotRowStyle;
var coldRowStyle = vUpdateInfo.coldRowStyle;
var cold2HotCall = [];
//let's pass cell by cell
var entireRow = this.grid.getRow(key);
for (var col in entireRow) {
var noNumIndex = -1;
var mayCell = entireRow[col];
for (var i=0; mayCell && (mayCell.isCell || i 0) {
var updateCellCommand = Executor.packTask(slidingCell.asynchUpdateValue,slidingCell,[phaseNum,this.useInner]);
var fadeId = this.fader.getNewFaderId(slidingCell, false, bgHot, colHot, cold2HotTime, updateCellCommand);
this.fader.launchFader(fadeId);
c2h = true;
} else {
this.fader.stopFader(slidingCell); //stop active fading if any
}
if (vUpdateInfo.hotToColdTime > 0) {
var applyStyleCommand = Executor.packTask(slidingCell.asynchUpdateStyles,slidingCell,[phaseNum,Cell.COLD]);
var fadeId = this.fader.getNewFaderId(slidingCell, true, bgCold, colCold, vUpdateInfo.hotToColdTime, applyStyleCommand);
Executor.addTimedTask(this.fader.launchFader,toColdFadeTime,this.fader,[fadeId]);
h2c = true;
}
}
if (!c2h) {
//cell update call
if (cold2HotTime > 0) {
Executor.addTimedTask(slidingCell.asynchUpdateValue,cold2HotTime,slidingCell,[phaseNum,this.useInner]);
} else {
var updateTask = Executor.packTask(slidingCell.asynchUpdateValue,slidingCell,[phaseNum,this.useInner]);
cold2HotCall.push(updateTask);
}
}
if (!h2c) {
//final cold state call
Executor.addTimedTask(slidingCell.asynchUpdateStyles,hot2ColdTime,slidingCell,[phaseNum,Cell.COLD]);
}
} else { //no styles to apply
if (cold2HotTime > 0) {
//Executor.addTimedTask(this.asynchUpdateCell,cold2HotTime,this,[updateKey,col,phaseNum]);
Executor.addTimedTask(slidingCell.asynchUpdateValue,cold2HotTime,slidingCell,[phaseNum,this.useInner]);
} else {
//var updateTask = Executor.packTask(this.asynchUpdateCell,this,[updateKey,col,phaseNum]);
var updateTask = Executor.packTask(slidingCell.asynchUpdateValue,slidingCell,[phaseNum,this.useInner]);
cold2HotCall.push(updateTask);
}
if (coldCellStyle) {
//final cold state call
if (vUpdateInfo.hotToColdTime > 0) {
bgCold = coldCellStyle[BG_ATTR];
colCold = coldCellStyle[COLOR_ATTR];
var applyStyleCommand = Executor.packTask(slidingCell.asynchUpdateStyles,slidingCell,[phaseNum,Cell.COLD]);
var fadeId = this.fader.getNewFaderId(slidingCell, true, bgCold, colCold, vUpdateInfo.hotToColdTime, applyStyleCommand);
Executor.addTimedTask(this.fader.launchFader,toColdFadeTime,this.fader,[fadeId]);
} else {
Executor.addTimedTask(slidingCell.asynchUpdateStyles,hot2ColdTime,slidingCell,[phaseNum,Cell.COLD]);
}
}
}
}
}
for (var t=0; t= newIndex + 1; pi--) {
this.map[pi] = this.map[pi-1];
this.hashMap[this.map[pi].getId()] = pi;
}
this.insertOperation(newNode, newIndex);
},
/*public*/ appendChild: function(node,appendOnBottom) {
if (!node) {
return;
}
//get rid of the previous parent
node.isolation();
if (appendOnBottom || this.length == 0) {
this.insertOperation(node, this.length);
} else {
this.insertBefore(node, this.map[0]);
}
},
/*private*/ insertOperation: function(node, index) {
this.length++;
this.hashMap[node.getId()] = index;
this.map[index] = node;
node.setParentNode(this);
node.invisible();
},
/*public*/ getChild: function(index) {
return this.map[index];
},
/*public*/ getElementById: function(id) {
return this.map[this.hashMap[id]];
},
/*public*/ clean: function() {
for (var i=0; i Specifically, for each row it supplies:
*
* - The current values for the row fields in the grid and in the
* underlying model and a method to modify the value in the gris cells
* before updating them on the DOM.
* - Methods to set the stylesheets to be applied to the HTML cells.
* - Methods to configure the visual effect to be applied to the HTML
* cells in order to emphasize the changes in the cell values.
*
* The provided visual effect consists of the following sequence:
*
* - A change in the cell colors, with a fading behaviour, to temporary
* "hot" colors.
* - The change of the values to the new values and the change of the
* stylesheets to the full "hot" stylesheet; after the change, the cell
* stays in this "hot" phase for a while.
* - A change in the cell colors, with a fading behaviour, to the final
* "cold" colors.
* - The change of the stylesheets to the full "cold" stylesheets.
*
*
The class constructor, its prototype and any other properties should never
* be used directly.
*
* @see StaticGridListener#onVisualUpdate
* @see DynaGridListener#onVisualUpdate
*/
var VisualUpdate = function() {
//TODO we didn't want the parameters to appear in the docs,
//what about now?
this.cellsGrid = arguments[0];
this.updatingRow = arguments[1];
this.key = arguments[2];
this.coldToHotTime = 0;
this.hotToColdTime = 0;
this.hotTime = DEFAULT_HOT_TIME;
this.hotRowStyle = null;
this.coldRowStyle = null;
};
VisualUpdate.prototype = {
/**
* Inquiry method that gets the value that is going to be shown in the grid
* in a specified cell or the current value if the value is not going to be
* changed.
*
Note that if the value is not changing then no effects or styles are
* going to be applied on the cell itself. If the effect is desired even if
* the value in the cell is unchanged, then a call to {@link VisualUpdate#setCellValue} can
* be performed using the value from this getter.
*
In order to inquiry the values for the row cells on the underlying
* model, the {@link VisualUpdate#getChangedFieldValue} method is available.
*
* @throws {IllegalArgumentException} if no cells were associated with
* the specified field.
*
* @param {String} field The field name associated with one of the cells in the
* grid (the "data-field" attribute).
* @param {String} [replicaId] A custom identifier that can be used in case two
* or more cells were defined for the same field (the "data-replica" attribute).
* If more cells have been defined but this parameter is not specified, then a random
* cell will be selected.
*
* @return {String} A text or null; if the value for the specified field has never been
* assigned in the model, the method also returns null.
*
* @see VisualUpdate#setCellValue
* @see VisualUpdate#getChangedFieldValue
*/
getCellValue: function(field,replicaId) {
var cell = this.cellsGrid.getCell(this.key,field,replicaId);
if (!cell) {
throw new IllegalArgumentException(NO_CELL);
}
if (!cell.isCell) {
//multiple cells for field and replicaID not specified
cell = cell[0];
}
return cell.getNextFormattedValue() || cell.retrieveValue();
},
/**
* Setter method that assigns the value to be shown in a specified cell of
* the grid.
* The specified value is the text that will be actually written in the cell
* (for instance, it may be a formatted version of the original value),
* unless it is null, in which case the value currently shown will be kept.
* The latter may still be the initial cell value (or the cell value
* specified on the template) if no formatted value hasn't been supplied
* for the field yet.
*
Note that this method does not update the internal model of the AbstractGrid
* so that if a value is set through this method it can't be used
* for features working on such model (e.g. it can't be used to sort the grid).
* If a change to the model is required use the {@link AbstractWidget#updateRow} method.
*
* @throws {IllegalArgumentException} if no cells were associated with
* the specified field.
*
* @param {String} field The field name associated with one of the cells in the
* grid (the "data-field" attribute).
* @param {String} value the value to be written in the cell, or null.
* @param {String} [replicaId] A custom identifier that can be used in case two
* or more cells were defined for the same field (the "data-replica" attribute).
* If more cells were defined but this parameter is not specified, then a random
* cell will be selected.
*/
setCellValue: function(field, value, replicaId) {
var cell = this.cellsGrid.getCell(this.key,field,replicaId);
if (!cell) {
throw new IllegalArgumentException(NO_CELL);
}
if (cell.isCell) {
cell.setNextFormattedValue(value);
} else for (var i=0; iBy default 1200 ms is set.
*
* @param {Number} val Duration in milliseconds of the "hot" phase.
*/
setHotTime: function(val) {
this.hotTime = this.checkPositiveNumber(val, true);
},
/**
* Setter method that configures the length of the color fading phase
* before the "hot" phase. This fading phase is one of the phases of
* the visual effect supplied by Lightstreamer to emphasize the change
* of the row values. A 0 length means that the color switch to "hot"
* colors should be instantaneous and should happen together with value
* and stylesheet switch.
*
Warning: The fading effect, if enabled, may be computation
* intensive for some client environments, when high-frequency updates
* are involved.
*
By default 0 ms (no fading at all) is set.
*
* @param {Number} val Duration in milliseconds of the fading phase before
* the "hot" phase.
*/
setColdToHotTime: function(val) {
this.coldToHotTime = this.checkPositiveNumber(val, true);
},
/**
* Setter method that configures the length of the color fading phase
* after the "hot" phase. This fading phase is one of the phases of
* the visual effect supplied by Lightstreamer to emphasize the change
* of the row values. A 0 length means that the color switch from "hot"
* to final "cold" colors should be instantaneous and should happen
* together with the stylesheet switch.
*
Warning: The fading effect, if enabled, may be very computation
* intensive for some client environments, when high-frequency updates
* are involved.
*
By default 0 ms (no fading at all) is set.
*
* @param {Number} val Duration in milliseconds of the fading phase after
* the "hot" phase.
*/
setHotToColdTime: function(val) {
this.hotToColdTime = this.checkPositiveNumber(val, true);
},
/**
* @private
*/
addStyle: function(field, hotValue, coldValue, _type, num) {
var cell = this.cellsGrid.getCell(this.key,field,num);
if (!cell) {
throw new IllegalArgumentException(NO_CELL);
}
if (cell.isCell) {
cell.addStyle(hotValue, coldValue, _type);
} else for (var i=0; iIf nonzero fading times are specified, through
* {@link VisualUpdate#setColdToHotTime} and/or {@link VisualUpdate#setHotToColdTime},
* then the "color" and "backgroundColor" attributes, if set, will be
* changed with a fading behaviour.
* Note that if color attributes are not set and nonzero fading times are
* specified in {@link VisualUpdate#setColdToHotTime} and/or {@link VisualUpdate#setHotToColdTime},
* this will cause a delay of the "hot" and "cold" phase switches;
* however, as fading times refer to the whole row, you may need to set
* them as nonzero in order to allow fading on some specific fields only.
*
If a row stylesheet is set through the {@link VisualUpdate#setStyle} method,
* then this method should be used only to set stylesheet properties
* not set by the row stylesheet. This condition applies throughout the
* whole lifecycle of the cell (i.e. manipulating the same style property
* through both methods, even at different times, does not guarantee
* the result).
*
By default for each stylesheet attribute that is not
* specified neither with this method nor with {@link VisualUpdate#setStyle}, the
* current value is left unchanged.
*
* @param {String} hotValue the temporary "hot" value for the involved
* attribute, or null if the attribute should not change while entering
* "hot" phase; an empty string causes the current attribute value
* to be cleared.
* @param {String} coldValue the final "cold" value for the involved
* attribute, or null if the attribute should not change while exiting
* "hot" phase; an empty string causes the "hot" phase attribute value
* to be cleared.
* @param {String} attrName the name of an HTML stylesheet attribute.
* The DOM attribute name should be used, not the CSS name (e.g.
* "backgroundColor" is accepted, while "background-color" is not).
* Note that if the "color" or "backgroundColor" attribute is being set,
* then several color name conventions are supported by the underlying
* DOM manipulation functions; however, in order to take advantage of the
* color fading support, only the "#RRGGBB" syntax is fully supported.
*/
setAttribute: function(hotValue, coldValue, attrName) {
this.addRowStyle(hotValue, coldValue, attrName);
},
/**
* Setter method that configures the stylesheets to be applied to the
* HTML cells of the involved row, while changing the field values.
* A temporary "hot" style can
* be specified as different than the final "cold" style. This allows
* Lightstreamer to perform a visual effect, in which a temporary "hot"
* phase is visible. By using this method, the names of existing
* stylesheets are supplied.
*
Note that in order to specify cell colors that can change with
* a fading behavior, the {@link VisualUpdate#setAttribute} method should be used instead,
* as fading is not supported when colors are specified in the stylesheets
* with this method. So, if nonzero fading times are specified in
* {@link VisualUpdate#setColdToHotTime} and/or {@link VisualUpdate#setHotToColdTime},
* this will just cause a delay of the "hot" and "cold" phase switches;
* however, as fading times refer to the whole row, you may need to set
* them as nonzero in order to allow fading on some specific fields only.
* for each stylesheet attribute that is not
* specified neither with this method nor with {@link VisualUpdate#setStyle}, the
* current value is left unchanged.
*
By default no stylesheet is applied to the cell.
*
* @param {String} hotStyle the name of the temporary "hot" stylesheet,
* or null if the cells style should not change while entering "hot" phase.
* @param {String} coldStyle the name of the final "cold" stylesheet,
* or null if the cells style should not change while exiting "hot" phase.
*/
setStyle: function(hotStyle, coldStyle) {
this.addRowStyle(hotStyle, coldStyle, "CLASS");
},
/**
* Setter method that configures the stylesheet changes to be applied
* to the HTML cell related with a specified field, while changing its
* value.
* The method can be used to override, for a specific field, the settings
* made through {@link VisualUpdate#setAttribute}.
*
If a specific stylesheet is assigned to the field through the
* {@link VisualUpdate#setStyle} or {@link VisualUpdate#setCellStyle} method,
* then this method can be used only in order to set stylesheet properties
* not set by the assigned specific stylesheet. This condition applies
* throughout the whole lifecycle of the cell (i.e. it is discouraged
* to manipulate the same style property through both methods,
* even at different times).
*
By default the settings possibly made by {@link VisualUpdate#setAttribute}
* are used.
*
* @throws {IllegalArgumentException} if no cells were associated with
* the specified field.
*
* @param {String} field The field name associated with one of the cells in the
* grid (the "data-field" attribute).
* @param {String} hotValue the temporary "hot" value for the involved
* attribute, or null if the attribute should not change while entering
* "hot" phase; an empty string causes the current attribute value
* to be cleared.
* @param {String} coldValue the final "cold" value for the involved
* attribute, or null if the attribute should not change while exiting
* "hot" phase; an empty string causes the "hot" phase attribute value
* to be cleared.
* @param {String} attrName the name of an HTML stylesheet attribute.
* The DOM attribute name should be used, not the CSS name (e.g.
* "backgroundColor" is accepted, while "background-color" is not).
* @param {String} [replicaId] A custom identifier that can be used in case two
* or more cells were defined for the same field (the "data-replica" attribute).
* If more cells were defined but this parameter is not specified, then a random
* cell will be selected.
*
* @see VisualUpdate#setAttribute
*/
setCellAttribute: function(field, hotValue, coldValue, attrName, replicaId) {
this.addStyle(field, hotValue, coldValue, attrName, replicaId);
},
/**
* Setter method that configures the stylesheet to be applied to the
* HTML cell related with a specified field, while changing its value.
*
This method can be used to override, for a specific field, the settings
* made through {@link VisualUpdate#setStyle}.
*
By default the stylesheet possibly set through {@link VisualUpdate#setStyle}
* is used.
*
* @throws {IllegalArgumentException} if no cells were associated with
* the specified field.
*
* @param {String} field The field name associated with one of the cells in the
* grid (the "data-field" attribute).
* @param {String} hotStyle the name of the temporary "hot" stylesheet,
* or null if the cell style should not change while entering "hot" phase
* (regardless of the settings made through {@link VisualUpdate#setStyle} and
* {@link VisualUpdate#setAttribute}).
* @param {String} coldStyle the name of the final "cold" stylesheet,
* or null if the cell style should not change while exiting "hot" phase
* (regardless of the settings made through {@link VisualUpdate#setStyle} and
* {@link VisualUpdate#setAttribute}).
* @param {String} [replicaId] A custom identifier that can be used in case two
* or more cells were defined for the same field (the "data-replica" attribute).
* If more cells were defined but this parameter is not specified, then a random
* cell will be selected.
*
* @see VisualUpdate#setStyle
*/
setCellStyle: function(field, hotStyle, coldStyle, replicaId) {
this.addStyle(field, hotStyle, coldStyle, "CLASS", replicaId);
},
/**
* Receives an iterator function and invokes it once per each field
* of the underlying model changed with the current update.
*
Note that in case of an event generated by the creation of a new row
* all the field will be iterated.
*
Note that the iterator is executed before this method returns.
*
Note that the iterator will iterate through all of the changed fields
* including fields not having associated cells. Also, even if a field is
* associated with more cells it will be passed to the iterator only once.
*
* @param {ChangedFieldCallback} iterator Function instance that will be called once per
* each field changed on the current update on the internal model.
*
* @see VisualUpdate#getChangedFieldValue
*/
forEachChangedField: function(iterator) {
for (var i in this.updatingRow) {
try {
iterator(i,this.updatingRow[i]);
} catch(_e) {
gridsLogger.logError(LoggerManager.resolve(120),_e);
}
}
}
};
//closure compiler exports
VisualUpdate.prototype["getCellValue"] = VisualUpdate.prototype.getCellValue;
VisualUpdate.prototype["setCellValue"] = VisualUpdate.prototype.setCellValue;
VisualUpdate.prototype["getChangedFieldValue"] = VisualUpdate.prototype.getChangedFieldValue;
VisualUpdate.prototype["setHotTime"] = VisualUpdate.prototype.setHotTime;
VisualUpdate.prototype["setColdToHotTime"] = VisualUpdate.prototype.setColdToHotTime;
VisualUpdate.prototype["setHotToColdTime"] = VisualUpdate.prototype.setHotToColdTime;
VisualUpdate.prototype["setAttribute"] = VisualUpdate.prototype.setAttribute;
VisualUpdate.prototype["setStyle"] = VisualUpdate.prototype.setStyle;
VisualUpdate.prototype["setCellAttribute"] = VisualUpdate.prototype.setCellAttribute;
VisualUpdate.prototype["setCellStyle"] = VisualUpdate.prototype.setCellStyle;
VisualUpdate.prototype["forEachChangedField"] = VisualUpdate.prototype.forEachChangedField;
Inheritance(VisualUpdate,Setter,true,true);
return VisualUpdate;
})();
/**
* Callback for {@link VisualUpdate#forEachChangedField}
* @callback ChangedFieldCallback
* @param {String} field name of the involved changed field.
* @param {String} value the new value for the field. See {@link VisualUpdate#getChangedFieldValue} for details.
* Note that changes to the values made through {@link VisualUpdate#setCellValue} calls will not be reflected
* by the iterator, as they don't affect the model.
*/
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var DynaGrid = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
var ELEMENT = "ELEMENT";
var PAGE = "PAGE";
var OFF = "OFF";
var WRONG_SCROLL_TYPE = "The given value is not a valid scroll type. Admitted values are OFF, ELEMENT, PAGE";
var NO_TEMPLATE = "No template defined";
var NO_SOURCE = "The template defined for the grid does not define the 'data-source' attribute";
var NO_CELLS = "No valid cells defined for grid";
var NO_PAGINATION = "This grid is configured to no support pagination";
var NO_PAGE_SWITCH = "Can't switch pages while 'no-page mode' is used";
var NO_AUTOSCROLL_ELEMENT = "Please specify an element id in order to use ELEMENT autoscroll";
var gridsLogger = LoggerManager.getLoggerProxy("lightstreamer.grids");
/**
* Creates an object that extends {@link AbstractGrid} displaying its values
* in a grid made of HTML elements. The grid rows are displayed into dynamically
* generated HTML rows. The object can be supplied to
* {@link Subscription#addListener} and {@link Subscription#removeListener}
* in order to display data from one or more Subscriptions.
* @constructor
*
* @param {String} id The HTML "id" attribute of the HTML element that represents the template from
* which rows of the grid will be cloned. The template can be either a visible
* or a hidden part of the page; anyway, it will become invisible
* as soon as the {@link AbstractGrid#parseHtml} method is executed.
*
* @param {boolean} autoParse If true the {@link AbstractGrid#parseHtml} method is executed
* before the constructor execution is completed. If false the parseHtml method
* has to be called later by custom code. It can be useful to set this flag
* to false if, at the time of the DynaGrid instance creation, the HTML element
* designated as template is not yet ready on the page.
*
* @exports DynaGrid
* @class An {@link AbstractGrid} implementation that can be used to display
* the values from the internal model in a dynamically created grid.
* The HTML structure suitable for the visualization of the tabular model values is
* dynamically maintained by Lightstreamer, starting from an HTML hidden template
* row, containing cells. The template row can be provided as any HTML element
* owning the "data-source='Lightstreamer'" special attribute.
*
The association between the DynaGrid and the HTML template is made during the
* execution of the {@link AbstractGrid#parseHtml} method: it is expected that the element
* representing the template row, in addition to the special "data-source"
* custom attribute, has an HTML "ID" attribute containing a unique value that has to be
* passed to the constructor of this class. The template will be then searched for by
* "id" on the page DOM.
*
Once the association is made with the row template, the cells within it have
* to be recognized: all the elements of the types specified in the
* {@link AbstractGrid#setNodeTypes} are scanned for the "data-source='Lightstreamer'"
* attribute that authorizes the library to track the HTML element as a cell
* for the row.
*
*
The "data-field" attribute will then instruct the library about
* what field of the internal model has to be associated with the cell.
*
It is possible to associate more cells with the same field.
* An optional "data-replica" attribute can be specified in this case. If used it will permit to access
* the single cells during {@link DynaGridListener#onVisualUpdate} executions.
*
*
By default, the content of the HTML element designated as cell will be
* updated with the value from the internal model; in case the cell is an INPUT
* or a TEXTAREA element, the "value" property will be updated instead.
* It is possible to update any attribute of the HTML element or its CSS
* styles by specifying the "data-update" custom attribute.
* To target an attribute the attribute name has to be specified; it can be a
* standard property (e.g. "data-update='href'"), a custom "data-" attribute
* (e.g. "data-update='data-foo'") or even a custom attribute not respecting
* the "data-" standard (e.g. "data-update='foo'").
* To target CSS attributes the "data-update='style.attributeName'" form has to
* be used (e.g. "data-update='style.backgroundColor'"); note that the form
* "data-update='style.background-color'" will not be recognized by some browsers.
*
WARNING: also events like "onclick" can be assigned; in such cases make
* sure that no malicious code may reach the internal model (for example
* through the injection of undesired JavaScript code from the Data Adapter).
*
For each update to the internal model, the involved row is determined and
* each value is displayed in the proper cell(s). If necessary, new rows are
* cloned from the hidden template and attached to the DOM, or existing rows are
* dropped. The position of new rows is determined by the {@link AbstractGrid#setAddOnTop}
* or {@link AbstractGrid#setSort} settings.
*
In fact, there is a 1:1 correspondence between rows in the underlying
* model and rows in the grid; however, pagination is also supported, so that
* only a subset of the grid can be made visible.
*
*
Note that the template element can contain an arbitrary HTML structure
* and should contain HTML cells to be used to display the row field values.
* However, it should not contain elements to which an HTML "id" attribute has been assigned,
* because the elements will be cloned and the HTML specification prescribes
* that an id must be unique in the document. (The id of the template element,
* required by Lightstreamer, is not cloned).
*
More visualization actions can be performed through the provided
* {@link VisualUpdate} event objects received on the {@link DynaGridListener}.
*
* @extends AbstractGrid
*/
var DynaGrid = function(id,autoParse) {
this._callSuperConstructor(DynaGrid,[id]);
this.pageNumber = 1;
this.currentPages = 0;
this.autoScrollElement = null;
this.autoScrollType = OFF;
this.initDOM();
autoParse = this.checkBool(autoParse,true);
if (autoParse) {
this.parseHtml();
}
};
DynaGrid.prototype = {
/**
* @ignore
*/
toString: function() {
return ["[",this.id,this.rowCount,this.currentPages,"]"].join("|");
},
/**
* Setter method that sets the maximum number of visible rows allowed
* in the grid.
* If a value for this property is set, then Lightstreamer
* maintains a paging mechanism, such that only one logical page is
* displayed at a time. Logical page 1 is shown by default, but each
* logical page can be shown by calling the {@link DynaGrid#goToPage} method.
*
Note that, due to the dynamical nature of the grid,
* logical pages other than page 1 may underlie to scrolling caused by
* operations on rows belonging to lower logical pages; this effect
* is emphasized if sorting is active.
*
Note that if this instance is used to listen to events from
* {@link Subscription} instance(s), and the first Subscription it listens to is
* a DISTINCT Subscription, then the behavior is different: when the limit
* posed by this setting is reached, adding a new row will always
* cause the removal of the oldest row from the model, with a
* consequent repositioning of the remaining rows.
*
* Default value: "unlimited".
*
* Lifecycle: this setting can be set and changed at any time.
* If the internal model is not empty when this method is called, it will cause
* the immediate adjustment of the rows to reflect the change. Moreover,
* if applicable, the current logical page is automatically switched to page 1.
*
*
* @param {Number} maxDynaRows The maximum number of visible rows allowed,
* or the string "unlimited", to mean that the grid is allowed
* to grow without limits, without the need for paging (the check is case
* insensitive).
*/
setMaxDynaRows: function(maxDynaRows) {
if (!maxDynaRows || new String(maxDynaRows).toLowerCase() == "unlimited") {
this.maxRow = 0;
} else {
this.maxRow = this.checkPositiveNumber(maxDynaRows,true);
}
if (this.updateIsKey()) {
this.limitRows();
} else {
this.calculatePages();
this.sortTable();
this.changePage(1);
}
},
/**
* Inquiry method that gets the maximum number of visible rows allowed
* in the grid.
*
* @return {Number} The maximum number of visible rows allowed, or the String
* "unlimited", to notify that the grid is allowed to grow
* without limits.
*
* @see DynaGrid#setMaxDynaRows
*/
getMaxDynaRows: function() {
if (this.maxRow == 0) {
return "unlimited";
}
return this.maxRow;
},
/**
* Operation method that shows a particular logical page in the internal model.
*
* Lifecycle: once the {@link AbstractGrid#parseHtml} method has been called,
* this method can be used at any time.
*
* @throws {IllegalStateException} if this instance is used to listen to events
* from {@link Subscription} instance(s), and the first Subscription it listens
* to is a DISTINCT Subscription (in such case pagination is disabled).
*
* @throws {IllegalStateException} if the maximum number of visible rows is
* set to unlimited.
*
* @throws {IllegalArgumentException} if the given value is not a valid
* positive integer number.
*
* @param {Number} pageNumber The number of the logical page to be displayed.
* The request is accepted even if the supplied number is higher than the
* number of currently available logical pages, by displaying an empty
* logical page, that may become nonempty as soon as enough rows are added
* to the internal model.
*
* @see DynaGrid#setMaxDynaRows
* @see DynaGrid#getCurrentPages
*/
goToPage: function(pageNumber) {
if (this.updateIsKey()) {
throw new IllegalStateException(NO_PAGINATION);
}
if (this.maxRow == 0) {
throw new IllegalStateException(NO_PAGE_SWITCH);
}
pageNumber = this.checkPositiveNumber(pageNumber);
this.changePage(pageNumber);
},
/**
* Inquiry method that gets the current number of nonempty logical pages
* needed to show the rows in the internal model.
*
* @return {Number} The current number of logical pages. If pagination is not active
* 1 is returned.
*/
getCurrentPages: function() {
return this.maxRow == 0 ? 1 : this.currentPages;
},
/**
* Setter method that enables or disables the automatic adjustment of
* the page or element scrollbars at each new update to focus on the most
* recently updated row.
* If a growing grid is included in an HTML element that declares
* (and supports) the "overflow" attribute then this element may develop
* a vertical scrollbar in order to contain all the rows. Also if the
* container elements do not declare any "overflow" CSS property, then the
* same may happen to the entire HTML page.
* In such a cases new rows added to the grid (or moved due to the sort settings)
* may be placed in the nonvisible part of the including element/page.
*
This can be avoided by enabling the auto-scroll. In this case,
* each time a row is added or updated, the scrollbar is repositioned
* to show the row involved. This feature, however, should be used only
* if the update rate is low or if this grid is listening to a DISTINCT
* Subscription; otherwise, the automatic scrolling activity may be excessive.
*
Note that in case the grid is configured in UPDATE_IS_KEY mode (that is
* the default mode used when the grid is listening to a DISTINCT subscription) and
* the scrollbar is moved from its automatic position, then the auto-scroll
* is disabled until the scrollbar is repositioned to its former
* position. This automatic interruption of the auto scrolling is not supported
* on pre-webkit Opera browsers.
*
The auto-scroll is performed only if single page mode is currently
* used (i.e. the maximum number of visible rows is set to unlimited).
*
* Default value: "OFF".
*
* Lifecycle: The auto-scroll policy can be set and changed
* at any time.
*
* @param {String} type The auto-scroll policy. Permitted values are:
*
* - "OFF": No auto-scrolling is required;
* - "ELEMENT": An element's scrollbar should auto-scroll;
* - "PAGE": The browser page's scrollbar should auto-scroll.
*
* @param {String} elementId The HTML "id" attribute of the HTML element whose scrollbar
* should auto-scroll, if the type argument is "ELEMENT"; not used,
* otherwise.
* @see DynaGrid#setMaxDynaRows
* @see AbstractGrid#forceSubscriptionInterpretation
*
*/
setAutoScroll: function(type, elementId) {
if (!type) {
throw new IllegalArgumentException(WRONG_SCROLL_TYPE);
}
type = new String(type).toUpperCase();
if (type == ELEMENT) {
if (!elementId) {
throw new IllegalArgumentException(NO_AUTOSCROLL_ELEMENT);
} else {
this.autoScrollElement = elementId;
}
} else if (type != PAGE && type != OFF) {
throw new IllegalArgumentException(WRONG_SCROLL_TYPE);
}
this.autoScrollType = type;
this.prepareAutoScroll();
},
//////////////////////////////////TEMPLATE SETUP
/**
* @protected
* @inheritdoc
*/
parseHtml: function() {
this.parsed = true;
var ancestorNode = this.clonedNode;
if (ancestorNode) {
if (Cell.isAttachedToDOM(ancestorNode)) {
//template already saved and still there
return true;
} else {
//the template disappeared, reset and read again
this.initDOM();
}
}
ancestorNode = document.getElementById(this.id);
//verifies the template
if (!this.templateControl(ancestorNode)) {
return false;
}
//clone the template
this.template = ancestorNode.cloneNode(true);
this.template.removeAttribute("id");
//save the original template for future use (var name is misleading)
this.clonedNode = ancestorNode;
//save its parent too, will be used to append clones
var ancestorParent = ancestorNode.parentNode;
this.nodeContainer = ancestorParent;
//let's hide the original template
ancestorNode.style.display = "none";
//search the position of the template in its parent
var ancestorBrothers = ancestorParent.childNodes;
var i = 0;
var offsetIndex = 0;
var refererNode = null;
for (i = 0; i < ancestorBrothers.length; i++) {
// Both in the upwardScroll case and the reverse
// the brother to be signed is the same, that is
// the brother immediately following the ancestor
// so the first inserted "will replace" the ancestor
// (it is inserted between the ancestor and that brother, given the invisibility
// dell'ancestor, it seems a substitution)
if (ancestorBrothers[i] == ancestorNode) {
if (ancestorBrothers[i+1]) {
refererNode = ancestorBrothers[i+1];
}
offsetIndex = i+1;
break;
}
}
this.clonedContainer = new VisibleParent(ancestorParent, refererNode, offsetIndex);
this.nextParent = new InvisibleParent();
this.prevParent = new InvisibleParent();
return true;
},
/**
* @protected
* @ignore
*/
computeFieldSymbolsSet: function(type) {
var template = this.clonedNode;
var lsTags = Cell.getLSTags(template,this.tagsToCheck);
var fieldSymbolsSet = {};
for (var j = 0; j < lsTags.length; j++) {
if (lsTags[j].getFieldType() == type) {
var fieldSymbol = lsTags[j].getField();
if (fieldSymbol) {
fieldSymbolsSet[fieldSymbol] = true;
}
}
}
return fieldSymbolsSet;
},
/**
* @private
*/
templateControl: function(template) {
//do a control on the template and its contained cells
if (!template) {
//no template no party!
throw new IllegalArgumentException(NO_TEMPLATE);
}
if (!Cell.verifyTag(template)) {
throw new IllegalArgumentException(NO_SOURCE);
}
var validLSTags = [];
var lsTags = Cell.getLSTags(template,this.tagsToCheck);
for (var pi = 0; pi < lsTags.length; pi++) {
if (lsTags[pi].getField()) {
validLSTags.push(lsTags[pi]);
}
}
if (validLSTags.length <= 0) {
throw new IllegalArgumentException(NO_CELLS);
}
return true;
},
/**
* @private
*/
prepareAutoScroll: function() {
// autoscroll management: I take the node to scroll if it is autoscroll on an html element
if (this.autoScrollType == ELEMENT) {
if (this.autoScrollElement && this.autoScrollElement.appendChild) ; else {
var scrollElement = document.getElementById(this.autoScrollElement);
if (!scrollElement) {
gridsLogger.logError(LoggerManager.resolve(121),this);
this.autoScrollType = OFF;
} else {
this.autoScrollElement = scrollElement;
}
}
}
},
////////////////////////////////////////////////////VISUAL CONTROL
/**
* @private
*/
initDOM: function() {
this.clonedNode = null; //original template
this.template = null; //template clone
this.nodeContainer = null; //the template parent
this.clonedContainer = null; //Visibleparent
this.nextParent = null; //InvisibleParent (required for paging)
this.prevParent = null; //InvisibleParent (required for paging)
this.clonesArray = {};
},
/**
* @private
*/
getTemplateClone: function() {
return this.template.cloneNode(true);
},
/**
* @ignore
*/
onNewCell: function(cell,row,field) {
this.grid.addCell(cell,row,field);
},
/**
* @inheritdoc
*/
clean: function() {
//why this useless implementation here? Is it because of closure? TODO verify
this._callSuperMethod(DynaGrid,"clean");
},
/**
* @private
*/
willAutoScroll: function() {
if (this.autoScrollType == OFF) {
return false;
}
if (!this.updateIsKey()) {
//always scroll
return true;
} else {
var scrollEl = this.autoScrollType == ELEMENT ? this.autoScrollElement : document.body;
if (this.addOnTop) {
return scrollEl.scrollTop == 0;
}
//we only scroll if the scrollbar is on the downmost position --> (scrollEl.clientHeight + scrollEl.scrollTop) == scrollEl.scrollHeight
if (BrowserDetection.isProbablyOldOpera()) {
//Opera handling of clientHeight, scrollTop and scrollHeight
//is a little bit different:
//When the scrollbar is on the bottom clientHeight + scrollTop is
//equal to scrollHeight on other browsers while on opera it might
//be a little bigger.
return true;
}
return Math.abs(scrollEl.clientHeight + scrollEl.scrollTop - scrollEl.scrollHeight) <= 1; //rogue pixel
}
},
/**
* @private
*/
getNewScrollPosition: function(el) {
var scrollEl = this.autoScrollType == PAGE ? document.body : this.autoScrollElement;
if (this.updateIsKey()) {
return this.addOnTop ? 0 : scrollEl.scrollHeight-scrollEl.clientHeight;
} else {
return el.offsetTop - scrollEl.offsetTop;
}
},
/**
* @private
*/
doAutoScroll: function(scrollTo) {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(122),this,scrollTo);
}
if (this.autoScrollType == PAGE) {
window.scrollTo(0,scrollTo);
} else {
this.autoScrollElement.scrollTop = scrollTo;
}
},
/**
* @protected
* @ignore
*/
sortTable: function() {
var mySortCode = this.sortField;
//temporary object, will contain all the nodes
var tempTable = new InvisibleParent();
var x = 1;
while (this.rowCount > 0) {
var first = this.getNodeByIndex(x);
if (!first) {
this.rowCount--;
x++;
continue;
}
//if sortField is not specified it will only reorder elements on the pages
if (mySortCode == null) {
tempTable.appendChild(first,true);
this.rowCount--;
continue;
}
var firstKey = first.getKey();
if (firstKey == "") {
this.rowCount--;
x++;
continue;
}
var sortKey = this.makeSortValue(this.values.get(firstKey,this.sortField));
//I should exploit the calculateSortedPosition method
var up = 0;
var down = tempTable.length - 1;
while (up < down) {
var j = Math.floor((up + down) / 2);
var compare = tempTable.getChild(j);
var k = this.makeSortValue(this.values.get(compare.getKey(),this.sortField));
if (!k) {
gridsLogger.logWarn(LoggerManager.resolve(123),this);
//can happen if a non existent field is specified
}
if (this.isBefore(sortKey,k)) {
down = j - 1;
} else {
up = j + 1;
}
}
var compare = tempTable.getChild(up);
if (up == down) {
var compareKey = this.makeSortValue(this.values.get(compare.getKey(),this.sortField));
if (this.isBefore(sortKey,compareKey)) {
tempTable.insertBefore(first,compare);
} else {
var afterDown = tempTable.getChild(down+1);
if (!afterDown) {
tempTable.appendChild(first,true);
} else {
tempTable.insertBefore(first,afterDown);
}
}
} else {
if (compare) {
tempTable.insertBefore(first,compare);
} else {
tempTable.appendChild(first,true);
}
}
this.rowCount--;
}
//everythin is in the temporary object, now distribute'em
var z = 0;
while (z < tempTable.length) {
this.rowCount++;
var el = tempTable.getChild(z);
if (this.rowCount <= (this.maxRow * (this.pageNumber - 1))) {
this.prevParent.appendChild(el,true);
} else if ((this.maxRow <= 0)||(this.rowCount <= (this.maxRow * this.pageNumber))) {
this.clonedContainer.appendChild(el,true);
} else {
this.nextParent.appendChild(el,true);
}
}
},
/**
* @protected
* @ignore
*/
changePage: function(toPage) {
if(this.rowCount <= 0) {
return;
}
if (this.pageNumber >= toPage) {
while (this.shiftDown(this.prevParent,this.clonedContainer,(toPage - 1) * this.maxRow)) {
this.shiftDown(this.clonedContainer,this.nextParent,this.maxRow);
}
} else {
while (this.fitFreeSpace(this.clonedContainer,this.prevParent,(toPage - 1) * this.maxRow,false)) {
this.fitFreeSpace(this.nextParent,this.clonedContainer,this.maxRow,false);
}
}
this.pageNumber = toPage;
},
/**
* @protected
* @ignore
*/
calculatePages: function() {
if (gridsLogger.isDebugLogEnabled()) {
gridsLogger.logDebug(LoggerManager.resolve(124),this);
}
var nowPages = 0;
if (this.maxRow <= 0) {
nowPages = 1;
} else {
nowPages = Math.ceil(this.rowCount / this.maxRow);
}
if (this.currentPages != nowPages) {
this.currentPages = nowPages;
this.dispatchEvent("onCurrentPagesChanged",[this.currentPages]);
}
return nowPages;
},
/////////////////////////////////////VISUAL EXECUTION
/**
* @protected
* @ignore
*/
removeRowExecution: function(key) {
var toRemove = this.clonesArray[key];
if (!toRemove) {
return;
}
this.rowCount--;
this.calculatePages();
//onTop is only possible if sort is disabled and grid is configured that way.
var onTop = false;
var upParent = this.prevParent;
var downParent = this.nextParent;
this.dispatchEvent("onVisualUpdate" ,[key,null,toRemove.element()]);
if(this.updateIsKey() && this.addOnTop && this.sortField == null) {
//only possible if updateIsKey
onTop = this.addOnTop;
upParent = this.nextParent;
downParent = this.prevParent;
}
if (toRemove.isSonOf(this.clonedContainer)) { //this.clonedContainer is a VisibleParent
//remove from visible nodes
this.clonedContainer.removeChild(toRemove);
//if necessary scroll up
this.fitFreeSpace(downParent, this.clonedContainer, this.maxRow, onTop);
} else if (toRemove.isSonOf(downParent)) {
//remove from non-visible nodes
downParent.removeChild(toRemove);
} else {
this.prevParent.removeChild(toRemove);
//if necessary scroll up (the visibile nodes)
if (this.fitFreeSpace(this.clonedContainer, upParent, this.maxRow * (this.pageNumber - 1), onTop)) {
//if necessary scroll up
this.fitFreeSpace(downParent, this.clonedContainer, this.maxRow, onTop);
}
}
this.grid.delRow(key);
delete(this.clonesArray[key]);
},
/**
* @protected
* @ignore
*/
updateRowExecution: function(key,serverValues) {
var calculatePagesFlag = false;
var toUpdate = this.clonesArray[key];
if (!toUpdate) {
//this is an add
toUpdate = new DynaElement(key, this);
this.clonesArray[key] = toUpdate;
toUpdate.element();
}
//>>excludeStart("debugExclude", pragmas.debugExclude);
ASSERT.verifyOk(toUpdate);
//>>excludeEnd("debugExclude");
this.fillFormattedValues(key,serverValues);
var upInfo = this.dispatchVisualUpdateEvent(key,serverValues,toUpdate);
var performScroll = this.willAutoScroll();
var isNew = !this.values.getRow(key);
var oldSortVal = this.sortField != null ? this.makeSortValue(this.values.get(key,this.sortField)) : null;
var newSortVal = this.sortField != null ? this.makeSortValue(serverValues[this.sortField]) : null;
var dontMove = oldSortVal == newSortVal || (!serverValues[this.sortField] && serverValues[this.sortField] !== null);
if (this.sortField != null && dontMove == false) {
var insInd = this.calculateSortedPosition(toUpdate,oldSortVal,newSortVal);
this.insertNodeAtIndex(insInd,toUpdate);
if (isNew) {
this.rowCount++;
calculatePagesFlag = true;
}
} else if (isNew) {
//No sort and new row:
//When upward scroll is not active, new updates are placed at the top of the model data table, so that old updates scroll downward.
//On the other hand, when upward scroll is active, new updates are placed at the bottom of the model data table.
this.appendNode(toUpdate,!this.addOnTop);
this.rowCount++;
calculatePagesFlag = true;
}
//place new values in the cells
this.visualUpdateExecution(key,upInfo);
this.updateInProgress = null;
//if too many rows and update_is_key remove the oldest one
if (isNew && this.updateIsKey()) {
this.limitRows();
}
if (performScroll && toUpdate.isSonOf(this.clonedContainer)) {
var newScrollPos = this.getNewScrollPosition(toUpdate.element());
this.doAutoScroll(newScrollPos);
}
if (calculatePagesFlag) {
this.calculatePages();
}
},
/**
* @private
*/
dispatchVisualUpdateEvent: function(key,serverValues,toUpdate) {
this.currentUpdateKey = key;
this.currentUpdateValues = serverValues;
//this references are used to handle reentrant calls; if an
//updateRow call is performed during the VisualUpdate it is
//merged in this update instead of spawning a new update.
var upInfo = new VisualUpdate(this.grid,serverValues,key);
this.dispatchEvent("onVisualUpdate",[key,upInfo,toUpdate.element()]);
this.currentUpdateKey = null;
this.currentUpdateValues = null;
return upInfo;
},
/**
* @private
*/
calculateSortedPosition: function(toUpdate,oldSortVal,newSortVal) {
var up = 1;
var down = this.rowCount;
var j = -1;
// search an equal key or the case in which up exceeds down
while (up < down) {
j = Math.floor((up + down) /2);
var thisKey = null;
if (j <= this.rowCount) {
var compare = this.getNodeByIndex(j);
if (compare == toUpdate) {
thisKey = oldSortVal;
} else {
thisKey = this.makeSortValue(this.values.get(compare.getKey(),this.sortField));
}
}
if (this.isBefore(newSortVal,thisKey)) {
down = j - 1;
} else {
up = j + 1;
}
}
if (up == down) {
// up and down coincide, our place is immediately before or immediately after this point
var compare = this.getNodeByIndex(up);
var compareKey = this.makeSortValue(this.values.get(compare.getKey(),this.sortField));
if (this.isBefore(newSortVal,compareKey)) {
return up;
} else {
return up + 1;
}
} else {
return up;
}
},
/**
* @protected
* @ignore
*/
getNodeByIndex: function(i) {
if (i > this.rowCount || i <= 0) {
return null;
}
if (i <= this.prevParent.length) {
return this.prevParent.getChild(i-1);
} else {
i -= this.prevParent.length;
if (i <= this.clonedContainer.length) {
return this.clonedContainer.getChild(i-1);
} else {
i -= this.clonedContainer.length;
return this.nextParent.getChild(i-1);
}
}
},
/**
* @protected
* @ignore
*/
appendNode: function(toUpdate,onBottom) {
var upParent = onBottom ? this.prevParent : this.nextParent;
var downParent = onBottom ? this.nextParent : this.prevParent;
if (downParent.length > 0 || (this.clonedContainer.length == this.maxRow && this.maxRow > 0)) {
//there is no room in the central "window", so append in the downParent
//having something in the downParent implies that the central window is full
downParent.appendChild(toUpdate,onBottom);
return downParent;
} else if (this.clonedContainer.length > 0 || upParent.length == (this.maxRow * (this.pageNumber - 1))) { //as a consequence we pass from here if this.maxRow == 0 (upParent.length would be 0 and 0*x = 0)
//central window is not empty (if empty the updates may have to be to be placed on the upParent)
//or all the existing elements are in the upParent (and all the pages in the upParent are full)
this.clonedContainer.appendChild(toUpdate,onBottom);
return this.clonedContainer;
} else {
upParent.appendChild(toUpdate,onBottom);
return upParent;
}
},
/**
* @protected
* @ignore
*/
insertNodeAtIndex: function(i, node) {
if (i > this.rowCount + 1 || i <= 0) {
return;
}
if (node == this.getNodeByIndex(i)) {
return;
}
var firstP = node.getParentNode();
var afterP;
var parent = this.clonedContainer;
var nextParent = this.nextParent;
var prevParent = this.prevParent;
var insBef = this.getNodeByIndex(i);
if (insBef == null) {
afterP = this.appendNode(node,true);
} else {
afterP = insBef.getParentNode();
afterP.insertBefore(node,insBef);
}
//let's handle overflows
if (afterP == parent) {
if ((!firstP) || (firstP == nextParent)) {
this.shiftDown(parent, nextParent, this.maxRow);
} else if (firstP == prevParent) {
this.fitFreeSpace(parent, prevParent, this.maxRow * (this.pageNumber - 1), false);
}
} else if (afterP == prevParent) {
if (firstP != prevParent) {
if (this.shiftDown(prevParent, parent, this.maxRow * (this.pageNumber - 1))) {
this.shiftDown(parent, nextParent, this.maxRow);
}
}
} else if (afterP == nextParent) {
if (firstP == prevParent) {
this.fitFreeSpace(parent, prevParent, this.maxRow * (this.pageNumber - 1), false);
}
this.fitFreeSpace(nextParent, parent, this.maxRow, false);
}
},
/**
* @private
*/
fitFreeSpace: function(fromNode, toNode, maxForTo, onTop) {
//if a place was freed in a parent we grab a row from the following parent and move it
//to the freed one
if (this.maxRow <= 0) {
//infinite visible places, only one parent has rows here.
return false;
}
if (toNode.length < maxForTo && fromNode.length > 0) {
var toMoveEl = fromNode.getChild(0);
toNode.appendChild(toMoveEl,!onTop);
return true;
}
return false;
},
/**
* @private
*/
shiftDown: function(fromNode, toNode, maxForFrom) {
if (this.maxRow <= 0) {
//infinite visible places
return false;
}
if (fromNode.length > maxForFrom) {
var toMoveEl = fromNode.getChild(fromNode.length-1);
toNode.insertBefore(toMoveEl,toNode.getChild(0));
return true;
}
return false;
},
/**
* @private
*/
limitRows: function() {
//only updateiskey limits rows in this way
while(this.maxRow > 0 && this.rowCount > this.maxRow) {
this.removeRow(this.getOldestKey());
}
},
/**
* Adds a listener that will receive events from the DynaGrid
* instance.
*
The same listener can be added to several different DynaGrid
* instances.
*
* Lifecycle: a listener can be added at any time.
*
* @param {DynaGridListener} listener An object that will receive the events
* as shown in the {@link DynaGridListener} interface.
*
Note that the given instance does not have to implement all of the
* methods of the DynaGridListener interface. In fact it may also
* implement none of the interface methods and still be considered a valid
* listener. In the latter case it will obviously receive no events.
*/
addListener: function(listener) {
this._callSuperMethod(DynaGrid,"addListener",[listener]);
},
/**
* Removes a listener from the DynaGrid instance so that it
* will not receive events anymore.
*
* Lifecycle: a listener can be removed at any time.
*
* @param {DynaGridListener} listener The listener to be removed.
*/
removeListener: function(listener) {
this._callSuperMethod(DynaGrid,"removeListener",[listener]);
},
/**
* Returns an array containing the {@link DynaGridListener} instances that
* were added to this client.
*
* @return {DynaGridListener[]} an array containing the listeners that were added to this instance.
* Listeners added multiple times are included multiple times in the array.
*/
getListeners: function() {
return this._callSuperMethod(DynaGrid,"getListeners");
}
};
//closure compiler exports
DynaGrid.prototype["setMaxDynaRows"] = DynaGrid.prototype.setMaxDynaRows;
DynaGrid.prototype["getMaxDynaRows"] = DynaGrid.prototype.getMaxDynaRows;
DynaGrid.prototype["goToPage"] = DynaGrid.prototype.goToPage;
DynaGrid.prototype["getCurrentPages"] = DynaGrid.prototype.getCurrentPages;
DynaGrid.prototype["setAutoScroll"] = DynaGrid.prototype.setAutoScroll;
DynaGrid.prototype["parseHtml"] = DynaGrid.prototype.parseHtml;
DynaGrid.prototype["clean"] = DynaGrid.prototype.clean;
DynaGrid.prototype["addListener"] = DynaGrid.prototype.addListener;
DynaGrid.prototype["removeListener"] = DynaGrid.prototype.removeListener;
DynaGrid.prototype["getListeners"] = DynaGrid.prototype.getListeners;
DynaGrid.prototype["updateRowExecution"] = DynaGrid.prototype.updateRowExecution;
DynaGrid.prototype["removeRowExecution"] = DynaGrid.prototype.removeRowExecution;
//<---- Listener Interface
Inheritance(DynaGrid,AbstractGrid);
return DynaGrid;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var SimpleChartListener = /*@__PURE__*/(function() {
/**
* Creates an object that will handle the positioning of the X axis and Y axis of
* a {@link Chart} based on a few simple configuration parameters.
*
Note that this listener expects to listen to only one Chart but can
* correctly handle different lines as long as they can be represented on the
* same scale.
*
Methods from the {@link ChartListener} interface should not be called
* directly on instances of this class.
* @constructor
*
* @param {Number} [xSpan=60] The size of the X axis. The units of the value depends on
* the model of the {@link Chart} instance and possibly on the parser configured on
* the {@link Chart#setXAxis} method. If not specified, then 60 will be used.
* @param {Number} [yPerc=20] A percentage that is used for the first positioning of the
* Y axis: the Y axis will have as initial maximum position a value that is yPerc%
* greater than the first Y position and as initial minimum position a value that
* is yPerc% smaller than it. If not specified, then 20 (meaning 20%) will be used.
*
* @exports SimpleChartListener
* @class Simple implementation of the {@link ChartListener} interface that can
* be used to automatically adjust the axis of a {@link Chart} to center the lines on
* the Chart.
*
In case of an X overflow the X axis limits will shift by half
* of their total length meaning that the next point falls in the middle of the
* visible area so that the shifting is not continuous for each new value
* of X.
*
In case of an Y overflow the Y axis limits are stretched
* in such a way that the new point falls on the visible part of the Y
* axis.
*
Note that in case of an Y overflow all of the ChartLine instances of the
* listened Chart will be stretched with the same values to keep the view consistent.
*
* @extends ChartListener
*/
var SimpleChartListener = function(xSpan,yPerc) {
this.xSpan = xSpan || 60;
yPerc = (yPerc || 20)/100;
this.moreY = 1+yPerc;
this.lessY = 1-yPerc;
this.handledLines = new List();
this.minY;
this.maxY;
};
SimpleChartListener.prototype = {
/**
* @inheritdoc
*/
onListenStart: function(chartTable) {
this.chartTable = chartTable;
},
/**
* @inheritdoc
*/
onYOverflow: function(key,toUpdateChartLine,lastY,minY,maxY) {
var shift = (maxY - minY)/2;
if (lastY > maxY) {
var newMax = maxY + shift;
if (lastY > newMax) {
newMax = lastY;
}
this.maxY = newMax;
this.updateYAxis(minY,newMax);
} else if (lastY < minY) {
var newMin = minY - shift;
if (lastY < newMin) {
newMin = lastY;
}
this.minY = newMin;
this.updateYAxis(newMin,maxY);
}
},
/**
* @inheritdoc
*/
onXOverflow: function(key, lastX, minX, maxX) {
if (lastX > maxX) {
var xMid = (maxX + minX) /2;
var diff = maxX - minX;
this.chartTable.positionXAxis(xMid,xMid + diff);
} //else {
//overflow point from the other side are discarded
},
/**
* @inheritdoc
*/
onNewLine: function(key,newChartLine,nowX,nowY) {
this.chartTable.positionXAxis(nowX,nowX+this.xSpan);
var localMin = nowY*this.lessY;
var localMax = nowY*this.moreY;
this.handledLines.add(newChartLine);
this.minY = this.minY !== null && this.minY <= localMin ? this.minY : localMin;
this.maxY = this.maxY !== null && this.maxY >= localMax ? this.maxY : localMax;
this.updateYAxis(this.minY,this.maxY);
},
/**
* @inheritdoc
*/
onRemovedLine: function(key,removedChartLine) {
this.handledLines.remove(removedChartLine);
},
/**
* @private
*/
updateYAxis: function(newMin,newMax) {
this.handledLines.forEach(function(line) {
line.positionYAxis(newMin,newMax);
});
}
};
//closure compiler exports
SimpleChartListener.prototype["onListenStart"] = SimpleChartListener.prototype.onListenStart;
SimpleChartListener.prototype["onYOverflow"] = SimpleChartListener.prototype.onYOverflow;
SimpleChartListener.prototype["onXOverflow"] = SimpleChartListener.prototype.onXOverflow;
SimpleChartListener.prototype["onNewLine"] = SimpleChartListener.prototype.onNewLine;
SimpleChartListener.prototype["onRemovedLine"] = SimpleChartListener.prototype.onRemovedLine;
return SimpleChartListener;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var SlidingCell = /*@__PURE__*/(function() {
var nextId = 0;
/**
* This class makes it possible to have a Cell interface bound to a key instead of being bound to a position.
* So if the logic cell moves across different rows this class is always able to call methods on it.
* This is not a complete proxy though
* @private
*/
var SlidingCell = function(ownerGrid, key, field, num, noNumIndex) {
this.ownerGrid = ownerGrid;
this.key = key;
this.field = field;
this.num = num || null;
this.noNumIndex = noNumIndex;
this.cellId = "s"+nextId++;
};
SlidingCell.prototype = {
/*private*/ getCurrentCell: function() {
var cell = this.ownerGrid.getCellByKey(this.key,this.field,this.num);
if(!cell) {
return null;
}
if (cell.isCell) {
if (this.num === cell.getNum() && this.noNumIndex <= 0) {
return cell;
}
} else {
var nowI = -1;
for (var i = 0; i < cell.length; i++) {
var parsingCellNum = cell[i].getNum();
if (parsingCellNum === null) {
nowI++;
}
if (this.num === parsingCellNum && this.noNumIndex == nowI) {
return cell[i];
}
}
}
return null;
},
/*public*/ incFadePhase: function() {
var c = this.getCurrentCell();
return c ? c.incFadePhase() : null;
},
/*public*/ getCellId: function() {
return this.cellId;
},
/*public*/ getFadePhase: function() {
var c = this.getCurrentCell();
return c ? c.getFadePhase() : null;
},
/*public*/ getEl: function() {
var c = this.getCurrentCell();
return c ? c.getEl() : null;
},
/*public*/ asynchUpdateStyles: function(phaseNum,type) {
var c = this.getCurrentCell();
if (c) {
c.asynchUpdateStyles(phaseNum,type);
}
},
/*public*/ asynchUpdateValue: function(phaseNum,useInner) {
var c = this.getCurrentCell();
if (c) {
c.asynchUpdateValue(phaseNum,useInner);
}
}
};
return SlidingCell;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var DoubleKeyMap = /*@__PURE__*/(function() {
function checkValidValue(v) {
return v !== null && typeof v != "undefined";
}
function doRemove(master,slave,key) {
var val = master[key];
if (checkValidValue(val)) {
delete(master[key]);
delete(slave[val]);
}
}
/*
* how does this work
*
* A 1 | 1 A
* B 2 | 2 B
* C 3 | 3 C
*
*
* CASE 1, one key overwriting
*
* set(a,7)
*
* --> just write
* A 7 | 2 B
* B 2 | 3 C
* C 3 | 7 A
*
* CASE 2, double key overwriting
*
* set(b,3)
*
* --> swap B and C values (can also be seen as swap 2 and 3 values)
* A 7 | 2 C
* B 3 | 3 B
* C 2 | 7 A
*
*/
function doSet(master,slave,a,b) {
if (!checkValidValue(a) || !checkValidValue(b)) {
throw new IllegalArgumentException("values can't be null nor missing");
}
var origB = master[a];
var origA = slave[b];
if (checkValidValue(origB)) {
if (origB === b) {
//value already set
return;
}
if(checkValidValue(origA)) {
//value already indexed in both master and slave, swap values
//doSwap
master[origA] = origB;
master[a] = b;
slave[b] = a;
slave[origB] = origA;
} else {
//value already indexed in the master but not in the slave, replace in master and remove + add in slave
doReplace(master,slave,a,b);
}
} else if(checkValidValue(origA)) {
//value already indexed in the slave but not in the master, replace in slave and remove + add in master
doReplace(slave,master,b,a);
} else {
//doAdd
master[a] = b;
slave[b] = a;
}
}
function doReplace(master,slave,key,val) {
delete(slave[master[key]]);
master[key] = val;
slave[val] = key;
}
function doForEach(coll,cb) {
for (var key in coll) {
cb(key,coll[key]);
}
}
/**
* Crates and empty DoubleKeyMap
* @constructor
*
* @exports DoubleKeyMap
* @class Simple map implementation that also keeps a reverse map whose keys are
* the values and values are the keys of the first map.
* For this reason the map can't contain duplicated values: collisions are
* handled by swapping values.
*/
var DoubleKeyMap = function() {
/**
* @private
*/
this.map = {};
/**
* @private
*/
this.reverseMap = {};
};
DoubleKeyMap.prototype = {
/**
* Inserts new values in the maps. If one of the keys is already present in the respective map,
* its value is overwritten and the related entry on the other map is deleted. If both the keys
* are already present in their respective map a swapping to maintain uniqueness in both maps is performed.
*
* @throws {IllegalArgumentException} if one (or both) of the specified values is null or missing.
*
* @param a {Object} the key to be used to insert the other value in the first map. It can't be null nor missing.
* @param b {Object} the key to be used to insert the other value in the second map. It can't be null nor missing.
*/
set: function(a,b) {
doSet(this.map,this.reverseMap,a,b);
},
/**
* Uses the given key to remove an element from the first map and the related element in the second map.
* @param a {Object} the key to be used to remove the element from the first map.
*/
remove: function(a) {
doRemove(this.map,this.reverseMap,a);
},
/**
* Uses the given key to remove an element from the second map and the related element in the first map.
* @param b {Object} the key to be used to remove the element from the second map.
*/
removeReverse: function(b) {
doRemove(this.reverseMap,this.map,b);
},
/**
* Gets an element from the first map using the given key.
* @param a {Object} the key to be used to get the element from the first map.
* @returns {Object} the found value if any or an undefined value
*/
get: function(a) {
return this.map[a];
},
/**
* Gets an element from the second map using the given key.
* @param b {Object} the key to be used to get the element from the second map.
* @returns {Object} the found value if any or an undefined value
*/
getReverse: function(b) {
return this.reverseMap[b];
},
/**
* Checks if there is a value in the first map at the specified key.
* @param a {Object} the key to be used to get the element from the first map.
* @returns {Boolean} true if the element exists, false otherwise.
*/
exist: function(a) {
return typeof this.get(a) != "undefined";
},
/**
* Checks if there is a value in the second map at the specified key.
* @param b {Object} the key to be used to get the element from the second map.
* @returns {Boolean} true if the element exists, false otherwise.
*/
existReverse: function(b) {
return typeof this.getReverse(b) != "undefined";
},
/**
* Executes a given callback passing each element of the first map as the only
* call parameter.
* Callbacks are executed synchronously before the method returns: calling
* {@link DoubleKeyMap#set}, {@link DoubleKeyMap#remove} or {@link DoubleKeyMap#removeReverse}
* during callback execution may result in a wrong iteration.
*
* @param {Function} callback The callback to be called.
*/
forEach: function(callback) {
doForEach(this.map,callback);
},
/**
* Executes a given callback passing each element of the second map as the only
* call parameter.
* Callbacks are executed synchronously before the method returns: calling
* {@link DoubleKeyMap#set}, {@link DoubleKeyMap#remove} or {@link DoubleKeyMap#removeReverse}
* during callback execution may result in
* a wrong iteration.
*
* @param {Function} callback The callback to be called.
*/
forEachReverse: function(callback) {
doForEach(this.reverseMap,callback);
}
};
DoubleKeyMap.prototype["set"] = DoubleKeyMap.prototype.set;
DoubleKeyMap.prototype["remove"] = DoubleKeyMap.prototype.remove;
DoubleKeyMap.prototype["removeReverse"] = DoubleKeyMap.prototype.removeReverse;
DoubleKeyMap.prototype["get"] = DoubleKeyMap.prototype.get;
DoubleKeyMap.prototype["getReverse"] = DoubleKeyMap.prototype.getReverse;
DoubleKeyMap.prototype["exist"] = DoubleKeyMap.prototype.exist;
DoubleKeyMap.prototype["existReverse"] = DoubleKeyMap.prototype.existReverse;
DoubleKeyMap.prototype["forEach"] = DoubleKeyMap.prototype.forEach;
DoubleKeyMap.prototype["forEachReverse"] = DoubleKeyMap.prototype.forEachReverse;
return DoubleKeyMap;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var StaticGrid = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
var NULL_CELL = "The given cell is null or undefined";
var WRONG_GRID = "The cell does not belong to the Grid";
var INVALID_ROOT = "The given root element is not valid";
var NO_ITEMS_TO_EXTRACT = "CanÂ’t extract schema from cells declared with the data-row property; use data-item instead.";
var MIX_ROW_ITEMS = "CanÂ’t mix data-item and data-row declarations on the same grid";
var NO_CELLS = "Please specify at least one cell";
function swap(arr,a,b) {
var tmp = arr[a];
arr[a] = arr[b];
arr[b] = tmp;
}
var gridsLogger = LoggerManager.getLoggerProxy("lightstreamer.grids");
/**
* Creates an object that extends {@link AbstractGrid} displaying its values
* in a grid made of HTML elements. The grid rows are displayed into statically
* prepared HTML rows. The object can be supplied to
* {@link Subscription#addListener} and {@link Subscription#removeListener}
* in order to display data from one or more Subscriptions.
* @constructor
*
* @param {String} id An identification string to be specified in the HTML element
* as the data "data-grid" property
* value to make it possible for this StaticGrid instance to recognize its cells.
* The binding between the cells and the StaticGrid is performed during the
* {@link AbstractGrid#parseHtml} execution.
*
* @param {boolean} autoParse If true the {@link AbstractGrid#parseHtml} method is executed
* before the constructor execution is completed. If false the parseHtml method
* has to be called later by custom code. It can be useful to set this flag
* to false if, at the time of the StaticGrid instance creation, the HTML elements
* designated as cells are not yet ready on the page.
*
* @param {Object} rootEl if specified, the cells to make up the HTML grid will
* only be searched in the list of descendants of this node. Equivalent to a
* {@link StaticGrid#setRootNode} call, but useful if autoParse is true.
*
* @param {Object[]} cells an array of DOMElement instances that will make up the
* HTML grid for this StaticGrid instance. If specified and not empty, the parseHtml
* method will avoid searching cells in the DOM of the page. Equivalent to multiple
* {@link StaticGrid#addCell} calls, but also useful if autoParse is true.
*
* @exports StaticGrid
* @class An {@link AbstractGrid} implementation that can be used to display
* the values from the internal model in a statically prepared grid.
* The HTML structure suitable for the visualization of the tabular model values
* must be prepared up-front in the DOM of the page as a matrix of HTML cells.
* The cells making up the grid can be any HTML element
* owning the "data-source='Lightstreamer'" special attribute. Such cells, to be
* properly bound to the StatiGrid instance must also define the following
* custom attributes:
*
* - "data-grid": an identification string that has to be equal to the id
* that is specified in the constructor of this class. This id is used
* to properly bind a cell to its StaticGrid instance.
* - "data-field": the name of a field in the internal model whose value will
* be displayed in this cell.
* - "data-fieldtype" (optional): "extra", "first-level" or "second-level" to
* specify the type of field. If not specified "first-level" is assumed.
* The "data-fieldtype" property is only exploited in the {@link AbstractGrid#extractFieldList}
* and {@link AbstractGrid#extractCommandSecondLevelFieldList} methods.
* - "data-row" (only if "data-item" is not specified): a progressive number
* representing the number of the row associated with the cell.
* When a new row enters the grid its position will be decided by the
* {@link AbstractGrid#setAddOnTop} and {@link AbstractGrid#setSort} settings.
* The "data-row" attribute will define to which row a cell pertains.
*
Note that the maximum value available for a data-row attribute in all
* the cells pertaining to this StaticGrid will define the size of the view.
* If the number of rows in the model exceeds the number of rows defined in the
* HTML grid, rows that would have been displayed in the extra rows are not shown
* in the view but are maintained in the model.
*
Note that if this instance is used to listen to events from
* {@link Subscription} instance(s), and the first Subscription it listens to is
* a DISTINCT Subscription, then the behavior is different: when the number of rows
* in the model exceeds the number of rows defined in the HTML grid, adding a new
* row will always cause the removal of the oldest row from the model, with a
* consequent repositioning of the remaining rows.
*
* - "data-item" (only if "data-row" is not specified): the name of a row in
* the internal model, whose value (for the chosen field) will be displayed in
* this cell; this attribute should
* only be used if this instance is used to listen to events from
* {@link Subscription} instance using the MERGE mode; so, this attribute should
* identify the name or the 1-based position of an item in the MERGE Subscription.
* This way it is possible to define a static positioning for each item in
* the MERGE Subscription. On the contrary, by using "data-row" attributes, each
* item will be placed based only on the {@link AbstractGrid#setAddOnTop} and
* {@link AbstractGrid#setSort} settings and the positioning may depend on the
* arrival order of the updates.
*
* - "data-replica" (optional): this attribute can be specified in case there
* are more cells associated to the same field. If used, it will permit to access
* the single cells during {@link StaticGridListener#onVisualUpdate} executions.
*
*
*
The association between the StaticGrid and the HTML cells is made during the
* execution of the {@link AbstractGrid#parseHtml} method. Note that only the elements
* specified in the {@link AbstractGrid#setNodeTypes} and that are descendants of the node
* specified in the {@link StaticGrid#setRootNode} are taken into account, unless a list
* of cells has been manually specified in the constructor or through the {@link StaticGrid#addCell}
* method, in which case no elements other than the given ones are taken into
* account.
*
Cells already associated to the grid can be removed from the page DOM,
* hence from the grid, at any time. Cells already associated can also be moved or
* altered so that they become no longer suitable for association or other HTML
* elements may be made suitable, but, in this case, all changes will affect the
* grid only after the next invocation of {@link AbstractGrid#parseHtml}.
*
Make sure that all the associated cells specify the same attribute among
* "data-row" and "data-item"; the behavior of the grid is left unspecified
* when this condition is not met.
*
*
By default, the content of the HTML element designated as cell will be
* updated with the value from the internal model; in case the cell is an INPUT
* or a TEXTAREA element, the "value" property will be updated instead.
* It is possible to update any attribute of the HTML element or its CSS
* styles by specifying the "data-update" custom attribute.
* To target an attribute the attribute name has to be specified; it can be a
* standard property (e.g. "data-update='href'"), a custom "data-" attribute
* (e.g. "data-update='data-foo'") or even a custom attribute not respecting
* the "data-" standard (e.g. "data-update='foo'").
* To target CSS attributes the "data-update='style.attributeName'" form has to
* be used (e.g. "data-update='style.backgroundColor'"); note that the form
* "data-update='style.background-color'" will not be recognized by some browsers.
*
WARNING: also events like "onclick" can be assigned; in such cases make
* sure that no malicious code may reach the internal model (for example
* through the injection of undesired JavaScript code from the Data Adapter).
*
More visualization actions can be performed through the provided
* {@link VisualUpdate} event objects received on the {@link StaticGridListener}.
*
* @extends AbstractGrid
*/
var StaticGrid = function(id, autoParse, rootEl, cells) {
this._callSuperConstructor(StaticGrid,[id]);
this.explicitSetting = false;
this.rootEl = null;
this.setRootNode(rootEl || document);
this.tableCells = [];
if (cells) {
this.addCell(cells);
}
this.keyRowMap = new DoubleKeyMap();
this.usingItems = null;
autoParse = this.checkBool(autoParse,true);
if (autoParse) {
this.parseHtml();
}
};
StaticGrid.prototype = {
/**
* @ignore
*/
toString: function() {
return ["[",this.id,"]"].join("|");
},
//////////////////////////////////TEMPLATE SETUP
/**
* Operation method that adds an HTML cell pointer to the StaticGrid.
*
Note that if at least one cell is manually specified then the
* {@link AbstractGrid#parseHtml} will not perform any search in the DOM of the page
* and will only use the given cells.
*
* Lifecycle: Cell pointers can be added to a StaticGrid at any time.
* However, before an HTML element is actually used as a cell by the StaticGrid
* a call to {@link AbstractGrid#parseHtml} is necessary.
*
* @param {Object} cellElement A DOM pointer to an HTML node.
* The specified HTML node should be a "legal" cell for the StaticGrid
* (i.e. should be defined according with the requirements for the
* StaticGrid as described in the overview of this class). Moreover,
* nodes of any types are allowed.
*/
addCell: function(cellElement) {
if (!cellElement) {
throw new IllegalArgumentException(NULL_CELL);
}
if (Helpers.isArray(cellElement)) {
for (var i=0; iAnyway note that if nodes are explicitly set through the constructor or through
* the {@link StaticGrid#addCell} method, then the search will not be performed at all.
*
* Default value: the entire document.
*
* Lifecycle: a root node can be specified at any time.
* However, before a new search is performed for the StaticGrid
* a call to {@link AbstractGrid#parseHtml} is necessary.
*
* @param {Object} rootNode a DOM node to be used as starting point
* when searching for grid cells.
*/
setRootNode: function(rootNode) {
if (rootNode && rootNode.getElementsByTagName) {
this.rootEl = rootNode;
} else {
throw new IllegalArgumentException(INVALID_ROOT);
}
},
/**
* Creates an array containing all of the unique values of the "data-item"
* properties in all of the HTML elements associated to this grid during the
* {@link AbstractGrid#parseHtml} execution.
* The result of this method is supposed to be used as "Item List" of a Subscription.
*
*
Execution of this method is pointless if HTML elements associated to this
* grid through "data-item" specify an item position instead of an item name.
*
* @return {String[]} The list of unique values found in the "data-item" properties
* of HTML element of this grid.
*/
extractItemList: function() {
this.checkParsed();
if (this.usingItems === false) {
throw new IllegalStateException(NO_ITEMS_TO_EXTRACT);
}
var itemSymbolsSet = this.computeItemSymbolsSet();
var incrementalGroup = [];
for (var itemSymbol in itemSymbolsSet) {
incrementalGroup.push(itemSymbol);
}
return incrementalGroup;
},
/**
* @inheritdoc
*/
parseHtml: function() {
this.parsed = true;
this.grid.cellsGarbageCollection();
var lsTags;
if (this.explicitSetting) {
lsTags = this.tableCells;
//on the next parseHtml call only newly added cells will be parsed
this.tableCells = [];
} else {
lsTags = Cell.getLSTags(this.rootEl,this.tagsToCheck);
}
for (var j = 0; j < lsTags.length; j++) {
var table = lsTags[j].getTable();
if (!table || table != this.id) {
//this is not the table you're looking for
continue;
}
var row = lsTags[j].getRow();
if (!row) {
continue;
}
if (!isNaN(row)) {
row = Number(row);
}
if (this.usingItems === null) {
this.usingItems = isNaN(row);
} else if (this.usingItems != isNaN(row)) {
throw IllegalStateException(MIX_ROW_ITEMS);
}
if (!this.usingItems) {
this.maxRow = row > this.maxRow ? row : this.maxRow;
}
var fieldSymbol = lsTags[j].getField();
if (!fieldSymbol) {
continue;
}
if (this.grid.alreadyInList(lsTags[j])) {
continue;
}
this.grid.addCell(lsTags[j]);
}
if (!this.grid.isEmpty()) {
//at least one cell, that's ok for us
return;
}
throw new IllegalStateException(NO_CELLS);
},
/**
* @protected
* @ignore
*/
computeFieldSymbolsSet: function(type) {
var fieldSymbolsSet = {};
this.grid.forEachCell(function(singleCell,itemSymbol,fieldSymbol) {
if (singleCell.getFieldType() == type) {
fieldSymbolsSet[fieldSymbol] = true;
}
});
return fieldSymbolsSet;
},
/**
* @protected
* @ignore
*/
computeItemSymbolsSet: function() {
var itemSymbolsSet = {};
this.grid.forEachRow(function(itemSymbol){
itemSymbolsSet[itemSymbol] = true;
});
return itemSymbolsSet;
},
/////////////////////////////////////VISUAL EXECUTION
/**
* @protected
* @ignore
*/
updateRowExecution: function(key,serverValues) {
var isNew = !this.values.getRow(key);
//I need to find the position of the updating row
var gridKey;
if (this.usingItems) {
//special case, item instead of row, only works for itemIsKey
//In this case upwardScroll and/or sorting settings are ignored
//as each key has only one possible place to go
gridKey = key;
} else { // if (!this.usingItems) {
//1 choose the new position
var oldSortVal = this.sortField != null ? this.makeSortValue(this.values.get(key,this.sortField)) : null;
var newSortVal = this.sortField != null ? this.makeSortValue(serverValues[this.sortField]) : null;
var dontMove = oldSortVal == newSortVal || (typeof serverValues[this.sortField] == "undefined");
if (this.sortField != null && dontMove == false) {
gridKey = this.calculateSortedPosition(key,oldSortVal,newSortVal);
} else if (isNew) {
//No sort and new row:
//When upward scroll is not active, new updates are placed at the top of the model data table, so that old updates scroll downward.
//On the other hand, when upward scroll is active, new updates are placed at the bottom of the model data table.
if (this.addOnTop) {
gridKey = 1;
} else if (this.updateIsKey()) {
//add on bottom with update is key means place the new row in the last row and scroll upward anything else (the top row will overflow and be deleted)
gridKey = this.rowCount == this.maxRow ? this.rowCount : this.rowCount+1;
} else {
//add on bottom without update is key means place the new row on the bottom even if not visible
gridKey = this.rowCount+1;
}
} else {
//not new and doesn't move, simply update the row
gridKey = this.keyRowMap.get(key);
}
if (this.updateIsKey() && this.maxRow == this.rowCount && isNew && this.sortField != null ) {
//new row (isNew) on a full static grid (this.maxRow == this.rowCount) on a sorted table (this.sortField != null) on a grid where overflow is deleted
//I need to remove the oldest row
var oldest = this.getOldestKey();
var freePos = this.scrollOut(oldest);
if(freePos this.maxRow && !this.grid.getRow(gridKey)) {
//new row out of sight
//let's make new fake cells for it
//updateIsKey can't pass from here: out-of-sight rows are completely deleted
//gridKey > this.maxRow === out of sight
//!this.grid.getRow(toPos) no cells defined for the row
var toCopyRow = this.grid.getRow(gridKey-1);
var fakeRow = CellMatrix.scrollRow(toCopyRow,null,this.useInner);
this.grid.insertRow(fakeRow,gridKey);
}
//3 fill formatter values
this.fillFormattedValues(gridKey,serverValues);
var upInfo = this.dispatchVisualUpdateEvent(key,gridKey,serverValues);
//5 do visualUpdateExecution (ie place values on cells)
this.visualUpdateExecution(gridKey,upInfo,key);
},
/**
* @private
*/
dispatchVisualUpdateEvent: function(key,gridKey,serverValues) {
//this references are used to handle reentrant calls; if an
//updateRow call is performed during the VisualUpdate it is
//merged in this update instead of spawning a new update.
//TODO if the sort value is changed during the second update call
//the row may appear in the wrong place should override
//mergeUpdate to prevent the case
this.currentUpdateKey = gridKey;
this.currentUpdateValues = serverValues;
//call onVisualUpdate callback
var upInfo = new VisualUpdate(this.grid,serverValues,gridKey);
this.dispatchEvent("onVisualUpdate",[key,upInfo,gridKey]);
this.currentUpdateKey = null;
this.currentUpdateValues = null;
return upInfo;
},
/**
* @protected
* @ignore
*/
getSlidingCell: function(cell,updateKey,col,num,noNumIndex) {
if(this.usingItems) {
return cell;
}
return new SlidingCell(this,updateKey,col,num,noNumIndex);
},
/**
* @protected
* @ignore
*/
getCellByKey: function(key,col,num) {
var gridKey = this.keyRowMap.get(key);
return this.grid.getCell(gridKey,col,num);
},
/**
* @protected
* @ignore
*/
removeRowExecution: function(key) {
var gridKey = this.usingItems ? key : this.keyRowMap.get(key);
this.dispatchEvent("onVisualUpdate",[key,null,gridKey]);
if (!this.usingItems) {
if (gridKey != this.rowCount) {
this.makeRoom(this.rowCount,key); //make the row to be deleted the last row of the grid
gridKey = this.keyRowMap.get(key); //reload the gridkey
}
//>>excludeStart("debugExclude", pragmas.debugExclude);
if (!ASSERT.verifyValue(this.rowCount,gridKey)) {
gridsLogger.logError(LoggerManager.resolve(125));
}
//>>excludeEnd("debugExclude");
}
this.grid.forEachCellInRow(gridKey,function(cell) {
cell.clean();
});
this.rowCount--;
if (!this.usingItems) {
this.keyRowMap.remove(key);
}
},
/**
* @private
*/
scrollOut: function(key) {
var cPos = this.keyRowMap.get(key);
this.keyRowMap.remove(key);
this.rowCount--;
this.values.delRow(key);
if (this.updateIsKey()) {
this.removeFromFifo(key);
}
return cPos;
},
/**
* @private
*/
makeRoom: function(endPos,key) {
//myRow will go @ endPos position
//NOTE: the only thing that scroll is this.grid (and maps have to be updated)
var origPos = this.keyRowMap.get(key);
if (endPos == origPos) {
//it actually doesn't move
return;
}
//move this row to a fake row
var fakeRow = origPos ? CellMatrix.scrollRow(this.grid.getRow(origPos),null,this.useInner) : null;
var origKey = origPos ? this.keyRowMap.getReverse(origPos) : null;
var unit; //unit to go to the next row to be moved
var start; //first rows that moves
var stop; //last row that moves
if (origPos) {
if (origPos > endPos) {
//goDown
start = origPos-1;
stop = endPos;
unit = -1;
} else {
//goUp
start = origPos+1;
stop = endPos;
unit = 1;
}
} else if (this.sortField != null || this.addOnTop) {
// goDown
stop = endPos; //X
start = this.rowCount; //C
unit = -1;
} else {
//goUp
start = 1; //A
stop = endPos; //X //this.rowCount
unit = 1;
}
for (var fromPos = start; fromPos-unit!=stop; fromPos+=unit) {
var toPos = fromPos-unit;
var fromRow = this.grid.getRow(fromPos);
var toRow = this.grid.getRow(toPos);
if (!toRow && !this.updateIsKey()) {
//matrix will grow
toRow = {};
this.grid.insertRow(toRow,toPos);
//>>excludeStart("debugExclude", pragmas.debugExclude);
ASSERT.verifyNotOk(origPos);
//>>excludeEnd("debugExclude");
}
if (!toRow) {
//overflow
//>>excludeStart("debugExclude", pragmas.debugExclude);
ASSERT.verifyOk(this.updateIsKey());
ASSERT.verifyValue(fromPos,start);
//>>excludeEnd("debugExclude");
var fromKey = this.keyRowMap.getReverse(fromPos); // fromPos 10 fromKey 4 toPos 11
this.scrollOut(fromKey);
} else {
CellMatrix.scrollRow(fromRow,toRow,this.useInner);
var fromKey = this.keyRowMap.getReverse(fromPos);
this.keyRowMap.set(fromKey,toPos);
}
}
if (fakeRow) {
CellMatrix.scrollRow(fakeRow,this.grid.getRow(endPos),this.useInner);
this.keyRowMap.set(origKey,endPos);
} else {
this.grid.forEachCellInRow(endPos,function(cell) {
cell.clean();
});
}
},
////////LIVE SORT
/**
* @private
*/
calculateSortedPosition: function(toUpdateKey,oldSortVal,newSortVal) {
var up = 1;
var down = this.rowCount;
var j = -1;
//do a binary search to find the new place
while (up < down) {
j = Math.floor((up + down) /2);
var thisKey = null;
if (j <= this.rowCount) {
var compare = this.keyRowMap.getReverse(j);
if (compare == toUpdateKey) {
thisKey = oldSortVal;
} else {
thisKey = this.makeSortValue(this.values.get(compare,this.sortField));
}
}
if (this.isBefore(newSortVal,thisKey)) {
down = j - 1;
} else {
up = j + 1;
}
}
if (up == down) {
//up == down, our place is right after or right before this one
var compare = this.keyRowMap.getReverse(up);
var compareKey = this.makeSortValue(this.values.get(compare,this.sortField));
if (this.isBefore(newSortVal,compareKey)) {
return up;
} else {
return up + 1;
}
} else {
return up;
}
},
////////INITIAL SORT
/**
* @private
*/
partition: function(newRowToKey, left, right, pivotIndex) {
var pivotValue = this.makeSortValue(this.values.get(newRowToKey[pivotIndex],this.sortField));
//Move pivot to end
swap(newRowToKey,right,pivotIndex);
var sI = left;
for (var i=left; iThe same listener can be added to several different StaticGrid
* instances.
*
* Lifecycle: a listener can be added at any time.
*
* @param {StaticGridListener} listener An object that will receive the events
* as shown in the {@link StaticGridListener} interface.
*
Note that the given instance does not have to implement all of the
* methods of the StaticGridListener interface. In fact it may also
* implement none of the interface methods and still be considered a valid
* listener. In the latter case it will obviously receive no events.
*/
addListener: function(listener) {
this._callSuperMethod(StaticGrid,"addListener",[listener]);
},
/**
* Removes a listener from the StaticGrid instance so that it
* will not receive events anymore.
*
* Lifecycle: a listener can be removed at any time.
*
* @param {StaticGridListener} listener The listener to be removed.
*/
removeListener: function(listener) {
this._callSuperMethod(StaticGrid,"removeListener",[listener]);
},
/**
* Returns an array containing the {@link StaticGridListener} instances that
* were added to this client.
*
* @return {StaticGridListener[]} an array containing the listeners that were added to this instance.
* Listeners added multiple times are included multiple times in the array.
*/
getListeners: function() {
return this._callSuperMethod(StaticGrid,"getListeners");
}
};
//closure compiler exports
StaticGrid.prototype["addCell"] = StaticGrid.prototype.addCell;
StaticGrid.prototype["setRootNode"] = StaticGrid.prototype.setRootNode;
StaticGrid.prototype["extractItemList"] = StaticGrid.prototype.extractItemList;
StaticGrid.prototype["parseHtml"] = StaticGrid.prototype.parseHtml;
StaticGrid.prototype["addListener"] = StaticGrid.prototype.addListener;
StaticGrid.prototype["removeListener"] = StaticGrid.prototype.removeListener;
StaticGrid.prototype["getListeners"] = StaticGrid.prototype.getListeners;
StaticGrid.prototype["updateRowExecution"] = StaticGrid.prototype.updateRowExecution;
StaticGrid.prototype["removeRowExecution"] = StaticGrid.prototype.removeRowExecution;
//<---- Listener Interface
Inheritance(StaticGrid,AbstractGrid);
return StaticGrid;
})();
/*
Copyright (c) Lightstreamer Srl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var StatusWidget = /*@__PURE__*/(function() {
Environment.browserDocumentOrDie();
var ATTACH_TO_BORDER_WRONG = "The given value is not valid. Admitted values are no, left and right";
var DISPLAY_TYPE_WRONG = "The given value is not valid. Admitted values are open, closed and dyna";
var widgetDisabled = BrowserDetection.isProbablyIE(6,true);
function generateLogoFallback() {
var sImg = createDefaultElement("div");
applyStyles(sImg,{
textAlign: "center",
textOverflow: "ellipsis",
fontFamily: "Arial, Helvetica, sans-serif",
fontSize: "10px",
color: "#333333",
verticalAlign: "middle",
paddingTop: "3px",
width: "32px",
height: "32px",
display: "none"
});
sImg.innerHTML = NETSTATE_LABEL;
return sImg;
}
function generateImage(src) {
var sImg = createDefaultElement("img");
sImg.src = src;
applyStyles(sImg,{
display: "none"
// width: "32px",
// height: "32px",
});
return sImg;
}
var BLINK_TIME = 500;
//32x32 PGN "S" image - Base64 encoding - green version
var GREEN_IMG = generateImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDwAACw8BkvkDpQAAABl0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuN6eEncwAAAQDSURBVFhH7ZZtaJVlGMet1IpcgZHVF6XQCAJBxVkUEeGG7KzlS8xe9PiyM888vnBg7gyXExbOkmDH3M7mmmVDK9nOKJ2bw41UfJ3tKCgOF80PRUUvREQQZNvd7/9wP3US5vN4Zh8CBz/uc3au+3/9n+u5X64xY279/Z8r0Hn+zXGQDWGogRbohuNwFNqhCabftOdEbAK8BltgLzRbkozH4ApchSE4CE/dlOQITYZqWAUTXdGSd0smQR6UQR20RHatPrz+/chJPidhJ1TAQph8w2ZIlmXL+wvjLAkgNAPegjdgAUyDh+BReAZC0AAXYRiM5U/GJpjgywgJp8KXYCDOxBzotWIhifz0fVUWPAshSyljHbRA8+XByo8/ORk719xTumff0Q1P+EqsIBLeCZdtcrOlrfQz92miuyM9iEfhNPwOG+HedHG+T4IF0AQ/goFhuARvQ/Z1zZC40E2++1iFWdawzCljuLHIdJ2NSkiCotjrqYgZB/Ohy5r4gzGlio04l+RVroGK1mJTWFuIgbBZmSgw3Z+vd5MPInKbl4FrKnMfc8Z7ziH5q66B2L4ikx/PN8HEYrOiLs/s7FzuGvjUUyjTAJKPh/Mykegucwzkx+eZxe/kmlB9wFz8olwmzmSq72seyR+GlEys2xPEQMDk1TxnCuLPm5KmfHNhoHwIE4/5Ess0yO6GzQf6qn+NNC81gZocx4R4qXau2d6x5Pi2jkV3Z6rve55Ov/bU1opNyVXfvLB97t8mZOSVhrzv4l3RGDH3+BbMNFBro3p/JLhwR06/WwmNMrW5LfzDwdTWTelHdaZ5POd19K65q7Zz6YlFO/6phl7PGl6TXhcmKvX6PIVGE8ACfDzVXzZU3BhwFqYqoYWqBWu3cJ8W8mhyeM7FRN+5/jJTlAg4W1RbVVtWW9ea0Fb2Png8M40QgIEOHcm17UHnkAomXnYM6PByDzIdar70ERrrK9AGEX87fC0Dh3rXcky/6NwXOrY3thSnG6gaUZfJy+Ew/Ay6JFohF+7wMkPMOvdS6jwTvRpuDDkGdHHpAkurQOH1DIxFZB7o2vzKFWT8FuqhAB645kK5n/9VwW/W/Iq1763usn3CMFf3kbTkAze0Gw71ls/+6MiG5IFTsUsDVyqTJPgQNKrJULOhxkNVywZnm5G4yCY/y5hLQjWoqoCamWlelXR+V5tk2yW1TW4LpXbqAtTbJE8zPgIPwlSYD2rLtsFM6ZBwJqh9i8O/mhS/RqYgpgbydWiENjWYNJrdfG6FBMQgICOuqE4/UMOqxnWKr2ReQQg9Cert1WKr1R4E9fut8IFFrbla9CWQ5aXp+3fEpsMuUG+vRSV6bHKVtwTmwH93yPh2eytwFBX4C/nwkj6r2tmsAAAAAElFTkSuQmCC");
//32x32 PGN "S" image - Base64 encoding - grey version
var GREY_IMG = generateImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDgAACw4BQL7hQQAAABp0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuMTAw9HKhAAAD00lEQVRYR+2WWWhUVxzG1bq0aBQsbi+KoiIIgpbGFsW3SmlB1AcfVEQxLi9CFjohZt/IIlRCJqNmAnGIEsjEENCEyczEKCpGM9rEYqlQ81BpCy2IlEJBTa6/73JOSYV4rxP7UHDgx5lkzvm+7557lv+0ae8//+cZGBgYmAWZcAy+hQ5IwA24BpchDBve2XMiNg/2QRVcgIihk/Y6jMILGIMr8Pk7MUdoOVTDUVhoRaurqxfDV/ANBKGjpqYmXldXd4vvnXAWTsJuWP7WYTDLMNP7jPYTCSC0EWqhAnbBGlgKq2ArZMEZ+B7GwTG8pA3DPF9BMFwNP4EDpxn4BdwxYlkSGR0dzYBtkGXIow1CB0SGh4fbe3p67kej0bbu7u71vozVCcM58KMxd5qbm6/ap6mvr08ing234W8ogPkTxfl7MeyCMPwBDozDQzgFmW8Mg/Eea97V1eWUlpa601hZWenE43EJSVAc8Xoq+syCnRAzIZ7T3tOMTToW83IbIBgMOgUFBW6A4uJiJ5FIWPPHiEz3CvDazCxgzGzPMZjvtQEaGhqcvLw817yoqMhpa2uzAbo9hdLtgPls+E4h2tvb3QC5ublOfn6+U1JS4qRSKYUYTFff1zjMl8E9hWDhuSGys7PdIBUVFc7Q0NAYIdb6Eku3k9kNJclk8s/a2lonJyfHDSE0G62trTdaWlo+Slff9zidfv39/Sebmpp+sTNhgxQWFv7GugjQZ65vwXQ7am2Ew+EDgUDgBxtArUKFQqHfCVk08ahO18dzXCwW+zASidwkyD+vRK+HO8DR6yJEsV6fp9BUOrAA1w0ODo6VlZW5C9POhBas2cIpLeSpeHiOJUSKEO4ZoUWpIHod2romhLay98Hj6TRJBwL06EhmN7iHlIIogA4ve5DpUPOlj9BMXx1NJ/rPgCcK0NfX55rruNax3djYODFA+aS6DD4IcXgKuiSisB0+8ApDnxP2UmJRvqiqqnID6OLSBTZhBva8KcBMRL4EXZs/W0HaXyEEO2DRaxfKx/yvHP4y4Q9xSMVMnTDO1Y23W0OIR2+1G7hqPyV9Z29v78ORkZFODC6CWhUZKjZUeGjWMsHdZhgfNuZ3abdjqAJV5ipm1njNpPu7yiRTLqlssiWUyqkHEDImW2hXwhJYDTtBZVkdbJIOhptA5dtp+FeR4jfICsRUQBbCObikApNCM8H3KDRBAL5WECuK2UJQwarCdYUvM69OCH0Gqu1VYqvUfgyq96Nw3qDSXCX6fsjw0vT9O2IboAVU29tP0phreo/DZvjvDhnfad93nMIMvAIArtySMI7UCwAAAABJRU5ErkJggg==");
//32x32 PGN "S" image - Base64 encoding - light green version
var LIGHT_GREEN_IMG = generateImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALDgAACw4BQL7hQQAAABp0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuMTAw9HKhAAAECElEQVRYR+2WX2hbZRjGp9uq4qqguOnNhrKJIAycWJWJdw5RkM2LXaiIYnXeCMLYwLkJFQeK4G5mVOqFQ0VoK+iyxJg2bZrEpK3LRpqky9iyLq3705b9UcSBs/38PWffGWfV5JxOxxC8+PGlzTnv++T9vvf9nnmhUGje1eSqJtcP/28LqE3tWwgtsAE+gA7ohjQkIQztsLLeNs+5AgRbBM/CO/AF7LJ0sfbDETgP07AHHm50xgILINBS2A6vwC1u0P6R/sXwBGyCndCRGknFMwdSP/C5Cz6GLfA0LJ0txlcAyZptec+y3q8ABLoP3oW3YR2sgNvhLngEWuEjKMIMGMsfrO2wyBXSUAAJl8NhMLCDFx+DQRusVUHO/fZjMzwKrZaNrDuhA3ad+XnoqyMncvsqP2U/P3Qse2/gCpDwOqjY5CZfzfa6vyZTSfUQ/HXIwTl4A27yBufvxbAO2mEKDMxAGd6HloZtSOL1bvLK8QGTKCUulLHcZ8YmMgqkgOJlv0HGMwthLcSsiN9Z86pY3S0geZsrYKCaNPFi3BHQV4qa8cmMm7xKkGv8BMyqzM280+R7Bkj+jCsgd6jPRAtRqhA3vcWIKdd6XQHfzCX53z3bqAJNCNgvEaXxrCMgWthj4sNhqhAxp87mJGLgiglQYJLfAXmJSB9MICBiIoVvWXezHVFz6kxuGhF3/xMRQeaAuuGt0cn8L6lKAgFhR4T4vrjbFGo96f212A2XK8JXgBtY0+/oVH7LYDV5TBVwRWjtLkVOFMYym3nmxrkKCSzAI6QpP5p6PjYcHvGKkKihav8kIrd6R7WfoDkLuChkInV9sZbIxIa91QgbbZO2CxHbNMyumAA7hu+ZOp2dTpYjzsG8cEAjzoG1LbxXB/lfuQ3rBaEL9iLCaU21qFpVLavWtSLUyhcHT+C7wK907vcIiGgkF48mnCGVKHU7AjS83EGmoVYv3iVngEALgia2W3At74xLQG0iTRW+c8a1xvbA4aRXQFtdAbz8AsThNOiS6IQ1MN9PDM+85l5KtZOZ87qoJEAXly4wTwXWNxKwgCCPg67NMc8td5zPIXgKbpt1odzK/9rgVyv+xfSBVMz6hBmu7j5P8oONuuEvbVibyD2AcegaPZkrYya6SPAlaJXJkNmQ8VDVWsBpMxK/ZJMPsa4hoQyqKiAzsyJQF8gmWbsk2+RaKNmpYQjZJKtZ74QlsBzWgmzZe7DK3h+rSCr7tgMuMSmBbkMCLQMZyDfhE/haBhOj2c3nTvgQNsOTEuId1SSUYZVxXeZ3ftzv/TzhQwSTt5fFltWugvx+J3xmkTWXRX8OmoMm9hVAsJXwKcjb61CJHptc5X0VHoS6QyaImMu+C4IED/LM/wL+BDxNDVItZyFPAAAAAElFTkSuQmCC");
var imgFallback = false;
var DATA_STREAMING_SERVER = "DATA STREAMING STATUS";
var ATTACHED = " (attached)";
var NETSTATE_LABEL = "Net
State";
var STATUS_OPEN = 0;
var STATUS_OPENING = 1;
var STATUS_CLOSE = 2;
var STATUS_CLOSING = 3;
var NO = "no";
var LEFT = "left";
var RIGHT = "right";
var ATTACH_TO_MAP = makeMap(NO,LEFT,RIGHT);
var DYNA = "dyna";
var OPEN = "open";
var CLOSED = "closed";
var DISPLAY_TYPES = makeMap(DYNA,OPEN,CLOSED);
function makeMap() {
var res = {};
for (var i=0; i
* The left led indicates the transport in use: green if WS/WSS; yellow if HTTP/HTTPS.
* The center led indicates the mode in use: green if streaming; yellow if polling.
* The right led indicates where the physical connection in held: green if this LightstreamerClient
* is the master instance, holding the connection; yellow if this LightstreamerClient instance is a slave
* attached to the master Lightstreamer Client instance.
*
* By rolling over or clicking over the widget, a panel appears with full details.
*
Note that the widget is generated using some features not available
* on old browsers but as long as the
* "data" URL scheme is supported
* the minimal functions of the widget will work (for instance, IE<=7 does not have support
* for the "data" URL scheme).
*
Also note that on IE if "Quirks Mode" is activated the widget will not
* be displayed correctly. Specify a doctype on the document where the widget
* is going to be shown to prevent IE from entering the "Quirks Mode".
*
* @extends ClientListener
*/
var StatusWidget = function(attachToBorder, distance, fromTop, initialDisplay) {
if (widgetDisabled) {
return;
}
this.ready = false;
this.readyStatusOpen = false;
this.cachedStatus = null;
this.lsClient = null;
attachToBorder = attachToBorder || LEFT;
if (!ATTACH_TO_MAP[attachToBorder]) {
throw new IllegalArgumentException(ATTACH_TO_BORDER_WRONG);
}
initialDisplay = initialDisplay || CLOSED;
if (!DISPLAY_TYPES[initialDisplay]) {
throw new IllegalArgumentException(DISPLAY_TYPE_WRONG);
}
this.isLeftBorder = attachToBorder === LEFT;
var topClosed = fromTop ? distance : "auto";
var bottomClosed = fromTop ? "auto" : distance;
///////////////////////HTML generation
this.statusWidgetContainer = createDefaultElement("div");
var widgetMainNode = createDefaultElement("div");
this.statusTextsContainer = createDefaultElement("div");
applyStyles(this.statusWidgetContainer,{
"zIndex": "99999"
});
applyStyles(widgetMainNode, {
"width": "42px",
"height": "42px",
"opacity": "0.95",
"filter": "alpha(opacity"+"=95)",
"backgroundColor": "#135656",
"zIndex": "99999",
"position": "relative"
});
applyStyles(this.statusTextsContainer,{
"width":"245px",
"height":"42px",
"backgroundColor": "#ECE981",
"fontFamily": "'Open Sans',Arial,sans-serif",
"fontSize": "11px",
"color": "#3E5B3E",
"position": "absolute",
"zIndex": "99998",
"visibility": "hidden",
"opacity": "0",
"filter": "alpha(opacity"+"=0)",
"transition": "all 0.5s",
"MozTransition": "all 0.5s",
"-webkit-transition": "all 0.5s",
"OTransition": "all 0.5s",
"-ms-transition": "all 0.5s"
/*
"transition-duration": "0.5s",
"MozTransitionDuration": "0.5s",
"-webkit-transition-duration": "0.5s",
"OTransitionDuration": "0.5s",
"-ms-transition-duration": "0.5s",
"transition-timing-function": "ease",
"MozTransitionTimingFunction": "ease",
"-webkit-transition-timing-function": "ease",
"OTransitionTimingFunction": "ease",
"-ms-transition-timing-function": "ease",
"transition-property": "all",
"MozTransitionProperty": "all",
"-webkit-transition-property": "all",
"OTransitionProperty": "all",
"-ms-transition-property": "all"
*/
});
if (attachToBorder == "no") {
applyStyles(this.statusWidgetContainer,{
"position": "absolute"
});
applyStyles(widgetMainNode, {
"borderRadius": "4px",
"float": "left"
});
applyStyles(this.statusTextsContainer,{
"borderTopRightRadius": "4px",
"borderBottomRightRadius": "4px",
"left": "38px"
});
} else {
applyStyles(this.statusWidgetContainer, {
"position": "fixed",
"top": topClosed,
"bottom": bottomClosed
});
if (attachToBorder == LEFT) {
applyStyles(this.statusWidgetContainer, {
"left": "0px"
});
applyStyles(widgetMainNode, {
"borderTopRightRadius": "4px",
"borderBottomRightRadius": "4px",
"float": "left"
});
applyStyles(this.statusTextsContainer, {
"borderTopRightRadius": "4px",
"borderBottomRightRadius": "4px",
"left": "38px"
});
} else {//if (attachToBorder == RIGHT)
applyStyles(this.statusWidgetContainer, {
"right": "0px"
});
applyStyles(widgetMainNode, {
"borderTopLeftRadius": "4px",
"borderBottomLeftRadius": "4px",
"float": "right"
});
applyStyles(this.statusTextsContainer, {
"borderTopLeftRadius": "4px",
"borderBottomLeftRadius": "4px",
"right": "38px"
});
}
}
this.statusWidgetContainer.appendChild(widgetMainNode);
this.statusWidgetContainer.appendChild(this.statusTextsContainer);
var imageContainer = createDefaultElement("div");
applyStyles(imageContainer,{
"position": "absolute",
"top": "2px",
"left": "5px",
"width": "32px",
"height": "32px"
});
widgetMainNode.appendChild(imageContainer);
this.initImages(imageContainer);
this.transportLed = new Led(widgetMainNode,1);
this.streamingLed = new Led(widgetMainNode,2);
this.masterLed = new Led(widgetMainNode,3);
this.dataStreamingServer = createDefaultElement("div");
applyStyles(this.dataStreamingServer,{
"position": "absolute",
"top": "7px",
"left": "13px"
});
this.statusTextsContainer.appendChild(this.dataStreamingServer);
this.statusText = createDefaultElement("div");
applyStyles(this.statusText,{
"position": "absolute",
"top": "21px",
"left": "13px"
});
this.statusTextsContainer.appendChild(this.statusText);
this.updateWidget(OFF_LED,OFF_LED,OFF_LED,"Ready",DATA_STREAMING_SERVER,this.greyImg);
this.activate();
this.displayStatus = STATUS_CLOSE;
this.pinned = false;
if (initialDisplay != CLOSED) {
this.openDetails(true);
if (initialDisplay == DYNA) {
Executor.addTimedTask(this.getMouseoutHandler(),1000);
} else {
this.pinned = true;
}
}
var trHandler = this.getTransitionendHandler();
Helpers.addEvent(this.statusTextsContainer,"transitionend",trHandler);
Helpers.addEvent(this.statusTextsContainer,"webkitTransitionEnd",trHandler);
Helpers.addEvent(this.statusTextsContainer,"MSTransitionEnd",trHandler);
Helpers.addEvent(this.statusTextsContainer,"oTransitionEnd",trHandler);
Helpers.addEvent(this.statusWidgetContainer,"click",this.getClickHandler());
Helpers.addEvent(this.statusWidgetContainer,"mouseover",this.getMouseoverHandler());
Helpers.addEvent(this.statusWidgetContainer,"mouseout",this.getMouseoutHandler());
};
StatusWidget.prototype = {
/**
* Inquiry method that gets the DOM element that makes the widget container.
* It may be necessary to extract it to specify some extra styles or to position
* it in case "no" was specified as the attachToBorder constructor parameter.
*
* @return {Object} The widget DOM element.
*/
getDomNode: function() {
return this.statusWidgetContainer;
},
/**
* @private
*/
initImages: function(imageContainer) {
this.greyImg = GREY_IMG.cloneNode(true);
imageContainer.appendChild(this.greyImg);
if (!imgFallback && this.greyImg.height != 32 && BrowserDetection.isProbablyIE(7)) {
imageContainer.removeChild(this.greyImg);
//fallback!
GREY_IMG = GREEN_IMG = LIGHT_GREEN_IMG = generateLogoFallback();
imgFallback = true;
this.greyImg = GREY_IMG.cloneNode(true);
imageContainer.appendChild(this.greyImg);
}
this.greenImg = GREEN_IMG.cloneNode(true);
imageContainer.appendChild(this.greenImg);
this.lgreenImg = LIGHT_GREEN_IMG.cloneNode(true);
imageContainer.appendChild(this.lgreenImg);
},
/**
* @private
*/
activate: function() {
if (this.ready) {
return;
}
var body = document.getElementsByTagName("body");
if (!body || body.length == 0) {
//no body means we can't append; if there is no body DOMContentLoaded cannot have already being fired, so let's wait for it
var that = this;
//this widget uses styles not available on older browsers so that we do not need to setup a fallback to help browsers not having the DOMContentLoadEvent
Helpers.addEvent(document,"DOMContentLoaded",function() {
document.getElementsByTagName("body")[0].appendChild(that.statusWidgetContainer);
that.ready = true;
if (that.cachedStatus) {
that.onStatusChange(that.cachedStatus);
}
if (that.displayStatus == STATUS_OPEN) {
that.openDetails();
} else {
that.closeDetails();
}
});
} else {
body[0].appendChild(this.statusWidgetContainer);
this.ready = true;
}
},
/**
* @inheritdoc
*/
onListenStart: function(lsClient) {
if(widgetDisabled) {
return;
}
this.onStatusChange(lsClient.getStatus());
this.lsClient = lsClient;
},
/**
* @inheritdoc
*/
onListenEnd: function() {
if(widgetDisabled) {
return;
}
this.updateWidget(OFF_LED,OFF_LED,OFF_LED,"Ready",DATA_STREAMING_SERVER);
this.lsClient = null;
},
/**
* @private
*/
updateWidget: function(l1,l2,l3,text,title,sImage) {
this.transportLed.changeColor(l1);
this.streamingLed.changeColor(l2);
this.masterLed.changeColor(l3);
this.statusText.innerHTML = text;
this.dataStreamingServer.innerHTML = title;
this.updateWidgetS(sImage,true);
},
updateWidgetS: function(sImage,stopBlinking) {
if (stopBlinking) {
this.stopBlinking();
}
if (this.widgetImageNode) {
this.widgetImageNode.style.display = "none";
}
sImage.style.display = "";
this.widgetImageNode = sImage;
},
/**
* @private
*/
stopBlinking: function() {
if (!this.blinkThread) {
return;
}
this.blinkFlag = false;
Executor.stopRepetitiveTask(this.blinkThread);
this.blinkThread = null;
},
/**
* @private
*/
startBlinking: function() {
this.blinkThread = Executor.addRepetitiveTask(this.doBlinking,BLINK_TIME,this);
},
/**
* @private
*/
doBlinking: function() {
this.updateWidgetS(this.blinkFlag?this.greyImg:this.greenImg);
this.blinkFlag = !this.blinkFlag;
},
/**
* @inheritdoc
*/
onStatusChange: function(status) {
if (!this.ready || widgetDisabled) {
this.cachedStatus = status;
return;
}
var isMaster = this.lsClient && ((this.lsClient.isMaster && this.lsClient.isMaster()) || (this.lsClient.connectionSharing && this.lsClient.connectionSharing.isMaster()));
var masterLed = isMaster ? GREEN_LED : YELLOW_LED;
var incipit = isMaster ? DATA_STREAMING_SERVER : DATA_STREAMING_SERVER+ATTACHED;
if (status == LightstreamerConstants.DISCONNECTED) {
this.updateWidget(OFF_LED,OFF_LED,OFF_LED,"Disconnected",DATA_STREAMING_SERVER,this.greyImg);
} else if (status == LightstreamerConstants.CONNECTING) {
this.updateWidget(OFF_LED,OFF_LED,masterLed,"Connecting...",incipit,this.greyImg);
this.startBlinking();
} else if (status.indexOf(LightstreamerConstants.CONNECTED) == 0) {
var HEAD_STATUS = "Connected over ";
var separator = this.lsClient && this.lsClient.connectionDetails.getServerAddress().indexOf("https") == 0 ? "S in " : " in ";
if (status == LightstreamerConstants.CONNECTED + LightstreamerConstants.SENSE) {
this.updateWidget(YELLOW_LED, YELLOW_LED, masterLed, "Stream-sensing...",incipit,this.greyImg);
this.startBlinking();
} else if (status == LightstreamerConstants.CONNECTED + LightstreamerConstants.WS_STREAMING) {
this.updateWidget(GREEN_LED, GREEN_LED, masterLed, HEAD_STATUS+"WS"+separator+"streaming mode",incipit,this.greenImg);
} else if (status == LightstreamerConstants.CONNECTED + LightstreamerConstants.HTTP_STREAMING) {
this.updateWidget(YELLOW_LED, GREEN_LED, masterLed, HEAD_STATUS+"HTTP"+separator+"streaming mode",incipit,this.greenImg);
} else if (status == LightstreamerConstants.CONNECTED + LightstreamerConstants.WS_POLLING) {
this.updateWidget(GREEN_LED, YELLOW_LED, masterLed, HEAD_STATUS+"WS"+separator+"polling mode",incipit,this.greenImg);
} else if (status == LightstreamerConstants.CONNECTED + LightstreamerConstants.HTTP_POLLING) {
this.updateWidget(YELLOW_LED, YELLOW_LED, masterLed, HEAD_STATUS+"HTTP"+separator+"polling mode",incipit,this.greenImg);
}
} else if (status == LightstreamerConstants.STALLED) {
this.updateWidget(OFF_LED,OFF_LED,masterLed,"Stalled",incipit,this.lgreenImg);
} else if (status == LightstreamerConstants.TRYING_RECOVERY) {
this.updateWidget(OFF_LED,OFF_LED,masterLed,"Recovering...",incipit,this.lgreenImg);
this.startBlinking();
} else {
this.updateWidget(OFF_LED,OFF_LED,masterLed,"Disconnected (will retry)",incipit,this.greyImg);
}
},
openDetails: function(force) {
if (this.displayStatus == STATUS_OPEN ||this.displayStatus == STATUS_OPENING) {
return;
}
this.displayStatus = STATUS_OPENING;
applyStyles(this.statusTextsContainer,{
"visibility": "",
"opacity": "1",
"filter": "alpha(opacity"+"=100)"
});
if (!TRANSITION_SUPPORTED || force) {
this.transitionendHandler();
}
},
closeDetails: function(force) {
if (this.displayStatus == STATUS_CLOSE ||this.displayStatus == STATUS_CLOSING) {
return;
}
this.displayStatus = STATUS_CLOSING;
applyStyles(this.statusTextsContainer, {
"visibility": "hidden",
"opacity": "0",
"filter": "alpha(opacity" + "=0)"
});
this.pinned = false;
if (!TRANSITION_SUPPORTED || force) {
this.transitionendHandler();
}
},
/**
* @private
*/
getMouseoverHandler: function() {
var that = this;
return function() {
that.openDetails();
};
},
/**
* @private
*/
getMouseoutHandler: function() {
var that = this;
return function() {
if (!that.pinned) {
that.closeDetails();
}
};
},
/**
* @private
*/
getClickHandler: function() {
var that = this;
return function() {
that.clickHandler();
};
},
/**
* @private
*/
clickHandler: function() {
if (!this.pinned) {
this.pinned = true;
this.openDetails();
} else {
this.closeDetails();
}
},
/**
* @private
*/
getTransitionendHandler: function() {
var that = this;
return function() {
that.transitionendHandler();
};
},
/**
* @private
*/
transitionendHandler: function() {
if (this.statusTextsContainer.style["visibility"] == "hidden") {
this.toggleState = STATUS_CLOSE;
} else {
this.toggleState = STATUS_OPEN;
}
}
};
var GREEN_LED = "#709F70";
var YELLOW_LED = "#ECE981";
var OFF_LED = "#135656";
function Led(container,ledCount) {
this.led = createDefaultElement("div");
applyStyles(this.led,{
"position": "absolute",
"bottom": "3px",
"left": 5+11*(ledCount-1)+"px",
"width": "10px",
"height": "3px",
"borderRadius": "2px",
"backgroundColor": OFF_LED
});
container.appendChild(this.led);
}
Led.prototype.changeColor = function(newColor) {
applyStyles(this.led,{"backgroundColor": newColor});
};
//closure compiler eports
StatusWidget.prototype["onStatusChange"] = StatusWidget.prototype.onStatusChange;
StatusWidget.prototype["onListenStart"] = StatusWidget.prototype.onListenStart;
StatusWidget.prototype["onListenEnd"] = StatusWidget.prototype.onListenEnd;
StatusWidget.prototype["getDomNode"] = StatusWidget.prototype.getDomNode;
return StatusWidget;
})();
var _virtual_virtualEntrypoint = {
'Chart': Chart,
'DynaGrid': DynaGrid,
'SimpleChartListener': SimpleChartListener,
'StaticGrid': StaticGrid,
'StatusWidget': StatusWidget
};
return _virtual_virtualEntrypoint;
}());
if (typeof define === 'function' && define.amd) {
define("lightstreamer", ["module"], function(module) {
var namespace = (module.config()['ns'] ? module.config()['ns'] + '/' : '');
define(namespace + 'Chart', function() { return lightstreamerExports['Chart'] });
define(namespace + 'DynaGrid', function() { return lightstreamerExports['DynaGrid'] });
define(namespace + 'SimpleChartListener', function() { return lightstreamerExports['SimpleChartListener'] });
define(namespace + 'StaticGrid', function() { return lightstreamerExports['StaticGrid'] });
define(namespace + 'StatusWidget', function() { return lightstreamerExports['StatusWidget'] });
});
require(["lightstreamer"]);
}
else if (typeof module === 'object' && module.exports) {
exports['Chart'] = lightstreamerExports['Chart'];
exports['DynaGrid'] = lightstreamerExports['DynaGrid'];
exports['SimpleChartListener'] = lightstreamerExports['SimpleChartListener'];
exports['StaticGrid'] = lightstreamerExports['StaticGrid'];
exports['StatusWidget'] = lightstreamerExports['StatusWidget'];
}
else {
var namespace = createNs(extractNs(), window);
namespace['Chart'] = lightstreamerExports['Chart'];
namespace['DynaGrid'] = lightstreamerExports['DynaGrid'];
namespace['SimpleChartListener'] = lightstreamerExports['SimpleChartListener'];
namespace['StaticGrid'] = lightstreamerExports['StaticGrid'];
namespace['StatusWidget'] = lightstreamerExports['StatusWidget'];
}
function extractNs() {
var scripts = window.document.getElementsByTagName("script");
for (var i = 0, len = scripts.length; i < len; i++) {
if ('data-lightstreamer-ns' in scripts[i].attributes) {
return scripts[i].attributes['data-lightstreamer-ns'].value;
}
}
return null;
}
function createNs(ns, root) {
if (! ns) {
return root;
}
var pieces = ns.split('.');
var parent = root || window;
for (var j = 0; j < pieces.length; j++) {
var qualifier = pieces[j];
var obj = parent[qualifier];
if (! (obj && typeof obj == 'object')) {
obj = parent[qualifier] = {};
}
parent = obj;
}
return parent;
}
}());