build/zebkit.js
(function(){
'use strict';
(function() {
/**
* WEB environment implementation. Provides elementary API zebkit needs to perform an
* environment specific operations.
* @class environment
* @access package
*/
var zebkitEnvironment = function() {
var pkg = {},
hostRe = /([a-zA-Z]+)\:\/\/([^/:]+)/,
isFF = typeof navigator !== 'undefined' &&
navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
function $sleep() {
var r = new XMLHttpRequest(),
t = (new Date()).getTime().toString(),
i = window.location.toString().lastIndexOf("?");
r.open('GET', window.location + (i > 0 ? "&" : "?") + t, false);
r.send(null);
}
function $Request() {
this.responseText = this.statusText = "";
this.onreadystatechange = this.responseXml = null;
this.readyState = this.status = 0;
}
$Request.prototype.open = function(method, url, async, user, password) {
var m = url.match(hostRe);
if (window.location.scheme.toLowerCase() === "file:" ||
(m !== null &&
m[2] !== undefined &&
m[2].toLowerCase() === window.location.host.toLowerCase()))
{
this._request = new XMLHttpRequest();
this._xdomain = false;
var $this = this;
this._request.onreadystatechange = function() {
$this.readyState = $this._request.readyState;
if ($this._request.readyState === 4) {
$this.responseText = $this._request.responseText;
$this.responseXml = $this._request.responseXml;
$this.status = $this._request.status;
$this.statusText = $this._request.statusText;
}
if ($this.onreadystatechange) {
$this.onreadystatechange();
}
};
return this._request.open(method, url, (async !== false), user, password);
} else {
this._xdomain = true;
this._async = (async === true);
this._request = new XDomainRequest();
return this._request.open(method, url);
}
};
$Request.prototype.send = function(data) {
if (this._xdomain) {
var originalReq = this._request,
$this = this;
//!!!! handler has to be defined after
//!!!! open method has been called and all
//!!!! four handlers have to be defined
originalReq.ontimeout = originalReq.onprogress = function () {};
originalReq.onerror = function() {
$this.readyState = 4;
$this.status = 404;
if ($this._async && $this.onreadystatechange) {
$this.onreadystatechange();
}
};
originalReq.onload = function() {
$this.readyState = 4;
$this.status = 200;
if ($this._async && $this.onreadystatechange) {
$this.onreadystatechange(originalReq.responseText, originalReq);
}
};
//!!! set time out zero to prevent data lost
originalReq.timeout = 0;
if (this._async === false) {
originalReq.send(data);
while (this.status === 0) {
$sleep();
}
this.readyState = 4;
this.responseText = originalReq.responseText;
} else {
//!!! short timeout to make sure bloody IE is ready
setTimeout(function () {
originalReq.send(data);
}, 10);
}
} else {
return this._request.send(data);
}
};
$Request.prototype.abort = function(data) {
return this._request.abort();
};
$Request.prototype.setRequestHeader = function(name, value) {
if (this._xdomain) {
if (name === "Content-Type") {
//!!!
// IE8 and IE9 anyway don't take in account the assignment
// IE8 throws exception every time a value is assigned to
// the property
// !!!
//this._request.contentType = value;
return;
} else {
throw new Error("Method 'setRequestHeader' is not supported for " + name);
}
} else {
this._request.setRequestHeader(name, value);
}
};
$Request.prototype.getResponseHeader = function(name) {
if (this._xdomain) {
throw new Error("Method is not supported");
}
return this._request.getResponseHeader(name);
};
$Request.prototype.getAllResponseHeaders = function() {
if (this._xdomain) {
throw new Error("Method is not supported");
}
return this._request.getAllResponseHeaders();
};
/**
* Build HTTP request that provides number of standard methods, fields and listeners:
*
* - "open(method, url [,async])" - opens the given URL
* - "send(data)" - sends data
* - "status" - HTTP status code
* - "statusText" - HTTP status text
* - "responseText" - response text
* - "readyState" - request ready state
* - "onreadystatechange()" - ready state listener
*
* @return {Object} an HTTP request object
* @method getHttpRequest
*/
pkg.getHttpRequest = function() {
var r = new XMLHttpRequest();
if (isFF) {
r.__send = r.send;
r.send = function(data) {
// !!! FF can throw NS_ERROR_FAILURE exception instead of
// !!! returning 404 File Not Found HTTP error code
// !!! No request status, statusText are defined in this case
try {
return this.__send(data);
} catch(e) {
if (!e.message || e.message.toUpperCase().indexOf("NS_ERROR_FAILURE") < 0) {
// exception has to be re-instantiate to be Error class instance
throw new Error(e.toString());
}
}
};
}
return ("withCredentials" in r) ? r // CORS is supported out of box
: new $Request(); // IE
};
pkg.parseXML = function(s) {
function rmws(node) {
if (node.childNodes !== null) {
for (var i = node.childNodes.length; i-- > 0;) {
var child= node.childNodes[i];
if (child.nodeType === 3 && child.data.match(/^\s*$/) !== null) {
node.removeChild(child);
}
if (child.nodeType === 1) {
rmws(child);
}
}
}
return node;
}
if (typeof DOMParser !== "undefined") {
return rmws((new DOMParser()).parseFromString(s, "text/xml"));
} else {
for (var n in { "Microsoft.XMLDOM":0, "MSXML2.DOMDocument":1, "MSXML.DOMDocument":2 }) {
var p = null;
try {
p = new ActiveXObject(n);
p.async = false;
} catch (e) {
continue;
}
if (p === null) {
throw new Error("XML parser is not available");
}
p.loadXML(s);
return p;
}
}
throw new Error("No XML parser is available");
};
/**
* Loads an image by the given URL.
* @param {String|HTMLImageElement} img an image URL or image object
* @param {Function} success a call back method to be notified when the image has
* been successfully loaded. The method gets an image as its parameter.
* @param {Function} [error] a call back method to be notified if the image loading
* has failed. The method gets an image instance as its parameter and an exception
* that describes an error has happened.
*
* @example
* // load image
* zebkit.environment.loadImage("test.png", function(image) {
* // handle loaded image
* ...
* }, function (img, exception) {
* // handle error
* ...
* });
*
* @return {HTMLImageElement} an image
* @method loadImage
*/
pkg.loadImage = function(ph, success, error) {
var img = null;
if (ph instanceof Image) {
img = ph;
} else {
img = new Image();
img.crossOrigin = '';
img.crossOrigin ='anonymous';
img.src = ph;
}
if (img.complete === true && img.naturalWidth !== 0) {
success.call(this, img);
} else {
var pErr = img.onerror,
pLoad = img.onload,
$this = this;
img.onerror = function(e) {
img.onerror = null;
try {
if (error !== undefined) {
error.call($this, img, new Error("Image '" + ph + "' cannot be loaded " + e));
}
} finally {
if (typeof pErr === 'function') {
img.onerror = pErr;
pErr.call(this, e);
}
}
};
img.onload = function(e) {
img.onload = null;
try {
success.call($this, img);
} finally {
if (typeof pLoad === 'function') {
img.onload = pLoad;
pLoad.call(this, e);
}
}
};
}
return img;
};
/**
* Parse JSON string
* @param {String} json a JSON string
* @method parseJSON
* @return {Object} parsed JSON as an JS Object
*/
pkg.parseJSON = JSON.parse;
/**
* Convert the given JS object into an JSON string
* @param {Object} jsonObj an JSON JS object to be converted into JSON string
* @return {String} a JSON string
* @method stringifyJSON
*
*/
pkg.stringifyJSON = JSON.stringify;
/**
* Call the given callback function repeatedly with the given calling interval.
* @param {Function} cb a callback function to be called
* @param {Integer} time an interval in milliseconds the given callback
* has to be called
* @return {Integer} an run interval id
* @method setInterval
*/
pkg.setInterval = function (cb, time) {
return window.setInterval(cb, time);
};
/**
* Clear the earlier started interval calling
* @param {Integer} id an interval id
* @method clearInterval
*/
pkg.clearInterval = function (id) {
return window.clearInterval(id);
};
if (typeof window !== 'undefined') {
var $taskMethod = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) { return setTimeout(callback, 35); };
pkg.decodeURIComponent = window.decodeURIComponent;
pkg.encodeURIComponent = window.encodeURIComponent;
} else {
pkg.decodeURIComponent = function(s) { return s; } ;
pkg.encodeURIComponent = function(s) { return s; } ;
}
/**
* Request to run a method as an animation task.
* @param {Function} f the task body method
* @method animate
*/
pkg.animate = function(f){
return $taskMethod.call(window, f);
};
function buildFontHelpers() {
// font metrics API
var e = document.getElementById("zebkit.fm");
if (e === null) {
e = document.createElement("div");
e.setAttribute("id", "zebkit.fm"); // !!! position fixed below allows to avoid 1px size in HTML layout for "zebkit.fm" element
e.setAttribute("style", "visibility:hidden;line-height:0;height:1px;vertical-align:baseline;position:fixed;");
e.innerHTML = "<span id='zebkit.fm.text' style='display:inline;vertical-align:baseline;'> </span>" +
"<img id='zebkit.fm.image' style='width:1px;height:1px;display:inline;vertical-align:baseline;' width='1' height='1'/>";
document.body.appendChild(e);
}
var $fmCanvas = document.createElement("canvas").getContext("2d"),
$fmText = document.getElementById("zebkit.fm.text"),
$fmImage = document.getElementById("zebkit.fm.image");
$fmImage.onload = function() {
// TODO: hope the base64 specified image load synchronously and
// checking it with "join()"
};
// set 1x1 transparent picture
$fmImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII%3D';
pkg.fontMeasure = $fmCanvas;
pkg.fontStringWidth = function(font, str) {
if (str.length === 0) {
return 0;
} else {
if ($fmCanvas.font !== font) {
$fmCanvas.font = font;
}
return Math.round($fmCanvas.measureText(str).width);
}
};
pkg.fontMetrics = function(font) {
if ($fmText.style.font !== font) {
$fmText.style.font = font;
}
var height = $fmText.offsetHeight;
//!!!
// Something weird is going sometimes in IE10 !
// Sometimes the property offsetHeight is 0 but
// second attempt to access to the property gives
// proper result
if (height === 0) {
height = $fmText.offsetHeight;
}
return {
height : height,
ascent : $fmImage.offsetTop - $fmText.offsetTop + 1
};
};
}
if (typeof document !== 'undefined') {
document.addEventListener("DOMContentLoaded", buildFontHelpers);
}
return pkg;
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports.zebkitEnvironment = zebkitEnvironment;
// TODO:
// typeof the only way to make environment visible is makling it global
// since module cannot be applied in the ase of browser context
if (typeof global !== 'undefined') {
global.zebkitEnvironment = zebkitEnvironment;
}
} else {
window.zebkitEnvironment = zebkitEnvironment;
}
})();
/**
* Promise-like sequential tasks runner (D-then). Allows developers to execute
* number of steps (async and sync) in the exact order they have been called with
* the class instance. The idea of the runner implementation is making the
* code more readable and plain nevertheless it includes asynchronous parts:
* @example
*
* var r = new zebkit.DoIt();
*
* // step 1
* r.then(function() {
* // call three asynchronous HTTP GET requests to read three files
* // pass join to every async. method to be notified when the async.
* // part is completed
* asyncHttpCall("http://test.com/a.txt", this.join());
* asyncHttpCall("http://test.com/b.txt", this.join());
* asyncHttpCall("http://test.com/c.txt", this.join());
* })
* . // step 2
* then(function(r1, r2, r3) {
* // handle completely read on previous step files
* r1.responseText // "a.txt" file content
* r2.responseText // "b.txt" file content
* r3.responseText // "c.txt" file content
* })
* . // handle error
* catch(function(e) {
* // called when an exception has occurred
* ...
* });
*
*
* @class zebkit.DoIt
* @param {Boolean} [ignore] flag to rule error ignorance
* @constructor
*/
function DoIt(body, ignore) {
this.recover();
if (arguments.length === 1) {
if (body !== undefined && body !== null && (typeof body === "boolean" || body.constructor === Boolean)) {
this.$ignoreError = body;
body = null;
} else {
this.then(body);
}
} else if (arguments.length === 2) {
this.$ignoreError = ignore;
this.then(body);
}
}
DoIt.prototype = {
/**
* Indicates if the error has to be ignored
* @attribute $ignoreError
* @private
* @type {Boolean}
*/
$ignoreError : false,
// TODO: not stable API
recover : function(body) {
if (this.$error !== null) {
var err = this.$error;
this.$error = null;
this.$tasks = [];
this.$results = [];
this.$taskCounter = this.$level = this.$busy = 0;
if (arguments.length === 1) {
body.call(this, err);
}
}
return this;
},
/**
* Restart the do it object to clear error that has happened and
* continue tasks that has not been run yet because of the error.
* @method restart
* @chainable
*/
restart : function() {
if (this.$error !== null) {
this.$error = null;
}
this.$schedule();
return this;
},
/**
* Run the given method as one of the sequential step of the doit execution.
* @method then
* @param {Function} body a method to be executed. The method can get results
* of previous step execution as its arguments. The method is called in context
* of a DoIt instance.
* @chainable
*/
then : function(body, completed) {
var level = this.$level; // store level then was executed for the given task
// to be used to compute correct the level inside the
// method below
if (body instanceof DoIt) {
if (body.$error !== null) {
this.error(body.$error);
} else {
var $this = this;
this.then(function() {
var jn = $this.join();
body.then(function() {
if (arguments.length > 0) {
// also pass result to body DoIt
this.join.apply(this, arguments);
}
}, function() {
if ($this.$error === null) {
jn.apply($this, arguments);
}
}).catch(function(e) {
$this.error(e);
});
});
}
return this;
} else {
var task = function() {
// clean results of execution of a previous task
this.$busy = 0;
var pc = this.$taskCounter, args = null, r;
if (this.$error === null) {
if (this.$results[level] !== undefined) {
args = this.$results[level];
}
this.$taskCounter = 0; // we have to count the tasks on this level
this.$level = level + 1;
this.$results[level] = [];
// it is supposed the call is embedded with other call, no need to
// catch it one more time
if (level > 0) {
r = body.apply(this, args);
} else {
try {
r = body.apply(this, args);
} catch(e) {
this.error(e);
}
}
// this.$busy === 0 means we have called synchronous task
// and make sure the task has returned a result
if (this.$busy === 0 && this.$error === null && r !== undefined) {
this.$results[level] = [ r ];
}
}
if (level === 0) {
// zero level is responsible for handling exception
try {
this.$schedule();
} catch(e) {
this.error(e);
}
} else {
this.$schedule();
}
this.$level = level; // restore level
this.$taskCounter = pc; // restore counter
// TODO: not a graceful solution. It has been done to let call "join" out
// outside of body. Sometimes it is required to provide proper level of
// execution since join calls schedule
if (typeof completed === 'function') {
if (level === 0) {
try {
if (args === null) {
completed.call(this);
} else {
completed.apply(this, args);
}
} catch(e) {
this.error(e);
}
} else {
if (args === null) {
completed.call(this);
} else {
completed.apply(this, args);
}
}
}
if (args !== null) {
args.length = 0;
}
};
if (this.$error === null) {
if (level === 0 && this.$busy === 0) {
if (this.$results[level] !== null &&
this.$results[level] !== undefined &&
this.$results[level].length > 0)
{
task.apply(this, this.$results[level]);
} else {
task.call(this);
}
} else {
// put task in list
if (this.$level > 0) {
this.$tasks.splice(this.$taskCounter++, 0, task);
} else {
this.$tasks.push(task);
}
}
}
}
if (this.$level === 0) {
this.$schedule();
}
return this;
},
$ignored : function(e) {
this.dumpError(e);
},
/**
* Force to fire error.
* @param {Error} [e] an error to be fired
* @method error
* @chainable
*/
error : function(e, pr) {
if (arguments.length === 0) {
if (this.$error !== null) {
this.dumpError(e);
}
} else {
if (this.$error === null) {
if (this.$ignoreError) {
this.$ignored(e);
} else {
this.$taskCounter = this.$level = this.$busy = 0;
this.$error = e;
this.$results = [];
}
this.$schedule();
} else if (arguments.length < 2 || pr === true) {
this.dumpError(e);
}
}
return this;
},
/**
* Wait for the given doit redness.
* @param {zebkit.DoIt} r a runner
* @example
*
* var async = new DoIt().then(function() {
* // imagine we do asynchronous ajax call
* ajaxCall("http://test.com/data", this.join());
* });
*
* var doit = new DoIt().till(async).then(function(res) {
* // handle result that has been fetched
* // by "async" do it
* ...
* });
*
* @chainable
* @method till
*/
till : function(r) {
// wait till the given DoIt is executed
this.then(function() {
var $this = this,
jn = this.join(), // block execution of the runner
res = arguments.length > 0 ? Array.prototype.slice.call(arguments) : []; // save arguments to restore it later
// call "doit" we are waiting for
r.then(function() {
if ($this.$error === null) {
// unblock the doit that waits for the runner we are in and
// restore its arguments
if (res.length > 0) {
jn.apply($this, res);
} else {
jn.call($this);
}
// preserve arguments for the next call
if (arguments.length > 0) {
this.join.apply(this, arguments);
}
}
}).catch(function(e) {
// delegate error to a waiting runner
$this.error(e);
});
});
return this;
},
/**
* Returns join callback for asynchronous parts of the doit. The callback
* has to be requested and called by an asynchronous method to inform the
* doit the given method is completed.
* @example
*
* var d = new DoIt().then(function() {
* // imagine we call ajax HTTP requests
* ajaxCall("http://test.com/data1", this.join());
* ajaxCall("http://test.com/data2", this.join());
* }).then(function(res1, res2) {
* // handle results of ajax requests from previous step
* ...
* });
*
* @return {Function} a method to notify doit the given asynchronous part
* has been completed. The passed to the method arguments will be passed
* to the next step of the runner. *
* @method join
*/
join : function() {
// if join is called outside runner than level is set to 0
var level = this.$level === 0 ? 0 : this.$level - 1;
if (arguments.length > 0) {
this.$results[level] = [];
for(var i = 0; i < arguments.length; i++) {
this.$results[level][i] = arguments[i];
}
} else {
// TODO: join uses busy flag to identify the result index the given join will supply
// what triggers a potential result overwriting problem (jn2 overwrite jn1 result):
// var jn1 = join(); jn1();
// var jn2 = join(); jn2();
var $this = this,
index = this.$busy++;
return function() {
if ($this.$results[level] === null || $this.$results[level] === undefined) {
$this.$results[level] = [];
}
// since error can occur and times variable
// can be reset to 0 we have to check it
if ($this.$busy > 0) {
var i = 0;
if (arguments.length > 0) {
$this.$results[level][index] = [];
for(i = 0; i < arguments.length; i++) {
$this.$results[level][index][i] = arguments[i];
}
}
if (--$this.$busy === 0) {
// collect result
if ($this.$results[level].length > 0) {
var args = $this.$results[level],
res = [];
for(i = 0; i < args.length; i++) {
Array.prototype.push.apply(res, args[i]);
}
$this.$results[level] = res;
}
// TODO: this code can bring to unexpected scheduling for a situation when
// doit is still in then:
// then(function () {
// var jn1 = join();
// ...
// jn1() // unexpected scheduling of the next then since busy is zero
// ...
// var jn2 = join(); // not actual
// })
$this.$schedule();
}
}
};
}
},
/**
* Method to catch error that has occurred during the doit sequence execution.
* @param {Function} [body] a callback to handle the error. The method
* gets an error that has happened as its argument. If there is no argument
* the error will be printed in output. If passed argument is null then
* no error output is expected.
* @chainable
* @method catch
*/
catch : function(body) {
var level = this.$level; // store level then was executed for the given task
// to be used to compute correct the level inside the
// method below
var task = function() {
// clean results of execution of a previous task
this.$busy = 0;
var pc = this.$taskCounter;
if (this.$error !== null) {
this.$taskCounter = 0; // we have to count the tasks on this level
this.$level = level + 1;
try {
if (typeof body === 'function') {
body.call(this, this.$error);
} else if (body === null) {
} else {
this.dumpError(this.$error);
}
} catch(e) {
this.$level = level; // restore level
this.$taskCounter = pc; // restore counter
throw e;
}
}
if (level === 0) {
try {
this.$schedule();
} catch(e) {
this.error(e);
}
} else {
this.$schedule();
}
this.$level = level; // restore level
this.$taskCounter = pc; // restore counter
};
if (this.$level > 0) {
this.$tasks.splice(this.$taskCounter++, 0, task);
} else {
this.$tasks.push(task);
}
if (this.$level === 0) {
this.$schedule();
}
return this;
},
/**
* Throw an exception if an error has happened before the method call,
* otherwise do nothing.
* @method throw
* @chainable
*/
throw : function() {
return this.catch(function(e) {
throw e;
});
},
$schedule : function() {
if (this.$tasks.length > 0 && this.$busy === 0) {
this.$tasks.shift().call(this);
}
},
end : function() {
this.recover();
},
dumpError: function(e) {
if (typeof console !== "undefined" && console.log !== undefined) {
if (e === null || e === undefined) {
console.log("Unknown error");
} else {
console.log((e.stack ? e.stack : e));
}
}
}
};
// Environment specific stuff
var $exports = {},
$zenv = {},
$global = (typeof window !== "undefined" && window !== null) ? window
: (typeof global !== 'undefined' ? global
: this),
$isInBrowser = typeof navigator !== "undefined",
isIE = $isInBrowser && (Object.hasOwnProperty.call(window, "ActiveXObject") ||
!!window.ActiveXObject ||
window.navigator.userAgent.indexOf("Edge") > -1),
isFF = $isInBrowser && window.mozInnerScreenX !== null,
isMacOS = $isInBrowser && navigator.platform.toUpperCase().indexOf('MAC') !== -1,
$FN = null;
/**
* Reference to global space.
* @attribute $global
* @private
* @readOnly
* @type {Object}
* @for zebkit
*/
if (parseInt.name !== "parseInt") {
$FN = function(f) { // IE stuff
if (f.$methodName === undefined) { // test if name has been earlier detected
var mt = f.toString().match(/^function\s+([^\s(]+)/);
f.$methodName = (mt === null) ? ''
: (mt[1] === undefined ? ''
: mt[1]);
}
return f.$methodName;
};
} else {
$FN = function(f) {
return f.name;
};
}
function $export() {
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (typeof arg === 'function') {
$exports[$FN(arg)] = arg;
} else {
for (var k in arg) {
if (arg.hasOwnProperty(k)) {
$exports[k] = arg[k];
}
}
}
}
}
if (typeof zebkitEnvironment === 'function') {
$zenv = zebkitEnvironment();
} else if (typeof window !== 'undefined') {
$zenv = window;
}
// Map class definition for old browsers
function $Map() {
var Map = function() {
this.keys = [];
this.values = [];
this.size = 0 ;
};
Map.prototype = {
set : function(key, value) {
var i = this.keys.indexOf(key);
if (i < 0) {
this.keys.push(key);
this.values.push(value);
this.size++;
} else {
this.values[i] = value;
}
return this;
},
delete: function(key) {
var i = this.keys.indexOf(key);
if (i < 0) {
return false;
}
this.keys.splice(i, 1);
this.values.splice(i, 1);
this.size--;
return true;
},
get : function(key) {
var i = this.keys.indexOf(key);
return i < 0 ? undefined : this.values[i];
},
clear : function() {
this.keys = [];
this.keys.length = 0;
this.values = [];
this.values.length = 0;
this.size = 0;
},
has : function(key) {
return this.keys.indexOf(key) >= 0;
},
forEach: function(callback, context) {
var $this = arguments.length < 2 ? this : context;
for(var i = 0 ; i < this.size; i++) {
callback.call($this, this.values[i], this.keys[i], this);
}
}
};
return Map;
}
// ES6 Map is class
if (typeof Map === 'undefined' && (typeof $global !== 'undefined' || typeof $global.Map === "undefined")) {
$global.Map = $Map();
}
function GET(url) {
var req = $zenv.getHttpRequest();
req.open("GET", url, true);
return new DoIt(function() {
var jn = this.join(),
$this = this;
req.onreadystatechange = function() {
if (req.readyState === 4) {
// evaluate HTTP response
if (req.status >= 400 || req.status < 100) {
var e = new Error("HTTP error '" + req.statusText + "', code = " + req.status + " '" + url + "'");
e.status = req.status;
e.statusText = req.statusText;
e.readyState = req.readyState;
$this.error(e);
} else {
jn(req);
}
}
};
try {
req.send(null);
} catch(e) {
this.error(e);
}
});
}
// Micro file system
var ZFS = {
catalogs : {},
load: function(pkg, files) {
var catalog = this.catalogs[pkg];
if (catalog === undefined) {
catalog = {};
this.catalogs[pkg] = catalog;
}
for(var file in files) {
catalog[file] = files[file];
}
},
read : function(uri) {
var p = null;
for(var catalog in this.catalogs) {
var pkg = zebkit.byName(catalog),
files = this.catalogs[catalog];
if (pkg === null) {
throw new ReferenceError("'" + catalog + "'");
}
p = new URI(uri).relative(pkg.$url);
if (p !== null && files[p] !== undefined && files[p] !== null) {
return files[p];
}
}
return null;
},
GET: function(uri) {
var f = ZFS.read(uri);
if (f !== null) {
return new DoIt(function() {
return {
status : 200,
statusText : "",
extension : f.ext,
responseText: f.data
};
});
} else {
return GET(uri);
}
}
};
/**
* Dump the given error to output.
* @param {Exception | Object} e an error.
* @method dumpError
* @for zebkit
*/
function dumpError(e) {
if (typeof console !== "undefined" && typeof console.log !== "undefined") {
var msg = "zebkit.err [";
if (typeof Date !== 'undefined') {
var date = new Date();
msg = msg + date.getDate() + "/" +
(date.getMonth() + 1) + "/" +
date.getFullYear() + " " +
date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
}
if (e === null || e === undefined) {
console.log("Unknown error");
} else {
console.log(msg + " : " + e);
console.log((e.stack ? e.stack : e));
}
}
}
/**
* Load image or complete the given image loading.
* @param {String|Image} ph path or image to complete loading.
* @param {Boolean} [fireErr] flag to force or preserve error firing.
* @return {zebkit.DoIt}
* @method image
* @for zebkit
*/
function image(ph, fireErr) {
if (arguments.length < 2) {
fireErr = false;
}
var doit = new DoIt(),
jn = doit.join(),
marker = "data:image";
if (isString(ph) && ph.length > marker.length) {
// use "for" instead of "indexOf === 0"
var i = 0;
for(; i < marker.length && marker[i] === ph[i]; i++) {}
if (i < marker.length) {
var file = ZFS.read(ph);
if (file !== null) {
ph = "data:image/" + file.ext + ";base64," + file.data;
}
}
}
$zenv.loadImage(ph,
function(img) {
jn(img);
},
function(img, e) {
if (fireErr === true) {
doit.error(e);
} else {
jn(img);
}
}
);
return doit;
}
// Faster match operation analogues:
// Math.floor(f) => ~~(a)
// Math.round(f) => (f + 0.5) | 0
/**
* Check if the given value is string
* @param {Object} v a value.
* @return {Boolean} true if the given value is string
* @method isString
* @for zebkit
*/
function isString(o) {
return o !== undefined && o !== null &&
(typeof o === "string" || o.constructor === String);
}
/**
* Check if the given value is number
* @param {Object} v a value.
* @return {Boolean} true if the given value is number
* @method isNumber
* @for zebkit
*/
function isNumber(o) {
return o !== undefined && o !== null &&
(typeof o === "number" || o.constructor === Number);
}
/**
* Check if the given value is boolean
* @param {Object} v a value.
* @return {Boolean} true if the given value is boolean
* @method isBoolean
* @for zebkit
*/
function isBoolean(o) {
return o !== undefined && o !== null &&
(typeof o === "boolean" || o.constructor === Boolean);
}
/**
* Test if the given value has atomic type (String, Number or Boolean).
* @param {Object} v a value
* @return {Boolean} true if the value has atomic type
* @method isAtomic
* @for zebkit
*/
function isAtomic(v) {
return v === null || v === undefined ||
(typeof v === "string" || v.constructor === String) ||
(typeof v === "number" || v.constructor === Number) ||
(typeof v === "boolean" || v.constructor === Boolean) ;
}
Number.isInteger = Number.isInteger || function(value) {
return typeof value === "number" &&
isFinite(value) &&
Math.floor(value) === value;
};
/**
* Get property value for the given object and the specified property path
* @param {Object} obj a target object.
* as the target object
* @param {String} path property path.
* @param {Boolean} [useGetter] says too try getter method when it exists.
* By default the parameter is false
* @return {Object} a property value, return undefined if property cannot
* be found
* @method getPropertyValue
* @for zebkit
*/
function getPropertyValue(obj, path, useGetter) {
// if (arguments.length < 3) {
// useGetter = false;
// }
path = path.trim();
if (path === undefined || path.length === 0) {
throw new Error("Invalid field path: '" + path + "'");
}
// if (obj === undefined || obj === null) {
// throw new Error("Undefined target object");
// }
var paths = null,
m = null,
p = null;
if (path.indexOf('.') > 0) {
paths = path.split('.');
for(var i = 0; i < paths.length; i++) {
p = paths[i];
if (obj !== undefined && obj !== null &&
((useGetter === true && (m = getPropertyGetter(obj, p))) || obj.hasOwnProperty(p)))
{
if (useGetter === true && m !== null) {
obj = m.call(obj);
} else {
obj = obj[p];
}
} else {
return undefined;
}
}
} else {
if (useGetter === true) {
m = getPropertyGetter(obj, path);
if (m !== null) {
return m.call(obj);
}
}
if (obj.hasOwnProperty(path) === true) {
obj = obj[path];
} else {
return undefined;
}
}
// detect object value factory
if (obj !== null && obj !== undefined && obj.$new !== undefined) {
return obj.$new();
} else {
return obj;
}
}
/**
* Get a property setter method if it is declared with the class of the specified object for the
* given property. Setter is a method whose name matches the following pattern: "set<PropertyName>"
* where the first letter of the property name is in upper case. For instance setter method for
* property "color" has to have name "setColor".
* @param {Object} obj an object instance
* @param {String} name a property name
* @return {Function} a method that can be used as a setter for the given property
* @method getPropertySetter
* @for zebkit
*/
function getPropertySetter(obj, name) {
var pi = obj.constructor.$propertySetterInfo,
m = null;
if (pi !== undefined) {
if (pi[name] === undefined) {
m = obj[ "set" + name[0].toUpperCase() + name.substring(1) ];
pi[name] = (typeof m === "function") ? m : null;
}
return pi[name];
} else {
// if this is not a zebkit class
m = obj[ "set" + name[0].toUpperCase() + name.substring(1) ];
return (typeof m === "function") ? m : null;
}
}
/**
* Get a property getter method if it is declared with the class of the specified object for the
* given property. Getter is a method whose name matches the following patterns: "get<PropertyName>"
* or "is<PropertyName>" where the first letter of the property name is in upper case. For instance
* getter method for property "color" has to have name "getColor".
* @param {Object} obj an object instance
* @param {String} name a property name
* @return {Function} a method that can be used as a getter for the given property
* @method getPropertyGetter
* @for zebkit
*/
function getPropertyGetter(obj, name) {
var pi = obj.constructor.$propertyGetterInfo,
m = null,
suffix = null;
if (pi !== undefined) {
if (pi[name] === undefined) {
suffix = name[0].toUpperCase() + name.substring(1);
m = obj[ "get" + suffix];
if (typeof m !== 'function') {
m = obj[ "is" + suffix];
}
pi[name] = (typeof m === "function") ? m : null;
}
return pi[name];
} else {
suffix = name[0].toUpperCase() + name.substring(1);
m = obj[ "get" + suffix];
if (typeof m !== 'function') {
m = obj[ "is" + suffix];
}
return (typeof m === 'function') ? m : null;
}
}
/**
* Populate the given target object with the properties set. The properties set
* is a dictionary that keeps properties names and its corresponding values.
* Applying of the properties to an object does the following:
*
*
* - Detects if a property setter method exits and call it to apply
* the property value. Otherwise property is initialized as a field.
* Setter method is a method that matches "set<PropertyName>" pattern.
*
* - Ignores properties whose names start from "$" character, equals "clazz"
* and properties whose values are function.
*
* - Remove properties from the target object for properties that start from "-"
* character.
*
* - Uses factory "$new" method to create a property value if the method can be
* detected in the property value.
*
* - Apply properties recursively for properties whose names end with '/'
* character.
*
*
* @param {Object} target a target object
* @param {Object} props a properties set
* @return {Object} an object with the populated properties set.
* @method properties
* @for zebkit
*/
function properties(target, props) {
for(var k in props) {
// skip private properties( properties that start from "$")
if (k !== "clazz" && k[0] !== '$' && props.hasOwnProperty(k) && props[k] !== undefined && typeof props[k] !== 'function') {
if (k[0] === '-') {
delete target[k.substring(1)];
} else {
var pv = props[k],
recursive = k[k.length - 1] === '/',
tv = null;
// value factory detected
if (pv !== null && pv.$new !== undefined) {
pv = pv.$new();
}
if (recursive === true) {
k = k.substring(0, k.length - 1);
tv = target[k];
// it is expected target value can be traversed recursively
if (pv !== null && (tv === null || tv === undefined || !(tv instanceof Object))) {
throw new Error("Target value is null, undefined or not an object. '" +
k + "' property cannot be applied as recursive");
}
} else {
tv = target[k];
}
if (recursive === true) {
if (pv === null) { // null value can be used to flush target value
target[k] = pv;
} else if (tv.properties !== undefined) {
tv.properties(pv); // target value itself has properties method
} else {
properties(tv, pv);
}
} else {
var m = getPropertySetter(target, k);
if (m === null) {
target[k] = pv; // setter doesn't exist, setup it as a field
} else {
// property setter is detected, call setter to
// set the property value
if (Array.isArray(pv)) {
m.apply(target, pv);
} else {
m.call(target, pv);
}
}
}
}
}
}
return target;
}
// ( (http) :// (host)? (:port)? (/)? )? (path)? (?query_string)?
//
// [1] scheme://host/
// [2] scheme
// [3] host
// [4] port
// [5] /
// [6] path
// [7] ?query_string
//
var $uriRE = /^(([a-zA-Z]+)\:\/\/([^\/:]+)?(\:[0-9]+)?(\/)?)?([^?]+)?(\?.+)?/;
/**
* URI class. Pass either a full uri (as a string or zebkit.URI) or number of an URI parts
* (scheme, host, etc) to construct it.
* @param {String} [uri] an URI.
* @param {String} [scheme] a scheme.
* @param {String} [host] a host.
* @param {String|Integer} [port] a port.
* @param {String} [path] a path.
* @param {String} [qs] a query string.
* @constructor
* @class zebkit.URI
*/
function URI(uri) {
if (arguments.length > 1) {
if (arguments[0] !== null) {
this.scheme = arguments[0].toLowerCase();
}
if (arguments[1] !== null) {
this.host = arguments[1];
}
var ps = false;
if (arguments.length > 2) {
if (isNumber(arguments[2])) {
this.port = arguments[2];
} else if (arguments[2] !== null) {
this.path = arguments[2];
ps = true;
}
}
if (arguments.length > 3) {
if (ps === true) {
this.qs = arguments[3];
} else {
this.path = arguments[3];
}
}
if (arguments.length > 4) {
this.qs = arguments[4];
}
} else if (uri instanceof URI) {
this.host = uri.host;
this.path = uri.path;
this.qs = uri.qs;
this.port = uri.port;
this.scheme = uri.scheme;
} else {
if (uri === null || uri.trim().length === 0) {
throw new Error("Invalid empty URI");
}
var m = uri.match($uriRE);
if (m === null) {
throw new Error("Invalid URI '" + uri + "'");
}
// fetch scheme
if (m[1] !== undefined) {
this.scheme = m[2].toLowerCase();
if (m[3] === undefined) {
if (this.scheme !== "file") {
throw new Error("Invalid host name : '" + uri + "'");
}
} else {
this.host = m[3];
}
if (m[4] !== undefined) {
this.port = parseInt(m[4].substring(1), 10);
}
}
// fetch path
if (m[6] !== undefined) {
this.path = m[6];
} else if (m[1] !== undefined) {
this.path = "/";
}
if (m[7] !== undefined && m[7].length > 1) {
this.qs = m[7].substring(1).trim();
}
}
if (this.path !== null) {
this.path = URI.normalizePath(this.path);
if ((this.host !== null || this.scheme !== null) && this.path[0] !== '/') {
this.path = "/" + this.path;
}
}
if (this.scheme !== null) {
this.scheme = this.scheme.toLowerCase();
}
if (this.host !== null) {
this.host = this.host.toLowerCase();
}
/**
* URI path.
* @attribute path
* @type {String}
* @readOnly
*/
/**
* URI host.
* @attribute host
* @type {String}
* @readOnly
*/
/**
* URI port number.
* @attribute port
* @type {Integer}
* @readOnly
*/
/**
* URI query string.
* @attribute qs
* @type {String}
* @readOnly
*/
/**
* URI scheme (e.g. 'http', 'ftp', etc).
* @attribute scheme
* @type {String}
* @readOnly
*/
}
URI.prototype = {
scheme : null,
host : null,
port : -1,
path : null,
qs : null,
/**
* Serialize URI to its string representation.
* @method toString
* @return {String} an URI as a string.
*/
toString : function() {
return (this.scheme !== null ? this.scheme + "://" : '') +
(this.host !== null ? this.host : '' ) +
(this.port !== -1 ? ":" + this.port : '' ) +
(this.path !== null ? this.path : '' ) +
(this.qs !== null ? "?" + this.qs : '' );
},
/**
* Get a parent URI.
* @method getParent
* @return {zebkit.URI} a parent URI.
*/
getParent : function() {
if (this.path === null) {
return null;
} else {
var i = this.path.lastIndexOf('/');
return (i < 0 || this.path === '/') ? null
: new URI(this.scheme,
this.host,
this.port,
this.path.substring(0, i),
this.qs);
}
},
/**
* Append the given parameters to a query string of the URI.
* @param {Object} obj a dictionary of parameters to be appended to
* the URL query string
* @method appendQS
*/
appendQS : function(obj) {
if (obj !== null) {
if (this.qs === null) {
this.qs = '';
}
if (this.qs.length > 0) {
this.qs = this.qs + "&" + URI.toQS(obj);
} else {
this.qs = URI.toQS(obj);
}
}
},
/**
* Test if the URI is absolute.
* @return {Boolean} true if the URI is absolute.
* @method isAbsolute
*/
isAbsolute : function() {
return URI.isAbsolute(this.toString());
},
/**
* Join URI with the specified path
* @param {String} p* relative paths
* @return {String} an absolute URI
* @method join
*/
join : function() {
var args = Array.prototype.slice.call(arguments);
args.splice(0, 0, this.toString());
return URI.join.apply(URI, args);
},
/**
* Test if the given URL is file path.
* @return {Boolean} true if the URL is file path
* @method isFilePath
*/
isFilePath : function() {
return this.scheme === null || this.scheme === 'file';
},
/**
* Get an URI relative to the given URI.
* @param {String|zebkit.URI} to an URI to that the relative URI has to be detected.
* @return {String} a relative URI
* @method relative
*/
relative : function(to) {
if ((to instanceof URI) === false) {
to = new URI(to);
}
if (this.isAbsolute() &&
to.isAbsolute() &&
this.host === to.host &&
this.port === to.port &&
(this.scheme === to.scheme || (this.isFilePath() && to.isFilePath()) ) &&
(this.path.indexOf(to.path) === 0 && (to.path.length === this.path.length ||
(to.path.length === 1 && to.path[0] === '/') ||
this.path[to.path.length] === '/' )))
{
return (to.path.length === 1 && to.path[0] === '/') ? this.path.substring(to.path.length)
: this.path.substring(to.path.length + 1);
} else {
return null;
}
}
};
/**
* Test if the given string is absolute path or URI.
* @param {String|zebkit.URI} u an URI
* @return {Boolean} true if the string is absolute path or URI.
* @method isAbsolute
* @static
*/
URI.isAbsolute = function(u) {
return u[0] === '/' || /^[a-zA-Z]+\:\/\//i.test(u);
};
/**
* Test if the given string is URL.
* @param {String|zebkit.URI} u a string to be checked.
* @return {Boolean} true if the string is URL
* @method isURL
* @static
*/
URI.isURL = function(u) {
return /^[a-zA-Z]+\:\/\//i.test(u);
};
/**
* Get a relative path.
* @param {String|zebkit.URI} base a base path
* @param {String|zebkit.URI} path a path
* @return {String} a relative path
* @method relative
* @static
*/
URI.relative = function(base, path) {
if ((path instanceof URI) === false) {
path = new URI(path);
}
return path.relative(base);
};
/**
* Parse the specified query string of the given URI.
* @param {String|zebkit.URI} url an URI
* @param {Boolean} [decode] pass true if query string has to be decoded.
* @return {Object} a parsed query string as a dictionary of parameters
* @method parseQS
* @static
*/
URI.parseQS = function(qs, decode) {
if (qs instanceof URI) {
qs = qs.qs;
if (qs === null) {
return null;
}
} else if (qs[0] === '?') {
qs = qs.substring(1);
}
var mqs = qs.match(/[a-zA-Z0-9_.]+=[^?&=]+/g),
parsedQS = {};
if (mqs !== null) {
for(var i = 0; i < mqs.length; i++) {
var q = mqs[i].split('='),
k = q[0].trim(),
v = decode === true ? $zenv.decodeURIComponent(q[1])
: q[1];
if (parsedQS.hasOwnProperty(k)) {
var p = parsedQS[k];
if (Array.isArray(p) === false) {
parsedQS[k] = [ p ];
}
parsedQS[k].push(v);
} else {
parsedQS[k] = v;
}
}
}
return parsedQS;
};
URI.decodeQSValue = function(value) {
if (Array.isArray(value)) {
var r = [];
for(var i = 0; i < value.length; i++) {
r[i] = URI.decodeQSValue(value[i]);
}
return r;
} else {
value = value.trim();
if (value[0] === "'") {
value = value.substring(1, value.length - 1);
} else if (value === "true" || value === "false") {
value = (value === "true");
} else if (value === "null") {
value = null;
} else if (value === "undefined") {
value = undefined;
} else {
var num = (value.indexOf('.') >= 0) ? parseFloat(value)
: parseInt(value, 10);
if (isNaN(num) === false) {
value = num;
}
}
return value;
}
};
URI.normalizePath = function(p) {
if (p !== null && p.length > 0) {
p = p.trim().replace(/[\\]+/g, '/');
for (; ; ) {
var len = p.length;
p = p.replace(/[^./]+[/]+\.\.[/]+/g, '');
p = p.replace(/[\/]+/g, '/');
if (p.length == len) {
break;
}
}
var l = p.length;
if (l > 1 && p[l - 1] === '/') {
p = p.substring(0, l - 1);
}
}
return p;
};
/**
* Convert the given dictionary of parameters to a query string.
* @param {Object} obj a dictionary of parameters
* @param {Boolean} [encode] pass true if the parameters values have to be
* encoded
* @return {String} a query string built from parameters list
* @static
* @method toQS
*/
URI.toQS = function(obj, encode) {
if (isString(obj) || isBoolean(obj) || isNumber(obj)) {
return "" + obj;
}
var p = [];
for(var k in obj) {
if (obj.hasOwnProperty(k)) {
p.push(k + '=' + (encode === true ? $zenv.encodeURIComponent(obj[k].toString())
: obj[k].toString()));
}
}
return p.join("&");
};
/**
* Join the given paths
* @param {String|zebkit.URI} p* relative paths
* @return {String} a joined path as string
* @method join
* @static
*/
URI.join = function() {
if (arguments.length === 0) {
throw new Error("No paths to join");
}
var uri = new URI(arguments[0]);
for(var i = 1; i < arguments.length; i++) {
var p = arguments[i];
if (p === null || p.length === 0) {
throw new Error("Empty sub-path is not allowed");
}
if (URI.isAbsolute(p)) {
throw new Error("Absolute path '" + p + "' cannot be joined");
}
if (p instanceof URI) {
p = arguments[i].path;
} else {
p = new URI(p).path;
}
if (p.length === 0) {
throw new Error("Empty path cannot be joined");
}
uri.path = uri.path + (uri.path === '/' ? '' : "/") + p;
}
uri.path = URI.normalizePath(uri.path);
return uri.toString();
};
$export(
URI, isNumber, isString, $Map, isAtomic,
dumpError, image, getPropertySetter,
getPropertyValue, getPropertyGetter,
properties, GET, isBoolean, DoIt,
{ "$global" : $global,
"$FN" : $FN,
"ZFS" : ZFS,
"environment": $zenv,
"isIE" : isIE,
"isFF" : isFF,
"isMacOS" : isMacOS }
);
var $$$ = 11, // hash code counter
$caller = null, // currently called method reference
$cachedO = {}, // class cache
$cachedE = [],
$cacheSize = 7777,
CNAME = '$',
CDNAME = '';
function $toString() {
return this.$hash$;
}
function $ProxyMethod(name, f, clazz) {
if (f.methodBody !== undefined) {
throw new Error("Proxy method '" + name + "' cannot be wrapped");
}
var a = function() {
var cm = $caller;
$caller = a;
// don't use finally section it is slower than try-catch
try {
var r = f.apply(this, arguments);
$caller = cm;
return r;
} catch(e) {
$caller = cm;
console.log(name + "(" + arguments.length + ") " + (e.stack ? e.stack : e));
throw e;
}
};
a.methodBody = f;
a.methodName = name;
a.boundTo = clazz;
return a;
}
/**
* Get an object by the given key from cache (and cached it if necessary)
* @param {String} key a key to an object. The key is hierarchical reference starting with the global
* name space as root. For instance "test.a" key will fetch $global.test.a object.
* @return {Object} an object
* @for zebkit
* @private
* @method $cache
*/
function $cache(key) {
if ($cachedO.hasOwnProperty(key) === true) {
// read cached entry
var e = $cachedO[key];
if (e.i < ($cachedE.length-1)) { // cached entry is not last one
// move accessed entry to the list tail to increase its access weight
var pn = $cachedE[e.i + 1];
$cachedE[e.i] = pn;
$cachedE[++e.i] = key;
$cachedO[pn].i--;
}
return e.o;
}
// don't cache global objects
if ($global.hasOwnProperty(key)) {
return $global[key];
}
var ctx = $global, i = 0, j = 0;
for( ;ctx !== null && ctx !== undefined; ) {
i = key.indexOf('.', j);
if (i < 0) {
ctx = ctx[key.substring(j, key.length)];
break;
}
ctx = ctx[key.substring(j, i)];
j = i + 1;
}
if (ctx !== null && ctx !== undefined) {
if ($cachedE.length >= $cacheSize) {
// cache is full, replace first element with the new one
var n = $cachedE[0];
$cachedE[0] = key;
$cachedO[key] = { o: ctx, i: 0 };
delete $cachedO[n];
} else {
$cachedO[key] = { o: ctx, i: $cachedE.length };
$cachedE[$cachedE.length] = key;
}
return ctx;
}
throw new Error("Reference '" + key + "' not found");
}
// copy methods from source to destination
function $cpMethods(src, dest, clazz) {
var overriddenAbstractMethods = 0;
for(var name in src) {
if (name !== CNAME &&
name !== "clazz" &&
src.hasOwnProperty(name) )
{
var method = src[name];
if (typeof method === "function" && method !== $toString) {
if (name === "$prototype") {
method.call(dest, clazz);
} else {
// TODO analyze if we overwrite existent field
if (dest[name] !== undefined) {
// abstract method is overridden, let's skip abstract method
// stub implementation
if (method.$isAbstract === true) {
overriddenAbstractMethods++;
continue;
}
if (dest[name].boundTo === clazz) {
throw new Error("Method '" + name + "(...)'' bound to this class already exists");
}
}
if (method.methodBody !== undefined) {
dest[name] = $ProxyMethod(name, method.methodBody, clazz);
} else {
dest[name] = $ProxyMethod(name, method, clazz);
}
// save information about abstract method
if (method.$isAbstract === true) {
dest[name].$isAbstract = true;
}
}
}
}
}
return overriddenAbstractMethods;
}
// return function that is meta class
// instanceOf - parent template function (can be null)
// templateConstructor - template function,
// inheritanceList - parent class and interfaces
function $make_template(instanceOf, templateConstructor, inheritanceList) {
// supply template with unique identifier that is returned with toString() method
templateConstructor.$hash$ = "$zEk$" + ($$$++);
templateConstructor.toString = $toString;
templateConstructor.prototype.clazz = templateConstructor; // instances of the template has to point to the template as a class
templateConstructor.clazz = templateConstructor.constructor = instanceOf;
/**
* Unique string hash code. The property is not defined if the class was not
* maid hashable by calling "hashable()" method.
* @attribute $hash$
* @private
* @type {String}
* @for zebkit.Class
* @readOnly
*/
/**
* Dictionary of all inherited interfaces where key is unique interface hash code and the value
* is interface itself.
* @private
* @readOnly
* @for zebkit.Class
* @type {Object}
* @attribute $parents
* @type {Object}
*/
templateConstructor.$parents = {};
// instances of the constructor also has to be unique
// so force toString method population
templateConstructor.prototype.constructor = templateConstructor; // set constructor of instances to the template
// setup parent entities
if (arguments.length > 2 && inheritanceList.length > 0) {
for(var i = 0; i < inheritanceList.length; i++) {
var toInherit = inheritanceList[i];
if (toInherit === undefined ||
toInherit === null ||
typeof toInherit !== "function" ||
toInherit.$hash$ === undefined )
{
throw new ReferenceError("Invalid parent class or interface:" + toInherit);
}
if (templateConstructor.$parents[toInherit.$hash$] !== undefined) {
var inh = '<unknown>';
// try to detect class or interface name
if (toInherit !== null && toInherit !== undefined) {
if (toInherit.$name !== null && toInherit.$name !== undefined) {
inh = toInherit.$name;
} else {
inh = toInherit;
}
}
throw Error("Duplicated inheritance: " + toInherit );
}
templateConstructor.$parents[toInherit.$hash$] = toInherit;
// if parent has own parents copy the parents references
for(var k in toInherit.$parents) {
if (templateConstructor.$parents[k] !== undefined) {
throw Error("Duplicate inherited class or interface: " + k);
}
templateConstructor.$parents[k] = toInherit.$parents[k];
}
}
}
return templateConstructor;
}
/**
* Clone the given object. The method tries to perform deep cloning by
* traversing the given object structure recursively. Any part of an
* object can be marked as not cloneable by adding "$notCloneable"
* field that equals to true. Also at any level of object structure
* the cloning can be customized with adding "$clone" method. In this
* case the method will be used to clone the part of object.
* clonable
* @param {Object} obj an object to be cloned
* @return {Object} a cloned object
* @method clone
* @for zebkit
*/
function clone(obj, map) {
// clone atomic type
// TODO: to speedup cloning we don't use isString, isNumber, isBoolean
if (obj === null || obj === undefined || obj.$notCloneable === true ||
(typeof obj === "string" || obj.constructor === String ) ||
(typeof obj === "boolean" || obj.constructor === Boolean ) ||
(typeof obj === "number" || obj.constructor === Number ) )
{
return obj;
}
map = map || new Map();
var t = map.get(obj);
if (t !== undefined) {
return t;
}
// clone with provided custom "clone" method
if (obj.$clone !== undefined) {
return obj.$clone(map);
}
// clone array
if (Array.isArray(obj)) {
var naobj = [];
map.set(obj, naobj);
map[obj] = naobj;
for(var i = 0; i < obj.length; i++) {
naobj[i] = clone(obj[i], map);
}
return naobj;
}
// clone class
if (obj.clazz === Class) {
var clazz = Class(obj, []);
clazz.inheritProperties = true;
return clazz;
}
// function cannot be cloned
if (typeof obj === 'function' || obj.constructor !== Object) {
return obj;
}
var nobj = {};
map.set(obj, nobj); // keep one instance of cloned for the same object
// clone object fields
for(var k in obj) {
if (obj.hasOwnProperty(k) === true) {
nobj[k] = clone(obj[k], map);
}
}
return nobj;
}
/**
* Instantiate a new class instance of the given class with the specified constructor
* arguments.
* @param {Function} clazz a class
* @param {Array} [args] an arguments list
* @return {Object} a new instance of the given class initialized with the specified arguments
* @method newInstance
* @for zebkit
*/
function newInstance(clazz, args) {
if (arguments.length > 1 && args.length > 0) {
var f = function () {};
f.prototype = clazz.prototype;
var o = new f();
clazz.apply(o, args);
return o;
}
return new clazz();
}
function $make_proto(props, superProto) {
if (superProto === null) {
return function $prototype(clazz) {
for(var k in props) {
if (props.hasOwnProperty(k)) {
this[k] = props[k];
}
}
};
} else {
return function $prototype(clazz) {
superProto.call(this, clazz);
for(var k in props) {
if (props.hasOwnProperty(k)) {
this[k] = props[k];
}
}
};
}
}
/**
* Interface is way to share common functionality by avoiding multiple inheritance.
* It allows developers to mix number of methods to different classes. For instance:
// declare "I" interface that contains one method a
var I = zebkit.Interface([
function a() {
}
]);
// declare "A" class
var A = zebkit.Class([]);
// declare "B" class that inherits class A and mix interface "I"
var B = zebkit.Class(A, I, []);
// instantiate "B" class
var b = new B();
zebkit.instanceOf(b, I); // true
zebkit.instanceOf(b, A); // true
zebkit.instanceOf(b, B); // true
// call mixed method
b.a();
* @return {Function} an interface
* @param {Array} [methods] list of methods declared in the interface
* @constructor
* @class zebkit.Interface
*/
var Interface = $make_template(null, function() {
var $Interface = $make_template(Interface, function() {
// Clone interface parametrized with the given properties set
if (typeof this === 'undefined' || this.constructor !== $Interface) { // means the method execution is not a result of "new" method
if (arguments.length !== 1) {
throw new Error("Invalid number of arguments. Properties set is expected");
}
if (arguments[0].constructor !== Object) {
throw new Error("Invalid argument type. Properties set is expected");
}
var iclone = $Interface.$clone();
iclone.prototype.$prototype = $make_proto(arguments[0],
$Interface.prototype.$prototype);
return iclone;
} else {
// Create a class that inherits the interface and instantiate it
if (arguments.length > 1) {
throw new Error("One or zero argument is expected");
}
return new (Class($Interface, arguments.length > 0 ? arguments[0] : []))();
}
});
if (arguments.length > 1) {
throw new Error("Invalid number of arguments. List of methods or properties is expected");
}
// abstract method counter, not used now, but can be used in the future
// to understand if the given class override all abstract methods (should be
// controlled in the places of "$cpMethods" call)
$Interface.$abstractMethods = 0;
var arg = arguments.length === 0 ? [] : arguments[0];
if (arg.constructor === Object) {
arg = [ $make_proto(arg, null) ];
} else if (Array.isArray(arg) === false) {
throw new Error("Invalid argument type. List of methods pr properties is expected");
}
if (arg.length > 0) {
var proto = $Interface.prototype,
isAbstract = false;
for(var i = 0; i < arg.length; i++) {
var method = arg[i];
if (method === "abstract") {
isAbstract = true;
} else {
if (typeof method !== "function") {
throw new Error("Method is expected instead of " + method);
}
var name = $FN(method);
if (name === CDNAME) {
throw new Error("Constructor declaration is not allowed in interface");
}
if (proto[name] !== undefined) {
throw new Error("Duplicated interface method '" + name + "(...)'");
}
if (name === "$clazz") {
method.call($Interface, $Interface);
} else if (isAbstract === true) {
(function(name) {
proto[name] = function() {
throw new Error("Abstract method '" + name + "(...)' is not implemented");
};
// mark method as abstract
proto[name].$isAbstract = true;
// count abstract methods
$Interface.$abstractMethods++;
})(name);
} else {
proto[name] = method;
}
}
}
}
/**
* Private implementation of an interface cloning.
* @return {zebkit.Interface} a clone of the interface
* @method $clone
* @private
*/
$Interface.$clone = function() {
var iclone = Interface(), k = null; // create interface
// clone interface level variables
for(k in this) {
if (this.hasOwnProperty(k)) {
iclone[k] = clone(this[k]);
}
}
// copy methods from proto
var proto = this.prototype;
for(k in proto) {
if (k !== "clazz" && proto.hasOwnProperty(k) === true) {
iclone.prototype[k] = clone(proto[k]);
}
}
return iclone;
};
$Interface.clazz.$name = "zebkit.Interface"; // assign name
return $Interface;
});
/**
* Core method method to declare a zebkit class following easy OOP approach. The easy OOP concept
* supports the following OOP features:
*
*
* __Single class inheritance.__ Any class can extend an another zebkit class
// declare class "A" that with one method "a"
var A = zebkit.Class([
function a() { ... }
]);
// declare class "B" that inherits class "A"
var B = zebkit.Class(A, []);
// instantiate class "B" and call method "a"
var b = new B();
b.a();
* __Class method overriding.__ Override a parent class method implementation
// declare class "A" that with one method "a"
var A = zebkit.Class([
function a() { ... }
]);
// declare class "B" that inherits class "A"
// and overrides method a with an own implementation
var B = zebkit.Class(A, [
function a() { ... }
]);
* __Constructors.__ Constructor is a method with empty name
// declare class "A" that with one constructor
var A = zebkit.Class([
function () { this.variable = 100; }
]);
// instantiate "A"
var a = new A();
a.variable // variable is 100
* __Static methods and variables declaration.__ Static fields and methods can be defined
by declaring special "$clazz" method whose context is set to declared class
var A = zebkit.Class([
// special method where static stuff has to be declared
function $clazz() {
// declare static field
this.staticVar = 100;
// declare static method
this.staticMethod = function() {};
}
]);
// access static field an method
A.staticVar // 100
A.staticMethod() // call static method
* __Access to super class context.__ You can call method declared in a parent class
// declare "A" class with one class method "a(p1,p2)"
var A = zebkit.Class([
function a(p1, p2) { ... }
]);
// declare "B" class that inherits "A" class and overrides "a(p1,p2)" method
var B = zebkit.Class(A, [
function a(p1, p2) {
// call "a(p1,p2)" method implemented with "A" class
this.$super(p1,p2);
}
]);
*
* One of the powerful feature of zebkit easy OOP concept is possibility to instantiate
* anonymous classes and interfaces. Anonymous class is an instance of an existing
* class that can override the original class methods with own implementations, implements
* own list of interfaces and methods. In other words the class instance customizes class
* definition for the particular instance of the class;
// declare "A" class
var A = zebkit.Class([
function a() { return 1; }
]);
// instantiate anonymous class that add an own implementation of "a" method
var a = new A([
function a() { return 2; }
]);
a.a() // return 2
* @param {zebkit.Class} [inheritedClass] an optional parent class to be inherited
* @param {zebkit.Interface} [inheritedInterfaces]* an optional list of interfaces for
* the declared class to be mixed in the class
* @param {Array} methods list of declared class methods. Can be empty array.
* @return {Function} a class definition
* @constructor
* @class zebkit.Class
*/
function $mixing(clazz, methods) {
if (Array.isArray(methods) === false) {
throw new Error("Methods array is expected (" + methods + ")");
}
var names = {};
for(var i = 0; i < methods.length; i++) {
var method = methods[i],
methodName = $FN(method);
// detect if the passed method is proxy method
if (method.methodBody !== undefined) {
throw new Error("Proxy method '" + methodName + "' cannot be mixed in a class");
}
// map user defined constructor to internal constructor name
if (methodName === CDNAME) {
methodName = CNAME;
} else if (methodName[0] === '$') {
// populate prototype fields if a special method has been defined
if (methodName === "$prototype") {
method.call(clazz.prototype, clazz);
if (clazz.prototype[CDNAME]) {
clazz.prototype[CNAME] = clazz.prototype[CDNAME];
delete clazz.prototype[CDNAME];
}
continue;
}
// populate class level fields if a special method has been defined
if (methodName === "$clazz") {
method.call(clazz);
continue;
}
}
if (names[methodName] === true) {
throw new Error("Duplicate declaration of '" + methodName+ "(...)' method");
}
var existentMethod = clazz.prototype[methodName];
if (existentMethod !== undefined && typeof existentMethod !== 'function') {
throw new Error("'" + methodName + "(...)' method clash with a field");
}
// if constructor doesn't have super definition than let's avoid proxy method
// overhead
if (existentMethod === undefined && methodName === CNAME) {
clazz.prototype[methodName] = method;
} else {
// Create and set proxy method that is bound to the given class
clazz.prototype[methodName] = $ProxyMethod(methodName, method, clazz);
}
// save method we have already added to check double declaration error
names[methodName] = true;
}
}
// Class methods to be populated in all classes
var classTemplateFields = {
/**
* Makes the class hashable. Hashable class instances are automatically
* gets unique hash code that is returned with its overridden "toString()"
* method. The hash code is stored in special "$hash$" field. The feature
* can be useful when you want to store class instances in "{}" object
* where key is the hash and the value is the instance itself.
* @method hashable
* @chainable
* @for zebkit.Class
*/
hashable : function() {
if (this.$uniqueness !== true) {
this.$uniqueness = true;
this.prototype.toString = $toString;
}
return this;
},
/**
* Makes the class hashless. Prevents generation of hash code for
* instances of the class.
* @method hashless
* @chainable
* @for zebkit.Class
*/
hashless : function() {
if (this.$uniqueness === true) {
this.$uniqueness = false;
this.prototype.toString = Object.prototype.toString;
}
return this;
},
/**
* Extend the class with new method and implemented interfaces.
* @param {zebkit.Interface} [interfaces]* number of interfaces the class has to implement.
* @param {Array} methods set of methods the given class has to be extended.
* @method extend
* @chainable
* @for zebkit.Class
*/
// add extend method later to avoid the method be inherited as a class static field
extend : function() {
var methods = arguments[arguments.length - 1],
hasMethod = Array.isArray(methods);
// inject class
if (hasMethod && this.$isExtended !== true) {
// create intermediate class
var A = this.$parent !== null ? Class(this.$parent, [])
: Class([]);
// copy this class prototypes methods to intermediate class A and re-define
// boundTo to the intermediate class A if they were bound to source class
// methods that have been moved from source class to class have to be re-bound
// to A class
for(var name in this.prototype) {
if (name !== "clazz" && this.prototype.hasOwnProperty(name) ) {
var f = this.prototype[name];
if (typeof f === 'function') {
A.prototype[name] = f.methodBody !== undefined ? $ProxyMethod(name, f.methodBody, f.boundTo)
: f;
if (A.prototype[name].boundTo === this) {
A.prototype[name].boundTo = A;
if (f.boundTo === this) {
f.boundTo = A;
}
}
}
}
}
this.$parent = A;
this.$isExtended = true;
}
if (hasMethod) {
$mixing(this, methods);
}
// add passed interfaces
for(var i = 0; i < arguments.length - (hasMethod ? 1 : 0); i++) {
var I = arguments[i];
if (I === null || I === undefined || I.clazz !== Interface) {
throw new Error("Interface is expected");
}
if (this.$parents[I.$hash$] !== undefined) {
throw new Error("Interface has been already inherited");
}
$cpMethods(I.prototype, this.prototype, this);
this.$parents[I.$hash$] = I;
}
return this;
},
/**
* Tests if the class inherits the given class or interface.
* @param {zebkit.Class | zebkit.Interface} clazz a class or interface.
* @return {Boolean} true if the class or interface is inherited with
* the class.
* @method isInherit
* @for zebkit.Class
*/
isInherit : function(clazz) {
if (this !== clazz) {
// detect class
if (clazz.clazz === this.clazz) {
for (var p = this.$parent; p !== null; p = p.$parent) {
if (p === clazz) {
return true;
}
}
} else { // detect interface
if (this.$parents[clazz.$hash$] === clazz) {
return true;
}
}
}
return false;
},
/**
* Create an instance of the class
* @param {Object} [arguments]* arguments to be passed to the class constructor
* @return {Object} an instance of the class.
* @method newInstance
* @for zebkit.Class
*/
newInstance : function() {
return arguments.length === 0 ? newInstance(this)
: newInstance(this, arguments);
},
/**
* Create an instance of the class
* @param {Array} args an arguments array
* @return {Object} an instance of the class.
* @method newInstancea
* @for zebkit.Class
*/
newInstancea : function(args) {
return arguments.length === 0 ? newInstance(this)
: newInstance(this, args);
}
};
// methods are populated in all instances of zebkit classes
var classTemplateProto = {
/**
* Extend existent class instance with the given methods and interfaces
* For example:
var A = zebkit.Class([ // declare class A that defines one "a" method
function a() {
console.log("A:a()");
}
]);
var a = new A();
a.a(); // show "A:a()" message
A.a.extend([
function b() {
console.log("EA:b()");
},
function a() { // redefine "a" method
console.log("EA:a()");
}
]);
a.b(); // show "EA:b()" message
a.a(); // show "EA:a()" message
* @param {zebkit.Interface} [interfaces]* interfaces to be implemented with the
* class instance
* @param {Array} methods list of methods the class instance has to be extended
* with
* @method extend
* @for zebkit.Class.zObject
*/
extend : function() {
var clazz = this.clazz,
l = arguments.length,
f = arguments[l - 1],
hasArray = Array.isArray(f),
i = 0;
// replace the instance class with a new intermediate class
// that inherits the replaced class. it is done to support
// $super method calls.
if (this.$isExtended !== true) {
clazz = Class(clazz, []);
this.$isExtended = true; // mark the instance as extended to avoid double extending.
clazz.$name = this.clazz.$name;
this.clazz = clazz;
}
if (hasArray) {
var init = null;
for(i = 0; i < f.length; i++) {
var n = $FN(f[i]);
if (n === CDNAME) {
init = f[i]; // postpone calling initializer before all methods will be defined
} else {
if (this[n] !== undefined && typeof this[n] !== 'function') {
throw new Error("Method '" + n + "' clash with a property");
}
this[n] = $ProxyMethod(n, f[i], clazz);
}
}
if (init !== null) {
init.call(this);
}
l--;
}
// add new interfaces if they has been passed
for (i = 0; i < arguments.length - (hasArray ? 1 : 0); i++) {
if (arguments[i].clazz !== Interface) {
throw new Error("Invalid argument " + arguments[i] + " Interface is expected.");
}
var I = arguments[i];
if (clazz.$parents[I.$hash$] !== undefined) {
throw new Error("Interface has been already inherited");
}
$cpMethods(I.prototype, this, clazz);
clazz.$parents[I.$hash$] = I;
}
return this;
},
/**
* Call super method implementation.
* @param {Function} [superMethod]? optional parameter that should be a method of the class instance
* that has to be called
* @param {Object} [args]* arguments list to pass the executed method
* @return {Object} return what super method returns
* @method $super
* @example
*
* var A = zebkit.Class([
* function a(p) { return 10 + p; }
* ]);
*
* var B = zebkit.Class(A, [
* function a(p) {
* return this.$super(p) * 10;
* }
* ]);
*
* var b = new B();
* b.a(10) // return 200
*
* @for zebkit.Class.zObject
*/
$super : function() {
if ($caller !== null) {
for (var $s = $caller.boundTo.$parent; $s !== null; $s = $s.$parent) {
var m = $s.prototype[$caller.methodName];
if (m !== undefined) {
return m.apply(this, arguments);
}
}
// handle method not found error
var cln = this.clazz && this.clazz.$name ? this.clazz.$name + "." : "";
throw new ReferenceError("Method '" +
cln +
($caller.methodName === CNAME ? "constructor"
: $caller.methodName) + "(" + arguments.length + ")" + "' not found");
} else {
throw new Error("$super is called outside of class context");
}
},
// TODO: not stable API
$supera : function(args) {
if ($caller !== null) {
for (var $s = $caller.boundTo.$parent; $s !== null; $s = $s.$parent) {
var m = $s.prototype[$caller.methodName];
if (m !== undefined) {
return m.apply(this, args);
}
}
// handle method not found error
var cln = this.clazz && this.clazz.$name ? this.clazz.$name + "." : "";
throw new ReferenceError("Method '" +
cln +
($caller.methodName === CNAME ? "constructor"
: $caller.methodName) + "(" + arguments.length + ")" + "' not found");
} else {
throw new Error("$super is called outside of class context");
}
},
// TODO: not stable API, $super that doesn't throw exception is there is no super implementation
$$super : function() {
if ($caller !== null) {
for(var $s = $caller.boundTo.$parent; $s !== null; $s = $s.$parent) {
var m = $s.prototype[$caller.methodName];
if (m !== undefined) {
return m.apply(this, arguments);
}
}
} else {
throw new Error("$super is called outside of class context");
}
},
/**
* Get a first super implementation of the given method in a parent classes hierarchy.
* @param {String} name a name of the method
* @return {Function} a super method implementation
* @method $getSuper
* @for zebkit.Class.zObject
*/
$getSuper : function(name) {
if ($caller !== null) {
for(var $s = $caller.boundTo.$parent; $s !== null; $s = $s.$parent) {
var m = $s.prototype[name];
if (typeof m === 'function') {
return m;
}
}
return null;
}
throw new Error("$super is called outside of class context");
},
$genHash : function() {
if (this.$hash$ === undefined) {
this.$hash$ = "$ZeInGen" + ($$$++);
}
return this.$hash$;
},
$clone : function(map) {
map = map || new Map();
var f = function() {};
f.prototype = this.constructor.prototype;
var nobj = new f();
map.set(this, nobj);
for(var k in this) {
if (this.hasOwnProperty(k)) {
// obj's layout is obj itself
var t = map.get(this[k]);
if (t !== undefined) {
nobj[k] = t;
} else {
nobj[k] = clone(this[k], map);
}
}
}
// speed up clearing resources
map.clear();
nobj.constructor = this.constructor;
if (nobj.$hash$ !== undefined) {
nobj.$hash$ = "$zObj_" + ($$$++);
}
nobj.clazz = this.clazz;
return nobj;
}
};
// create Class template what means we define a function (meta class) that has to be used to define
// Class. That means we define a function that returns another function that is a Class
var Class = $make_template(null, function() {
if (arguments.length === 0) {
throw new Error("No class method list was found");
}
if (Array.isArray(arguments[arguments.length - 1]) === false) {
throw new Error("No class methods have been passed");
}
if (arguments.length > 1 && typeof arguments[0] !== "function") {
throw new ReferenceError("Invalid parent class or interface '" + arguments[0] + "'");
}
var classMethods = arguments[arguments.length - 1],
parentClass = null,
toInherit = [];
// detect parent class in inheritance list as the first argument that has "clazz" set to Class
if (arguments.length > 0 && (arguments[0] === null || arguments[0].clazz === Class)) {
parentClass = arguments[0];
}
// use instead of slice for performance reason
for(var i = 0; i < arguments.length - 1; i++) {
toInherit[i] = arguments[i];
// let's make sure we inherit interface
if (parentClass === null || i > 0) {
if (toInherit[i] === undefined || toInherit[i] === null) {
throw new ReferenceError("Undefined inherited interface [" + i + "] " );
} else if (toInherit[i].clazz !== Interface) {
throw new ReferenceError("Inherited interface is not an Interface ( [" + i + "] '" + toInherit[i] + "'')");
}
}
}
// define Class (function) that has to be used to instantiate the class instance
var classTemplate = $make_template(Class, function() {
if (classTemplate.$uniqueness === true) {
this.$hash$ = "$ZkIo" + ($$$++);
}
if (arguments.length > 0) {
var a = arguments[arguments.length - 1];
// anonymous is customized class instance if last arguments is array of functions
if (Array.isArray(a) === true && typeof a[0] === 'function') {
a = a[0];
// prepare arguments list to declare an anonymous class
var args = [ classTemplate ], // first of all the class has to inherit the original class
k = arguments.length - 2;
// collect interfaces the anonymous class has to implement
for(; k >= 0 && arguments[k].clazz === Interface; k--) {
args.push(arguments[k]);
}
// add methods list
args.push(arguments[arguments.length - 1]);
var cl = Class.apply(null, args), // declare new anonymous class
// create a function to instantiate an object that will be made the
// anonymous class instance. The intermediate object is required to
// call constructor properly since we have arguments as an array
f = function() {};
cl.$name = classTemplate.$name; // the same class name for anonymous
f.prototype = cl.prototype; // the same prototypes
var o = new f();
// call constructor
// use array copy instead of cloning with slice for performance reason
// (Array.prototype.slice.call(arguments, 0, k + 1))
args = [];
for (var i = 0; i < k + 1; i++) {
args[i] = arguments[i];
}
cl.apply(o, args);
// set constructor field for consistency
o.constructor = cl;
return o;
}
}
// call class constructor
if (this.$ !== undefined) { // TODO: hard-coded constructor name to speed up
return this.$.apply(this, arguments);
}
}, toInherit);
/**
* Internal attribute that caches properties setter references.
* @attribute $propertySetterInfo
* @type {Object}
* @private
* @for zebkit.Class
* @readOnly
*/
// prepare fields that caches the class properties. existence of the property
// force getPropertySetter method to cache the method
classTemplate.$propertySetterInfo = {};
classTemplate.$propertyGetterInfo = {};
/**
* Reference to a parent class
* @attribute $parent
* @type {zebkit.Class}
* @protected
* @readOnly
*/
// copy parents prototype methods and fields into
// new class template
classTemplate.$parent = parentClass;
if (parentClass !== null) {
for(var k in parentClass.prototype) {
if (parentClass.prototype.hasOwnProperty(k)) {
var f = parentClass.prototype[k];
classTemplate.prototype[k] = (f !== undefined &&
f !== null &&
f.hasOwnProperty("methodBody")) ? $ProxyMethod(f.methodName, f.methodBody, f.boundTo)
: f;
}
}
}
/**
* The instance class.
* @attribute clazz
* @type {zebkit.Class}
*/
classTemplate.prototype.clazz = classTemplate;
// check if the method has been already defined in the class
if (classTemplate.prototype.properties === undefined) {
classTemplate.prototype.properties = function(p) {
return properties(this, p);
};
}
// populate class template prototype methods and fields
for(var ptf in classTemplateProto) {
classTemplate.prototype[ptf] = classTemplateProto[ptf];
}
// copy methods from interfaces before mixing class methods
if (toInherit.length > 0) {
for(var idx = toInherit[0].clazz === Interface ? 0 : 1; idx < toInherit.length; idx++) {
var ic = toInherit[idx];
$cpMethods(ic.prototype, classTemplate.prototype, classTemplate);
// copy static fields from interface to the class
for(var sk in ic) {
if (sk[0] !== '$' &&
ic.hasOwnProperty(sk) === true &&
classTemplate.hasOwnProperty(sk) === false)
{
classTemplate[sk] = clone(ic[sk]);
}
}
}
}
// initialize uniqueness field with false
classTemplate.$uniqueness = false;
// inherit static fields from parent class
if (parentClass !== null) {
for (var key in parentClass) {
if (key[0] !== '$' &&
parentClass.hasOwnProperty(key) &&
classTemplate.hasOwnProperty(key) === false)
{
classTemplate[key] = clone(parentClass[key]);
}
}
// inherit uni
if (parentClass.$uniqueness === true) {
classTemplate.hashable();
}
}
// add class declared methods after the previous step to get a chance to
// overwrite class level definitions
$mixing(classTemplate, classMethods);
// populate class level methods and fields into class template
for (var tf in classTemplateFields) {
classTemplate[tf] = classTemplateFields[tf];
}
// assign proper name to class
classTemplate.clazz.$name = "zebkit.Class";
// copy methods from interfaces
if (toInherit.length > 0) {
// notify inherited class and interfaces that they have been inherited with the given class
for(var j = 0; j < toInherit.length; j++) {
if (typeof toInherit[j].inheritedWidth === 'function') {
toInherit[j].inheritedWidth(classTemplate);
}
}
}
return classTemplate;
});
/**
* Get class by the given class name
* @param {String} name a class name
* @return {Function} a class. Throws exception if the class cannot be
* resolved by the given class name
* @method forName
* @throws Error
* @for zebkit.Class
*/
Class.forName = function(name) {
return $cache(name);
};
/**
* Test if the given object is instance of the specified class or interface. It is preferable
* to use this method instead of JavaScript "instanceof" operator whenever you are dealing with
* zebkit classes and interfaces.
* @param {Object} obj an object to be evaluated
* @param {Function} clazz a class or interface
* @return {Boolean} true if a passed object is instance of the given class or interface
* @method instanceOf
* @for zebkit
*/
function instanceOf(obj, clazz) {
if (clazz !== null && clazz !== undefined) {
if (obj === null || obj === undefined) {
return false;
} else if (obj.clazz === undefined) {
return (obj instanceof clazz);
} else {
return obj.clazz !== null &&
(obj.clazz === clazz ||
obj.clazz.$parents[clazz.$hash$] !== undefined);
}
}
throw new Error("instanceOf(): null class");
}
/**
* Dummy class that implements nothing but can be useful to instantiate
* anonymous classes with some on "the fly" functionality:
*
* // instantiate and use zebkit class with method "a()" implemented
* var ac = new zebkit.Dummy([
* function a() {
* ...
* }
* ]);
*
* // use it
* ac.a();
*
* @constructor
* @class zebkit.Dummy
*/
var Dummy = Class([]);
$export(clone, instanceOf, newInstance,
{ "Class": Class, "Interface" : Interface, "Dummy": Dummy, "CDNAME": CDNAME, "CNAME" : CNAME });
/**
* JSON object loader class is a handy way to load hierarchy of objects encoded with
* JSON format. The class supports standard JSON types plus it extends JSON with a number of
* features that helps to make object creation more flexible. Zson allows developers
* to describe creation of any type of object. For instance if you have a class "ABC" with
* properties "prop1", "prop2", "prop3" you can use instance of the class as a value of
* a JSON property as follow:
*
* { "instanceOfABC": {
* "@ABC" : [],
* "prop1" : "property 1 value",
* "prop2" : true,
* "prop3" : 200
* }
* }
*
* And than:
*
* // load JSON mentioned above
* zebkit.Zson.then("abc.json", function(zson) {
* zson.get("instanceOfABC");
* });
*
* Features the JSON zson supports are listed below:
*
* - **Access to hierarchical properties** You can use dot notation to get a property value. For
* instance:
*
* { "a" : {
* "b" : {
* "c" : 100
* }
* }
* }
*
* zebkit.Zson.then("abc.json", function(zson) {
* zson.get("a.b.c"); // 100
* });
*
*
* - **Property reference** Every string JSON value that starts from "@" considers as reference to
* another property value in the given JSON.
*
* { "a" : 100,
* "b" : {
* "c" : "%{a.b}"
* }
* }
*
* here property "b.c" equals to 100 since it refers to property "a.b"
* *
* - **Class instantiation** Property can be easily initialized with an instantiation of required class. JSON
* zson considers all properties whose name starts from "@" character as a class name that has to be instantiated:
*
* { "date": {
* { "@Date" : [] }
* }
* }
*
* Here property "date" is set to instance of JS Date class.
*
* - **Factory classes** JSON zson follows special pattern to describe special type of property whose value
* is re-instantiated every time the property is requested. Definition of the property value is the same
* to class instantiation, but the name of class has to prefixed with "*" character:
*
*
* { "date" : {
* "@ *Date" : []
* }
* }
*
*
* Here, every time you call get("date") method a new instance of JS date object will be returned. So
* every time will have current time.
*
* - **JS Object initialization** If you have an object in your code you can easily fulfill properties of the
* object with JSON zson. For instance you can create zebkit UI panel and adjust its background, border and so on
* with what is stored in JSON:
*
*
* {
* "background": "red",
* "borderLayout": 0,
* "border" : { "@zebkit.draw.RoundBorder": [ "black", 2 ] }
* }
*
* var pan = new zebkit.ui.Panel();
* new zebkit.Zson(pan).then("pan.json", function(zson) {
* // loaded and fulfill panel
* ...
* });
*
*
* - **Expression** You can evaluate expression as a property value:
*
*
* {
* "a": { ".expr": "100*10" }
* }
*
*
* Here property "a" equals 1000
*
*
* - **Load external resources** You can combine Zson from another Zson:
*
*
* {
* "a": "%{<json> embedded.json}",
* "b": 100
* }
*
*
* Here property "a" is loaded with properties set with loading external "embedded.json" file
*
* @class zebkit.Zson
* @constructor
* @param {Object} [obj] a root object to be loaded with
* the given JSON configuration
*/
var Zson = Class([
function (root) {
if (arguments.length > 0) {
this.root = root;
}
/**
* Map of aliases and appropriate classes
* @attribute classAliases
* @protected
* @type {Object}
* @default {}
*/
this.classAliases = {};
},
function $clazz() {
/**
* Build zson from the given json file
* @param {String|Object} json a JSON or path to JSOn file
* @param {Object} [root] an object to be filled with the given JSON
* @param {Function} [cb] a callback function to catch the JSON loading is
* completed
* @return {zebkit.DoIt} a promise to catch result
* @method then
* @static
*/
this.then = function(json, root, cb) {
if (typeof root === 'function') {
cb = root;
root = null;
}
var zson = arguments.length > 1 && root !== null ? new Zson(root)
: new Zson();
if (typeof cb === 'function') {
return zson.then(json, cb);
} else {
return zson.then(json);
}
};
},
function $prototype() {
/**
* URL the JSON has been loaded from
* @attribute uri
* @type {zebkit.URI}
* @default null
*/
this.uri = null;
/**
* Object that keeps loaded and resolved content of a JSON
* @readOnly
* @attribute root
* @type {Object}
* @default {}
*/
this.root = null;
/**
* Original JSON as a JS object
* @attribute content
* @protected
* @type {Object}
* @default null
*/
this.content = null;
/**
* The property says if the object introspection is required to try find a setter
* method for the given key. For instance if an object is loaded with the
* following JSON:
{
"color": "red"
}
* the introspection will cause zson class to try finding "setColor(c)" method in
* the loaded with the JSON object and call it to set "red" property value.
* @attribute usePropertySetters
* @default true
* @type {Boolean}
*/
this.usePropertySetters = true;
/**
* Cache busting flag.
* @attribute cacheBusting
* @type {Boolean}
* @default false
*/
this.cacheBusting = false;
/**
* Internal variables set
* @attribute $variables
* @protected
* @type {Object}
*/
this.$variables = null;
/**
* Base URI to be used to build paths to external resources. The path is
* used for references that occur in zson.
* @type {String}
* @attribute baseUri
* @default null
*/
this.baseUri = null;
/**
* Get a property value by the given key. The property name can point to embedded fields:
*
* new zebkit.Zson().then("my.json", function(zson) {
* zson.get("a.b.c");
* });
*
*
* @param {String} key a property key.
* @return {Object} a property value
* @throws Error if property cannot be found and it doesn't start with "?"
* @method get
*/
this.get = function(key) {
if (key === null || key === undefined) {
throw new Error("Null key");
}
var ignore = false;
if (key[0] === '?') {
key = key.substring(1).trim();
ignore = true;
}
if (ignore) {
try {
return getPropertyValue(this.root, key);
} catch(e) {
if ((e instanceof ReferenceError) === false) {
throw e;
}
}
} else {
return getPropertyValue(this.root, key);
}
};
/**
* Call the given method defined with the Zson class instance and
* pass the given arguments to the method.
* @param {String} name a method name
* @param {Object} d arguments
* @return {Object} a method execution result
* @method callMethod
*/
this.callMethod = function(name, d) {
var m = this[name.substring(1).trim()],
ts = this.$runner.$tasks.length,
bs = this.$runner.$busy;
if (typeof m !== 'function') {
throw new Error("Method '" + name + "' cannot be found");
}
var args = this.buildValue(Array.isArray(d) ? d
: [ d ]),
$this = this;
if (this.$runner.$tasks.length === ts &&
this.$runner.$busy === bs )
{
var res = m.apply(this, args);
if (res instanceof DoIt) {
return new DoIt().till(this.$runner).then(function() {
var jn = this.join();
res.then(function(res) {
jn(res);
return res;
}).then(function(res) {
return res;
});
}).catch(function(e) {
$this.$runner.error(e);
});
} else {
return res;
}
} else {
return new DoIt().till(this.$runner).then(function() {
if (args instanceof DoIt) {
var jn = this.join();
args.then(function(res) {
jn(res);
return res;
});
} else {
return args;
}
}).then(function(args) {
var res = m.apply($this, args);
if (res instanceof DoIt) {
var jn = this.join();
res.then(function(res) {
jn(res);
return res;
});
} else {
return res;
}
}).then(function(res) {
return res;
}).catch(function(e) {
$this.$runner.error(e);
});
}
};
this.$resolveRef = function(target, names) {
var fn = function(ref, rn) {
rn.then(function(target) {
if (target !== null && target !== undefined && target.hasOwnProperty(ref) === true) {
var v = target[ref];
if (v instanceof DoIt) {
var jn = this.join();
v.then(function(res) {
jn.call(rn, res);
return res;
});
} else {
return v;
}
} else {
return undefined;
}
});
};
for (var j = 0; j < names.length; j++) {
var ref = names[j];
if (target.hasOwnProperty(ref)) {
var v = target[ref];
if (v instanceof DoIt) {
var rn = new DoIt(),
trigger = rn.join();
for(var k = j; k < names.length; k++) {
fn(names[k], rn);
}
trigger.call(rn, target);
return rn;
} else {
target = target[ref];
}
} else {
return undefined;
}
}
return target;
};
this.$buildArray = function(d) {
var hasAsync = false;
for (var i = 0; i < d.length; i++) {
var v = this.buildValue(d[i]);
if (v instanceof DoIt) {
hasAsync = true;
this.$assignValue(d, i, v);
} else {
d[i] = v;
}
}
if (hasAsync) {
return new DoIt().till(this.$runner).then(function() {
return d;
});
} else {
return d;
}
};
/**
* Build a class instance.
* @param {String} classname a class name
* @param {Array|null|Object} args a class constructor arguments
* @param {Object} props properties to be applied to class instance
* @return {Object|zebkit.DoIt}
* @method $buildClass
* @private
*/
this.$buildClass = function(classname, args, props) {
var clz = null,
busy = this.$runner.$busy,
tasks = this.$runner.$tasks.length;
classname = classname.trim();
// '?' means optional class instance.
if (classname[0] === '?') {
classname = classname.substring(1).trim();
try {
clz = this.resolveClass(classname[0] === '*' ? classname.substring(1).trim()
: classname);
} catch (e) {
return null;
}
} else {
clz = this.resolveClass(classname[0] === '*' ? classname.substring(1).trim()
: classname);
}
args = this.buildValue(Array.isArray(args) ? args
: [ args ]);
if (classname[0] === '*') {
return (function(clazz, args) {
return {
$new : function() {
return newInstance(clazz, args);
}
};
})(clz, args);
}
var props = this.buildValue(props);
// let's do optimization to avoid unnecessary overhead
// equality means nor arguments neither properties has got async call
if (this.$runner.$busy === busy && this.$runner.$tasks.length === tasks) {
var inst = newInstance(clz, args);
this.merge(inst, props, true);
return inst;
} else {
var $this = this;
return new DoIt().till(this.$runner).then(function() {
var jn1 = this.join(), // create all join here to avoid result overwriting
jn2 = this.join();
if (args instanceof DoIt) {
args.then(function(res) {
jn1(res);
return res;
});
} else {
jn1(args);
}
if (props instanceof DoIt) {
props.then(function(res) {
jn2(res);
return res;
});
} else {
jn2(props);
}
}).then(function(args, props) {
var inst = newInstance(clz, args);
$this.merge(inst, props, true);
return inst;
});
}
};
this.$qsToVars = function(uri) {
var qs = null,
vars = null;
if ((uri instanceof URI) === false) {
qs = new URI(uri.toString()).qs;
} else {
qs = uri.qs;
}
if (qs !== null || qs === undefined) {
qs = URI.parseQS(qs);
for(var k in qs) {
if (vars === null) {
vars = {};
}
vars[k] = URI.decodeQSValue(qs[k]);
}
}
return vars;
};
this.$buildRef = function(d) {
var idx = -1;
if (d[2] === "<" || d[2] === '.' || d[2] === '/') { //TODO: not complete solution that cannot detect URLs
var path = null,
type = null,
$this = this;
if (d[2] === '<') {
// if the referenced path is not absolute path and the zson has been also
// loaded by an URL than build the full URL as a relative path from
// BAG URL
idx = d.indexOf('>');
if (idx <= 4) {
throw new Error("Invalid content type in URL '" + d + "'");
}
path = d.substring(idx + 1, d.length - 1).trim();
type = d.substring(3, idx).trim();
} else {
path = d.substring(2, d.length - 1).trim();
type = "json";
}
if (type === 'js') {
return this.expr(path);
}
if (URI.isAbsolute(path) === false) {
if (this.baseUri !== null) {
path = URI.join(this.baseUri, path);
} else if (this.uri !== null) {
var pURL = new URI(this.uri).getParent();
if (pURL !== null) {
path = URI.join(pURL, path);
}
}
}
if (type === "json") {
var bag = new this.clazz();
bag.usePropertySetters = this.usePropertySetters;
bag.$variables = this.$qsToVars(path);
bag.cacheBusting = this.cacheBusting;
var bg = bag.then(path).catch();
this.$runner.then(bg.then(function(res) {
return res.root;
}));
return bg;
} else if (type === 'img') {
if (this.uri !== null && URI.isAbsolute(path) === false) {
path = URI.join(new URI(this.uri).getParent(), path);
}
return image(path, false);
} else if (type === 'txt') {
return new ZFS.GET(path).then(function(r) {
return r.responseText;
}).catch(function(e) {
$this.$runner.error(e);
});
} else {
throw new Error("Invalid content type " + type);
}
} else {
// ? means don't throw exception if reference cannot be resolved
idx = 2;
if (d[2] === '?') {
idx++;
}
var name = d.substring(idx, d.length - 1).trim(),
names = name.split('.'),
targets = [ this.$variables, this.content, this.root, $global];
for(var i = 0; i < targets.length; i++) {
var target = targets[i];
if (target !== null) {
var value = this.$resolveRef(target, names);
if (value !== undefined) {
return value;
}
}
}
if (idx === 2) {
throw new Error("Reference '" + name + "' cannot be resolved");
} else {
return d;
}
}
};
/**
* Build a value by the given JSON description
* @param {Object} d a JSON description
* @return {Object} a value
* @protected
* @method buildValue
*/
this.buildValue = function(d) {
if (d === undefined || d === null || d instanceof DoIt ||
(typeof d === "number" || d.constructor === Number) ||
(typeof d === "boolean" || d.constructor === Boolean) )
{
return d;
}
if (Array.isArray(d)) {
return this.$buildArray(d);
}
if (typeof d === "string" || d.constructor === String) {
if (d[0] === '%' && d[1] === '{' && d[d.length - 1] === '}') {
return this.$buildRef(d);
} else {
return d;
}
}
var k = null;
if (d.hasOwnProperty("class") === true) {
k = d["class"];
delete d["class"];
if (isString(k) === false) {
var kk = null;
for (kk in k) {
return this.$buildClass(kk, k[kk], d);
}
}
return this.$buildClass(k, [], d);
}
// test whether we have a class definition
for (k in d) {
// handle class definition
if (k[0] === '@' && d.hasOwnProperty(k) === true) {
var args = d[k];
delete d[k]; // delete class name
return this.$buildClass(k.substring(1), args, d);
}
//!!!! trust the name of class occurs first what in general
// cannot be guaranteed by JSON spec but we can trust
// since many other third party applications stands
// on it too :)
break;
}
for (k in d) {
if (d.hasOwnProperty(k)) {
var v = d[k];
// special field name that says to call method to create a
// value by the given description
if (k[0] === "." || k[0] === '#') {
delete d[k];
if (k[0] === '#') {
this.callMethod(k, v);
} else {
return this.callMethod(k, v);
}
} else if (k[0] === '%') {
delete d[k];
this.mixin(d, this.$buildRef(k));
} else {
this.$assignValue(d, k, this.buildValue(v));
}
}
}
return d;
};
this.$assignValue = function(o, k, v) {
o[k] = v;
if (v instanceof DoIt) {
this.$runner.then(v.then(function(res) {
o[k] = res;
return res;
}));
}
};
this.$assignProperty = function(o, m, v) {
// setter has to be placed in queue to let
// value resolves its DoIts
this.$runner.then(function(res) {
if (Array.isArray(v)) {
m.apply(o, v);
} else {
m.call (o, v);
}
return res;
});
};
/**
* Merge values of the given destination object with the values of
* the specified source object.
* @param {Object} dest a destination object
* @param {Object} src a source object
* @param {Boolean} [recursively] flag that indicates if the complex
* properties of destination object has to be traversing recursively.
* By default the flag is true. The destination property value is
* considered not traversable if its class defines "mergeable" property
* that is set top true.
* @return {Object} a merged destination object.
* @protected
* @method merge
*/
this.merge = function(dest, src, recursively) {
if (arguments.length < 3) {
recursively = true;
}
for (var k in src) {
if (src.hasOwnProperty(k)) {
var sv = src [k],
dv = dest[k];
if (this.usePropertySetters === true) {
var m = getPropertySetter(dest, k);
if (m !== null) {
this.$assignProperty(dest, m, sv);
continue;
}
}
if (isAtomic(dv) || Array.isArray(dv) ||
isAtomic(sv) || Array.isArray(sv) ||
sv.clazz !== undefined )
{
this.$assignValue(dest, k, sv);
} else if (recursively === true) {
if (dv !== null && dv !== undefined && dv.clazz !== undefined && dv.clazz.mergeable === false) {
this.$assignValue(dest, k, sv);
} else {
this.merge(dv, sv);
}
}
}
}
return dest;
};
this.mixin = function(dest, src) {
if (src instanceof DoIt) {
var $this = this;
this.$runner.then(src.then(function(src) {
for (var k in src) {
if (src.hasOwnProperty(k) && (dest[k] === undefined || dest[k] === null)) {
$this.$assignValue(dest, k, src[k]);
}
}
}));
} else {
for (var k in src) {
if (src.hasOwnProperty(k) && (dest[k] === undefined || dest[k] === null)) {
this.$assignValue(dest, k, src[k]);
}
}
}
};
/**
* Called every time the given class name has to be transformed into
* the class object (constructor) reference. The method checks if the given class name
* is alias that is mapped with the zson to a class.
* @param {String} className a class name
* @return {Function} a class reference
* @method resolveClass
* @protected
*/
this.resolveClass = function(className) {
return this.classAliases.hasOwnProperty(className) ? this.classAliases[className]
: Class.forName(className);
};
/**
* Adds class aliases
* @param {Object} aliases dictionary where key is a class alias that can be referenced
* from JSON and the value is class itself (constructor)
* @method addClassAliases
*/
this.addClassAliases = function(aliases) {
for(var k in aliases) {
this.classAliases[k] = Class.forName(aliases[k].trim());
}
};
this.expr = function(expr) {
if (expr.length > 300) {
throw new Error("Out of evaluated script limit");
}
return eval("'use strict';" + expr);
};
/**
* Load and parse the given JSON content.
* @param {String|Object} json a JSON content. It can be:
* - **String**
* - JSON string
* - URL to a JSON
* - **Object** JavaScript object
* @return {zebkit.DoIt} a reference to the runner
* @method then
* @example
*
* // load JSON in zson from a remote site asynchronously
* new zebkit.Zson().then("http://test.com/test.json", function(zson) {
* // zson is loaded and ready for use
* zson.get("a.c");
* }
* ).catch(function(error) {
* // handle error
* ...
* });
*/
this.then = function(json, fn) {
if (json === null || json === undefined || (isString(json) && json.trim().length === 0)) {
throw new Error("Null content");
}
this.$runner = new DoIt();
var $this = this;
this.$runner.then(function() {
if (isString(json)) {
json = json.trim();
// detect if the passed string is not a JSON, but URL
if ((json[0] !== '[' || json[json.length - 1] !== ']') &&
(json[0] !== '{' || json[json.length - 1] !== '}') )
{
$this.$variables = $this.$qsToVars(json);
$this.uri = json;
if ($this.cacheBusting === false) {
$this.uri = $this.uri + (json.lastIndexOf("?") > 0 ? "&" : "?") + (new Date()).getTime().toString();
}
var join = this.join();
ZFS.GET($this.uri).then(function(r) {
join.call($this, r.responseText);
}).catch(function(e) {
$this.$runner.error(e);
});
} else {
return json;
}
} else {
return json;
}
}).then(function(json) { // populate JSON content
if (isString(json)) {
try {
if ($this.uri !== null && typeof jsyaml !== 'undefined') {
var uri = new URI($this.uri);
if (uri.path !== null && uri.path.toLowerCase().indexOf(".yaml") === uri.path.length - 5) {
$this.content = jsyaml.load(json.trim());
}
}
if ($this.content === null) {
$this.content = $zenv.parseJSON(json.trim());
}
} catch(e) {
throw new Error("JSON format error: " + e);
}
} else {
$this.content = json;
}
$this.$assignValue($this, "content", $this.buildValue($this.content));
}).then(function() {
if ($this.root !== null) {
$this.merge($this.root, $this.content);
} else {
$this.root = $this.content;
}
return $this;
});
if (typeof $this.completed === 'function') {
this.$runner.then(function() {
$this.completed.call($this);
return $this;
});
}
if (arguments.length > 1) {
this.$runner.then(fn);
}
return this.$runner;
};
}
]);
$export({ "Zson" : Zson } );
/**
* Finds an item by xpath-like simplified expression applied to a tree-like structure.
* Passed tree-like structure doesn't have a special requirements except every item of
* the structure have to define its kids by exposing "kids" field. The field is array
* of children elements:
*
* // example of tree-like structure
* var treeLikeRoot = {
* value : "Root",
* kids : [
* { value: "Item 1" },
* { value: "Item 2" }
* ]
* };
*
* zebkit.findInTree(treeLikeRoot,
* "/item1",
* function(foundElement) {
* ...
* // returning true means stop lookup
* return true;
* },
* function(item, fragment) {
* return item.value === fragment;
* });
*
*
* The find method traverse the tree-like structure according to the xpath-like
* expression. To understand if the given tree item confronts with the currently
* traversing path fragment a special equality method has to be passed. The method
* gets the traversing tree item and a string path fragment. The method has to
* decide if the given tree item complies the specified path fragment.
*
* @param {Object} root a tree root element. If the element has a children elements
* the children have to be stored in "kids" field as an array.
* @param {String} path a path-like expression. The path has to satisfy number of
* requirements:
*
* - has to start with "." or "/" or "//" character
* - has to define path part after "/" or "//"
* - path part can be either "*" or a name
* - the last path that starts from '@' character is considered as an attribute
* value requester In this case an attribute value will be returned.
* - optionally an attribute or/and its value can be defined as "[@<attr_name>=<attr_value>]"
* - attribute value is optional and can be boolean (true or false), integer, null
* or string value
* - string attribute value has to be wrapped with single quotes
*
*
* For examples:
*
* - "//*" traverse all tree elements
* - "//*[@a=10]" traverse all tree elements that has an attribute "a" that equals 10
* - "//*[@a]" traverse all tree elements that has an attribute "a" defined
* - "/Item1/Item2" find an element by exact path
* - ".//" traverse all tree elements including the root element
* - "./Item1/@k" value of property 'k' for a tree node found with "./Item1" path
*
* @param {Function} cb callback function that is called every time a new tree element
* matches the given path fragment. The function has to return true if the tree look up
* has to be interrupted
* @param {Function} [eq] an equality function. The function gets current evaluated
* tree element and a path fragment against which the tree element has to be evaluated.
* It is expected the method returns boolean value to say if the given passed tree
* element matches the path fragment. If the parameter is not passed or null then default
* equality method is used. The default method expects a tree item has "path" field that
* is matched with given path fragment.
* @method findInTree
* @for zebkit
*/
var PATH_RE = /^[.]?(\/[\/]?)([^\[\/]+)(\[\s*\@([a-zA-Z_][a-zA-Z0-9_\.]*)\s*(\=\s*[0-9]+|\=\s*true|\=\s*false|\=\s*null|\=\s*\'[^']*\')?\s*\])?/,
DEF_EQ = function(n, fragment) { return n.value === fragment; };
function findInTree(root, path, cb, eq) {
if (root === null || root === undefined) {
throw new Error("Null tree root");
}
path = path.trim();
if (path[0] === '#') { // id shortcut
path = "//*[@id='" + path.substring(1).trim() + "']";
} else if (path === '.') { // current node shortcut
return cb.call(root, root);
} else if (path[0] === '.' && path[1] === '/') { // means we have to include root in search
if (path[2] !== '@') {
root = { kids: [ root ] };
}
path = path.substring(1);
}
// no match method has been defined, let use default method
// to match the given node to the current path fragment
if (eq === null || arguments.length < 4) { // check null first for perf.
eq = DEF_EQ;
}
return $findInTree(root, path, cb, eq, null);
}
function $findInTree(root, path, cb, eq, m) {
if (path[0] === '/' && path[1] === '/' && path[2] === '@') {
path = "//*" + path.substring(1);
}
var pathValue,
pv = undefined,
isTerminal = false;
if (path[0] === '/' && path[1] === '@') {
if (m === null || m[0].length !== m.input.length) {
m = path.match(PATH_RE);
if (m === null) {
throw new Error("Cannot resolve path '" + path + "'");
}
// check if the matched path is not terminal
if (m[0].length !== path.length) {
path = path.substring(m[0].length); // cut found fragment from the path
}
}
pathValue = m[2].trim();
if (pathValue[1] === '{') {
if (pathValue[pathValue.length - 1] !== '}') {
throw new Error("Invalid properties aggregation expression '" + pathValue + "'");
}
pv = {};
var names = pathValue.substring(2, pathValue.length - 1).split(',');
for (var ni = 0; ni < names.length; ni++) {
var name = names[ni].trim();
pv[name] = getPropertyValue(root, name, true);
}
} else {
pv = getPropertyValue(root, pathValue.substring(1), true);
}
if (m[0].length === m.input.length) { // terminal path
if (pv !== undefined && cb.call(root, pv) === true) {
return true;
}
} else {
if (isAtomic(pv)) {
throw new Error("Atomic typed node cannot be traversed");
} else if (pv !== null && pv !== undefined) {
if ($findInTree(pv, path, cb, eq, m) === true) {
return true;
}
}
}
} else if (root.kids !== undefined && // a node has children
root.kids !== null &&
root.kids.length > 0 ) {
var ppath = path;
//
// m == null : means this is the first call of the method
// m[0].length !== m.input.length : means this is terminal part of the path
//
if (m === null || m[0].length !== m.input.length) {
m = path.match(PATH_RE);
if (m === null) {
throw new Error("Cannot resolve path '" + path + "'");
}
// check if the matched path is not terminal
if (m[0].length !== path.length) {
path = path.substring(m[0].length); // cut found fragment from the path
}
// normalize attribute value
if (m[3] !== undefined && m[5] !== undefined) {
m[5] = m[5].substring(1).trim();
if (m[5][0] === "'") {
m[5] = m[5].substring(1, m[5].length - 1);
} else if (m[5] === "true") {
m[5] = true;
} else if (m[5] === "false") {
m[5] = false;
} else if (m[5] === "null") {
m[5] = null;
} else {
var vv = parseInt(m[5], 10);
if (isNaN(vv) === false) {
m[5] = vv;
}
}
}
}
if (m[0].length === m.input.length) {
isTerminal = true;
}
pathValue = m[2].trim();
// traverse root kid nodes
for (var i = 0; i < root.kids.length ; i++) {
var kid = root.kids[i],
isMatch = false;
// XOR
if (pathValue === "*" || (eq(kid, pathValue) ? pathValue[0] !== '!' : pathValue[0] === '!') === true) {
if (m[3] !== undefined) { // has attributes
var attrName = m[4].trim();
// leave if attribute doesn't match
if (kid[attrName] !== undefined && (m[5] === undefined || kid[attrName] === m[5])) {
isMatch = true;
}
} else {
isMatch = true;
}
}
if (isTerminal === true) {
// node match then call callback and leave if the callback says to do it
if (isMatch === true) {
if (cb.call(root, kid) === true) {
return true;
}
}
if (m[1] === "//") {
if ($findInTree(kid, path, cb, eq, m) === true) {
return true;
}
}
} else {
// not a terminal and match, then traverse kid
if (isMatch === true) {
if ($findInTree(kid, path, cb, eq, m) === true) {
return true;
}
}
// not a terminal and recursive traversing then do it
// with previous path
if (m[1] === "//") {
if ($findInTree(kid, ppath, cb, eq, m) === true) {
return true;
}
}
}
}
}
return false;
}
/**
* Interface that provides path search functionality for a tree-like structure.
* @class zebkit.PathSearch
* @interface zebkit.PathSearch
*/
var PathSearch = Interface([
function $prototype() {
/**
* Method to match two element in tree.
* @protected
* @attribute $matchPath
* @type {Function}
*/
this.$matchPath = null;
/**
* Find children items or values with the passed path expression.
* @param {String} path path expression. Path expression is simplified form
* of XPath-like expression. See {{#crossLink "findInTree"}}findInTree{{/crossLink}}
* method to get more details.
*
* @param {Function} [cb] function that is called every time a new children
* component has been found. If callback has not been passed then the method
* return first found item or null. If the callback has been passed as null
* then all found elements will be returned as array.
* @method byPath
* @return {Object} found children item/property value or null if no children
* items were found
*/
this.byPath = function(path, cb) {
if (arguments.length === 2) {
if (arguments[1] === null) {
var r = [];
findInTree(this, path, function(n) {
r.push(n);
return false;
}, this.$matchPath !== null ? this.$matchPath
: null);
return r;
} else {
findInTree(this, path, cb, this.$matchPath !== null ? this.$matchPath
: null);
}
} else {
var res = null;
findInTree(this, path, function(n) {
res = n;
return true;
}, this.$matchPath !== null ? this.$matchPath : null);
return res;
}
};
}
]);
$export(findInTree, { "PathSearch": PathSearch } );
/**
* Abstract event class.
* @class zebkit.Event
* @constructor
*/
var Event = Class([
function $prototype() {
/**
* Source of an event
* @attribute source
* @type {Object}
* @default null
* @readOnly
*/
this.source = null;
}
]);
/**
* This method allows to declare a listeners container class for the given
* dedicated event types.
*
* // create listener container to keep three different events
* // handlers
* var MyListenerContainerClass = zebkit.ListenersClass("event1",
* "event2",
* "event3");
* // instantiate listener class container
* var listeners = new MyListenerContainerClass();
*
* // add "event1" listener
* listeners.add(function event1() {
* ...
* });
*
* // add "event2" listener
* listeners.add(function event2() {
* ...
* });
*
* // add listener for both event1 and event2 events
* listeners.add(function() {
* ...
* });
*
* // and firing event1 to registered handlers
* listeners.event1(...);
*
* // and firing event2 to registered handlers
* listeners.event2(...);
*
* @for zebkit
* @method ListenersClass
* @param {String} [events]* events types the listeners container has to support
* @return {zebkit.Listener} a listener container class
*/
var $NewListener = function() {
var clazz = function() {};
clazz.eventNames = arguments.length === 0 ? [ "fired" ]
: Array.prototype.slice.call(arguments);
clazz.ListenersClass = function() {
var args = this.eventNames.slice(); // clone
for(var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
return $NewListener.apply(this, args);
};
if (clazz.eventNames.length === 1) {
var $ename = clazz.eventNames[0];
clazz.prototype.v = null;
clazz.prototype.add = function() {
var ctx = this,
l = arguments[arguments.length - 1]; // last arguments are handler(s)
if (typeof l !== 'function') {
ctx = l;
l = l[$ename];
if (typeof l !== "function") {
return null;
}
}
if (arguments.length > 1 && arguments[0] !== $ename) {
throw new Error("Unknown event type :" + $ename);
}
if (this.v === null) {
this.v = [];
}
this.v.push(ctx, l);
return l;
};
clazz.prototype.remove = function(l) {
if (this.v !== null) {
if (arguments.length === 0) {
// remove all
this.v.length = 0;
} else {
var name = arguments.length > 1 || zebkit.isString(arguments[0]) ? arguments[0]
: null,
fn = arguments.length > 1 ? arguments[1]
: (name === null ? arguments[0] : null),
i = 0;
if (name !== null && name !== $ename) {
throw new Error("Unknown event type :" + name);
}
if (fn === null) {
this.v.length = 0;
} else {
while ((i = this.v.indexOf(fn)) >= 0) {
if (i % 2 > 0) {
i--;
}
this.v.splice(i, 2);
}
}
}
}
};
clazz.prototype.hasHandler = function(l) {
if (zebkit.isString(l)) {
return this.v !== null && l === $ename && this.v.length > 0;
} else {
return this.v.length > 0 && this.v.indexOf(l) >= 0;
}
};
clazz.prototype[$ename] = function() {
if (this.v !== null) {
for (var i = 0; i < this.v.length; i += 2) {
if (this.v[i + 1].apply(this.v[i], arguments) === true) {
return true;
}
}
}
return false;
};
clazz.prototype.hasEvent = function(nm) {
return nm === $ename;
};
} else {
var names = {};
for(var i = 0; i < clazz.eventNames.length; i++) {
names[clazz.eventNames[i]] = true;
}
clazz.prototype.$methods = null;
clazz.prototype.add = function(l) {
if (this.$methods === null) {
this.$methods = {};
}
var n = null,
k = null,
nms = this.$names !== undefined ? this.$names : names;
if (arguments.length > 1) {
n = arguments[0];
l = arguments[arguments.length - 1]; // last arguments are handler(s)
}
if (typeof l === 'function') {
if (n !== null && nms[n] === undefined) {
throw new Error("Unknown event type " + n);
}
if (n === null) {
for(k in nms) {
if (this.$methods[k] === undefined) {
this.$methods[k] = [];
}
this.$methods[k].push(this, l);
}
} else {
if (this.$methods[n] === undefined) {
this.$methods[n] = [];
}
this.$methods[n].push(this, l);
}
} else {
var b = false;
for (k in nms) {
if (typeof l[k] === "function") {
b = true;
if (this.$methods[k] === undefined) {
this.$methods[k] = [];
}
this.$methods[k].push(l, l[k]);
}
}
if (b === false) {
return null;
}
}
return l;
};
clazz.prototype.hasHandler = function(l) {
if (zebkit.isString(l)) {
return this.$methods !== null &&
this.$methods.hasOwnProperty(l) &&
this.$methods[l].length > 0;
} else {
for(var k in this.$methods) {
var v = this.$methods[k];
if (v.indexOf(l) >= 0) {
return true;
}
}
return false;
}
};
clazz.prototype.addEvents = function() {
if (this.$names === undefined) {
this.$names = {};
for (var k in names) {
this.$names[k] = names[k];
}
}
for(var i = 0; i < arguments.length; i++) {
var name = arguments[i];
if (name === null || name === undefined || this[name] !== undefined) {
throw new Error("Invalid " + name + " (event name)");
}
this[name] = (function(name) {
return function() {
// typeof is faster then hasOwnProperty under nodejs
if (this.$methods !== null && this.$methods[name] !== undefined) {
var c = this.$methods[name];
for(var i = 0; i < c.length; i += 2) {
if (c[i + 1].apply(c[i], arguments) === true) {
return true;
}
}
}
return false;
};
})(name);
this.$names[name] = true;
}
};
// populate methods that has to be called to send appropriate events to
// registered listeners
clazz.prototype.addEvents.apply(clazz.prototype, clazz.eventNames);
clazz.prototype.remove = function() {
if (this.$methods !== null) {
var k = null;
if (arguments.length === 0) {
for(k in this.$methods) {
if (this.$methods[k] !== undefined) {
this.$methods[k].length = 0;
}
}
this.$methods = {};
} else {
var name = arguments.length > 1 || zebkit.isString(arguments[0]) ? arguments[0]
: null,
fn = arguments.length > 1 ? arguments[1]
: (name === null ? arguments[0] : null),
i = 0,
v = null;
if (name !== null) {
if (this.$methods[name] !== undefined) {
if (fn === null) {
this.$methods[name].length = 0;
delete this.$methods[name];
} else {
v = this.$methods[name];
while ((i = v.indexOf(fn)) >= 0) {
if (i % 2 > 0) {
i--;
}
v.splice(i, 2);
}
if (v.length === 0) {
delete this.$methods[name];
}
}
}
} else {
for (k in this.$methods) {
v = this.$methods[k];
while ((i = v.indexOf(fn)) >= 0) {
if (i % 2 > 0) {
i--;
}
v.splice(i, 2);
}
if (v.length === 0) {
delete this.$methods[k];
}
}
}
}
}
};
clazz.prototype.hasEvent = function(nm) {
return (this.$names !== undefined && this.$names[nm] !== undefined) || names[nm] !== undefined;
};
}
return clazz;
};
/**
* Listeners container class that can be handy to store number of listeners
* for one type of event.
* @param {String} [eventName] an event name the listeners container has been
* created. By default "fired" is default event name. Event name is used to fire
* the given event to a listener container.
* @constructor
* @class zebkit.Listeners
* @example
*
* // create container with a default event name
* var container = new Listeners();
*
* // register a listener
* var listener = container.add(function(param1, param2) {
* // handle fired event
* });
*
* ...
* // fire event
* container.fired(1, 2, 3);
*
* // remove listener
* container.remove(listener);
*
* @extends zebkit.Listener
*/
/**
* Add listener
* @param {Function|Object} l a listener method or object.
* @return {Function} a listener that has been registered in the container. The result should
* be used to un-register the listener
* @method add
*/
/**
* Remove listener or all registered listeners from the container
* @param {Function} [l] a listener to be removed. If the argument has not been specified
* all registered in the container listeners will be removed
* @method remove
*/
var Listeners = $NewListener();
/**
* Event producer interface. This interface provides number of methods
* to register, un-register, fire events. It follows on/off notion like
* JQuery does it. It is expected an event producer class implementation
* has a special field "_" that keeps listeners.
*
* var MyClass = zebkit.Class(zebkit.EventProducer, [
* function() {
* // "fired" events listeners container
* this._ = new zebkit.Listeners();
* }
* ]);
*
* var a = new MyClass();
* a.on("fired", function(arg) {
* // handle "fired" events
* });
*
* a.fire(10);
*
* @class zebkit.EventProducer
* @interface zebkit.EventProducer
*/
var EventProducer = Interface([
function $prototype() {
// on(event, path, cb) handle the given event for all elements identified with the path
// on(cb) handle all events
// on(path | event, cb) handle the given event or all events for elements matched with the path
/**
* Register listener for the given events types or/and the given nodes in tree-like
* structure or listen all events types.
* @param {String} [eventName] an event type name to listen. If the event name is not passed
* then listen all events types.
* @param {String} [path] a xpath-like path to traversing elements in tree and register event
* handlers for the found elements. The parameter can be used if the interface is implemented
* with tree-like structure (for instance zebkit UI components).
* @param {Function|Object} cb a listener method or an object that contains number of methods
* to listen the specified events types.
* @example
* var comp = new zebkit.ui.Panel();
* comp.add(new zebkit.ui.Button("Test 1").setId("c1"));
* comp.add(new zebkit.ui.Button("Test 2").setId("c2"));
* ...
* // register event handler for children components of "comp"
* comp.on("/*", function() {
* // handle button fired event
* ...
* });
*
* // register event handler for button component with id equals "c1"
* comp.on("#c1", function() {
* // handle button fired event
* ...
* });
*
* @method on
*/
this.on = function() {
var cb = arguments[arguments.length - 1], // callback or object
pt = null, // path
nm = null; // event name
if (cb === null || (typeof cb === "string" || cb.constructor === String)) {
throw new Error("Invalid event handler");
}
if (arguments.length === 1) {
if (this._ === undefined) {
if (this.clazz.Listeners !== undefined) {
this._ = new this.clazz.Listeners();
} else {
return false;
}
}
return this._.add(cb);
} else if (arguments.length === 2) {
if (arguments[0] === null) {
throw new Error("Invalid event or path");
} else if (arguments[0][0] === '.' || arguments[0][0] === '/' || arguments[0][0] === '#') { // a path detected
pt = arguments[0];
} else {
if (this._ === undefined) {
if (this.clazz.Listeners !== undefined) {
this._ = new this.clazz.Listeners();
} else {
return false;
}
}
return this._.add(arguments[0], cb);
}
} else if (arguments.length === 3) {
pt = arguments[1];
nm = arguments[0];
if (pt === null) {
if (this._ === undefined) {
if (this.clazz.Listeners !== undefined) {
this._ = new this.clazz.Listeners();
} else {
return false;
}
}
return this._.add(nm, cb);
}
}
if (instanceOf(this, PathSearch) === false) {
throw new Error("Path search is not supported");
}
this.byPath(pt, function(node) {
// try to initiate
if (node._ === undefined && node.clazz.Listeners !== undefined) {
node._ = new node.clazz.Listeners();
}
if (node._ !== undefined) {
if (nm !== null) {
if (node._[nm] !== undefined) {
node._.add(nm, cb);
}
} else {
node._.add(cb);
}
}
return false;
});
return cb;
};
// off() remove all events handler
// off(event) remove the event handler
// off(event, path) remove the event handler for all nodes detected with the path
// off(path)
// off(cb)
// off(path, cb)
//
/**
* Stop listening the given event type.
* @param {String} [eventName] an event type name to stop listening. If the event name is not passed
* then stop listening all events types.
* @param {String} [path] a xpath-like path to traversing elements in tree and stop listening
* the event type for the found in the tree elements. The parameter can be used if the interface
* is implemented with tree-like structure (for instance zebkit UI components).
* @param [cb] remove the given event handler.
* @method off
*/
this.off = function() {
var pt = null, // path
fn = null, // handler
nm = null; // event name or listener
if (arguments.length === 0) {
if (this._ !== undefined) {
return this._.remove();
} else {
return;
}
} else if (arguments.length === 1) {
if (isString(arguments[0]) && (arguments[0][0] === '.' || arguments[0][0] === '/' || arguments[0][0] === '#')) {
pt = arguments[0];
} else {
if (this._ !== undefined) {
return this._.remove(arguments[0]);
} else {
return;
}
}
} else if (arguments.length === 2) {
if (isString(arguments[1])) { // detect path
pt = arguments[1];
nm = arguments[0];
} else {
if (isString(arguments[1])) {
nm = arguments[1];
} else {
fn = arguments[1];
}
if (arguments[0][0] === '.' || arguments[0][0] === '/' || arguments[0][0] === '#') {
pt = arguments[0];
} else {
throw new Error("Path is expected");
}
}
}
this.byPath(pt, function(node) {
if (node._ !== undefined) {
if (fn !== null) {
node._.remove(fn);
} else if (nm !== null) {
if (node._[nm] !== undefined) {
node._.remove(nm);
}
} else {
node._.remove();
}
}
return false;
});
};
/**
* Fire event with the given parameters.
* @param {String} name an event name
* @param {String} [path] a path if the event has to be send to multiple destination in the tree
* @param {Object|Array} [params] array of parameters or single parameter to be passed to an event
* handler or handlers.
* @method fire
*/
this.fire = function(name) {
if (arguments.length > 0 && arguments.length < 3) {
if (this._ !== undefined) {
if (this._.hasEvent(name) === false) {
throw new Error("Listener doesn't support '" + name + "' event");
}
if (arguments.length === 2) {
Array.isArray(arguments[1]) ? this._[name].apply(this._, arguments[1])
: this._[name].call(this._, arguments[1]);
} else {
this._[name].call(this._);
}
}
} else if (arguments.length === 3) {
var args = arguments[2];
this.byPath(arguments[1], function(n) {
if (n._ !== undefined && n._.hasEvent(name)) {
var ec = n._;
if (args !== null && Array.isArray(args)) {
ec[name].apply(ec, args);
} else {
ec[name].call(ec, args);
}
}
return false;
});
} else {
throw new Error("Invalid number of arguments");
}
};
}
]);
// class instance method
classTemplateProto.isEventFired = function(name) {
if (this.clazz.Listeners === undefined) {
return false;
}
if (arguments.length === 0) {
name = "fired";
}
var names = this.clazz.Listeners.eventNames;
if (names.length === 1) {
return names[0] === name;
}
for(var i = 0; i < names.length; i++) {
if (names[i] === name) {
return true;
}
}
return false;
};
/**
* Extends zebkit.Class with the given events support.
* @param {String} [args]* list of events names
* @method events
* @for zebkit.Class
*/
classTemplateFields.events = function() {
if (arguments.length === 0) {
throw new Error("No an event name was found");
}
var args = Array.prototype.slice.call(arguments),
c = args.length;
// collect events the class already declared
if (this.Listeners !== undefined) {
for (var i = 0; i < this.Listeners.eventNames.length; i++) {
var en = this.Listeners.eventNames[i];
if (args.indexOf(en) < 0) {
args.push(en);
}
}
}
if (this.Listeners === undefined || c !== args.length) {
this.Listeners = $NewListener.apply($NewListener, args);
}
if (this.isInherit(EventProducer) === false) {
this.extend(EventProducer);
}
return this;
};
$export({
"Event" : Event,
"Listeners" : Listeners,
"ListenersClass" : $NewListener,
"EventProducer" : EventProducer
});
/**
* This class represents a font and provides basic font metrics like height, ascent. Using
* the class developers can compute string width.
*
* // plain font
* var f = new zebkit.Font("Arial", 14);
*
* // bold font
* var f = new zebkit.Font("Arial", "bold", 14);
*
* // defining font with CSS font name
* var f = new zebkit.Font("100px Futura, Helvetica, sans-serif");
*
* @constructor
* @param {String} name a name of the font. If size and style parameters has not been passed
* the name is considered as CSS font name that includes size and style
* @param {String} [style] a style of the font: "bold", "italic", etc
* @param {Integer} [size] a size of the font
* @class zebkit.Font
*/
var Font = Class([
function(family, style, size) {
if (arguments.length === 1) {
this.size = this.clazz.decodeSize(family);
if (this.size === null) {
// trim
family = family.trim();
// check if a predefined style has been used
if (family === "bold" || family === "italic") {
this.style = family;
} else { // otherwise handle it as CSS-like font style
// try to parse font if possible
var re = /([a-zA-Z_\- ]+)?(([0-9]+px|[0-9]+em)\s+([,\"'a-zA-Z_ \-]+))?/,
m = family.match(re);
if (m[4] !== undefined) {
this.family = m[4].trim();
}
if (m[3] !== undefined) {
this.size = m[3].trim();
}
if (m[1] !== undefined) {
this.style = m[1].trim();
}
this.s = family;
}
}
} else if (arguments.length === 2) {
this.family = family;
this.size = this.clazz.decodeSize(style);
this.style = this.size === null ? style : null;
} else if (arguments.length === 3) {
this.family = family;
this.style = style;
this.size = this.clazz.decodeSize(size);
}
if (this.size === null) {
this.size = this.clazz.size + "px";
}
if (this.s === null) {
this.s = ((this.style !== null) ? this.style + " ": "") +
this.size + " " +
this.family;
}
var mt = $zenv.fontMetrics(this.s);
/**
* Height of the font
* @attribute height
* @readOnly
* @type {Integer}
*/
this.height = mt.height;
/**
* Ascent of the font
* @attribute ascent
* @readOnly
* @type {Integer}
*/
this.ascent = mt.ascent;
},
function $clazz() {
// default values
this.family = "Arial, Helvetica";
this.style = null;
this.size = 14;
this.mergeable = false;
this.decodeSize = function(s, defaultSize) {
if (arguments.length < 2) {
defaultSize = this.size;
}
if (typeof s === "string" || s.constructor === String) {
var size = Number(s);
if (isNaN(size)) {
var m = s.match(/^([0-9]+)(%)$/);
if (m !== null && m[1] !== undefined && m[2] !== undefined) {
size = Math.floor((defaultSize * parseInt(m[1], 10)) / 100);
return size + "px";
} else {
return /^([0-9]+)(em|px)$/.test(s) === true ? s : null;
}
} else {
if (s[0] === '+') {
size = defaultSize + size;
} else if (s[0] === '-') {
size = defaultSize - size;
}
return size + "px";
}
}
return s === null ? null : s + "px";
};
},
function $prototype(clazz) {
this.s = null;
/**
* Font family.
* @attribute family
* @type {String}
* @default null
*/
this.family = clazz.family;
/**
* Font style (for instance "bold").
* @attribute style
* @type {String}
* @default null
*/
this.style = clazz.style;
this.size = clazz.size;
/**
* Returns CSS font representation
* @return {String} a CSS representation of the given Font
* @method toString
* @for zebkit.Font
*/
this.toString = function() {
return this.s;
};
/**
* Compute the given string width in pixels basing on the
* font metrics.
* @param {String} s a string
* @return {Integer} a string width
* @method stringWidth
*/
this.stringWidth = function(s) {
if (s.length === 0) {
return 0;
} else {
var fm = $zenv.fontMeasure;
if (fm.font !== this.s) {
fm.font = this.s;
}
return Math.round(fm.measureText(s).width);
}
};
/**
* Calculate the specified substring width
* @param {String} s a string
* @param {Integer} off fist character index
* @param {Integer} len length of substring
* @return {Integer} a substring size in pixels
* @method charsWidth
* @for zebkit.Font
*/
this.charsWidth = function(s, off, len) {
var fm = $zenv.fontMeasure;
if (fm.font !== this.s) {
fm.font = this.s;
}
return Math.round((fm.measureText(len === 1 ? s[off]
: s.substring(off, off + len))).width );
};
/**
* Resize font and return new instance of font class with new size.
* @param {Integer | String} size can be specified in pixels as integer value or as
* a percentage from the given font:
* @return {zebkit.Font} a font
* @for zebkit.Font
* @method resize
* @example
*
* ```javascript
* var font = new zebkit.Font(10); // font 10 pixels
* font = font.resize("200%"); // two times higher font
* ```
*/
this.resize = function(size) {
var nsize = this.clazz.decodeSize(size, this.height);
if (nsize === null) {
throw new Error("Invalid font size : " + size);
}
return new this.clazz(this.family, this.style, nsize);
};
/**
* Restyle font and return new instance of the font class
* @param {String} style a new style
* @return {zebkit.Font} a font
* @method restyle
*/
this.restyle = function(style) {
return new this.clazz(this.family, style, this.height + "px");
};
}
]);
function $font() {
if (arguments.length === 1) {
if (instanceOf(arguments[0], Font)) {
return arguments[0];
} if (Array.isArray(arguments[0])) {
return Font.newInstance.apply(Font, arguments[0]);
} else {
return new Font(arguments[0]);
}
} else if (arguments.length > 1) {
return Font.newInstance.apply(Font, arguments);
} else {
throw Error("No an argument has been defined");
}
}
$export( { "Font" : Font }, $font );
function $ls(callback, all) {
for (var k in this) {
var v = this[k];
if (this.hasOwnProperty(k) && (v instanceof Package) === false) {
if ((k[0] !== '$' && k[0] !== '_') || all === true) {
if (callback.call(this, k, this[k]) === true) {
return true;
}
}
}
}
return false;
}
function $lsall(fn) {
return $ls.call(this, function(k, v) {
if (v === undefined) {
throw new Error(fn + "," + k);
}
if (v !== null && v.clazz === Class) {
// class is detected, set the class name and ref to the class package
if (v.$name === undefined) {
v.$name = fn + k;
v.$pkg = getPropertyValue($global, fn.substring(0, fn.length - 1));
if (v.$pkg === undefined) {
throw new ReferenceError(fn);
}
}
return $lsall.call(v, v.$name + ".");
}
});
}
/**
* Package is a special class to declare zebkit packages. Global variable "zebkit" is
* root package for all other packages. To declare a new package use "zebkit" global
* variable:
*
* // declare new "mypkg" package
* zebkit.package("mypkg", function(pkg, Class) {
* // put the package entities in
* pkg.packageVariable = 10;
* ...
* });
* ...
*
* // now we can access package and its entities directly
* zebkit.mypkg.packageVariable
*
* // or it is preferable to wrap a package access with "require"
* // method
* zebkit.require("mypkg", function(mypkg) {
* mypkg.packageVariable
* });
*
* @class zebkit.Package
* @constructor
*/
function Package(name, parent) {
/**
* URL the package has been loaded
* @attribute $url
* @readOnly
* @type {String}
*/
this.$url = null;
/**
* Name of the package
* @attribute $name
* @readOnly
* @type {String}
*/
this.$name = name;
/**
* Package configuration parameters.
* @attribute $config
* @readOnly
* @private
* @type {Object}
*/
this.$config = {};
this.$ready = new DoIt();
/**
* Reference to a parent package
* @attribute $parent
* @private
* @type {zebkit.Package}
*/
this.$parent = arguments.length < 2 ? null : parent;
}
/**
* Get or set configuration parameter.
* @param {String} [name] a parameter name.
* @param {Object} [value] a parameter value.
* @param {Boolean} [overwrite] boolean flag that indicates if the
* parameters value have to be overwritten if it exists
* @method config
*/
Package.prototype.config = function(name, value, overwrite) {
if (arguments.length === 0) {
return this.$config;
} else if (arguments.length === 1 && isString(arguments[0])) {
return this.$config[name];
} else {
if (isString(arguments[0])) {
var old = this.$config[name];
if (value === undefined) {
delete this.$config[name];
} else if (arguments.length < 3 || overwrite === true) {
this.$config[name] = value;
} else if (this.$config.hasOwnProperty(name) === false) {
this.$config[name] = value;
}
return old;
} else {
overwrite = arguments.length > 1 ? value : false;
for (var k in arguments[0]) {
this.config(k, arguments[0][k], overwrite);
}
}
}
};
/**
* Detect the package location and store the location into "$url"
* package field
* @private
* @method $detectLocation
*/
Package.prototype.$detectLocation = function() {
if (typeof __dirname !== 'undefined') {
this.$url = __dirname;
} else if (typeof document !== "undefined") {
//
var s = document.getElementsByTagName('script'),
ss = s[s.length - 1].getAttribute('src'),
i = ss === null ? -1 : ss.lastIndexOf("/"),
a = document.createElement('a');
a.href = (i > 0) ? ss.substring(0, i + 1)
: document.location.toString();
this.$url = a.href.toString();
}
};
/**
* Get full name of the package. Full name includes not the only the given
* package name, but also all parent packages separated with "." character.
* @return {String} a full package name
* @method fullname
*/
Package.prototype.fullname = function() {
var n = [ this.$name ], p = this;
while (p.$parent !== null) {
p = p.$parent;
n.unshift(p.$name);
}
return n.join(".");
};
/**
* Find a package with the given file like path relatively to the given package.
* @param {String} path a file like path
* @return {String} path a path
* @example
*
* // declare "zebkit.test" package
* zebkit.package("test", function(pkg, Class) {
* ...
* });
* ...
*
* zebkit.require("test", function(test) {
* var parent = test.cd(".."); // parent points to zebkit package
* ...
* });
*
* @method cd
*/
Package.prototype.cd = function(path) {
if (path[0] === '/') {
path = path.substring(1);
}
var paths = path.split('/'),
pk = this;
for (var i = 0; i < paths.length; i++) {
var pn = paths[i];
if (pn === "..") {
pk = pk.$parent;
} else {
pk = pk[pn];
}
if (pk === undefined || pk === null) {
throw new Error("Package path '" + path + "' cannot be resolved");
}
}
return pk;
};
/**
* List the package sub-packages.
* @param {Function} callback callback function that gets a sub-package name and the
* sub-package itself as its arguments
* @param {boolean} [recursively] indicates if sub-packages have to be traversed recursively
* @method packages
*/
Package.prototype.packages = function(callback, recursively) {
for (var k in this) {
var v = this[k];
if (k !== "$parent" && this.hasOwnProperty(k) && v instanceof Package) {
if (callback.call(this, k, v) === true || (recursively === true && v.packages(callback, recursively) === true)) {
return true;
}
}
}
return false;
};
/**
* Get a package by the specified name.
* @param {String} name a package name
* @return {zebkit.Package} a package
* @method byName
*/
Package.prototype.byName = function(name) {
if (this.fullname() === name) {
return this;
} else {
var i = name.indexOf('.');
if (i > 0) {
var vv = getPropertyValue(this, name.substring(i + 1), false);
return vv === undefined ? null : vv;
} else {
return null;
}
}
};
/**
* List classes, variables and interfaces defined in the given package.
* If second parameter "all" passed to the method is false, the method
* will skip package entities whose name starts from "$" or "_" character.
* These entities are considered as private ones. Pay attention sub-packages
* are not listed.
* @param {Function} cb a callback method that get the package entity key
* and the entity value as arguments.
* @param {Boolean} [all] flag that specifies if private entities are
* should be listed.
* @method ls
*/
Package.prototype.ls = function(cb, all) {
return $ls.call(this, cb, all);
};
/**
* Build import JS code string that can be evaluated in a local space to make visible
* the given package or packages classes, variables and methods.
* @example
*
* (function() {
* // make visible variables, classes and methods declared in "zebkit.ui"
* // package in the method local space
* eval(zebkit.import("ui"));
*
* // use imported from "zebkit.ui.Button" class without necessity to specify
* // full path to it
* var bt = new Button("Ok");
* })();
*
* @param {String} [pkgname]* names of packages to be imported
* @return {String} an import string to be evaluated in a local JS space
* @method import
* @deprecated Usage of the method has to be avoided. Use zebkit.require(...) instead.
*/
Package.prototype.import = function() {
var code = [];
if (arguments.length > 0) {
for(var i = 0; i < arguments.length; i++) {
var v = getPropertyValue(this, arguments[i]);
if ((v instanceof Package) === false) {
throw new Error("Package '" + arguments[i] + " ' cannot be found");
}
code.push(v.import());
}
return code.length > 0 ? code.join(";") : null;
} else {
var fn = this.fullname();
this.ls(function(k, v) {
code.push(k + '=' + fn + '.' + k);
});
return code.length > 0 ? "var " + code.join(",") + ";" : null;
}
};
/**
* This method has to be used to start building a zebkit application. It
* expects a callback function where an application code has to be placed and
* number of required for the application packages names. The call back gets
* the packages instances as its arguments. The method guarantees the callback
* is called at the time zebkit and requested packages are loaded, initialized
* and ready to be used.
* @param {String} [packages]* name or names of packages to make visible
* in callback method
* @param {Function} [callback] a method to be called. The method is called
* in context of the given package and gets requested packages passed as the
* method arguments in order they have been requested.
* @method require
* @example
*
* zebkit.require("ui", function(ui) {
* var b = new ui.Button("Ok");
* ...
* });
*
*/
Package.prototype.require = function() {
var pkgs = [],
$this = this,
fn = arguments[arguments.length - 1];
if (typeof fn !== 'function') {
throw new Error("Invalid callback function");
}
for(var i = 0; isString(arguments[i]) && i < arguments.length; i++) {
var pkg = getPropertyValue(this, arguments[i]);
if ((pkg instanceof Package) === false) {
throw new Error("Package '" + arguments[i] + "' cannot be found");
}
pkgs.push(pkg);
}
return this.then(function() {
fn.apply($this, pkgs);
});
};
/**
* Detect root package.
* @return {zebkit.Package} a root package
* @method getRootPackage
*/
Package.prototype.getRootPackage = function() {
var rootPkg = this;
while (rootPkg.$parent !== null) {
rootPkg = rootPkg.$parent;
}
return rootPkg;
};
var $textualFileExtensions = [
"txt", "json", "htm", "html", "md", "properties", "conf", "xml", "java", "js", "css", "scss", "log"
],
$imageFileExtensions = [
"jpg", "jpeg", "png", "tiff", "gif", "ico", "exif", "bmp"
];
/**
* This method loads resources (images, textual files, etc) and call callback
* method with completely loaded resources as input arguments.
* @example
*
* zebkit.resources(
* "http://test.com/image1.jpg",
* "http://test.com/text.txt",
* function(image, text) {
* // handle resources here
* ...
* }
* );
*
* @param {String} paths* paths to resources to be loaded
* @param {Function} cb callback method that is executed when all listed
* resources are loaded and ready to be used.
* @method resources
*/
Package.prototype.resources = function() {
var args = Array.prototype.slice.call(arguments),
$this = this,
fn = args.pop();
if (typeof fn !== 'function') {
throw new Error("Invalid callback function");
}
this.then(function() {
for(var i = 0; i < args.length ; i++) {
(function(path, jn) {
var m = path.match(/^(\<[a-z]+\>\s*)?(.*)$/),
type = "txt",
p = m[2].trim();
if (m[1] !== undefined) {
type = m[1].trim().substring(1, m[1].length - 1).trim();
} else {
var li = p.lastIndexOf('.');
if (li > 0) {
var ext = p.substring(li + 1).toLowerCase();
if ($textualFileExtensions.indexOf(ext) >= 0) {
type = "txt";
} else if ($imageFileExtensions.indexOf(ext) >= 0) {
type = "img";
}
}
}
if (type === "img") {
$zenv.loadImage(p, function(img) {
jn(img);
}, function(img, e) {
jn(null);
});
} else if (type === "txt") {
ZFS.GET(p).then(function(req) {
jn(req.responseText);
}).catch(function(e) {
jn(null);
});
} else {
jn(null);
}
})(args[i], this.join());
}
}).then(function() {
fn.apply($this, arguments);
});
};
/**
* This method helps to sync accessing to package entities with the
* package internal state. For instance package declaration can initiate
* loading resources that happens asynchronously. In this case to make sure
* the package completed loading its configuration we should use package
* "then" method.
* @param {Function} f a callback method where we can safely access the
* package entities
* @chainable
* @private
* @example
*
* zebkit.then(function() {
* // here we can make sure all package declarations
* // are completed and we can start using it
* });
*
* @method then
*/
Package.prototype.then = function(f) {
this.$ready.then(f).catch(function(e) {
dumpError(e);
// re-start other waiting tasks
this.restart();
});
return this;
};
Package.prototype.join = function() {
return this.$ready.join.apply(this.$ready, arguments);
};
/**
* Method that has to be used to declare packages.
* @param {String} name a name of the package
* @param {Function} [callback] a call back method that is called in package
* context. The method has to be used to populate the given package classes,
* interfaces and variables.
* @param {String|Boolean} [config] a path to configuration JSON file or boolean flag that says
* to perform configuration using package as configuration name
* @example
* // declare package "zebkit.log"
* zebkit.package("log", function(pkg) {
* // declare the package class Log
* pkg.Log = zebkit.Class([
* function error() { ... },
* function warn() { ... },
* function info() { ... }
* ]);
* });
*
* // later on you can use the declared package stuff as follow
* zebkit.require("log", function(log) {
* var myLog = new log.Log();
* ...
* myLog.warn("Warning");
* });
*
* @return {zebkit.Package} a package
* @method package
*/
Package.prototype.package = function(name, callback, path) {
// no arguments than return the package itself
if (arguments.length === 0) {
return this;
} else {
var target = this;
if (typeof name !== 'function') {
if (name === undefined || name === null) {
throw new Error("Null package name");
}
name = name.trim();
if (name.match(/^[a-zA-Z_][a-zA-Z0-9_]+(\.[a-zA-Z_][a-zA-Z0-9_]+)*$/) === null) {
throw new Error("Invalid package name '" + name + "'");
}
var names = name.split('.');
for(var i = 0, k = names[0]; i < names.length; i++, k = k + '.' + names[i]) {
var n = names[i],
p = target[n];
if (p === undefined) {
p = new Package(n, target);
target[n] = p;
} else if ((p instanceof Package) === false) {
throw new Error("Requested package '" + name + "' conflicts with variable '" + n + "'");
}
target = p;
}
} else {
path = callback;
callback = name;
}
// detect url later then sooner since
if (target.$url === null) {
target.$detectLocation();
}
if (typeof callback === 'function') {
this.then(function() {
callback.call(target, target, typeof Class !== 'undefined' ? Class : null);
}).then(function() {
// initiate configuration loading if it has been requested
if (path !== undefined && path !== null) {
var jn = this.join();
if (path === true) {
var fn = target.fullname();
path = fn.substring(fn.indexOf('.') + 1) + ".json";
target.configWithRs(path, jn);
} else {
target.configWith(path, jn);
}
}
}).then(function(r) {
if (r instanceof Error) {
this.error(r);
} else {
// initiate "clazz.$name" resolving
$lsall.call(target, target.fullname() + ".");
}
});
}
return target;
}
};
function resolvePlaceholders(path, env) {
// replace placeholders in dir path
var ph = path.match(/\%\{[a-zA-Z$][a-zA-Z0-9_$.]*\}/g);
if (ph !== null) {
for (var i = 0; i < ph.length; i++) {
var p = ph[i],
v = env[p.substring(2, p.length - 1)];
if (v !== null && v !== undefined) {
path = path.replace(p, v);
}
}
}
return path;
}
/**
* Configure the given package with the JSON.
* @param {String | Object} path a path to JSON or JSON object
* @param {Function} [cb] a callback method
* @method configWith
*/
Package.prototype.configWith = function(path, cb) {
// catch error to keep passed callback notified
try {
if ((path instanceof URI || isString(path)) && URI.isAbsolute(path) === false) {
path = URI.join(this.$url, path);
}
} catch(e) {
if (arguments.length > 1 && cb !== null) {
cb.call(this, e);
return;
} else {
throw e;
}
}
var $this = this;
if (arguments.length > 1 && cb !== null) {
new Zson($this).then(path, function() {
cb.call(this, path);
}).catch(function(e) {
cb.call(this, e);
});
} else {
this.getRootPackage().then(function() { // calling the guarantees it will be called when previous actions are completed
this.till(new Zson($this).then(path)); // now we can trigger other loading action
});
}
};
/**
* Configure the given package with the JSON.
* @param {String | Object} path a path to JSON or JSON object
* @param {Function} [cb] a callback
* @method configWithRs
*/
Package.prototype.configWithRs = function(path, cb) {
if (URI.isAbsolute(path)) {
throw new Error("Absulute path cannot be used");
}
var pkg = this;
// detect root package (common sync point) and package that
// defines path to resources
while (pkg !== null && (pkg.$config.basedir === undefined || pkg.$config.basedir === null)) {
pkg = pkg.$parent;
}
if (pkg === null) {
path = URI.join(this.$url, "rs", path);
} else {
// TODO: where config placeholders have to be specified
path = URI.join(resolvePlaceholders(pkg.$config.basedir, pkg.$config), path);
}
return arguments.length > 1 ? this.configWith(path, cb)
: this.configWith(path);
};
$export(Package);
/**
* This is the core package that provides powerful easy OOP concept, packaging
* and number of utility methods. The package doesn't have any dependencies
* from others zebkit packages and can be used independently. Briefly the
* package possibilities are listed below:
- **easy OOP concept**. Use "zebkit.Class" and "zebkit.Interface" to declare
classes and interfaces
```JavaScript
// declare class A
var ClassA = zebkit.Class([
function() { // class constructor
...
},
// class method
function a(p1, p2, p3) { ... }
]);
var ClassB = zebkit.Class(ClassA, [
function() { // override cotoString.nanstructor
this.$super(); // call super constructor
},
function a(p1, p2, p3) { // override method "a"
this.$super(p1, p2, p3); // call super implementation of method "a"
}
]);
var b = new ClassB(); // instantiate classB
b.a(1,2,3); // call "a"
// instantiate anonymous class with new method "b" declared and
// overridden method "a"
var bb = new ClassB([
function a(p1, p2, p3) { // override method "a"
this.$super(p1, p2, p3); // call super implementation of method "a"
},
function b() { ... } // declare method "b"
]);
b.a();
b.b();
```
- **Packaging.** Zebkit uses Java-like packaging system where your code is bundled in
the number of hierarchical packages.
```JavaScript
// declare package "zebkit.test"
zebkit.package("test", function(pkg) {
// declare class "Test" in the package
pkg.Test = zebkit.Class([ ... ]);
});
...
// Later on use class "Test" from package "zebkit.test"
zebkit.require("test", function(test) {
var test = new test.Test();
});
```
- **Resources loading.** Resources should be loaded with a special method to guarantee
its proper loading in zebkit sequence and the loading completeness.
```JavaScript
// declare package "zebkit.test"
zebkit.resources("http://my.com/test.jpg", function(img) {
// handle completely loaded image here
...
});
zebkit.package("test", function(pkg, Class) {
// here we can be sure all resources are loaded and ready
});
```
- **Declaring number of core API method and classes**
- **"zebkit.DoIt"** - improves Promise like alternative class
- **"zebkit.URI"** - URI helper class
- **"zebkit.Dummy"** - dummy class
- **instanceOf(...)** method to evaluate zebkit classes and and interfaces inheritance.
The method has to be used instead of JS "instanceof" operator to provide have valid
result.
- **zebkit.newInstance(...)** method
- **zebkit.clone(...)** method
- etc
* @class zebkit
* @access package
*/
// =================================================================================================
//
// Zebkit root package declaration
//
// =================================================================================================
var zebkit = new Package("zebkit");
/**
* Reference to zebkit environment. Environment is basic, minimal API
* zebkit and its components require.
* @for zebkit
* @attribute environment
* @readOnly
* @type {Environment}
*/
// declaring zebkit as a global variable has to be done before calling "package" method
// otherwise the method cannot find zebkit to resolve class names
//
// nodejs
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = zebkit;
}
$global.zebkit = zebkit;
// collect exported entities in zebkit package space
zebkit.package(function(pkg) {
for(var exp in $exports) {
pkg[exp] = $exports[exp];
}
});
if ($isInBrowser) {
// collect query string parameters
try {
var uri = new URI(document.URL);
if (uri.qs !== null) {
var params = URI.parseQS(uri.qs);
for (var k in params) {
zebkit.config(k, URI.decodeQSValue(params[k]));
}
var cacheBusting = zebkit.config("zson.cacheBusting");
if (cacheBusting !== undefined && cacheBusting !== null) {
Zson.prototype.cacheBusting = cacheBusting;
}
}
} catch(e) {
dumpError(e);
}
zebkit.then(function() {
var jn = this.join(),
$interval = $zenv.setInterval(function () {
if (document.readyState === "complete") {
$zenv.clearInterval($interval);
jn(zebkit);
}
}, 50);
});
}
})();
zebkit.package("util", function(pkg, Class) {
/**
* Number of different utilities methods and classes.
* @class zebkit.util
* @access package
*/
/**
* Validate the specified value to be equal one of the given values
* @param {value} value a value to be validated
* @param {Object} [allowedValues]* a number of valid values to test against
* @throws Error if the value doesn't match any valid value
* @for zebkit.util
* @method validateValue
* @example
* // test if the alignment is equal one of the possible values
* // throws error otherwise
* zebkit.util.validateValue(alignment, "top", "left", "right", "bottom");
* @protected
*/
pkg.validateValue = function(value) {
if (arguments.length < 2) {
throw new Error("Invalid arguments list. List of valid values is expected");
}
for(var i = 1; i < arguments.length; i++) {
if (arguments[i] === value) {
return value;
}
}
var values = Array.prototype.slice.call(arguments).slice(1);
throw new Error("Invalid value '" + value + "',the following values are expected: " + values.join(','));
};
/**
* Compare two dates.
* @param {Date} d1 a first date
* @param {Date} d2 a second sate
* @return {Integer} 0 if the two dates are equal, -1 if d1 < d2, 1 if d1 > d2,
* null if one of the date is null
* @method compareDates
*/
pkg.compareDates = function(d1, d2) {
if (arguments.length === 2 && d1 === d2) {
return 0;
}
// exclude null dates
if (d1 === null || d2 === null) {
return null;
}
var day1, month1, year1,
day2, month2, year2,
i = 1;
if (d1 instanceof Date) {
day1 = d1.getDate();
month1 = d1.getMonth();
year1 = d1.getFullYear();
} else {
day1 = arguments[0];
month1 = arguments[1];
year1 = arguments[2];
i = 3;
}
d2 = arguments[i];
if (d2 instanceof Date) {
day2 = d2.getDate();
month2 = d2.getMonth();
year2 = d2.getFullYear();
} else {
day2 = arguments[i];
month2 = arguments[i + 1];
year2 = arguments[i + 2];
}
if (day1 === day2 && month1 === month2 && year1 === year2) {
return 0;
} else if (year1 > year2 ||
(year1 === year2 && month1 > month2) ||
(year1 === year2 && month1 === month2 && day1 > day2))
{
return 1;
} else {
return -1;
}
};
/**
* Validate the given date
* @param {Date} date a date to be validated
* @return {Boolean} true if the date is valid
* @method validateDate
*/
pkg.validateDate = function(day, month, year) {
var d = (arguments.length < 3) ? (arguments.length === 1 ? day : new Date(month, day))
: new Date(year, month, day);
if (d.isValid() === false) {
throw new Error("Invalid date : " + d);
}
};
pkg.format = function(s, obj, ph) {
if (arguments.length < 3) {
ph = '';
}
var rg = /\$\{([0-9]+\s*,)?(.?,)?([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
r = [],
i = 0,
j = 0,
m = null;
while ((m = rg.exec(s)) !== null) {
r[i++] = s.substring(j, m.index);
j = m.index + m[0].length;
var v = obj[m[3]],
mn = "get" + m[3][0].toUpperCase() + m[3].substring(1),
f = obj[mn];
if (typeof f === "function") {
v = f.call(obj);
}
if (m[1] !== undefined) {
var ml = parseInt(m[1].substring(0, m[1].length - 1).trim()),
ph2 = m[2] !== undefined ? m[2].substring(0, m[2].length - 1) : ph;
if (v === null || v === undefined) {
ph2 = ph;
v = "";
} else {
v = "" + v;
}
for(var k = v.length; k < ml; k++) {
v = ph2 + v;
}
}
if (v === null || v === undefined) {
v = ph;
}
r[i++] = v;
}
if (i > 0) {
if (j < s.length) {
r[i++] = s.substring(j);
}
return pkg.format(r.join(''), obj, ph);
}
return s;
};
/**
* Compute intersection of the two given rectangular areas
* @param {Integer} x1 a x coordinate of the first rectangular area
* @param {Integer} y1 a y coordinate of the first rectangular area
* @param {Integer} w1 a width of the first rectangular area
* @param {Integer} h1 a height of the first rectangular area
* @param {Integer} x2 a x coordinate of the first rectangular area
* @param {Integer} y2 a y coordinate of the first rectangular area
* @param {Integer} w2 a width of the first rectangular area
* @param {Integer} h2 a height of the first rectangular area
* @param {Object} r an object to store result
*
* { x: {Integer}, y:{Integer}, width:{Integer}, height:{Integer} }
*
* @method intersection
* @for zebkit.util
*/
pkg.intersection = function(x1,y1,w1,h1,x2,y2,w2,h2,r){
r.x = x1 > x2 ? x1 : x2;
r.width = Math.min(x1 + w1, x2 + w2) - r.x;
r.y = y1 > y2 ? y1 : y2;
r.height = Math.min(y1 + h1, y2 + h2) - r.y;
};
/**
* Test if two rectangular areas have intersection
* @param {Integer} x1 a x coordinate of the first rectangular area
* @param {Integer} y1 a y coordinate of the first rectangular area
* @param {Integer} w1 a width of the first rectangular area
* @param {Integer} h1 a height of the first rectangular area
* @param {Integer} x2 a x coordinate of the first rectangular area
* @param {Integer} y2 a y coordinate of the first rectangular area
* @param {Integer} w2 a width of the first rectangular area
* @param {Integer} h2 a height of the first rectangular area
* @return {Boolean} true if the given two rectangular areas intersect
*
* @method isIntersect
* @for zebkit.util
*/
pkg.isIntersect = function(x1,y1,w1,h1,x2,y2,w2,h2){
return (Math.min(x1 + w1, x2 + w2) - (x1 > x2 ? x1 : x2)) > 0 &&
(Math.min(y1 + h1, y2 + h2) - (y1 > y2 ? y1 : y2)) > 0;
};
/**
* Unite two rectangular areas to one rectangular area.
* @param {Integer} x1 a x coordinate of the first rectangular area
* @param {Integer} y1 a y coordinate of the first rectangular area
* @param {Integer} w1 a width of the first rectangular area
* @param {Integer} h1 a height of the first rectangular area
* @param {Integer} x2 a x coordinate of the first rectangular area
* @param {Integer} y2 a y coordinate of the first rectangular area
* @param {Integer} w2 a width of the first rectangular area
* @param {Integer} h2 a height of the first rectangular area
* @param {Object} r an object to store result
*
* { x: {Integer}, y:{Integer}, width:{Integer}, height:{Integer} }
*
* @method unite
* @for zebkit.util
*/
pkg.unite = function(x1,y1,w1,h1,x2,y2,w2,h2,r){
r.x = x1 < x2 ? x1 : x2;
r.y = y1 < y2 ? y1 : y2;
r.width = Math.max(x1 + w1, x2 + w2) - r.x;
r.height = Math.max(y1 + h1, y2 + h2) - r.y;
};
var letterRE = /[A-Za-z]/;
pkg.isLetter = function (ch) {
if (ch.length !== 1) {
throw new Error("Incorrect character");
}
return letterRE.test(ch);
};
/**
* Useful class to track a virtual cursor position in a structure that has dedicated number of lines
* where every line has a number of elements. The structure metric has to be described by providing
* an instance of zebkit.util.Position.Metric interface that discovers how many lines the structure
* has and how many elements every line includes.
* @param {zebkit.util.Position.Metric} m a position metric
* @constructor
* @class zebkit.util.Position
*/
/**
* Fire when a virtual cursor position has been updated
*
* position.on(function(src, prevOffset, prevLine, prevCol) {
* ...
* });
*
* @event posChanged
* @param {zebkit.util.Position} src an object that triggers the event
* @param {Integer} prevOffest a previous virtual cursor offset
* @param {Integer} prevLine a previous virtual cursor line
* @param {Integer} prevCol a previous virtual cursor column in the previous line
*/
pkg.Position = Class([
function(pi){
/**
* Shows if the position object is in valid state.
* @private
* @type {Boolean}
* @attribute isValid
*/
this.isValid = false;
/**
* Current virtual cursor line position
* @attribute currentLine
* @type {Integer}
* @readOnly
*/
/**
* Current virtual cursor column position
* @attribute currentCol
* @type {Integer}
* @readOnly
*/
/**
* Current virtual cursor offset
* @attribute offset
* @type {Integer}
* @readOnly
*/
this.currentLine = this.currentCol = this.offset = 0;
this.setMetric(pi);
},
function $clazz() {
/**
* Position metric interface. This interface is designed for describing
* a navigational structure that consists on number of lines where
* every line consists of number of elements
* @class zebkit.util.Position.Metric
* @interface zebkit.util.Position.Metric
*/
/**
* Get number of lines to navigate through
* @return {Integer} a number of lines
* @method getLines
*/
/**
* Get a number of elements in the given line
* @param {Integer} l a line index
* @return {Integer} a number of elements in a line
* @method getLineSize
*/
/**
* Get a maximal element index (a last element of a last line)
* @return {Integer} a maximal element index
* @method getMaxOffset
*/
this.Metric = zebkit.Interface([
"abstract",
function getLines() {},
function getLineSize() {},
function getMaxOffset() {}
]);
},
/**
* @for zebkit.util.Position
*/
function $prototype() {
/**
* Set the specified virtual cursor offsest
* @param {Integer} o an offset, pass null to set position to indefinite state.
*
* - if offset is null than offset will set to -1 (undefined state)
* - if offset is less than zero than offset will be set to zero
* - if offset is greater or equal to maximal possible offset it will be set to maximal possible offset
*
* @return {Integer} an offset that has been set
* @method setOffset
*/
this.setOffset = function(o){
if (o < 0) {
o = 0;
} else if (o === null) {
o = -1;
} else {
var max = this.metrics.getMaxOffset();
if (o >= max) {
o = max;
}
}
if (o !== this.offset){
var prevOffset = this.offset,
prevLine = this.currentLine,
prevCol = this.currentCol,
p = this.getPointByOffset(o);
this.offset = o;
if (p !== null){
this.currentLine = p[0];
this.currentCol = p[1];
} else {
this.currentLine = this.currentCol = -1;
}
this.isValid = true;
this.fire("posChanged", [this, prevOffset, prevLine, prevCol]);
}
return o;
};
/**
* Seek virtual cursor offset with the given shift
* @param {Integer} off a shift
* @return {Integer} an offset that has been set
* @method seek
*/
this.seek = function(off) {
return this.setOffset(this.offset + off);
};
/**
* Set the virtual cursor line and the given column in the line
* @param {Integer} r a line
* @param {Integer} c a column in the line
* @method setRowCol
*/
this.setRowCol = function(r, c) {
if (r !== this.currentLine || c !== this.currentCol){
var prevOffset = this.offset,
prevLine = this.currentLine,
prevCol = this.currentCol;
this.offset = this.getOffsetByPoint(r, c);
this.currentLine = r;
this.currentCol = c;
this.fire("posChanged", [this, prevOffset, prevLine, prevCol]);
}
};
/**
* Special method to inform the position object that its state has to be adjusted
* because of the given portion of data had been inserted .
* @param {Integer} off an offset the insertion has happened
* @param {Integer} size a length of the inserted portion
* @protected
* @method removed
*/
this.inserted = function(off, size) {
if (this.offset >= 0 && off <= this.offset){
this.isValid = false;
this.setOffset(this.offset + size);
}
};
/**
* Special method to inform the position object that its state has to be adjusted
* because of the given portion of data had been removed.
* @param {Integer} off an offset the removal has happened
* @param {Integer} size a length of the removed portion
* @protected
* @method removed
*/
this.removed = function (off, size){
if (this.offset >= 0 && this.offset >= off){
this.isValid = false;
this.setOffset(this.offset >= (off + size) ? this.offset - size
: off);
}
};
/**
* Calculate a line and line column by the given offset.
* @param {Integer} off an offset
* @return {Array} an array that contains a line as the first
* element and a column in the line as the second element.
* @method getPointByOffset
*/
this.getPointByOffset = function(off){
if (off >= 0) {
var m = this.metrics,
max = m.getMaxOffset();
if (off > max) {
throw new Error("Out of bounds:" + off);
} else if (max === 0) {
return [(m.getLines() > 0 ? 0 : -1), 0];
} else if (off === 0) {
return [0, 0];
}
var d = 0, sl = 0, so = 0;
if (this.isValid === true && this.offset !== -1) {
sl = this.currentLine;
so = this.offset - this.currentCol;
if (off > this.offset) {
d = 1;
} else if (off < this.offset) {
d = -1;
} else {
return [sl, this.currentCol];
}
} else {
d = (Math.floor(max / off) === 0) ? -1 : 1;
if (d < 0) {
sl = m.getLines() - 1;
so = max - m.getLineSize(sl);
}
}
for(; sl < m.getLines() && sl >= 0; sl += d){
var ls = m.getLineSize(sl);
if (off >= so && off < so + ls) {
return [sl, off - so];
}
so += d > 0 ? ls : -m.getLineSize(sl - 1);
}
}
return null;
};
/**
* Calculate an offset by the given line and column in the line
* @param {Integer} row a line
* @param {Integer} col a column in the line
* @return {Integer} an offset
* @method getOffsetByPoint
*/
this.getOffsetByPoint = function (row, col){
var startOffset = 0, startLine = 0, m = this.metrics, i = 0;
if (row >= m.getLines()) {
throw new RangeError(row);
}
if (col >= m.getLineSize(row)) {
throw new RangeError(col);
}
if (this.isValid === true && this.offset !== -1) {
startOffset = this.offset - this.currentCol;
startLine = this.currentLine;
}
if (startLine <= row) {
for(i = startLine;i < row; i++) {
startOffset += m.getLineSize(i);
}
} else {
for(i = startLine - 1;i >= row; i--) {
startOffset -= m.getLineSize(i);
}
}
return startOffset + col;
};
/**
* Seek virtual cursor to the next position. How the method has to seek to the next position
* has to be denoted by one of the following constants:
- **"begin"** seek cursor to the begin of the current line
- **"end"** seek cursor to the end of the current line
- **"up"** seek cursor one line up
- **"down"** seek cursor one line down
* If the current virtual position is not known (-1) the method always sets
* it to the first line, the first column in the line (offset is zero).
* @param {Integer} t an action the seek has to be done
* @param {Integer} num number of seek actions
* @method seekLineTo
*/
this.seekLineTo = function(t,num){
if (this.offset < 0){
this.setOffset(0);
} else {
if (arguments.length === 1) {
num = 1;
}
var prevOffset = this.offset,
prevLine = this.currentLine,
prevCol = this.currentCol,
maxCol = 0,
i = 0;
switch(t) {
case "begin":
if (this.currentCol > 0){
this.offset -= this.currentCol;
this.currentCol = 0;
} break;
case "end":
maxCol = this.metrics.getLineSize(this.currentLine);
if (this.currentCol < (maxCol - 1)) {
this.offset += (maxCol - this.currentCol - 1);
this.currentCol = maxCol - 1;
} break;
case "up":
if (this.currentLine > 0) {
this.offset -= (this.currentCol + 1);
this.currentLine--;
for(i = 0; this.currentLine > 0 && i < (num - 1); i++, this.currentLine--) {
this.offset -= this.metrics.getLineSize(this.currentLine);
}
maxCol = this.metrics.getLineSize(this.currentLine);
if (this.currentCol < maxCol) {
this.offset -= (maxCol - this.currentCol - 1);
} else {
this.currentCol = maxCol - 1;
}
} break;
case "down":
if (this.currentLine < (this.metrics.getLines() - 1)) {
this.offset += (this.metrics.getLineSize(this.currentLine) - this.currentCol);
this.currentLine++;
var size = this.metrics.getLines() - 1;
for (i = 0; this.currentLine < size && i < (num - 1); i++ ,this.currentLine++ ) {
this.offset += this.metrics.getLineSize(this.currentLine);
}
maxCol = this.metrics.getLineSize(this.currentLine);
if (this.currentCol < maxCol) {
this.offset += this.currentCol;
} else {
this.currentCol = maxCol - 1;
this.offset += this.currentCol;
}
} break;
default: throw new Error("" + t);
}
this.fire("posChanged", [this, prevOffset, prevLine, prevCol]);
}
};
/**
* Set position metric. Metric describes how many lines
* and elements in these line the virtual cursor can be navigated
* @param {zebkit.util.Position.Metric} p a position metric
* @method setMetric
*/
this.setMetric = function(p) {
if (p === null || p === undefined) {
throw new Error("Null metric");
}
if (p !== this.metrics){
this.metrics = p;
this.setOffset(null);
}
};
}
]).events("posChanged");
/**
* Single column position implementation. More simple and more fast implementation of
* position class for the cases when only one column is possible.
* @param {zebkit.util.Position.Metric} m a position metric
* @constructor
* @class zebkit.util.SingleColPosition
* @extends zebkit.util.Position
*/
pkg.SingleColPosition = Class(pkg.Position, [
function $prototype() {
this.setRowCol = function(r,c) {
this.setOffset(r);
};
this.setOffset = function(o) {
if (o < 0) { o = 0;
} else if (o === null) {
o = -1;
} else {
var max = this.metrics.getMaxOffset();
if (o >= max) {
o = max;
}
}
if (o !== this.offset) {
var prevOffset = this.offset,
prevLine = this.currentLine,
prevCol = this.currentCol;
this.currentLine = this.offset = o;
this.isValid = true;
this.fire("posChanged", [this, prevOffset, prevLine, prevCol]);
}
return o;
};
this.seekLineTo = function(t, num){
if (this.offset < 0){
this.setOffset(0);
} else {
if (arguments.length === 1) {
num = 1;
}
switch(t) {
case "begin":
case "end": break;
case "up":
if (this.offset > 0) {
this.setOffset(this.offset - num);
} break;
case "down":
if (this.offset < (this.metrics.getLines() - 1)){
this.setOffset(this.offset + num);
} break;
default: throw new Error("" + t);
}
}
};
}
]);
/**
* Task set is light-weight class to host number of callbacks methods that
* are called within a context of one JS interval method execution. The
* class manages special tasks queue to run it one by one as soon as a
* dedicated interval for the given task is elapsed
var tasks = new zebkit.util.TasksSet();
tasks.run(function(t) {
// task1 body
...
if (condition) {
t.shutdown();
}
}, 1000, 200);
tasks.run(function(t) {
// task2 body
...
if (condition) {
t.shutdown();
}
}, 2000, 300);
* @constructor
* @param {Integer} [maxTasks] maximal possible number of active tasks in queue.
* @class zebkit.util.TasksSet
*/
pkg.TasksSet = Class([
function(c) {
this.tasks = Array(arguments.length > 0 ? c : 5);
// pre-fill tasks pool
for(var i = 0; i < this.tasks.length; i++) {
this.tasks[i] = new this.clazz.Task(this);
}
},
function $clazz() {
/**
* Task class
* @class zebkit.util.TasksSet.Task
* @for zebkit.util.TasksSet.Task
* @param {zebkit.util.TasksSet} tasksSet a reference to tasks set that manages the task
* @constructor
*/
this.Task = Class([
function(set) {
/**
* Reference to a tasks set that owns the task
* @type {zebkit.util.TasksSet}
* @attribute taskSet
* @private
* @readOnly
*/
this.taskSet = set;
/**
* Indicates if the task is executed (active)
* @type {Boolean}
* @attribute isStarted
* @readOnly
*/
this.isStarted = false;
},
function $prototype() {
this.task = null;
this.ri = this.si = 0;
/**
* Shutdown the given task.
* @return {Boolean} true if the task has been stopped
* @method shutdown
*/
this.shutdown = function() {
return this.taskSet.shutdown(this);
};
/**
* Pause the given task.
* @return {Boolean} true if the task has been paused
* @method pause
*/
this.pause = function() {
if (this.task === null) {
throw new Error("Stopped task cannot be paused");
}
if (this.isStarted === true) {
this.isStarted = false;
return true;
} else {
return false;
}
};
/**
* Resume the given task
* @param {Integer} [startIn] a time in milliseconds to resume the task
* @return {Boolean} true if the task has been resumed
* @method resume
*/
this.resume = function(t) {
if (this.task === null) {
throw new Error("Stopped task cannot be paused");
}
this.si = arguments.length > 0 ? t : 0;
if (this.isStarted === true) {
return false;
} else {
this.isStarted = true;
return true;
}
};
}
]);
},
/**
* @for zebkit.util.TasksSet
*/
function $prototype() {
/**
* Interval
* @attribute quantum
* @private
* @type {Number}
* @default 40
*/
this.quantum = 40;
/**
* pid of executed JS interval method callback
* @attribute pid
* @private
* @type {Number}
* @default -1
*/
this.pid = -1;
/**
* Number of run in the set tasks
* @attribute count
* @private
* @type {Number}
* @default 0
*/
this.count = 0;
/**
* Shut down all active at the given moment tasks
* body and the given context.
* @method shutdownAll
*/
this.shutdownAll = function() {
for(var i = 0; i < this.tasks.length; i++) {
this.shutdown(this.tasks[i]);
}
};
/**
* Shutdown the given task
* @param {zebkit.util.TasksSet.Task} t a task
* @return {Boolean} true if the task has been stopped, false if the task has not been started
* to be stopped
* @protected
* @method shutdown
*/
this.shutdown = function(t) {
if (t.task !== null) {
this.count--;
t.task = null;
t.isStarted = false;
t.ri = t.si = 0;
return true;
} else {
if (this.count === 0 && this.pid >= 0) {
zebkit.environment.clearInterval(this.pid);
this.pid = -1;
}
return false;
}
};
/**
* Take a free task from tasks pool and run it once in the specified period of time.
* @param {Function|Object} f a task function that has to be executed. The task method gets the task
* context as its argument. You can pass an object as the argument if the object has "run" method
* implemented. In this cases "run" method will be used as the task body.
* @param {Integer} [startIn] time in milliseconds the task has to be executed in
* @method runOnce
*/
this.runOnce = function(f, startIn) {
this.run(f, startIn, -1);
};
/**
* Take a free task from pool and run it with the specified body and the given context.
* @param {Function|Object} f a task function that has to be executed. The task method gets the task
* context as its argument. You can pass an object as the argument if the object has "run" method
* implemented. In this cases "run" method will be used as the task body.
* @param {Integer} [si] time in milliseconds the task has to be executed
* @param {Integer} [ri] the time in milliseconds the task has to be periodically repeated
* @return {zebkit.util.Task} an allocated task
* @example
var tasks = new zebkit.util.TasksSet();
// execute task
var task = tasks.run(function (t) {
// do something
...
// complete task if necessary
t.shutdown();
}, 100, 300);
// pause task
task.pause(1000, 2000);
...
// resume task in a second
task.resume(1000);
* @example
var tasks = new zebkit.util.TasksSet();
var a = new zebkit.Dummy([
function run() {
// task body
...
}
]);
// execute task
var task = tasks.runOnce(a);
* @method run
*/
this.run = function(f, si, ri){
if (f === null || f === undefined) {
throw new Error("" + f);
}
var $this = this;
function dispatcher() {
var c = 0;
for(var i = 0; i < $this.tasks.length; i++) {
var t = $this.tasks[i];
// count paused or run tasks
if (t.task !== null) { // means task has been shutdown
c++;
}
if (t.isStarted === true) {
if (t.si <= 0) {
try {
if (t.task.run !== undefined) {
t.task.run(t);
} else {
t.task(t);
}
if (t.ri < 0) {
t.shutdown();
}
} catch(e) {
zebkit.dumpError(e);
}
t.si += t.ri;
} else {
t.si -= $this.quantum;
}
}
}
if (c === 0 && $this.pid >= 0) {
zebkit.environment.clearInterval($this.pid);
$this.pid = -1;
}
}
// find free and return free task
for(var i = 0; i < this.tasks.length; i++) {
var j = (i + this.count) % this.tasks.length,
t = this.tasks[j];
if (t.task === null) {
// initialize internal variables start in and repeat in
// arguments
t.si = (arguments.length > 1) ? si : 0;
t.ri = (arguments.length > 2) ? ri : -1;
t.isStarted = true;
t.task = f;
this.count++;
if (this.count > 0 && this.pid < 0) {
this.pid = zebkit.environment.setInterval(dispatcher, this.quantum);
}
return t;
}
}
throw new Error("Out of active tasks limit (" + this.tasks.length + ")");
};
}
]);
/**
* Predefined default tasks set.
* @attribute tasksSet
* @type {zebkit.util.TasksSet}
* @for zebkit.util
*/
pkg.tasksSet = new pkg.TasksSet(7);
});
zebkit.package("data", function(pkg, Class) {
/**
* Collection of various data models. The models are widely used by zebkit UI
* components as part of model-view-controller approach, but the package doesn't depend on
* zebkit UI and can be used independently.
*
* var model = new zebkit.data.TreeModel();
* model.on("itemInserted", function(model, item) {
* // handle item inserted tree model event
* ...
* });
*
* model.add(model.root, new zebkit.data.Item("Child 1"));
* model.add(model.root, new zebkit.data.Item("Child 2"));
*
* @class zebkit.data
* @access package
*/
pkg.descent = function descent(a, b) {
if (a === undefined || a === null) {
return 1;
} else {
return zebkit.isString(a) ? a.localeCompare(b) : a - b;
}
};
pkg.ascent = function ascent(a, b) {
if (b === null || b === undefined) {
return 1;
} else {
return zebkit.isString(b) ? b.localeCompare(a) : b - a;
}
};
/**
* Data model is marker interface. It has no methods implemented, but the interface
* is supposed to be inherited with data models implementations
* @class zebkit.data.DataModel
* @interface zebkit.data.DataModel
*/
pkg.DataModel = zebkit.Interface();
/**
* Abstract text model class
* @class zebkit.data.TextModel
* @uses zebkit.data.DataModel
* @uses zebkit.EventProducer
*/
/**
* Get the given string line stored in the model
* @method getLine
* @param {Integer} line a line number
* @return {String} a string line
*/
/**
* Get wrapped by the text model original text string
* @method getValue
* @return {String} an original text
*/
/**
* Get number of lines stored in the text model
* @method getLines
* @return {Integer} a number of lines
*/
/**
* Get number of characters stored in the model
* @method getTextLength
* @return {Integer} a number of characters
*/
/**
* Write the given string in the text model starting from the specified offset
* @method write
* @param {String} s a string to be written into the text model
* @param {Integer} offset an offset starting from that the passed
* string has to be written into the text model
*/
/**
* Remove substring from the text model.
* @method remove
* @param {Integer} offset an offset starting from that a substring
* will be removed
* @param {Integer} size a size of a substring to be removed
*/
/**
* Fill the text model with the given text
* @method setValue
* @param {String} text a new text to be set for the text model
*/
/**
* Fired when the text model has been updated: a string has been
* inserted or removed
text.on(function(e) {
...
});
*
* @event textUpdated
* @param {zebkit.data.TextEvent} e a text model event
*/
pkg.TextModel = Class(pkg.DataModel, [
function $prototype() {
this.replace = function(s, off, size) {
if (s.length === 0) {
return this.remove(off, size);
} else if (size === 0) {
return this.write(s, off);
} else {
var b = this.remove(off, size, false);
return this.write(s, off) && b;
}
};
}
]).events("textUpdated");
/**
* Text model event class.
* @constructor
* @class zebkit.data.TextEvent
* @extends zebkit.Event
*/
pkg.TextEvent = Class(zebkit.Event, [
function $prototype() {
/**
* Event id.
* @attribute id
* @type {String}
*/
this.id = null;
/**
* First line number that has participated in the event action.
* @attribute line
* @type {Integer}
*/
this.line = 0;
/**
* Number of lines that have participated in the event action.
* @attribute lines
* @type {Integer}
*/
this.lines = 0;
/**
* Offset in a text.
* @attribute offset
* @type {Integer}
*/
this.offset = 0;
/**
* Number of characters.
* @attribute size
* @type {Integer}
*/
this.size = 0;
this.isLastStep = true;
/**
* Fill the event with the give parameters
* @param {zebkit.data.TextModel} src a source of the event
* @param {String} id an id of the event ("remove", "insert")
* @param {Integer} line a first line
* @param {Integer} lines a number of lines
* @param {Integer} offset an offset
* @param {Integer} size a number of characters
* @method $fillWith
* @chainable
* @protected
*/
this.$fillWith = function(src, id, line, lines, offset, size) {
this.isLastStep = true;
this.source = src;
this.id = id;
this.line = line;
this.lines = lines;
this.offset = offset;
this.size = size;
return this;
};
}
]);
var TE_STUB = new pkg.TextEvent();
/**
* Multi-lines text model implementation
* @class zebkit.data.Text
* @param {String} [s] the specified text the model has to be filled
* @constructor
* @extends zebkit.data.TextModel
*/
pkg.Text = Class(pkg.TextModel, [
function(s) {
/**
* Array of lines
* @attribute lines
* @type {zebkit.data.Text.Line[]}
* @private
* @readOnly
*/
this.$lines = [ new this.clazz.Line("") ];
this.setValue(arguments.length === 0 || s === null ? "" : s);
},
function $clazz() {
this.Line = function(s) {
this.$s = s;
};
// toString for array.join method
this.Line.prototype.toString = function() {
return this.$s;
};
},
function $prototype() {
/**
* Text length
* @attribute textLength
* @private
* @readOnly
* @type {Integer}
*/
this.textLength = 0;
/**
* Detect line by offset starting from the given line and offset.
* @param {Integer} [start] start line
* @param {Integer} [startOffset] start offset of the start line
* @param {Integer} o offset to detect line
* @private
* @method calcLineByOffset
* @return {Array} an array that consists of two elements: detected line index and its offset
*/
this.calcLineByOffset = function(start, startOffset, o) {
if (arguments.length === 1) {
startOffset = start = 0;
}
for(; start < this.$lines.length; start++){
var line = this.$lines[start].$s;
if (o >= startOffset && o <= startOffset + line.length){
return [start, startOffset];
}
startOffset += (line.length + 1);
}
return [];
};
/**
* Calculate an offset in the text the first character of the specified line.
* @param {Integer} line a line index
* @return {Integer} an offset
* @protected
* @method calcLineOffset
*/
this.calcLineOffset = function(line) {
var off = 0;
for(var i = 0; i < line; i++){
off += (this.$lines[i].$s.length + 1);
}
return off;
};
this.$lineTags = function(i) {
return this.$lines[i];
};
this.getLine = function(line) {
if (line < 0 || line >= this.$lines.length) {
throw RangeError(line);
}
return this.$lines[line].$s;
};
this.getValue = function() {
return this.$lines.join("\n");
};
this.toString = function() {
return this.$lines.join("\n");
};
this.getLines = function () {
return this.$lines.length;
};
this.getTextLength = function() {
return this.textLength;
};
/**
* Remove number of text lines starting form the specified line
* @param {Integer} start a starting line to remove text lines
* @param {Integer} [size] a number of lines to be removed. If the
* argument is not passed number equals 1
* @method removeLines
*/
this.removeLines = function(start, size) {
if (start < 0 || start >= this.$lines.length) {
throw new RangeError(start);
}
if (arguments.length === 1) {
size = 1;
} else if (size <= 0) {
throw new Error("Invalid number of lines : " + size);
}
// normalize number required lines to be removed
if ((start + size) > this.$lines.length) {
size = this.$lines.length - start;
}
var end = start + size - 1, // last line to be removed
off = this.calcLineOffset(start), // offset of the first line to be removed
olen = start !== end ? this.calcLineOffset(end) + this.$lines[end].$s.length + 1 - off
: this.$lines[start].$s.length + 1;
// if this is the last line we have to correct offset to point to "\n" character in text
if (start === this.$lines.length - 1) {
off--;
}
this.$lines.splice(start, size);
this.fire("textUpdated", TE_STUB.$fillWith(this, "remove", start, size, off, olen));
};
/**
* Insert number of lines starting from the given starting line
* @param {Integer} startLine a starting line to insert lines
* @param {String} [lines]* string lines to inserted
* @method insertLines
*/
this.insertLines = function(startLine) {
if (startLine < 0 || startLine > this.$lines.length) {
throw new RangeError(startLine);
}
var off = this.calcLineOffset(startLine), offlen = 0;
if (startLine === this.$lines.length) {
off--;
}
for(var i = 1; i < arguments.length; i++) {
offlen += arguments[i].length + 1;
this.$lines.splice(startLine + i - 1, 0, new this.clazz.Line(arguments[i]));
}
this.fire("textUpdated", TE_STUB.$fillWith(this, "insert", startLine, arguments.length - 1, off, offlen));
};
this.write = function (s, offset, b) {
if (s.length > 0) {
var slen = s.length,
info = this.calcLineByOffset(0, 0, offset),
line = this.$lines[info[0]].$s,
j = 0,
lineOff = offset - info[1],
tmp = line.substring(0, lineOff) + s + line.substring(lineOff);
for(; j < slen && s[j] !== '\n'; j++) {
}
if (j >= slen) { // means the update has occurred withing one line
this.$lines[info[0]].$s = tmp;
j = 1;
} else {
this.$lines.splice(info[0], 1); // remove line
j = this.parse(info[0], tmp); // re-parse the updated part of text
}
if (slen > 0) {
this.textLength += slen;
TE_STUB.$fillWith(this, "insert", info[0], j, offset, slen);
if (arguments.length > 2) {
TE_STUB.isLastStep = b;
}
this.fire("textUpdated", TE_STUB);
return true;
}
}
return false;
};
this.remove = function(offset, size, b) {
if (size > 0) {
var i1 = this.calcLineByOffset(0, 0, offset),
i2 = this.calcLineByOffset(i1[0], i1[1], offset + size),
l1 = this.$lines[i1[0]].$s,
l2 = this.$lines[i2[0]].$s,
off1 = offset - i1[1],
off2 = offset + size - i2[1],
buf = l1.substring(0, off1) + l2.substring(off2);
if (i2[0] === i1[0]) {
this.$lines.splice(i1[0], 1, new this.clazz.Line(buf));
} else {
this.$lines.splice(i1[0], i2[0] - i1[0] + 1);
this.$lines.splice(i1[0], 0, new this.clazz.Line(buf));
}
if (size > 0) {
this.textLength -= size;
TE_STUB.$fillWith(this, "remove", i1[0], i2[0] - i1[0] + 1, offset, size);
if (arguments.length > 2) {
TE_STUB.isLastStep = b;
}
this.fire("textUpdated", TE_STUB);
return true;
}
}
return false;
};
this.parse = function (startLine, text) {
var size = text.length,
prevIndex = 0,
prevStartLine = startLine;
for(var index = 0; index <= size; prevIndex = index, startLine++) {
var fi = text.indexOf("\n", index);
index = (fi < 0 ? size : fi);
this.$lines.splice(startLine, 0, new this.clazz.Line(text.substring(prevIndex, index)));
index++;
}
return startLine - prevStartLine;
};
this.setValue = function(text) {
var old = this.getValue();
if (old !== text) {
if (old.length > 0) {
var numLines = this.getLines(), txtLen = this.getTextLength();
this.$lines.length = 0;
this.$lines = [ new this.clazz.Line("") ];
TE_STUB.$fillWith(this, "remove", 0, numLines, 0, txtLen);
TE_STUB.isLastStep = false;
this.fire("textUpdated", TE_STUB);
}
this.$lines = [];
this.parse(0, text);
this.textLength = text.length;
this.fire("textUpdated", TE_STUB.$fillWith(this, "insert", 0, this.getLines(), 0, this.textLength));
return true;
}
return false;
};
}
]);
/**
* Single line text model implementation
* @param {String} [s] the specified text the model has to be filled
* @param {Integer} [max] the specified maximal text length
* @constructor
* @class zebkit.data.SingleLineTxt
* @extends zebkit.data.TextModel
*/
pkg.SingleLineTxt = Class(pkg.TextModel, [
function (s, max) {
if (arguments.length > 1) {
this.maxLen = max;
}
this.setValue(arguments.length === 0 || s === null ? "" : s);
},
function $prototype() {
this.$buf = "";
this.extra = 0;
/**
* Maximal text length. -1 means the text is not restricted
* regarding its length.
* @attribute maxLen
* @type {Integer}
* @default -1
* @readOnly
*/
this.maxLen = -1;
this.$lineTags = function(i) {
return this;
};
this.getValue = function(){
return this.$buf;
};
this.toString = function() {
return this.$buf;
};
/**
* Get number of lines stored in the text model. The model
* can have only one line
* @method getLines
* @return {Integer} a number of lines
*/
this.getLines = function(){
return 1;
};
this.getTextLength = function(){
return this.$buf.length;
};
this.getLine = function(line){
if (line !== 0) {
throw new RangeError(line);
}
return this.$buf;
};
this.write = function(s, offset, b) {
// cut to the first new line character
var j = s.indexOf("\n");
if (j >= 0) {
s = s.substring(0, j);
}
var l = (this.maxLen > 0 && (this.$buf.length + s.length) >= this.maxLen) ? this.maxLen - this.$buf.length
: s.length;
if (l !== 0) {
var nl = (offset === this.$buf.length ? this.$buf + s.substring(0, l) // append
: this.$buf.substring(0, offset) +
s.substring(0, l) +
this.$buf.substring(offset));
this.$buf = nl;
if (l > 0) {
TE_STUB.$fillWith(this, "insert", 0, 1, offset, l);
if (arguments.length > 2) {
TE_STUB.isLastStep = b;
}
this.fire("textUpdated", TE_STUB);
return true;
}
}
return false;
};
this.remove = function(offset, size, b) {
if (size > 0 && offset < this.$buf.length) {
// normalize size
if (offset + size > this.$buf.length) {
size = this.$buf.length - offset;
}
if (size > 0) {
// build new cut line
var nl = this.$buf.substring(0, offset) +
this.$buf.substring(offset + size);
if (nl.length !== this.$buf.length) {
this.$buf = nl;
TE_STUB.$fillWith(this, "remove", 0, 1, offset, size);
if (arguments.length > 2) {
TE_STUB.isLastStep = b;
}
this.fire("textUpdated", TE_STUB);
return true;
}
}
}
return false;
};
this.setValue = function(text){
// cut to next line
var i = text.indexOf('\n');
if (i >= 0) {
text = text.substring(0, i);
}
if (this.$buf === null || this.$buf !== text) {
if (this.$buf !== null && this.$buf.length > 0) {
TE_STUB.$fillWith(this, "remove", 0, 1, 0, this.$buf.length);
TE_STUB.isLastStep = false;
this.fire("textUpdated", TE_STUB);
}
if (this.maxLen > 0 && text.length > this.maxLen) {
text = text.substring(0, this.maxLen);
}
this.$buf = text;
this.fire("textUpdated", TE_STUB.$fillWith(this, "insert", 0, 1, 0, text.length));
return true;
}
return false;
};
/**
* Set the given maximal length the text can have
* @method setMaxLength
* @param {Integer} max a maximal length of text
*/
this.setMaxLength = function (max){
if (max !== this.maxLen){
this.maxLen = max;
this.setValue("");
}
};
}
]);
/**
* List model class
* @param {Array} [a] an array the list model has to be initialized with
* @example
// create list model that contains three integer elements
var l = new zebkit.data.ListModel([1,2,3]);
l.on("elementInserted", function(list, element, index) {
// handle list item inserted event
...
})
...
l.add(10)
* @constructor
* @class zebkit.data.ListModel
* @uses zebkit.data.DataModel
* @uses zebkit.EventProducer
*/
/**
* Fired when a new element has been added to the list model
list.on("elementInserted", function(src, o, i) {
...
});
* @event elementInserted
* @param {zebkit.data.ListModel} src a list model that triggers the event
* @param {Object} o an element that has been added
* @param {Integer} i an index at that the new element has been added
*/
/**
* Fired when an element has been removed from the list model
list.on("elementRemoved", function(src, o, i) {
...
});
* @event elementRemoved
* @param {zebkit.data.ListModel} src a list model that triggers the event
* @param {Object} o an element that has been removed
* @param {Integer} i an index at that the element has been removed
*/
/**
* Fired when an element has been re-set
list.on("elementSet", function(src, o, p, i) {
...
});
* @event elementSet
* @param {zebkit.data.ListModel} src a list model that triggers the event
* @param {Object} o an element that has been set
* @param {Object} p a previous element
* @param {Integer} i an index at that the element has been re-set
*/
pkg.ListModel = Class(pkg.DataModel, zebkit.EventProducer,[
function() {
this.$data = (arguments.length === 0) ? [] : arguments[0];
},
function $prototype() {
/**
* Get an item stored at the given location in the list
* @method get
* @param {Integer} i an item location
* @return {object} a list item
*/
this.get = function(i) {
if (i < 0 || i >= this.$data.length) {
throw new RangeError(i);
}
return this.$data[i];
};
/**
* Add the given item to the end of the list
* @method add
* @param {Object} o an item to be added
*/
this.add = function(o) {
this.$data.push(o);
this.fire("elementInserted", [this, o, this.$data.length - 1]);
};
/**
* Remove all elements from the list model
* @method removeAll
*/
this.removeAll = function() {
var size = this.$data.length;
for(var i = size - 1; i >= 0; i--) {
this.removeAt(i);
}
};
/**
* Remove an element at the given location of the list model
* @method removeAt
* @param {Integer} i a location of an element to be removed from the list
*/
this.removeAt = function(i) {
var re = this.$data[i];
this.$data.splice(i, 1);
this.fire("elementRemoved", [this, re, i]);
};
/**
* Remove the given element from the list
* @method remove
* @param {Object} o an element to be removed from the list
*/
this.remove = function(o) {
for(var i = 0;i < this.$data.length; i++) {
if (this.$data[i] === o) {
this.removeAt(i);
}
}
};
/**
* Insert the given element into the given position of the list
* @method insert
* @param {Integer} i a position at which the element has to be inserted into the list
* @param {Object} o an element to be inserted into the list
*/
this.insert = function(i, o){
if (i < 0 || i > this.$data.length) {
throw new RangeError(i);
}
this.$data.splice(i, 0, o);
this.fire("elementInserted", [this, o, i]);
};
/**
* Get number of elements stored in the list
* @method count
* @return {Integer} a number of element in the list
*/
this.count = function () {
return this.$data.length;
};
/**
* Set the new element at the given position
* @method setAt
* @param {Integer} i a position
* @param {Object} o a new element to be set as the list element at the given position
* @return {Object} previous element that was stored at the given position
*/
this.setAt = function(i, o) {
if (i < 0 || i >= this.$data.length) {
throw new RangeError(i);
}
var pe = this.$data[i];
this.$data[i] = o;
this.fire("elementSet", [this, o, pe, i]);
return pe;
};
/**
* Check if the element is in the list
* @method contains
* @param {Object} o an element to be checked
* @return {Boolean} true if the element is in the list
*/
this.contains = function (o){
return this.indexOf(o) >= 0;
};
/**
* Get position the given element is stored in the list
* @method indexOf
* @param {Object} o an element
* @return {Integer} the element position. -1 if the element cannot be found in the list
*/
this.indexOf = function(o){
return this.$data.indexOf(o);
};
}
]).events("elementInserted", "elementRemoved", "elementSet");
/**
* Tree model item class. The structure is used by tree model to store
* tree items values, parent and children item references.
* @class zebkit.data.Item
* @param {Object} [v] the item value
* @constructor
*/
pkg.Item = Class([
function(v) {
/**
* Array of children items of the item element
* @attribute kids
* @type {Array}
* @default []
* @readOnly
*/
this.kids = [];
if (arguments.length > 0) {
this.value = v;
}
},
function $prototype() {
/**
* Reference to a parent item
* @attribute parent
* @type {zebkit.data.Item}
* @default null
* @readOnly
*/
this.parent = null;
/**
* The tree model item value. It is supposed the value should be updated
* via execution of "setValue(...)" method of a tree model the item
* belongs to.
* @attribute value
* @default null
* @type {Object}
* @readOnly
*/
this.value = null;
}
]).hashable();
/**
* Tree model class. The class is simple and handy way to keep hierarchical structure.
*
* @param {zebkit.data.Item|Object} [r] a root item. As the argument you can pass "zebkit.data.Item" or
* a JavaScript object. In the second case you can describe the tree as it is shown in example below:
* @example
// create tree model initialized with tree structure passed as
// special formated JavaScript object. The tree will look as follow:
// "Root"
// |
// +--- "Root kid 1"
// +--- "Root kid 2"
// |
// +--- "Kid of kid 2"
var tree = new zebkit.data.TreeModel({
value:"Root",
kids: [
"Root kid 1",
{
value: "Root kid 2",
kids: [ "Kid of kid 2"]
}
]
});
...
// reg item modified events handler
tree.on("itemModified", function(tree, item, prevValue) {
// catch item value modification
...
});
// item value has to be updated via tree model API
tree.setValue(tree.root.kids[0], "new value");
* @class zebkit.data.TreeModel
* @uses zebkit.data.DataModel
* @uses zebkit.EventProducer
* @constructor
*/
/**
* Fired when the tree model item value has been updated.
tree.on("itemModified", function(src, item, prevValue) {
...
});
* @event itemModified
* @param {zebkit.data.TreeModel} src a tree model that triggers the event
* @param {zebkit.data.Item} item an item whose value has been updated
* @param {Object} prevValue a previous value the item has had
*/
/**
* Fired when the tree model item has been removed
tree.on("itemRemoved", function(src, item) {
...
});
* @event itemRemoved
* @param {zebkit.data.TreeModel} src a tree model that triggers the event
* @param {zebkit.data.Item} item an item that has been removed from the tree model
*/
/**
* Fired when the tree model item has been inserted into the model
tree.on("itemInserted", function(src, item) {{
...
});
* @event itemInserted
* @param {zebkit.data.TreeModel} src a tree model that triggers the event
* @param {zebkit.data.Item} item an item that has been inserted into the tree model
*/
pkg.TreeModel = Class(pkg.DataModel, [
function(r) {
if (arguments.length === 0) {
this.root = new pkg.Item();
} else {
this.root = zebkit.instanceOf(r, pkg.Item) ? r : this.clazz.create(r);
}
},
function $clazz() {
/**
* Create tree model item hierarchy by the given JavaScript object.
* @param {Object} r
* @return {zebkit.data.Item} a built items hierarchy
* @example
*
* // create the following items hierarchy:
* // "Root"
* // +--- "Kid 1"
* // | +--- "Kid 1.1"
* // | | +--- "Kid 1.1.1"
* // | +--- "Kid 2.2"
* // +--- "Kid 2"
* // | +--- "Kid 2.1"
* // | +--- "Kid 2.2"
* // | +--- "Kid 2.3"
* // +--- "Kid 3"
* //
* var rootItem = zebkit.data.TreeModel.create({
* value : "Root",
* kids : [
* { value : "Kid 1"
* kids : [
* { value: "Kid 1.1",
* kids : "Kid 1.1.1"
* },
* "Kid 2.2"
* ]
* },
* { value: "Kid 2",
* kids : ["Kid 2.1", "Kid 2.2", "Kid 2.3"]
* },
* "Kid 3"
* ]
* });
*
* @static
* @method create
*/
this.create = function(r, p) {
var item = new pkg.Item(r.hasOwnProperty("value")? r.value : r);
item.parent = arguments.length < 2 ? null : p;
if (r.kids !== undefined && r.kids !== null) {
for(var i = 0; i < r.kids.length; i++) {
item.kids[i] = this.create(r.kids[i], item);
}
}
return item;
};
/**
* Find the first tree item (starting from the specified root item) whose value equals the given value.
* @param {zebkit.data.Item} root a root item of the tree
* @param {Object} value a value to evaluate
* @return {zebkit.data.Item} a found tree item
* @static
* @method findOne
*/
this.findOne = function(root, value) {
var res = null;
this.find(root, value, function(item) {
res = item;
return true;
});
return res;
};
/**
* Find all items (starting from the specified root item) whose value equals the given value.
* @param {zebkit.data.Item} root a root item of the tree
* @param {Object} value a value to evaluate
* @param {Function} [cb] a callback method that is called for every tree item whose value matches
* the specified one. The method gets the found item as its argument. The method can return true
* if the tree traversing has to be interrupted.
* @return {Array} a list of all found item whose value matches the specified one. The array is returned
* only if no callback method has been passed to the method.
* @example
*
* // create tree items
* var rootItem = zebkit.data.TreeModel.create({
* value: "Root",
* kids : [ "Kid 1", "Kid 2", "Kid 1", "Kid 3", "Kid 1" ]
* });
*
* // find all items that have its value set to "Kid 1" and return
* // it as array
* var items = zebkit.data.TreeModel.find(rootItem, "Kid 1");
*
* // find the first two "Kid 1" item in the tree using callback
* var items = [];
* zebkit.data.TreeModel.find(rootItem, "Kid 1", function(item) {
* items.push(item);
*
* // stop the tree traversing as soon as we found two items
* return items.length > 1;
* });
*
* @static
* @method find
*/
this.find = function(root, value, cb) {
if (arguments.length < 3) {
var res = [];
this.find(root, value, function(item) {
res.push(item);
return false;
});
return res;
}
if (root.value === value) {
if (cb.call(this, root) === true) {
return true;
}
}
if (root.kids !== undefined && root.kids !== null) {
for (var i = 0; i < root.kids.length; i++) {
if (this.find(root.kids[i], value, cb)) {
return true;
}
}
}
return false;
};
this.print = function(root, render, shift) {
if (zebkit.instanceOf(root, pkg.TreeModel)) {
root = root.root;
}
if (arguments.length < 2) {
shift = "";
render = null;
} else if (arguments.length === 2) {
if (zebkit.isString(render)) {
shift = render;
render = null;
} else {
shift = "";
}
}
var b = root.kids !== undefined && root.kids !== null;
if (render !== null) {
render(root);
}
if (b) {
shift = shift + " ";
for (var i = 0; i < root.kids.length; i++) {
this.print(root.kids[i], render, shift);
}
}
};
},
function $prototype() {
/**
* Reference to the tree model root item
* @attribute root
* @type {zebkit.data.Item}
* @readOnly
*/
this.root = null;
/**
* Iterate over tree hierarchy starting from its root element
* @param {zebkit.data.Item} r a root element to start traversing the tree model
* @param {Function} f a callback function that is called for every tree item traversed item.
* The callback gets tree model and the item as its arguments
* @method iterate
*/
this.iterate = function(r, f) {
var res = f.call(this, r);
if (res === 1 || res === 2) { //TODO: make it clear what is a mening of the res ?
return r;
}
for (var i = 0; i < r.kids.length; i++) {
res = this.iterate(r.kids[i], f);
if (res === 2) {
return res;
}
}
};
/**
* Update a value of the given tree model item with the new one
* @method setValue
* @param {zebkit.data.Item} item an item whose value has to be updated
* @param {Object} v a new item value
*/
this.setValue = function(item, v){
var prev = item.value;
item.value = v;
this.fire("itemModified", [this, item, prev]);
};
/**
* Add the new item to the tree model as a children element of the given parent item
* @method add
* @param {zebkit.data.Item} [to] a parent item to which the new item has to be added.
* If it has not been passed the node will be added to root.
* @param {Object|zebkit.data.Item} an item or value of the item to be
* added to the parent item of the tree model
*/
this.add = function(to,item) {
if (arguments.length < 2) {
to = this.root;
}
this.insert(to, item, to.kids.length);
};
/**
* Insert the new item to the tree model as a children element at the
* given position of the parent element
* @method insert
* @param {zebkit.data.Item} to a parent item to which the new item
* has to be inserted
* @param {Object|zebkit.data.Item} an item or value of the item to be
* inserted to the parent item
* @param {Integer} i a position the new item has to be inserted into
* the parent item
*/
this.insert = function(to, item, i) {
if (i < 0 || to.kids.length < i) {
throw new RangeError(i);
}
if (zebkit.isString(item)) {
item = new pkg.Item(item);
}
to.kids.splice(i, 0, item);
item.parent = to;
this.fire("itemInserted", [this, item]);
// !!!
// it is necessary to analyze if the inserted item has kids and
// generate inserted event for all kids recursively
};
/**
* Remove the given item from the tree model
* @method remove
* @param {zebkit.data.Item} item an item to be removed from the tree model
*/
this.remove = function(item){
if (item === this.root) {
this.root = null;
} else {
if (item.kids !== undefined) {
for(var i = item.kids.length - 1; i >= 0; i--) {
this.remove(item.kids[i]);
}
}
item.parent.kids.splice(item.parent.kids.indexOf(item), 1);
}
// preserve reference to parent when we call a listener
try {
this.fire("itemRemoved", [this, item]);
} catch(e) {
item.parent = null;
throw e;
}
item.parent = null;
};
/**
* Remove all children items from the given item of the tree model
* @method removeKids
* @param {zebkit.data.Item} item an item from that all children items have to be removed
*/
this.removeKids = function(item) {
for(var i = item.kids.length - 1; i >= 0; i--) {
this.remove(item.kids[i]);
}
};
}
]).events("itemModified", "itemRemoved", "itemInserted");
/**
* Matrix model class.
* @constructor
* @param {Array} [data] the given data as two dimensional array
* @param {Integer} [rows] a number of rows
* @param {Integer} [cols] a number of columns
* @class zebkit.data.Matrix
* @uses zebkit.EventProducer
* @uses zebkit.data.DataModel
* @example
*
* // create matrix with 10 rows and 5 columns
* var matrix = zebkit.data.Matrix(10, 5);
*
* matrix.get(0,0);
* matrix.put(0,0, "Cell [0,0]");
*
* @example
*
* // create matrix with 3 rows and 5 columns
* var matrix = zebkit.data.Matrix([
* [ 0, 1, 2, 3, 4 ], // row 0
* [ 0, 1, 2, 3, 4 ], // row 1
* [ 0, 1, 2, 3, 4 ], // row 2
* [ 0, 1, 2, 3, 4 ], // row 3
* [ 0, 1, 2, 3, 4 ] // row 4
* ]);
*
* @example
*
* // create matrix with 0 rows and 0 columns
* var matrix = zebkit.data.Matrix();
*
* // setting value for cell (2, 4) will change
* // matrix size to 2 rows and 3 columns
* matrix.put(2, 4, "Cell [row = 2, col = 4]");
*/
/**
* Fired when the matrix model size (number of rows or columns) is changed.
matrix.on("matrixResized", function(src, pr, pc) {
...
});
* @event matrixResized
* @param {zebkit.data.Matrix} src a matrix that triggers the event
* @param {Integer} pr a previous number of rows
* @param {Integer} pc a previous number of columns
*/
/**
* Fired when the matrix model cell has been updated.
matrix.on("cellModified", function(src, row, col, old) {
...
});
* @event cellModified
* @param {zebkit.data.Matrix} src a matrix that triggers the event
* @param {Integer} row an updated row
* @param {Integer} col an updated column
* @param {Object} old a previous cell value
*/
/**
* Fired when the matrix data has been re-ordered.
matrix.on("matrixSorted", function(src, sortInfo) {
...
});
* @event matrixSorted
* @param {zebkit.data.Matrix} src a matrix that triggers the event
* @param {Object} sortInfo a new data order info. The information
* contains:
*
* {
* func: sortFunction,
* name: sortFunctionName,
* col : sortColumn
* }
*
*/
/**
* Fired when a row has been inserted into the matrix.
matrix.on("matrixRowInserted", function(src, rowIndex) {
...
});
* @event matrixColInserted
* @param {zebkit.data.Matrix} src a matrix that triggers the event
* @param {Integer} rowIndex a row that has been inserted
* contains:
*/
/**
* Fired when a column has been inserted into the matrix.
matrix.on("matrixColInserted", function(src, colIndex) {
...
});
* @event matrixColInserted
* @param {zebkit.data.Matrix} src a matrix that triggers the event
* @param {Integer} colIndex a column that has been inserted
* contains:
*/
pkg.Matrix = Class(pkg.DataModel, [
function() {
/**
* Number of rows in the matrix model
* @attribute rows
* @type {Integer}
* @readOnly
*/
/**
* Number of columns in the matrix model
* @attribute cols
* @type {Integer}
* @readOnly
*/
/**
* The multi-dimensional embedded arrays to host matrix data
* @attribute $objs
* @type {Array}
* @readOnly
* @private
*/
if (arguments.length === 1) {
this.$objs = arguments[0];
this.cols = (this.$objs.length > 0) ? this.$objs[0].length : 0;
this.rows = this.$objs.length;
} else {
this.$objs = [];
this.rows = this.cols = 0;
if (arguments.length > 1) {
this.setRowsCols(arguments[0], arguments[1]);
}
}
},
function $prototype() {
/**
* Get a matrix model cell value at the specified row and column
* @method get
* @param {Integer} row a cell row
* @param {Integer} col a cell column
* @return {Object} matrix model cell value
*/
this.get = function (row,col){
if (row < 0 || row >= this.rows) {
throw new RangeError(row);
}
if (col < 0 || col >= this.cols) {
throw new RangeError(col);
}
return this.$objs[row] === undefined ? undefined : this.$objs[row][col];
};
/**
* Get the given column data as an array object
* @param {Integer} col a column
* @return {Array} a column data
* @method getCol
*/
this.getCol = function(col) {
var r = [];
for (var i = 0; i < this.rows ; i++) {
r[i] = this.get(i, col);
}
return r;
};
/**
* Get the given row data as an array object
* @param {Integer} row a row
* @return {Array} a row data
* @method getRow
*/
this.getRow = function(row) {
var r = [];
for (var i = 0; i < this.cols ; i++) {
r[i] = this.get(row, i);
}
return r;
};
/**
* Get a matrix model cell value by the specified index
* @method geti
* @param {Integer} index a cell index
* @return {Object} matrix model cell value
*/
this.geti = function(i) {
return this.get(Math.floor(i / this.cols), i % this.cols);
};
/**
* Set the specified by row and column cell value. If the specified row or column
* is greater than the matrix model has the model size will be adjusted to new one.
* @method put
* @param {Integer} row a cell row
* @param {Integer} col a cell column
* @param {Object} obj a new cell value
* @chainable
*/
this.put = function(row,col,obj){
var nr = this.rows,
nc = this.cols;
if (row >= nr) {
nr += (row - nr + 1);
}
if (col >= nc) {
nc += (col - nc + 1);
}
this.setRowsCols(nr, nc);
var old = this.$objs[row] !== undefined ? this.$objs[row][col] : undefined;
if (old === undefined || obj !== old) {
// allocate array if no data for the given row exists
if (this.$objs[row] === undefined) {
this.$objs[row] = [];
}
this.$objs[row][col] = obj;
this.fire("cellModified", [this, row, col, old]);
}
return this;
};
/**
* Set the specified by index cell value. The index identifies cell starting from [0,0]
* cell till [rows,columns]. If the index is greater than size of model the model size
* will be adjusted to new one.
* @method puti
* @param {Integer} i a cell row
* @param {Object} obj a new cell value
* @chainable
*/
this.puti = function(i, obj){
this.put( Math.floor(i / this.cols),
i % this.cols, obj);
return this;
};
/**
* Set the given number of rows and columns the model has to have.
* @method setRowsCols
* @param {Integer} rows a new number of rows
* @param {Integer} cols a new number of columns
* @chainable
*/
this.setRowsCols = function(rows, cols){
if (rows !== this.rows || cols !== this.cols){
var pc = this.cols,
pr = this.rows;
this.cols = cols;
this.rows = rows;
// re-locate matrix space
if (this.$objs.length > rows) {
this.$objs.length = rows; // shrink number of rows
}
// shrink columns
if (pc > cols) {
for(var i = 0; i < this.$objs.length; i++) {
// check if data for columns has been allocated and the size
// is greater than set number of columns
if (this.$objs[i] !== undefined && this.$objs[i].length > cols) {
this.$objs[i].length = cols;
}
}
}
this.fire("matrixResized", [this, pr, pc]);
}
return this;
};
/**
* Set the given number of rows the model has to have.
* @method setRows
* @param {Integer} rows a new number of rows
* @chainable
*/
this.setRows = function(rows) {
this.setRowsCols(rows, this.cols);
return this;
};
/**
* Set the given number of columns the model has to have.
* @method setCols
* @param {Integer} cols a new number of columns
* @chainable
*/
this.setCols = function(cols) {
this.setRowsCols(this.rows, cols);
return this;
};
/**
* Remove specified number of rows from the model starting
* from the given row.
* @method removeRows
* @param {Integer} begrow a start row
* @param {Integer} count a number of rows to be removed
* @chainable
*/
this.removeRows = function(begrow, count) {
if (arguments.length === 1) {
count = 1;
}
if (begrow < 0 || begrow + count > this.rows) {
throw new RangeError(begrow);
}
this.$objs.splice(begrow, count);
this.rows -= count;
this.fire("matrixResized", [this, this.rows + count, this.cols]);
return this;
};
/**
* Remove specified number of columns from the model starting
* from the given column.
* @method removeCols
* @param {Integer} begcol a start column
* @param {Integer} count a number of columns to be removed
* @chainable
*/
this.removeCols = function (begcol, count){
if (arguments.length === 1) {
count = 1;
}
if (begcol < 0 || begcol + count > this.cols) {
throw new RangeError(begcol);
}
for(var i = 0; i < this.$objs.length; i++) {
if (this.$objs[i] !== undefined && this.$objs[i].length > 0) {
this.$objs[i].splice(begcol, count);
}
}
this.cols -= count;
this.fire("matrixResized", [this, this.rows, this.cols + count]);
return this;
};
/**
* Insert the given number of rows at the specified row
* @param {Integer} row a starting row to insert
* @param {Integer} count a number of rows to be added
* @method insertRows
* @chainable
*/
this.insertRows = function(row, count) {
if (arguments.length === 1) {
count = 1;
}
var i = 0;
if (row <= this.$objs.length - 1) {
for(i = 0; i < count; i++) {
this.$objs.splice(row, 0, undefined);
this.fire("matrixRowInserted", [this, row + i]);
}
} else {
for(i = 0; i < count; i++) {
this.fire("matrixRowInserted", [this, row + i]);
}
}
this.rows += count;
this.fire("matrixResized", [this, this.rows - count, this.cols]);
return this;
};
/**
* Insert the given number of columns at the specified column
* @param {Integer} col a starting column to insert
* @param {Integer} count a number of columns to be added
* @method insertCols
* @chainable
*/
this.insertCols = function(col, count) {
if (arguments.length === 1) {
count = 1;
}
if (this.$objs.length > 0) {
for(var j = 0; j < count; j++) {
for(var i = 0; i < this.rows; i++) {
if (this.$objs[i] !== undefined && j <= this.$objs[i].length) {
this.$objs[i].splice(col, 0, undefined);
}
}
this.fire("matrixColInserted", [this, col + j]);
}
}
this.cols += count;
this.fire("matrixResized", [this, this.rows, this.cols - count]);
return this;
};
/**
* Insert the column data at the given column
* @param {Integer} col a column
* @param {Array} [data] a column data
* @method insertCol
* @chainable
*/
this.insertCol = function(col, data) {
this.insertCols(col, 1);
if (arguments.length > 0) {
for (var i = 0; i < Math.min(data.length, this.rows); i++) {
this.put(i, col, data[i]);
}
}
return this;
};
/**
* Insert the row data at the given row
* @param {Integer} row a row
* @param {Array} [data] a row data
* @method insertRow
* @chainable
*/
this.insertRow = function(row, data) {
this.insertRows(row, 1);
if (arguments.length > 0) {
for (var i = 0; i < Math.min(data.length, this.cols); i++) {
this.put(row, i, data[i]);
}
}
return this;
};
/**
* Sort the given column of the matrix model.
* @param {Integer} col a column to be re-ordered
* @param {Function} [f] an optional sort function. The name of the function
* is grabbed to indicate type of the sorting the method does. For instance:
* "descent", "ascent".
* @method sortCol
*/
this.sortCol = function(col, f) {
if (arguments.length < 2) {
f = pkg.descent;
}
this.$objs.sort(function(a, b) {
return f(a[col], b[col]);
});
this.fire("matrixSorted", [ this, { col : col,
func: f,
name: zebkit.$FN(f).toLowerCase() }]);
};
}
]).events("matrixResized", "cellModified",
"matrixSorted", "matrixRowInserted",
"matrixColInserted");
});
zebkit.package("io", function(pkg, Class) {
/**
* The module provides number of classes to help to communicate with remote services and servers by HTTP,
* JSON-RPC, XML-RPC protocols.
*
* // shortcut method to perform HTTP GET request
* zebkit.io.GET("http://test.com").then(function(req) {
* // handle request
* req.responseText
* ...
* }).catch(function(exception) {
* // handle error
* });
*
* @class zebkit.io
* @access package
*/
// TODO: Web dependencies:
// -- Uint8Array
// -- ArrayBuffer
// !!!
// b64 is supposed to be used with binary stuff, applying it to utf-8 encoded data can bring to error
// !!!
var HEX = "0123456789ABCDEF",
b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
/**
* Generate UUID of the given length
* @param {Integer} [size] the generated UUID length. The default size is 16 characters.
* @return {String} an UUID
* @method UID
* @for zebkit.io
*/
pkg.UID = function(size) {
if (arguments.length === 0) {
size = 16;
}
var id = "";
for (var i = 0; i < size; i++) {
id = id + HEX[~~(Math.random() * 16)];
}
return id;
};
/**
* Encode the given string into base64
* @param {String} input a string to be encoded
* @method b64encode
* @for zebkit.io
*/
pkg.b64encode = function(input) {
var out = [], i = 0, len = input.length, c1, c2, c3;
if (input instanceof ArrayBuffer) {
input = new Uint8Array(input);
}
input.charCodeAt = function(i) { return this[i]; };
if (Array.isArray(input)) {
input.charCodeAt = function(i) { return this[i]; };
}
while(i < len) {
c1 = input.charCodeAt(i++) & 0xff;
out.push(b64str.charAt(c1 >> 2));
if (i === len) {
out.push(b64str.charAt((c1 & 0x3) << 4), "==");
break;
}
c2 = input.charCodeAt(i++);
out.push(b64str.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)));
if (i === len) {
out.push(b64str.charAt((c2 & 0xF) << 2), "=");
break;
}
c3 = input.charCodeAt(i++);
out.push(b64str.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)), b64str.charAt(c3 & 0x3F));
}
return out.join('');
};
/**
* Decode the base64 encoded string
* @param {String} input base64 encoded string
* @return {String} a string
* @for zebkit.io
* @method b64decode
*/
pkg.b64decode = function(input) {
var output = [], chr1, chr2, chr3, enc1, enc2, enc3, enc4;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while ((input.length % 4) !== 0) {
input += "=";
}
for(var i=0; i < input.length;) {
enc1 = b64str.indexOf(input.charAt(i++));
enc2 = b64str.indexOf(input.charAt(i++));
enc3 = b64str.indexOf(input.charAt(i++));
enc4 = b64str.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output.push(String.fromCharCode(chr1));
if (enc3 !== 64) {
output.push(String.fromCharCode(chr2));
}
if (enc4 !== 64) {
output.push(String.fromCharCode(chr3));
}
}
return output.join('');
};
pkg.dateToISO8601 = function(d) {
function pad(n) { return n < 10 ? '0'+ n : n; }
return [ d.getUTCFullYear(), '-', pad(d.getUTCMonth()+1), '-', pad(d.getUTCDate()), 'T', pad(d.getUTCHours()), ':',
pad(d.getUTCMinutes()), ':', pad(d.getUTCSeconds()), 'Z'].join('');
};
// http://webcloud.se/log/JavaScript-and-ISO-8601/
pkg.ISO8601toDate = function(v) {
var regexp = ["([0-9]{4})(-([0-9]{2})(-([0-9]{2})", "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?",
"(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"].join(''), d = v.match(new RegExp(regexp)),
offset = 0, date = new Date(d[1], 0, 1);
if (d[3]) {
date.setMonth(d[3] - 1);
}
if (d[5]) {
date.setDate(d[5]);
}
if (d[7]) {
date.setHours(d[7]);
}
if (d[8]) {
date.setMinutes(d[8]);
}
if (d[10]) {
date.setSeconds(d[10]);
}
if (d[12]) {
date.setMilliseconds(Number("0." + d[12]) * 1000);
}
if (d[14]) {
offset = (Number(d[16]) * 60) + Number(d[17]);
offset *= ((d[15] === '-') ? 1 : -1);
}
offset -= date.getTimezoneOffset();
date.setTime(Number(date) + (offset * 60 * 1000));
return date;
};
/**
* HTTP request class. This class provides API to generate different
* (GET, POST, etc) HTTP requests
* @class zebkit.io.HTTP
* @constructor
* @param {String} url an URL to a HTTP resource
*/
pkg.HTTP = Class([
function(url) {
this.url = url;
this.header = {};
},
function $prototype() {
/**
* Perform HTTP GET request with the given query parameters.
* @param {Object} [q] a dictionary of query parameters
* @return {zebkit.DoIt} an object to get response
* @example
*
* // GET request with the number of query parameters
* var result = zebkit.io.HTTP("google.com").GET({
* param1: "var1",
* param3: "var2",
* param3: "var3"
* }).then(function(req) {
* // handle response
* req.responseText;
* }).catch(function(e) {
* // handle error
* ...
* });
*
* @method GET
*/
this.GET = function(q) {
var u = this.url + ((arguments.length === 0 || q === null) ? ''
: ((this.url.indexOf("?") > 0) ? '&'
: '?') + zebkit.URI.toQS(q, true));
return this.SEND("GET", u);
};
/**
* Perform HTTP POST request with the give data to be sent.
* @param {String|Object} d a data to be sent by HTTP POST request. It can be
* either a parameters set or a string.
* @return {zebkit.DoIt} an object to get response
* @example
*
* // asynchronously send POST
* zebkit.io.HTTP("google.com").POST("Hello").then(function(req) {
* // handle HTTP GET response ...
* }).catch(function(e) {
* // handle error ...
* });
*
* Or you can pass a number of parameters to be sent:
*
* // send parameters synchronously by HTTP POST request
* zebkit.io.HTTP("google.com").POST({
* param1: "val1",
* param2: "val3",
* param3: "val3"
* }).then(function(req) {
* // handle HTTP GET response ...
* }).catch(function(e) {
* // handle error ...
* });
*
* @method POST
*/
this.POST = function(d) {
// if the passed data is simple dictionary object encode it as POST
// parameters
//
// TODO: think also about changing content type
// "application/x-www-form-urlencoded; charset=UTF-8"
if (d !== null && zebkit.isString(d) === false && d.constructor === Object) {
d = zebkit.URI.toQS(d, false);
}
return this.SEND("POST", this.url, d);
};
/**
* Universal HTTP request method that can be used to generate a HTTP request with
* any HTTP method to the given URL with the given data to be sent asynchronously.
* @param {String} method an HTTP method (GET, POST, DELETE, PUT, etc)
* @param {String} url an URL
* @param {String} [data] a data to be sent to the given URL
* @return {zebkit.DoIt} an object to handle result
* @method SEND
*/
this.SEND = function(method, url, data) {
var req = zebkit.environment.getHttpRequest();
req.open(method, url, true);
for (var k in this.header) {
req.setRequestHeader(k, this.header[k]);
}
return new zebkit.DoIt(function() {
var jn = this.join(),
$this = this;
req.onreadystatechange = function() {
if (req.readyState === 4) {
// evaluate http response
if (req.status >= 400 || req.status < 100) {
var e = new Error("HTTP error '" + req.statusText + "', code = " + req.status + " '" + url + "'");
e.status = req.status;
e.statusText = req.statusText;
e.readyState = req.readyState;
$this.error(e);
} else {
jn(req);
}
}
};
try {
req.send(arguments.length > 2 ? data : null);
} catch(e) {
this.error(e);
}
});
};
}
]);
/**
* Shortcut method to perform HTTP GET requests.
zebkit.io.GET("http://test.com").then(function(request) {
// handle result ...
}).catch(function(e) {
// handle error ...
});
var res = zebkit.io.GET("http://test.com", {
param1 : "var1",
param1 : "var2",
param1 : "var3"
}).then(function(req) {
// handle result ...
});
* @param {String|Object} url an URL
* @param {Object} [parameters] a dictionary of query parameters
* @return {zebkit.DoIt} an object to handle result
* @method GET
* @for zebkit.io
*/
pkg.GET = function(url) {
var http = new pkg.HTTP(url);
return http.GET.apply(http, Array.prototype.slice.call(arguments, 1));
};
/**
* Shortcut method to perform HTTP POST requests.
zebkit.io.POST("http://test.com", null).then(function(request) {
// handle result
...
}).catch(function(e) {
// handle error ...
});
var res = zebkit.io.POST("http://test.com", {
param1 : "var1",
param1 : "var2",
param1 : "var3"
}).then(function(request) {
// handle result
...
});
zebkit.io.POST("http://test.com", "request").then(function(request) {
// handle error
...
});
* @param {String} url an URL
* @param {Object} [data] a data or form data parameters
* @return {zebkit.DoIt} an object to handle result
* @method POST
* @for zebkit.io
*/
pkg.POST = function(url) {
var http = new pkg.HTTP(url);
return http.POST.apply(http, Array.prototype.slice.call(arguments, 1));
};
/**
* A remote service connector class. It is supposed the class has to be extended with
* different protocols like RPC, JSON etc. The typical pattern of connecting to
* a remote service is shown below:
// create service connector that has two methods "a()" and "b(param1)"
var service = new zebkit.io.Service("http://myservice.com", [
"a", "b"
]);
// call the methods of the remote service
service.a();
service.b(10);
* Also the methods of a remote service can be called asynchronously. In this case
* a callback method has to be passed as the last argument of called remote methods:
// create service connector that has two methods "a()" and "b(param1)"
var service = new zebkit.io.Service("http://myservice.com", [
"a", "b"
]);
// call "b" method from the remote service asynchronously
service.b(10, function(res) {
// handle a result of the remote method execution here
...
});
*
* Ideally any specific remote service extension of "zebkit.io.Service"
* class has to implement two methods:
- **encode** to say how the given remote method with passed parameters have
to be transformed into a concrete service side protocol (JSON, XML, etc)
- **decode** to say how the specific service response has to be converted into
JavaScript object
* @class zebkit.io.Service
* @constructor
* @param {String} url an URL of remote service
* @param {Array} methods a list of methods names the remote service provides
*/
pkg.Service = Class([
function(url, methods) {
var $this = this;
/**
* Remote service url
* @attribute url
* @readOnly
* @type {String}
*/
this.url = url;
/**
* Remote service methods names
* @attribute methods
* @readOnly
* @type {Array}
*/
if (Array.isArray(methods) === false) {
methods = [ methods ];
}
for(var i = 0; i < methods.length; i++) {
(function() {
var name = methods[i];
$this[name] = function() {
var args = Array.prototype.slice.call(arguments);
return this.send(url, this.encode(name, args)).then(function(req) {
if (req.status === 200) {
return $this.decode(req.responseText);
} else {
this.error(new Error("Status: " + req.status + ", '" + req.statusText + "'"));
}
});
};
})();
}
},
function $prototype() {
this.contentType = null;
/**
* Send the given data to the given url and return a response. Callback
* function can be passed for asynchronous result handling.
* @protected
* @param {String} url an URL
* @param {String} data a data to be send
* @return {zebkit.DoIt} a result
* @method send
*/
this.send = function(url, data) {
var http = new pkg.HTTP(url);
if (this.contentType !== null) {
http.header['Content-Type'] = this.contentType;
}
return http.POST(data);
};
}
/**
* Transforms the given remote method execution with the specified parameters
* to service specific protocol.
* @param {String} name a remote method name
* @param {Array} args an passed to the remote method arguments
* @return {String} a remote service specific encoded string
* @protected
* @method encode
*/
/**
* Transforms the given remote method response to a JavaScript
* object.
* @param {String} name a remote method name
* @return {Object} a result of the remote method calling as a JavaScript
* object
* @protected
* @method decode
*/
]);
/**
* Build invoke method that calls a service method.
* @param {zebkit.Class} clazz a class
* @param {String} url an URL
* @param {String} a service method name
* @return {Function} a wrapped method to call RPC method with
* @private
* @method invoke
* @static
*/
pkg.Service.invoke = function(clazz, url, method) {
var rpc = new clazz(url, method);
return function() {
return rpc[method].apply(rpc, arguments);
};
};
/**
* The class is implementation of JSON-RPC remote service connector.
// create JSON-RPC connector to a remote service that
// has three remote methods
var service = new zebkit.io.JRPC("json-rpc.com", [
"method1", "method2", "method3"
]);
// synchronously call remote method "method1"
service.method1();
// asynchronously call remote method "method1"
service.method1(function(res) {
...
});
* @class zebkit.io.JRPC
* @constructor
* @param {String} url an URL of remote service
* @param {Array} methods a list of methods names the remote service provides
* @extends zebkit.io.Service
*/
pkg.JRPC = Class(pkg.Service, [
function $prototype() {
this.version = "2.0";
this.contentType = "application/json; charset=ISO-8859-1;";
this.encode = function(name, args) {
return zebkit.environment.stringifyJSON({
jsonrpc : this.version,
method : name,
params : args,
id : pkg.UID() });
};
this.decode = function(r) {
if (r === null || r.length === 0) {
throw new Error("Empty JSON result string");
}
r = zebkit.environment.parseJSON(r);
if (r.error !== undefined) {
throw new Error(r.error.message);
}
if (r.result === undefined || r.id === undefined) {
throw new Error("Wrong JSON response format");
}
return r.result;
};
}
]);
/**
* Shortcut to call the specified method of a JSON-RPC service.
* @param {String} url an URL
* @param {String} method a method name
* @for zebkit.io.JRPC
* @static
* @method invoke
*/
pkg.JRPC.invoke = function(url, method) {
return pkg.Service.invoke(pkg.JRPC, url, method);
};
pkg.Base64 = function(s) {
if (arguments.length > 0) {
this.encoded = pkg.b64encode(s);
}
};
pkg.Base64.prototype.toString = function() { return this.encoded; };
pkg.Base64.prototype.decode = function() { return pkg.b64decode(this.encoded); };
/**
* The class is implementation of XML-RPC remote service connector.
// create XML-RPC connector to a remote service that
// has three remote methods
var service = new zebkit.io.XRPC("xmlrpc.com", [
"method1", "method2", "method3"
]);
// synchronously call remote method "method1"
service.method1();
// asynchronously call remote method "method1"
service.method1(function(res) {
...
});
* @class zebkit.io.XRPC
* @constructor
* @extends zebkit.io.Service
* @param {String} url an URL of remote service
* @param {Array} methods a list of methods names the remote service provides
*/
pkg.XRPC = Class(pkg.Service, [
function $prototype() {
this.contentType = "text/xml";
this.encode = function(name, args) {
var p = ["<?xml version=\"1.0\"?>\n<methodCall><methodName>", name, "</methodName><params>"];
for(var i=0; i < args.length;i++) {
p.push("<param>");
this.encodeValue(args[i], p);
p.push("</param>");
}
p.push("</params></methodCall>");
return p.join('');
};
this.encodeValue = function(v, p) {
if (v === null) {
throw new Error("Null is not allowed");
}
if (zebkit.isString(v)) {
v = v.replace("<", "<");
v = v.replace("&", "&");
p.push("<string>", v, "</string>");
} else {
if (zebkit.isNumber(v)) {
if (Math.round(v) === v) {
p.push("<i4>", v.toString(), "</i4>");
} else {
p.push("<double>", v.toString(), "</double>");
}
} else {
if (zebkit.isBoolean(v)) {
p.push("<boolean>", v?"1":"0", "</boolean>");
} else {
if (v instanceof Date) {
p.push("<dateTime.iso8601>", pkg.dateToISO8601(v), "</dateTime.iso8601>");
} else {
if (Array.isArray(v)) {
p.push("<array><data>");
for(var i=0;i<v.length;i++) {
p.push("<value>");
this.encodeValue(v[i], p);
p.push("</value>");
}
p.push("</data></array>");
} else {
if (v instanceof pkg.Base64) {
p.push("<base64>", v.toString(), "</base64>");
} else {
p.push("<struct>");
for (var k in v) {
if (v.hasOwnProperty(k)) {
p.push("<member><name>", k, "</name><value>");
this.encodeValue(v[k], p);
p.push("</value></member>");
}
}
p.push("</struct>");
}
}
}
}
}
}
};
this.decodeValue = function (node) {
var tag = node.tagName.toLowerCase(), i = 0;
if (tag === "struct") {
var p = {};
for(i = 0; i < node.childNodes.length; i++) {
var member = node.childNodes[i], // <member>
key = member.childNodes[0].childNodes[0].nodeValue.trim(); // <name>/text()
p[key] = this.decodeValue(member.childNodes[1].childNodes[0]); // <value>/<xxx>
}
return p;
}
if (tag === "array") {
var a = [];
node = node.childNodes[0]; // <data>
for(i = 0; i < node.childNodes.length; i++) {
a[i] = this.decodeValue(node.childNodes[i].childNodes[0]); // <value>
}
return a;
}
var v = node.childNodes[0].nodeValue.trim();
switch (tag) {
case "datetime.iso8601": return pkg.ISO8601toDate(v);
case "boolean": return v === "1";
case "int":
case "i4": return parseInt(v, 10);
case "double": return Number(v);
case "base64":
var b64 = new pkg.Base64();
b64.encoded = v;
return b64;
case "string": return v;
}
throw new Error("Unknown tag " + tag);
};
this.decode = function(r) {
var p = zebkit.environment.parseXML(r),
c = p.getElementsByTagName("fault");
if (c.length > 0) {
var err = this.decodeValue(c[0].getElementsByTagName("struct")[0]);
throw new Error(err.faultString);
}
c = p.getElementsByTagName("methodResponse")[0];
c = c.childNodes[0].childNodes[0]; // <params>/<param>
if (c.tagName.toLowerCase() === "param") {
return this.decodeValue(c.childNodes[0].childNodes[0]); // <value>/<xxx>
}
throw new Error("Incorrect XML-RPC response");
};
}
]);
/**
* Shortcut to call the specified method of a XML-RPC service.
* @param {String} url an URL
* @param {String} method a method name
* @for zebkit.io.XRPC
* @method invoke
* @static
*/
pkg.XRPC.invoke = function(url, method) {
return pkg.Service.invoke(pkg.XRPC, url, method);
};
});
zebkit.package("layout", function(pkg, Class) {
/**
* Layout package provides number of classes, interfaces, methods and variables that allows
* developers easily implement rules based layouting of hierarchy of rectangular elements.
* The package has no relation to any concrete UI, but it can be applied to a required UI
* framework very easily. In general layout manager requires an UI component to provide:
* - **setLocation(x,y)** method
* - **setSize(w,h)** method
* - **setBounds()** method
* - **getPreferredSize(x,y)** method
* - **getTop(), getBottom(), getRight(), getLeft()** methods
* - **constraints** read only property
* - **width, height, x, y** read only metrics properties
* - **kids** read only property that keep all children components
*
* @access package
* @class zebkit.layout
*/
/**
* Find a direct children element for the given children component
* and the specified parent component
* @param {zebkit.layout.Layoutable} parent a parent component
* @param {zebkit.layout.Layoutable} child a children component
* @return {zebkit.layout.Layoutable} a direct children component
* @method getDirectChild
* @for zebkit.layout
*/
pkg.getDirectChild = function(parent, child) {
for(; child !== null && child.parent !== parent; child = child.parent) {}
return child;
};
/**
* Layout manager interface is simple interface that all layout managers have to
* implement. One method has to calculate preferred size of the given component and
* another one method has to perform layouting of children components of the given
* target component.
* @class zebkit.layout.Layout
* @interface zebkit.layout.Layout
*/
/**
* Calculate preferred size of the given component
* @param {zebkit.layout.Layoutable} t a target layoutable component
* @method calcPreferredSize
*/
/**
* Layout children components of the specified layoutable target component
* @param {zebkit.layout.Layoutable} t a target layoutable component
* @method doLayout
*/
pkg.Layout = new zebkit.Interface([
"abstract",
function doLayout(target) {},
function calcPreferredSize(target) {}
]);
/**
* Find a direct component located at the given location of the specified parent component
* and the specified parent component
* @param {Integer} x a x coordinate relatively to the parent component
* @param {Integer} y a y coordinate relatively to the parent component
* @param {zebkit.layout.Layoutable} parent a parent component
* @return {zebkit.layout.Layoutable} an index of direct children component
* or -1 if no a children component can be found
* @method getDirectAt
* @for zebkit.layout
*/
pkg.getDirectAt = function(x, y, p){
for(var i = 0;i < p.kids.length; i++){
var c = p.kids[i];
if (c.isVisible === true && c.x <= x && c.y <= y && c.x + c.width > x && c.y + c.height > y) {
return i;
}
}
return -1;
};
/**
* Get a top (the highest in component hierarchy) parent component
* of the given component
* @param {zebkit.layout.Layoutable} c a component
* @return {zebkit.layout.Layoutable} a top parent component
* @method getTopParent
* @for zebkit.layout
*/
pkg.getTopParent = function(c){
for(; c !== null && c.parent !== null; c = c.parent) {}
return c;
};
/**
* Translate the given relative location into the parent relative location.
* @param {Integer} [x] a x coordinate relatively to the given component
* @param {Integer} [y] a y coordinate relatively to the given component
* @param {zebkit.layout.Layoutable} c a component
* @param {zebkit.layout.Layoutable} [p] a parent component
* @return {Object} a relative to the given parent UI component location:
*
* { x:{Integer}, y:{Integer} }
*
* @method toParentOrigin
* @for zebkit.layout
*/
pkg.toParentOrigin = function(x,y,c,p){
if (arguments.length === 1) {
c = x;
x = y = 0;
p = null;
} else if (arguments.length < 4) {
p = null;
}
while (c !== null && c !== p) {
x += c.x;
y += c.y;
c = c.parent;
}
if (c === null) {
//throw new Error("Invalid params");
}
return { x:x, y:y };
};
/**
* Convert the given component location into relative
* location of the specified children component successor.
* @param {Integer} x a x coordinate relatively to the given
* component
* @param {Integer} y a y coordinate relatively to the given
* component
* @param {zebkit.layout.Layoutable} p a component
* @param {zebkit.layout.Layoutable} c a children successor component
* @return {Object} a relative location
*
* { x:{Integer}, y:{Integer} }
*
* @method toChildOrigin
* @for zebkit.layout
*/
pkg.toChildOrigin = function(x, y, p, c){
while(c !== p){
x -= c.x;
y -= c.y;
c = c.parent;
}
return { x:x, y:y };
};
/**
* Calculate maximal preferred width and height of
* children component of the given target component.
* @param {zebkit.layout.Layoutable} target a target component
* @return {Object} a maximal preferred width and height
*
* { width:{Integer}, height:{Integer} }
*
* @method getMaxPreferredSize
* @for zebkit.layout
*/
pkg.getMaxPreferredSize = function(target) {
var maxWidth = 0,
maxHeight = 0;
for(var i = 0;i < target.kids.length; i++) {
var l = target.kids[i];
if (l.isVisible === true){
var ps = l.getPreferredSize();
if (ps.width > maxWidth) {
maxWidth = ps.width;
}
if (ps.height > maxHeight) {
maxHeight = ps.height;
}
}
}
return { width: maxWidth, height: maxHeight };
};
pkg.$align = function(a, cellSize, compSize) {
if (a === "left" || a === "top" || a === "stretch") {
return 0;
} else if (a === "right" || a === "bottom") {
return cellSize - compSize;
} else if (a === "center") {
return Math.floor((cellSize - compSize) / 2);
} else {
zebkit.dumpError("Invalid alignment '" + a + "'");
return 0;
}
};
/**
* Test if the given parent component is ancestor of the specified component.
* @param {zebkit.layout.Layoutable} p a parent component
* @param {zebkit.layout.Layoutable} c a component
* @return {Boolean} true if the given parent is ancestor of the specified component
* @for zebkit.layout
* @method isAncestorOf
*/
pkg.isAncestorOf = function(p, c){
for(; c !== null && c !== p; c = c.parent) {}
return c !== null;
};
/**
* Layoutable class defines rectangular component that has elementary metrical properties like width,
* height and location and can be a participant of layout management process. Layoutable component is
* container that can contains other layoutable component as its children. The children components are
* ordered by applying a layout manager of its parent component.
* @class zebkit.layout.Layoutable
* @constructor
* @uses zebkit.layout.Layout
* @uses zebkit.EventProducer
* @uses zebkit.PathSearch
*/
pkg.Layoutable = Class(pkg.Layout, zebkit.EventProducer, zebkit.PathSearch, [
function() {
/**
* Reference to children components
* @attribute kids
* @type {Array}
* @default empty array
* @readOnly
*/
this.kids = [];
/**
* Layout manager that is used to order children layoutable components
* @attribute layout
* @default itself
* @readOnly
* @type {zebkit.layout.Layout}
*/
this.layout = this;
},
function $prototype() {
/**
* x coordinate
* @attribute x
* @default 0
* @readOnly
* @type {Integer}
*/
/**
* y coordinate
* @attribute y
* @default 0
* @readOnly
* @type {Integer}
*/
/**
* Width of rectangular area
* @attribute width
* @default 0
* @readOnly
* @type {Integer}
*/
/**
* Height of rectangular area
* @attribute height
* @default 0
* @readOnly
* @type {Integer}
*/
/**
* Indicate a layoutable component visibility
* @attribute isVisible
* @default true
* @readOnly
* @type {Boolean}
*/
/**
* Indicate a layoutable component validity
* @attribute isValid
* @default false
* @readOnly
* @type {Boolean}
*/
/**
* Reference to a parent layoutable component
* @attribute parent
* @default null
* @readOnly
* @type {zebkit.layout.Layoutable}
*/
this.x = this.y = this.height = this.width = this.cachedHeight = 0;
this.psWidth = this.psHeight = this.cachedWidth = -1;
this.isLayoutValid = this.isValid = false;
this.layout = null;
/**
* The component layout constraints. The constraints is specific to
* the parent component layout manager value that customizes the
* children component layouting on the parent component.
* @attribute constraints
* @default null
* @type {Object}
*/
this.constraints = this.parent = null;
this.isVisible = true;
this.$matchPath = function(node, name) {
if (name[0] === '~') {
return node.clazz !== undefined &&
node.clazz !== null &&
zebkit.instanceOf(node, zebkit.Class.forName(name.substring(1)));
} else {
return node.clazz !== undefined &&
node.clazz.$name !== undefined &&
node.clazz.$name === name;
}
};
/**
* Set the given id for the component
* @param {String} id an ID to be set
* @method setId
* @chainable
*/
this.setId = function(id) {
this.id = id;
return this;
};
/**
* Set the component properties. This is wrapper for "properties" method to supply
* properties setter method.
* @param {String} [path] a path to find children components
* @param {Object} props a dictionary of properties to be applied
* @method setProperties
*/
this.setProperties = function() {
this.properties.apply(this, arguments);
return this;
};
/**
* Apply the given set of properties to the given component or a number of children
* its components.
* @example
*
* var c = new zebkit.layout.Layoutable();
* c.properties({
* width: [100, 100],
* location: [10,10],
* layout: new zebkit.layout.BorderLayout()
* })
*
* c.add(new zebkit.layout.Layoutable()).add(zebkit.layout.Layoutable())
* .add(zebkit.layout.Layoutable());
* c.properties("//*", {
* size: [100, 200]
* });
*
* @param {String} [path] a path to find children components
* @param {Object} props a dictionary of properties to be applied
* @chainable
* @method properties
*/
this.properties = function(path, props) {
if (arguments.length === 1) {
return zebkit.properties(this, path);
}
this.byPath(path, function(kid) {
zebkit.properties(kid, props);
});
return this;
};
/**
* Set the given property to the component or children component
* specified by the given path (optionally).
* @param {String} [path] a path to find children components
* @param {String} name a property name
* @param {object} value a property value
* @chainable
* @method property
*/
this.property = function() {
var p = {};
if (arguments.length > 2) {
p[arguments[1]] = arguments[2];
return this.properties(arguments[0], p);
} else {
p[arguments[0]] = arguments[1];
return this.properties(p);
}
};
/**
* Validate the component metrics. The method is called as a one step of the component validation
* procedure. The method causes "recalc" method execution if the method has been implemented and
* the component is in invalid state. It is supposed the "recalc" method has to be implemented by
* a component as safe place where the component metrics can be calculated. Component metrics is
* individual for the given component properties that has influence to the component preferred
* size value. In many cases the properties calculation has to be minimized what can be done by
* moving the calculation in "recalc" method
* @method validateMetric
* @protected
*/
this.validateMetric = function(){
if (this.isValid === false) {
if (typeof this.recalc === 'function') {
this.recalc();
}
this.isValid = true;
}
};
/**
* By default there is no any implementation of "recalc" method in the layoutable component. In other
* words the method doesn't exist. Developer should implement the method if the need a proper and
* efficient place to calculate component properties that have influence to the component preferred
* size. The "recalc" method is called only when it is really necessary to compute the component metrics.
* @method recalc
* @protected
*/
/**
* Invalidate the component layout. Layout invalidation means the component children components have to
* be placed with the component layout manager. Layout invalidation causes a parent component layout is
* also invalidated.
* @method invalidateLayout
* @protected
*/
this.invalidateLayout = function(){
this.isLayoutValid = false;
if (this.parent !== null) {
this.parent.invalidateLayout();
}
};
/**
* Invalidate component layout and metrics.
* @method invalidate
*/
this.invalidate = function(){
this.isLayoutValid = this.isValid = false;
this.cachedWidth = -1;
if (this.parent !== null) {
this.parent.invalidate();
}
};
/**
* Force validation of the component metrics and layout if it is not valid
* @method validate
*/
this.validate = function() {
if (this.isValid === false) {
this.validateMetric();
}
if (this.width > 0 && this.height > 0 &&
this.isLayoutValid === false &&
this.isVisible === true)
{
this.layout.doLayout(this);
for (var i = 0; i < this.kids.length; i++) {
this.kids[i].validate();
}
this.isLayoutValid = true;
if (this.laidout !== undefined) {
this.laidout();
}
}
};
/**
* The method can be implemented to be informed every time the component has completed to layout
* its children components
* @method laidout
*/
/**
* Get preferred size. The preferred size includes top, left, bottom and right paddings and
* the size the component wants to have
* @method getPreferredSize
* @return {Object} return size object the component wants to
* have as the following structure:
*
* {width:{Integer}, height:{Integer}} object
*
*/
this.getPreferredSize = function(){
this.validateMetric();
if (this.cachedWidth < 0) {
var ps = (this.psWidth < 0 || this.psHeight < 0) ? this.layout.calcPreferredSize(this)
: { width:0, height:0 };
ps.width = this.psWidth >= 0 ? this.psWidth
: ps.width + this.getLeft() + this.getRight();
ps.height = this.psHeight >= 0 ? this.psHeight
: ps.height + this.getTop() + this.getBottom();
this.cachedWidth = ps.width;
this.cachedHeight = ps.height;
return ps;
}
return { width:this.cachedWidth,
height:this.cachedHeight };
};
/**
* Get top padding.
* @method getTop
* @return {Integer} top padding in pixel
*/
this.getTop = function () { return 0; };
/**
* Get left padding.
* @method getLeft
* @return {Integer} left padding in pixel
*/
this.getLeft = function () { return 0; };
/**
* Get bottom padding.
* @method getBottom
* @return {Integer} bottom padding in pixel
*/
this.getBottom = function () { return 0; };
/**
* Get right padding.
* @method getRight
* @return {Integer} right padding in pixel
*/
this.getRight = function () { return 0; };
/**
* Set the parent component.
* @protected
* @param {zebkit.layout.Layoutable} o a parent component
* @method setParent
* @protected
*/
this.setParent = function(o) {
if (o !== this.parent){
this.parent = o;
this.invalidate();
}
};
/**
* Set the given layout manager that is used to place
* children component. Layout manager is simple class
* that defines number of rules concerning the way
* children components have to be ordered on its parent
* surface.
* @method setLayout
* @param {zebkit.ui.Layout} m a layout manager
* @chainable
*/
this.setLayout = function (m){
if (m === null || m === undefined) {
throw new Error("Null layout");
}
if (this.layout !== m){
this.layout = m;
this.invalidate();
}
return this;
};
/**
* Internal implementation of the component preferred size calculation.
* @param {zebkit.layout.Layoutable} target a component for that the metric has to be calculated
* @return {Object} a preferred size. The method always
* returns { width:10, height:10 } as the component preferred
* size
* @private
* @method calcPreferredSize
*/
this.calcPreferredSize = function (target){
return { width:10, height:10 };
};
/**
* By default layoutbable component itself implements layout manager to order its children
* components. This method implementation does nothing, so children component will placed
* according locations and sizes they have set.
* @method doLayout
* @private
*/
this.doLayout = function (target) {};
/**
* Detect index of a children component.
* @param {zebkit.ui.Layoutbale} c a children component
* @method indexOf
* @return {Integer}
*/
this.indexOf = function (c){
return this.kids.indexOf(c);
};
/**
* Insert the new children component at the given index with the specified layout constraints.
* The passed constraints can be set via a layoutable component that is inserted. Just
* set "constraints" property of in inserted component.
* @param {Integer} i an index at that the new children component has to be inserted
* @param {Object} constr layout constraints of the new children component
* @param {zebkit.layout.Layoutbale} d a new children layoutable component to be added
* @return {zebkit.layout.Layoutable} an inserted children layoutable component
* @method insert
*/
this.insert = function(i, constr, d){
if (d.constraints !== null) {
constr = d.constraints;
} else {
d.constraints = constr;
}
if (i === this.kids.length) {
this.kids.push(d);
} else {
this.kids.splice(i, 0, d);
}
d.setParent(this);
if (this.kidAdded !== undefined) {
this.kidAdded(i, constr, d);
}
this.invalidate();
return d;
};
/**
* The method can be implemented to be informed every time a new component
* has been inserted into the component
* @param {Integer} i an index at that the new children component has been inserted
* @param {Object} constr layout constraints of the new children component
* @param {zebkit.layout.Layoutbale} d a new children layoutable component that has
* been added
* @method kidAdded
*/
/**
* Set the layoutable component location. Location is x, y coordinates relatively to
* a parent component
* @param {Integer} xx x coordinate relatively to the layoutable component parent
* @param {Integer} yy y coordinate relatively to the layoutable component parent
* @method setLocation
* @chainable
*/
this.setLocation = function (xx,yy){
if (xx !== this.x || this.y !== yy) {
var px = this.x, py = this.y;
this.x = xx;
this.y = yy;
if (this.relocated !== undefined) {
this.relocated(px, py);
}
}
return this;
};
/**
* The method can be implemented to be informed every time the component
* has been moved
* @param {Integer} px x previous coordinate of moved children component
* @param {Integer} py y previous coordinate of moved children component
* @method relocated
*/
/**
* Set the layoutable component bounds. Bounds defines the component location and size.
* @param {Integer} x x coordinate relatively to the layoutable component parent
* @param {Integer} y y coordinate relatively to the layoutable component parent
* @param {Integer} w a width of the component
* @param {Integer} h a height of the component
* @method setBounds
* @chainable
*/
this.setBounds = function(x, y, w, h) {
this.setLocation(x, y);
this.setSize(w, h);
return this;
};
/**
* Set the layoutable component size.
* @param {Integer} w a width of the component
* @param {Integer} h a height of the component
* @method setSize
* @chainable
*/
this.setSize = function(w,h) {
if (w !== this.width || h !== this.height) {
var pw = this.width,
ph = this.height;
this.width = w;
this.height = h;
this.isLayoutValid = false;
if (this.resized !== undefined) {
this.resized(pw, ph);
}
}
return this;
};
/**
* The method can be implemented to be informed every time the component
* has been resized
* @param {Integer} w a previous width of the component
* @param {Integer} h a previous height of the component
* @method resized
*/
/**
* Get a children layoutable component by the given path (optionally)
* and the specified constraints.
* @param {String} [p] a path.
* @param {zebkit.layout.Layoutable} c a constraints
* @return {zebkit.layout.Layoutable} a children component
* @method byConstraints
*/
this.byConstraints = function(constr) {
if (arguments.length === 2) {
var res = null;
constr = arguments[1];
this.byPath(arguments[0], function(kid) {
if (kid.constraints === constr) {
res = kid;
return true;
} else {
return false;
}
});
return res;
} else {
if (this.kids.length > 0){
for(var i = 0; i < this.kids.length; i++ ){
var l = this.kids[i];
if (constr === l.constraints) {
return l;
}
}
}
return null;
}
};
/**
* Set the component constraints without invalidating the component and its parents components
* layouts and metrics. It is supposed to be used for internal use
* @protected
* @param {Object} c a constraints
* @chainable
* @method $setConstraints
*/
this.$setConstraints = function(c) {
this.constraints = c;
return this;
};
/**
* Remove the given children component.
* @param {zebkit.layout.Layoutable} c a children component to be removed
* @method remove
* @return {zebkit.layout.Layoutable} a removed children component
*/
this.remove = function(c) {
return this.removeAt(this.kids.indexOf(c));
};
/**
* Remove a children component at the specified position.
* @param {Integer} i a children component index at which it has to be removed
* @method removeAt
* @return {zebkit.layout.Layoutable} a removed children component
*/
this.removeAt = function (i){
var obj = this.kids[i],
ctr = obj.constraints;
obj.setParent(null);
if (obj.constraints !== null) {
obj.constraints = null;
}
this.kids.splice(i, 1);
if (this.kidRemoved !== undefined) {
this.kidRemoved(i, obj, ctr);
}
this.invalidate();
return obj;
};
/**
* Remove the component from its parent if it has a parent
* @param {Integer} [after] timeout in milliseconds the component has
* to be removed
* @method removeMe
*/
this.removeMe = function(after) {
var i = -1;
if (this.parent !== null && (i = this.parent.indexOf(this)) >= 0) {
if (arguments.length > 0 && after > 0) {
var $this = this;
zebkit.util.tasksSet.runOnce(function() {
$this.removeMe();
}, after);
} else {
this.parent.removeAt(i);
}
}
};
/**
* Remove a component by the given constraints.
* @param {Object} ctr a constraints
* @return {zebkit.layout.Layoutable} a removed component
* @method removeByConstraints
*/
this.removeByConstraints = function(ctr) {
var c = this.byConstraints(ctr);
if (c !== null) {
return this.remove(c);
} else {
return null;
}
};
/**
* Replace the component with the new one. The replacement keeps
* layout constraints of the replaced component if other constraints
* value is not passed to the method.
* @param {String} [ctr] a new constraints
* @param {zebkit.layout.Layoutable} c a replacement component
* @chainable
* @method replaceMe
*/
this.replaceMe = function(ctr, c) {
if (this.parent !== null) {
if (arguments.length === 1) {
c = ctr;
c.constraints = this.constraints;
} else {
c.constraints = ctr;
}
this.parent.setAt(this.parent.kids.indexOf(this), c);
}
return this;
};
/**
* The method can be implemented to be informed every time a children component
* has been removed
* @param {Integer} i a children component index at which it has been removed
* @param {zebkit.layout.Layoutable} c a children component that has been removed
* @method kidRemoved
*/
/**
* Set the specified preferred size the component has to have. Component preferred size is
* important thing that is widely used to layout the component. Usually the preferred
* size is calculated by a concrete component basing on its metrics. For instance, label
* component calculates its preferred size basing on text size. But if it is required
* the component preferred size can be fixed with the desired value.
* @param {Integer} w a preferred width. Pass "-1" as the
* argument value to not set preferred width
* @param {Integer} h a preferred height. Pass "-1" as the
* argument value to not set preferred height
* @chainable
* @method setPreferredSize
*/
this.setPreferredSize = function(w, h) {
if (arguments.length === 1) {
h = w;
}
if (w !== this.psWidth || h !== this.psHeight){
this.psWidth = w;
this.psHeight = h;
this.invalidate();
}
return this;
};
/**
* Set preferred width.
* @param {Integer} w a preferred width
* @chainable
* @method setPreferredWidth
*/
this.setPreferredWidth = function(w) {
if (w !== this.psWidth){
this.psWidth = w;
this.invalidate();
}
return this;
};
/**
* Set preferred height.
* @param {Integer} h a preferred height
* @chainable
* @method setPreferredHeigh
*/
this.setPreferredHeight = function(h) {
if (h !== this.psHeight){
this.psHeight = h;
this.invalidate();
}
return this;
};
/**
* Get accumulated vertical (top and bottom) padding.
* @return {Integer} a vertical padding
* @method getVerPadding
*/
this.getVerPadding = function() {
return this.getTop() + this.getBottom();
};
/**
* Get accumulated horizontal (top and bottom) padding.
* @return {Integer} a horizontal padding
* @method getHorPadding
*/
this.getHorPadding = function() {
return this.getLeft() + this.getRight();
};
/**
* Replace a children component at the specified index
* with the given new children component
* @param {Integer} i an index of a children component to be replaced
* @param {zebkit.layout.Layoutable} d a new children
* @return {zebkit.layout.Layoutable} a previous component that has
* been re-set with the new one
* @method setAt
*/
this.setAt = function(i, d) {
var constr = this.kids[i].constraints,
pd = this.removeAt(i);
if (d !== null) {
this.insert(i, constr, d);
}
return pd;
};
/**
* Set the component by the given constraints or add new one with the given constraints
* @param {Object} constr a layout constraints
* @param {zebkit.layout.Layoutable} c a component to be added
* @return {zebkit.layout.Layoutable} a previous component that has
* been re-set with the new one
* @method setByConstraints
*/
this.setByConstraints = function(constr, c) {
var prev = this.byConstraints(constr);
if (prev === null) {
return this.add(constr, c);
} else {
return this.setAt(this.indexOf(prev), c);
}
};
/**
* Add the new children component with the given constraints
* @param {Object} constr a constraints of a new children component
* @param {zebkit.layout.Layoutable} d a new children component to
* be added
* @method add
* @return {zebkit.layout.Layoutable} added layoutable component
*/
this.add = function(constr,d) {
return (arguments.length === 1) ? this.insert(this.kids.length, null, constr)
: this.insert(this.kids.length, constr, d);
};
}
]);
/**
* Layout manager implementation that places layoutbale components on top of
* each other stretching its to fill all available parent component space.
* Components that want to have be sized according to its preferred sizes
* have to have its constraints set to "usePsSize".
* @example
*
* var pan = new zebkit.ui.Panel();
* pan.setStackLayout();
*
* // label component will be stretched over all available pan area
* pan.add(new zebkit.ui.Label("A"));
*
* // button component will be sized according to its preferred size
* // and aligned to have centered vertical and horizontal alignments
* pan.add("usePsSize", new zebkit.ui.Button("Ok"));
*
*
* @class zebkit.layout.StackLayout
* @uses zebkit.layout.Layout
* @constructor
*/
pkg.StackLayout = Class(pkg.Layout, [
function $prototype() {
this.calcPreferredSize = function (target){
return pkg.getMaxPreferredSize(target);
};
this.doLayout = function(t){
var top = t.getTop(),
hh = t.height - t.getBottom() - top,
left = t.getLeft(),
ww = t.width - t.getRight() - left;
for(var i = 0;i < t.kids.length; i++){
var l = t.kids[i];
if (l.isVisible === true) {
var ctr = l.constraints === null ? null : l.constraints;
if (ctr === "usePsSize") {
var ps = l.getPreferredSize();
l.setBounds(left + Math.floor((ww - ps.width )/2),
top + Math.floor((hh - ps.height)/2),
ps.width, ps.height);
} else {
l.setBounds(left, top, ww, hh);
}
}
}
};
}
]);
/**
* Layout manager implementation that logically splits component area into five areas: top, bottom,
* left, right and center. Top and bottom components are stretched to fill all available space
* horizontally and are sized to have preferred height horizontally. Left and right components are
* stretched to fill all available space vertically and are sized to have preferred width vertically.
* Center component is stretched to occupy all available space taking in account top, left, right
* and bottom components.
*
* // create panel with border layout
* var p = new zebkit.ui.Panel(new zebkit.layout.BorderLayout());
*
* // add children UI components with top, center and left constraints
* p.add("top", new zebkit.ui.Label("Top"));
* p.add("center", new zebkit.ui.Label("Center"));
* p.add("left", new zebkit.ui.Label("Left"));
*
*
* Construct the layout with the given vertical and horizontal gaps.
* @param {Integer} [hgap] horizontal gap. The gap is a horizontal distance between laid out components
* @param {Integer} [vgap] vertical gap. The gap is a vertical distance between laid out components
* @constructor
* @class zebkit.layout.BorderLayout
* @uses zebkit.layout.Layout
*/
pkg.BorderLayout = Class(pkg.Layout, [
function(hgap,vgap){
if (arguments.length > 0) {
this.hgap = this.vgap = hgap;
if (arguments.length > 1) {
this.vgap = vgap;
}
}
},
function $prototype() {
/**
* Horizontal gap (space between components)
* @attribute hgap
* @default 0
* @readOnly
* @type {Integer}
*/
/**
* Vertical gap (space between components)
* @attribute vgap
* @default 0
* @readOnly
* @type {Integer}
*/
this.hgap = this.vgap = 0;
this.calcPreferredSize = function (target){
var center = null, left = null, right = null, top = null, bottom = null, d = null;
for(var i = 0; i < target.kids.length; i++){
var l = target.kids[i];
if (l.isVisible === true){
switch(l.constraints) {
case null:
case undefined:
case "center" : center = l; break;
case "top" : top = l; break;
case "bottom" : bottom = l; break;
case "left" : left = l; break;
case "right" : right = l; break;
default: throw new Error("Invalid constraints: " + l.constraints);
}
}
}
var dim = { width:0, height:0 };
if (right !== null) {
d = right.getPreferredSize();
dim.width = d.width + this.hgap;
dim.height = (d.height > dim.height ? d.height: dim.height );
}
if (left !== null) {
d = left.getPreferredSize();
dim.width += d.width + this.hgap;
dim.height = d.height > dim.height ? d.height : dim.height;
}
if (center !== null) {
d = center.getPreferredSize();
dim.width += d.width;
dim.height = d.height > dim.height ? d.height : dim.height;
}
if (top !== null) {
d = top.getPreferredSize();
dim.width = d.width > dim.width ? d.width : dim.width;
dim.height += d.height + this.vgap;
}
if (bottom !== null) {
d = bottom.getPreferredSize();
dim.width = d.width > dim.width ? d.width : dim.width;
dim.height += d.height + this.vgap;
}
return dim;
};
this.doLayout = function(target){
var t = target.getTop(),
b = target.height - target.getBottom(),
l = target.getLeft(),
r = target.width - target.getRight(),
center = null,
left = null,
top = null,
bottom = null,
right = null;
for(var i = 0;i < target.kids.length; i++){
var kid = target.kids[i];
if (kid.isVisible === true) {
switch(kid.constraints) {
case null:
case undefined:
case "center":
if (center !== null) {
throw new Error("Component with center constraints is already defined");
}
center = kid;
break;
case "top" :
if (top !== null) {
throw new Error("Component with top constraints is already defined");
}
kid.setBounds(l, t, r - l, kid.getPreferredSize().height);
t += kid.height + this.vgap;
top = kid;
break;
case "bottom":
if (bottom !== null) {
throw new Error("Component with bottom constraints is already defined");
}
var bh = kid.getPreferredSize().height;
kid.setBounds(l, b - bh, r - l, bh);
b -= bh + this.vgap;
bottom = kid;
break;
case "left":
if (left !== null) {
throw new Error("Component with left constraints is already defined");
}
left = kid;
break;
case "right":
if (right !== null) {
throw new Error("Component with right constraints is already defined");
}
right = kid;
break;
default: throw new Error("Invalid constraints: '" + kid.constraints + "'");
}
}
}
if (right !== null) {
var rw = right.getPreferredSize().width;
right.setBounds(r - rw, t, rw, b - t);
r -= rw + this.hgap;
}
if (left !== null) {
left.setBounds(l, t, left.getPreferredSize().width, b - t);
l += left.width + this.hgap;
}
if (center !== null) {
center.setBounds(l, t, r - l, b - t);
}
};
}
]);
/**
* Rester layout manager can be used to use absolute position of layoutable components. That means
* all components will be laid out according coordinates and size they have. Raster layout manager
* provides extra possibilities to control children components placing. It is possible to align
* components by specifying layout constraints, size component to its preferred size and so on.
* Constraints that can be set for components are the following
* - "top"
* - "topRight"
* - "topLeft"
* - "bottom"
* - "bottomLeft"
* - "bottomRight"
* - "right"
* - "center"
* - "left"
* @example
* // instantiate component to be ordered
* var topLeftLab = zebkit.ui.Label("topLeft");
* var leftLab = zebkit.ui.Label("left");
* var centerLab = zebkit.ui.Label("center");
*
* // instantiate a container with raster layoyt manager set
* // the manager is adjusted to size added child component to
* // its preferred sizes
* var container = new zebkit.ui.Panel(new zebkit.layout.RasterLayout(true));
*
* // add child components with appropriate constraints
* container.add("topLeft", topLeftLab);
* container.add("left", leftLab);
* container.add("center", centerLab);
*
* @param {Boolean} [usePsSize] flag to add extra rule to set components size to its preferred
* sizes.
* @class zebkit.layout.RasterLayout
* @constructor
* @uses zebkit.layout.Layout
*/
pkg.RasterLayout = Class(pkg.Layout, [
function(usePsSize) {
if (arguments.length > 0) {
this.usePsSize = usePsSize;
}
},
function $prototype() {
/**
* Define if managed with layout manager components have to be sized according to its
* preferred size
* @attribute usePsSize
* @type {Boolean}
* @default false
*/
this.usePsSize = false;
this.calcPreferredSize = function(c){
var m = { width:0, height:0 };
for(var i = 0;i < c.kids.length; i++ ){
var kid = c.kids[i];
if (kid.isVisible === true) {
var ps = this.usePsSize ? kid.getPreferredSize()
: { width: kid.width, height: kid.height },
px = kid.x + ps.width,
py = kid.y + ps.height;
if (px > m.width) {
m.width = px;
}
if (py > m.height) {
m.height = py;
}
}
}
return m;
};
this.doLayout = function(c) {
var r = c.getRight(),
b = c.getBottom(),
t = c.getTop(),
l = c.getLeft();
for(var i = 0;i < c.kids.length; i++){
var kid = c.kids[i];
if (kid.isVisible === true){
if (this.usePsSize) {
kid.toPreferredSize();
}
var ctr = kid.constraints === null ? null
: kid.constraints;
if (ctr !== null) {
var x = kid.x,
y = kid.y;
if (ctr === "stretch") {
kid.setBounds(l, t, c.width - l - r, c.height - t - b);
} else {
if (ctr === "top" || ctr === "topRight" || ctr === "topLeft") {
y = t;
} else if (ctr === "bottom" || ctr === "bottomLeft" || ctr === "bottomRight") {
y = c.height - kid.height - b;
} else if (ctr === "center" || ctr === "left" || ctr === "right") {
y = Math.floor((c.height - kid.height) / 2);
}
if (ctr === "left" || ctr === "topLeft" || ctr === "bottomLeft") {
x = l;
} else if (ctr === "right" || ctr === "topRight" || ctr === "bottomRight") {
x = c.width - kid.width - r;
} else if (ctr === "center" || ctr === "top" || ctr === "bottom") {
x = Math.floor((c.width - kid.width) / 2);
}
}
kid.setLocation(x, y);
}
}
}
};
}
]);
/**
* Flow layout manager group and places components ordered with different vertical and horizontal
* alignments
*
* // create panel and set flow layout for it
* // components added to the panel will be placed
* // horizontally aligned at the center of the panel
* var p = new zebkit.ui.Panel();
* p.setFlowLayout("center", "center");
*
* // add three buttons into the panel with flow layout
* p.add(new zebkit.ui.Button("Button 1"));
* p.add(new zebkit.ui.Button("Button 2"));
* p.add(new zebkit.ui.Button("Button 3"));
*
* @param {String} [ax] ("left" by default) horizontal alignment:
*
* "left"
* "center"
* "right"
*
* @param {String} [ay] ("top" by default) vertical alignment:
*
* "top"
* "center"
* "bottom"
*
* @param {String} [dir] ("horizontal" by default) a direction the component has to be placed
* in the layout
*
* "vertical"
* "horizontal"
*
* @param {Integer} [gap] a space in pixels between laid out components
* @class zebkit.layout.FlowLayout
* @constructor
* @uses zebkit.layout.Layout
*/
pkg.FlowLayout = Class(pkg.Layout, [
function (ax, ay, dir, g){
if (arguments.length === 1) {
this.gap = ax;
} else {
if (arguments.length > 1) {
this.ax = ax;
this.ay = ay;
}
if (arguments.length > 2) {
this.direction = zebkit.util.validateValue(dir, "horizontal", "vertical");
}
if (arguments.length > 3) {
this.gap = g;
}
}
},
function $prototype() {
/**
* Gap between laid out components
* @attribute gap
* @readOnly
* @type {Integer}
* @default 0
*/
this.gap = 0;
/**
* Horizontal laid out components alignment
* @attribute ax
* @readOnly
* @type {String}
* @default "left"
*/
this.ax = "left";
/**
* Vertical laid out components alignment
* @attribute ay
* @readOnly
* @type {String}
* @default "center"
*/
this.ay = "center";
/**
* Laid out components direction
* @attribute direction
* @readOnly
* @type {String}
* @default "horizontal"
*/
this.direction = "horizontal";
/**
* Define if the last added component has to be stretched to occupy
* the rest of horizontal or vertical space of a parent component.
* @attribute stretchLast
* @type {Boolean}
* @default false
*/
this.stretchLast = false;
this.calcPreferredSize = function (c){
var m = { width:0, height:0 }, cc = 0;
for(var i = 0;i < c.kids.length; i++){
var a = c.kids[i];
if (a.isVisible === true){
var d = a.getPreferredSize();
if (this.direction === "horizontal"){
m.width += d.width;
m.height = d.height > m.height ? d.height : m.height;
}
else {
m.width = d.width > m.width ? d.width : m.width;
m.height += d.height;
}
cc++;
}
}
var add = this.gap * (cc > 0 ? cc - 1 : 0);
if (this.direction === "horizontal") {
m.width += add;
} else {
m.height += add;
}
return m;
};
this.doLayout = function(c){
var psSize = this.calcPreferredSize(c),
t = c.getTop(),
l = c.getLeft(),
lastOne = null,
ew = c.width - l - c.getRight(),
eh = c.height - t - c.getBottom(),
px = ((this.ax === "right") ? ew - psSize.width
: ((this.ax === "center") ? Math.floor((ew - psSize.width) / 2) : 0)) + l,
py = ((this.ay === "bottom") ? eh - psSize.height
: ((this.ay === "center") ? Math.floor((eh - psSize.height) / 2): 0)) + t;
for(var i = 0;i < c.kids.length; i++){
var a = c.kids[i];
if (a.isVisible === true) {
var d = a.getPreferredSize(),
ctr = a.constraints === null ? null : a.constraints;
if (this.direction === "horizontal") {
ctr = ctr || this.ay;
if (ctr === "stretch") {
d.height = c.height - t - c.getBottom();
}
a.setLocation(px, py + pkg.$align(ctr, psSize.height, d.height));
px += (d.width + this.gap);
} else {
ctr = ctr || this.ax;
if (ctr === "stretch") {
d.width = c.width - l - c.getRight();
}
a.setLocation(px + pkg.$align(ctr, psSize.width, d.width), py);
py += d.height + this.gap;
}
a.setSize(d.width, d.height);
lastOne = a;
}
}
if (lastOne !== null && this.stretchLast === true){
if (this.direction === "horizontal") {
lastOne.setSize(c.width - lastOne.x - c.getRight(), lastOne.height);
} else {
lastOne.setSize(lastOne.width, c.height - lastOne.y - c.getBottom());
}
}
};
}
]);
/**
* List layout places components vertically one by one
*
* // create panel and set list layout for it
* var p = new zebkit.ui.Panel();
* p.setListLayout();
*
* // add three buttons into the panel with list layout
* p.add(new zebkit.ui.Button("Item 1"));
* p.add(new zebkit.ui.Button("Item 2"));
* p.add(new zebkit.ui.Button("Item 3"));
*
* @param {String} [ax] horizontal list item alignment:
*
* "left"
* "right"
* "center"
* "stretch"
*
* @param {Integer} [gap] a space in pixels between laid out components
* @class zebkit.layout.ListLayout
* @constructor
* @uses zebkit.layout.Layout
*/
pkg.ListLayout = Class(pkg.Layout,[
function (ax, gap) {
if (arguments.length === 1) {
this.gap = ax;
} else if (arguments.length > 1) {
this.ax = zebkit.util.validateValue(ax, "stretch", "left", "right", "center");
this.gap = gap;
}
},
function $prototype() {
/**
* Horizontal list items alignment
* @attribute ax
* @type {String}
* @readOnly
*/
this.ax = "stretch";
/**
* Pixel gap between list items
* @attribute gap
* @type {Integer}
* @readOnly
*/
this.gap = 0;
this.calcPreferredSize = function (lw){
var w = 0, h = 0, c = 0;
for(var i = 0; i < lw.kids.length; i++){
var kid = lw.kids[i];
if (kid.isVisible === true){
var d = kid.getPreferredSize();
h += (d.height + (c > 0 ? this.gap : 0));
c++;
if (w < d.width) {
w = d.width;
}
}
}
return { width:w, height:h };
};
this.doLayout = function (lw){
var x = lw.getLeft(),
y = lw.getTop(),
psw = lw.width - x - lw.getRight();
for(var i = 0;i < lw.kids.length; i++){
var kid = lw.kids[i];
if (kid.isVisible === true){
var d = kid.getPreferredSize(),
constr = kid.constraints === null ? this.ax
: kid.constraints;
kid.setSize((constr === "stretch") ? psw : d.width, d.height);
kid.setLocation(x + pkg.$align(constr, psw, kid.width), y);
y += (d.height + this.gap);
}
}
};
}
]);
/**
* Percent layout places components vertically or horizontally and sizes its
* according to its percentage constraints.
*
* // create panel and set percent layout for it
* var p = new zebkit.ui.Panel();
* p.setLayout(new zebkit.layout.PercentLayout());
*
* // add three buttons to the panel that are laid out horizontally with
* // percent layout according to its constraints: 20, 30 and 50 percents
* p.add(20, new zebkit.ui.Button("20%"));
* p.add(30, new zebkit.ui.Button("30%"));
* p.add(50, new zebkit.ui.Button("50%"));
*
*
* Percentage constraints can be more complex. It is possible to specify a component
* vertical and horizontal alignments. Pass the following structure to control the
* alignments as the component constraints:
*
* {
* ax: "center | left | right | stretch",
* ay: "center | top | bottom | stretch",
* occupy: <Integer> // -1 means to use preferred size
* }
*
* @param {String} [dir] a direction of placing components. The
* value can be "horizontal" or "vertical"
* @param {Integer} [gap] a space in pixels between laid out components
* @param {String} [ax] default horizontally component alignment. Use
* "center", "left", "right", "stretch" as the parameter value
* @param {String} [ay] default vertical component alignment. Use
* "center", "top", "bottom", "stretch" as the parameter value
* @param {Integer} [occupy] default percentage size of a component. -1 means
* to use preferred size.
* @class zebkit.layout.PercentLayout
* @constructor
* @uses zebkit.layout.Layout
*/
pkg.PercentLayout = Class(pkg.Layout, [
function(dir, gap, ax, ay, occupy) {
if (arguments.length > 0) {
this.direction = zebkit.util.validateValue(dir, "horizontal", "vertical");
if (arguments.length > 1) {
this.gap = gap;
if (arguments.length > 2) {
this.ax = zebkit.util.validateValue(ax, "center", "left", "right", "stretch");
if (arguments.length > 3) {
this.ay = zebkit.util.validateValue(ay, "center", "top", "bottom", "stretch");
if (arguments.length > 4) {
this.occupy = occupy;
}
}
}
}
}
},
function $prototype() {
/**
* Direction the components have to be placed (vertically or horizontally)
* @attribute direction
* @readOnly
* @type {String}
* @default "horizontal"
*/
this.direction = "horizontal";
/**
* Pixel gap between components
* @attribute gap
* @readOnly
* @type {Integer}
* @default 2
*/
this.gap = 2;
/**
* Default horizontal alignment. Use "left", "right", "center" or "stretch" as
* the attribute value
* @attribute ax
* @type {String}
* @default "stretch"
*/
this.ax = "stretch";
/**
* Default vertical alignment. Use "top", "bottom", "center" or "stretch" as
* the attribute value
* @attribute ay
* @type {String}
* @default "center"
*/
this.ay = "center";
/**
* Default percentage size of placed component. -1 means use preferred size
* as the component size.
* @attribute occupy
* @default -1
* @type {Integer}
*/
this.occupy = -1;
this.doLayout = function(target){
var right = target.getRight(),
top = target.getTop(),
bottom = target.getBottom(),
left = target.getLeft(),
size = target.kids.length,
rs = -this.gap * (size === 0 ? 0 : size - 1),
loc = 0,
cellWidth = 0,
cellHeight = 0;
if (this.direction === "horizontal") {
rs += target.width - left - right;
loc = left;
cellHeight = target.height - top - bottom;
} else {
rs += target.height - top - bottom;
loc = top;
cellWidth = target.width - left - right;
}
for (var i = 0; i < size; i++) {
var l = target.kids[i],
ctr = l.constraints,
ps = null,
ax = this.ax,
ay = this.ay,
occupy = this.occupy,
compW = 0,
compH = 0,
xx = 0,
yy = 0;
if (ctr !== null) {
if (ctr.constructor === Object) {
ax = ctr.ax === undefined ? this.ax : ctr.ax;
ay = ctr.ay === undefined ? this.ay : ctr.ay;
occupy = ctr.occupy === undefined ? this.occupy : ctr.occupy;
} else if (ctr.constructor === Number) {
ax = this.ax;
ay = this.ay;
occupy = ctr;
}
}
if (this.direction === "horizontal") {
// cell size
if (i === size - 1) {
cellWidth = target.width - loc - right;
} else if (occupy === -1) {
ps = l.getPreferredSize();
cellWidth = ps.width;
} else {
cellWidth = Math.floor((rs * occupy) / 100);
}
// component size
if (ax === "stretch") {
compW = cellWidth;
xx = loc;
} else {
if (ps === null) {
ps = l.getPreferredSize();
}
compW = ps.width <= cellWidth ? ps.width : cellWidth;
xx = loc + pkg.$align(ax, cellWidth, compW);
}
// component size
if (ay === "stretch") {
compH = cellHeight;
yy = top;
} else {
if (ps === null) {
ps = l.getPreferredSize();
}
compH = ps.height <= cellHeight ? ps.height : cellHeight;
yy = top + pkg.$align(ay, cellHeight, compH);
}
loc += (cellWidth + this.gap);
} else {
// cell size
if (i === size - 1) {
cellHeight = target.height - loc - bottom;
} else if (occupy === -1) {
ps = l.getPreferredSize();
cellHeight = ps.height;
} else {
cellHeight = Math.floor((rs * occupy) / 100);
}
// component size
if (ay === "stretch") {
compH = cellHeight;
yy = loc;
} else {
if (ps === null) {
ps = l.getPreferredSize();
}
compH = ps.height <= cellHeight ? ps.height : cellHeight;
yy = loc + pkg.$align(ay, cellHeight, compH);
}
// component size
if (ax === "stretch") {
compW = cellWidth;
xx = left;
} else {
if (ps === null) {
ps = l.getPreferredSize();
}
compW = ps.width <= cellWidth ? ps.width : cellWidth;
xx = left + pkg.$align(ax, cellWidth, compW);
}
loc += (cellHeight + this.gap);
}
l.setBounds(xx, yy, compW, compH);
}
};
this.calcPreferredSize = function (target){
var max = 0,
size = target.kids.length,
asz = this.gap * (size === 0 ? 0 : size - 1);
for(var i = 0; i < size; i++) {
var d = target.kids[i].getPreferredSize();
if (this.direction === "horizontal") {
if (d.height > max) {
max = d.height;
}
asz += d.width;
} else {
if (d.width > max) {
max = d.width;
}
asz += d.height;
}
}
return (this.direction === "horizontal") ? { width:asz, height:max }
: { width:max, height:asz };
};
}
]);
/**
* Grid layout manager constraints. Constraints says how a component has to be placed in
* grid layout virtual cell. The constraints specifies vertical and horizontal alignments,
* a virtual cell paddings, etc.
* @param {Integer} [ax] a horizontal alignment
* @param {Integer} [ay] a vertical alignment
* @param {Integer} [p] a cell padding
* @constructor
* @class zebkit.layout.Constraints
*/
pkg.Constraints = Class([
function(ax, ay, p) {
if (arguments.length > 0) {
this.ax = ax;
if (arguments.length > 1) {
this.ay = ay;
}
if (arguments.length > 2) {
this.setPadding(p);
}
zebkit.util.validateValue(this.ax, "stretch", "left", "center", "right");
zebkit.util.validateValue(this.ay, "stretch", "top", "center", "bottom");
}
},
function $prototype() {
/**
* Top cell padding
* @attribute top
* @type {Integer}
* @default 0
*/
this.top = 0;
/**
* Left cell padding
* @attribute left
* @type {Integer}
* @default 0
*/
this.left = 0;
/**
* Right cell padding
* @attribute right
* @type {Integer}
* @default 0
*/
this.right = 0;
/**
* Bottom cell padding
* @attribute bottom
* @type {Integer}
* @default 0
*/
this.bottom = 0;
/**
* Horizontal alignment
* @attribute ax
* @type {String}
* @default "stretch"
*/
this.ax = "stretch";
/**
* Vertical alignment
* @attribute ay
* @type {String}
* @default "stretch"
*/
this.ay = "stretch";
this.rowSpan = this.colSpan = 1;
/**
* Set all four paddings (top, left, bottom, right) to the given value
* @param {Integer} p a padding
* @chainable
* @method setPadding
*/
/**
* Set top, left, bottom, right paddings
* @param {Integer} t a top padding
* @param {Integer} l a left padding
* @param {Integer} b a bottom padding
* @param {Integer} r a right padding
* @chainable
* @method setPadding
*/
this.setPadding = function(t,l,b,r) {
if (arguments.length === 1) {
this.top = this.bottom = this.left = this.right = t;
} else {
this.top = t;
this.bottom = b;
this.left = l;
this.right = r;
}
return this;
};
}
]);
/**
* Grid layout manager. can be used to split a component area to number of virtual cells where
* children components can be placed. The way how the children components have to be laid out
* in the cells can be customized by using "zebkit.layout.Constraints" class:
*
* // create constraints
* var ctr = new zebkit.layout.Constraints();
*
* // specify cell top, left, right, bottom paddings
* ctr.setPadding(8);
* // say the component has to be left aligned in a
* // virtual cell of grid layout
* ctr.ax = "left";
*
* // create panel and set grid layout manager with two
* // virtual rows and columns
* var p = new zebkit.ui.Panel();
* p.setLayout(new zebkit.layout.GridLayout(2, 2));
*
* // add children component
* p.add(ctr, new zebkit.ui.Label("Cell 1, 1"));
* p.add(ctr, new zebkit.ui.Label("Cell 1, 2"));
* p.add(ctr, new zebkit.ui.Label("Cell 2, 1"));
* p.add(ctr, new zebkit.ui.Label("Cell 2, 2"));
*
* @param {Integer} rows a number of virtual rows to layout children components
* @param {Integer} cols a number of virtual columns to layout children components
* @param {Boolean} [stretchRows] true if virtual cell height has to be stretched to occupy the
* whole vertical container component space
* @param {Boolean} [stretchCols] true if virtual cell width has to be stretched to occupy the
* whole horizontal container component space
* @constructor
* @class zebkit.layout.GridLayout
* @uses zebkit.layout.Layout
*/
pkg.GridLayout = Class(pkg.Layout, [
function(r, c, stretchRows, stretchCols) {
/**
* Number of virtual rows to place children components
* @attribute rows
* @readOnly
* @type {Integer}
*/
this.rows = r;
/**
* Number of virtual columns to place children components
* @attribute cols
* @readOnly
* @type {Integer}
*/
this.cols = c;
/**
* Computed columns sizes.
* @attribute colSizes
* @type {Array}
* @private
*/
this.colSizes = Array(c + 1);
/**
* Computed rows sizes.
* @attribute rowSizes
* @type {Array}
* @private
*/
this.rowSizes = Array(r + 1);
/**
* Default constraints that is applied for children components
* that doesn't define own constraints
* @type {zebkit.layout.Constraints}
* @attribute constraints
*/
this.constraints = new pkg.Constraints();
if (arguments.length > 2) {
this.stretchRows = (stretchRows === true);
}
if (arguments.length > 3) {
this.stretchCols = (stretchCols === true);
}
},
function $prototype() {
/**
* Attributes that indicates if component has to be stretched
* horizontally to occupy the whole space of a virtual cell.
* @attribute stretchCols
* @readOnly
* @type {Boolean}
* @default false
*/
this.stretchCols = false;
/**
* Attributes that indicates if component has to be stretched
* vertically to occupy the whole space of a virtual cell.
* @attribute stretchRows
* @readOnly
* @type {Boolean}
* @default false
*/
this.stretchRows = false;
/**
* Set default grid layout cell paddings (top, left, bottom, right) to the given value
* @param {Integer} p a padding
* @chainable
* @method setPadding
*/
/**
* Set default grid layout cell paddings: top, left, bottom, right
* @param {Integer} t a top padding
* @param {Integer} l a left padding
* @param {Integer} b a bottom padding
* @param {Integer} r a right padding
* @chainable
* @method setPadding
*/
this.setPadding = function() {
this.constraints.setPadding.apply(this.constraints, arguments);
return this;
};
/**
* Set default constraints.
* @method setDefaultConstraints
* @chainable
* @param {zebkit.layout.Constraints} c a constraints
*/
this.setDefaultConstraints = function(c) {
this.constraints = c;
return this;
};
/**
* Calculate columns metrics
* @param {zebkit.layout.Layoutable} c the target container
* @return {Array} a columns widths
* @method calcCols
* @protected
*/
this.calcCols = function(c){
this.colSizes[this.cols] = 0;
for(var i = 0;i < this.cols; i++) {
this.colSizes[i] = this.calcCol(i, c);
this.colSizes[this.cols] += this.colSizes[i];
}
return this.colSizes;
};
/**
* Calculate rows metrics
* @param {zebkit.layout.Layoutable} c the target container
* @return {Array} a rows heights
* @method calcRows
* @protected
*/
this.calcRows = function(c){
this.rowSizes[this.rows] = 0;
for(var i = 0;i < this.rows; i++) {
this.rowSizes[i] = this.calcRow(i, c);
this.rowSizes[this.rows] += this.rowSizes[i];
}
return this.rowSizes;
};
/**
* Calculate the given row height
* @param {Integer} row a row
* @param {zebkit.layout.Layoutable} c the target container
* @return {Integer} a size of the row
* @method calcRow
* @protected
*/
this.calcRow = function(row, c){
var max = 0, s = row * this.cols;
for (var i = s; i < c.kids.length && i < s + this.cols; i++) {
var a = c.kids[i];
if (a.isVisible === true) {
var arg = a.constraints || this.constraints,
top = arg.top !== undefined ? arg.top : this.constraints.top,
bottom = arg.bottom !== undefined ? arg.bottom : this.constraints.bottom,
d = a.getPreferredSize().height;
d += (top + bottom);
if (d > max) {
max = d;
}
}
}
return max;
};
/**
* Calculate the given column width
* @param {Integer} col a column
* @param {zebkit.layout.Layoutable} c the target container
* @return {Integer} a size of the column
* @method calcCol
* @protected
*/
this.calcCol = function(col, c){
var max = 0;
for(var i = col; i < c.kids.length; i += this.cols) {
var a = c.kids[i];
if (a.isVisible === true) {
var ctr = a.constraints || this.constraints,
left = ctr.left !== undefined ? ctr.left : this.constraints.left,
right = ctr.right !== undefined ? ctr.right: this.constraints.right,
d = a.getPreferredSize().width + left + right;
if (d > max) {
max = d;
}
}
}
return max;
};
this.calcPreferredSize = function(c){
return { width : this.calcCols(c)[this.cols],
height: this.calcRows(c)[this.rows] };
};
this.doLayout = function(c) {
var rows = this.rows,
cols = this.cols,
colSizes = this.calcCols(c),
rowSizes = this.calcRows(c),
top = c.getTop(),
left = c.getLeft(),
cc = 0,
i = 0;
if (this.stretchCols) {
var dw = c.width - left - c.getRight() - colSizes[cols];
for(i = 0; i < cols; i ++ ) {
colSizes[i] = colSizes[i] + (colSizes[i] !== 0 ? Math.floor((dw * colSizes[i]) / colSizes[cols]) : 0);
}
}
if (this.stretchRows) {
var dh = c.height - top - c.getBottom() - rowSizes[rows];
for(i = 0; i < rows; i++) {
rowSizes[i] = rowSizes[i] + (rowSizes[i] !== 0 ? Math.floor((dh * rowSizes[i]) / rowSizes[rows]) : 0);
}
}
for (i = 0; i < rows && cc < c.kids.length; i++) {
var xx = left;
for(var j = 0;j < cols && cc < c.kids.length; j++, cc++) {
var l = c.kids[cc];
if (l.isVisible === true){
var arg = l.constraints || this.constraints,
d = l.getPreferredSize(),
cleft = arg.left !== undefined ? arg.left : this.constraints.left,
cright = arg.right !== undefined ? arg.right : this.constraints.right,
ctop = arg.top !== undefined ? arg.top : this.constraints.top,
cbottom = arg.bottom !== undefined ? arg.bottom : this.constraints.bottom,
cax = arg.ax !== undefined ? arg.ax : this.constraints.ax,
cay = arg.ay !== undefined ? arg.ay : this.constraints.ay,
cellW = colSizes[j],
cellH = rowSizes[i];
cellW -= (cleft + cright);
cellH -= (ctop + cbottom);
if ("stretch" === cax) {
d.width = cellW;
}
if ("stretch" === cay) {
d.height = cellH;
}
l.setSize(d.width, d.height);
l.setLocation(xx + cleft + pkg.$align(cax, cellW, d.width),
top + ctop + pkg.$align(cay, cellH, d.height));
xx += colSizes[j];
}
}
top += rowSizes[i];
}
};
}
]);
});
zebkit.package("draw", function(pkg, Class) {
'use strict';
/**
* View package
*
* @class zebkit.draw
* @access package
*/
/**
* Dictionary of useful methods an HTML Canvas 2D context can be extended. The following methods are
* included:
*
* - **setFont(f)** set font
* - **setColor(c)** set background and foreground colors
* - **drawLine(x1, y1, x2, y2, [w])** draw line of the given width
* - **ovalPath(x,y,w,h)** build oval path
* - **polylinePath(xPoints, yPoints, nPoints)** build path by the given points
* - **drawDottedRect(x,y,w,h)** draw dotted rectangle
* - **drawDashLine(x,y,x2,y2)** draw dashed line
*
* @attribute Context2D
* @type {Object}
* @protected
* @readOnly
*/
pkg.Context2D = {
setFont : function(f) {
f = (f.s !== undefined ? f.s : f.toString());
if (f !== this.font) {
this.font = f;
}
},
setColor : function (c) {
c = (c.s !== undefined ? c.s : c.toString());
if (c !== this.fillStyle) {
this.fillStyle = c;
}
if (c !== this.strokeStyle) {
this.strokeStyle = c;
}
},
drawLine : function (x1, y1, x2, y2, w){
if (arguments.length < 5) {
w = 1;
}
var pw = this.lineWidth;
this.beginPath();
if (this.lineWidth !== w) {
this.lineWidth = w;
}
if (x1 === x2) {
x1 += w / 2;
x2 = x1;
} else if (y1 === y2) {
y1 += w / 2;
y2 = y1;
}
this.moveTo(x1, y1);
this.lineTo(x2, y2);
this.stroke();
if (pw !== this.lineWidth) {
this.lineWidth = pw;
}
},
ovalPath: function (x,y,w,h){
this.beginPath();
x += this.lineWidth;
y += this.lineWidth;
w -= 2 * this.lineWidth;
h -= 2 * this.lineWidth;
var kappa = 0.5522848,
ox = Math.floor((w / 2) * kappa),
oy = Math.floor((h / 2) * kappa),
xe = x + w,
ye = y + h,
xm = x + w / 2,
ym = y + h / 2;
this.moveTo(x, ym);
this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
this.closePath();
},
polylinePath : function(xPoints, yPoints, nPoints){
this.beginPath();
this.moveTo(xPoints[0], yPoints[0]);
for(var i = 1; i < nPoints; i++) {
this.lineTo(xPoints[i], yPoints[i]);
}
},
drawRect : function(x,y,w,h) {
this.beginPath();
this.rect(x,y,w,h);
this.stroke();
},
drawDottedRect : function(x,y,w,h) {
var ctx = this, m = ["moveTo", "lineTo", "moveTo"];
function dv(x, y, s) { for(var i=0; i < s; i++) { ctx[m[i%3]](x + 0.5, y + i); } }
function dh(x, y, s) { for(var i=0; i < s; i++) { ctx[m[i%3]](x + i, y + 0.5); } }
ctx.beginPath();
dh(x, y, w);
dh(x, y + h - 1, w);
ctx.stroke();
ctx.beginPath();
dv(x, y, h);
dv(w + x - 1, y, h);
ctx.stroke();
},
drawDashLine : function(x,y,x2,y2) {
var pattern = [1,2],
compute = null,
dx = (x2 - x), dy = (y2 - y),
b = (Math.abs(dx) > Math.abs(dy)),
slope = b ? dy / dx : dx / dy,
sign = b ? (dx < 0 ?-1:1) : (dy < 0?-1:1),
dist = Math.sqrt(dx * dx + dy * dy);
if (b) {
compute = function(step) {
x += step;
y += slope * step;
};
} else {
compute = function(step) {
x += slope * step;
y += step;
};
}
this.beginPath();
this.moveTo(x, y);
for (var i = 0; dist >= 0.1; i++) {
var idx = i % pattern.length,
dl = dist < pattern[idx] ? dist : pattern[idx],
step = Math.sqrt(dl * dl / (1 + slope * slope)) * sign;
compute(step);
this[(i % 2 === 0) ? 'lineTo' : 'moveTo'](x + 0.5, y + 0.5);
dist -= dl;
}
this.stroke();
}
};
/**
* Dictionary of predefined views. Every view is accessible by an id associated
* with the view.
* @attribute $views
* @type {Object}
* @protected
* @for zebkit.draw
*/
pkg.$views = {};
/**
* Build a view instance by the given object.
* @param {Object} v an object that can be used to build a view. The following variants
* of object types are possible
*
* - **null** null is returned
* - **String** if the string is color or border view id than "zebkit.draw.rgb" or border view
* is returned. Otherwise an instance of zebkit.draw.StringRender is returned.
* - **String** if the string starts from "#" or "rgb" it is considered as encoded color. "zebkit.draw.rgb"
* instance will be returned as the view
* - **Array** an instance of "zebkit.draw.CompositeView" is returned
* - **Function** in this case the passed method is considered as ans implementation of "paint(g, x, y, w, h, d)"
* method of "zebkit.draw.View" class. Ans instance of "zebkit.draw.View" with the method implemented is returned.
* - **Object** an instance of "zebkit.draw.ViewSet" is returned
*
* @return zebkit.draw.View a view
* @method $view
* @example
*
* // string render
* var view = zebkit.draw.$view("String render");
*
* // color render
* var view = zebkit.draw.$view("red");
*
* // composite view
* var view = zebkit.draw.$view([
* zebkit.draw.rgb.yellow,
* "String Render"
* ]);
*
* // custom view
* var view = zebkit.draw.$view(function(g,x,y,w,h,d) {
* g.drawLine(x, y, x + w, y + w);
* ...
* });
*
* @protected
* @for zebkit.draw
*/
pkg.$view = function(v) {
if (v === null || v.paint !== undefined) {
return v;
} else if (typeof v === "string" || v.constructor === String) {
if (pkg.rgb[v] !== undefined) { // detect color
return pkg.rgb[v];
} else if (pkg.$views[v] !== undefined) { // detect predefined view
return pkg.$views[v];
} else {
if (v.length > 0 &&
(v[0] === '#' ||
( v.length > 2 &&
v[0] === 'r' &&
v[1] === 'g' &&
v[2] === 'b' ) ))
{
return new pkg.rgb(v);
} else {
return new pkg.StringRender(v);
}
}
} else if (Array.isArray(v)) {
return new pkg.CompositeView(v);
} else if (typeof v !== 'function') {
return new pkg.ViewSet(v);
} else {
var vv = new pkg.View();
vv.paint = v;
return vv;
}
};
/**
* View class that is designed as a basis for various reusable decorative UI elements implementations
* @class zebkit.draw.View
* @constructor
*/
pkg.View = Class([
function $prototype() {
this.gap = 2;
/**
* Get left gap. The method informs UI component that uses the view as
* a border view how much space left side of the border occupies
* @return {Integer} a left gap
* @method getLeft
*/
/**
* Get right gap. The method informs UI component that uses the view as
* a border view how much space right side of the border occupies
* @return {Integer} a right gap
* @method getRight
*/
/**
* Get top gap. The method informs UI component that uses the view as
* a border view how much space top side of the border occupies
* @return {Integer} a top gap
* @method getTop
*/
/**
* Get bottom gap. The method informs UI component that uses the view as
* a border view how much space bottom side of the border occupies
* @return {Integer} a bottom gap
* @method getBottom
*/
this.getRight = this.getLeft = this.getBottom = this.getTop = function() {
return this.gap;
};
/**
* Return preferred size the view desires to have
* @method getPreferredSize
* @return {Object}
*/
this.getPreferredSize = function() {
return { width : 0,
height : 0 };
};
/**
* The method is called to render the decorative element on the given surface of the specified
* UI component
* @param {CanvasRenderingContext2D} g graphical context
* @param {Integer} x x coordinate
* @param {Integer} y y coordinate
* @param {Integer} w required width
* @param {Integer} h required height
* @param {zebkit.layout.Layoutable} c an UI component on which the view
* element has to be drawn
* @method paint
*/
this.paint = function(g,x,y,w,h,c) {};
}
]);
/**
* Render class extends "zebkit.draw.View" class with a notion
* of target object. Render stores reference to a target that
* the render knows how to visualize. Basically Render is an
* object visualizer. For instance, developer can implement
* text, image and so other objects visualizers.
* @param {Object} target a target object to be visualized
* with the render
* @constructor
* @extends zebkit.draw.View
* @class zebkit.draw.Render
*/
pkg.Render = Class(pkg.View, [
function(target) {
if (arguments.length > 0) {
this.setValue(target);
}
},
function $prototype() {
/**
* Target object to be visualized
* @attribute target
* @default null
* @readOnly
* @type {Object}
*/
this.target = null;
/**
* Set the given target object. The method triggers "valueWasChanged(oldTarget, newTarget)"
* execution if the method is declared. Implement the method if you need to track a target
* object updating.
* @method setValue
* @param {Object} o a target object to be visualized
* @chainable
*/
this.setValue = function(o) {
if (this.target !== o) {
var old = this.target;
this.target = o;
if (this.valueWasChanged !== undefined) {
this.valueWasChanged(old, o);
}
}
return this;
};
/**
* Get as rendered object.
* @return {Object} a rendered object
* @method getValue
*/
this.getValue = function() {
return this.target;
};
}
]);
/**
* RGB color class. This class represents a rgb color as JavaScript structure:
*
* // rgb color
* var rgb1 = new zebkit.draw.rgb(100,200,100);
*
* // rgb with transparency
* var rgb2 = new zebkit.draw.rgb(100,200,100, 0.6);
*
* // encoded as a string rgb color
* var rgb3 = new zebkit.draw.rgb("rgb(100,100,200)");
*
* // hex rgb color
* var rgb3 = new zebkit.draw.rgb("#CCDDFF");
*
* @param {Integer|String} r the meaning of the argument depends on number of arguments the
* constructor gets:
*
* - If constructor gets only this argument the argument is considered as encoded rgb color:
* - **String** means its hex encoded ("#CCFFDD") or rgb ("rgb(100,10,122)", "rgba(100,33,33,0.6)") encoded color
* - **Integer** means this is number encoded rgb color
* - Otherwise the argument is an integer value that depicts a red intensity of rgb color
*
* encoded in string rgb color
* @param {Integer} [g] green color intensity
* @param {Integer} [b] blue color intensity
* @param {Float} [a] alpha color intensity
* @constructor
* @class zebkit.draw.rgb
* @extends zebkit.draw.View
*/
pkg.rgb = Class(pkg.View, [
function (r, g, b, a) {
this.isOpaque = true;
if (arguments.length === 1) {
if (zebkit.isString(r)) {
this.s = r = r.trim();
if (r[0] === '#') { // hex color has been detected
if (r.length >= 7) { // long hex color #RRGGBB[AA]
var rr = parseInt(r.substring(1, 7), 16);
this.r = rr >> 16;
this.g = (rr >> 8) & 0xFF;
this.b = (rr & 0xFF);
if (r.length > 7) { // check if alpha is represnted with the color
this.a = parseInt(r.substring(7, r.length), 16);
this.isOpaque = (this.a === 0xFF);
}
} else { // short hex color #RGB[A]
this.r = parseInt(r.substring(1, 2), 16);
this.g = parseInt(r.substring(2, 3), 16);
this.b = parseInt(r.substring(3, 4), 16);
if (r.length > 4) { // check if alpha is represnted with the color
this.a = parseInt(r.substring(4, 5), 16);
this.isOpaque = (this.a === 0xF);
}
}
} else if (r[0] === 'r' && r[1] === 'g' && r[2] === 'b') { // rgb encoded color has been detected
var i = r.indexOf('(', 3),
s = r.substring(i + 1, r.indexOf(')', i + 1)),
p = s.split(",");
this.r = parseInt(p[0].trim(), 10);
this.g = parseInt(p[1].trim(), 10);
this.b = parseInt(p[2].trim(), 10);
if (p.length > 3) {
var aa = p[3].trim();
if (aa[aa.length - 1] === '%') {
this.isOpaque = (aa == "100%");
this.a = parseFloat((parseInt(aa, 10) / 100).toFixed(2));
} else {
this.a = parseFloat(aa, 10);
this.isOpaque = (this.a == 1.0);
}
}
} else if (r.length > 2 && this.clazz[r] !== undefined) {
var col = this.clazz.colors[r];
this.r = col.r;
this.g = col.g;
this.b = col.b;
this.a = col.a;
this.s = col.s;
this.isOpaque = col.isOpaque;
}
} else { // consider an number has been passed
this.r = r >> 16;
this.g = (r >> 8) & 0xFF;
this.b = (r & 0xFF);
}
} else if (arguments.length > 1) {
this.r = r;
this.g = g;
this.b = b;
if (arguments.length > 3) {
this.a = a;
this.isOpaque = (a == 1.0);
}
}
if (this.s === null) {
this.s = (this.isOpaque === false) ? 'rgba(' + this.r + "," + this.g + "," +
this.b + "," + this.a + ")"
: '#' +
((this.r < 16) ? "0" + this.r.toString(16) : this.r.toString(16)) +
((this.g < 16) ? "0" + this.g.toString(16) : this.g.toString(16)) +
((this.b < 16) ? "0" + this.b.toString(16) : this.b.toString(16));
}
},
function $prototype() {
this.s = null;
this.gap = 0;
/**
* Indicates if the color is opaque
* @attribute isOpaque
* @readOnly
* @type {Boolean}
*/
this.isOpaque = true;
/**
* Red color intensity
* @attribute r
* @type {Integer}
* @readOnly
*/
this.r = 0;
/**
* Green color intensity
* @attribute g
* @type {Integer}
* @readOnly
*/
this.g = 0;
/**
* Blue color intensity
* @attribute b
* @type {Integer}
* @readOnly
*/
this.b = 0;
/**
* Alpha
* @attribute a
* @type {Float}
* @readOnly
*/
this.a = 1.0;
this.paint = function(g,x,y,w,h,d) {
if (this.s !== g.fillStyle) {
g.fillStyle = this.s;
}
// fix for IE10/11, calculate intersection of clipped area
// and the area that has to be filled. IE11/10 have a bug
// that triggers filling more space than it is restricted
// with clip
// if (g.$states !== undefined) {
// var t = g.$states[g.$curState],
// rx = x > t.x ? x : t.x,
// rw = Math.min(x + w, t.x + t.width) - rx;
// if (rw > 0) {
// var ry = y > t.y ? y : t.y,
// rh = Math.min(y + h, t.y + t.height) - ry;
// if (rh > 0) {
// g.fillRect(rx, ry, rw, rh);
// }
// }
// } else {
g.fillRect(x, y, w, h);
// }
};
this.toString = function() {
return this.s;
};
},
function $clazz() {
/**
* Black color constant
* @attribute black
* @type {zebkit.draw.rgb}
* @static
*/
// CSS1
this.black = new this(0);
this.silver = new this(0xC0, 0xC0, 0xC0);
this.grey = this.gray = new this(0x80, 0x80, 0x80);
this.white = new this(0xFFFFFF);
this.maroon = new this(0x800000);
this.red = new this(255,0,0);
this.purple = new this(0x800080);
this.fuchsia = new this(0xff00ff);
this.green = new this(0x008000);
this.lime = new this(0x00ff00);
this.olive = new this(0x808000);
this.yellow = new this(255,255,0);
this.navy = new this(0x000080);
this.blue = new this(0,0,255);
this.teal = new this(0x008080);
this.aqua = new this(0x00ffff);
// CSS2
this.orange = new this(255,165,0);
this.aliceblue = new this(0xf0f8ff);
this.antiqueWhite = this.antiquewhite = new this(0xfaebd7);
this.aquamarine = new this(0x7fffd4);
this.azure = new this(0xf0ffff);
this.beige = new this(0xf5f5dc);
this.bisque = new this(0xffe4c4);
this.blanchedalmond = new this(0xffebcd);
this.blueViolet = this.blueviolet = new this(0x8a2be2);
this.brown = new this(0xa52a2a);
this.burlywood = new this(0xdeb887);
this.cadetblue = new this(0x5f9ea0);
this.chartreuse = new this(0x7fff00);
this.chocolate = new this(0xd2691e);
this.coral = new this(0xff7f50);
this.cornflowerblue = new this(0x6495ed);
this.cornsilk = new this(0xfff8dc);
this.crimson = new this(0xdc143c);
this.cyan = new this(0,255,255);
this.darkBlue = this.darkblue = new this(0x00008b);
this.darkCyan = this.darkcyan = new this(0x008b8b);
this.darkGoldenrod = this.darkgoldenrod = new this(0xb8860b);
this.darkGrey = this.darkgrey = this.darkGray = this.darkgray = new this(0xa9a9a9);
this.darkGreen = this.darkgreen = new this(0x006400);
this.darkKhaki = this.darkkhaki = new this(0xbdb76b);
this.darkMagenta = this.darkmagenta = new this(0x8b008b);
this.darkOliveGreen = this.darkolivegreen = new this(0x556b2f);
this.darkOrange = this.darkorange = new this(0xff8c00);
this.darkOrchid = this.darkorchid = new this(0x9932cc);
this.darkRed = this.darkred = new this(0x8b0000);
this.darkSalmon = this.darksalmon = new this(0xe9967a);
this.darkSeaGreen = this.darkseagreen = new this(0x8fbc8f);
this.darkSlateBlue = this.darkslateblue = new this(0x483d8b);
this.darkSlateGrey = this.darkSlateGray = this.darkslategray = this.darkslategrey = new this(0x2f4f4f);
this.darkTurquoise = this.darkturquoise = new this(0x00ced1);
this.darkViolet = this.darkviolet = new this(0x9400d3);
this.deepPink = this.deeppink = new this(0xff1493);
this.dimGrey = this.dimGray = this.dimgray = this.dimgrey = new this(0x696969);
this.dodgerBlue = this.dodgerblue = new this(0x1e90ff);
this.firebrick = new this(0xb22222);
this.floralwhite = new this(0xfffaf0);
this.forestgreen = new this(0x228b22);
this.gainsboro = new this(0xdcdcdc);
this.ghostwhite = new this(0xf8f8ff);
this.gold = new this(0xffd700);
this.goldenrod = new this(0xdaa520);
this.greenyellow = new this(0xadff2f);
this.honeydew = new this(0xf0fff0);
this.hotpink = new this(0xff69b4);
this.indianred = new this(0xcd5c5c);
this.indigo = new this(0x4b0082);
this.ivory = new this(0xfffff0);
this.khaki = new this(0xf0e68c);
this.lavender = new this(0xe6e6fa);
this.lavenderblush = new this(0xfff0f5);
this.lawngreen = new this(0x7cfc00);
this.lemonchiffon = new this(0xfffacd);
this.lightBlue = this.lightblue = new this(0xadd8e6);
this.lightCoral = this.lightcoral = new this(0xf08080);
this.lightCyan = this.lightcyan = new this(0xe0ffff);
this.lightGoldenRodYellow = this.lightgoldenrodyellow = new this(0xfafad2);
// CSS3
this.lightGrey = this.lightGray = this.lightgray = this.lightgrey = new this(0xd3d3d3);
this.lightGreen = this.lightgreen = new this(0x90ee90);
this.lightPink = this.lightpink = new this(0xffb6c1);
this.lightSalmon = this.lightsalmon = new this(0xffa07a);
this.lightSeaGreen = this.lightseagreen = new this(0x20b2aa);
this.lightSkyBlue = this.lightskyblue = new this(0x87cefa);
this.lightSlateGrey = this.lightSlateGray = this.lightslategrey = this.lightslategray = new this(0x778899);
this.lightSteelBlue = this.lightsteelblue = new this(0xb0c4de);
this.lightYellow = this.lightyellow = new this(0xffffe0);
this.linen = new this(0xfaf0e6);
this.magenta = new this(0xff00ff);
this.pink = new this(0xffc0cb);
this.transparent = new this(0, 0, 0, 0.0);
this.mergeable = false;
}
]);
/**
* Composite view. The view allows developers to combine number of
* views and renders its together.
* @class zebkit.draw.CompositeView
* @param {Object} ...views number of views to be composed.
* @constructor
* @extends zebkit.draw.View
*/
pkg.CompositeView = Class(pkg.View, [
function() {
/**
* Composed views array.
* @attribute views
* @type {Array}
* @protected
* @readOnly
*/
this.views = [];
var args = arguments.length === 1 ? arguments[0] : arguments;
for(var i = 0; i < args.length; i++) {
this.views[i] = pkg.$view(args[i]);
this.$recalc(this.views[i]);
}
},
function $prototype() {
/**
* Left padding
* @readOnly
* @private
* @attribute left
* @type {Integer}
*/
/**
* Right padding
* @private
* @readOnly
* @attribute right
* @type {Integer}
*/
/**
* Top padding
* @private
* @readOnly
* @attribute top
* @type {Integer}
*/
/**
* Bottom padding
* @readOnly
* @private
* @attribute bottom
* @type {Integer}
*/
this.left = this.right = this.bottom = this.top = this.height = this.width = 0;
this.getTop = function() {
return this.top;
};
this.getLeft = function() {
return this.left;
};
this.getBottom = function () {
return this.bottom;
};
this.getRight = function () {
return this.right;
};
this.getPreferredSize = function (){
return { width:this.width, height:this.height};
};
this.$recalc = function(v) {
var b = 0, ps = v.getPreferredSize();
if (v.getLeft !== undefined) {
b = v.getLeft();
if (b > this.left) {
this.left = b;
}
}
if (v.getRight !== undefined) {
b = v.getRight();
if (b > this.right) {
this.right = b;
}
}
if (v.getTop !== undefined) {
b = v.getTop();
if (b > this.top) {
this.top = b;
}
}
if (v.getBottom !== undefined) {
b = v.getBottom();
if (b > this.bottom) {
this.bottom = b;
}
}
if (ps.width > this.width) {
this.width = ps.width;
}
if (ps.height > this.height) {
this.height = ps.height;
}
if (this.voutline === undefined && v.outline !== undefined) {
this.voutline = v;
}
};
/**
* Iterate over composed views.
* @param {Function} f callback that is called for every iterated view. The callback
* gets a view index and view itself as its argument.
* @method iterate
*/
this.iterate = function(f) {
for(var i = 0; i < this.views.length; i++) {
f.call(this, i, this.views[i]);
}
};
this.recalc = function() {
this.left = this.right = this.bottom = this.top = this.height = this.width = 0;
this.iterate(function(k, v) {
this.$recalc(v);
});
};
this.ownerChanged = function(o) {
this.iterate(function(k, v) {
if (v !== null && v.ownerChanged !== undefined) {
v.ownerChanged(o);
}
});
};
this.paint = function(g,x,y,w,h,d) {
var ctx = false;
for(var i = 0; i < this.views.length; i++) {
var v = this.views[i];
v.paint(g, x, y, w, h, d);
if (i < this.views.length - 1 && typeof v.outline === 'function' && v.outline(g, x, y, w, h, d)) {
if (ctx === false) {
g.save();
ctx = true;
}
g.clip();
}
}
if (ctx === true) {
g.restore();
}
};
/**
* Return number of composed views.
* @return {Integer} number of composed view.
* @method count
*/
this.count = function() {
return this.views.length;
};
this.outline = function(g,x,y,w,h,d) {
return this.voutline !== undefined && this.voutline.outline(g,x,y,w,h,d);
};
}
]);
/**
* ViewSet view. The view set is a special view container that includes
* number of views accessible by a key and allows only one view be active
* in a particular time. Active is view that has to be rendered. The view
* set can be used to store number of decorative elements where only one
* can be rendered depending from an UI component state.
* @param {Object} views object that represents views instances that have
* to be included in the ViewSet
* @constructor
* @class zebkit.draw.ViewSet
* @extends zebkit.draw.CompositeView
*/
pkg.ViewSet = Class(pkg.CompositeView, [
function(views) {
if (arguments.length === 0 || views === null) {
throw new Error("" + views);
}
/**
* Views set
* @attribute views
* @type Object
* @default {}
* @readOnly
*/
this.views = {};
this.$size = 0;
var activeId = "*";
for(var k in views) {
var id = k;
if (k[0] === '+') {
id = k.substring(1);
activeId = id;
}
this.views[id] = pkg.$view(views[k]);
this.$size++;
if (this.views[id] !== null) {
this.$recalc(this.views[id]);
}
}
this.activate(activeId);
},
function $prototype() {
/**
* Active in the set view
* @attribute activeView
* @type View
* @default null
* @readOnly
*/
this.activeView = null;
this.paint = function(g,x,y,w,h,d) {
if (this.activeView !== null) {
this.activeView.paint(g, x, y, w, h, d);
}
};
this.count = function() {
return this.$size;
};
/**
* Activate the given view from the given set.
* @param {String} id a key of a view from the set to be activated. Pass
* null to make current view to undefined state
* @return {Boolean} true if new view has been activated, false otherwise
* @method activate
*/
this.activate = function(id) {
var old = this.activeView;
if (id === null) {
return (this.activeView = null) !== old;
} else if (this.views.hasOwnProperty(id)) {
return (this.activeView = this.views[id]) !== old;
} else if (id.length > 1 && id[0] !== '*' && id[id.length - 1] !== '*') {
var i = id.indexOf('.');
if (i > 0) {
var k = id.substring(0, i) + '.*';
if (this.views.hasOwnProperty(k)) {
return (this.activeView = this.views[k]) !== old;
} else {
k = "*" + id.substring(i);
if (this.views.hasOwnProperty(k)) {
return (this.activeView = this.views[k]) !== old;
}
}
}
}
// "*" is default view
return this.views.hasOwnProperty("*") ? (this.activeView = this.views["*"]) !== old
: false;
};
this.iterate = function(f) {
for(var k in this.views) {
f.call(this, k, this.views[k]);
}
};
}
]);
/**
* Abstract shape view.
* @param {String} [c] a color of the shape
* @param {String} [fc] a fill color of the shape
* @param {Integer} [w] a line size
* @class zebkit.draw.Shape
* @constructor
* @extends zebkit.draw.View
*/
pkg.Shape = Class(pkg.View, [
function (c, fc, w) {
if (arguments.length > 0) {
this.color = c;
if (arguments.length > 1) {
this.fillColor = fc;
if (arguments.length > 1) {
this.lineWidth = this.gap = w;
}
}
}
},
function $prototype() {
this.gap = 1;
/**
* Shape color.
* @attribute color
* @type {String}
* @default "gray"
*/
this.color = "gray";
/**
* Shape line width
* @attribute lineWidth
* @type {Integer}
* @default 1
*/
this.lineWidth = 1;
/**
* Fill color. null if the shape should not be filled with a color
* @attribute fillColor
* @type {String}
* @default null
*/
this.fillColor = null;
//TODO: comment
this.width = this.height = 8;
//TODO: comment
this.stretched = true;
// TODO: comment
this.setLineWidth = function(w) {
if (w !== this.lineWidth) {
this.lineWidth = this.gap = w;
}
return this;
};
this.paint = function(g,x,y,w,h,d) {
if (this.stretched === false) {
x = x + Math.floor((w - this.width) / 2);
y = y + Math.floor((h - this.height) / 2);
w = this.width;
h = this.height;
}
this.outline(g,x,y,w,h,d);
if (this.fillColor !== null) {
if (this.fillColor !== g.fillStyle) {
g.fillStyle = this.fillColor;
}
g.fill();
}
if (this.color !== null) {
if (g.lineWidth !== this.lineWidth) {
g.lineWidth = this.lineWidth;
}
if (this.color !== g.strokeStyle) {
g.strokeStyle = this.color;
}
g.stroke();
}
};
this.getPreferredSize = function() {
return {
width : this.width,
height : this.height
};
};
}
]);
/**
* Sunken border view
* @class zebkit.draw.Sunken
* @constructor
* @param {String} [brightest] a brightest border line color
* @param {String} [moddle] a middle border line color
* @param {String} [darkest] a darkest border line color
* @extends zebkit.draw.View
*/
pkg.Sunken = Class(pkg.View, [
function (brightest,middle,darkest) {
if (arguments.length > 0) {
this.brightest = brightest;
if (arguments.length > 1) {
this.middle = middle;
if (arguments.length > 2) {
this.darkest = darkest;
}
}
}
},
function $prototype() {
/**
* Brightest border line color
* @attribute brightest
* @readOnly
* @type {String}
* @default "white"
*/
/**
* Middle border line color
* @attribute middle
* @readOnly
* @type {String}
* @default "gray"
*/
/**
* Darkest border line color
* @attribute darkest
* @readOnly
* @type {String}
* @default "black"
*/
this.brightest = "white";
this.middle = "gray" ;
this.darkest = "black";
this.paint = function(g,x1,y1,w,h,d){
var x2 = x1 + w - 1, y2 = y1 + h - 1;
g.setColor(this.middle);
g.drawLine(x1, y1, x2 - 1, y1);
g.drawLine(x1, y1, x1, y2 - 1);
g.setColor(this.brightest);
g.drawLine(x2, y1, x2, y2 + 1);
g.drawLine(x1, y2, x2, y2);
g.setColor(this.darkest);
g.drawLine(x1 + 1, y1 + 1, x1 + 1, y2);
g.drawLine(x1 + 1, y1 + 1, x2, y1 + 1);
};
}
]);
/**
* Etched border view
* @class zebkit.draw.Etched
* @constructor
* @param {String} [brightest] a brightest border line color
* @param {String} [moddle] a middle border line color
* @extends zebkit.draw.View
*/
pkg.Etched = Class(pkg.View, [
function (brightest, middle) {
if (arguments.length > 0) {
this.brightest = brightest;
if (arguments.length > 1) {
this.middle = middle;
}
}
},
function $prototype() {
/**
* Brightest border line color
* @attribute brightest
* @readOnly
* @type {String}
* @default "white"
*/
/**
* Middle border line color
* @attribute middle
* @readOnly
* @type {String}
* @default "gray"
*/
this.brightest = "white";
this.middle = "gray" ;
this.paint = function(g,x1,y1,w,h,d){
var x2 = x1 + w - 1, y2 = y1 + h - 1;
g.setColor(this.middle);
g.drawLine(x1, y1, x1, y2 - 1);
g.drawLine(x2 - 1, y1, x2 - 1, y2);
g.drawLine(x1, y1, x2, y1);
g.drawLine(x1, y2 - 1, x2 - 1, y2 - 1);
g.setColor(this.brightest);
g.drawLine(x2, y1, x2, y2);
g.drawLine(x1 + 1, y1 + 1, x1 + 1, y2 - 1);
g.drawLine(x1 + 1, y1 + 1, x2 - 1, y1 + 1);
g.drawLine(x1, y2, x2 + 1, y2);
};
}
]);
/**
* Raised border view
* @class zebkit.draw.Raised
* @param {String} [brightest] a brightest border line color
* @param {String} [middle] a middle border line color
* @constructor
* @extends zebkit.draw.View
*/
pkg.Raised = Class(pkg.View, [
function(brightest, middle) {
/**
* Brightest border line color
* @attribute brightest
* @readOnly
* @type {String}
* @default "white"
*/
/**
* Middle border line color
* @attribute middle
* @readOnly
* @type {String}
* @default "gray"
*/
if (arguments.length > 0) {
this.brightest = brightest;
if (arguments.length > 1) {
this.middle = middle;
}
}
},
function $prototype() {
this.brightest = "white";
this.middle = "gray";
this.paint = function(g,x1,y1,w,h,d){
var x2 = x1 + w - 1, y2 = y1 + h - 1;
g.setColor(this.brightest);
g.drawLine(x1, y1, x2, y1);
g.drawLine(x1, y1, x1, y2);
g.setColor(this.middle);
g.drawLine(x2, y1, x2, y2 + 1);
g.drawLine(x1, y2, x2, y2);
};
}
]);
/**
* Dotted border view
* @class zebkit.draw.Dotted
* @param {String} [c] the dotted border color
* @constructor
* @extends zebkit.draw.View
*/
pkg.Dotted = Class(pkg.View, [
function (c){
if (arguments.length > 0) {
this.color = c;
}
},
function $prototype() {
/**
* @attribute color
* @readOnly
* @type {String}
* @default "black"
*/
this.color = "black";
this.paint = function(g,x,y,w,h,d){
g.setColor(this.color);
g.drawDottedRect(x, y, w, h);
};
}
]);
/**
* Border view. Can be used to render CSS-like border. Border can be applied to any
* zebkit UI component by calling setBorder method:
// create label component
var lab = new zebkit.ui.Label("Test label");
// set red border to the label component
lab.setBorder(new zebkit.draw.Border("red"));
* @param {String} [c] border color
* @param {Integer} [w] border width
* @param {Integer} [r] border corners radius
* @constructor
* @class zebkit.draw.Border
* @extends zebkit.draw.View
*/
pkg.Border = Class(pkg.View, [
function(c, w, r) {
if (arguments.length > 0) {
this.color = c;
if (arguments.length > 1) {
this.width = this.gap = w;
if (arguments.length > 2) {
this.radius = r;
if (arguments.length > 3) {
for (var i = 3; i < arguments.length; i++) {
this.setSides(arguments[i]);
}
}
}
}
}
},
function $prototype() {
/**
* Border color
* @attribute color
* @readOnly
* @type {String}
* @default "gray"
*/
/**
* Border line width
* @attribute width
* @readOnly
* @type {Integer}
* @default 1
*/
/**
* Border radius
* @attribute radius
* @readOnly
* @type {Integer}
* @default 0
*/
this.color = "gray";
this.gap = this.width = 1;
this.radius = 0;
this.sides = 15;
/**
* Control border sides visibility.
* @param {String} side* list of visible sides. You can pass number of arguments
* to say which sides of the border are visible. The arguments can equal one of the
* following value: "top", "bottom", "left", "right"
* @method setSides
* @chainable
*/
this.setSides = function() {
this.sides = 0;
for(var i = 0; i < arguments.length; i++) {
if (arguments[i] === "top") {
this.sides |= 1;
} else if (arguments[i] === "left") {
this.sides |= 2;
} else if (arguments[i] === "bottom") {
this.sides |= 4;
} else if (arguments[i] === "right" ) {
this.sides |= 8;
}
}
return this;
};
this.paint = function(g,x,y,w,h,d){
if (this.color !== null && this.width > 0) {
var ps = g.lineWidth;
if (g.lineWidth !== this.width) {
g.lineWidth = this.width;
}
if (this.radius > 0) {
this.outline(g,x,y,w,h, d);
g.setColor(this.color);
g.stroke();
} else if (this.sides !== 15) {
g.setColor(this.color);
// top
if ((this.sides & 1) > 0) {
g.drawLine(x, y, x + w, y, this.width);
}
// right
if ((this.sides & 8) > 0) {
g.drawLine(x + w - this.width, y, x + w - this.width, y + h, this.width);
}
// bottom
if ((this.sides & 4) > 0) {
g.drawLine(x, y + h - this.width, x + w, y + h - this.width, this.width);
}
// left
if ((this.sides & 2) > 0) {
g.drawLine(x, y, x, y + h, this.width);
}
} else {
var dt = this.width / 2;
g.beginPath();
g.rect(x + dt, y + dt, w - this.width, h - this.width);
g.closePath();
g.setColor(this.color);
g.stroke();
}
if (g.lineWidth !== ps) {
g.lineWidth = ps;
}
}
};
/**
* Defines border outline for the given 2D Canvas context
* @param {CanvasRenderingContext2D} g
* @param {Integer} x x coordinate
* @param {Integer} y y coordinate
* @param {Integer} w required width
* @param {Integer} h required height
* @param {Integer} d target UI component
* @method outline
* @return {Boolean} true if the outline has to be applied as an
* UI component shape
*/
this.outline = function(g,x,y,w,h,d) {
if (this.radius <= 0) {
return false;
}
var r = this.radius,
dt = this.width / 2,
xx = x + w - dt,
yy = y + h - dt;
x += dt;
y += dt;
// !!! this code can work improperly in IE 10 in Vista !
// g.beginPath();
// g.moveTo(x+r, y);
// g.arcTo(xx, y, xx, yy, r);
// g.arcTo(xx, yy, x, yy, r);
// g.arcTo(x, yy, x, y, r);
// g.arcTo(x, y, xx, y, r);
// g.closePath();
// return true;
g.beginPath();
g.moveTo(x + r, y);
g.lineTo(xx - r, y);
g.quadraticCurveTo(xx, y, xx, y + r);
g.lineTo(xx, yy - r);
g.quadraticCurveTo(xx, yy, xx - r, yy);
g.lineTo(x + r, yy);
g.quadraticCurveTo(x, yy, x, yy - r);
g.lineTo(x, y + r);
g.quadraticCurveTo(x, y, x + r, y);
g.closePath();
return true;
};
}
]);
/**
* Round border view.
* @param {String} [col] border color. Use null as the
* border color value to prevent painting of the border
* @param {Integer} [width] border width
* @constructor
* @class zebkit.draw.RoundBorder
* @extends zebkit.draw.View
*/
pkg.RoundBorder = Class(pkg.View, [
function(col, width) {
if (arguments.length > 0) {
if (zebkit.isNumber(col)) {
this.width = col;
} else {
this.color = col;
if (zebkit.isNumber(width)) {
this.width = width;
}
}
}
this.gap = this.width;
},
function $prototype() {
/**
* Border width
* @attribute width
* @readOnly
* @type {Integer}
* @default 1
*/
this.width = 1;
/**
* Border color
* @attribute color
* @readOnly
* @type {String}
* @default null
*/
this.color = null;
/**
* Color to fill the inner area surrounded with the round border.
* @attribute fillColor
* @type {String}
* @default null
*/
this.fillColor = null;
this.paint = function(g,x,y,w,h,d) {
if (this.color !== null && this.width > 0) {
this.outline(g,x,y,w,h,d);
g.setColor(this.color);
g.stroke();
if (this.fillColor !== null) {
g.setColor(this.fillColor);
g.fill();
}
}
};
this.outline = function(g,x,y,w,h,d) {
g.lineWidth = this.width;
if (w === h) {
g.beginPath();
g.arc(Math.floor(x + w / 2) + (w % 2 === 0 ? 0 : 0.5),
Math.floor(y + h / 2) + (h % 2 === 0 ? 0 : 0.5),
Math.floor((w - g.lineWidth) / 2), 0, 2 * Math.PI, false);
g.closePath();
} else {
g.ovalPath(x,y,w,h);
}
return true;
};
this.getPreferredSize = function() {
var s = this.width * 8;
return {
width : s, height : s
};
};
}
]);
/**
* Render class that allows developers to render a border with a title area.
* The title area has to be specified by an UI component that uses the border
* by defining "getTitleInfo()"" method. The method has to return object that
* describes title size, location and alignment:
*
*
* {
* x: {Integer}, y: {Integer},
* width: {Integer}, height: {Integer},
* orient: {String}
* }
*
*
* @class zebkit.draw.TitledBorder
* @extends zebkit.draw.Render
* @constructor
* @param zebkit.draw.View border a border to be rendered with a title area
* @param {String} [lineAlignment] a line alignment. Specifies how
* a title area has to be aligned relatively border line:
*
* "bottom" - title area will be placed on top of border line:
* ___| Title area |___
*
*
* "center" - title area will be centered relatively to border line:
* ---| Title area |-----
*
*
* "top" - title area will be placed underneath of border line:
* ____ ________
* | Title area |
*
*/
pkg.TitledBorder = Class(pkg.Render, [
function (b, a){
if (arguments.length > 1) {
this.lineAlignment = zebkit.util.validateValue(a, "bottom", "top", "center");
}
this.setValue(pkg.$view(b));
},
function $prototype() {
this.lineAlignment = "bottom";
this.getTop = function (){
return this.target.getTop();
};
this.getLeft = function (){
return this.target.getLeft();
};
this.getRight = function (){
return this.target.getRight();
};
this.getBottom = function (){
return this.target.getBottom();
};
this.outline = function (g,x,y,w,h,d) {
var xx = x + w, yy = y + h;
if (d.getTitleInfo !== undefined) {
var r = d.getTitleInfo();
if (r !== null) {
switch(r.orient) {
case "bottom":
var bottom = this.target.getBottom();
switch (this.lineAlignment) {
case "center" : yy = r.y + Math.floor((r.height - bottom)/ 2) + bottom; break;
case "top" : yy = r.y + r.height + bottom; break;
case "bottom" : yy = r.y; break;
}
break;
case "top":
var top = this.target.getTop();
switch (this.lineAlignment) {
case "center" : y = r.y + Math.floor((r.height - top)/2); break; // y = r.y + Math.floor(r.height/ 2) ; break;
case "top" : y = r.y - top; break;
case "bottom" : y = r.y + r.height; break;
}
break;
case "left":
var left = this.target.getLeft();
switch (this.lineAlignment) {
case "center" : x = r.x + Math.floor((r.width - left) / 2); break;
case "top" : x = r.x - left; break;
case "bottom" : x = r.x + r.width; break;
}
break;
case "right":
var right = this.target.getRight();
switch (this.lineAlignment) {
case "center" : xx = r.x + Math.floor((r.width - right) / 2) + right; break;
case "top" : xx = r.x + r.width + right; break;
case "bottom" : xx = r.x; break;
}
break;
}
}
}
if (this.target !== null &&
this.target.outline !== undefined &&
this.target.outline(g, x, y, xx - x, yy - y, d) === true)
{
return true;
}
g.beginPath();
g.rect(x, y, xx - x, yy - y);
g.closePath();
return true;
};
this.$isIn = function(clip, x, y, w, h) {
var rx = clip.x > x ? clip.x : x,
ry = clip.y > y ? clip.y : y,
rw = Math.min(clip.x + clip.width, x + w) - rx,
rh = Math.min(clip.y + clip.height, y + h) - ry;
return (clip.x === rx && clip.y === ry && clip.width === rw && clip.height === rh);
};
this.paint = function(g,x,y,w,h,d){
if (d.getTitleInfo !== undefined) {
var r = d.getTitleInfo();
if (r !== null) {
var xx = x + w, yy = y + h, t = g.$states[g.$curState];
switch (r.orient) {
case "top":
var top = this.target.getTop();
// compute border y
switch (this.lineAlignment) {
case "center" : y = r.y + Math.floor((r.height - top) / 2) ; break;
case "top" : y = r.y - top; break;
case "bottom" : y = r.y + r.height; break;
}
// skip rendering border if the border is not in clip rectangle
// This is workaround because of IE10/IE11 have bug what causes
// handling rectangular clip + none-rectangular clip side effect
// to "fill()" subsequent in proper working (fill without respect of
// clipping area)
if (this.$isIn(t, x + this.target.getLeft(), y,
w - this.target.getRight() - this.target.getLeft(),
yy - y - this.target.getBottom()))
{
return;
}
g.save();
g.beginPath();
g.moveTo(x, y);
g.lineTo(r.x, y);
g.lineTo(r.x, y + top);
g.lineTo(r.x + r.width, y + top);
g.lineTo(r.x + r.width, y);
g.lineTo(xx, y);
g.lineTo(xx, yy);
g.lineTo(x, yy);
g.lineTo(x, y);
break;
case "bottom":
var bottom = this.target.getBottom();
switch (this.lineAlignment) {
case "center" : yy = r.y + Math.floor((r.height - bottom) / 2) + bottom; break;
case "top" : yy = r.y + r.height + bottom; break;
case "bottom" : yy = r.y ; break;
}
if (this.$isIn(t, x + this.target.getLeft(), y + this.target.getTop(),
w - this.target.getRight() - this.target.getLeft(),
yy - y - this.target.getTop()))
{
return;
}
g.save();
g.beginPath();
g.moveTo(x, y);
g.lineTo(xx, y);
g.lineTo(xx, yy);
g.lineTo(r.x + r.width, yy);
g.lineTo(r.x + r.width, yy - bottom);
g.lineTo(r.x, yy - bottom);
g.lineTo(r.x, yy);
g.lineTo(x, yy);
g.lineTo(x, y);
break;
case "left":
var left = this.target.getLeft();
switch (this.lineAlignment) {
case "center" : x = r.x + Math.floor((r.width - left) / 2); break;
case "top" : x = r.x - left; break;
case "bottom" : x = r.x + r.width; break;
}
if (this.$isIn(t, x, y + this.target.getTop(),
xx - x - this.target.getRight(),
h - this.target.getTop() - this.target.getBottom()))
{
return;
}
g.save();
g.beginPath();
g.moveTo(x, y);
g.lineTo(xx, y);
g.lineTo(xx, yy);
g.lineTo(x, yy);
g.lineTo(x, r.y + r.height);
g.lineTo(x + left, r.y + r.height);
g.lineTo(x + left, r.y);
g.lineTo(x, r.y);
g.lineTo(x, y);
break;
case "right":
var right = this.target.getRight();
switch (this.lineAlignment) {
case "center" : xx = r.x + Math.floor((r.width - right) / 2) + right; break;
case "top" : xx = r.x + r.width + right; break;
case "bottom" : xx = r.x; break;
}
if (this.$isIn(t, x + this.target.getLeft(),
y + this.target.getTop(),
xx - x - this.target.getLeft(),
h - this.target.getTop() - this.target.getBottom()))
{
return;
}
g.save();
g.beginPath();
g.moveTo(x, y);
g.lineTo(xx, y);
g.lineTo(xx, r.y);
g.lineTo(xx - right, r.y);
g.lineTo(xx - right, r.y + r.height);
g.lineTo(xx, r.y + r.height);
g.lineTo(xx, yy);
g.lineTo(x, yy);
g.lineTo(x, y);
break;
// throw error to avoid wrongly called restore method below
default: throw new Error("Invalid title orientation " + r.orient);
}
g.closePath();
g.clip();
this.target.paint(g, x, y, xx - x, yy - y, d);
g.restore();
}
} else {
this.target.paint(g, x, y, w, h, d);
}
};
}
]);
/**
* Break the given line to parts that can be placed in the area with
* the specified width.
* @param {zebkit.Font} font a font to compute text metrics
* @param {Integer} maxWidth a maximal area with
* @param {String} line a line
* @param {Array} result a result array to accumulate wrapped lines
* @method wrapToLines
* @for zebkit.draw
*/
pkg.wrapToLines = function(font, maxWidth, line, result) {
// The method goes through number of tokens the line is split. Token
// is a word or delimiter. Delimiter is specified with the regexp
// (in this implementation delimiter is one or more space). Delimiters
// are also considered as tokens. On every iteration accumulated tokens
// width is compared with maximal possible and if the width is greater
// then maximal we sift to previous tokens set and put it as line to
// result. Then start iterating again from the last token we could
// not accommodate.
if (line === "") {
result.push(line);
} else {
var len = font.stringWidth(line);
if (len <= maxWidth) {
result.push(line);
} else {
var m = "not null",
b = true,
i = 0,
al = 0,
pos = 0,
skip = false,
tokenEnd = 0,
searchRE = /\s+/g,
tokenStart = -1;
for(; pos !== line.length; ) {
if (skip !== true && m !== null) {
if (b) {
m = searchRE.exec(line);
if (m === null) {
tokenStart = tokenEnd;
tokenEnd = line.length;
}
}
if (m !== null) {
if (m.index > tokenEnd) {
// word token detected
tokenStart = tokenEnd;
tokenEnd = m.index;
b = false;
} else {
// space token detected
tokenStart = m.index;
tokenEnd = m.index + m[0].length;
b = true;
}
}
}
skip = false;
al = font.stringWidth(line.substring(pos, tokenEnd));
if (al > maxWidth) {
if (i === 0) {
result.push(line.substring(pos, tokenEnd));
pos = tokenEnd;
} else {
result.push(line.substring(pos, tokenStart));
pos = tokenStart;
skip = true;
i = 0;
}
} else {
if (tokenEnd === line.length) {
result.push(line.substring(pos, tokenEnd));
break;
} else {
i++;
}
}
}
}
}
};
/**
* Default normal font
* @attribute font
* @type {zebkit.Font}
* @for zebkit.draw
*/
pkg.font = new zebkit.Font("Arial", 14);
/**
* Default small font
* @attribute smallFont
* @type {zebkit.Font}
* @for zebkit.draw
*/
pkg.smallFont = new zebkit.Font("Arial", 10);
/**
* Default bold font
* @attribute boldFont
* @type {zebkit.Font}
* @for zebkit.draw
*/
pkg.boldFont = new zebkit.Font("Arial", "bold", 12);
/**
* Base class to build text render implementations.
* @class zebkit.draw.BaseTextRender
* @constructor
* @param {Object} [target] target component to be rendered
* @extends zebkit.draw.Render
*/
pkg.BaseTextRender = Class(pkg.Render, [
function $clazz() {
this.font = pkg.font;
this.color = "gray";
this.disabledColor = "white";
},
function $prototype(clazz) {
/**
* UI component that holds the text render
* @attribute owner
* @default null
* @readOnly
* @protected
* @type {zebkit.layout.Layoutable}
*/
this.owner = null;
/**
* Line indention
* @attribute lineIndent
* @type {Integer}
* @default 1
*/
this.lineIndent = 1;
// implement position metric methods
this.getMaxOffset = this.getLineSize = this.getLines = function() {
return 0;
};
/**
* Set the rendered text font.
* @param {String|zebkit.Font} f a font as CSS string or
* zebkit.Font class instance
* @chainable
* @method setFont
*/
this.setFont = function(f) {
if ((f instanceof zebkit.Font) === false && f !== null) {
f = zebkit.newInstance(zebkit.Font, arguments);
}
if (f != this.font) {
this.font = f;
if (this.owner !== null && this.owner.isValid === true) {
this.owner.invalidate();
}
if (this.invalidate !== undefined) {
this.invalidate();
}
}
return this;
};
/**
* Resize font
* @param {String|Integer} size a new size of the font
* @chainable
* @method resizeFont
*/
this.resizeFont = function(size) {
return this.setFont(this.font.resize(size));
};
/**
* Re-style font.
* @param {String} style a new font style
* @method restyleFont
* @chainable
*/
this.restyleFont = function(style) {
return this.setFont(this.font.restyle(style));
};
/**
* Get line height
* @method getLineHeight
* @return {Integer} a line height
*/
this.getLineHeight = function() {
return this.font.height;
};
/**
* Set rendered text color
* @param {String} c a text color
* @method setColor
* @chainable
*/
this.setColor = function(c) {
if (c != this.color) {
this.color = c.toString();
}
return this;
};
/**
* Called whenever an owner UI component has been changed
* @param {zebkit.layout.Layoutable} v a new owner UI component
* @method ownerChanged
*/
this.ownerChanged = function(v) {
this.owner = v;
};
this.getLine = function(i) {
throw new Error("Not implemented");
};
/**
* Overridden method to catch target value changing events.
* @param {Object} o an old target value
* @param {Object} n a new target value
* @method valueWasChanged
*/
this.valueWasChanged = function(o, n) {
if (this.owner !== null && this.owner.isValid) {
this.owner.invalidate();
}
if (this.invalidate !== undefined) {
this.invalidate();
}
};
this.toString = function() {
return this.target === null ? null
: this.target;
};
}
]);
/**
* Lightweight implementation of single line string render. The render requires
* a simple string as a target object.
* @param {String} str a string to be rendered
* @param {zebkit.Font} [font] a text font
* @param {String} [color] a text color
* @constructor
* @extends zebkit.draw.BaseTextRender
* @use zebkit.util.Position.Metric
* @class zebkit.draw.StringRender
*/
pkg.StringRender = Class(pkg.BaseTextRender, zebkit.util.Position.Metric, [
function $prototype() {
/**
* Calculated string width (in pixels). If string width has not been calculated
* the value is set to -1.
* @attribute stringWidth
* @protected
* @default -1
* @type {Integer}
*/
this.stringWidth = -1;
// for the sake of speed up construction of the widely used render
// declare it none standard way.
this[''] = function(txt, font, color) {
this.setValue(txt);
/**
* Font to be used to render the target string
* @attribute font
* @readOnly
* @type {zebkit.Font}
*/
this.font = arguments.length > 1 ? font : this.clazz.font;
if ((this.font instanceof zebkit.Font) === false) {
this.font = zebkit.$font(this.font);
}
/**
* Color to be used to render the target string
* @readOnly
* @attribute color
* @type {String}
*/
this.color = arguments.length > 2 ? color : this.clazz.color;
};
// TODO: the methods below simulate text model methods. it is done for the future
// usage of the string render for text field. The render can be convenient for masked
// input implementation.
this.write = function(s, off) {
if (off === 0) {
this.target = s + this.target;
} else if (off === s.length) {
this.target = this.target + s;
} else {
this.target = this.target.substring(0, off) + s + this.target.substring(off);
}
return true;
};
this.remove = function(off, len) {
this.target = this.target.substring(0, off) +
this.target.substring(off + len);
return true;
};
this.replace = function(s, off, size) {
if (s.length === 0) {
return this.remove(off, size);
} else if (size === 0) {
return this.write(s, off);
} else {
var b = this.remove(off, size, false);
return this.write(s, off) && b;
}
};
/**
* Implementation of position metric interface. Returns maximal
* possible offset within the given string.
* @method getMaxOffset
* @return {Integer} a maximal possible offset.
*/
this.getMaxOffset = function() {
return this.target.length;
};
/**
* Implementation of position metric interface. Returns the given
* line size (in characters).
* @param {Integer} line a line number. This render supports only
* single line.
* @method getLineSize
* @return {Integer} a line size
*/
this.getLineSize = function(line) {
if (line > 0) {
throw new RangeError("Line number " + line + " is out of the range");
}
return this.target.length + 1;
};
/**
* Implementation of position metric interface. Returns number
* of lines.
* @method getLines
* @return {Integer} a number of lines.
*/
this.getLines = function() {
return 1;
};
/**
* Calculates string width if it has not been done yet.
* @method calcLineWidth
* @protected
* @return {Integer} a string width
*/
this.calcLineWidth = function() {
if (this.stringWidth < 0) {
this.stringWidth = this.font.stringWidth(this.target);
}
return this.stringWidth;
};
/**
* Invalidate the render state. Invalidation flushes string metrics
* to be re-calculated again.
* @protected
* @method invalidate
*/
this.invalidate = function() {
this.stringWidth = -1;
};
this.paint = function(g,x,y,w,h,d) {
// save a few milliseconds
if (this.font.s !== g.font) {
g.setFont(this.font);
}
if (d !== null && d.getStartSelection !== undefined) {
var startSel = d.getStartSelection(),
endSel = d.getEndSelection();
if (startSel !== null &&
endSel !== null &&
startSel.col !== endSel.col &&
d.selectView !== null )
{
d.selectView.paint(g, x + this.font.charsWidth(this.target, 0, startSel.col),
y,
this.font.charsWidth(this.target,
startSel.col,
endSel.col - startSel.col),
this.getLineHeight(), d);
}
}
// save a few milliseconds
if (this.color !== g.fillStyle) {
g.fillStyle = this.color;
}
if (d !== null && d.isEnabled === false) {
g.fillStyle = d !== null &&
d.disabledColor !== null &&
d.disabledColor !== undefined ? d.disabledColor
: this.clazz.disabledColor;
}
g.fillText(this.target, x, y);
};
/**
* Get the given line.
* @param {Integer} l a line number
* @return {String} a line
* @method getLine
*/
this.getLine = function(l) {
if (l < 0 || l > 1) {
throw new RangeError();
}
return this.target;
};
this.getPreferredSize = function() {
if (this.stringWidth < 0) {
this.stringWidth = this.font.stringWidth(this.target);
}
return {
width: this.stringWidth,
height: this.font.height
};
};
}
]);
/**
* Text render that expects and draws a text model or a string as its target
* @class zebkit.draw.TextRender
* @constructor
* @extends zebkit.draw.BaseTextRender
* @uses zebkit.util.Position.Metric
* @param {String|zebkit.data.TextModel} text a text as string or text model object
*/
pkg.TextRender = Class(pkg.BaseTextRender, zebkit.util.Position.Metric, [
function $prototype() {
this.textWidth = this.textHeight = this.startInvLine = this.invLines = 0;
// speed up constructor by avoiding super execution since
// text render is one of the most used class
this[''] = function(text) {
/**
* Text color
* @attribute color
* @type {String}
* @default zebkit.draw.TextRender.color
* @readOnly
*/
this.color = this.clazz.color;
/**
* Text font
* @attribute font
* @type {String|zebkit.Font}
* @default zebkit.draw.TextRender.font
* @readOnly
*/
this.font = this.clazz.font;
this.setValue(text);
};
this.write = function() {
return this.target.write.apply(this.target, arguments);
};
this.remove = function() {
return this.target.remove.apply(this.target, arguments);
};
this.replace = function() {
return this.target.replace.apply(this.target, arguments);
};
this.on = function() {
return this.target.on.apply(this.target, arguments);
};
this.off = function() {
return this.target.off.apply(this.target, arguments);
};
/**
* Get number of lines of target text
* @return {Integer} a number of line in the target text
* @method getLines
*/
this.getLines = function() {
return this.target.getLines();
};
this.getLineSize = function(l) {
return this.target.getLine(l).length + 1;
};
this.getMaxOffset = function() {
return this.target.getTextLength();
};
/**
* Paint the specified text line
* @param {CanvasRenderingContext2D} g graphical 2D context
* @param {Integer} x x coordinate
* @param {Integer} y y coordinate
* @param {Integer} line a line number
* @param {zebkit.layout.Layoutable} d an UI component on that the line has to be rendered
* @method paintLine
*/
this.paintLine = function(g,x,y,line,d) {
g.fillText(this.getLine(line), x, y);
};
/**
* Get text line by the given line number
* @param {Integer} r a line number
* @return {String} a text line
* @method getLine
*/
this.getLine = function(r) {
return this.target.getLine(r);
};
/**
* Set the text model content
* @param {String|zebkit.data.TextModel} s a text as string object
* @method setValue
* @chainable
*/
this.setValue = function(s) {
if (s !== null && (typeof s === "string" || s.constructor === String)) {
if (this.target !== null) {
this.target.setValue(s);
return this;
} else {
s = new zebkit.data.Text(s);
}
}
//TODO: copy paste from Render to speed up
if (this.target !== s) {
var old = this.target;
this.target = s;
if (this.valueWasChanged !== undefined) {
this.valueWasChanged(old, s);
}
}
return this;
};
/**
* Get the given text line width in pixels
* @param {Integer} line a text line number
* @return {Integer} a text line width in pixels
* @method lineWidth
*/
this.calcLineWidth = function(line){
if (this.invLines > 0) {
this.recalc();
}
return this.target.$lineTags(line).$lineWidth;
};
/**
* Called every time the target text metrics has to be recalculated
* @method recalc
*/
this.recalc = function() {
if (this.invLines > 0 && this.target !== null){
var model = this.target, i = 0;
if (this.invLines > 0) {
for(i = this.startInvLine + this.invLines - 1; i >= this.startInvLine; i--) {
model.$lineTags(i).$lineWidth = this.font.stringWidth(this.getLine(i));
}
this.startInvLine = this.invLines = 0;
}
this.textWidth = 0;
var size = model.getLines();
for(i = 0; i < size; i++){
var len = model.$lineTags(i).$lineWidth;
if (len > this.textWidth) {
this.textWidth = len;
}
}
this.textHeight = this.getLineHeight() * size + (size - 1) * this.lineIndent;
}
};
/**
* Text model update listener handler
* @param {zebkit.data.TextEvent} e text event
* @method textUpdated
*/
this.textUpdated = function(e) {
if (e.id === "remove") {
if (this.invLines > 0) {
var p1 = e.line - this.startInvLine,
p2 = this.startInvLine + this.invLines - e.line - e.lines;
this.invLines = ((p1 > 0) ? p1 : 0) + ((p2 > 0) ? p2 : 0) + 1;
this.startInvLine = this.startInvLine < e.line ? this.startInvLine : e.line;
} else {
this.startInvLine = e.line;
this.invLines = 1;
}
if (this.owner !== null && this.owner.isValid === true) {
this.owner.invalidate();
}
} else { // insert
// TODO: check the code
if (this.invLines > 0) {
if (e.line <= this.startInvLine) {
this.startInvLine += (e.lines - 1);
} else if (e.line < (this.startInvLine + this.invLines)) {
this.invLines += (e.lines - 1);
}
}
this.invalidate(e.line, e.lines);
}
};
/**
* Invalidate metrics for the specified range of lines.
* @param {Integer} start first line to be invalidated
* @param {Integer} size number of lines to be invalidated
* @method invalidate
* @private
*/
this.invalidate = function(start,size) {
if (arguments.length === 0) {
start = 0;
size = this.getLines();
if (size === 0) {
this.invLines = 0;
return;
}
}
if (size > 0 && (this.startInvLine !== start || size !== this.invLines)) {
if (this.invLines === 0){
this.startInvLine = start;
this.invLines = size;
} else {
var e = this.startInvLine + this.invLines;
this.startInvLine = start < this.startInvLine ? start : this.startInvLine;
this.invLines = Math.max(start + size, e) - this.startInvLine;
}
if (this.owner !== null) {
this.owner.invalidate();
}
}
};
this.getPreferredSize = function(){
if (this.invLines > 0 && this.target !== null) {
this.recalc();
}
return { width:this.textWidth, height:this.textHeight };
};
this.paint = function(g,x,y,w,h,d) {
var ts = g.$states[g.$curState];
if (ts.width > 0 && ts.height > 0) {
var lineIndent = this.lineIndent,
lineHeight = this.getLineHeight(),
lilh = lineHeight + lineIndent,
startInvLine = 0;
w = ts.width < w ? ts.width : w;
h = ts.height < h ? ts.height : h;
if (y < ts.y) {
startInvLine = Math.floor((lineIndent + ts.y - y) / lilh);
h += (ts.y - startInvLine * lineHeight - startInvLine * lineIndent);
} else if (y > (ts.y + ts.height)) {
return;
}
var size = this.getLines();
if (startInvLine < size){
var lines = Math.floor((h + lineIndent) / lilh) + (((h + lineIndent) % lilh > lineIndent) ? 1 : 0), i = 0;
if (startInvLine + lines > size) {
lines = size - startInvLine;
}
y += startInvLine * lilh;
// save few milliseconds
if (this.font.s !== g.font) {
g.setFont(this.font);
}
if (d === null || d.isEnabled === true){
// save few milliseconds
if (this.color != g.fillStyle) {
g.fillStyle = this.color;
}
var p1 = null, p2 = null, bsel = false;
if (lines > 0 && d !== null && d.getStartSelection !== undefined) {
p1 = d.getStartSelection();
p2 = d.getEndSelection();
bsel = p1 !== null && (p1.row !== p2.row || p1.col !== p2.col);
}
for(i = 0; i < lines; i++){
if (bsel === true) {
var line = i + startInvLine;
if (line >= p1.row && line <= p2.row){
var s = this.getLine(line),
lw = this.calcLineWidth(line),
xx = x;
if (line === p1.row) {
var ww = this.font.charsWidth(s, 0, p1.col);
xx += ww;
lw -= ww;
if (p1.row === p2.row) {
lw -= this.font.charsWidth(s, p2.col, s.length - p2.col);
}
} else if (line === p2.row) {
lw = this.font.charsWidth(s, 0, p2.col);
}
this.paintSelection(g, xx, y, lw === 0 ? 1 : lw, lilh, line, d);
// restore color to paint text since it can be
// res-set with paintSelection method
if (this.color !== g.fillStyle) {
g.fillStyle = this.color;
}
}
}
this.paintLine(g, x, y, i + startInvLine, d);
y += lilh;
}
} else {
var dcol = d !== null &&
d.disabledColor !== null &&
d.disabledColor !== undefined ? d.disabledColor
: pkg.TextRender.disabledColor;
for(i = 0; i < lines; i++) {
g.setColor(dcol);
this.paintLine(g, x, y, i + startInvLine, d);
y += lilh;
}
}
}
}
};
/**
* Paint the specified text selection of the given line. The area
* where selection has to be rendered is denoted with the given
* rectangular area.
* @param {CanvasRenderingContext2D} g a canvas graphical context
* @param {Integer} x a x coordinate of selection rectangular area
* @param {Integer} y a y coordinate of selection rectangular area
* @param {Integer} w a width of of selection rectangular area
* @param {Integer} h a height of of selection rectangular area
* @param {Integer} line [description]
* @param {zebkit.layout.Layoutable} d a target UI component where the text
* has to be rendered
* @protected
* @method paintSelection
*/
this.paintSelection = function(g, x, y, w, h, line, d) {
if (d.selectView !== null) {
d.selectView.paint(g, x, y, w, h, d);
}
};
this.toString = function() {
return this.target === null ? null
: this.target.getValue();
};
},
function valueWasChanged(o, n) {
if (o !== null) {
o.off(this);
}
if (n !== null) {
n.on(this);
}
this.$super(o, n);
}
]);
/**
* Render to visualize string whose width is greater than available or specified width.
* @class zebkit.draw.CutStringRender
* @extends zebkit.draw.StringRender
* @constructor
* @param {String} [txt] a string to be rendered
*/
pkg.CutStringRender = Class(pkg.StringRender, [
function $prototype() {
/**
* Maximal width of the string in pixels. By default the attribute
* is set to -1.
* @attribute maxWidth
* @type {Integer}
* @default -1
*/
this.maxWidth = -1;
/**
* String to be rendered at the end of cut string
* @attribute dots
* @type {String}
* @default "..."
*/
this.dots = "...";
},
function paint(g,x,y,w,h,d) {
var maxw = -1;
if (this.maxWidth > 0 && this.stringWidth > this.maxWidth) {
maxw = this.maxWidth;
} else if (this.stringWidth > w) {
maxw = w;
} else if (g.$states !== undefined) {
var ts = g.$states[g.$curState];
if (ts.x > x || (ts.x + ts.width) < (x + w)) {
maxw = ts.width;
}
}
if (maxw <= 0) {
this.$super(g,x,y,w,h,d);
} else {
var dotsLen = this.font.stringWidth(this.dots);
try {
g.save();
g.clipRect(x, y, maxw - dotsLen, h);
this.$super(g,x,y,w,h,d);
} catch(e) {
g.restore();
throw e;
}
g.restore();
g.setColor(this.color);
g.setFont(this.font);
g.fillText(this.dots, x + maxw - dotsLen, y);
}
}
]);
/**
* Wrapped text render.
* @constructor
* @param {String|zebkit.data.TextModel} text a text as string or text model object
* @class zebkit.draw.WrappedTextRender
* @extends zebkit.draw.TextRender
*/
pkg.WrappedTextRender = Class(pkg.TextRender, [
function $prototype() {
this.$brokenLines = [];
this.$lastWidth = -1;
/**
* Break text model to number of lines taking in account the maximal width.
* @param {Integer} w a maximal width
* @return {Array} an array of lines
* @method $breakToLines
* @private
*/
this.$breakToLines = function(w) {
var res = [];
for (var i = 0; i < this.target.getLines(); i++) {
pkg.wrapToLines(this.font, w, this.target.getLine(i), res);
}
return res;
};
},
function getLines() {
return this.$lastWidth < 0 ? this.$super()
: this.$brokenLines.length;
},
function getLine(i) {
return this.$lastWidth < 0 ? this.$super(i)
: this.$brokenLines[i];
},
function invalidate(sl, len){
this.$super(sl, len);
this.$brokenLines.length = 0;
this.$lastWidth = -1;
},
/**
* Get preferred size of the text render
* @param {Integer} [pw] a width the wrapped text has to be computed
* @return {Object} a preferred size
*
* { width: {Integer}, height:{Integer} }
*
* @method calcPreferredSize
*/
function calcPreferredSize(pw) {
if (arguments.length > 0) {
var bl = [];
if (this.$lastWidth < 0 || this.$lastWidth !== pw) {
bl = this.$breakToLines(pw);
} else {
bl = this.$brokenLines;
}
return {
width : pw,
height : bl.length * this.getLineHeight() +
(bl.length - 1) * this.lineIndent
};
} else {
return this.$super();
}
},
function paint(g,x,y,w,h,d) {
if (this.$lastWidth < 0 || this.$lastWidth !== w) {
this.$lastWidth = w;
this.$brokenLines = this.$breakToLines(this.$lastWidth);
}
this.$super(g,x,y,w,h,d);
}
]);
/**
* Decorated text render. This decorator allows developer to draw under, over or strike
* lines over the rendered text.
* @class zebkit.draw.DecoratedTextRender
* @extends zebkit.draw.TextRender
* @constructor
* @param {String|zebkit.data.TextModel} text a text as string or text model object
*/
pkg.DecoratedTextRender = zebkit.Class(pkg.TextRender, [
function(text) {
this.decorations = {
underline : false,
strike : false,
overline : false
};
this.$super(text);
},
function $prototype() {
/**
* Line width
* @attribute lineWidth
* @type {Integer}
* @default 1
*/
this.lineWidth = 1;
/**
* Decoration line color
* @attribute lineColor
* @type {String}
* @default null
*/
this.lineColor = null;
/**
* Set set of decorations.
* @param {String} [decoration]* set of decorations.
* @method setDecorations
* @chainable
*/
this.setDecorations = function() {
for(var k in this.decorations) {
this.decorations[k] = false;
}
this.addDecorations.apply(this, arguments);
return this;
};
/**
* Clear the given decorations.
* @param {String} [decorations]* decorations IDs.
* @chainable
* @method clearDecorations
*/
this.clearDecorations = function() {
for (var i = 0; i < arguments.length; i++) {
zebkit.util.validateValue(arguments[i], "underline", "overline", "strike");
this.decorations[arguments[i]] = false;
}
return this;
};
/**
* Add the given decorations.
* @param {String} [decorations]* decorations IDs.
* @chainable
* @method addDecorations
*/
this.addDecorations = function() {
for (var i = 0; i < arguments.length; i++) {
zebkit.util.validateValue(arguments[i], "underline", "overline", "strike");
this.decorations[arguments[i]] = true;
}
return this;
};
this.hasDecoration = function(dec) {
return this.decorations[dec] === true;
};
},
function paintLine(g,x,y,line,d) {
this.$super(g,x,y,line,d);
var lw = this.calcLineWidth(line),
lh = this.getLineHeight(line);
if (this.lineColor !== null) {
g.setColor(this.lineColor);
} else {
g.setColor(this.color);
}
if (this.decorations.overline) {
g.lineWidth = this.lineWidth;
g.drawLine(x, y + this.lineWidth, x + lw, y + this.lineWidth);
}
if (this.decorations.underline) {
g.lineWidth = this.lineWidth;
g.drawLine(x, y + lh - 1, x + lw, y + lh - 1);
}
if (this.decorations.strike) {
var yy = y + Math.round(lh / 2) - 1;
g.lineWidth = this.lineWidth;
g.drawLine(x, yy, x + lw, yy);
}
// restore text color
if (this.lineColor !== null) {
g.setColor(this.color);
}
}
]);
pkg.BoldTextRender = Class(pkg.TextRender, [
function $clazz() {
this.font = pkg.boldFont;
}
]);
/**
* Password text render class. This class renders a secret text with hiding
* it with the given character.
* @param {String|zebkit.data.TextModel} [text] a text as string or text
* model instance
* @class zebkit.draw.PasswordText
* @constructor
* @extends zebkit.draw.TextRender
*/
pkg.PasswordText = Class(pkg.TextRender, [
function(text){
if (arguments.length === 0) {
text = new zebkit.data.SingleLineTxt("");
}
this.$super(text);
},
function $prototype() {
/**
* Echo character that will replace characters of hidden text
* @attribute echo
* @type {String}
* @readOnly
* @default "*"
*/
this.echo = "*";
/**
* Indicates if the last entered character doesn't have to be replaced
* with echo character
* @type {Boolean}
* @attribute showLast
* @default true
* @readOnly
*/
this.showLast = true;
/**
* Set the specified echo character. The echo character is used to
* hide secret text.
* @param {String} ch an echo character
* @method setEchoChar
* @chainable
*/
this.setEchoChar = function(ch){
if (this.echo !== ch){
this.echo = ch;
if (this.target !== null) {
this.invalidate(0, this.target.getLines());
}
}
return this;
};
},
function getLine(r) {
var buf = [],
ln = this.$super(r);
for(var i = 0;i < ln.length; i++) {
buf[i] = this.echo;
}
if (this.showLast && ln.length > 0) {
buf[ln.length - 1] = ln[ln.length - 1];
}
return buf.join('');
}
]);
/**
* Triangle shape view.
* @param {String} [c] a color of the shape
* @param {Integer} [w] a line size
* @class zebkit.draw.TriangleShape
* @constructor
* @extends zebkit.draw.Shape
*/
pkg.TriangleShape = Class(pkg.Shape, [
function $prototype() {
this.outline = function(g,x,y,w,h,d) {
g.beginPath();
w -= 2 * this.lineWidth;
h -= 2 * this.lineWidth;
g.moveTo(x + w - 1, y);
g.lineTo(x + w - 1, y + h - 1);
g.lineTo(x, y + h - 1);
g.closePath();
return true;
};
}
]);
/**
* Vertical or horizontal linear gradient view
* @param {String} startColor start color
* @param {String} endColor end color
* @param {String} [type] type of gradient
* "vertical" or "horizontal"
* @constructor
* @class zebkit.draw.Gradient
* @extends zebkit.draw.View
*/
pkg.Gradient = Class(pkg.View, [
function() {
/**
* Gradient start and stop colors
* @attribute colors
* @readOnly
* @type {Array}
*/
this.colors = Array.prototype.slice.call(arguments, 0);
if (arguments.length > 2) {
this.orient = arguments[arguments.length - 1];
this.colors.pop();
}
},
function $prototype() {
/**
* Gradient orientation: vertical or horizontal
* @attribute orient
* @readOnly
* @default "vertical"
* @type {String}
*/
this.orient = "vertical";
this.$gradient = null;
this.$gy2 = this.$gy1 = this.$gx2 = this.$gx1 = 0;
this.paint = function(g,x,y,w,h,dd){
var d = (this.orient === "horizontal" ? [0,1]: [1,0]),
x1 = x * d[1],
y1 = y * d[0],
x2 = (x + w - 1) * d[1],
y2 = (y + h - 1) * d[0];
if (this.$gradient === null || this.$gx1 !== x1 ||
this.$gx2 !== x2 || this.$gy1 !== y1 ||
this.$gy2 !== y2 )
{
this.$gx1 = x1;
this.$gx2 = x2;
this.$gy1 = y1;
this.$gy2 = y2;
this.$gradient = g.createLinearGradient(x1, y1, x2, y2);
for(var i = 0; i < this.colors.length; i++) {
this.$gradient.addColorStop(i, this.colors[i].toString());
}
}
g.fillStyle = this.$gradient;
g.fillRect(x, y, w, h);
};
}
]);
/**
* Radial gradient view
* @param {String} startColor a start color
* @param {String} stopColor a stop color
* @constructor
* @class zebkit.draw.Radial
* @extends zebkit.draw.View
*/
pkg.Radial = Class(pkg.View, [
function() {
this.colors = [];
for(var i = 0; i < arguments.length; i++) {
this.colors[i] = arguments[i] !== null ? arguments[i].toString() : null;
}
this.colors = Array.prototype.slice.call(arguments, 0);
},
function $prototype() {
this.$gradient = null;
this.$cx1 = this.$cy1 = this.$rad1 = this.$rad2 = 0;
this.$colors = [];
this.radius = 10;
this.paint = function(g,x,y,w,h,d){
var cx1 = Math.floor(w / 2),
cy1 = Math.floor(h / 2),
rad2 = w > h ? w : h;
if (this.$gradient === null ||
this.$cx1 !== cx1 ||
this.$cy1 !== cy1 ||
this.$rad1 !== this.radius ||
this.$rad2 !== this.rad2 )
{
this.$gradient = g.createRadialGradient(cx1, cy1, this.radius, cx1, cy1, rad2);
}
var b = false,
i = 0;
if (this.$colors.length !== this.colors.length) {
b = true;
} else {
for (i = 0; i < this.$colors.length; i++) {
if (this.$colors[i] !== this.colors[i]) {
b = true;
break;
}
}
}
if (b) {
for (i = 0; i < this.colors.length; i++) {
this.$gradient.addColorStop(i, this.colors[i]);
}
}
g.fillStyle = this.$gradient;
g.fillRect(x, y, w, h);
};
}
]);
/**
* Image render. Render an image target object or specified area of
* the given target image object.
* @param {Image} img the image to be rendered
* @param {Integer} [x] a x coordinate of the rendered image part
* @param {Integer} [y] a y coordinate of the rendered image part
* @param {Integer} [w] a width of the rendered image part
* @param {Integer} [h] a height of the rendered image part
* @constructor
* @class zebkit.draw.Picture
* @extends zebkit.draw.Render
*/
pkg.Picture = Class(pkg.Render, [
function(img, x, y, w, h) {
this.setValue(img);
if (arguments.length === 2) {
this.height = this.width = x;
} else if (arguments.length === 3) {
this.width = x;
this.height = y;
} else if (arguments.length > 3) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
}
},
function $prototype() {
/**
* A x coordinate of the image part that has to be rendered
* @attribute x
* @readOnly
* @type {Integer}
* @default -1
*/
this.x = -1;
/**
* A y coordinate of the image part that has to be rendered
* @attribute y
* @readOnly
* @type {Integer}
* @default -1
*/
this.y = -1;
/**
* A width of the image part that has to be rendered
* @attribute width
* @readOnly
* @type {Integer}
* @default -1
*/
this.width = -1;
/**
* A height of the image part that has to be rendered
* @attribute height
* @readOnly
* @type {Integer}
* @default -1
*/
this.height = -1;
this.paint = function(g,x,y,w,h,d) {
if (this.target !== null &&
this.target.complete === true &&
this.target.naturalWidth > 0 &&
w > 0 && h > 0)
{
if (this.x >= 0) {
g.drawImage(this.target, this.x, this.y,
this.width, this.height, x, y, w, h);
} else {
g.drawImage(this.target, x, y, w, h);
}
}
};
this.getPreferredSize = function() {
var img = this.target;
return (img === null ||
img.naturalWidth <= 0 ||
img.complete !== true) ? { width:0, height:0 }
: (this.width > 0) ? { width:this.width, height:this.height }
: { width:img.width, height:img.height };
};
}
]);
/**
* Pattern render.
* @class zebkit.draw.Pattern
* @param {Image} [img] an image to be used as the pattern
* @constructor
* @extends zebkit.draw.Render
*/
pkg.Pattern = Class(pkg.Render, [
function $prototype() {
/**
* Buffered pattern
* @type {Pattern}
* @protected
* @attribute $pattern
* @readOnly
*/
this.$pattern = null;
this.paint = function(g,x,y,w,h,d) {
if (this.$pattern === null && this.target !== null) {
this.$pattern = g.createPattern(this.target, 'repeat');
}
g.beginPath();
g.rect(x, y, w, h);
g.closePath();
g.fillStyle = this.$pattern;
g.fill();
};
this.valueWasChanged = function(o, n) {
this.$pattern = null;
};
}
]);
/**
* Line view.
* @class zebkit.draw.Line
* @extends zebkit.draw.View
* @constructor
* @param {String} [side] a side of rectangular area where the line has to be rendered. Use
* "left", "top", "right" or "bottom" as the parameter value
* @param {String} [color] a line color
* @param {Integer} [width] a line width
*/
pkg.LineView = Class(pkg.View, [
function(side, color, lineWidth) {
if (arguments.length > 0) {
this.side = zebkit.util.validateValue(side, "top", "right", "bottom", "left");
if (arguments.length > 1) {
this.color = color;
if (arguments.length > 2) {
this.lineWidth = lineWidth;
}
}
}
},
function $prototype() {
/**
* Side the line has to be rendered
* @attribute side
* @type {String}
* @default "top"
* @readOnly
*/
this.side = "top";
/**
* Line color
* @attribute color
* @type {String}
* @default "black"
* @readOnly
*/
this.color = "black";
/**
* Line width
* @attribute lineWidth
* @type {Integer}
* @default 1
* @readOnly
*/
this.lineWidth = 1;
this.paint = function(g,x,y,w,h,t) {
g.setColor(this.color);
g.beginPath();
g.lineWidth = this.lineWidth;
var d = this.lineWidth / 2;
if (this.side === "top") {
g.moveTo(x, y + d);
g.lineTo(x + w - 1, y + d);
} else if (this.side === "bottom") {
g.moveTo(x, y + h - d);
g.lineTo(x + w - 1, y + h - d);
} else if (this.side === "left") {
g.moveTo(x + d, y);
g.lineTo(x + d, y + h - 1);
} else if (this.side === "right") {
g.moveTo(x + w - d, y);
g.lineTo(x + w - d, y + h - 1);
} else if (this.side === "verCenter") {
// TODO: not implemented
} else if (this.side === "horCenter") {
// TODO: not implemented
}
g.stroke();
};
this.getPreferredSize = function() {
return {
width : this.lineWidth,
height : this.lineWidth
};
};
}
]);
/**
* Arrow view. Tye view can be use to render triangle arrow element to one of the
* following direction: "top", "left", "bottom", "right".
* @param {String} direction an arrow view direction.
* @param {String} color an arrow view line color.
* @param {String} fillColor an arrow view filling.
* @constructor
* @class zebkit.draw.ArrowView
* @extends zebkit.draw.Shape
*/
pkg.ArrowView = Class(pkg.Shape, [
function (direction, color, fillColor) {
if (arguments.length > 0) {
this.direction = zebkit.util.validateValue(direction, "left", "right", "bottom", "top");
if (arguments.length > 1) {
this.color = color;
if (arguments.length > 2) {
this.fillColor = fillColor;
}
}
}
},
function $prototype() {
this.gap = 0;
this.color = null;
this.fillColor = "black";
/**
* Arrow direction.
* @attribute direction
* @type {String}
* @default "bottom"
*/
this.direction = "bottom";
this.outline = function(g, x, y, w, h, d) {
x += this.gap;
y += this.gap;
w -= this.gap * 2;
h -= this.gap * 2;
var dt = this.lineWidth / 2,
w2 = Math.round(w / 2) - (w % 2 === 0 ? 0 : dt),
h2 = Math.round(h / 2) - (h % 2 === 0 ? 0 : dt);
g.beginPath();
if ("bottom" === this.direction) {
g.moveTo(x, y + dt);
g.lineTo(x + w - 1, y + dt);
g.lineTo(x + w2, y + h - dt);
g.lineTo(x + dt, y + dt);
} else if ("top" === this.direction) {
g.moveTo(x, y + h - dt);
g.lineTo(x + w - 1, y + h - dt);
g.lineTo(x + w2, y);
g.lineTo(x + dt, y + h - dt);
} else if ("left" === this.direction) {
g.moveTo(x + w - dt, y);
g.lineTo(x + w - dt, y + h - 1);
g.lineTo(x, y + h2);
g.lineTo(x + w + dt, y);
} else if ("right" === this.direction) {
g.moveTo(x + dt, y);
g.lineTo(x + dt, y + h - 1);
g.lineTo(x + w, y + h2);
g.lineTo(x - dt, y);
}
return true;
};
/**
* Set gap.
* @param {Integer} gap a gap
* @chainable
* @method setGap
*/
this.setGap = function(gap) {
this.gap = gap;
return this;
};
}
]);
pkg.TabBorder = Class(pkg.View, [
function(t, w) {
if (arguments.length > 1) {
this.width = w;
}
if (arguments.length > 0) {
this.state = t;
}
this.left = this.top = this.bottom = this.right = 6 + this.width;
},
function $prototype() {
this.state = "out";
this.width = 1;
this.fillColor1 = "#DCF0F7";
this.fillColor2 = "white";
this.fillColor3 = "#F3F3F3";
this.onColor1 = "black";
this.onColor2 = "#D9D9D9";
this.offColor = "#A1A1A1";
this.paint = function(g,x,y,w,h,d){
var xx = x + w - 1,
yy = y + h - 1,
o = d.parent.orient,
t = this.state,
s = this.width,
ww = 0,
hh = 0,
dt = s / 2;
g.beginPath();
g.lineWidth = s;
switch(o) {
case "left":
g.moveTo(xx + 1, y + dt);
g.lineTo(x + s * 2, y + dt);
g.lineTo(x + dt , y + s * 2);
g.lineTo(x + dt, yy - s * 2 + dt);
g.lineTo(x + s * 2, yy + dt);
g.lineTo(xx + 1, yy + dt);
if (d.isEnabled === true){
g.setColor(t === "over" ? this.fillColor1 : this.fillColor2);
g.fill();
}
g.setColor((t === "selected" || t === "over") ? this.onColor1 : this.offColor);
g.stroke();
if (d.isEnabled === true) {
ww = Math.floor((w - 6) / 2);
g.setColor(this.fillColor3);
g.fillRect(xx - ww + 1, y + s, ww, h - s - 1);
}
if (t === "out") {
g.setColor(this.onColor2);
g.drawLine(x + 2*s + 1, yy - s, xx + 1, yy - s, s);
}
break;
case "right":
xx -= dt; // thick line grows left side and right side proportionally
// correct it
g.moveTo(x, y + dt);
g.lineTo(xx - 2 * s, y + dt);
g.lineTo(xx, y + 2 * s);
g.lineTo(xx, yy - 2 * s);
g.lineTo(xx - 2 * s, yy + dt);
g.lineTo(x, yy + dt);
if (d.isEnabled === true){
g.setColor(t === "over" ? this.fillColor1 : this.fillColor2);
g.fill();
}
g.setColor((t === "selected" || t === "over") ? this.onColor1 : this.offColor);
g.stroke();
if (d.isEnabled === true) {
ww = Math.floor((w - 6) / 2);
g.setColor(this.fillColor3);
g.fillRect(x, y + s, ww, h - s - 1);
}
if (t === "out") {
g.setColor(this.onColor2);
g.drawLine(x, yy - s, xx - s - 1, yy - s, s);
}
break;
case "top":
g.moveTo(x + dt, yy + 1 );
g.lineTo(x + dt, y + s*2);
g.lineTo(x + s * 2, y + dt);
g.lineTo(xx - s * 2 + s, y + dt);
g.lineTo(xx + dt, y + s * 2);
g.lineTo(xx + dt, yy + 1);
if (d.isEnabled === true){
g.setColor(t === "over" ? this.fillColor1 : this.fillColor2);
g.fill();
}
g.setColor((t === "selected" || t === "over") ? this.onColor1 : this.offColor);
g.stroke();
if (d.isEnabled === true){
g.setColor(this.fillColor3);
hh = Math.floor((h - 6) / 2);
g.fillRect(x + s, yy - hh + 1 , w - s - 1, hh);
}
if (t === "selected") {
g.setColor(this.onColor2);
g.beginPath();
g.moveTo(xx + dt - s, yy + 1);
g.lineTo(xx + dt - s, y + s * 2);
g.stroke();
}
break;
case "bottom":
yy -= dt;
g.moveTo(x + dt, y);
g.lineTo(x + dt, yy - 2 * s);
g.lineTo(x + 2 * s + dt, yy);
g.lineTo(xx - 2 * s, yy);
g.lineTo(xx + dt, yy - 2 * s);
g.lineTo(xx + dt, y);
if (d.isEnabled === true){
g.setColor(t === "over" ? this.fillColor1 : this.fillColor2);
g.fill();
}
g.setColor((t === "selected" || t === "over") ? this.onColor1 : this.offColor);
g.stroke();
if (d.isEnabled === true){
g.setColor(this.fillColor3);
hh = Math.floor((h - 6) / 2);
g.fillRect(x + s, y, w - s - 1, hh);
}
if (t === "selected") {
g.setColor(this.onColor2);
g.beginPath();
g.moveTo(xx + dt - s, y);
g.lineTo(xx + dt - s, yy - s - 1);
g.stroke();
}
break;
default: throw new Error("Invalid tab alignment");
}
};
this.getTop = function () { return this.top; };
this.getBottom = function () { return this.bottom;};
this.getLeft = function () { return this.left; };
this.getRight = function () { return this.right; };
}
]);
/**
* The check box ticker view.
* @class zebkit.draw.CheckboxView
* @extends zebkit.draw.View
* @constructor
* @param {String} [color] color of the ticker
*/
pkg.CheckboxView = Class(pkg.View, [
function(color) {
if (arguments.length > 0) {
this.color = color;
}
},
function $prototype() {
/**
* Ticker color.
* @attribute color
* @type {String}
* @readOnly
* @default "rgb(65, 131, 255)"
*/
this.color = "rgb(65, 131, 255)";
this.paint = function(g,x,y,w,h,d){
g.beginPath();
g.strokeStyle = this.color;
g.lineWidth = 2;
g.moveTo(x + 1, y + 2);
g.lineTo(x + w - 3, y + h - 3);
g.stroke();
g.beginPath();
g.moveTo(x + w - 2, y + 2);
g.lineTo(x + 2, y + h - 2);
g.stroke();
g.lineWidth = 1;
};
}
]);
/**
* Thumb element view.
* @class zebkit.draw.ThumbView
* @extends zebkit.draw.Shape
* @constructor
* @param {String} [dir] a direction
* @param {String} [color] a shape line color
* @param {String} [fillColor] a fill color
* @param {Integer} [lineWidth] a shape line width
*/
pkg.ThumbView = Class(pkg.Shape, [
function(dir, color, fillColor, lineWidth) {
if (arguments.length > 0) {
this.direction = zebkit.util.validateValue(dir, "vertical", "horizontal");
if (arguments.length > 1) {
this.color = color;
if (arguments.length > 2) {
this.fillColor = fillColor;
if (arguments.length > 3) {
this.lineWidth = lineWidth;
}
}
}
}
},
function $prototype() {
this.fillColor = "#AAAAAA";
this.color = null;
/**
* Direction.
* @attribute direction
* @type {String}
* @default "vertical"
*/
this.direction = "vertical";
this.outline = function(g, x, y, w, h, d) {
g.beginPath();
var r = 0;
if (this.direction === "vertical") {
r = w / 2;
g.arc(x + r, y + r, r, Math.PI, 0, false);
g.lineTo(x + w, y + h - r);
g.arc(x + r, y + h - r, r, 0, Math.PI, false);
g.lineTo(x, y + r);
} else {
r = h / 2;
g.arc(x + r, y + r, r, 0.5 * Math.PI, 1.5 * Math.PI, false);
g.lineTo(x + w - r, y);
g.arc(x + w - r, y + h - r, r, 1.5 * Math.PI, 0.5 * Math.PI, false);
g.lineTo(x + r, y + h);
}
};
}
]);
/**
* The radio button ticker view.
* @class zebkit.draw.RadioView
* @extends zebkit.draw.View
* @constructor
* @param {String} [outerColor] color one to fill the outer circle
* @param {String} [innerColor] color tow to fill the inner circle
*/
pkg.RadioView = Class(pkg.View, [
function(outerColor, innerColor) {
if (arguments.length > 0) {
this.outerColor = outerColor;
if (arguments.length > 1) {
this.innerColor = innerColor;
}
}
},
function $prototype() {
/**
* Outer circle filling color.
* @attribute outerColor
* @readOnly
* @default "rgb(15, 81, 205)"
* @type {String}
*/
this.outerColor = "rgb(15, 81, 205)";
/**
* Inner circle filling color.
* @attribute innerColor
* @readOnly
* @default "rgb(65, 131, 255)"
* @type {String}
*/
this.innerColor = "rgb(65, 131, 255)";
this.paint = function(g,x,y,w,h,d){
g.beginPath();
if (g.fillStyle !== this.outerColor) {
g.fillStyle = this.outerColor;
}
g.arc(Math.floor(x + w/2), Math.floor(y + h/2) , Math.floor(w/3 - 0.5), 0, 2 * Math.PI, 1, false);
g.fill();
g.beginPath();
if (g.fillStyle !== this.innerColor) {
g.fillStyle = this.innerColor;
}
g.arc(Math.floor(x + w/2), Math.floor(y + h/2) , Math.floor(w/4 - 0.5), 0, 2 * Math.PI, 1, false);
g.fill();
};
}
]);
/**
* Toggle view element class
* @class zebkit.draw.ToggleView
* @extends zebkit.draw.View
* @constructor
* @param {Boolean} [plus] indicates the sign type plus (true) or minus (false)
* @param {String} [color] a color
* @param {String} [bg] a background
* @param {Integer} [w] a width
* @param {Integer} [h] a height
* @param {zebkit.draw.View | String} [br] a border view
*/
pkg.ToggleView = Class(pkg.View, [
function(plus, color, bg, w, h, br) {
if (arguments.length > 0) {
this.plus = plus;
if (arguments.length > 1) {
this.color = color;
if (arguments.length > 2) {
this.bg = bg;
if (arguments.length > 3) {
this.width = this.height = w;
if (arguments.length > 4) {
this.height = h;
if (arguments.length > 5) {
this.br = pkg.$view(br);
}
}
}
}
}
}
},
function $prototype() {
this.color = "white";
this.bg = "lightGray";
this.plus = false;
this.br = new pkg.Border("rgb(65, 131, 215)", 1, 3);
this.width = this.height = 12;
this.paint = function(g, x, y, w, h, d) {
if (this.bg !== null && (this.br === null || this.br.outline(g, x, y, w, h, d) === false)) {
g.beginPath();
g.rect(x, y, w, h);
}
if (this.bg !== null) {
g.setColor(this.bg);
g.fill();
}
if (this.br !== null) {
this.br.paint(g, x, y, w, h, d);
}
g.setColor(this.color);
g.lineWidth = 2;
x += 2;
w -= 4;
h -= 4;
y += 2;
g.beginPath();
g.moveTo(x, y + h / 2);
g.lineTo(x + w, y + h / 2);
if (this.plus) {
g.moveTo(x + w / 2, y);
g.lineTo(x + w / 2, y + h);
}
g.stroke();
g.lineWidth = 1;
};
this.getPreferredSize = function() {
return { width:this.width, height:this.height };
};
}
]);
pkg.CaptionBgView = Class(pkg.View, [
function(bg, gap, radius) {
if (arguments.length > 0) {
this.bg = bg;
if (arguments.length > 1) {
this.gap = gap;
if (arguments.length > 2) {
this.radius = radius;
}
}
}
},
function $prototype() {
this.gap = 6;
this.radius = 6;
this.bg = "#66CCFF";
this.paint = function(g,x,y,w,h,d) {
this.outline(g,x,y,w,h,d);
g.setColor(this.bg);
g.fill();
};
this.outline = function(g,x,y,w,h,d) {
g.beginPath();
g.moveTo(x + this.radius, y);
g.lineTo(x + w - this.radius*2, y);
g.quadraticCurveTo(x + w, y, x + w, y + this.radius);
g.lineTo(x + w, y + h);
g.lineTo(x, y + h);
g.lineTo(x, y + this.radius);
g.quadraticCurveTo(x, y, x + this.radius, y);
return true;
};
}
]);
// TODO: not sure it makes sense to put here
// a bit dirty implementation
pkg.CloudView = Class(pkg.Shape, [
function outline(g,x,y,w,h,d) {
g.beginPath();
g.moveTo(x + w * 0.2, y + h * 0.25);
g.bezierCurveTo(x, y + h*0.25, x, y + h*0.75, x + w * 0.2, y + h*0.75);
g.bezierCurveTo(x + 0.1 * w, y + h - 1 , x + 0.8*w, y + h - 1, x + w * 0.7, y + h*0.75);
g.bezierCurveTo(x + w - 1, y + h * 0.75 , x + w - 1, y, x + w * 0.65, y + h*0.25) ;
g.bezierCurveTo(x + w - 1, y, x + w * 0.1, y, x + w * 0.2, y + h * 0.25) ;
g.closePath();
return true;
}
]);
/**
* Function render. The render draws a chart for the specified function
* within the given interval.
* @constructor
* @class zebkit.draw.FunctionRender
* @extends zebkit.draw.Render
* @param {Function} fn a function to be rendered
* @param {Number} x1 a minimal value of rendered function interval
* @param {Number} x2 a maximal value of rendered function interval
* @param {Integer} [granularity] a granularity
* @param {String} [color] a chart color
* @example
*
* zebkit.require("ui", "draw", function(ui, draw) {
* var root = new ui.zCanvas(400, 300).root;
* // paint sin(x) function as a background of root component
* root.setBackground(new draw.FunctionRender(function(x) {
* return Math.sin(x);
* }, -3, 3));
* });
*
*/
pkg.FunctionRender = Class(pkg.Render, [
function(fn, x1, x2, granularity) {
this.$super(fn);
this.setRange(x1, x2);
if (arguments.length > 3) {
this.granularity = arguments[3];
if (arguments.length > 4) {
this.color = arguments[4];
}
}
},
function $prototype(g,x,y,w,h,c) {
/**
* Granularity defines how many points the given interval have to be split.
* @type {Integer}
* @attribute granularity
* @readOnly
* @default 200
*/
this.granularity = 200;
/**
* Function chart color.
* @type {String}
* @attribute color
* @default "orange"
*/
this.color = "orange";
/**
* Chart line width.
* @type {Integer}
* @attribute lineWidth
* @default 1
*/
this.lineWidth = 1;
/**
* Indicates if the function chart has to be stretched vertically
* to occupy the whole vertical space
* @type {Boolean}
* @attribute stretch
* @default true
*/
this.stretch = true;
this.$fy = null;
this.valueWasChanged = function(o, n) {
if (n !== null && typeof n !== 'function') {
throw new Error("Function is expected");
}
this.$fy = null;
};
/**
* Set the given granularity. Granularity defines how smooth
* the rendered chart should be.
* @param {Integer} g a granularity
* @method setGranularity
*/
this.setGranularity = function(g) {
if (g !== this.granularity) {
this.granularity = g;
this.$fy = null;
}
};
/**
* Set the specified interval - minimal and maximal function
* argument values.
* @param {Number} x1 a minimal value
* @param {Number} x2 a maximal value
* @method setRange
*/
this.setRange = function(x1, x2) {
if (x1 > x2) {
throw new RangeError("Incorrect range interval");
}
if (this.x1 !== x1 || this.x2 !== x2) {
this.x1 = x1;
this.x2 = x2;
this.$fy = null;
}
};
this.$recalc = function() {
if (this.$fy === null) {
this.$fy = [];
this.$maxy = -100000000;
this.$miny = 100000000;
this.$dx = (this.x2 - this.x1) / this.granularity;
for(var x = this.x1, i = 0; x <= this.x2; i++) {
this.$fy[i] = this.target(x);
if (this.$fy[i] > this.$maxy) {
this.$maxy = this.$fy[i];
}
if (this.$fy[i] < this.$miny) {
this.$miny = this.$fy[i];
}
x += this.$dx;
}
}
};
this.paint = function(g,x,y,w,h,c) {
if (this.target !== null) {
this.$recalc();
var cx = (w - this.lineWidth * 2) / (this.x2 - this.x1), // value to pixel scale
cy = cx,
vx = this.$dx,
sy = this.lineWidth,
dy = h - this.lineWidth * 2;
if (this.stretch) {
cy = (h - this.lineWidth * 2) / (this.$maxy - this.$miny);
} else {
dy = (this.$maxy - this.$miny) * cx;
sy = (h - dy) / 2;
}
g.beginPath();
g.setColor(this.color);
g.lineWidth = this.lineWidth;
g.moveTo(this.lineWidth,
sy + dy - (this.$fy[0] - this.$miny) * cy);
for(var i = 1; i < this.$fy.length; i++) {
g.lineTo(this.lineWidth + vx * cx,
sy + dy - (this.$fy[i] - this.$miny) * cy);
vx += this.$dx;
}
g.stroke();
}
};
}
]);
/**
* Pentahedron shape view.
* @param {String} [c] a color of the shape
* @param {Integer} [w] a line size
* @class zebkit.draw.PentahedronShape
* @constructor
* @extends zebkit.draw.Shape
*/
pkg.PentahedronShape = Class(pkg.Shape, [
function outline(g,x,y,w,h,d) {
g.beginPath();
x += this.lineWidth;
y += this.lineWidth;
w -= 2*this.lineWidth;
h -= 2*this.lineWidth;
g.moveTo(x + w/2, y);
g.lineTo(x + w - 1, y + h/3);
g.lineTo(x + w - 1 - w/3, y + h - 1);
g.lineTo(x + w/3, y + h - 1);
g.lineTo(x, y + h/3);
g.lineTo(x + w/2, y);
return true;
}
]);
/**
* Base class to implement model values renders.
* @param {zebkit.draw.Render} [render] a render to visualize values.
* By default string render is used.
* @class zebkit.draw.BaseViewProvider
* @constructor
*/
pkg.BaseViewProvider = Class([
function(render) {
/**
* Default render that is used to paint grid content.
* @type {zebkit.draw.Render}
* @attribute render
* @readOnly
* @protected
*/
this.render = (arguments.length === 0 || render === undefined ? new zebkit.draw.StringRender("")
: render);
zebkit.properties(this, this.clazz);
},
function $prototype() {
/**
* Set the default view provider font if defined render supports it
* @param {zebkit.Font} f a font
* @method setFont
* @chainable
*/
this.setFont = function(f) {
if (this.render.setFont !== undefined) {
this.render.setFont(f);
}
return this;
};
/**
* Set the default view provider color if defined render supports it
* @param {String} c a color
* @method setColor
* @chainable
*/
this.setColor = function(c) {
if (this.render.setColor !== undefined) {
this.render.setColor(c);
}
return this;
};
/**
* Get a view to render the specified value of the target component.
* @param {Object} target a target component
* @param {Object} [arg]* arguments list
* @param {Object} obj a value to be rendered
* @return {zebkit.draw.View} an instance of view to be used to
* render the given value
* @method getView
*/
this.getView = function(target) {
var obj = arguments[arguments.length - 1];
if (obj !== null && obj !== undefined) {
if (obj.toView !== undefined) {
return obj.toView();
} else if (obj.paint !== undefined) {
return obj;
} else {
this.render.setValue(obj.toString());
return this.render;
}
} else {
return null;
}
};
}
]);
},false);
zebkit.package("ui.event", function(pkg, Class) {
'use strict';
/**
* UI event and event manager package.
* @class zebkit.ui.event
* @access package
*/
/**
* UI manager class. The class is widely used as a basement for building various UI managers
* like focus, event managers etc. Manager is automatically registered as global events
* listener for events it implements to handle.
* @class zebkit.ui.event.Manager
* @constructor
*/
pkg.Manager = Class([
function() {
//TODO: correct to event package
if (zebkit.ui.events !== null && zebkit.ui.events !== undefined) {
zebkit.ui.events.on(this);
}
}
]);
/**
* Component event class. Component events are fired when:
*
* - a component is re-located ("compMoved" event)
* - a component is re-sized ("compResized" event)
* - a component visibility is updated ("compShown" event)
* - a component is enabled ("compEnabled" event)
* - a component has been inserted into another component ("compAdded" event)
* - a component has been removed from another component ("compRemoved" event)
*
* Appropriate event type is set in the event id property.
* @constructor
* @class zebkit.ui.event.CompEvent
* @extends zebkit.Event
*/
pkg.CompEvent = Class(zebkit.Event, [
function $prototype() {
/**
* A kid component that has been added or removed (depending on event type).
* @attribute kid
* @readOnly
* @default null
* @type {zebkit.ui.Panel}
*/
this.kid = this.constraints = null;
/**
* A constraints with that a kid component has been added or removed (depending on event type).
* @attribute constraints
* @readOnly
* @default null
* @type {Object}
*/
/**
* A previous x location the component has had.
* @readOnly
* @attribute prevX
* @type {Integer}
* @default -1
*/
/**
* A previous y location the component has had.
* @readOnly
* @attribute prevY
* @type {Integer}
* @default -1
*/
/**
* An index at which a component has been added or removed.
* @readOnly
* @attribute index
* @type {Integer}
* @default -1
*/
/**
* A previous width the component has had.
* @readOnly
* @attribute prevWidth
* @type {Integer}
* @default -1
*/
/**
* A previous height the component has had.
* @readOnly
* @attribute height
* @type {Integer}
* @default -1
*/
this.prevX = this.prevY = this.index = -1;
this.prevWidth = this.prevHeight = -1;
}
]);
/**
* Input key event class.
* @class zebkit.ui.event.KeyEvent
* @extends zebkit.Event
* @constructor
*/
pkg.KeyEvent = Class(zebkit.Event, [
function $prototype() {
/**
* A code of a pressed key
* @attribute code
* @readOnly
* @type {Strung}
*/
this.code = null;
/**
* A pressed key
* @attribute key
* @readOnly
* @type {String}
*/
this.key = null;
/**
* Input device type. Can be for instance "keyboard", vkeyboard" (virtual keyboard)
* @attribute device
* @default "keyboard"
* @type {String}
*/
this.device = "keyboard";
/**
* Boolean that shows state of ALT key.
* @attribute altKey
* @type {Boolean}
* @readOnly
*/
this.altKey = false;
/**
* Boolean that shows state of SHIFT key.
* @attribute shiftKey
* @type {Boolean}
* @readOnly
*/
this.shiftKey = false;
/**
* Boolean that shows state of CTRL key.
* @attribute ctrlKey
* @type {Boolean}
* @readOnly
*/
this.ctrlKey = false;
/**
* Boolean that shows state of META key.
* @attribute metaKey
* @type {Boolean}
* @readOnly
*/
this.metaKey = false;
/**
* Repeat counter
* @attribute repeat
* @type {Number}
*/
this.repeat = 0;
/**
* Time stamp
* @attribute timeStamp
* @type {Number}
*/
this.timeStamp = 0;
/**
* Get the given modifier key state. The following modifier key codes are supported:
* "Meta", "Control", "Shift", "Alt".
* @param {String} m a modifier key code
* @return {Boolean} true if the modifier key state is pressed.
* @method getModifierState
*/
this.getModifierState = function(m) {
if (m === "Meta") {
return this.metaKey;
}
if (m === "Control") {
return this.ctrlKey;
}
if (m === "Shift") {
return this.shiftKey;
}
if (m === "Alt") {
return this.altKey;
}
throw new Error("Unknown modifier key '" + m + "'");
};
}
]);
/**
* Mouse and touch screen input event class. The input event is triggered by a mouse or
* touch screen.
* @class zebkit.ui.event.PointerEvent
* @extends zebkit.Event
* @constructor
*/
pkg.PointerEvent = Class(zebkit.Event, [
function $prototype() {
/**
* Pointer type. Can be "mouse", "touch", "pen"
* @attribute pointerType
* @type {String}
*/
this.pointerType = "mouse";
/**
* Touch counter
* @attribute touchCounter
* @type {Integer}
* @default 0
*/
this.touchCounter = 0;
/**
* Page x
* @attribute pageX
* @type {Integer}
* @default -1
*/
this.pageX = -1;
/**
* Page y
* @attribute pageY
* @type {Integer}
* @default -1
*/
this.pageY = -1;
/**
* Target DOM element
* @attribute target
* @type {DOMElement}
* @default null
*/
this.target = null;
/**
* Pointer identifier.
* @attribute identifier
* @type {Object}
* @default null
*/
this.identifier = null;
this.shiftKey = this.altKey = this.metaKey = this.ctrlKey = false;
this.pressure = 0.5;
/**
* Absolute mouse pointer x coordinate
* @attribute absX
* @readOnly
* @type {Integer}
*/
this.absX = 0;
/**
* Absolute mouse pointer y coordinate
* @attribute absY
* @readOnly
* @type {Integer}
*/
this.absY = 0;
/**
* Mouse pointer x coordinate (relatively to source UI component)
* @attribute x
* @readOnly
* @type {Integer}
*/
this.x = 0;
/**
* Mouse pointer y coordinate (relatively to source UI component)
* @attribute y
* @readOnly
* @type {Integer}
*/
this.y = 0;
/**
* Recompute the event relative location for the new source component and it
* absolute location
* @private
* @param {zebkit.ui.Panel} source a source component that triggers the event
* @param {Integer} ax an absolute (relatively to a canvas where the source
* component is hosted) x mouse cursor coordinate
* @param {Integer} ay an absolute (relatively to a canvas where the source
* component is hosted) y mouse cursor coordinate
* @method updateCoordinates
*/
this.update = function(source, ax, ay){
// this can speed up calculation significantly check if source zebkit component
// has not been changed, his location and parent component also has not been
// changed than we can skip calculation of absolute location by traversing
// parent hierarchy
if (this.source === source &&
this.source.parent === source.parent &&
source.x === this.$px &&
source.y === this.$py )
{
this.x += (ax - this.absX);
this.y += (ay - this.absY);
this.absX = ax;
this.absY = ay;
this.source = source;
} else {
this.source = source;
this.absX = ax;
this.absY = ay;
// convert absolute location to relative location
while (source.parent !== null) {
ax -= source.x;
ay -= source.y;
source = source.parent;
}
this.x = ax;
this.y = ay;
}
this.$px = source.x;
this.$py = source.y;
return this;
};
this.setLocation = function(x, y) {
if (this.source === null) {
throw new Error("Unknown source component");
}
if (this.x !== x || this.y !== y) {
this.absX = this.x = x;
this.absY = this.y = y;
// convert relative coordinates to absolute
var source = this.source;
while (source.parent !== null) {
this.absX += source.x;
this.absY += source.y;
source = source.parent;
}
}
};
this.isAction = function() {
// TODO: actually this is abstract method
throw new Error("Not implemented");
};
this.getTouches = function() {
// TODO: actually this is abstract method
throw new Error("Not implemented");
};
}
]);
/**
* Event manager class. One of the key zebkit manager that is responsible for distributing various
* events in zebkit UI. The manager provides possibility to catch and handle UI events globally. Below
* is list event types that can be caught with the event manager:
*
* - Key events:
* - "keyTyped"
* - "keyReleased"
* - "keyPressed"
*
* - Pointer events:
* - "pointerDragged"
* - "pointerDragStarted"
* - "pointerDragEnded"
* - "pointerMoved"
* - "pointerClicked"
* - "pointerDoubleClicked"
* - "pointerPressed"
* - "pointerReleased"
* - "pointerEntered"
* - "pointerExited"
*
* - Focus event:
* - "focusLost"
* - "focusGained"
*
* - Component events:
* - "compSized"
* - "compMoved"
* - "compEnabled"
* - "compShown"
* - "compAdded"
* - "compRemoved"
*
* - Window events:
* - "winOpened"
* - "winActivated"
*
* - Menu events:
* - "menuItemSelected'
*
* - Shortcut events:
* - "shortcutFired"
*
* Current events manager is available with "zebkit.ui.events"
*
* @class zebkit.ui.event.EventManager
* @constructor
* @extends zebkit.ui.event.Manager
* @example
*
* // catch all pointer pressed events that are triggered by zebkit UI
* zebkit.ui.events.on("pointerPressed", function(e) {
* // handle event
* ...
* });
*/
pkg.EventManager = Class(pkg.Manager, zebkit.EventProducer, [
function() {
this._ = new this.clazz.Listerners();
this.$super();
},
function $clazz() {
var eventNames = [
'keyTyped',
'keyReleased',
'keyPressed',
'pointerDragged',
'pointerDragStarted',
'pointerDragEnded',
'pointerMoved',
'pointerClicked',
'pointerDoubleClicked',
'pointerPressed',
'pointerReleased',
'pointerEntered',
'pointerExited',
'focusLost',
'focusGained',
'compSized',
'compMoved',
'compEnabled',
'compShown',
'compAdded',
'compRemoved'
];
this.$CHILD_EVENTS_MAP = {};
// add child<eventName> events names mapping
for(var i = 0; i < eventNames.length; i++) {
var eventName = eventNames[i];
this.$CHILD_EVENTS_MAP[eventName] = "child" + eventName[0].toUpperCase() + eventName.substring(1);
}
this.Listerners = zebkit.ListenersClass.apply(this, eventNames);
},
function $prototype(clazz) {
var $CEM = clazz.$CHILD_EVENTS_MAP;
this.regEvents = function() {
this._.addEvents.apply(this._, arguments);
// add child<eventName> events names mapping
for(var i = 0; i < arguments.length; i++) {
var eventName = arguments[i];
$CEM[eventName] = "child" + eventName[0].toUpperCase() + eventName.substring(1);
}
};
/**
* Fire event with the given id
* @param {String} id an event id type
* @param {zebkit.Event} e different sort of event
* @return {Boolean} boolean flag that indicates if a event handling has been interrupted on one of a stage:
*
* - Suppressed by a target component
* - By a global listener
* - By a target component event listener
*
* @method fire
* @protected
*/
this.fire = function(id, e) {
var childEvent = $CEM[id];
// assign id that matches method to be called
e.id = id;
// TODO: not stable concept. the idea to suppress event
// distribution to global listeners (managers) and child
// components
if (e.source.$suppressEvent !== undefined &&
e.source.$suppressEvent(e) === true)
{
return true;
}
// call global listeners
if (this._[id](e) === false) {
// call target component listener
if (e.source[id] !== undefined && e.source[id].call(e.source, e) === true) {
return true;
}
// call parents listeners
for(var t = e.source.parent; t !== null && t !== undefined; t = t.parent) {
if (t[childEvent] !== undefined) {
t[childEvent].call(t, e);
}
}
return false;
} else {
return true;
}
};
}
]);
/**
* Event manager reference. The reference can be used to register listeners that can
* get all events of the given type that are fired by zebkit UI. For instance you can
* catch all pointer pressed events as follow:
* @example
*
* zebkit.ui.events.on("pointerPressed", function(e) {
* // handle pointer pressed event here
* ...
* });
*
* @attribute events
* @type {zebkit.ui.event.EventManager}
* @readOnly
*/
// TODO: correct to event package
//this.events = new pkg.EventManager();
zebkit.ui.events = new pkg.EventManager();
/**
* Base class to implement clipboard manager.
* @class zebkit.ui.event.Clipboard
* @constructor
* @extends zebkit.ui.event.Manager
*/
pkg.Clipboard = Class(pkg.Manager, [
function $prototype() {
/**
* Get destination component. Destination component is a component that
* is currently should participate in clipboard data exchange.
* @return {zebkit.ui.Panel} a destination component.
* @method getDestination
*/
this.getDestination = function() {
//TODO: may be focusManager has to be moved to "ui.event" package
return zebkit.ui.focusManager.focusOwner;
};
}
]);
/**
* Base class to implement cursor manager.
* @class zebkit.ui.event.CursorManager
* @constructor
* @extends zebkit.ui.event.Manager
*/
pkg.CursorManager = Class(pkg.Manager, [
function $prototype() {
/**
* Current cursor type
* @attribute cursorType
* @type {String}
* @readOnly
* @default "default"
*/
this.cursorType = "default";
}
]);
/**
* Input events state handler interface. The interface implements pointer and key
* events handler to track the current state where State can have one of the following
* value:
*
* - **over** the pointer cursor is inside the component
* - **out** the pointer cursor is outside the component
* - **pressed.over** the pointer cursor is inside the component and an action pointer
* button or key is pressed
* - **pressed.out** the pointer cursor is outside the component and an action pointer
* button or key is pressed
*
* Every time a state has been updated "stateUpdated" method is called (if it implemented).
* The interface can be handy way to track typical states. For instance to implement a
* component that changes its view depending its state the following code can be used:
*
* // create panel
* var pan = new zebkit.ui.Panel();
*
* // let's track the panel input events state and update
* // the component background view depending the state
* pan.extend(zebkit.ui.event.TrackInputEventState, [
* function stateUpdate(o, n) {
* if (n === "over") {
* this.setBackround("orange");
* } else if (n === "out") {
* this.setBackround("red");
* } else {
* this.setBackround(null);
* }
* }
* ]);
*
*
* @class zebkit.ui.event.TrackInputEventState
* @interface zebkit.ui.event.TrackInputEventState
*/
var OVER = "over",
PRESSED_OVER = "pressed.over",
OUT = "out",
PRESSED_OUT = "pressed.out";
pkg.TrackInputEventState = zebkit.Interface([
function $prototype() {
this.state = OUT;
this.$isIn = false;
this._keyPressed = function(e) {
if (this.state !== PRESSED_OVER &&
this.state !== PRESSED_OUT &&
(e.code === "Enter" || e.code === "Space"))
{
this.setState(PRESSED_OVER);
}
};
this._keyReleased = function(e) {
if (this.state === PRESSED_OVER || this.state === PRESSED_OUT){
this.setState(OVER);
if (this.$isIn === false) {
this.setState(OUT);
}
}
};
this._pointerEntered = function(e) {
if (this.isEnabled === true) {
this.setState(this.state === PRESSED_OUT ? PRESSED_OVER : OVER);
this.$isIn = true;
}
};
this._pointerPressed = function(e) {
if (this.state !== PRESSED_OVER && this.state !== PRESSED_OUT && e.isAction()){
this.setState(PRESSED_OVER);
}
};
this._pointerReleased = function(e) {
if ((this.state === PRESSED_OVER || this.state === PRESSED_OUT) && e.isAction()){
if (e.source === this) {
this.setState(e.x >= 0 && e.y >= 0 && e.x < this.width && e.y < this.height ? OVER
: OUT);
} else {
var p = zebkit.layout.toParentOrigin(e.x, e.y, e.source, this);
this.$isIn = p.x >= 0 && p.y >= 0 && p.x < this.width && p.y < this.height;
this.setState(this.$isIn ? OVER : OUT);
}
}
};
this.childKeyPressed = function(e) {
this._keyPressed(e);
};
this.childKeyReleased = function(e) {
this._keyReleased(e);
};
this.childPointerEntered = function(e) {
this._pointerEntered(e);
};
this.childPointerPressed = function(e) {
this._pointerPressed(e);
};
this.childPointerReleased = function(e) {
this._pointerReleased(e);
};
this.childPointerExited = function(e) {
// check if the pointer cursor is in of the source component
// that means another layer has grabbed control
if (e.x >= 0 && e.y >= 0 && e.x < e.source.width && e.y < e.source.height) {
this.$isIn = false;
} else {
var p = zebkit.layout.toParentOrigin(e.x, e.y, e.source, this);
this.$isIn = p.x >= 0 && p.y >= 0 && p.x < this.width && p.y < this.height;
}
if (this.$isIn === false) {
this.setState(this.state === PRESSED_OVER ? PRESSED_OUT : OUT);
}
};
/**
* Define key pressed events handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function(e){
this._keyPressed(e);
};
/**
* Define key released events handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyReleased
*/
this.keyReleased = function(e){
this._keyReleased(e);
};
/**
* Define pointer entered events handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerEntered
*/
this.pointerEntered = function (e){
this._pointerEntered();
};
/**
* Define pointer exited events handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerExited
*/
this.pointerExited = function(e){
if (this.isEnabled === true) {
this.setState(this.state === PRESSED_OVER ? PRESSED_OUT : OUT);
this.$isIn = false;
}
};
/**
* Define pointer pressed events handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerPressed
*/
this.pointerPressed = function(e){
this._pointerPressed(e);
};
/**
* Define pointer released events handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerReleased
*/
this.pointerReleased = function(e){
this._pointerReleased(e);
};
/**
* Define pointer dragged events handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerDragged
*/
this.pointerDragged = function(e){
if (e.isAction()) {
var pressed = (this.state === PRESSED_OUT || this.state === PRESSED_OVER);
if (e.x > 0 && e.y > 0 && e.x < this.width && e.y < this.height) {
this.setState(pressed ? PRESSED_OVER : OVER);
} else {
this.setState(pressed ? PRESSED_OUT : OUT);
}
}
};
/**
* Set the component state
* @param {Object} s a state
* @method setState
* @chainable
*/
this.setState = function(s) {
if (s !== this.state){
var prev = this.state;
this.state = s;
if (this.stateUpdated !== undefined) {
this.stateUpdated(prev, s);
}
}
return this;
};
}
]);
/**
* Focus event class.
* @class zebkit.ui.event.FocusEvent
* @constructor
* @extends zebkit.Event
*/
pkg.FocusEvent = Class(zebkit.Event, [
function $prototype() {
/**
* Related to the event component. For focus gained event it should be a component
* that lost focus. For focus lost event it should be a component that is going to
* get a focus.
* @attribute related
* @readOnly
* @default null
* @type {zebkit.ui.Panel}
*/
this.related = null;
}
]);
var FOCUS_EVENT = new pkg.FocusEvent();
/**
* Focus manager class defines the strategy of focus traversing among hierarchy of UI components.
* It keeps current focus owner component and provides API to change current focus component
* @class zebkit.ui.event.FocusManager
* @constructor
* @extends zebkit.ui.event.Manager
*/
pkg.FocusManager = Class(pkg.Manager, [
function $prototype() {
/**
* Reference to the current focus owner component.
* @attribute focusOwner
* @readOnly
* @type {zebkit.ui.Panel}
*/
this.focusOwner = null;
this.$freeFocus = function(comp) {
if ( this.focusOwner !== null &&
(this.focusOwner === comp || zebkit.layout.isAncestorOf(comp, this.focusOwner)))
{
this.requestFocus(null);
}
};
/**
* Component enabled event handler
* @param {zebkit.ui.Panel} c a component
* @method compEnabled
*/
this.compEnabled = function(e) {
var c = e.source;
if (c.isVisible === true && c.isEnabled === false && this.focusOwner !== null) {
this.$freeFocus(c);
}
};
/**
* Component shown event handler
* @param {zebkit.ui.Panel} c a component
* @method compShown
*/
this.compShown = function(e) {
var c = e.source;
if (c.isEnabled === true && c.isVisible === false && this.focusOwner !== null) {
this.$freeFocus(c);
}
};
/**
* Component removed event handler
* @param {zebkit.ui.Panel} p a parent
* @param {Integer} i a removed component index
* @param {zebkit.ui.Panel} c a removed component
* @method compRemoved
*/
this.compRemoved = function(e) {
var c = e.kid;
if (c.isEnabled === true && c.isVisible === true && this.focusOwner !== null) {
this.$freeFocus(c);
}
};
/**
* Test if the given component is a focus owner
* @param {zebkit.ui.Panel} c an UI component to be tested
* @method hasFocus
* @return {Boolean} true if the given component holds focus
*/
this.hasFocus = function(c) {
return this.focusOwner === c;
};
/**
* Key pressed event handler.
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function(e){
if ("Tab" === e.code) {
var cc = this.ff(e.source, e.shiftKey ? -1 : 1);
if (cc !== null) {
this.requestFocus(cc);
}
return true;
}
};
/**
* Find next candidate to grab focus starting from the given component.
* @param {zebkit.ui.Panel} c a component to start looking for next focusable.
* @return {zebkit.ui.Panel} a next component to gain focus.
* @method findFocusable
*/
this.findFocusable = function(c) {
return (this.isFocusable(c) ? c : this.fd(c, 0, 1));
};
/**
* Test if the given component can catch focus
* @param {zebkit.ui.Panel} c an UI component to be tested
* @method isFocusable
* @return {Boolean} true if the given component can catch a focus
*/
this.isFocusable = function(c) {
var d = c.getCanvas();
if (d !== null &&
(c.canHaveFocus === true ||
(typeof c.canHaveFocus === "function" && c.canHaveFocus() === true)))
{
for(;c !== d && c !== null; c = c.parent) {
if (c.isVisible === false || c.isEnabled === false) {
return false;
}
}
return c === d;
}
return false;
};
// looking recursively a focusable component among children components of
// the given target starting from the specified by index kid with the
// given direction (forward or backward lookup)
this.fd = function(t, index, d) {
if (t.kids.length > 0){
var isNComposite = t.catchInput === undefined || t.catchInput === false;
for(var i = index; i >= 0 && i < t.kids.length; i += d) {
var cc = t.kids[i];
// check if the current children component satisfies
// conditions it can grab focus or any deeper in hierarchy
// component that can grab the focus exist
if (cc.isEnabled === true &&
cc.isVisible === true &&
cc.width > 0 &&
cc.height > 0 &&
(isNComposite || (t.catchInput !== true &&
t.catchInput(cc) === false) ) &&
( (cc.canHaveFocus === true || (cc.canHaveFocus !== undefined &&
cc.canHaveFocus !== false &&
cc.canHaveFocus()) ) ||
(cc = this.fd(cc, d > 0 ? 0 : cc.kids.length - 1, d)) !== null) )
{
return cc;
}
}
}
return null;
};
// find next focusable component
// c - component starting from that a next focusable component has to be found
// d - a direction of next focusable component lookup: 1 (forward) or -1 (backward)
this.ff = function(c, d) {
var top = c;
while (top !== null && top.getFocusRoot === undefined) {
top = top.parent;
}
if (top === null) {
return null;
}
top = top.getFocusRoot();
if (top === null) {
return null;
}
if (top.traverseFocus !== undefined) {
return top.traverseFocus(c, d);
}
for(var index = (d > 0) ? 0 : c.kids.length - 1; c !== top.parent; ){
var cc = this.fd(c, index, d);
if (cc !== null) {
return cc;
}
cc = c;
c = c.parent;
if (c !== null) {
index = d + c.indexOf(cc);
}
}
return this.fd(top, d > 0 ? 0 : top.kids.length - 1, d);
};
/**
* Force to pass a focus to the given UI component
* @param {zebkit.ui.Panel} c an UI component to pass a focus
* @method requestFocus
*/
this.requestFocus = function(c) {
if (c !== this.focusOwner && (c === null || this.isFocusable(c))) {
var oldFocusOwner = this.focusOwner;
if (c !== null) {
var nf = c.getEventDestination();
if (nf === null || oldFocusOwner === nf) {
return;
}
this.focusOwner = nf;
} else {
this.focusOwner = c;
}
if (oldFocusOwner !== null) {
FOCUS_EVENT.source = oldFocusOwner;
FOCUS_EVENT.related = this.focusOwner;
oldFocusOwner.focused();
zebkit.ui.events.fire("focusLost", FOCUS_EVENT);
}
if (this.focusOwner !== null) {
FOCUS_EVENT.source = this.focusOwner;
FOCUS_EVENT.related = oldFocusOwner;
this.focusOwner.focused();
zebkit.ui.events.fire("focusGained", FOCUS_EVENT);
}
}
};
/**
* Pointer pressed event handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerPressed
*/
this.pointerPressed = function(e){
if (e.isAction()) {
this.requestFocus(e.source);
}
};
}
]);
// add shortcut event type
zebkit.ui.events.regEvents("shortcutFired");
/**
* Build all possible combination of the characters set:
* "abc" -> abc, acb, bac, bca, cab, cba
* @param {String} sequence character set
* @param {Function} cb called for every new unique characters
* sequence
*/
function variants(sequence, cb) {
if (sequence.length === 2) {
cb(sequence);
cb(sequence[1] + sequence[0]);
} else if (sequence.length > 2) {
for(var i = 0; i < sequence.length; i++) {
(function(sequence, ch) {
variants(sequence, function(v) {
cb(ch + v);
});
})(sequence.substring(0, i) + sequence.substring(i+1), sequence[i]);
}
} else {
cb(sequence);
}
}
/**
* Shortcut event class
* @constructor
* @param {zebkit.ui.Panel} src a source of the event
* @param {String} shortcut a shortcut name
* @param {String} keys a keys combination ("Control + KeyV")
* @class zebkit.ui.event.ShortcutEvent
* @extends zebkit.Event
*/
pkg.ShortcutEvent = Class(zebkit.Event, [
function(src, shortcut, keys) {
this.source = src;
/**
* Shortcut name
* @attribute shortcut
* @readOnly
* @type {String}
*/
this.shortcut = shortcut;
/**
* Shortcut keys combination
* @attribute keys
* @readOnly
* @type {String}
*/
this.keys = keys;
}
]);
var SHORTCUT_EVENT = new pkg.ShortcutEvent();
/**
* Shortcut manager supports short cut (keys) definition and listening. The shortcuts have to be defined in
* zebkit JSON configuration files. There are two sections:
*
* - **osx** to keep shortcuts for Mac OS X platform
* - **common** to keep shortcuts for all other platforms
*
* The JSON configuration entity has simple structure:
*
*
* {
* "common": {
* "UNDO": "Control + KeyZ",
* "REDO": "Control + Shift + KeyZ",
* ...
* },
* "osx" : {
* "UNDO": "MetaLeft + KeyZ",
* ...
* }
* }
*
* The configuration contains list of shortcuts. Every shortcut is bound to a key combination that triggers it.
* Shortcut has a name and an optional list of arguments that have to be passed to a shortcut listener method.
* The optional arguments can be used to differentiate two shortcuts that are bound to the same command.
*
* On the component level shortcut can be listened by implementing "shortcutFired(e)" listener handler.
* Pay attention to catch shortcut your component has to be focusable - be able to hold focus.
* For instance, to catch "UNDO" shortcut do the following:
*
* var pan = new zebkit.ui.Panel([
* function shortcutFired(e) {
* // handle shortcut here
* if (e.shortcut === "UNDO") {
*
* }
* },
*
* // visualize the component gets focus
* function focused() {
* this.$super();
* this.setBackground(this.hasFocus()?"red":null);
* }
* ]);
*
* // let our panel to hold focus by setting appropriate property
* pan.canHaveFocus = true;
*
*
* @constructor
* @class zebkit.ui.event.ShortcutManager
* @extends zebkit.ui.event.Manager
*/
pkg.ShortcutManager = Class(pkg.Manager, [
function(shortcuts) {
this.$super();
// special structure that is a path from the first key of a sjortcut to the ID
// for instance SELECTALL : [ "Control + KeyA", "Control + KeyW"], ... } will
// be stored as:
// {
// "Control" : {
// "KeyA" : SELECTALL,
// "KeyW" : SELECTALL
// }
// }
this.keyShortcuts = {};
if (arguments.length > 0) {
this.setShortcuts(shortcuts.common);
if (zebkit.isMacOS === true && shortcuts.osx !== undefined) {
this.setShortcuts(shortcuts.osx);
}
}
},
function $prototype() {
this.$keyPath = [];
/**
* Key pressed event handler.
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function(e) {
if (e.code === null || this.$keyPath.length > 5) {
this.$keyPath = [];
} else if (e.repeat === 1) {
this.$keyPath[this.$keyPath.length] = e.code;
}
// TODO: may be focus manager has to be moved to "ui.event" package
var fo = zebkit.ui.focusManager.focusOwner;
if (this.$keyPath.length > 1) {
var sh = this.keyShortcuts;
for(var i = 0; i < this.$keyPath.length; i++) {
var code = this.$keyPath[i];
if (sh.hasOwnProperty(code)) {
sh = sh[code];
} else {
sh = null;
break;
}
}
if (sh !== null) {
SHORTCUT_EVENT.source = fo;
SHORTCUT_EVENT.shortcut = sh;
SHORTCUT_EVENT.keys = this.$keyPath.join('+');
zebkit.ui.events.fire("shortcutFired", SHORTCUT_EVENT);
}
}
};
this.keyReleased = function(e) {
if (e.key === "Meta") {
this.$keyPath = [];
} else {
for(var i = 0; i < this.$keyPath.length; i++) {
if (this.$keyPath[i] === e.code) {
this.$keyPath.splice(i, 1);
break;
}
}
}
};
/**
* Set shortcuts. Expected shortcuts format is:
*
* { "<ID>" : "Control + KeyZ", ... }
*
* or
*
* { "<ID>" : ["Control + KeyZ", "Control + KeyV" ], ... }
*
* @param {shortcuts} shortcuts
* @method setShortcuts
*/
this.setShortcuts = function(shortcuts) {
for (var id in shortcuts) {
var shortcut = shortcuts[id],
j = 0;
id = id.trim();
if (Array.isArray(shortcut) === false) {
shortcut = [ shortcut ];
}
var re = /\(([^()]+)\)/;
for(j = 0; j < shortcut.length; j++) {
var m = re.exec(shortcut[j]);
if (m !== null) {
var variants = m[1].replace(/\s+/g, '').split('+'),
prefix = shortcut[j].substring(0, m.lastIndex),
suffix = shortcut[j].substring(m[1].length + 1);
}
}
for(j = 0; j < shortcut.length; j++) {
var keys = shortcut[j].replace(/\s+/g, '').split('+'),
st = this.keyShortcuts,
len = keys.length;
for(var i = 0; i < len; i++) {
var key = keys[i];
if (i === (len - 1)) {
st[key] = id;
} else if (st.hasOwnProperty(key) === false || zebkit.isString(st[key])) {
st[key] = {};
}
st = st[key];
}
}
}
};
}
]);
},false);
zebkit.package("ui", function(pkg, Class) {
'use strict';
var basedir = zebkit.config("ui.basedir"),
theme = zebkit.config("ui.theme");
this.config( { "basedir" : basedir ? basedir
: zebkit.URI.join(this.$url, "rs/themes/%{theme}"),
"theme" : theme ? theme
: "dark" },
false);
// Panel WEB specific dependencies:
// - getCanvas() -> zCanvas
// - $da (dirty area)
// - $isRootCanvas
// - $waitingForPaint (created and controlled by Panel painting !)
// - $context
// - restore(...)
// - restoreAll(...)
// - save()
// - clipRect(...)
// - clip()
// - clearRect(...)
// - translate(...)
// - $states[g.$curState] ?
//
// Panel zebkit classes dependencies
// - ui.CompEvent
// - ui.events EventManager
// - util.*
/**
* Zebkit UI package contains a lot of various components. Zebkit UI idea is rendering
* hierarchy of UI components on a canvas (HTML5 Canvas). Typical zebkit application
* looks as following:
*
* zebkit.require("ui", "layout", function(ui) {
* // create canvas and save reference to root layer
* // where zebkit UI components should live.
* var root = new ui.zCanvas(400, 400).root;
*
* // build UI layout
* root.properties({
* layout : new layout.BorderLayout(4),
* padding: 8,
* kids : {
* "center" : new ui.TextArea("A text"),
* "top" : new ui.ToolbarPan().properties({
* kids : [
* new ui.ImagePan("icon1.png"),
* new ui.ImagePan("icon2.png"),
* new ui.ImagePan("icon3.png")
* ]
* }),
* "bottom" : new ui.Button("Apply")
* }
* });
* });
*
* UI components are ordered with help of layout managers. You should not use absolute
* location or size your component. It is up to layout manager to decide which size and
* location the given component has to have. In the example above we add number of UI
* components to "root" (UI Panel). The root panel uses "BorderLayout" [to order the
* added components. The layout manager split root area to number of areas: "center",
* "top", "left", "right", "bottom" where children components can be placed.
*
* @class zebkit.ui
* @access package
*/
// extend Zson with special method to fill predefined views set
zebkit.Zson.prototype.views = function(v) {
for (var k in v) {
if (v.hasOwnProperty(k)) {
zebkit.draw.$views[k] = v[k];
}
}
};
function testCondition(target, cond) {
for(var k in cond) {
var cv = cond[k],
tv = zebkit.getPropertyValue(target, k, true);
if (cv !== tv) {
return false;
}
}
return true;
}
zebkit.Zson.prototype.completed = function() {
if (this.$actions !== undefined && this.$actions.length > 0) {
try {
var root = this.root;
for (var i = 0; i < this.$actions.length; i++) {
var a = this.$actions[i];
(function(source, eventName, cond, targets) {
var args = [],
srcComp = root.byPath(source);
if (eventName !== undefined && eventName !== null) {
args.push(eventName);
}
args.push(source);
args.push(function() {
if (cond === null || cond === undefined || testCondition(srcComp, cond)) {
// targets
for(var j = 0; j < targets.length; j++) {
var target = targets[j],
targetPath = (target.path === undefined) ? source : target.path;
// find target
root.byPath(targetPath, function(c) {
if (target.condition === undefined || testCondition(c, target.condition)) {
if (target.update !== undefined) {
c.properties(target.update);
}
if (target.do !== undefined) {
for(var cmd in target['do']) {
c[cmd].apply(c, target['do'][cmd]);
}
}
}
});
}
}
});
root.on.apply(root, args);
} (a.source, a.event, a.condition, a.targets !== undefined ? a.targets : [ a.target ]));
}
} finally {
this.$actions.length = 0;
}
}
};
zebkit.Zson.prototype.actions = function(v) {
this.$actions = [];
for (var i = 0; i < arguments.length; i++) {
this.$actions.push(arguments[i]);
}
};
zebkit.Zson.prototype.font = function() {
return zebkit.$font.apply(this, arguments);
};
zebkit.Zson.prototype.gradient = function() {
if (arguments.length === 1) {
if (zebkit.instanceOf(arguments[0], zebkit.draw.Gradient)) {
return arguments[0];
} else {
return new zebkit.draw.Gradient(arguments[0]);
}
} else {
return zebkit.draw.Gradient.newInstancea(arguments);
}
};
zebkit.Zson.prototype.border = function() {
if (arguments.length === 1 && zebkit.instanceOf(arguments[0], zebkit.draw.Border)) {
return arguments[0];
} else {
return zebkit.draw.Border.newInstancea(arguments);
}
};
zebkit.Zson.prototype.pic = function() {
if (arguments.length === 1 && zebkit.instanceOf(arguments[0], zebkit.draw.Picture)) {
return arguments[0];
} else {
return zebkit.draw.Picture.newInstancea(arguments);
}
};
// TODO: prototype of zClass, too simple to say something
pkg.zCanvas = Class([]);
/**
* Get preferred size shortcut. Null can be passed as the method argument
* @private
* @param {zebkit.ui.Layoutable} l a layoutable component
* @return {Object} a preferred size:
* { width : {Integer}, height: {Integer} }
* @method $getPS
* @for zebkit.ui
*/
pkg.$getPS = function(l) {
return l !== null && l.isVisible === true ? l.getPreferredSize()
: { width:0, height:0 };
};
/**
* Calculate visible area of the given components taking in account
* intersections with parent hierarchy.
* @private
* @param {zebkit.ui.Panel} c a component
* @param {Object} r a variable to store visible area
*
* { x: {Integer}, y: {Integer}, width: {Integer}, height: {Integer} }
*
* @method $cvp
* @for zebkit.ui
*/
pkg.$cvp = function(c, r) {
if (c.width > 0 && c.height > 0 && c.isVisible === true){
var p = c.parent,
px = -c.x, // transform parent coordinates to
py = -c.y; // children component coordinate system
// since the result has to be in
if (arguments.length < 2) {
r = { x:0, y:0, width : c.width, height : c.height };
} else {
r.x = r.y = 0;
r.width = c.width;
r.height = c.height;
}
while (p !== null && r.width > 0 && r.height > 0) {
var xx = r.x > px ? r.x : px,
yy = r.y > py ? r.y : py,
w1 = r.x + r.width,
w2 = px + p.width,
h1 = r.y + r.height,
h2 = py + p.height;
r.width = (w1 < w2 ? w1 : w2) - xx;
r.height = (h1 < h2 ? h1 : h2) - yy;
r.x = xx;
r.y = yy;
px -= p.x; // transform next parent coordinates to
py -= p.y; // children component coordinate system
p = p.parent;
}
return r.width > 0 && r.height > 0 ? r : null;
} else {
return null;
}
};
/**
* Relocate the given component to make them fully visible.
* @param {zebkit.ui.Panel} [d] a parent component where the given component has to be re-located
* @param {zebkit.ui.Panel} c a component to re-locate to make it fully visible in the parent
* component
* @method makeFullyVisible
* @for zebkit.ui
*/
pkg.makeFullyVisible = function(d, c){
if (arguments.length === 1) {
c = d;
d = c.parent;
}
var right = d.getRight(),
top = d.getTop(),
bottom = d.getBottom(),
left = d.getLeft(),
xx = c.x,
yy = c.y;
if (xx < left) {
xx = left;
}
if (yy < top) {
yy = top;
}
if (xx + c.width > d.width - right) {
xx = d.width + right - c.width;
}
if (yy + c.height > d.height - bottom) {
yy = d.height + bottom - c.height;
}
c.setLocation(xx, yy);
};
pkg.calcOrigin = function(x,y,w,h,px,py,t,tt,ll,bb,rr){
if (arguments.length < 8) {
tt = t.getTop();
ll = t.getLeft();
bb = t.getBottom();
rr = t.getRight();
}
var dw = t.width, dh = t.height;
if (dw > 0 && dh > 0){
if (dw - ll - rr > w){
var xx = x + px;
if (xx < ll) {
px += (ll - xx);
} else {
xx += w;
if (xx > dw - rr) {
px -= (xx - dw + rr);
}
}
}
if (dh - tt - bb > h){
var yy = y + py;
if (yy < tt) {
py += (tt - yy);
} else {
yy += h;
if (yy > dh - bb) {
py -= (yy - dh + bb);
}
}
}
return [px, py];
}
return [0, 0];
};
var $paintTask = null,
$paintTasks = [],
temporary = { x:0, y:0, width:0, height:0 },
COMP_EVENT = new zebkit.ui.event.CompEvent();
/**
* Trigger painting for all collected paint tasks
* @protected
* @method $doPaint
* @for zebkit.ui
*/
pkg.$doPaint = function() {
for (var i = $paintTasks.length - 1; i >= 0; i--) {
var canvas = $paintTasks.shift();
try {
// do validation before timer will be set to null to avoid
// unnecessary timer initiating what can be caused by validation
// procedure by calling repaint method
if (canvas.isValid === false || canvas.isLayoutValid === false) {
canvas.validate();
}
if (canvas.$da.width > 0) {
canvas.$context.save();
// check if the given canvas has transparent background
// if it is true call clearRect method to clear dirty area
// with transparent background, otherwise it will be cleaned
// by filling the canvas with background later
if (canvas.bg === null || canvas.bg.isOpaque !== true) {
// Clear method can be applied to scaled (retina screens) canvas
// The real cleaning location is calculated as x' = scaleX * x.
// The problem appears when scale factor is not an integer value
// what brings to situation x' can be float like 1.2 or 1.8.
// Most likely canvas applies round operation to x' so 1.8 becomes 2.
// That means clear method will clear less then expected, what results
// in visual artifacts are left on the screen. The code below tries
// to correct cleaning area to take in account the round effect.
if (canvas.$context.$scaleRatioIsInt === false) {
// Clear canvas scaling and calculate dirty area bounds.
// Bounds are calculated taking in account the fact that float
// bounds can leave visual traces
var xx = Math.floor(canvas.$da.x * canvas.$context.$scaleRatio),
yy = Math.floor(canvas.$da.y * canvas.$context.$scaleRatio),
ww = Math.ceil((canvas.$da.x + canvas.$da.width) * canvas.$context.$scaleRatio) - xx,
hh = Math.ceil((canvas.$da.y + canvas.$da.height) * canvas.$context.$scaleRatio) - yy;
canvas.$context.save();
canvas.$context.setTransform(1, 0, 0, 1, 0, 0);
canvas.$context.clearRect(xx, yy, ww, hh);
// !!!! clipping has to be done over not scaled
// canvas, otherwise if we have two overlapped panels
// with its own background moving third panel over overlapped
// part will leave traces that comes from lowest overlapped panel
// !!! Have no idea why !
// canvas.$context.beginPath();
// canvas.$context.rect(xx, yy, ww, hh);
// canvas.$context.closePath();
// canvas.$context.clip();
canvas.$context.restore();
canvas.$context.clipRect(canvas.$da.x - 1,
canvas.$da.y - 1,
canvas.$da.width + 2,
canvas.$da.height + 2);
} else {
canvas.$context.clearRect(canvas.$da.x, canvas.$da.y,
canvas.$da.width, canvas.$da.height);
// !!!
// call clipping area later than possible
// clearRect since it can bring to error in IE
canvas.$context.clipRect(canvas.$da.x,
canvas.$da.y,
canvas.$da.width,
canvas.$da.height);
}
}
// no dirty area anymore. put it hear to prevent calling
// animation task from repaint() method that can be called
// inside paintComponent method.
canvas.$da.width = -1;
// clear flag that says the canvas is waiting for repaint, that allows to call
// repaint from paint method
canvas.$waitingForPaint = false;
canvas.paintComponent(canvas.$context);
canvas.$context.restore();
} else {
canvas.$waitingForPaint = false;
}
} catch(ex) {
// catch error and clean task list if any to avoid memory leaks
try {
if (canvas !== null) {
canvas.$waitingForPaint = false;
canvas.$da.width = -1;
if (canvas.$context !== null) {
canvas.$context.restoreAll();
}
}
} catch(exx) {
$paintTask = null;
$paintTasks.length = 0;
throw exx;
}
zebkit.dumpError(ex);
}
}
// paint task is done
$paintTask = null;
// test if new dirty canvases have appeared and start
// animation again
if ($paintTasks.length !== 0) {
$paintTask = zebkit.environment.animate(pkg.$doPaint);
}
};
/**
* This the core UI component class. All other UI components has to be successor of panel class.
*
* // instantiate panel with no arguments
* var p = new zebkit.ui.Panel();
*
* // instantiate panel with border layout set as its layout manager
* var p = new zebkit.ui.Panel(new zebkit.layout.BorderLayout());
*
* // instantiate panel with the given properties (border
* // layout manager, blue background and plain border)
* var p = new zebkit.ui.Panel({
* layout: new zebkit.ui.BorderLayout(),
* background : "blue",
* border : "plain"
* });
*
* **Container**
* Panel can contains number of other UI components as its children where the children components
* are placed with a defined by the panel layout manager:
*
* // add few children component to panel top, center and bottom parts
* // with help of border layout manager
* var p = new zebkit.ui.Panel();
* p.setBorderLayout(4); // set layout manager to
* // order children components
*
* p.add("top", new zebkit.ui.Label("Top label"));
* p.add("center", new zebkit.ui.TextArea("Text area"));
* p.add("bottom", new zebkit.ui.Button("Button"));
*
* **Input and component events**
* The class provides possibility to catch various component and input events by declaring an
* appropriate event method handler. The most simple case you just define a method:
*
* var p = new zebkit.ui.Panel();
* p.pointerPressed = function(e) {
* // handle event here
* };
*
* If you prefer to create an anonymous class instance you can do it as follow:
*
* var p = new zebkit.ui.Panel([
* function pointerPressed(e) {
* // handle event here
* }
* ]);
*
* One more way to add the event handler is dynamic extending of an instance class demonstrated
* below:
*
* var p = new zebkit.ui.Panel("Test");
* p.extend([
* function pointerPressed(e) {
* // handle event here
* }
* ]);
*
* Pay attention Zebkit UI components often declare own event handlers and in this case you can
* overwrite the default event handler with a new one. Preventing the basic event handler execution
* can cause the component will work improperly. You should care about the base event handler
* execution as follow:
*
* // button component declares own pointer pressed event handler
* // we have to call the original handler to keep the button component
* // properly working
* var p = new zebkit.ui.Button("Test");
* p.extend([
* function pointerPressed(e) {
* this.$super(e); // call parent class event handler implementation
* // handle event here
* }
* ]);
*
* @class zebkit.ui.Panel
* @param {Object|zebkit.layout.Layout} [l] pass a layout manager or number of properties that have
* to be applied to the instance of the panel class.
* @constructor
* @extends zebkit.layout.Layoutable
*/
/**
* Implement the event handler method to catch pointer pressed event. The event is triggered every time
* a pointer button has been pressed or a finger has touched a touch screen.
*
* var p = new zebkit.ui.Panel();
* p.pointerPressed = function(e) { ... }; // add event handler
*
* @event pointerPressed
* @param {zebkit.ui.event.PointerEvent} e a pointer event
*/
/**
* Implement the event handler method to catch pointer released event. The event is triggered every time
* a pointer button has been released or a finger has untouched a touch screen.
*
* var p = new zebkit.ui.Panel();
* p.pointerReleased = function(e) { ... }; // add event handler
*
* @event pointerReleased
* @param {zebkit.ui.event.PointerEvent} e a pointer event
*/
/**
* Implement the event handler method to catch pointer moved event. The event is triggered every time
* a pointer cursor has been moved with no a pointer button pressed.
*
* var p = new zebkit.ui.Panel();
* p.pointerMoved = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerMoved
*/
/**
* Implement the event handler method to catch pointer entered event. The event is triggered every
* time a pointer cursor entered the given component.
*
* var p = new zebkit.ui.Panel();
* p.pointerEntered = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerEntered
*/
/**
* Implement the event handler method to catch pointer exited event. The event is triggered every
* time a pointer cursor exited the given component.
*
* var p = new zebkit.ui.Panel();
* p.pointerExited = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerExited
*/
/**
* Implement the event handler method to catch pointer clicked event. The event is triggered every
* time a pointer button has been clicked. Click events are generated only if no one pointer moved
* or drag events has been generated in between pointer pressed -> pointer released events sequence.
*
* var p = new zebkit.ui.Panel();
* p.pointerClicked = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerClicked
*/
/**
* Implement the event handler method to catch pointer dragged event. The event is triggered every
* time a pointer cursor has been moved when a pointer button has been pressed. Or when a finger
* has been moved over a touch screen.
*
* var p = new zebkit.ui.Panel();
* p.pointerDragged = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerDragged
*/
/**
* Implement the event handler method to catch pointer drag started event. The event is triggered
* every time a pointer cursor has been moved first time when a pointer button has been pressed.
* Or when a finger has been moved first time over a touch screen.
*
* var p = new zebkit.ui.Panel();
* p.pointerDragStarted = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerDragStarted
*/
/**
* Implement the event handler method to catch pointer drag ended event. The event is triggered
* every time a pointer cursor has been moved last time when a pointer button has been pressed.
* Or when a finger has been moved last time over a touch screen.
*
* var p = new zebkit.ui.Panel();
* p.pointerDragEnded = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @event pointerDragEnded
*/
/**
* Implement the event handler method to catch key pressed event The event is triggered every
* time a key has been pressed.
*
* var p = new zebkit.ui.Panel();
* p.keyPressed = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.KeyEvent} e a key event
* @event keyPressed
*/
/**
* Implement the event handler method to catch key types event The event is triggered every
* time a key has been typed.
*
* var p = new zebkit.ui.Panel();
* p.keyTyped = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.KeyEvent} e a key event
* @event keyTyped
*/
/**
* Implement the event handler method to catch key released event
* The event is triggered every time a key has been released.
*
* var p = new zebkit.ui.Panel();
* p.keyReleased = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.KeyEvent} e a key event
* @event keyReleased
*/
/**
* Implement the event handler method to catch the component sized event
* The event is triggered every time the component has been re-sized.
*
* var p = new zebkit.ui.Panel();
* p.compSized = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.CompEvent} e a component event. Source of the event
* is a component that has been sized, "prevWidth" and "prevHeight" fields
* keep a previous size the component had.
* @event compSized
*/
/**
* Implement the event handler method to catch component moved event
* The event is triggered every time the component location has been
* updated.
*
* var p = new zebkit.ui.Panel();
* p.compMoved = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.CompEvent} e a component event. Source of the event
* is a component that has been moved.
* @event compMoved
*/
/**
* Implement the event handler method to catch component enabled event
* The event is triggered every time a component enabled state has been
* updated.
*
* var p = new zebkit.ui.Panel();
* p.compEnabled = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.CompEvent} e a component event.
* @event compEnabled
*/
/**
* Implement the event handler method to catch component shown event
* The event is triggered every time a component visibility state has
* been updated.
*
* var p = new zebkit.ui.Panel();
* p.compShown = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.CompEvent} e a component event.
* @event compShown
*/
/**
* Implement the event handler method to catch component added event
* The event is triggered every time the component has been inserted into
* another one.
*
* var p = new zebkit.ui.Panel();
* p.compAdded = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.CompEvent} e a component event. The source of the passed event
* is set to a container component.
* @event compAdded
*/
/**
* Implement the event handler method to catch component removed event
* The event is triggered every time the component has been removed from
* its parent UI component.
*
* var p = new zebkit.ui.Panel();
* p.compRemoved = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.CompEvent} e a component event. The source of the passed event
* is set to the container component.
* @event compRemoved
*/
/**
* Implement the event handler method to catch component focus gained event
* The event is triggered every time a component has gained focus.
*
* var p = new zebkit.ui.Panel();
* p.focusGained = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.FocusEvent} e an input event
* @event focusGained
*/
/**
* Implement the event handler method to catch component focus lost event
* The event is triggered every time a component has lost focus
*
* var p = new zebkit.ui.Panel();
* p.focusLost = function(e) { ... }; // add event handler
*
* @param {zebkit.ui.event.FocusEvent} e an input event
* @event focusLost
*/
/**
* It is also possible to listen all the listed above event for children component. To handle
* the event register listener method following the pattern below:
*
*
* var p = new zebkit.ui.Panel();
* p.childPointerPressed = function(e) { ... }; // add event handler
*
*
* @param {zebkit.ui.event.KeyEvent | zebkit.ui.event.PointerEvent | zebkit.ui.event.CompEvent | zebkit.ui.event.FocusEvent}
* e an UI event fired by a child component.
* @event childEventName
*/
/**
* The method is called for focusable UI components (components that can hold input focus) to ask
* a string to be saved in native clipboard
*
* @return {String} a string to be copied in native clipboard
*
* @event clipCopy
*/
/**
* The method is called to pass string from clipboard to a focusable (a component that can hold
* input focus) UI component
*
* @param {String} s a string from native clipboard
*
* @event clipPaste
*/
pkg.Panel = Class(zebkit.layout.Layoutable, [
function $prototype() {
/**
* Request the whole UI component or part of the UI component to be repainted
* @param {Integer} [x] x coordinate of the component area to be repainted
* @param {Integer} [y] y coordinate of the component area to be repainted
* @param {Integer} [w] width of the component area to be repainted
* @param {Integer} [h] height of the component area to be repainted
* @method repaint
*/
this.repaint = function(x, y, w ,h) {
// step I: skip invisible components and components that are not in hierarchy
// don't initiate repainting thread for such sort of the components,
// but don't forget for zCanvas whose parent field is null, but it has $context
if (this.isVisible === true && (this.parent !== null || this.$context !== undefined)) {
//!!! find context buffer that holds the given component
var canvas = this;
for(; canvas.$context === undefined; canvas = canvas.parent) {
// component either is not in visible state or is not in hierarchy
// than stop repaint procedure
if (canvas.isVisible === false || canvas.parent === null) {
return;
}
}
// no arguments means the whole component has top be repainted
if (arguments.length === 0) {
x = y = 0;
w = this.width;
h = this.height;
}
// step II: calculate new actual dirty area
if (w > 0 && h > 0) {
var r = pkg.$cvp(this, temporary);
if (r !== null) {
zebkit.util.intersection(r.x, r.y, r.width, r.height, x, y, w, h, r);
if (r.width > 0 && r.height > 0) {
x = r.x;
y = r.y;
w = r.width;
h = r.height;
// calculate repainted component absolute location
var cc = this;
while (cc !== canvas) {
x += cc.x;
y += cc.y;
cc = cc.parent;
}
// normalize repaint area coordinates
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (w + x > canvas.width ) {
w = canvas.width - x;
}
if (h + y > canvas.height) {
h = canvas.height - y;
}
// still have what to repaint than calculate new
// dirty area of target canvas element
if (w > 0 && h > 0) {
var da = canvas.$da;
// if the target canvas already has a dirty area set than
// unite it with requested
if (da.width > 0) {
// check if the requested repainted area is not in
// exiting dirty area
if (x < da.x ||
y < da.y ||
x + w > da.x + da.width ||
y + h > da.y + da.height )
{
// !!!
// speed up to comment method call
//MB.unite(da.x, da.y, da.width, da.height, x, y, w, h, da);
var dax = da.x, day = da.y;
if (da.x > x) {
da.x = x;
}
if (da.y > y) {
da.y = y;
}
da.width = Math.max(dax + da.width, x + w) - da.x;
da.height = Math.max(day + da.height, y + h) - da.y;
}
} else {
// if the target canvas doesn't have a dirty area set than
// cut (if necessary) the requested repainting area by the
// canvas size
// !!!
// not necessary to call the method since we have already normalized
// repaint coordinates and sizes
//!!! MB.intersection(0, 0, canvas.width, canvas.height, x, y, w, h, da);
da.x = x;
da.width = w;
da.y = y;
da.height = h;
}
}
}
}
}
if (canvas.$waitingForPaint !== true && (canvas.isValid === false ||
canvas.$da.width > 0 ||
canvas.isLayoutValid === false))
{
$paintTasks[$paintTasks.length] = canvas;
canvas.$waitingForPaint = true;
if ($paintTask === null) {
$paintTask = zebkit.environment.animate(pkg.$doPaint);
}
}
}
};
// destination is component itself or one of his composite parent.
// composite component is a component that grab control from his
// children component. to make a component composite
// it has to implement catchInput field or method. If composite component
// has catchInput method it will be called
// to detect if the composite component takes control for the given kid.
// composite components can be embedded (parent composite can take
// control on its child composite component)
this.getEventDestination = function() {
var c = this, p = this;
while ((p = p.parent) !== null) {
if (p.catchInput !== undefined &&
(p.catchInput === true || (p.catchInput !== false &&
p.catchInput(c) === true )))
{
c = p;
}
}
return c;
};
/**
* Paint the component and all its child components using the
* given 2D HTML Canvas context
* @param {CanvasRenderingContext2D} g a canvas 2D context
* @method paintComponent
*/
this.paintComponent = function(g) {
var ts = g.$states[g.$curState]; // current state including clip area
if (ts.width > 0 &&
ts.height > 0 &&
this.isVisible === true)
{
// !!! TODO: WTF
// calling setSize in the case of raster layout doesn't
// cause hierarchy layout invalidation
if (this.isLayoutValid === false) {
this.validate();
}
var b = this.bg !== null && (this.parent === null || this.bg !== this.parent.bg);
// if component defines shape and has update, [paint?] or background that
// differs from parent background try to apply the shape and than build
// clip from the applied shape
if ( (this.border !== null && this.border.outline !== undefined) &&
(b === true || this.update !== undefined) &&
this.border.outline(g, 0, 0, this.width, this.height, this) === true)
{
g.save();
g.clip();
if (b) {
this.bg.paint(g, 0, 0, this.width, this.height, this);
}
if (this.update !== undefined) {
this.update(g);
}
g.restore();
this.border.paint(g, 0, 0, this.width, this.height, this);
} else {
if (b === true) {
this.bg.paint(g, 0, 0, this.width, this.height, this);
}
if (this.update !== undefined) {
this.update(g);
}
if (this.border !== null) {
this.border.paint(g, 0, 0, this.width, this.height, this);
}
}
if (this.paint !== undefined) {
var left = this.getLeft(),
top = this.getTop(),
bottom = this.getBottom(),
right = this.getRight();
if (left > 0 || right > 0 || top > 0 || bottom > 0) {
var tsxx = ts.x + ts.width,
tsyy = ts.y + ts.height,
cright = this.width - right,
cbottom = this.height - bottom,
x1 = (ts.x > left ? ts.x : left), // max
y1 = (ts.y > top ? ts.y : top), // max
w1 = (tsxx < cright ? tsxx : cright) - x1, // min
h1 = (tsyy < cbottom ? tsyy : cbottom) - y1; // min
if (x1 !== ts.x || y1 !== ts.y || w1 !== ts.width || h1 !== ts.height) {
g.save();
g.clipRect(x1, y1, w1, h1);
this.paint(g);
if (this.$doNotClipChildComponents === true) {
g.restore();
this.paintChildComponents(g, false);
} else {
this.paintChildComponents(g, false);
g.restore();
}
} else {
// It has been checked that the optimization works for some components
this.paint(g);
this.paintChildComponents(g, this.$doNotClipChildComponents !== true);
}
} else {
this.paint(g);
this.paintChildComponents(g, this.$doNotClipChildComponents !== true);
}
} else {
this.paintChildComponents(g, this.$doNotClipChildComponents !== true);
}
if (this.paintOnTop !== undefined) {
this.paintOnTop(g);
}
}
};
/**
* Paint child components.
* @param {CanvasRenderingContext2D} g a canvas 2D context
* @param {Boolean} clipChild true if child components have to be clipped with
* the parent component paddings.
* @method paintChildComponents
*/
this.paintChildComponents = function(g, clipChild) {
var ts = g.$states[g.$curState]; // current state including clip area
if (ts.width > 0 && ts.height > 0 && this.kids.length > 0) {
var shouldClip = false,
tsxx = ts.x + ts.width,
tsyy = ts.y + ts.height;;
if (clipChild === true) {
var left = this.getLeft(),
top = this.getTop(),
bottom = this.getBottom(),
right = this.getRight();
if (left > 0 || right > 0 || top > 0 || bottom > 0) {
var x1 = (ts.x > left ? ts.x : left), // max
y1 = (ts.y > top ? ts.y : top), // max
cright = this.width - right,
cbottom = this.height - bottom,
w1 = (tsxx < cright ? tsxx : cright) - x1, // min
h1 = (tsyy < cbottom ? tsyy : cbottom) - y1; // min
shouldClip = (x1 !== ts.x || y1 !== ts.y || w1 !== ts.width || h1 !== ts.height);
if (shouldClip === true) {
g.save();
g.clipRect(x1, y1, w1, h1);
}
}
}
for(var i = 0; i < this.kids.length; i++) {
var kid = this.kids[i];
// ignore invisible components and components that declare own 2D context
if (kid.isVisible === true && kid.$context === undefined) {
// calculate if the given component area has intersection
// with current clipping area
var kidxx = kid.x + kid.width,
kidyy = kid.y + kid.height,
iw = (kidxx < tsxx ? kidxx : tsxx) - (kid.x > ts.x ? kid.x : ts.x),
ih = (kidyy < tsyy ? kidyy : tsyy) - (kid.y > ts.y ? kid.y : ts.y);
if (iw > 0 && ih > 0) {
g.save();
g.translate(kid.x, kid.y);
g.clipRect(0, 0, kid.width, kid.height);
kid.paintComponent(g);
g.restore();
}
}
}
if (shouldClip === true) {
g.restore();
}
}
};
/**
* UI component border view
* @attribute border
* @default null
* @readOnly
* @type {zebkit.draw.View}
*/
this.border = null;
/**
* UI component background view
* @attribute bg
* @default null
* @readOnly
* @type {zebkit.draw.View}
*/
this.bg = null;
/**
* Define and set the property to true if the component has to catch focus
* @attribute canHaveFocus
* @type {Boolean}
* @default undefined
*/
this.top = this.left = this.right = this.bottom = 0;
/**
* UI component enabled state
* @attribute isEnabled
* @default true
* @readOnly
* @type {Boolean}
*/
this.isEnabled = true;
/**
* Find a zebkit.ui.zCanvas where the given UI component is hosted
* @return {zebkit.ui.zCanvas} a zebkit canvas
* @method getCanvas
*/
this.getCanvas = function() {
var c = this;
for(; c !== null && c.$isRootCanvas !== true; c = c.parent) {}
return c;
};
this.notifyRender = function(o, n){
if (o !== null && o.ownerChanged !== undefined) {
o.ownerChanged(null);
}
if (n !== null && n.ownerChanged !== undefined) {
n.ownerChanged(this);
}
};
/**
* Set border layout shortcut method
* @param {Integer} [gap] a gap
* @method setBorderLayout
* @chainable
*/
this.setBorderLayout = function() {
this.setLayout(zebkit.layout.BorderLayout.newInstancea(arguments));
return this;
};
/**
* Set flow layout shortcut method.
* @param {String} [ax] ("left" by default) horizontal alignment:
*
* "left"
* "center"
* "right"
*
* @param {String} [ay] ("top" by default) vertical alignment:
*
* "top"
* "center"
* "bottom"
*
* @param {String} [dir] ("horizontal" by default) a direction the component has to be placed
* in the layout
*
* "vertical"
* "horizontal"
*
* @param {Integer} [gap] a space in pixels between laid out components
* @method setFlowLayout
* @chainable
*/
this.setFlowLayout = function() {
this.setLayout(zebkit.layout.FlowLayout.newInstancea(arguments));
return this;
};
/**
* Set stack layout shortcut method
* @param {Integer} [gap] a gap
* @method setStackLayout
* @chainable
*/
this.setStackLayout = function() {
this.setLayout(zebkit.layout.StackLayout.newInstancea(arguments));
return this;
};
/**
* Set list layout shortcut method
* @param {String} [ax] horizontal list item alignment:
*
* "left"
* "right"
* "center"
* "stretch"
*
* @param {Integer} [gap] a space in pixels between laid out components
* @method setListLayout
* @chainable
*/
this.setListLayout = function() {
this.setLayout(zebkit.layout.ListLayout.newInstancea(arguments));
return this;
};
/**
* Set raster layout shortcut method
* @param {Boolean} [usePsSize] flag to add extra rule to set components size to its preferred
* sizes.
* @method setRasterLayout
* @chainable
*/
this.setRasterLayout = function() {
this.setLayout(zebkit.layout.RasterLayout.newInstancea(arguments));
return this;
};
/**
* Set raster layout shortcut method
* sizes.
* @method setGrisLayout
* @chainable
*/
this.setGridLayout = function() {
this.setLayout(zebkit.layout.GridLayout.newInstancea(arguments));
return this;
};
/**
* Load content of the panel UI components from the specified JSON file.
* @param {String|Object} JSON URL, JSON string or JS object that describes UI
* to be loaded into the panel
* @return {zebkit.DoIt} a runner to track JSON loading
* @method load
*/
this.load = function(jsonPath) {
return new zebkit.Zson(this).then(jsonPath);
};
/**
* Get a children UI component that embeds the given point. The method
* calculates the component visible area first and than looks for a
* children component only in this calculated visible area. If no one
* children component has been found than component return itself as
* a holder of the given point if one of the following condition is true:
*
* - The component doesn't implement custom "contains(x, y)" method
* - The component implements "contains(x, y)" method and for the given point the method return true
*
* @param {Integer} x x coordinate
* @param {Integer} y y coordinate
* @return {zebkit.ui.Panel} a children UI component
* @method getComponentAt
*/
this.getComponentAt = function(x, y){
var r = pkg.$cvp(this, temporary);
if (r === null ||
(x < r.x || y < r.y || x >= r.x + r.width || y >= r.y + r.height))
{
return null;
}
if (this.kids.length > 0) {
for(var i = this.kids.length; --i >= 0; ){
var kid = this.kids[i];
kid = kid.getComponentAt(x - kid.x,
y - kid.y);
if (kid !== null) {
return kid;
}
}
}
return this.contains === undefined || this.contains(x, y) === true ? this : null;
};
/**
* Shortcut method to invalidating the component and then initiating the component
* repainting.
* @method vrp
*/
this.vrp = function(){
this.invalidate();
// extra condition to save few millisecond on repaint() call
if (this.isVisible === true && this.parent !== null) {
this.repaint();
}
};
this.getTop = function() {
return this.border !== null ? this.top + this.border.getTop()
: this.top;
};
this.getLeft = function() {
return this.border !== null ? this.left + this.border.getLeft()
: this.left;
};
this.getBottom = function() {
return this.border !== null ? this.bottom + this.border.getBottom()
: this.bottom;
};
this.getRight = function() {
return this.border !== null ? this.right + this.border.getRight()
: this.right;
};
//TODO: the method is not used yet
this.isInvalidatedByChild = function(c) {
return true;
};
/**
* The method is implemented to be aware about a children component insertion.
* @param {Integer} index an index at that a new children component
* has been added
* @param {Object} constr a layout constraints of an inserted component
* @param {zebkit.ui.Panel} l a children component that has been inserted
* @method kidAdded
*/
this.kidAdded = function(index, constr, l) {
COMP_EVENT.source = this;
COMP_EVENT.constraints = constr;
COMP_EVENT.kid = l;
pkg.events.fire("compAdded", COMP_EVENT);
if (l.width > 0 && l.height > 0) {
l.repaint();
} else {
this.repaint(l.x, l.y, 1, 1);
}
};
/**
* Set the component layout constraints.
* @param {Object} ctr a constraints whose value depends on layout manager that has been set
* @method setConstraints
* @chainable
*/
this.setConstraints = function(ctr) {
if (this.constraints !== ctr) {
this.constraints = ctr;
if (this.parent !== null) {
this.vrp();
}
}
return this;
};
/**
* The method is implemented to be aware about a children component removal.
* @param {Integer} i an index of a removed component
* @param {zebkit.ui.Panel} l a removed children component
* @param {Object} ctr a constraints the kid component had
* @method kidRemoved
*/
this.kidRemoved = function(i, l, ctr){
COMP_EVENT.source = this;
COMP_EVENT.index = i;
COMP_EVENT.kid = l;
pkg.events.fire("compRemoved", COMP_EVENT);
if (l.isVisible === true) {
this.repaint(l.x, l.y, l.width, l.height);
}
};
/**
* The method is implemented to be aware the component location updating
* @param {Integer} px a previous x coordinate of the component
* @param {Integer} py a previous y coordinate of the component
* @method relocated
*/
this.relocated = function(px, py) {
COMP_EVENT.source = this;
COMP_EVENT.prevX = px;
COMP_EVENT.prevY = py;
pkg.events.fire("compMoved", COMP_EVENT);
var p = this.parent,
w = this.width,
h = this.height;
if (p !== null && w > 0 && h > 0) {
var x = this.x,
y = this.y,
nx = x < px ? x : px,
ny = y < py ? y : py;
if (nx < 0) {
nx = 0;
}
if (ny < 0) {
ny = 0;
}
var w1 = p.width - nx,
w2 = w + (x > px ? x - px : px - x),
h1 = p.height - ny,
h2 = h + (y > py ? y - py : py - y);
p.repaint(nx, ny, (w1 < w2 ? w1 : w2),
(h1 < h2 ? h1 : h2));
}
};
/**
* The method is implemented to be aware the component size updating
* @param {Integer} pw a previous width of the component
* @param {Integer} ph a previous height of the component
* @method resized
*/
this.resized = function(pw,ph) {
COMP_EVENT.source = this;
COMP_EVENT.prevWidth = pw;
COMP_EVENT.prevHeight = ph;
pkg.events.fire("compSized", COMP_EVENT);
if (this.parent !== null) {
this.parent.repaint(this.x, this.y,
((this.width > pw) ? this.width : pw),
((this.height > ph) ? this.height : ph));
}
};
/**
* Checks if the component has a focus
* @return {Boolean} true if the component has focus
* @method hasFocus
*/
this.hasFocus = function(){
return pkg.focusManager.hasFocus(this);
};
/**
* Force the given component to catch focus if the component is focusable.
* @method requestFocus
*/
this.requestFocus = function(){
pkg.focusManager.requestFocus(this);
};
/**
* Force the given component to catch focus in the given timeout.
* @param {Integer} [timeout] a timeout in milliseconds. The default value is 50
* milliseconds
* @method requestFocusIn
*/
this.requestFocusIn = function(timeout) {
if (arguments.length === 0) {
timeout = 50;
}
var $this = this;
zebkit.util.tasksSet.runOnce(function () {
$this.requestFocus();
}, timeout);
};
/**
* Set the UI component visibility
* @param {Boolean} b a visibility state
* @method setVisible
* @chainable
*/
this.setVisible = function (b) {
if (this.isVisible !== b) {
this.isVisible = b;
this.invalidate();
COMP_EVENT.source = this;
pkg.events.fire("compShown", COMP_EVENT);
if (this.parent !== null) {
if (b) {
this.repaint();
} else {
this.parent.repaint(this.x, this.y, this.width, this.height);
}
}
}
return this;
};
/**
* Set the UI component enabled state. Using this property
* an UI component can be excluded from getting input events
* @param {Boolean} b a enabled state
* @method setEnabled
* @chainable
*/
this.setEnabled = function (b){
if (this.isEnabled !== b){
this.isEnabled = b;
COMP_EVENT.source = this;
pkg.events.fire("compEnabled", COMP_EVENT);
if (this.kids.length > 0) {
for(var i = 0;i < this.kids.length; i++) {
this.kids[i].setEnabled(b);
}
}
this.repaint();
}
return this;
};
/**
* Set the UI component top, right, left, bottom paddings to the same given value
* @param {Integer} v the value that will be set as top, right, left, bottom UI
* component paddings
* @method setPadding
* @chainable
*/
/**
* Set UI component top, left, bottom, right paddings. The paddings are
* gaps between component border and painted area.
* @param {Integer} top a top padding
* @param {Integer} left a left padding
* @param {Integer} bottom a bottom padding
* @param {Integer} right a right padding
* @method setPadding
* @chainable
*/
this.setPadding = function (top,left,bottom,right){
if (arguments.length === 1) {
left = bottom = right = top;
}
if (this.top !== top || this.left !== left ||
this.bottom !== bottom || this.right !== right )
{
this.top = top;
this.left = left;
this.bottom = bottom;
this.right = right;
this.vrp();
}
return this;
};
/**
* Set top padding
* @param {Integer} top a top padding
* @method setTopPadding
* @chainable
*/
this.setTopPadding = function(top) {
if (this.top !== top) {
this.top = top;
this.vrp();
}
return this;
};
/**
* Set left padding
* @param {Integer} left a left padding
* @method setLeftPadding
* @chainable
*/
this.setLeftPadding = function(left) {
if (this.left !== left) {
this.left = left;
this.vrp();
}
return this;
};
/**
* Set bottom padding
* @param {Integer} bottom a bottom padding
* @method setBottomPadding
* @chainable
*/
this.setBottomPadding = function(bottom) {
if (this.bottom !== bottom) {
this.bottom = bottom;
this.vrp();
}
return this;
};
/**
* Set right padding
* @param {Integer} right a right padding
* @method setRightPadding
* @chainable
*/
this.setRightPadding = function(right) {
if (this.right !== right) {
this.right = right;
this.vrp();
}
return this;
};
/**
* Set the border view
* @param {zebkit.draw.View|Function|String} [v] a border view or border "paint(g,x,y,w,h,c)"
* rendering function or one of predefined border name: "plain", "sunken", "raised", "etched".
* If no argument has been passed the method tries to set "plain" as the component border.
* @method setBorder
* @example
*
* var pan = new zebkit.ui.Panel();
*
* // set round border
* pan.setBorder(zebkit.draw.RoundBorder("red"));
*
* ...
* // set one of predefined border
* pan.setBorder("plain");
*
* @chainable
*/
this.setBorder = function (v) {
if (arguments.length === 0) {
v = "plain";
}
var old = this.border;
v = zebkit.draw.$view(v);
if (v != old){
this.border = v;
this.notifyRender(old, v);
if ( old === null || v === null ||
old.getTop() !== v.getTop() ||
old.getLeft() !== v.getLeft() ||
old.getBottom() !== v.getBottom() ||
old.getRight() !== v.getRight() )
{
this.invalidate();
}
if (v !== null && v.activate !== undefined) {
v.activate(this.hasFocus() ? "focuson": "focusoff", this);
}
this.repaint();
}
return this;
};
/**
* Set the background. Background can be a color string or a zebkit.draw.View class
* instance, or a function(g,x,y,w,h,c) that paints the background:
*
* // set background color
* comp.setBackground("red");
*
* // set a picture as a component background
* comp.setBackground(new zebkit.draw.Picture(...));
*
* // set a custom rendered background
* comp.setBackground(function(g,x,y,w,h,target) {
* // paint a component background here
* g.setColor("blue");
* g.fillRect(x,y,w,h);
* g.drawLine(...);
* ...
* });
*
*
* @param {String|zebkit.draw.View|Function} v a background view, color or
* background "paint(g,x,y,w,h,c)" rendering function.
* @method setBackground
* @chainable
*/
this.setBackground = function(v) {
var old = this.bg;
v = zebkit.draw.$view(v);
if (v !== old) {
this.bg = v;
this.notifyRender(old, v);
this.repaint();
}
return this;
};
/**
* Add the given children component or number of components to the given panel.
* @protected
* @param {zebkit.ui.Panel|Array|Object} a children component of number of
* components to be added. The parameter can be:
*
* - Component
* - Array of components
* - Dictionary object where every element is a component to be added and the key of
* the component is stored in the dictionary is considered as the component constraints
*
* @method setKids
* @chainable
*/
this.setKids = function(a) {
if (arguments.length === 1 && zebkit.instanceOf(a, pkg.Panel)) {
this.add(a);
} else {
var i = 0;
// if components list passed as number of arguments
if (arguments.length > 1) {
for(i = 0; i < arguments.length; i++) {
var kid = arguments[i];
if (kid !== null) {
this.add(kid.$new !== undefined ? kid.$new() : kid);
}
}
} else {
if (Array.isArray(a)) {
for(i = 0; i < a.length; i++) {
if (a[i] !== null) {
this.add(a[i]);
}
}
} else {
var kids = a;
for(var k in kids) {
if (kids.hasOwnProperty(k)) {
this.add(k, kids[k]);
}
}
}
}
}
return this;
};
/**
* The method is called whenever the UI component gets or looses focus
* @method focused
* @protected
*/
this.focused = function() {
// extents of activate method indicates it is
if (this.border !== null && this.border.activate !== undefined) {
var id = this.hasFocus() ? "focuson" : "focusoff" ;
if (this.border.views[id] !== undefined) {
this.border.activate(id, this);
this.repaint();
}
}
// TODO: think if the background has to be focus dependent
// if (this.bg !== null && this.bg.activate !== undefined) {
// var id = this.hasFocus() ? "focuson" : "focusoff" ;
// if (this.bg.views[id]) {
// this.bg.activate(id);
// this.repaint();
// }
// }
};
/**
* Remove all children components
* @method removeAll
* @chainable
*/
this.removeAll = function (){
if (this.kids.length > 0){
var size = this.kids.length, mx1 = Number.MAX_VALUE, my1 = mx1, mx2 = 0, my2 = 0;
for(; size > 0; size--){
var child = this.kids[size - 1];
if (child.isVisible === true){
var xx = child.x, yy = child.y;
mx1 = mx1 < xx ? mx1 : xx;
my1 = my1 < yy ? my1 : yy;
mx2 = Math.max(mx2, xx + child.width);
my2 = Math.max(my2, yy + child.height);
}
this.removeAt(size - 1);
}
this.repaint(mx1, my1, mx2 - mx1, my2 - my1);
}
return this;
};
/**
* Bring the UI component to front
* @method toFront
* @chainable
*/
this.toFront = function(){
if (this.parent !== null && this.parent.kids[this.parent.kids.length-1] !== this){
var p = this.parent;
p.kids.splice(p.indexOf(this), 1);
p.kids[p.kids.length] = this;
p.vrp();
}
return this;
};
/**
* Send the UI component to back
* @method toBack
* @chainable
*/
this.toBack = function(){
if (this.parent !== null && this.parent.kids[0] !== this){
var p = this.parent;
p.kids.splice(p.indexOf(this), 1);
p.kids.unshift(this);
p.vrp();
}
return this;
};
/**
* Set the UI component size to its preferred size
* @chainable
* @method toPreferredSize
*/
this.toPreferredSize = function() {
var ps = this.getPreferredSize();
this.setSize(ps.width, ps.height);
return this;
};
/**
* Set the UI component height to its preferred height
* @method toPreferredHeight
* @chainable
*/
this.toPreferredHeight = function() {
var ps = this.getPreferredSize();
this.setSize(this.width, ps.height);
return this;
};
/**
* Set the UI component width to its preferred width
* @method toPreferredWidth
* @chainable
*/
this.toPreferredWidth = function() {
var ps = this.getPreferredSize();
this.setSize(ps.width, this.height);
return this;
};
/**
* Build zebkit.draw.View that represents the UI component
* @return {zebkit.draw.View} a view of the component
* @param {zebkit.ui.Panel} target a target component
* @method toView
*/
this.toView = function(target) {
return new pkg.CompRender(this);
};
/**
* Paint the given view with he specified horizontal and vertical
* alignments.
* @param {CanvasRenderingContext2D} g a 2D context
* @param {String} ax a horizontal alignment ("left", "right", "center")
* @param {String} ay a vertical alignment ("top", "center", "bottom")
* @param {zebkit.draw.View} v a view
* @chainable
* @method paintViewAt
*/
this.paintViewAt = function(g, ax, ay, v) {
var x = this.getLeft(),
y = this.getTop(),
ps = v.getPreferredSize();
if (ax === "center") {
x = Math.floor((this.width - ps.width)/2);
} else if (ax === "right") {
x = this.width - this.getRight() - ps.width;
}
if (ay === "center") {
y = Math.floor((this.height - ps.height)/2);
} else if (ay === "bottom") {
y = this.height - this.getBottom() - ps.height;
}
v.paint(g, x, y, ps.width, ps.height, this);
return this;
};
this[''] = function(l) {
// TODO:
// !!! dirty trick to call super, for the sake of few milliseconds back
//this.$super();
if (this.kids === undefined) {
this.kids = [];
}
if (this.layout === null) {
this.layout = this;
}
if (this.clazz.inheritProperties === true) {
// instead of recursion collect stack in array than go through it
var hierarchy = [],
props = {},
pp = this.clazz;
// collect clazz hierarchy
while (pp.$parent !== null && pp.inheritProperties === true) {
pp = pp.$parent;
hierarchy[hierarchy.length] = pp;
}
// collect properties taking in account possible overwriting
var b = false;
for (var i = hierarchy.length; i >= 0; i--) {
pp = hierarchy[i];
for (var k in pp) {
if (this.clazz[k] === undefined && props[k] === undefined) {
props[k] = pp[k];
if (b === false) {
b = true;
}
}
}
}
if (b) {
this.properties(props);
}
}
this.properties(this.clazz);
if (arguments.length > 0) {
if (l === undefined || l === null) {
throw new Error("Undefined arguments. Properties set (Object) or layout manager instance is expected as Panel constructor input");
}
if (l.constructor === Object) { // TODO: not 100% method to detect "{}" dictionary
this.properties(l);
} else {
this.setLayout(l);
}
}
};
}
]);
/**
* Root layer interface.
* @class zebkit.ui.RootLayerMix
* @constructor
* @interface zebkit.ui.RootLayerMix
*/
pkg.RootLayerMix = zebkit.Interface([
function $clazz() {
/**
* Root layer id.
* @attribute id
* @type {String}
* @readOnly
* @default "root"
*/
this.id = "root";
},
function $prototype() {
this.getFocusRoot = function() {
return this;
};
}
]);
/**
* Root layer panel implementation basing on zebkit.ui.Panel component.
* @class zebkit.ui.RootLayer
* @extends zebkit.ui.Panel
* @uses zebkit.ui.RootLayerMix
*/
pkg.RootLayer = Class(pkg.Panel, pkg.RootLayerMix, []);
/**
* Class that holds mouse cursor constant.
* @constructor
* @class zebkit.ui.Cursor
*/
pkg.Cursor = {
/**
* "default"
* @const DEFAULT
* @type {String}
*/
DEFAULT: "default",
/**
* "move"
* @const MOVE
* @type {String}
*/
MOVE: "move",
/**
* "wait"
* @const WAIT
* @type {String}
*/
WAIT: "wait",
/**
* "text"
* @const TEXT
* @type {String}
*/
TEXT: "text",
/**
* "pointer"
* @const HAND
* @type {String}
*/
HAND: "pointer",
/**
* "ne-resize"
* @const NE_RESIZE
* @type {String}
*/
NE_RESIZE: "ne-resize",
/**
* "sw-resize"
* @const SW_RESIZE
* @type {String}
*/
SW_RESIZE: "sw-resize",
/**
* "se-resize"
* @const SE_RESIZE
* @type {String}
*/
SE_RESIZE: "se-resize",
/**
* "nw-resize"
* @const NW_RESIZE
* @type {String}
*/
NW_RESIZE: "nw-resize",
/**
* "s-resize"
* @const S_RESIZE
* @type {String}
*/
S_RESIZE: "s-resize",
/**
* "w-resize"
* @const W_RESIZE
* @type {String}
*/
W_RESIZE: "w-resize",
/**
* "n-resize"
* @const N_RESIZE
* @type {String}
*/
N_RESIZE: "n-resize",
/**
* "e-resize"
* @const E_RESIZE
* @type {String}
*/
E_RESIZE: "e-resize",
/**
* "col-resize"
* @const COL_RESIZE
* @type {String}
*/
COL_RESIZE: "col-resize",
/**
* "help"
* @const HELP
* @type {String}
*/
HELP: "help"
};
/**
* UI component render class. Renders the given target UI component
* on the given surface using the specified 2D context
* @param {zebkit.layout.Layoutable} [target] an UI component to be rendered
* @class zebkit.ui.CompRender
* @constructor
* @extends zebkit.draw.Render
*/
pkg.CompRender = Class(zebkit.draw.Render, [
function $prototype() {
/**
* Get preferred size of the render. The method doesn't calculates
* preferred size it simply calls the target component "getPreferredSize"
* method.
* @method getPreferredSize
* @return {Object} a preferred size
*
* {width:<Integer>, height: <Integer>}
*/
this.getPreferredSize = function(){
return this.target === null || this.target.isVisible === false ? { width:0, height:0 }
: this.target.getPreferredSize();
};
this.paint = function(g,x,y,w,h,d){
var c = this.target;
if (c !== null && c.isVisible) {
var prevW = -1,
prevH = 0,
parent = null;
if (w !== c.width || h !== c.height) {
if (c.getCanvas() !== null) {
parent = c.parent;
c.parent = null;
}
prevW = c.width;
prevH = c.height;
c.setSize(w, h);
}
// validate should be done here since setSize can be called
// above
c.validate();
g.translate(x, y);
try {
c.paintComponent(g);
} catch(e) {
if (parent !== null) {
c.parent = parent;
}
g.translate(-x, -y);
throw e;
}
g.translate(-x, -y);
if (prevW >= 0){
c.setSize(prevW, prevH);
if (parent !== null) {
c.parent = parent;
}
c.validate();
}
}
};
}
]);
/**
* Shortcut to create a UI component by the given description. Depending on the description type
* the following components are created:
*
* - **String**
* - String encoded as "[x] Text" or "[] Text" will considered as checkbox component
* - String encoded as "@(image_path:WxH) Text" will considered as image or image label component
* - All other strings will be considered as label component
* - **Array** zebkit.ui.Combobox
* - **2D Array** zebkit.ui.grid.Grid
* - **Image** will be embedded with zebkit.ui.ImagePan component
* - **zebkit.ui.View instance** will be embedded with zebkit.ui.ViewPan component
* - **zebkit.ui.Panel instance** will be returned as is
*
* @method $component
* @protected
* @for zebkit.ui
* @param {Object} desc a description
* @return {zebkit.ui.Panel} a created UI component
*/
pkg.$component = function(desc, instance) {
var hasInstance = arguments.length > 1;
if (zebkit.isString(desc)) {
// [x] Text
// @(image-path:WxH) label
// %{<json> json-path} !!! not supported
// { "zebkit.ui.Panel" }
var m = desc.match(/^(\[[x ]?\])/),
txt = null;
if (m !== null) {
txt = desc.substring(m[1].length);
var ch = hasInstance && instance.clazz.Checkbox !== undefined ? new instance.clazz.Checkbox(txt)
: new pkg.Checkbox(txt);
ch.setValue(m[1].indexOf('x') > 0);
return ch;
} else {
m = desc.match(/^@\((.*)\)(\:[0-9]+x[0-9]+)?/);
if (m !== null) {
var path = m[1];
txt = desc.substring(path.length + 3 + (m[2] !== undefined ? m[2].length : 0)).trim();
var img = hasInstance && instance.clazz.ImagePan !== undefined ? new instance.clazz.ImagePan(path)
: new pkg.ImagePan(path);
if (m[2] !== undefined) {
var s = m[2].substring(1).split('x'),
w = parseInt(s[0], 10),
h = parseInt(s[1], 10);
img.setPreferredSize(w, h);
}
if (txt.length === 0) {
return img;
} else {
return hasInstance && instance.clazz.ImageLabel !== undefined ? new instance.clazz.ImageLabel(txt, img)
: new pkg.ImageLabel(txt, img);
}
} else {
return hasInstance && instance.clazz.Label !== undefined ? new instance.clazz.Label(desc)
: new pkg.Label(desc);
}
}
} else if (Array.isArray(desc)) {
if (desc.length > 0 && Array.isArray(desc[0])) {
var model = new zebkit.data.Matrix(desc.length, desc[0].length);
for(var row = 0; row < model.rows; row++) {
for(var col = 0; col < model.cols; col++) {
model.put(row, col, desc[row][col]);
}
}
return new pkg.grid.Grid(model);
} else {
var clz = hasInstance && instance.clazz.Combo !== undefined ? instance.clazz.Combo
: pkg.Combo,
combo = new clz(new clz.CompList(true)),
selectedIndex = -1;
for(var i = 0; i < desc.length; i++) {
var ss = desc[i];
if (zebkit.isString(ss)) {
if (selectedIndex === -1 && ss.length > 1 && ss[0] === '*') {
selectedIndex = i;
desc[i] = ss.substring(1);
}
}
combo.list.add(pkg.$component(desc[i], combo.list));
}
combo.select(selectedIndex);
return combo;
}
} else if (desc instanceof Image) {
return hasInstance && instance.clazz.ImagePan !== undefined ? new instance.clazz.ImagePan(desc)
: new pkg.ImagePan(desc);
} else if (zebkit.instanceOf(desc, zebkit.draw.View)) {
var v = hasInstance && instance.clazz.ViewPan !== undefined ? new instance.clazz.ViewPan()
: new pkg.ViewPan();
v.setView(desc);
return v;
} else if (zebkit.instanceOf(desc, pkg.Panel)) {
return desc;
} else {
throw new Error("Invalid component description '" + desc + "'");
}
};
/**
* Named views holder interface.
* @class zebkit.ui.HostDecorativeViews
* @interface zebkit.ui.HostDecorativeViews
*/
pkg.HostDecorativeViews = zebkit.Interface([
function $prototype() {
/**
* Set views set.
* @param {Object} v named views set.
* @method setViews
* @chainable
*/
this.setViews = function(v){
if (this.views === undefined) {
this.views = {};
}
var b = false;
for(var k in v) {
if (v.hasOwnProperty(k)) {
var nv = zebkit.draw.$view(v[k]);
if (this.views[k] !== nv) {
this.views[k] = nv;
b = true;
}
}
}
if (b === true) {
this.vrp();
}
return this;
};
}
]);
/**
* UI component to keep and render the given "zebkit.draw.View" class
* instance. The target view defines the component preferred size
* and the component view.
* @class zebkit.ui.ViewPan
* @constructor
* @extends zebkit.ui.Panel
*/
pkg.ViewPan = Class(pkg.Panel, [
function $prototype() {
/**
* Reference to a view that the component visualize
* @attribute view
* @type {zebkit.draw.View}
* @default null
* @readOnly
*/
this.view = null;
this.paint = function (g){
if (this.view !== null){
var l = this.getLeft(),
t = this.getTop();
this.view.paint(g, l, t, this.width - l - this.getRight(),
this.height - t - this.getBottom(), this);
}
};
/**
* Set the target view to be wrapped with the UI component
* @param {zebkit.draw.View|Function} v a view or a rendering
* view "paint(g,x,y,w,h,c)" function
* @method setView
* @chainable
*/
this.setView = function(v) {
var old = this.view;
v = zebkit.draw.$view(v);
if (v !== old) {
this.view = v;
this.notifyRender(old, v);
this.vrp();
}
return this;
};
/**
* Override the parent method to calculate preferred size basing on a target view.
* @param {zebkit.ui.Panel} t a target container
* @return {Object} return a target view preferred size if it is defined.
* The returned structure is the following:
*
* { width: {Integer}, height:{Integer} }
*
* @method calcPreferredSize
*/
this.calcPreferredSize = function(t) {
return this.view !== null ? this.view.getPreferredSize() : { width:0, height:0 };
};
}
]);
/**
* Image panel UI component class. The component renders an image.
* @param {String|Image} [img] a path or direct reference to an image object.
* If the passed parameter is string it considered as path to an image.
* In this case the image will be loaded using the passed path.
* @param {Integer} [w] a preferred with of the image
* @param {Integer} [h] a preferred height of the image
* @class zebkit.ui.ImagePan
* @constructor
* @extends zebkit.ui.ViewPan
*/
pkg.ImagePan = Class(pkg.ViewPan, [
function(img, w, h) {
this.setImage(arguments.length > 0 ? img : null);
this.$super();
if (arguments.length > 1) {
this.setPreferredSize(w, arguments < 3 ? w : h);
}
},
function $prototype() {
this.$runner = null;
/**
* Set image to be rendered in the UI component
* @method setImage
* @param {String|Image|zebkit.draw.Picture} img a path or direct reference to an
* image or zebkit.draw.Picture render.
* If the passed parameter is string it considered as path to an image.
* In this case the image will be loaded using the passed path
* @chainable
*/
this.setImage = function(img) {
var $this = this;
if (img !== null) {
var isPic = zebkit.instanceOf(img, zebkit.draw.Picture);
this.setView(isPic ? img : null);
this.$runner = zebkit.image(isPic ? img.target : img);
this.$runner.then(function(img) {
$this.$runner = null;
if (isPic === false) {
$this.setView(new zebkit.draw.Picture(img));
}
$this.vrp();
if ($this.imageLoaded !== undefined) {
$this.imageLoaded(img);
}
// fire imageLoaded event to children
for(var t = $this.parent; t !== null; t = t.parent){
if (t.childImageLoaded !== undefined) {
t.childImageLoaded(img);
}
}
}).catch(function(e) {
console.log(img);
zebkit.dumpError(e);
$this.$runner = null;
$this.setView(null);
});
} else {
if (this.$runner === null) {
this.setView(null);
} else {
this.$runner.then(function() {
$this.setView(null);
});
}
}
return this;
};
}
]);
/**
* Line UI component class. Draw series of vertical or horizontal lines of using
* the given line width and color. Vertical or horizontal line rendering s selected
* depending on the line component size: if height is greater than width than vertical
* line will be rendered.
* @constructor
* @param {String} [colors]* line colors
* @class zebkit.ui.Line
* @extends zebkit.ui.Panel
*/
pkg.Line = Class(pkg.Panel, [
function() {
/**
* Line colors
* @attribute colors
* @type {Array}
* @readOnly
* @default [ "gray" ]
*/
this.$super();
if (arguments.length > 0) {
this.setColors.apply(this, arguments);
}
},
function $prototype() {
/**
* Line colors set.
* @attribute colors
* @type {Array}
* @readOnly
* @default [ "gray" ]
*/
this.colors = [ "gray" ];
/**
* Line width
* @attribute lineWidth
* @type {Integer}
* @default 1
*/
this.lineWidth = 1;
/**
* Line direction attribute. Can be "vertical" or "horizontal" or null value.
* @attribute direction
* @type {String}
* @default null
*/
this.direction = null;
/**
* Set line color.
* @param {String} c a color
* @method setColor
* @chainable
* @readOnly
*/
this.setColor = function(c) {
this.setColors(c);
return this;
};
/**
* Set set of colors to be used to paint the line. Number of colors defines the number of
* lines to be painted.
* @param {String} colors* colors
* @method setLineColors
* @chainable
*/
this.setColors = function() {
this.colors = (arguments.length === 1) ? (Array.isArray(arguments[0]) ? arguments[0].slice(0)
: [ arguments[0] ] )
: Array.prototype.slice.call(arguments);
this.repaint();
return this;
};
/**
* Set the given line direction.
* @param {String} d a line direction. Can be "vertical" or "horizontal" or null value.
* null means auto detected direction.
* @method setDirection
*/
this.setDirection = function(d) {
if (d !== this.direction) {
this.direction = d;
this.vrp();
}
return this;
};
this.paint = function(g) {
var isHor = this.direction === null ? this.width > this.height
: this.direction === "horizontal",
left = this.getLeft(),
right = this.getRight(),
top = this.getTop(),
bottom = this.getBottom(),
xy = isHor ? top : left;
for(var i = 0; i < this.colors.length; i++) {
if (this.colors[i] !== null) {
g.setColor(this.colors[i]);
if (isHor === true) {
g.drawLine(this.left, xy, this.width - right - left, xy, this.lineWidth);
} else {
g.drawLine(xy, top, xy, this.height - top - bottom, this.lineWidth);
}
}
xy += this.lineWidth;
}
};
this.calcPreferredSize = function(target) {
var s = this.colors.length * this.lineWidth;
return { width: s, height:s};
};
}
]);
/**
* Label UI component class. The label can be used to visualize simple string or multi lines text or
* the given text render implementation:
*
* // render simple string
* var l = new zebkit.ui.Label("Simple string");
*
* // render multi lines text
* var l = new zebkit.ui.Label(new zebkit.data.Text("Multiline\ntext"));
*
* // render password text
* var l = new zebkit.ui.Label(new zebkit.draw.PasswordText("password"));
*
* @param {String|zebkit.data.TextModel|zebkit.draw.TextRender} [r] a text to be shown with the label.
* You can pass a simple string or an instance of a text model or an instance of text render as the
* text value.
* @class zebkit.ui.Label
* @constructor
* @extends zebkit.ui.ViewPan
*/
pkg.Label = Class(pkg.ViewPan, [
function (r) {
if (arguments.length === 0) {
this.setView(new zebkit.draw.StringRender(""));
} else {
// test if input string is string
if (typeof r === "string" || r.constructor === String) {
this.setView(r.length === 0 || r.indexOf('\n') >= 0 ? new zebkit.draw.TextRender(new zebkit.data.Text(r))
: new zebkit.draw.StringRender(r));
} else if (r.clazz !== undefined &&
r.getTextLength !== undefined && // a bit faster than instanceOf checking if
r.getLines !== undefined ) // test if this is an instance of zebkit.data.TextModel
{
this.setView(new zebkit.draw.TextRender(r));
} else {
this.setView(r);
}
}
this.$super();
},
function $prototype() {
/**
* Get the label text
* @return {String} a zebkit label text
* @method getValue
*/
this.getValue = function() {
return this.view.toString();
};
/**
* Set the text field text model
* @param {zebkit.data.TextModel|String} m a text model to be set
* @method setModel
* @chainable
*/
this.setModel = function(m) {
this.setView(zebkit.isString(m) ? new zebkit.draw.StringRender(m)
: new zebkit.draw.TextRender(m));
return this;
};
/**
* Get a text model
* @return {zebkit.data.TextModel} a text model
* @method getModel
*/
this.getModel = function() {
return this.view !== null ? this.view.target : null;
};
/**
* Get the label text color
* @return {String} a zebkit label color
* @method getColor
*/
this.getColor = function (){
return this.view.color;
};
/**
* Get the label text font
* @return {zebkit.Font} a zebkit label font
* @method getFont
*/
this.getFont = function (){
return this.view.font;
};
/**
* Set the label text value
* @param {String} s a new label text
* @method setValue
* @chainable
*/
this.setValue = function(s){
if (s === null) {
s = "";
}
var old = this.view.toString();
if (old !== s) {
this.view.setValue(s);
this.repaint();
}
return this;
};
/**
* Set the label text color
* @param {String} c a text color
* @method setColor
* @chainable
*/
this.setColor = function(c) {
var old = this.view.color;
if (old !== c) {
this.view.setColor(c);
this.repaint();
}
return this;
};
/**
* Set the label text font
* @param {zebkit.Font} f a text font
* @method setFont
* @chainable
*/
this.setFont = function(f) {
var old = this.view.font;
this.view.setFont.apply(this.view, arguments);
if (old != this.view.font) {
this.repaint();
}
return this;
};
}
]);
/**
* Shortcut class to render bold text in Label
* @param {String|zebkit.draw.TextRender|zebkit.data.TextModel} [t] a text string,
* text model or text render instance
* @constructor
* @class zebkit.ui.BoldLabel
* @extends zebkit.ui.Label
*/
pkg.BoldLabel = Class(pkg.Label, []);
/**
* Image label UI component. This is UI container that consists from an image
* component and an label component.Image is located at the left size of text.
* @param {Image|String} [img] an image or path to the image
* @param {String|zebkit.draw.TextRender|zebkit.data.TextModel} [txt] a text string,
* text model or text render instance
* @param {Integer} [w] an image preferred width
* @param {Integer} [h] an image preferred height
* @constructor
* @class zebkit.ui.ImageLabel
* @extends zebkit.ui.Panel
*/
pkg.ImageLabel = Class(pkg.Panel, [
function(txt, path, w, h) {
var img = null,
lab = null;
if (arguments.length > 0) {
lab = zebkit.instanceOf(txt, pkg.Panel) ? txt
: new this.clazz.Label(txt);
if (arguments.length > 1) {
img = zebkit.instanceOf(path, pkg.ImagePan) ? path
: new this.clazz.ImagePan(path);
if (arguments.length > 2) {
img.setPreferredSize(w, (arguments.length > 3 ? h : w));
}
}
}
// TODO: this is copy paste of Panel constructor to initialize fields that has to
// be used for adding child components. these components have to be added before
// properties() call. a bit dirty trick
if (this.kids === undefined) {
this.kids = [];
}
this.layout = new zebkit.layout.FlowLayout("left", "center", "horizontal", 6);
// add before panel constructor thanks to copy pasted code above
if (img !== null) {
this.add(img);
}
if (lab !== null) {
this.add(lab);
}
this.$super();
lab.setVisible(txt !== null);
},
function $clazz() {
this.ImagePan = Class(pkg.ImagePan, []);
this.Label = Class(pkg.Label, []);
},
function $prototype() {
/**
* Set the specified caption
* @param {String|zebkit.ui.Label} c a label text or component
* @method setValue
* @chainable
*/
this.setValue = function(c) {
var lab = this.getLabel();
if (zebkit.instanceOf(c, pkg.Label)) {
var i = -1;
if (lab !== null) {
i = this.indexOf(lab);
}
if (i >= 0) {
this.setAt(i, c);
}
} else {
lab.setValue(c);
lab.setVisible(c !== null);
}
return this;
};
/**
* Set the specified label image
* @param {String|Image} p a path to an image of image object
* @method setImage
* @chainable
*/
this.setImage = function(p) {
var image = this.getImagePan();
image.setImage(p);
image.setVisible(p !== null);
return this;
};
/**
* Get image panel.
* @return {zebkit.ui.ImagePan} an image panel.
* @method getImagePan
*/
this.getImagePan = function() {
return this.byPath("/~zebkit.ui.ImagePan");
};
/**
* Get label component.
* @return {zebkit.ui.ImagePan} a label component.
* @method getLabel
*/
this.getLabel = function(p) {
return this.byPath("/~zebkit.ui.Label");
};
/**
* Set the caption font
* @param {zebkit.Font} a font
* @method setFont
* @chainable
*/
this.setFont = function() {
var lab = this.getLabel();
if (lab !== null) {
lab.setFont.apply(lab, arguments);
}
return this;
};
/**
* Set the caption color
* @param {String} a color
* @method setColor
* @chainable
*/
this.setColor = function (c) {
var lab = this.getLabel();
if (lab !== null) {
lab.setColor(c);
}
return this;
};
/**
* Get caption value
* @return {zebkit.ui.Panel} a caption value
* @method getValue
*/
this.getValue = function () {
var lab = this.getLabel();
return lab === null ? null : lab.getValue();
};
/**
* Set the image alignment.
* @param {String} an alignment. Following values are possible:
*
* - "left"
* - "right"
* - "top"
* - "bottom"
*
* @method setImgAlignment
* @chainable
*/
this.setImgAlignment = function(a) {
var b = false,
img = this.getImagePan(),
i = this.indexOf(img);
if (a === "top" || a === "bottom") {
if (this.layout.direction !== "vertical") {
this.layout.direction = "vertical";
b = true;
}
} else if (a === "left" || a === "right") {
if (this.layout.direction !== "horizontal") {
this.layout.direction = "horizontal";
b = true;
}
}
if (this.layout.ax !== "center") {
this.layout.ax = "center";
b = true;
}
if (this.layout.ay !== "center") {
this.layout.ay = "center";
b = true;
}
if ((a === "top" || a === "left") && i !== 0 ) {
this.insert(null, 0, this.removeAt(i));
b = false;
} else if ((a === "bottom" || a === "right") && i !== 1) {
this.add(null, this.removeAt(i));
b = false;
}
if (b) {
this.vrp();
}
return this;
};
/**
* Set image preferred size.
* @param {Integer} w a width and height if the second argument has not been specified
* @param {Integer} [h] a height
* @method setImgPreferredSize
* @chainable
*/
this.setImgPreferredSize = function (w, h) {
if (arguments.length === 1) {
h = w;
}
this.getImagePan().setPreferredSize(w, h);
return this;
};
}
]);
/**
* Progress bar UI component class.
* @class zebkit.ui.Progress
* @constructor
* @param {String} [orient] an orientation of the progress bar. Use
* "vertical" or "horizontal" as the parameter value
* @extends zebkit.ui.Panel
*/
/**
* Fired when a progress bar value has been updated
*
* progress.on(function(src, oldValue) {
* ...
* });
*
* @event fired
* @param {zebkit.ui.Progress} src a progress bar that triggers
* the event
* @param {Integer} oldValue a progress bar previous value
*/
pkg.Progress = Class(pkg.Panel, [
function(orient) {
this.$super();
if (arguments.length > 0) {
this.setOrientation(orient);
}
},
function $prototype() {
/**
* Progress bar value
* @attribute value
* @type {Integer}
* @readOnly
*/
this.value = 0;
/**
* Progress bar element width
* @attribute barWidth
* @type {Integer}
* @readOnly
* @default 6
*/
this.barWidth = 6;
/**
* Progress bar element height
* @attribute barHeight
* @type {Integer}
* @readOnly
* @default 6
*/
this.barHeight = 6;
/**
* Gap between bar elements
* @default 2
* @attribute gap
* @type {Integer}
* @readOnly
*/
this.gap = 2;
/**
* Progress bar maximal value
* @attribute maxValue
* @type {Integer}
* @readOnly
* @default 20
*/
this.maxValue = 20;
/**
* Bar element view
* @attribute barView
* @readOnly
* @type {String|zebkit.draw.View}
* @default "blue"
*/
this.barView = "blue";
this.titleView = null;
/**
* Progress bar orientation
* @default "horizontal"
* @attribute orient
* @type {String}
* @readOnly
*/
this.orient = "horizontal";
this.paint = function(g){
var left = this.getLeft(),
right = this.getRight(),
top = this.getTop(),
bottom = this.getBottom(),
rs = (this.orient === "horizontal") ? this.width - left - right
: this.height - top - bottom,
barSize = (this.orient === "horizontal") ? this.barWidth
: this.barHeight;
if (rs >= barSize){
var vLoc = Math.floor((rs * this.value) / this.maxValue),
x = left,
y = this.height - bottom,
bar = this.barView,
wh = this.orient === "horizontal" ? this.height - top - bottom
: this.width - left - right;
while (x < (vLoc + left) && this.height - vLoc - bottom < y){
if (this.orient === "horizontal"){
bar.paint(g, x, top, barSize, wh, this);
x += (barSize + this.gap);
} else {
bar.paint(g, left, y - barSize, wh, barSize, this);
y -= (barSize + this.gap);
}
}
if (this.titleView !== null) {
var ps = this.barView.getPreferredSize();
this.titleView.paint(g, Math.floor((this.width - ps.width ) / 2),
Math.floor((this.height - ps.height) / 2),
ps.width, ps.height, this);
}
}
};
this.calcPreferredSize = function(l) {
var barSize = (this.orient === "horizontal") ? this.barWidth
: this.barHeight,
v1 = (this.maxValue * barSize) + (this.maxValue - 1) * this.gap,
ps = this.barView.getPreferredSize();
ps = (this.orient === "horizontal") ? {
width :v1,
height:(this.barHeight >= 0 ? this.barHeight
: ps.height)
}
: {
width:(this.barWidth >= 0 ? this.barWidth
: ps.width),
height: v1
};
if (this.titleView !== null) {
var tp = this.titleView.getPreferredSize();
ps.width = Math.max(ps.width, tp.width);
ps.height = Math.max(ps.height, tp.height);
}
return ps;
};
/**
* Set the progress bar orientation
* @param {String} o an orientation: "vertical" or "horizontal"
* @method setOrientation
* @chainable
*/
this.setOrientation = function(o) {
if (o !== this.orient) {
this.orient = zebkit.util.validateValue(o, "horizontal", "vertical");
this.vrp();
}
return this;
};
/**
* Set maximal integer value the progress bar value can rich
* @param {Integer} m a maximal value the progress bar value can rich
* @method setMaxValue
* @chainable
*/
this.setMaxValue = function(m) {
if (m !== this.maxValue) {
this.maxValue = m;
this.setValue(this.value);
this.vrp();
}
return this;
};
/**
* Set the current progress bar value
* @param {Integer} p a progress bar
* @method setValue
* @chainable
*/
this.setValue = function(p) {
p = p % (this.maxValue + 1);
if (this.value !== p){
var old = this.value;
this.value = p;
this.fire("fired", [this, old]);
this.repaint();
}
return this;
};
/**
* Set the given gap between progress bar element elements
* @param {Integer} g a gap
* @method setGap
* @chainable
*/
this.setGap = function(g) {
if (this.gap !== g){
this.gap = g;
this.vrp();
}
return this;
};
/**
* Set the progress bar element element view
* @param {zebkit.draw.View} v a progress bar element view
* @method setBarView
* @chainable
*/
this.setBarView = function(v) {
if (this.barView != v){
this.barView = zebkit.draw.$view(v);
this.vrp();
}
return this;
};
/**
* Set the progress bar element size
* @param {Integer} w a element width
* @param {Integer} h a element height
* @method setBarSize
* @chainable
*/
this.setBarSize = function(w, h) {
if (w !== this.barWidth && h !== this.barHeight){
this.barWidth = w;
this.barHeight = h;
this.vrp();
}
return this;
};
}
]).events("fired");
/**
* State panel class. The class is UI component that allows to customize
* the component face, background and border depending on the component
* state. Number and names of states the component can have is defined
* by developers. To bind a view to the specified state use zebkit.draw.ViewSet
* class. For instance if a component has to support two states : "state1" and
* "state2" you can do it as following:
*
* // create state component
* var p = new zebkit.ui.StatePan();
*
* // define border view that contains views for "state1" and "state2"
* p.setBorder({
* "state1": new zebkit.draw.Border("red", 1),
* "state2": new zebkit.draw.Border("blue", 2)
* });
*
* // define background view that contains views for "state1" and "state2"
* p.setBackground({
* "state1": "yellow",
* "state2": "green"
* });
*
* // set component state
* p.setState("state1");
*
* State component children components can listening when the state of the component
* has been updated by implementing "parentStateUpdated(o,n,id)" method. It gets old
* state, new state and a view id that is mapped to the new state. The feature is
* useful if we are developing a composite components whose children component also
* should react to a state changing.
* @class zebkit.ui.StatePan
* @constructor
* @extends zebkit.ui.ViewPan
*/
pkg.StatePan = Class(pkg.ViewPan, [
function $prototype() {
/**
* Current component state
* @attribute state
* @readOnly
* @default null
* @type {Object}
*/
this.state = null;
/**
* Set the component state
* @param {Object} s a state
* @method setState
* @chainable
*/
this.setState = function(s) {
if (s !== this.state){
var prev = this.state;
this.state = s;
this.stateUpdated(prev, s);
}
return this;
};
/**
* Define the method if the state value has to be
* somehow converted to a view id. By default the state value
* itself is used as a view id.
* @param {Object} s a state to be converted
* @return {String} a view ID
* @method toViewId
*/
this.toViewId = function(st) {
return st;
};
/**
* Called every time the component state has been updated
* @param {Integer} o a previous component state
* @param {Integer} n a new component state
* @method stateUpdated
*/
this.stateUpdated = function(o, n) {
var b = false,
id = this.toViewId(n);
if (id !== null) {
for(var i = 0; i < this.kids.length; i++) {
var kid = this.kids[i];
if (kid.setState !== undefined) {
kid.setState(id);
}
}
if (this.border !== null && this.border.activate !== undefined) {
b = this.border.activate(id, this) === true || b;
}
if (this.view !== null && this.view.activate !== undefined) {
b = this.view.activate(id, this) === true || b;
}
if (this.bg !== null && this.bg.activate !== undefined) {
b = this.bg.activate(id, this) === true || b;
}
if (b) {
this.repaint();
}
}
// TODO: code to support potential future state update listener support
if (this._ !== undefined && this._.stateUpdated !== undefined) {
this._.stateUpdated(this, o, n, id);
}
};
/**
* Refresh state
* @protected
* @method syncState
*/
this.syncState = function() {
this.stateUpdated(this.state, this.state);
};
},
function setView(v) {
if (v !== this.view) {
this.$super(v);
// check if the method called after constructor execution
// otherwise sync is not possible
if (this.kids !== undefined) {
this.syncState(this.state, this.state);
}
}
return this;
},
function setBorder(v) {
if (v !== this.border) {
this.$super(v);
this.syncState(this.state, this.state);
}
return this;
},
function setBackground(v) {
if (v !== this.bg) {
this.$super(v);
this.syncState(this.state, this.state);
}
return this;
},
function setEnabled(b) {
this.$super(b);
this.setState(b ? "out" : "disabled");
return this;
}
]);
// TODO: probably should be removed
/**
* Input events state panel.
* @class zebkit.ui.EvStatePan
* @extends zebkit.ui.StatePan
* @uses zebkit.ui.event.TrackInputEventState
* @uses zebkit.ui.StatePan
* @constructor
*/
pkg.EvStatePan = Class(pkg.StatePan, pkg.event.TrackInputEventState, []);
/**
* Interface to add focus marker rendering. Focus marker is drawn either over
* the component space or around the specified anchor child component.
* @class zebkit.ui.DrawFocusMarker
* @interface zebkit.ui.DrawFocusMarker
*/
pkg.DrawFocusMarker = zebkit.Interface([
function $prototype() {
/**
* Component that has to be used as focus indicator anchor
* @attribute focusComponent
* @type {zebkit.ui.Panel}
* @default null
* @readOnly
*/
this.focusComponent = null;
/**
* Reference to an anchor focus marker component
* @attribute focusMarkerView
* @readOnly
* @type {zebkit.draw.View}
*/
this.focusMarkerView = null;
/**
* Focus marker vertical and horizontal gaps.
* @attribute focusMarkerGaps
* @type {Integer}
* @default 2
*/
this.focusMarkerGaps = 2;
this.paintOnTop = function(g) {
var fc = this.focusComponent;
if (this.focusMarkerView !== null && fc !== null && this.hasFocus()) {
if (fc === this) {
this.focusMarkerView.paint(g, this.focusMarkerGaps,
this.focusMarkerGaps,
fc.width - this.focusMarkerGaps * 2,
fc.height - this.focusMarkerGaps * 2,
this);
} else {
this.focusMarkerView.paint(g, fc.x - this.focusMarkerGaps,
fc.y - this.focusMarkerGaps,
this.focusMarkerGaps * 2 + fc.width,
this.focusMarkerGaps * 2 + fc.height,
this);
}
}
};
/**
* Set the view that has to be rendered as focus marker when the component gains focus.
* @param {String|zebkit.draw.View|Function} c a view.
* The view can be a color or border string code or view
* or an implementation of zebkit.draw.View "paint(g,x,y,w,h,t)" method.
* @method setFocusMarkerView
* @chainable
*/
this.setFocusMarkerView = function(c) {
if (c != this.focusMarkerView){
this.focusMarkerView = zebkit.draw.$view(c);
this.repaint();
}
return this;
};
/**
* Says if the component can hold focus or not
* @param {Boolean} b true if the component can gain focus
* @method setCanHaveFocus
*/
this.setCanHaveFocus = function(b){
if (this.canHaveFocus !== b) {
var fm = pkg.focusManager;
if (b === false && fm.focusOwner === this) {
fm.requestFocus(null);
}
this.canHaveFocus = b;
}
return this;
};
/**
* Set the specified children component to be used as focus marker view anchor
* component. Anchor component is a component over that the focus marker view
* is painted.
* @param {zebkit.ui.Panel} c an anchor component
* @method setFocusAnchorComponent
* @chainable
*/
this.setFocusAnchorComponent = function(c) {
if (this.focusComponent !== c) {
if (c !== this && c !== null && this.kids.indexOf(c) < 0) {
throw new Error("Focus component doesn't exist");
}
this.focusComponent = c;
this.repaint();
}
return this;
};
},
function focused() {
this.$super();
this.repaint();
},
function kidRemoved(i, l, ctr){
if (l === this.focusComponent) {
this.focusComponent = null;
}
this.$super(i, l, ctr);
}
]);
/**
* Special interface that provides set of method for state components to implement repeatable
* state.
* @class zebkit.ui.FireEventRepeatedly
* @interface zebkit.ui.FireEventRepeatedly
*/
pkg.FireEventRepeatedly = zebkit.Interface([
function $prototype() {
/**
* Indicate if the button should
* fire event by pressed event
* @attribute isFireByPress
* @type {Boolean}
* @default false
* @readOnly
*/
this.isFireByPress = false;
/**
* Fire button event repeating period. -1 means
* the button event repeating is disabled.
* @attribute firePeriod
* @type {Integer}
* @default -1
* @readOnly
*/
this.firePeriod = -1;
/**
* Indicates a time the repeat state events have to start in
* @attribute startIn
* @type {Integer}
* @readOnly
* @default 400
*/
this.startIn = 400;
/**
* Task that has been run to support repeatable "fired" event.
* @attribute $repeatTask
* @type {zebkit.util.Task}
* @private
*/
this.$repeatTask = null;
/**
* Set the mode the button has to fire events. Button can fire
* event after it has been unpressed or immediately when it has
* been pressed. Also button can start firing events periodically
* when it has been pressed and held in the pressed state.
* @param {Boolean} b true if the button has to fire event by
* pressed event
* @param {Integer} firePeriod the period of time the button
* has to repeat firing events if it has been pressed and
* held in pressed state. -1 means event doesn't have
* repeated
* @param {Integer} [startIn] the timeout when repeat events
* has to be initiated
* @method setFireParams
*/
this.setFireParams = function (b, firePeriod, startIn){
if (this.$repeatTask !== null) {
this.$repeatTask.shutdown();
}
this.isFireByPress = b;
this.firePeriod = firePeriod;
if (arguments.length > 2) {
this.startIn = startIn;
}
return this;
};
this.$fire = function() {
this.fire("fired", [ this ]);
if (this.fired !== undefined) {
this.fired();
}
};
},
function stateUpdated(o,n){
this.$super(o, n);
if (n === "pressed.over") {
if (this.isFireByPress === true){
this.$fire();
if (this.firePeriod > 0) {
var $this = this;
this.$repeatTask = zebkit.util.tasksSet.run(function() {
if ($this.state === "pressed.over") {
$this.$fire();
}
},
this.startIn,
this.firePeriod
);
}
}
} else {
if (this.firePeriod > 0 && this.$repeatTask !== null) {
this.$repeatTask.shutdown();
}
if (n === "over" && (o === "pressed.over" && this.isFireByPress === false)) {
this.$fire();
}
}
}
]);
/**
* Button UI component. Button is composite component whose look and feel can
* be easily customized:
*
* // create image button
* var button = new zebkit.ui.Button(new zebkit.ui.ImagePan("icon1.gif"));
*
* // create image + caption button
* var button = new zebkit.ui.Button(new zebkit.ui.ImageLabel("Caption", "icon1.gif"));
*
* // create multilines caption button
* var button = new zebkit.ui.Button("Line1\nLine2");
*
*
* @class zebkit.ui.Button
* @constructor
* @param {String|zebkit.ui.Panel|zebkit.draw.View} [t] a button label.
* The label can be a simple text or an UI component.
* @extends zebkit.ui.EvStatePan
* @uses zebkit.ui.FireEventRepeatedly
* @uses zebkit.ui.DrawFocusMarker
*/
/**
* Fired when a button has been pressed
*
* var b = new zebkit.ui.Button("Test");
* b.on(function (src) {
* ...
* });
*
* Button can be adjusted in respect how it generates the pressed event. Event can be
* triggered by pressed or clicked even. Also event can be generated periodically if
* the button is kept in pressed state.
* @event fired
* @param {zebkit.ui.Button} src a button that has been pressed
*
*/
pkg.Button = Class(pkg.EvStatePan, pkg.DrawFocusMarker, pkg.FireEventRepeatedly, [
function(t) {
this.$super();
if (arguments.length > 0 && t !== null) {
t = pkg.$component(t, this);
this.add(t);
this.setFocusAnchorComponent(t);
}
},
function $clazz() {
this.Label = Class(pkg.Label, []);
this.ViewPan = Class(pkg.ViewPan, [
function(v) {
this.$super();
this.setView(v);
},
function $prototype() {
this.setState = function(id) {
if (this.view !== null && this.view.activate !== undefined) {
this.activate(id);
}
};
}
]);
this.ImageLabel = Class(pkg.ImageLabel, []);
},
function $prototype() {
/**
* Indicates the component can have focus
* @attribute canHaveFocus
* @type {Boolean}
* @default true
*/
this.canHaveFocus = true;
this.catchInput = true;
}
]).events("fired");
/**
* Group class to help managing group of element where only one can be on.
*
* // create group of check boxes that will work as a radio group
* var gr = new zebkit.ui.Group();
* var ch1 = new zebkit.ui.Checkbox("Test 1", gr);
* var ch2 = new zebkit.ui.Checkbox("Test 2", gr);
* var ch3 = new zebkit.ui.Checkbox("Test 3", gr);
*
* @class zebkit.ui.Group
* @uses zebkit.EventProducer
* @param {Boolean} [un] indicates if group can have no one item selected.
* @constructor
*/
pkg.Group = Class([
function(un) {
this.selected = null;
if (arguments.length > 0) {
this.allowNoneSelected = un;
}
},
function $prototype() {
/**
* indicates if group can have no one item selected.
* @attribute allowNoneSelected
* @readOnly
* @type {Boolean}
* @default false
*/
this.allowNoneSelected = false;
this.$group = null;
this.$locked = false;
this.$allowValueUpdate = function(src) {
if (this.$group === null || this.$group.indexOf(src) < 0) {
throw new Error("Component is not the group member");
}
return (this.selected !== src ||
src.getValue() === false ||
this.allowNoneSelected === true);
};
this.attach = function(c) {
if (this.$group === null) {
this.$group = [];
}
if (this.$group.indexOf(c) >= 0) {
throw new Error("Duplicated group element");
}
if (this.selected !== null && c.getValue() === true) {
c.setValue(false);
}
c.on(this);
this.$group.push(c);
};
this.detach = function(c) {
if (this.$group === null || this.$group.indexOf(c) < 0) {
throw new Error("Component is not the group member");
}
if (this.selected !== null && c.getValue() === true) {
c.setValue(false);
}
c.off(this);
var i = this.$group.indexOf(c);
this.$group.splice(i, 1);
if (this.selected === c) {
if (this.allowNoneSelected !== true && this.$group.length > 0) {
this.$group[i % this.$group.length].setValue(true);
}
this.selected = null;
}
};
this.fired = function(c) {
if (this.$locked !== true) {
try {
this.$locked = true;
var b = c.getValue(),
old = this.selected;
if (this.allowNoneSelected && b === false && this.selected !== null) {
this.selected = null;
this.updated(old);
} else if (b && this.selected !== c) {
this.selected = c;
if (old !== null) {
old.setValue(false);
}
this.updated(old);
}
} finally {
this.$locked = false;
}
}
};
this.updated = function(old) {
this.fire("selected", [this, this.selected, old]);
};
}
]).events("selected");
/**
* Check-box UI component. The component is a container that consists from two other UI components:
*
* - Box component to keep checker indicator
* - Label component to paint label
*
* Developers are free to customize the component as they want. There is no limitation regarding
* how the box and label components have to be laid out, which UI components have to be used as
* the box or label components, etc. The check box extends state panel component and re-map states
* to own views IDs:
*
* - **"pressed.out"** - checked and pointer cursor is out
* - **"out"** - un-checked and pointer cursor is out
* - **"pressed.disabled"** - disabled and checked,
* - **"disabled"** - disabled and un-checked ,
* - **"pressed.over"** - checked and pointer cursor is over
* - **"over"** - un-checked and pointer cursor is out
*
*
* Customize is quite similar to what explained for zebkit.ui.EvStatePan:
*
*
* // create checkbox component
* var ch = new zebkit.ui.Checkbox("Checkbox");
*
* // change border when the component checked to green
* // otherwise set it to red
* ch.setBorder(new zebkit.draw.ViewSet({
* "*": new zebkit.draw.Border("red"),
* "pressed.*": new zebkit.draw.Border("green")
* }));
*
* // customize checker box children UI component to show
* // green for checked and red for un-cheked states
* ch.kids[0].setView(new zebkit.draw.ViewSet({
* "*": "red",
* "pressed.*": "green"
* }));
* // sync current state with new look and feel
* ch.syncState();
*
* Listening checked event should be done by registering a listener in the check box switch manager
* as follow:
*
* // create check box component
* var ch = new zebkit.ui.Checkbox("Checkbox");
*
* // register a check box listener
* ch.on(function(src) {
* var s = src.getValue();
* ...
* });
*
* @class zebkit.ui.Checkbox
* @extends zebkit.ui.EvStatePan
* @uses zebkit.ui.DrawFocusMarker
* @constructor
* @param {String|zebkit.ui.Panel} [label] a label
*/
pkg.Checkbox = Class(pkg.EvStatePan, pkg.DrawFocusMarker, [
function (c) {
if (arguments.length > 0 && c !== null && zebkit.isString(c)) {
c = new this.clazz.Label(c);
}
this.$super();
/**
* Reference to box component
* @attribute box
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.box = new this.clazz.Box();
this.add(this.box);
if (arguments.length > 0 && c !== null) {
this.add(c);
this.setFocusAnchorComponent(c);
}
},
function $clazz() {
/**
* The box UI component class that is used by default with the check box component.
* @constructor
* @class zebkit.ui.Checkbox.Box
* @extends zebkit.ui.ViewPan
*/
this.Box = Class(pkg.StatePan, []);
/**
* @for zebkit.ui.Checkbox
*/
this.Label = Class(pkg.Label, []);
},
function $prototype() {
/**
* Check box state
* @attribute value
* @type {Boolean}
* @readOnly
* @protected
*/
this.value = false;
this.$group = null;
this.catchInput = true;
/**
* Called every time the check box state has been updated
* @param {zebkit.ui.Checkbox} ch a check box that has triggered the swicth
* @method switched
*/
/**
* Set the check box state.
* @param {Boolean} v a state of the check box
* @method setValue
* @chainable
*/
this.setValue = function(v) {
if (this.value !== v && (this.$group === null || this.$group.$allowValueUpdate(this))) {
this.value = v;
this.stateUpdated(this.state, this.state);
if (this.switched !== undefined) {
this.switched(this);
}
this.fire("fired", this);
}
return this;
};
/**
* Get the check box state
* @return {Boolean} a state
* @method getValue
*/
this.getValue = function() {
return this.value;
};
/**
* Toggle check box state.
* @method toggle
* @chainable
*/
this.toggle = function() {
this.setValue(this.value !== true);
return this;
};
/**
* Map the specified state into its symbolic name.
* @protected
* @param {String} state a state
* @return {String} a symbolic name of the state
* @method toViewId
*/
this.toViewId = function(state){
if (this.isEnabled === true) {
return this.getValue() ? (state === "over" ? "pressed.over" : "pressed.out")
: (state === "over" ? "over" : "out");
} else {
return this.getValue() ? "pressed.disabled" : "disabled";
}
};
/**
* Attach the given check box tho the specified group
* @param {zebkit.ui.Group} g a group
* @method setGroup
* @chainable
*/
this.setGroup = function(g) {
if (this.$group !== null) {
this.$group.detach(this);
this.$group = null;
}
if (this.$group !== g) {
this.$group = g;
if (this.$group !== null) {
this.$group.attach(this);
}
}
return this;
};
},
function stateUpdated(o, n) {
if (o === "pressed.over" && n === "over") {
this.toggle();
}
this.$super(o, n);
},
function kidRemoved(index, c, ctr) {
if (this.box === c) {
this.box = null;
}
this.$super(index,c);
},
function keyPressed(e){
if (this.$group !== null && this.getValue()){
var d = 0;
if (e.code === "ArrowLeft" || e.code === "ArrowUp") {
d = -1;
} else if (e.code === "ArrowRight" || e.code === "ArrowDown") {
d = 1;
}
if (d !== 0) {
var p = this.parent;
for(var i = p.indexOf(this) + d; i < p.kids.length && i >= 0; i += d) {
var l = p.kids[i];
if (l.isVisible === true &&
l.isEnabled === true &&
l.$group === this.$group )
{
l.requestFocus();
l.setValue(true);
break;
}
}
return ;
}
}
this.$super(e);
}
]).events("fired");
/**
* Radio-box UI component class. This class is extension of "zebkit.ui.Checkbox" class that sets group
* as a default switch manager. The other functionality id identical to check box component. Generally
* speaking this class is a shortcut for radio box creation.
* @class zebkit.ui.Radiobox
* @constructor
* @param {String|zebkit.ui.Panel} [label] a label
* @param {zebkit.ui.Group} [m] a group
* @extends zebkit.ui.Checkbox
*/
pkg.Radiobox = Class(pkg.Checkbox, [
function(lab, group) {
if (arguments.length > 0) {
if (zebkit.instanceOf(lab, pkg.Group)) {
group = lab;
lab = null;
}
this.$super(lab);
} else {
this.$super();
}
if (group !== undefined) {
this.setGroup(group);
}
}
]);
/**
* UI link component class.
* @class zebkit.ui.Link
* @param {String} s a link text
* @constructor
* @extends zebkit.ui.Button
*/
pkg.Link = Class(pkg.Button, [
function(s) {
// do it before super
this.view = new zebkit.draw.DecoratedTextRender(s);
this.overDecoration = "underline";
this.$super(null);
// if colors have not been set with default property set it here
if (this.colors === null) {
this.colors = {
"pressed.over" : "blue",
"out" : "white",
"over" : "white",
"pressed.out" : "black",
"disabled" : "gray"
};
}
this.stateUpdated(this.state, this.state);
},
function $prototype() {
this.colors = null;
/**
* Mouse cursor type.
* @attribute cursorType
* @default zebkit.ui.Cursor.HAND;
* @type {String}
* @readOnly
*/
this.cursorType = pkg.Cursor.HAND;
/**
* Set link font
* @param {zebkit.Font} f a font
* @method setFont
* @chainable
*/
this.setFont = function(f) {
var old = this.view !== null ? this.view.font
: null;
this.view.setFont.apply(this.view, arguments);
if (old !== this.view.font) {
this.vrp();
}
return this;
};
/**
* Set the link text color for the specified link state
* @param {String} state a link state
* @param {String} c a link text color
* @method setColor
* @chainable
*/
this.setColor = function(state,c){
if (this.colors[state] !== c){
this.colors[state] = c;
this.syncState();
}
return this;
};
this.setColors = function(colors) {
this.colors = zebkit.clone(colors);
this.syncState();
return this;
};
this.setValue = function(s) {
this.view.setValue(s.toString());
this.repaint();
return this;
};
},
function stateUpdated(o, n){
this.$super(o, n);
var k = this.toViewId(n),
b = false;
if (this.view !== null &&
this.view.color !== this.colors[k] &&
this.colors[k] !== null &&
this.colors[k] !== undefined)
{
this.view.setColor(this.colors[k]);
b = true;
}
if (zebkit.instanceOf(this.view, zebkit.draw.DecoratedTextRender) && this.isEnabled === true) {
if (n === "over") {
this.view.addDecorations(this.overDecoration);
b = true;
} else if (this.view.hasDecoration(this.overDecoration)) {
this.view.clearDecorations(this.overDecoration);
b = true;
}
}
if (b) {
this.repaint();
}
}
]);
// cannot be declared in Button.$clazz since Link appears later and link inherits Button class
pkg.Button.Link = Class(pkg.Link, []);
/**
* Toolbar UI component. Handy way to place number of click able elements
* @class zebkit.ui.Toolbar
* @constructor
* @extends zebkit.ui.Panel
*/
/**
* Fired when a toolbar element has been pressed
*
* var t = new zebkit.ui.Toolbar();
*
* // add three pressable icons
* t.addImage("icon1.jpg");
* t.addImage("icon2.jpg");
* t.addLine();
* t.addImage("ico3.jpg");
*
* // catch a toolbar icon has been pressed
* t.on(function (src) {
* ...
* });
*
* @event pressed
* @constructor
* @param {zebkit.ui.Panel} src a toolbar element that has been pressed
*/
pkg.Toolbar = Class(pkg.Panel, [
function $clazz() {
this.ToolPan = Class(pkg.EvStatePan, [
function(c) {
this.$super(new zebkit.layout.BorderLayout());
this.add("center", c);
},
function getContentComponent() {
return this.kids[0];
},
function stateUpdated(o, n) {
this.$super(o, n);
if (o === "pressed.over" && n === "over") {
this.parent.fire("fired", [ this.parent, this.getContentComponent() ]);
}
}
]);
this.ImagePan = Class(pkg.ImagePan, []);
this.Line = Class(pkg.Line, []);
this.Checkbox = Class(pkg.Checkbox, []);
this.Radiobox = Class(pkg.Radiobox, []);
// TODO: combo is not available in this module yet
// ui + ui.list has to be combined as one package
//this.Combo = Class(pkg.Combo, []);
},
function $prototype() {
/**
* Test if the given component is a decorative element
* in the toolbar
* @param {zebkit.ui.Panel} c a component
* @return {Boolean} return true if the component is
* decorative element of the toolbar
* @method isDecorative
* @protected
*/
this.isDecorative = function(c){
return zebkit.instanceOf(c, pkg.StatePan) === false;
};
/**
* Add a radio box as the toolbar element that belongs to the
* given group and has the specified content component
* @param {zebkit.ui.Group} g a radio group the radio box belongs
* @param {zebkit.ui.Panel} c a content
* @return {zebkit.ui.Panel} a component that has been added
* @method addRadio
*/
this.addRadio = function(g,c) {
var cbox = new this.clazz.Radiobox(c, g);
cbox.setCanHaveFocus(false);
return this.add(cbox);
};
/**
* Add a check box as the toolbar element with the specified content
* component
* @param {zebkit.ui.Panel} c a content
* @return {zebkit.ui.Panel} a component that has been added
* @method addSwitcher
*/
this.addSwitcher = function(c){
var cbox = new this.clazz.Checkbox(c);
cbox.setCanHaveFocus(false);
return this.add(cbox);
};
/**
* Add an image as the toolbar element
* @param {String|Image} img an image or a path to the image
* @return {zebkit.ui.Panel} a component that has been added
* @method addImage
*/
this.addImage = function(img) {
this.validateMetric();
return this.add(new this.clazz.ImagePan(img));
};
/**
* Add line to the toolbar component. Line is a decorative ]
* element that logically splits toolbar elements. Line as any
* other decorative element doesn't fire event
* @return {zebkit.ui.Panel} a component that has been added
* @method addLine
*/
this.addLine = function(){
var line = new this.clazz.Line();
line.constraints = "stretch";
return this.addDecorative(line);
};
},
/**
* Add the given component as decorative element of the toolbar.
* Decorative elements don't fire event and cannot be pressed
* @param {zebkit.ui.Panel} c a component
* @return {zebkit.ui.Panel} a component that has been added
* @method addDecorative
*/
function addDecorative(c) {
return this.$getSuper("insert").call(this, this.kids.length, null, c);
},
function insert(i,id,d){
if (d === "-") {
var line = new this.clazz.Line();
line.constraints = "stretch";
return this.$super(i, null, line);
} else if (Array.isArray(d)) {
d = new this.clazz.Combo(d);
}
return this.$super(i, id, new this.clazz.ToolPan(d));
}
]).events("fired");
pkg.ApplyStateProperties = zebkit.Interface([
function $prototype() {
this.stateProperties = null;
/**
* Set properties set for the states
* @param {Object} s a states
* @method setStateProperties
* @chainable
*/
this.setStateProperties = function(s) {
this.stateProperties = zebkit.clone(s);
this.syncState();
this.vrp();
return this;
};
},
function stateUpdated(o, n) {
this.$super(o, n);
if (this.stateProperties !== undefined &&
this.stateProperties !== null )
{
if (this.stateProperties[n] !== undefined &&
this.stateProperties[n] !== null )
{
zebkit.properties(this,
this.stateProperties[n]);
} else if (this.stateProperties["*"] !== undefined &&
this.stateProperties["*"] !== null )
{
zebkit.properties(this,
this.stateProperties["*"]);
}
}
}
]);
/**
* Arrow button component.
* @class zebkit.ui.ArrowButton
* @constructor
* @param {String} direction an arrow icon direction. Use "left", "right", "top", "bottom" as
* the parameter value.
* @extends zebkit.ui.Button
*/
pkg.ArrowButton = Class(pkg.Button, pkg.ApplyStateProperties, [
function(direction) {
this.view = new zebkit.draw.ArrowView();
this.$super();
this.syncState();
},
function $prototype() {
this.cursorType = pkg.Cursor.HAND;
this.canHaveFocus = false;
/**
* Set arrow direction. Use one of the following values: "top",
* "left", "right" or "bottom" as the argument value.
* @param {String} d an arrow direction
* @method setDirection
* @chainable
*/
this.setDirection = function(d) {
if (this.view.direction !== d) {
this.view.direction = d;
this.repaint();
}
return this;
};
this.setStretchArrow = function(b) {
if (this.view.stretched !== b) {
this.view.stretched = b;
this.repaint();
}
return this;
};
this.setArrowSize = function(w, h) {
if (arguments.length === 1) {
h = w;
}
if (this.view.width !== w || this.view.height !== h) {
this.view.width = w;
this.view.height = h;
this.vrp();
}
return this;
};
this.setFillColor = function(c) {
if (this.view.fillColor !== c) {
this.view.fillColor = c;
this.repaint();
}
return this;
};
this.setColor = function(c) {
if (this.view.color !== c) {
this.view.color = c;
this.repaint();
}
return this;
};
this.setLineSize = function(s) {
if (this.view.lineSize !== s) {
this.view.lineSize = s;
this.repaint();
}
return this;
};
}
]);
/**
* Scroll manager class.
* @param {zebkit.ui.Panel} t a target component to be scrolled
* @constructor
* @class zebkit.ui.ScrollManager
* @uses zebkit.EventProducer
*/
/**
* Fired when a target component has been scrolled
*
* scrollManager.on(function(px, py) {
* ...
* });
*
* @event scrolled
* @param {Integer} px a previous x location target component scroll location
* @param {Integer} py a previous y location target component scroll location
*/
/**
* Fired when a scroll state has been updated
*
* scrollManager.scrollStateUpdated = function(x, y, px, py) {
* ...
* };
*
* @event scrollStateUpdated
* @param {Integer} x a new x location target component scroll location
* @param {Integer} y a new y location target component scroll location
* @param {Integer} px a previous x location target component scroll location
* @param {Integer} py a previous y location target component scroll location
*/
pkg.ScrollManager = Class([
function(c) {
/**
* Target UI component for that the scroll manager has been instantiated
* @attribute target
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.target = c;
},
function $prototype() {
this.sx = this.sy = 0;
/**
* Get current target component x scroll location
* @return {Integer} a x scroll location
* @method getSX
*/
this.getSX = function() {
return this.sx;
};
/**
* Get current target component y scroll location
* @return {Integer} a y scroll location
* @method getSY
*/
this.getSY = function() {
return this.sy;
};
/**
* Set a target component scroll x location to the
* specified value
* @param {Integer} v a x scroll location
* @method scrollXTo
*/
this.scrollXTo = function(v){
this.scrollTo(v, this.getSY());
};
/**
* Set a target component scroll y location to the
* specified value
* @param {Integer} v a y scroll location
* @method scrollYTo
*/
this.scrollYTo = function(v){
this.scrollTo(this.getSX(), v);
};
/**
* Scroll the target component into the specified location
* @param {Integer} x a x location
* @param {Integer} y a y location
* @method scrollTo
*/
this.scrollTo = function(x, y){
var psx = this.getSX(),
psy = this.getSY();
if (psx !== x || psy !== y){
this.sx = x;
this.sy = y;
if (this.scrollStateUpdated !== undefined) {
this.scrollStateUpdated(x, y, psx, psy);
}
if (this.target.catchScrolled !== undefined) {
this.target.catchScrolled(psx, psy);
}
// TODO: a bit faster then .fire("scrolled", [this, psx, psy])
if (this._ !== undefined) {
this._.scrolled(this, psx, psy);
}
}
return this;
};
/**
* Make visible the given rectangular area of the
* scrolled target component
* @param {Integer} x a x coordinate of top left corner
* of the rectangular area
* @param {Integer} y a y coordinate of top left corner
* of the rectangular area
* @param {Integer} w a width of the rectangular area
* @param {Integer} h a height of the rectangular area
* @method makeVisible
* @chainable
*/
this.makeVisible = function(x,y,w,h){
var p = pkg.calcOrigin(x, y, w, h, this.getSX(), this.getSY(), this.target);
this.scrollTo(p[0], p[1]);
return this;
};
}
]).events("scrolled");
/**
* Scroll bar UI component
* @param {String} [t] orientation of the scroll bar components:
"vertical" - vertical scroll bar
"horizontal"- horizontal scroll bar
* @class zebkit.ui.Scroll
* @constructor
* @extends zebkit.ui.Panel
* @uses zebkit.util.Position.Metric
*/
pkg.Scroll = Class(pkg.Panel, zebkit.util.Position.Metric, [
function(t) {
if (arguments.length > 0) {
this.orient = zebkit.util.validateValue(t, "vertical", "horizontal");
}
/**
* Increment button
* @attribute incBt
* @type {zebkit.ui.Button}
* @readOnly
*/
/**
* Decrement button
* @attribute decBt
* @type {zebkit.ui.Button}
* @readOnly
*/
/**
* Scroll bar thumb component
* @attribute thumb
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.thumbLoc = 0;
this.startDragLoc = Number.MAX_VALUE;
this.$super(this);
var b = (this.orient === "vertical");
this.add("center", b ? new pkg.Scroll.VerticalThumb() : new pkg.Scroll.HorizontalThumb());
this.add("top" , b ? new pkg.Scroll.BottomArrowButton() : new pkg.Scroll.RightArrowButton());
this.add("bottom", b ? new pkg.Scroll.TopArrowButton() : new pkg.Scroll.LeftArrowButton());
this.setPosition(new zebkit.util.SingleColPosition(this));
},
function $clazz() {
this.isDragable = true;
this.ArrowButton = Class(pkg.ArrowButton, [
function $prototype() {
this.isFireByPress = true;
this.firePeriod = 20;
}
]);
this.ArrowButton.inheritProperties = true;
this.TopArrowButton = Class(this.ArrowButton, []);
this.BottomArrowButton = Class(this.ArrowButton, []);
this.LeftArrowButton = Class(this.ArrowButton, []);
this.RightArrowButton = Class(this.ArrowButton, []);
this.VerticalThumb = Class(pkg.ViewPan, []);
this.HorizontalThumb = Class(pkg.ViewPan, []);
},
function $prototype() {
this.MIN_BUNDLE_SIZE = 16;
this.incBt = this.decBt = this.thumb = this.position = null;
/**
* Maximal possible value
* @attribute max
* @type {Integer}
* @readOnly
* @default 100
*/
this.extra = this.max = 100;
/**
* Page increment value
* @attribute pageIncrement
* @type {Integer}
* @readOnly
* @default 20
*/
this.pageIncrement = 20;
/**
* Unit increment value
* @attribute unitIncrement
* @type {Integer}
* @readOnly
* @default 5
*/
this.unitIncrement = 5;
/**
* Scroll orientation.
* @attribute orient
* @type {String}
* @readOnly
* @default "vertical"
*/
this.orient = "vertical";
/**
* Evaluate if the given point is in scroll bar thumb element
* @param {Integer} x a x location
* @param {Integer} y a y location
* @return {Boolean} true if the point is located inside the
* scroll bar thumb element
* @method isInThumb
*/
this.isInThumb = function(x,y){
var bn = this.thumb;
return (bn !== null &&
bn.isVisible === true &&
bn.x <= x && bn.y <= y &&
bn.x + bn.width > x &&
bn.y + bn.height > y);
};
this.amount = function(){
var db = this.decBt;
return (this.orient === "vertical") ? this.incBt.y - db.y - db.height
: this.incBt.x - db.x - db.width;
};
this.pixel2value = function(p) {
var db = this.decBt;
return (this.orient === "vertical") ? Math.floor((this.max * (p - db.y - db.height)) / (this.amount() - this.thumb.height))
: Math.floor((this.max * (p - db.x - db.width )) / (this.amount() - this.thumb.width));
};
this.value2pixel = function(){
var db = this.decBt, bn = this.thumb, off = this.position.offset;
return (this.orient === "vertical") ? db.y + db.height + Math.floor(((this.amount() - bn.height) * off) / this.max)
: db.x + db.width + Math.floor(((this.amount() - bn.width) * off) / this.max);
};
/**
* Define composite component catch input method
* @param {zebkit.ui.Panel} child a children component
* @return {Boolean} true if the given children component has to be input events transparent
* @method catchInput
*/
this.catchInput = function (child){
return child === this.thumb || (this.thumb.kids.length > 0 &&
zebkit.layout.isAncestorOf(this.thumb, child));
};
this.posChanged = function(target, po, pl, pc) {
if (this.thumb !== null) {
if (this.orient === "horizontal") {
this.thumb.setLocation(this.value2pixel(), this.getTop());
} else {
this.thumb.setLocation(this.getLeft(), this.value2pixel());
}
}
};
this.getLines = function () { return this.max; };
this.getLineSize = function (line) { return 1; };
this.getMaxOffset = function () { return this.max; };
this.fired = function(src){
this.position.setOffset(this.position.offset + ((src === this.incBt) ? this.unitIncrement
: -this.unitIncrement));
};
/**
* Define pointer dragged events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragged
*/
this.pointerDragged = function(e){
if (Number.MAX_VALUE !== this.startDragLoc) {
this.position.setOffset(this.pixel2value(this.thumbLoc -
this.startDragLoc +
((this.orient === "horizontal") ? e.x : e.y)));
}
};
/**
* Define pointer drag started events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragStarted
*/
this.pointerDragStarted = function (e){
if (this.isDragable === true && this.isInThumb(e.x, e.y)) {
this.startDragLoc = this.orient === "horizontal" ? e.x : e.y;
this.thumbLoc = this.orient === "horizontal" ? this.thumb.x : this.thumb.y;
}
};
/**
* Define pointer drag ended events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragEnded
*/
this.pointerDragEnded = function(e) {
this.startDragLoc = Number.MAX_VALUE;
};
/**
* Define pointer clicked events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerClicked
*/
this.pointerClicked = function (e){
if (this.isInThumb(e.x, e.y) === false && e.isAction()){
var d = this.pageIncrement;
if (this.orient === "vertical"){
if (e.y < (this.thumb !== null ? this.thumb.y : Math.floor(this.height / 2))) {
d = -d;
}
} else {
if (e.x < (this.thumb !== null ? this.thumb.x : Math.floor(this.width / 2))) {
d = -d;
}
}
this.position.setOffset(this.position.offset + d);
}
};
this.calcPreferredSize = function (target){
var ps1 = pkg.$getPS(this.incBt),
ps2 = pkg.$getPS(this.decBt),
ps3 = pkg.$getPS(this.thumb);
if (this.orient === "horizontal"){
ps1.width += (ps2.width + ps3.width);
ps1.height = Math.max((ps1.height > ps2.height ? ps1.height : ps2.height), ps3.height);
} else {
ps1.height += (ps2.height + ps3.height);
ps1.width = Math.max((ps1.width > ps2.width ? ps1.width : ps2.width), ps3.width);
}
return ps1;
};
this.doLayout = function(target){
var right = this.getRight(),
top = this.getTop(),
bottom = this.getBottom(),
left = this.getLeft(),
ew = this.width - left - right,
eh = this.height - top - bottom,
b = (this.orient === "horizontal"),
ps1 = pkg.$getPS(this.decBt),
ps2 = pkg.$getPS(this.incBt),
minbs = this.MIN_BUNDLE_SIZE;
this.decBt.setBounds(left, top, b ? ps1.width
: ew,
b ? eh
: ps1.height);
this.incBt.setBounds(b ? this.width - right - ps2.width : left,
b ? top : this.height - bottom - ps2.height,
b ? ps2.width : ew,
b ? eh : ps2.height);
if (this.thumb !== null && this.thumb.isVisible === true){
var am = this.amount();
if (am > minbs) {
var bsize = Math.max(Math.min(Math.floor((this.extra * am) / this.max), am - minbs), minbs);
this.thumb.setBounds(b ? this.value2pixel() : left,
b ? top : this.value2pixel(),
b ? bsize : ew,
b ? eh : bsize);
} else {
this.thumb.setSize(0, 0);
}
}
};
/**
* Set the specified maximum value of the scroll bar component
* @param {Integer} m a maximum value
* @method setMaximum
* @chainable
*/
this.setMaximum = function (m){
if (m !== this.max) {
this.max = m;
if (this.position.offset > this.max) {
this.position.setOffset(this.max);
}
this.vrp();
}
return this;
};
/**
* Set the scroll bar value.
* @param {Integer} v a scroll bar value.
* @method setValue
* @chainable
*/
this.setValue = function(v){
this.position.setOffset(v);
return this;
};
this.setPosition = function(p){
if (p !== this.position){
if (this.position !== null) {
this.position.off(this);
}
this.position = p;
if (this.position !== null){
this.position.on(this);
this.position.setMetric(this);
this.position.setOffset(0);
}
}
return this;
};
this.setExtraSize = function(e){
if (e !== this.extra){
this.extra = e;
this.vrp();
}
return this;
};
},
function kidAdded(index,ctr,lw) {
this.$super(index, ctr, lw);
if ("center" === ctr) {
this.thumb = lw;
} else if ("bottom" === ctr) {
this.incBt = lw;
this.incBt.on(this);
} else if ("top" === ctr) {
this.decBt = lw;
this.decBt.on(this);
} else {
throw new Error("Invalid constraints : " + ctr);
}
},
function kidRemoved(index, lw, ctr) {
this.$super(index, lw, ctr);
if (lw === this.thumb) {
this.thumb = null;
} else if (lw === this.incBt) {
this.incBt.off(this);
this.incBt = null;
} else if (lw === this.decBt) {
this.decBt.off(this);
this.decBt = null;
}
}
]);
/**
* Scroll UI panel. The component is used to manage scrolling for a children UI component
* that occupies more space than it is available. The usage is very simple, just put an
* component you want to scroll horizontally or/and vertically in the scroll panel:
// scroll vertically and horizontally a large picture
var scrollPan = new zebkit.ui.ScrollPan(new zebkit.ui.ImagePan("largePicture.jpg"));
// scroll vertically a large picture
var scrollPan = new zebkit.ui.ScrollPan(new zebkit.ui.ImagePan("largePicture.jpg"),
"vertical");
// scroll horizontally a large picture
var scrollPan = new zebkit.ui.ScrollPan(new zebkit.ui.ImagePan("largePicture.jpg"),
"horizontal");
* @param {zebkit.ui.Panel} [c] an UI component that has to be placed into scroll panel
* @param {String} [scrolls] a scroll bars that have to be shown. Use "vertical", "horizontal"
* or "both" string value to control scroll bars visibility. By default the value is "both"
* @constructor
* @param {Boolean} [autoHide] a boolean value that says if the scrollbars have to work in
* auto hide mode. Pass true to switch scrollbars in auto hide mode. By default the value is
* false
* @class zebkit.ui.ScrollPan
* @extends zebkit.ui.Panel
*/
pkg.ScrollPan = Class(pkg.Panel, [
function (c, scrolls, autoHide) {
if (arguments.length < 2) {
scrolls = "both";
}
this.$isPosChangedLocked = false;
this.$super();
if (arguments.length > 0 && c !== null) {
this.add("center", c);
}
if (arguments.length < 2 || scrolls === "both" || scrolls === "horizontal") {
this.add("bottom", new pkg.Scroll("horizontal"));
}
if (arguments.length < 2 || scrolls === "both" || scrolls === "vertical") {
this.add("right", new pkg.Scroll("vertical"));
}
if (arguments.length > 2) {
this.setAutoHide(autoHide);
}
},
function $clazz() {
this.ContentPanLayout = Class(zebkit.layout.Layout, [
function $prototype() {
this.calcPreferredSize = function(t) {
return t.kids[0].getPreferredSize();
};
this.doLayout = function(t) {
var kid = t.kids[0];
if (kid.constraints === "stretch") {
var ps = kid.getPreferredSize(),
w = t.parent.hBar !== null ? ps.width : t.width,
h = t.parent.vBar !== null ? ps.height : t.height;
kid.setSize(w, h);
} else {
kid.toPreferredSize();
}
};
}
]);
var SM = this.ContentPanScrollManager = Class(pkg.ScrollManager, [
function $prototype() {
this.getSX = function() {
return this.target.x;
};
this.getSY = function() {
return this.target.y;
};
this.scrollStateUpdated = function(sx,sy,psx,psy) {
this.target.setLocation(sx, sy);
};
}
]);
var contentPanLayout = new this.ContentPanLayout();
this.ContentPan = Class(pkg.Panel, [
function(c) {
this.$super(contentPanLayout);
this.scrollManager = new SM(c);
this.add(c);
}
]);
},
function $prototype() {
/**
* Horizontal scroll bar component
* @attribute hBar
* @type {zebkit.ui.Scroll}
* @readOnly
*/
this.hBar = null;
/**
* Vertical scroll bar component
* @attribute vBar
* @type {zebkit.ui.Scroll}
* @readOnly
*/
this.vBar = null;
/**
* Scrollable target component
* @attribute scrollObj
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.scrollObj = null;
/**
* Indicate if the scroll bars should be hidden
* when they are not active
* @attribute autoHide
* @type {Boolean}
* @readOnly
*/
this.autoHide = false;
this.$interval = 0;
/**
* Set the given auto hide state.
* @param {Boolean} b an auto hide state.
* @method setAutoHide
* @chainable
*/
this.setAutoHide = function(b) {
if (this.autoHide !== b) {
this.autoHide = b;
if (this.hBar !== null) {
if (this.hBar.incBt !== null) {
this.hBar.incBt.setVisible(b === false);
}
if (this.hBar.decBt !== null) {
this.hBar.decBt.setVisible(b === false);
}
if (b === true) {
this.hBar.toBack();
} else {
this.hBar.toFront();
}
}
if (this.vBar !== null) {
if (this.vBar.incBt !== null) {
this.vBar.incBt.setVisible(b === false);
}
if (this.vBar.decBt !== null) {
this.vBar.decBt.setVisible(b === false);
}
if (b === true) {
this.vBar.toBack();
} else {
this.vBar.toFront();
}
}
if (this.$interval !== 0) {
zebkit.environment.clearInterval(this.$interval);
this.$interval = 0;
}
this.vrp();
}
return this;
};
/**
* Scroll horizontally and vertically to the given positions
* @param {Integer} sx a horizontal position
* @param {Integer} sy a vertical position
* @method scrollTo
*/
this.scrollTo = function(sx, sy) {
this.scrollObj.scrollManager.scrollTo(sx, sy);
};
/**
* Scroll horizontally
* @param {Integer} sx a position
* @method scrollXTo
*/
this.scrollXTo = function(sx) {
this.scrollObj.scrollManager.scrollXTo(sx);
};
/**
* Scroll vertically
* @param {Integer} sy a position
* @method scrollYTo
*/
this.scrollYTo = function(sx, sy) {
this.scrollObj.scrollManager.scrollYTo(sy);
};
this.doScroll = function(dx, dy, source) {
var b = false, v = 0;
if (dy !== 0 && this.vBar !== null && this.vBar.isVisible === true) {
v = this.vBar.position.offset + dy;
if (v >= 0) {
this.vBar.position.setOffset(v);
} else {
this.vBar.position.setOffset(0);
}
b = true;
}
if (dx !== 0 && this.hBar !== null && this.hBar.isVisible === true) {
v = this.hBar.position.offset + dx;
if (v >= 0) {
this.hBar.position.setOffset(v);
} else {
this.hBar.position.setOffset(0);
}
b = true;
}
return b;
};
/**
* Scroll manager listener method that is called every time
* a target component has been scrolled
* @param {Integer} psx previous scroll x location
* @param {Integer} psy previous scroll y location
* @method scrolled
*/
this.scrolled = function (psx,psy){
this.validate();
try {
this.$isPosChangedLocked = true;
if (this.hBar !== null) {
this.hBar.position.setOffset( -this.scrollObj.scrollManager.getSX());
}
if (this.vBar !== null) {
this.vBar.position.setOffset( -this.scrollObj.scrollManager.getSY());
}
if (this.scrollObj.scrollManager === null || this.scrollObj.scrollManager === undefined) {
this.invalidate();
}
this.$isPosChangedLocked = false;
} catch(e) {
this.$isPosChangedLocked = false;
throw e;
}
};
this.calcPreferredSize = function (target){
return pkg.$getPS(this.scrollObj);
};
this.doLayout = function (target){
var sman = (this.scrollObj === null) ? null : this.scrollObj.scrollManager,
right = this.getRight(),
top = this.getTop(),
bottom = this.getBottom(),
left = this.getLeft(),
ww = this.width - left - right, maxH = ww,
hh = this.height - top - bottom, maxV = hh,
so = this.scrollObj.getPreferredSize(),
vps = this.vBar === null ? { width:0, height:0 } : this.vBar.getPreferredSize(),
hps = this.hBar === null ? { width:0, height:0 } : this.hBar.getPreferredSize();
// compensate scrolled vertical size by reduction of horizontal bar height if necessary
// autoHidded scrollbars don't have an influence to layout
if (this.hBar !== null && this.autoHide === false &&
(so.width > ww ||
(so.height > hh && so.width > (ww - vps.width))))
{
maxV -= hps.height;
}
maxV = so.height > maxV ? (so.height - maxV) : -1;
// compensate scrolled horizontal size by reduction of vertical bar width if necessary
// autoHidded scrollbars don't have an influence to layout
if (this.vBar !== null && this.autoHide === false &&
(so.height > hh ||
(so.width > ww && so.height > (hh - hps.height))))
{
maxH -= vps.width;
}
maxH = so.width > maxH ? (so.width - maxH) : -1;
var sy = sman.getSY(), sx = sman.getSX();
if (this.vBar !== null) {
if (maxV < 0) {
if (this.vBar.isVisible === true){
this.vBar.setVisible(false);
sman.scrollTo(sx, 0);
this.vBar.position.setOffset(0);
}
sy = 0;
} else {
this.vBar.setVisible(true);
}
}
if (this.hBar !== null){
if (maxH < 0){
if (this.hBar.isVisible === true){
this.hBar.setVisible(false);
sman.scrollTo(0, sy);
this.hBar.position.setOffset(0);
}
} else {
this.hBar.setVisible(true);
}
}
if (this.scrollObj.isVisible === true) {
this.scrollObj.setBounds(left, top,
ww - (this.autoHide === false && this.vBar !== null && this.vBar.isVisible === true ? vps.width : 0),
hh - (this.autoHide === false && this.hBar !== null && this.hBar.isVisible === true ? hps.height : 0));
}
if (this.$interval === 0 && this.autoHide === true) {
hps.height = vps.width = 0;
}
if (this.hBar !== null && this.hBar.isVisible === true){
this.hBar.setBounds(left, this.height - bottom - hps.height,
ww - (this.vBar !== null && this.vBar.isVisible === true ? vps.width : 0),
hps.height);
this.hBar.setMaximum(maxH);
}
if (this.vBar !== null && this.vBar.isVisible === true){
this.vBar.setBounds(this.width - right - vps.width, top,
vps.width,
hh - (this.hBar !== null && this.hBar.isVisible === true ? hps.height : 0));
this.vBar.setMaximum(maxV);
}
};
this.posChanged = function (target,prevOffset,prevLine,prevCol){
if (this.$isPosChangedLocked === false) {
// show if necessary hidden scroll bar(s)
if (this.autoHide === true) {
// make sure autohide thread has not been initiated and make sure it makes sense
// to initiate the thread (one of the scroll bar has to be visible)
if (this.$interval === 0 && ((this.vBar !== null && this.vBar.isVisible === true) ||
(this.hBar !== null && this.hBar.isVisible === true) ))
{
var $this = this;
// show scroll bar(s)
if (this.vBar !== null) {
this.vBar.toFront();
}
if (this.hBar !== null) {
this.hBar.toFront();
}
this.vrp();
// pointer move should keep scroll bars visible and pointer entered exited
// events have to be caught to track if pointer cursor is on a scroll
// bar. add temporary listeners
$this.$hiddingCounter = 2;
$this.$targetBar = null;
var listener = pkg.events.on({
pointerMoved: function(e) {
$this.$hiddingCounter = 1;
},
pointerExited: function(e) {
$this.$targetBar = null;
},
pointerEntered: function(e) {
if (e.source === $this.vBar) {
$this.$targetBar = $this.vBar;
} else {
if (e.source === $this.hBar) {
$this.$targetBar = $this.hBar;
return;
}
$this.$targetBar = null;
}
}
});
// start thread to autohide shown scroll bar(s)
this.$interval = zebkit.environment.setInterval(function() {
if ($this.$hiddingCounter-- === 0 && $this.$targetBar === null) {
zebkit.environment.clearInterval($this.$interval);
$this.$interval = 0;
pkg.events.off(listener);
$this.doLayout();
}
}, 500);
}
}
if (this.vBar !== null && this.vBar.position === target) {
this.scrollObj.scrollManager.scrollYTo(-this.vBar.position.offset);
} else if (this.hBar !== null && this.hBar.position === target) {
this.scrollObj.scrollManager.scrollXTo(-this.hBar.position.offset);
}
}
};
this.setIncrements = function (hUnit,hPage,vUnit,vPage) {
if (this.hBar !== null){
if (hUnit !== -1) {
this.hBar.unitIncrement = hUnit;
}
if (hPage !== -1) {
this.hBar.pageIncrement = hPage;
}
}
if (this.vBar !== null){
if (vUnit !== -1) {
this.vBar.unitIncrement = vUnit;
}
if (vPage !== -1) {
this.vBar.pageIncrement = vPage;
}
}
return this;
};
},
function insert(i,ctr,c) {
if ("center" === ctr) {
if (c.scrollManager === null || c.scrollManager === undefined) {
c = new this.clazz.ContentPan(c);
}
this.scrollObj = c;
c.scrollManager.on(this);
} else {
if ("bottom" === ctr || "top" === ctr){
this.hBar = c;
} else if ("left" === ctr || "right" === ctr) {
this.vBar = c;
} else {
throw new Error("Invalid constraints");
}
// valid for scroll bar only
if (c.incBt !== null) {
c.incBt.setVisible(!this.autoHide);
}
if (c.decBt !== null) {
c.decBt.setVisible(!this.autoHide);
}
c.position.on(this);
}
return this.$super(i, ctr, c);
},
function kidRemoved(index, comp, ctr){
this.$super(index, comp, ctr);
if (comp === this.scrollObj){
this.scrollObj.scrollManager.off(this);
this.scrollObj = null;
} else if (comp === this.hBar) {
this.hBar.position.off(this);
this.hBar = null;
} else if (comp === this.vBar) {
this.vBar.position.off(this);
this.vBar = null;
}
}
]);
/**
* Mobile scroll manager class. Implements inertial scrolling in zebkit mobile application.
* @class zebkit.ui.MobileScrollMan
* @extends zebkit.ui.event.Manager
* @constructor
*/
pkg.MobileScrollMan = Class(zebkit.ui.event.Manager, [
function $prototype() {
this.$timer = this.identifier = this.target = null;
/**
* Define pointer drag started events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragStarted
*/
this.pointerDragStarted = function(e) {
if (e.touchCounter === 1 && e.pointerType === "touch") {
this.$identifier = e.identifier;
this.$target = e.source;
// detect scrollable component
while (this.$target !== null && this.$target.doScroll === undefined) {
this.$target = this.$target.parent;
}
if (this.$target !== null && this.$target.pointerDragged !== undefined) {
this.$target = null;
}
}
};
/**
* Define pointer dragged events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragged
*/
this.pointerDragged = function(e) {
if (e.touchCounter === 1 &&
this.$target !== null &&
this.$identifier === e.identifier &&
e.direction !== null )
{
this.$target.doScroll(-e.dx, -e.dy, "touch");
}
};
this.$taskMethod = function() {
var bar = this.$target.vBar,
o = bar.position.offset;
// this is linear function with angel 42. every next value will
// be slightly lower prev. one. theoretically angel 45 should
// bring to infinite scrolling :)
this.$dt = Math.tan(42 * Math.PI / 180) * this.$dt;
bar.position.setOffset(o - Math.round(this.$dt));
this.$counter++;
if (o === bar.position.offset) {
this.$target = null;
zebkit.environment.clearInterval(this.$timer);
this.$timer = null;
}
};
/**
* Define pointer drag ended events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragEnded
*/
this.pointerDragEnded = function(e) {
if (this.$target !== null &&
this.$timer === null &&
this.$identifier === e.identifier &&
(e.direction === "bottom" || e.direction === "top") &&
this.$target.vBar !== null &&
this.$target.vBar.isVisible &&
e.dy !== 0)
{
this.$dt = 2 * e.dy;
this.$counter = 0;
var $this = this;
this.$timer = zebkit.environment.setInterval(function() {
$this.$taskMethod($this);
}, 50);
}
};
/**
* Define pointer pressed events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerPressed
*/
this.pointerPressed = function(e) {
if (this.$timer !== null) {
zebkit.environment.clearInterval(this.$timer);
this.$timer = null;
}
this.$target = null;
};
}
]);
/**
* Text field UI component. The component is designed to enter single line, multi lines or password text.
* The component implement text field functionality from the scratch. It supports the following features
*
* - Text selection
* - Redu/Undo actions
* - Native WEB clipboard
* - Basic text navigation
* - Read-only mode
* - Left or right text alignment
*
* @constructor
* @param {String|zebkit.data.TextModel|zebkit.draw.TextRender} [txt] a text the text field component
* has to be filled. The parameter can be a simple string, text model or text render class instance.
* @param {Integer} [maxCol] a maximal size of entered text. -1 means the size of the edited text
* has no length limit.
* @class zebkit.ui.TextField
* @extends zebkit.ui.Label
*/
/**
* Fire when a text field content has been updated.
*
* textField.on("updated", function(src) {
* ...
* });
*
* @event updated
* @param {zebkit.ui.TextField} src a source of the event
*/
/**
* Fire when a text field content has been selected.
*
* textField.on("selected", function(src) {
* ...
* });
*
* @event selected
* @param {zebkit.ui.TextField} src a source of the event
*/
/**
* Fire when a cursor position has been changed.
*
* textField.on("posChanged", function(src) {
* ...
* });
*
* @event posChanged
* @param {zebkit.ui.TextField} src a source of the event
*/
pkg.TextField = Class(pkg.Label, [
function (render, maxCol){
this.$history = Array(100);
this.scrollManager = new pkg.ScrollManager(this);
var renderDefined = false;
if (arguments.length === 0) {
maxCol = -1;
render = new zebkit.draw.TextRender(new zebkit.data.SingleLineTxt());
renderDefined = true;
} else if (arguments.length === 1){
if (zebkit.isNumber(render)) {
maxCol = render;
render = new zebkit.draw.TextRender(new zebkit.data.SingleLineTxt());
renderDefined = true;
} else {
maxCol = -1;
}
}
if (renderDefined === false) {
if (zebkit.isString(render)) {
render = new zebkit.draw.TextRender(new zebkit.data.SingleLineTxt(render));
} else if (zebkit.instanceOf(render, zebkit.data.TextModel)) {
render = new zebkit.draw.TextRender(render);
}
}
this.$super(render);
if (maxCol > 0) {
this.setPSByRowsCols(-1, maxCol);
}
},
function $clazz() {
/**
* Text field hint text render
* @constructor
* @class zebkit.ui.TextField.HintRender
* @extends zebkit.draw.StringRender
*/
this.HintRender = Class(zebkit.draw.StringRender, []);
},
/**
* @for zebkit.ui.TextField
*/
function $prototype() {
this.$historyPos = -1;
this.$lineHeight = 0;
this.$redoCounter = 0;
this.$undoCounter = 0;
this.$blinkTask = null;
/**
* Cursor x loacation
* @attribute cursorX
* @type {Integer}
* @readOnly
*/
this.cursorX = 0;
/**
* Cursor y loacation
* @attribute cursorY
* @type {Integer}
* @readOnly
*/
this.cursorY = 0;
/**
* Cursor width
* @attribute cursorWidth
* @type {Integer}
* @readOnly
*/
this.cursorWidth = 0;
/**
* Cursor height
* @attribute cursorHeight
* @type {Integer}
* @readOnly
*/
this.cursorHeight = 0;
/**
* Selection view.
* @attribute selectView
* @type {zebkit.draw.View|String}
* @readOnly
*/
this.selectView = null;
/**
* Hint view
* @attribute hint
* @type {zebkit.draw.View}
* @readOnly
*/
this.hint = null;
// TODO: check the place the property is required
this.vkMode = "indirect";
this.startLine = this.startCol = this.endLine = this.endCol = 0;
this.startOff = this.endOff = -1;
/**
* Cursor position manager
* @attribute position
* @type {zebkit.util.Position}
* @readOnly
*/
this.position = null;
/**
* Specify the text field cursor blinking period in milliseconds.
* -1 means no blinkable cursor
* @type {Number}
* @default -1
* @readOnly
* @attribute blinkigPeriod
*/
this.blinkingPeriod = -1;
this.$blinkMe = true;
this.$blinkMeCounter = 0;
/**
* Cursor type
* @attribute cursorType
* @type {String}
* @default zebkit.ui.Cursor.TEXT;
*/
this.cursorType = pkg.Cursor.TEXT;
/**
* Text alignment
* @attribute textAlign
* @type {String}
* @default "left"
* @readOnly
*/
this.textAlign = "left";
/**
* Cursor view
* @attribute cursorView
* @type {zebkit.draw.View}
* @readOnly
*/
this.cursorView = null;
/**
* Indicate if the text field is editable
* @attribute isEditable
* @type {Boolean}
* @default true
* @readOnly
*/
this.canHaveFocus = this.isEditable = true;
/**
* Set the specified blinking period of the text field cursor
* @param {Integer} [period] a text field cursor blinking period (in milliseconds),
* use -1 to disable cursor blinking. If the argument is not passed the default (500ms)
* blinking period will be applied.
* @method setBlinking
* @chainable
*/
this.setBlinking = function(period) {
if (arguments.length === 0) {
period = 500;
}
if (period !== this.blinkingPeriod) {
this.blinkingPeriod = period;
this.repaintCursor();
}
return this;
};
/**
* Set the text algnment.
* @method setTextAlignment
* @param {String} a a text alignment. Use "left" or "right" as the parameter value
* @chainable
*/
this.setTextAlignment = function(a) {
if (this.textAlign !== a) {
this.textAlign = a;
this.vrp();
}
return this;
};
this.textUpdated = function(e) {
if (this.position !== null) {
if (this.endOff !== this.startOff) {
this.endOff = this.startOff = -1; // clear selection
this.fire("selected", this);
}
if (e.id === "insert") {
this.position.inserted(e.offset, e.size);
} else {
this.position.removed(e.offset, e.size);
}
}
if (e.isLastStep) {
if (this.updated !== undefined) {
this.updated();
}
this.fire("updated", this);
}
};
/**
* Compute a text column and row by the given location.
* @param {Integer} x a x coordinate
* @param {Integer} y a y coordinate
* @return {Object} a text row and column as an object { row:, col }.
* @method getTextRowColAt
*/
this.getTextRowColAt = function(x, y) {
var lines = this.getLines();
// normalize text location to virtual (zero, zero)
y -= (this.scrollManager.getSY() + this.getTop());
x -= this.scrollManager.getSX();
if (this.textAlign === "left") {
x -= this.getLeft();
} else {
x -= (this.width - this.view.getPreferredSize().width - this.getRight());
}
if (x >= 0 && y >= 0 && lines > 0) {
var lh = this.view.getLineHeight(),
li = this.view.lineIndent,
row = (y < 0) ? 0 : Math.floor((y + li) / (lh + li)) + ((y + li) % (lh + li) > li ? 1 : 0) -1;
if (row < lines && row >= 0) {
var s = this.view.getLine(row),
pdt = 1000000,
pcol = -1;
for(var col = Math.floor((x / this.view.calcLineWidth(row)) * s.length); col >= 0 && col <= s.length;) {
var l = this.view.font.charsWidth(s, 0, col),
dt = Math.abs(l - x);
if (dt >= pdt) {
return { row : row, col : pcol };
}
pdt = dt;
pcol = col;
col += (l > x ? -1: 1);
}
return { row : row, col : s.length };
}
}
return null;
};
/**
* Find the next or previous word in the given text model starting from the given
* line and column.
* @param {zebkit.data.TextModel | zebkit.draw.BaseTextRender} t a text model
* @param {Integer} line a starting line
* @param {Integer} col a starting column
* @param {Integer} d a direction. 1 means looking for a next word and -1 means
* search for a previous word.
* @return {Object} a structure with the next or previous word location:
*
* { row: {Integer}, col: {Integer} }
*
*
* The method returns null if the next or previous word cannot be found.
* @method findNextWord
* @protected
*/
this.findNextWord = function(t, line, col, d){
if (line < 0 || line >= t.getLines()) {
return null;
}
var ln = t.getLine(line);
col += d;
if (col < 0 && line > 0) {
return { row: line - 1, col : t.getLine(line - 1).length };
} else if (col > ln.length && line < t.getLines() - 1) {
return { row : line + 1, col : 0 };
}
var b = false;
for(; col >= 0 && col < ln.length; col += d){
if (b) {
if (d > 0) {
if (zebkit.util.isLetter(ln[col])) {
return { row:line, col:col };
}
} else {
if (!zebkit.util.isLetter(ln[col])) {
return { row : line, col: col + 1 };
}
}
} else {
b = d > 0 ? !zebkit.util.isLetter(ln[col]) : zebkit.util.isLetter(ln[col]);
}
}
return (d > 0 ? { row: line, col : ln.length }: { row : line, col : 0 } );
};
// collect text model lines into string by the given start and end offsets
// r - text view
// start - start offset
// end - end offset
this.getSubString = function(r, start, end){
var res = [],
sr = start.row,
er = end.row;
for(var i = sr; i < er + 1; i++){
var ln = r.getLine(i);
if (i !== sr) {
res.push('\n');
} else {
ln = ln.substring(start.col);
}
if (i === er) {
ln = ln.substring(0, end.col - ((sr === er) ? start.col : 0));
}
res.push(ln);
}
return res.join('');
};
/**
* Remove selected text
* @method removeSelected
* @chainable
*/
this.removeSelected = function(){
if (this.hasSelection()){
var start = this.startOff < this.endOff ? this.startOff : this.endOff;
this.remove(start, (this.startOff > this.endOff ? this.startOff : this.endOff) - start);
this.clearSelection();
}
return this;
};
/**
* Start selection.
* @protected
* @method startSelection
* @chainable
*/
this.startSelection = function() {
if (this.startOff < 0 && this.position !== null){
var pos = this.position;
this.endLine = this.startLine = pos.currentLine;
this.endCol = this.startCol = pos.currentCol;
this.endOff = this.startOff = pos.offset;
}
return this;
};
this.keyTyped = function(e) {
// Test if selection has been initiated (but nothing has been selected yet)
// Typing a character changes position so if selection is active then
// typed character will be unexpectedly selected.
if (e.shiftKey && this.startOff >= 0 && this.endOff === this.startOff) {
this.clearSelection();
}
if (this.isEditable === true &&
e.ctrlKey === false &&
e.metaKey === false &&
e.key !== '\t')
{
this.write(this.position.offset, e.key);
}
};
/**
* Select all text.
* @method selectAll
* @chainable
*/
this.selectAll = function() {
this.select(0, this.getMaxOffset());
return this;
};
/**
* Shortcut event handler
* @param {java.ui.event.ShortcutEvent} e a shortcut event
* @method shortcutFired
*/
this.shortcutFired = function(e) {
if (e.shortcut === "SELECTALL") {
this.selectAll();
} else {
var d = (e.shortcut === "PREVWORDSELECT" || e.shortcut === "PREVWORD") ? -1 : 1;
if (e.shortcut === "PREVWORDSELECT" ||
e.shortcut === "NEXTWORDSELECT" ||
e.shortcut === "NEXTPAGESELECT" ||
e.shortcut === "PREVPAGESELECT" )
{
this.startSelection();
}
switch (e.shortcut) {
case "UNDO" : this.undo(); break;
case "REDO" : this.redo(); break;
case "NEXTPAGESELECT":
case "NEXTPAGE" : this.position.seekLineTo("down", this.pageSize()); break;
case "PREVPAGESELECT":
case "PREVPAGE" : this.position.seekLineTo("up", this.pageSize()); break;
case "NEXTWORDSELECT":
case "PREVWORDSELECT":
case "PREVWORD":
case "NEXTWORD" : {
var p = this.findNextWord(this.view, this.position.currentLine,
this.position.currentCol, d);
if (p !== null) {
this.position.setRowCol(p.row, p.col);
}
} break;
}
}
};
this.keyPressed = function(e) {
if (this.isFiltered(e) === false) {
var position = this.position;
if (e.shiftKey) {
this.startSelection();
}
switch(e.code) {
case "ArrowDown" : position.seekLineTo("down"); break;
case "ArrowUp" : position.seekLineTo("up"); break;
case "ArrowLeft" :
if (e.ctrlKey === false && e.metaKey === false) {
position.seek(-1);
}
break;
case "ArrowRight":
if (e.ctrlKey === false && e.metaKey === false) {
position.seek(1);
}
break;
case "End":
if (e.ctrlKey) {
position.seekLineTo("down", this.getLines() - position.currentLine - 1);
} else {
position.seekLineTo("end");
}
break;
case "Home":
if (e.ctrlKey) {
position.seekLineTo("up", position.currentLine);
} else {
position.seekLineTo("begin");
}
break;
case "PageDown" :
position.seekLineTo("down", this.pageSize());
break;
case "PageUp" :
position.seekLineTo("up", this.pageSize());
break;
case "Delete":
if (this.hasSelection() && this.isEditable === true) {
this.removeSelected();
} else if (this.isEditable === true) {
this.remove(position.offset, 1);
} break;
case "Backspace":
if (this.isEditable === true) {
if (this.hasSelection()) {
this.removeSelected();
} else if (this.isEditable === true && position.offset > 0) {
position.seek(-1);
this.remove(position.offset, 1);
}
} break;
default: return ;
}
if (e.shiftKey === false) {
this.clearSelection();
}
}
};
/**
* Test if the given key pressed event has to be processed
* @protected
* @param {zebkit.ui.event.KeyEvent} e a key event
* @return {Boolean} true if the given key pressed event doesn't
* have be processed
* @method isFiltered
*/
this.isFiltered = function(e){
var code = e.code;
return code === "Shift" || code === "Control" ||
code === "Tab" || code === "Alt" ||
e.altKey;
};
/**
* Remove the specified part of edited text
* @param {Integer} pos a start position of a removed text
* @param {Integer} size a size of removed text
* @method remove
*/
this.remove = function(pos, size){
if (this.isEditable === true) {
if (pos >= 0 && (pos + size) <= this.getMaxOffset()) {
if (size < 10000) {
this.$historyPos = (this.$historyPos + 1) % this.$history.length;
this.$history[this.$historyPos] = [-1, pos, this.getValue().substring(pos, pos+size)];
if (this.$undoCounter < this.$history.length) {
this.$undoCounter++;
}
}
if (this.view.remove(pos, size)) {
this.repaint();
return true;
}
}
}
return false;
};
/**
* Insert the specified text into the edited text at the given position
* @param {Integer} pos a start position of a removed text
* @param {String} s a text to be inserted
* @return {Boolean} true if repaint has been requested
* @method write
*/
this.write = function (pos,s) {
if (this.isEditable === true) {
// TODO: remove hard coded undo/redo deepness value
if (s.length < 10000) {
this.$historyPos = (this.$historyPos + 1) % this.$history.length;
this.$history[this.$historyPos] = [1, pos, s.length];
if (this.$undoCounter < this.$history.length) {
this.$undoCounter++;
}
}
// has selection then replace the selection with the given text nevertheless
// the requested pos
if (this.startOff !== this.endOff) {
var start = this.startOff < this.endOff ? this.startOff : this.endOff,
end = this.startOff > this.endOff ? this.startOff : this.endOff;
if (this.view.replace(s, start, end - start)) {
this.repaint();
return true;
}
} else {
if (this.view.write(s, pos)) {
this.repaint();
return true;
}
}
}
return false;
};
this.recalc = function() {
var r = this.view;
if (this.position.offset >= 0) {
var l = r.getLine(this.position.currentLine);
if (this.textAlign === "left") {
this.curX = r.font.charsWidth(l, 0, this.position.currentCol) + this.getLeft();
} else {
this.curX = this.width - this.getRight() - this.view.getPreferredSize().width +
r.font.charsWidth(l, 0, this.position.currentCol);
}
this.curY = this.position.currentLine * (r.getLineHeight() + r.lineIndent) +
this.getTop();
}
this.$lineHeight = r.getLineHeight() - 1;
};
this.catchScrolled = function(psx, psy) {
this.repaint();
};
/**
* Draw the text field cursor.
* @protected
* @param {CanvasRenderingContext2D} g a 2D context
* @method drawCursor
*/
this.drawCursor = function (g) {
if (this.position.offset >= 0 &&
this.cursorView !== null &&
this.$blinkMe &&
this.hasFocus() )
{
if (this.textAlign === "left") {
this.cursorView.paint(g, this.curX, this.curY,
this.cursorWidth,
(this.cursorHeight === 0 ? this.$lineHeight : this.cursorHeight),
this);
} else {
this.cursorView.paint(g, this.curX - this.cursorWidth, this.curY,
this.cursorWidth,
(this.cursorHeight === 0 ? this.$lineHeight : this.cursorHeight),
this);
}
}
};
this.pointerDragStarted = function (e){
if (e.isAction() && this.getMaxOffset() > 0) {
this.startSelection();
}
};
this.pointerDragEnded =function (e){
if (e.isAction() && this.hasSelection() === false) {
this.clearSelection();
}
};
this.pointerDragged = function (e){
if (e.isAction()){
var p = this.getTextRowColAt(e.x, e.y);
if (p !== null) {
this.position.setRowCol(p.row, p.col);
}
}
};
/**
* Select the specified part of the edited text
* @param {Integer} startOffset a start position of a selected text
* @param {Integer} endOffset an end position of a selected text
* @method select
* @chainable
*/
this.select = function (startOffset, endOffset){
if (endOffset < startOffset ||
startOffset < 0 ||
endOffset > this.getMaxOffset())
{
throw new Error("Invalid selection offsets");
}
if (this.startOff !== startOffset || endOffset !== this.endOff) {
if (startOffset === endOffset) {
this.clearSelection();
} else {
this.startOff = startOffset;
var p = this.position.getPointByOffset(startOffset);
this.startLine = p[0];
this.startCol = p[1];
this.endOff = endOffset;
p = this.position.getPointByOffset(endOffset);
this.endLine = p[0];
this.endCol = p[1];
this.fire("selected", this);
this.repaint();
}
}
return this;
};
/**
* Tests if the text field has a selected text
* @return {Boolean} true if the text field has a selected text
* @method hasSelection
*/
this.hasSelection = function () {
return this.startOff !== this.endOff;
};
this.posChanged = function (target, po, pl, pc){
this.recalc();
var position = this.position;
if (position.offset >= 0) {
this.$blinkMeCounter = 0;
this.$blinkMe = true;
var lineHeight = this.view.getLineHeight(),
top = this.getTop();
this.scrollManager.makeVisible(this.textAlign === "left" ? this.curX
: this.curX - this.cursorWidth,
this.curY, this.cursorWidth, lineHeight);
if (pl >= 0) {
// means selected text exists, than we have to correct selection
// according to the new position
if (this.startOff >= 0) {
this.endLine = position.currentLine;
this.endCol = position.currentCol;
this.endOff = position.offset;
this.fire("selected", this);
}
var minUpdatedLine = pl < position.currentLine ? pl : position.currentLine,
li = this.view.lineIndent,
bottom = this.getBottom(),
left = this.getLeft(),
y1 = lineHeight * minUpdatedLine + minUpdatedLine * li +
top + this.scrollManager.getSY();
if (y1 < top) {
y1 = top;
}
if (y1 < this.height - bottom){
var h = ((pl > position.currentLine ? pl
: position.currentLine) - minUpdatedLine + 1) * (lineHeight + li);
if (y1 + h > this.height - bottom) {
h = this.height - bottom - y1;
}
this.repaint(left, y1, this.width - left - this.getRight(), h);
}
} else {
this.repaint();
}
}
this.fire("posChanged", this);
};
this.paintOnTop = function(g) {
if (this.hint !== null && this.getMaxOffset() === 0) {
var ps = this.hint.getPreferredSize(),
yy = Math.floor((this.height - ps.height)/2),
xx = ("left" === this.textAlign) ? this.getLeft() + this.cursorWidth
: this.width - ps.width - this.getRight() - this.cursorWidth;
this.hint.paint(g, xx, yy, this.width, this.height, this);
}
};
/**
* Set the specified hint text to be drawn with the given font and color.
* The hint is not-editable text that is shown in empty text field to help
* a user to understand which input the text field expects.
* @param {String|zebkit.draw.View|Function} hint a hint text, view or view render method
* @method setHint
* @chainable
*/
this.setHint = function(hint) {
if (this.hint !== hint) {
this.hint = zebkit.isString(hint) ? new this.clazz.HintRender(hint)
: zebkit.draw.$view(hint);
this.repaint();
}
return this;
};
/**
* Performs undo operation
* @method undo
* @chainable
*/
this.undo = function() {
if (this.$undoCounter > 0) {
var h = this.$history[this.$historyPos];
this.$historyPos--;
if (h[0] === 1) {
this.remove(h[1], h[2]);
}
else {
this.write (h[1], h[2]);
}
this.$undoCounter -= 2;
this.$redoCounter++;
this.$historyPos--;
if (this.$historyPos < 0) {
this.$historyPos = this.$history.length - 1;
}
this.repaint();
}
return this;
};
/**
* Performs redo operation
* @method redo
* @chainable
*/
this.redo = function() {
if (this.$redoCounter > 0) {
var h = this.$history[(this.$historyPos + 1) % this.$history.length];
if (h[0] === 1) {
this.remove(h[1], h[2]);
} else {
this.write (h[1], h[2]);
}
this.$redoCounter--;
this.repaint();
}
return this;
};
/**
* Get a starting position (row and column) of a selected text
* @return {Array} a position of a selected text. First element
* of is a row and second column of selected text. null if
* there is no any selected text
* @method getStartSelection
*/
this.getStartSelection = function(){
return this.startOff !== this.endOff ? ((this.startOff < this.endOff) ? { row: this.startLine, col: this.startCol }
: { row: this.endLine, col: this.endCol } )
: null;
};
/**
* Get an ending position (row and column) of a selected text
* @return {Array} a position of a selected text. First element
* of is a row and second column of selected text. null if
* there is no any selected text
* @method getEndSelection
*/
this.getEndSelection = function(){
return this.startOff !== this.endOff ? ((this.startOff < this.endOff) ? { row : this.endLine, col : this.endCol }
: { row : this.startLine, col : this.startCol })
: null;
};
/**
* Get a selected text
* @return {String} a selected text
* @method getSelectedText
*/
this.getSelectedText = function(){
return this.startOff !== this.endOff ? this.getSubString(this.view,
this.getStartSelection(),
this.getEndSelection())
: null;
};
this.getLines = function() {
return this.position === null ? -1 : this.position.metrics.getLines();
};
this.getMaxOffset = function() {
return this.position === null ? -1 : this.position.metrics.getMaxOffset();
};
this.focusGained = function (e){
if (this.position.offset < 0) {
this.position.setOffset(this.textAlign === "left" || this.getLines() > 1 ? 0
: this.getMaxOffset());
} else if (this.hint !== null) {
this.repaint();
} else {
this.repaintCursor();
}
if (this.isEditable === true && this.blinkingPeriod > 0) {
this.$blinkMeCounter = 0;
this.$blinkMe = true;
var $this = this;
this.$blinkTask = zebkit.util.tasksSet.run(function() {
$this.$blinkMeCounter = ($this.$blinkMeCounter + 1) % 3;
if ($this.$blinkMeCounter === 0) {
$this.$blinkMe = !$this.$blinkMe;
$this.repaintCursor();
}
},
Math.floor(this.blinkingPeriod / 3),
Math.floor(this.blinkingPeriod / 3)
);
}
};
this.focusLost = function(e) {
this.repaintCursor();
if (this.isEditable === true) {
if (this.hint !== null) {
this.repaint();
}
if (this.blinkingPeriod > 0) {
if (this.$blinkTask !== null) {
this.$blinkTask.shutdown();
this.$blinkTask = null;
}
this.$blinkMe = true;
}
}
};
/**
* Force text field cursor repainting.
* @method repaintCursor
* @protected
*/
this.repaintCursor = function() {
if (this.curX > 0 && this.cursorWidth > 0 && (this.cursorHeight > 0 || this.$lineHeight > 0)) {
this.repaint(this.curX + this.scrollManager.getSX(),
this.curY + this.scrollManager.getSY(),
this.cursorWidth,
(this.cursorHeight === 0 ? this.$lineHeight : this.cursorHeight));
}
};
/**
* Clear a text selection.
* @method clearSelection
* @chainable
*/
this.clearSelection = function() {
if (this.startOff >= 0){
var b = this.hasSelection();
this.endOff = this.startOff = -1;
this.startLine = this.startCol = -1;
this.endLine = this.endCol = -1;
if (b) {
this.repaint();
this.fire("selected", this);
}
}
return this;
};
this.pageSize = function (){
var height = this.height - this.getTop() - this.getBottom(),
indent = this.view.lineIndent,
textHeight = this.view.getLineHeight();
return Math.round((height + indent) / (textHeight + indent)) +
(((height + indent) % (textHeight + indent) > indent) ? 1 : 0);
};
this.clipPaste = function(txt){
if (txt !== null) {
this.removeSelected();
this.write(this.position.offset, txt);
}
};
this.clipCopy = function() {
return this.getSelectedText();
};
/**
* Cut selected text
* @return {String} a text that has been selected and cut
* @method cut
*/
this.cut = function() {
var t = this.getSelectedText();
if (this.isEditable === true) {
this.removeSelected();
}
return t;
};
/**
* Set the specified cursor position controller
* @param {zebkit.util.Position} p a position controller
* @method setPosition
* @chainable
*/
this.setPosition = function (p){
if (this.position !== p) {
if (this.position !== null) {
this.position.off(this);
}
this.position = p;
if (this.position !== null) {
this.position.on(this);
}
this.invalidate();
}
return this;
};
/**
* Set the cursor view. The view defines rendering of the text field
* cursor.
* @param {zebkit.draw.View} v a cursor view
* @method setCursorView
* @chainable
*/
this.setCursorView = function (v){
if (v !== this.cursorView) {
this.cursorWidth = 1;
this.cursorView = zebkit.draw.$view(v);
if (this.cursorView !== null && this.cursorWidth === 0) {
this.cursorWidth = this.cursorView.getPreferredSize().width;
}
this.vrp();
}
return this;
};
/**
* Set cursor width.
* @param {Integer} w a cursor width
* @method setCursorWidth
* @chainable
*/
this.setCursorWidth = function(w) {
if (w !== this.cursorWidth) {
this.cursorWidth = w;
this.vrp();
}
return this;
};
/**
* Set cursor size.
* @param {Integer} w a cursor width
* @param {Integer} h a cursor height
* @method setCursorSize
* @chainable
*/
this.setCursorSize = function(w, h) {
if (w !== this.cursorWidth || h !== this.cursorHeight) {
this.cursorWidth = w;
this.cursorHeight = h;
this.vrp();
}
return this;
};
/**
* Adjust the size of the text field component to be enough to place the given
* number of rows and columns.
* @param {Integer} r a row of the text the height of the text field has to be adjusted
* @param {Integer} c a column of the text the width of the text field has to be adjusted
* @method setPSByRowsCols
* @chainable
*/
this.setPSByRowsCols = function (r,c){
var tr = this.view,
w = (c > 0) ? (tr.font.stringWidth("W") * c)
: this.psWidth,
h = (r > 0) ? (r * tr.getLineHeight() + (r - 1) * tr.lineIndent)
: this.psHeight;
this.setPreferredSize(w, h);
return this;
};
/**
* Control the text field editable state
* @param {Boolean} b true to make the text field editable
* @method setEditable
* @chainable
*/
this.setEditable = function (b){
if (b !== this.isEditable){
this.isEditable = b;
if (b && this.blinkingPeriod > 0 && this.hasFocus()) {
if (this.$blinkTask !== null) {
this.$blinkTask.shutdown();
}
this.$blinkMe = true;
}
this.vrp();
}
return this;
};
this.pointerDoubleClicked = function(e){
if (e.isAction()) {
this.select(0, this.getMaxOffset());
}
};
this.pointerPressed = function(e){
if (e.isAction()) {
if (e.shiftKey) {
this.startSelection();
} else {
this.clearSelection();
}
var p = this.getTextRowColAt(e.x, e.y);
if (p !== null) {
this.position.setRowCol(p.row, p.col);
}
}
};
/**
* Set selection color or view
* @param {String|zebkit.draw.View} c a selection color or view
* @method setSelectView
* @chainable
*/
this.setSelectView = function(c) {
if (c != this.selectView) {
this.selectView = zebkit.draw.$view(c);
if (this.hasSelection()) {
this.repaint();
}
}
return this;
};
this.calcPreferredSize = function (t) {
var ps = this.view.getPreferredSize();
ps.width += this.cursorWidth;
return ps;
};
//!!! to maximize optimize performance the method duplicates part of ViewPan.paint() code
this.paint = function(g){
var sx = this.scrollManager.getSX(),
sy = this.scrollManager.getSY(),
l = this.getLeft(),
t = this.getTop(),
r = this.getRight();
try {
g.translate(sx, sy);
if (this.textAlign === "left") {
this.view.paint(g, l, t,
this.width - l - r,
this.height - t - this.getBottom(), this);
} else {
this.view.paint(g, this.width - r - this.view.getPreferredSize().width, t,
this.width - l - r,
this.height - t - this.getBottom(), this);
}
this.drawCursor(g);
} catch(e) {
g.translate(-sx, -sy);
throw e;
}
g.translate(-sx, -sy);
};
},
function setView(v){
if (v != this.view) {
if (this.view !== null && this.view.off !== undefined) {
this.view.off(this);
}
this.$super(v);
if (this.position === null) {
this.setPosition(new zebkit.util.Position(this.view));
} else {
this.position.setMetric(this.view);
}
if (this.view !== null && this.view.on !== undefined) {
this.view.on(this);
}
}
return this;
},
/**
* Set the text content of the text field component
* @param {String} s a text the text field component has to be filled
* @method setValue
* @chainable
*/
function setValue(s) {
var txt = this.getValue();
if (txt !== s){
if (this.position !== null) {
this.position.setOffset(0);
}
this.scrollManager.scrollTo(0, 0);
this.$super(s);
}
return this;
},
function setEnabled(b){
this.clearSelection();
this.$super(b);
return this;
}
]).events("updated", "selected", "posChanged");
/**
* Text area UI component. The UI component to render multi-lines text.
* @class zebkit.ui.TextArea
* @constructor
* @param {String} [txt] a text
* @extends zebkit.ui.TextField
*/
pkg.TextArea = Class(pkg.TextField, [
function(txt) {
if (arguments.length === 0) {
txt = "";
}
this.$super(new zebkit.data.Text(txt));
}
]);
/**
* Password text field.
* @class zebkit.ui.PassTextField
* @constructor
* @param {String} txt password text
* @param {Integer} [maxSize] maximal size
* @param {Boolean} [showLast] indicates if last typed character should
* not be disguised with a star character
* @extends zebkit.ui.TextField
*/
pkg.PassTextField = Class(pkg.TextField, [
function(txt, size, showLast) {
if (arguments.length === 1) {
showLast = false;
size = -1;
if (zebkit.isBoolean(txt)) {
showLast = txt;
txt = "";
} else if (zebkit.isNumber(txt)) {
size = txt;
txt = "";
}
} else if (arguments.length === 0) {
showLast = false;
size = -1;
txt = "";
} else if (arguments.length === 2) {
showLast = false;
}
var pt = new zebkit.draw.PasswordText(new zebkit.data.SingleLineTxt(txt, size));
pt.showLast = showLast;
this.$super(pt);
if (size > 0) {
this.setPSByRowsCols(-1, size);
}
},
function $prototype() {
/**
* Set flag that indicates if the last password character has to be visible.
* @param {Boolean} b a boolean flag that says if last password character has
* to be visible.
* @method setShowLast
* @chainable
*/
this.setShowLast = function(b) {
if (this.showLast !== b) {
this.view.showLast = b;
this.repaint();
}
return this;
};
}
]);
/**
* Base UI list component class that has to be extended with a
* concrete list component implementation. The list component
* visualizes list data model (zebkit.data.ListModel).
* @class zebkit.ui.BaseList
* @constructor
* @param {zebkit.data.ListModel|Array} [m] a list model that should be passed as an instance
* of zebkit.data.ListModel or as an array.
* @param {Boolean} [b] true if the list navigation has to be triggered by
* pointer cursor moving
* @extends zebkit.ui.Panel
* @uses zebkit.util.Position.Metric
* @uses zebkit.ui.HostDecorativeViews
*/
/**
* Fire when a list item has been selected:
*
* list.on("selected", function(src, prev) {
* ...
* });
*
* @event selected
* @param {zebkit.ui.BaseList} src a list that triggers the event
* @param {Integer|Object} prev a previous selected index, return null if the selected item has been re-selected
*/
pkg.BaseList = Class(pkg.Panel, zebkit.util.Position.Metric, pkg.HostDecorativeViews, [
function (m, b) {
if (arguments.length === 0) {
m = [];
b = false;
} else if (arguments.length === 1) {
if (zebkit.isBoolean(m)) {
b = m;
m = [];
} else {
b = false;
}
} else if (m === null) {
m = [];
}
/**
* Currently selected list item index
* @type {Integer}
* @attribute selectedIndex
* @default -1
* @readOnly
*/
this.selectedIndex = -1;
/**
* Indicate the current mode the list items selection has to work
* @readOnly
* @default false
* @attribute isComboMode
* @type {Boolean}
*/
this.isComboMode = b;
/**
* Scroll manager
* @attribute scrollManager
* @readOnly
* @protected
* @type {zebkit.ui.ScrollManager}
*/
this.scrollManager = new pkg.ScrollManager(this);
this.$super();
// position manager should be set before model initialization
this.setPosition(new zebkit.util.Position(this));
/**
* List model
* @readOnly
* @attribute model
*/
this.setModel(m);
},
function $prototype() {
this.scrollManager = null;
/**
* Makes the component focusable
* @attribute canHaveFocus
* @type {Boolean}
* @default true
*/
this.canHaveFocus = true;
/**
* List model the component visualizes
* @attribute model
* @type {zebkit.data.ListModel}
* @readOnly
*/
this.model = null;
/**
* Position manager.
* @attribute position
* @type {zebkit.util.Position}
* @readOnly
*/
this.position = null;
/**
* Select the specified list item.
* @param {Object} v a list item to be selected. Use null as
* the parameter value to clean an item selection
* @return {Integer} an index of a selected item
* @method setValue
*/
this.setValue = function(v) {
if (v === null) {
this.select(-1);
} else if (this.model !== null) {
for(var i = 0; i < this.model.count(); i++) {
if (this.model.get(i) === v && this.isItemSelectable(i)) {
this.select(i);
return i;
}
}
}
return -1;
};
/**
* Get the list component selected item
* @return {Object} a selected item
* @method getValue
*/
this.getValue = function() {
return this.getSelected();
};
/**
* Test if the given item is selectable.
* @param {Integer} i an item index
* @return {Boolean} true if the given item is selectable
* @method isItemSelectable
*/
this.isItemSelectable = function(i) {
return true;
};
/**
* Get selected list item
* @return {Object} an item
* @method getSelected
*/
this.getSelected = function() {
return this.selectedIndex < 0 ? null
: this.model.get(this.selectedIndex);
};
/**
* Lookup a list item buy the given first character
* @param {String} ch a first character to lookup
* @return {Integer} a position of found list item in the list or -1 if no item is found.
* @method lookupItem
* @protected
*/
this.lookupItem = function(ch){
var count = this.model === null ? 0 : this.model.count();
if (zebkit.util.isLetter(ch) && count > 0){
var index = this.selectedIndex < 0 ? 0 : this.selectedIndex + 1;
ch = ch.toLowerCase();
for(var i = 0;i < count - 1; i++){
var idx = (index + i) % count,
item = this.model.get(idx).toString();
if (this.isItemSelectable(idx) && item.length > 0 && item[0].toLowerCase() === ch) {
return idx;
}
}
}
return -1;
};
/**
* Test if the given list item is selected
* @param {Integer} i an item index
* @return {Boolean} true if the item with the given index is selected
* @method isSelected
*/
this.isSelected = function(i) {
return i === this.selectedIndex;
};
/**
* Called when a pointer (pointer or finger on touch screen) is moved
* to a new location
* @param {Integer} x a pointer x coordinate
* @param {Integer} y a pointer y coordinate
* @method $pointerMoved
* @protected
*/
this.$pointerMoved = function(x, y){
if (this.isComboMode === true && this.model !== null) {
var index = this.getItemIdxAt(x, y);
if (index !== this.position.offset && (index < 0 || this.isItemSelectable(index) === true)) {
this.$triggeredByPointer = true;
if (index < 0) {
this.position.setOffset(null);
} else {
this.position.setOffset(index);
}
this.makeItemVisible(index);
this.$triggeredByPointer = false;
}
}
};
/**
* Return the given list item location.
* @param {Integer} i a list item index
* @return {Object} a location of the list item. The result is object that
* has the following structure:
{ x:{Integer}, y:{Integer} }
* @method getItemLocation
*/
this.getItemLocation = function(index) {
this.validate();
var y = this.getTop() + this.scrollManager.getSY();
for(var i = 0; i < index; i++) {
y += this.getItemSize(i).height;
}
return { x:this.getLeft(), y:y };
};
/**
* Return the given list item size.
* @param {Integer} i a list item index
* @return {Object} a size of the list item. The result is object that
* has the following structure:
{ width:{Integer}, height:{Integer} }
* @method getItemSize
*/
this.getItemSize = function(i) {
throw new Error("Not implemented");
};
this.getLines = function() {
return this.model === null ? 0 : this.model.count();
};
this.getLineSize = function(l) {
return 1;
};
this.getMaxOffset = function() {
return this.getLines() - 1;
};
this.catchScrolled = function(psx, psy) {
this.repaint();
};
/**
* Detect an item by the specified location
* @param {Integer} x a x coordinate
* @param {Integer} y a y coordinate
* @return {Integer} a list item that is located at the given position.
* -1 if no any list item can be found.
* @method getItemIdxAt
*/
this.getItemIdxAt = function(x,y) {
return -1;
};
/**
* Calculate maximal width and maximal height the items in the list have
* @protected
* @return {Integer} a max items size
* @method calcMaxItemSize
*/
this.calcMaxItemSize = function (){
var maxH = 0,
maxW = 0;
this.validate();
if (this.model !== null) {
for(var i = 0;i < this.model.count(); i++){
var is = this.getItemSize(i);
if (is.height > maxH) {
maxH = is.height;
}
if (is.width > maxW) {
maxW = is.width;
}
}
}
return { width:maxW, height:maxH };
};
/**
* Force repainting of the given list items
* @protected
* @param {Integer} p an index of the first list item to be repainted
* @param {Integer} n an index of the second list item to be repainted
* @method repaintByOffsets
*/
this.repaintByOffsets = function(p, n) {
this.validate();
var xx = this.width - this.getRight(),
l = 0,
count = this.model === null ? 0
: this.model.count();
if (p >= 0 && p < count){
l = this.getItemLocation(p);
this.repaint(l.x, l.y, xx - l.x, this.getItemSize(p).height);
}
if (n >= 0 && n < count){
l = this.getItemLocation(n);
this.repaint(l.x, l.y, xx - l.x, this.getItemSize(n).height);
}
};
/**
* Draw the given list view element identified by the given id
* on the given list item.
* @param {CanvasRenderingContext2D} g a graphical context
* @param {String} id a view id
* @param {Integer} index a list item index
* @protected
* @method drawViewAt
*/
this.drawViewAt = function(g, id, index) {
if (index >= 0 && this.views.hasOwnProperty(id) && this.views[id] !== null && this.isItemSelectable(index)) {
var is = this.getItemSize(index),
l = this.getItemLocation(index);
this.drawView(g, id, this.views[id],
l.x, l.y,
is.width ,
is.height);
}
};
/**
* Draw the given list view element identified by the given id
* at the specified location.
* @param {CanvasRenderingContext2D} g a graphical context
* @param {String} id a view id
* @param {Integer} x a x coordinate the view has to be drawn
* @param {Integer} y a y coordinate the view has to be drawn
* @param {Integer} w a view width
* @param {Integer} h a view height
* @protected
* @method drawView
*/
this.drawView = function(g, id, v, x, y, w ,h) {
this.views[id].paint(g, x, y, w, h, this);
};
this.update = function(g) {
if (this.isComboMode === true || this.hasFocus() === true) {
this.drawViewAt(g, "marker", this.position.offset);
}
this.drawViewAt(g, "select", this.selectedIndex);
};
this.paintOnTop = function(g) {
if (this.isComboMode === true || this.hasFocus()) {
this.drawViewAt(g, "topMarker", this.position.offset);
}
};
/**
* Select the given list item
* @param {Integer} index an item index to be selected
* @method select
*/
this.select = function(index){
if (index === null || index === undefined) {
throw new Error("Null index");
}
if (this.model !== null && index >= this.model.count()){
throw new RangeError(index);
}
if (this.selectedIndex !== index) {
if (index < 0 || this.isItemSelectable(index)) {
var prev = this.selectedIndex;
this.selectedIndex = index;
this.makeItemVisible(index);
this.repaintByOffsets(prev, this.selectedIndex);
this.fireSelected(prev);
}
} else {
this.fireSelected(null);
}
};
/**
* Fire selected event
* @param {Integer|null} prev a previous selected item index. null if the
* same item has been re-selected
* @method fireSelected
* @protected
*/
this.fireSelected = function(prev) {
this.fire("selected", [this, prev]);
};
this.pointerClicked = function(e) {
if (this.model !== null && e.isAction() && this.model.count() > 0) {
this.$select(this.position.offset < 0 ? 0 : this.position.offset);
}
};
this.pointerReleased = function(e){
if (this.model !== null &&
this.model.count() > 0 &&
e.isAction() &&
this.position.offset !== this.selectedIndex)
{
this.position.setOffset(this.selectedIndex);
}
};
this.pointerPressed = function(e){
if (e.isAction() && this.model !== null && this.model.count() > 0) {
var index = this.getItemIdxAt(e.x, e.y);
if (index >= 0 && this.position.offset !== index && this.isItemSelectable(index)) {
this.position.setOffset(index);
}
}
};
this.pointerDragged = this.pointerMoved = this.pointerEntered = function(e){
this.$pointerMoved(e.x, e.y);
};
this.pointerExited = function(e){
this.$pointerMoved(-10, -10);
};
this.pointerDragEnded = function(e){
if (this.model !== null && this.model.count() > 0 && this.position.offset >= 0) {
this.select(this.position.offset < 0 ? 0 : this.position.offset);
}
};
this.keyPressed = function(e){
if (this.model !== null && this.model.count() > 0){
switch(e.code) {
case "End":
if (e.ctrlKey) {
this.position.setOffset(this.position.metrics.getMaxOffset());
} else {
this.position.seekLineTo("end");
}
break;
case "Home":
if (e.ctrlKey) {
this.position.setOffset(0);
} else {
this.position.seekLineTo("begin");
}
break;
case "ArrowRight": this.position.seek(1); break;
case "ArrowDown" : this.position.seekLineTo("down"); break;
case "ArrowLeft" : this.position.seek(-1);break;
case "ArrowUp" : this.position.seekLineTo("up");break;
case "PageUp" : this.position.seek(this.pageSize(-1));break;
case "PageDown" : this.position.seek(this.pageSize(1));break;
case "Space" :
case "Enter" : this.$select(this.position.offset); break;
}
}
};
/**
* Select the given list item. The method is called when an item
* selection is triggered by a user interaction: key board, or pointer
* @param {Integer} o an item index
* @method $select
* @protected
*/
this.$select = function(o) {
this.select(o);
};
/**
* Define key typed events handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyTyped
*/
this.keyTyped = function (e){
var i = this.lookupItem(e.key);
if (i >= 0) {
this.$select(i);
}
};
this.elementInserted = function(target, e,index){
this.invalidate();
if (this.selectedIndex >= 0 && this.selectedIndex >= index) {
this.selectedIndex++;
}
this.position.inserted(index, 1);
this.repaint();
};
this.elementRemoved = function(target, e,index){
this.invalidate();
if (this.selectedIndex === index || this.model.count() === 0) {
this.select(-1);
} else {
if (this.selectedIndex > index) {
this.selectedIndex--;
}
}
this.position.removed(index, 1);
this.repaint();
};
this.elementSet = function (target, e, pe,index){
if (this.selectedIndex === index) {
this.select(-1);
}
this.vrp();
};
/**
* Find a next selectable list item starting from the given offset
* with the specified direction
* @param {Integer} off a start item index to perform search
* @param {Integer} d a direction increment. Cam be -1 or 1
* @return {Integer} a next selectable item index
* @method findSelectable
* @protected
*/
this.findSelectable = function(off, d) {
var c = this.model.count(), i = 0, dd = Math.abs(d);
while (this.isItemSelectable(off) === false && i < c) {
off = (c + off + d) % c;
i += dd;
}
return i < c ? off : -1;
};
this.posChanged = function (target, prevOffset, prevLine, prevCol) {
var off = this.position.offset;
if (off >= 0) {
off = this.findSelectable(off, prevOffset < off ? 1 : -1);
if (off !== this.position.offset) {
this.position.setOffset(off);
this.repaintByOffsets(prevOffset, off);
return;
}
}
if (this.isComboMode === true) {
this.makeItemVisible(off);
} else {
this.select(off);
}
// this.makeItemVisible(off);
this.repaintByOffsets(prevOffset, off);
};
/**
* Set the list model to be rendered with the list component
* @param {zebkit.data.ListModel} m a list model
* @method setModel
* @chainable
*/
this.setModel = function (m){
if (m !== this.model) {
if (m !== null && Array.isArray(m)) {
m = new zebkit.data.ListModel(m);
}
if (this.model !== null) {
this.model.off(this);
}
this.model = m;
if (this.model !== null) {
this.model.on(this);
}
this.vrp();
}
return this;
};
/**
* Set the given position controller. List component uses position to
* track virtual cursor.
* @param {zebkit.util.Position} c a position
* @method setPosition
* @chainable
*/
this.setPosition = function(c) {
if (c !== this.position) {
if (this.position !== null) {
this.position.off(this);
}
this.position = c;
this.position.on(this);
this.position.setMetric(this);
this.repaint();
}
return this;
};
/**
* Set the list items view provider. Defining a view provider allows developers
* to customize list item rendering.
* @param {Object|Function} v a view provider class instance or a function that
* says which view has to be used for the given list model data. The function
* has to satisfy the following method signature: "function(list, modelItem, index)"
* @method setViewProvider
* @chainable
*/
this.setViewProvider = function (v){
if (this.provider !== v) {
if (typeof v === "function") {
var o = new zebkit.Dummy();
o.getView = v;
v = o;
}
this.provider = v;
this.vrp();
}
return this;
};
/**
* Scroll if necessary the given item to make it visible
* @param {Integer} index an item index
* @chainable
* @method makeItemVisible
*/
this.makeItemVisible = function (index){
if (index >= 0 && this.scrollManager !== null) {
this.validate();
var is = this.getItemSize(index);
if (is.width > 0 && is.height > 0) {
var l = this.getItemLocation(index);
this.scrollManager.makeVisible(l.x - this.scrollManager.getSX(),
l.y - this.scrollManager.getSY(),
is.width, is.height);
}
}
return this;
};
this.makeSelectedVisible = function(){
if (this.selectedIndex >= 0) {
this.makeItemVisible(this.selectedIndex);
}
return this;
};
/**
* The method returns the page size that has to be scroll up or down
* @param {Integer} d a scrolling direction. -1 means scroll up, 1 means
* scroll down
* @return {Integer} a number of list items to be scrolled
* @method pageSize
* @protected
*/
this.pageSize = function(d){
var offset = this.position.offset;
if (offset >= 0) {
var vp = pkg.$cvp(this, {});
if (vp !== null) {
var sum = 0, i = offset;
for(;i >= 0 && i <= this.position.metrics.getMaxOffset() && sum < vp.height; i += d){
sum += (this.getItemSize(i).height);
}
return i - offset - d;
}
}
return 0;
};
},
/**
* Sets the views for the list visual elements. The following elements are
* supported:
*
* - "select" - a selection view element
* - "topMarker" - a position marker view element that is rendered on top of list item
* - "marker" - a position marker view element
*
* @param {Object} views view elements
* @method setViews
*/
function focused(){
this.$super();
this.repaint();
}
]).events("selected");
/**
* The class is list component implementation that visualizes zebkit.data.ListModel.
* It is supposed the model can have any type of items. Visualization of the items
* is customized by defining a view provider.
*
* The general use case:
*
* // create list component that contains three item
* var list = new zebkit.ui.List([
* "Item 1",
* "Item 2",
* "Item 3"
* ]);
*
* ...
* // add new item
* list.model.add("Item 4");
*
* ...
* // remove first item
* list.model.removeAt(0);
*
*
* To customize list items views you can redefine item view provider as following:
*
* // suppose every model item is an array that contains two elements,
* // first element points to the item icon and the second element defines
* // the list item text
* var list = new zebkit.ui.List([
* [ "icon1.gif", "Caption 1" ],
* [ "icon2.gif", "Caption 1" ],
* [ "icon3.gif", "Caption 1" ]
* ]);
*
* // define new list item views provider that represents every
* // list model item as icon with a caption
* list.setViewProvider(new zebkit.ui.List.ViewProvider([
* function getView(target, i, value) {
* var caption = value[1];
* var icon = value[0];
* return new zebkit.ui.CompRender(new zebkit.ui.ImageLabel(caption, icon));
* }
* ]));
*
* @class zebkit.ui.List
* @extends zebkit.ui.BaseList
* @constructor
* @param {zebkit.data.ListModel|Array} [model] a list model that should be passed as an instance
* of zebkit.data.ListModel or as an array.
* @param {Boolean} [isComboMode] true if the list navigation has to be triggered by
* pointer cursor moving
*/
pkg.List = Class(pkg.BaseList, [
function (m, b){
/**
* Index of the first visible list item
* @readOnly
* @attribute firstVisible
* @type {Integer}
* @private
*/
this.firstVisible = -1;
/**
* Y coordinate of the first visible list item
* @readOnly
* @attribute firstVisibleY
* @type {Integer}
* @private
*/
this.firstVisibleY = this.psWidth_ = this.psHeight_ = 0;
/**
* Internal flag to track list items visibility status. It is set
* to false to trigger list items metrics and visibility recalculation
* @attribute visValid
* @type {Boolean}
* @private
*/
this.visValid = false;
this.setViewProvider(new this.clazz.ViewProvider());
this.$supera(arguments);
},
function $clazz() {
/**
* List view provider class. This implementation renders list item using string
* render. If a list item is an instance of "zebkit.draw.View" class than it will
* be rendered as the view.
* @class zebkit.ui.List.ViewProvider
* @extends zebkit.draw.BaseViewProvider
* @constructor
*/
this.ViewProvider = Class(zebkit.draw.BaseViewProvider, []);
this.Item = Class([
function(value, caption) {
this.value = value;
if (arguments.length > 1) {
this.caption = caption;
} else {
this.caption = value;
}
},
function $prototype() {
this.toString = function() {
return this.caption;
};
}
]);
this.ItemViewProvider = Class(zebkit.draw.BaseViewProvider, [
function getView(t, i, v) {
if (v !== null) {
if (typeof v.getCaption === 'function') {
v = v.getCaption();
} else if (v.caption !== undefined) {
v = v.caption;
}
}
return this.$super(t, i, v);
}
]);
/**
* @for zebkit.ui.List
*/
},
function $prototype() {
this.heights = this.widths = this.vArea = null;
/**
* Extra list item side gaps
* @type {Integer}
* @attribute gap
* @default 2
* @readOnly
*/
this.gap = 2;
/**
* Set the left, right, top and bottom a list item paddings
* @param {Integer} g a left, right, top and bottom a list item paddings
* @method setItemGap
* @chainable
*/
this.setItemGap = function(g){
if (this.gap !== g){
this.gap = g;
this.vrp();
}
return this;
};
this.paint = function(g){
this.vVisibility();
if (this.firstVisible >= 0){
var sx = this.scrollManager.getSX(),
sy = this.scrollManager.getSY();
try {
g.translate(sx, sy);
var y = this.firstVisibleY,
x = this.getLeft(),
yy = this.vArea.y + this.vArea.height - sy,
count = this.model.count(),
dg = this.gap * 2;
for (var i = this.firstVisible; i < count; i++){
if (i !== this.selectedIndex && typeof this.provider.getCellColor === 'function') {
var bc = this.provider.getCellColor(this, i);
if (bc !== null) {
g.setColor(bc);
g.fillRect(x, y, this.width, this.heights[i]);
}
}
this.provider.getView(this, i, this.model.get(i))
.paint(g, x + this.gap, y + this.gap,
this.widths[i] - dg,
this.heights[i]- dg, this);
y += this.heights[i];
if (y > yy) {
break;
}
}
g.translate(-sx, -sy);
} catch(e) {
g.translate(-sx, -sy);
throw e;
}
}
};
this.setColor = function(c) {
this.provider.setColor(c);
return this;
};
this.setFont = function(f) {
this.provider.setFont(f);
return this;
};
this.recalc = function(){
this.psWidth_ = this.psHeight_ = 0;
if (this.model !== null) {
var count = this.model.count();
if (this.heights === null || this.heights.length !== count) {
this.heights = Array(count);
}
if (this.widths === null || this.widths.length !== count) {
this.widths = Array(count);
}
var provider = this.provider;
if (provider !== null) {
var dg = 2 * this.gap;
for(var i = 0;i < count; i++){
var ps = provider.getView(this, i, this.model.get(i)).getPreferredSize();
this.heights[i] = ps.height + dg;
this.widths [i] = ps.width + dg;
if (this.widths[i] > this.psWidth_) {
this.psWidth_ = this.widths[i];
}
this.psHeight_ += this.heights[i];
}
}
}
};
this.calcPreferredSize = function(l){
return { width : this.psWidth_,
height: this.psHeight_ };
};
this.vVisibility = function(){
this.validate();
var prev = this.vArea;
this.vArea = pkg.$cvp(this, {});
if (this.vArea === null) {
this.firstVisible = -1;
} else {
if (this.visValid === false ||
(prev === null || prev.x !== this.vArea.x ||
prev.y !== this.vArea.y || prev.width !== this.vArea.width ||
prev.height !== this.vArea.height))
{
var top = this.getTop();
if (this.firstVisible >= 0){
var dy = this.scrollManager.getSY();
while (this.firstVisibleY + dy >= top && this.firstVisible > 0){
this.firstVisible--;
this.firstVisibleY -= this.heights[this.firstVisible];
}
} else {
this.firstVisible = 0;
this.firstVisibleY = top;
}
if (this.firstVisible >= 0) {
var count = this.model === null ? 0 : this.model.count(),
hh = this.height - this.getBottom();
for(; this.firstVisible < count; this.firstVisible++) {
var y1 = this.firstVisibleY + this.scrollManager.getSY(),
y2 = y1 + this.heights[this.firstVisible] - 1;
if ((y1 >= top && y1 < hh) || (y2 >= top && y2 < hh) || (y1 < top && y2 >= hh)) {
break;
}
this.firstVisibleY += (this.heights[this.firstVisible]);
}
if (this.firstVisible >= count) {
this.firstVisible = -1;
}
}
this.visValid = true;
}
}
};
this.getItemLocation = function(index){
this.validate();
var y = this.getTop() + this.scrollManager.getSY();
for(var i = 0; i < index; i++) {
y += this.heights[i];
}
return { x:this.getLeft(), y : y };
};
this.getItemSize = function(i){
this.validate();
return { width:this.widths[i], height:this.heights[i] };
};
this.getItemIdxAt = function(x,y){
this.vVisibility();
if (this.vArea !== null && this.firstVisible >= 0) {
var yy = this.firstVisibleY + this.scrollManager.getSY(),
hh = this.height - this.getBottom(),
count = this.model.count();
for (var i = this.firstVisible; i < count; i++) {
if (y >= yy && y < yy + this.heights[i]) {
return i;
}
yy += (this.heights[i]);
if (yy > hh) {
break;
}
}
}
return -1;
};
},
function invalidate(){
this.visValid = false;
this.firstVisible = -1;
this.$super();
},
function drawView(g,id,v,x,y,w,h) {
this.$super(g, id, v, x, y, this.width - this.getRight() - x, h);
},
function catchScrolled(psx,psy){
this.firstVisible = -1;
this.visValid = false;
this.$super(psx, psy);
}
]);
/**
* List component consider its children UI components as a list model items. Every added to the component
* UI children component becomes a list model element. The implementation allows developers to use
* other UI components as its elements what makes list item view customization very easy and powerful:
*
* // use image label as the component list items
* var list = new zebkit.ui.CompList();
* list.add(new zebkit.ui.ImageLabel("Caption 1", "icon1.gif"));
* list.add(new zebkit.ui.ImageLabel("Caption 2", "icon2.gif"));
* list.add(new zebkit.ui.ImageLabel("Caption 3", "icon3.gif"));
*
*
* @class zebkit.ui.CompList
* @constructor
* @extends zebkit.ui.BaseList
* @param {Boolean} [isComboMode] true if the list navigation has to be triggered by
* pointer cursor moving
*/
pkg.CompList = Class(pkg.BaseList, [
function (b) {
this.model = this;
this.setViewProvider(new zebkit.Dummy([
function $prototype() {
this.render = new pkg.CompRender();
this.getView = function (target,i, obj) {
this.render.setValue(obj);
return this.render;
};
}
]));
this.$supera(arguments);
},
function $clazz() {
this.Label = Class(pkg.Label, []);
this.ImageLabel = Class(pkg.ImageLabel, []);
this.ScrollableLayout = Class(pkg.ScrollManager, [
function(layout, target) {
this.layout = layout;
this.$super(target);
},
function $prototype() {
this.calcPreferredSize = function(t) {
return this.layout.calcPreferredSize(t);
};
this.doLayout = function(t){
this.layout.doLayout(t);
for(var i = 0; i < t.kids.length; i++){
var kid = t.kids[i];
if (kid.isVisible === true) {
kid.setLocation(kid.x + this.getSX(),
kid.y + this.getSY());
}
}
};
this.scrollStateUpdated = function(sx,sy,px,py){
this.target.vrp();
};
}
]);
},
function $prototype() {
this.max = null;
this.get = function(i) {
if (i < 0 || i >= this.kids.length) {
throw new RangeError(i);
}
return this.kids[i];
};
this.contains = function (c) {
return this.indexOf(c) >= 0;
};
this.count = function () {
return this.kids.length;
};
this.catchScrolled = function(px, py) {};
this.getItemLocation = function(i) {
return { x:this.kids[i].x, y:this.kids[i].y };
};
this.getItemSize = function(i) {
return this.kids[i].isVisible === false ? { width:0, height: 0 }
: { width:this.kids[i].width,
height:this.kids[i].height};
};
this.recalc = function (){
this.max = zebkit.layout.getMaxPreferredSize(this);
};
this.calcMaxItemSize = function() {
this.validate();
return { width:this.max.width, height:this.max.height };
};
this.getItemIdxAt = function(x, y) {
return zebkit.layout.getDirectAt(x, y, this);
};
this.isItemSelectable = function(i) {
return this.model.get(i).isVisible === true &&
this.model.get(i).isEnabled === true;
};
this.catchInput = function(child){
if (this.isComboMode !== true) {
var p = child;
while (p !== this) {
if (p.stopCatchInput === true) {
return false;
}
p = p.parent;
}
}
return true;
};
this.makeComponent = function(e) {
return new pkg.Label(e.toString());
};
},
function setModel(m) {
if (Array.isArray(m)) {
for(var i = 0; i < m.length; i++) {
this.add(m[i]);
}
} else {
throw new Error("Model cannot be updated");
}
return this;
},
function elementInserted(target, e, index) {
this.insert(index, null, this.makeComponent(e));
this.$super(target, e, index);
},
function setPosition(c) {
if (c !== this.position){
this.$super(c);
if (zebkit.instanceOf(this.layout, zebkit.util.Position.Metric)) {
c.setMetric(this.layout);
}
}
return this;
},
function setLayout(layout){
if (layout !== this.layout){
this.scrollManager = new this.clazz.ScrollableLayout(layout, this);
this.$super(this.scrollManager);
if (this.position !== null) {
this.position.setMetric(zebkit.instanceOf(layout, zebkit.util.Position.Metric) ? layout : this);
}
}
return this;
},
function setAt(i, item) {
if (i < 0 || i >= this.kids.length) {
throw new RangeError(i);
}
return this.$super(i, item);
},
function insert(i, constr, e) {
if (arguments.length === 2) {
e = constr;
constr = null;
}
if (i < 0 || i > this.kids.length) {
throw new RangeError(i);
}
return this.$super(i, constr, pkg.$component(e, this));
},
function kidAdded(index,constr,comp){
this.$super(index, constr, comp);
if (this.model === this) {
this.model.fire("elementInserted", [this, comp, index]);
}
},
function kidRemoved(index, e, xtr) {
this.$super(index, e, ctr);
if (this.model === this) {
this.model.fire("elementRemoved", [this, e, index]);
}
}
]).events("elementInserted", "elementRemoved", "elementSet");
/**
* Combo box UI component class. Combo uses a list component to show in drop down window.
* You can use any available list component implementation:
// use simple list as combo box drop down window
var combo = new zebkit.ui.Combo(new zebkit.ui.List([
"Item 1",
"Item 2",
"Item 3"
]));
// use component list as combo box drop down window
var combo = new zebkit.ui.Combo(new zebkit.ui.CompList([
"Item 1",
"Item 2",
"Item 3"
]));
// let combo box decides which list component has to be used
var combo = new zebkit.ui.Combo([
"Item 1",
"Item 2",
"Item 3"
]);
* @class zebkit.ui.Combo
* @extends zebkit.ui.Panel
* @constructor
* @param {Array|zebkit.ui.BaseList} data an combo items array or a list component
*/
/**
* Fired when a new value in a combo box component has been selected
combo.on("selected", function(combo, value) {
...
});
* @event selected
* @param {zebkit.ui.Combo} combo a combo box component where a new value
* has been selected
* @param {Object} value a previously selected index
*/
/**
* Implement the event handler method to detect when a combo pad window
* is shown or hidden
var p = new zebkit.ui.Combo();
p.padShown = function(src, b) { ... }; // add event handler
* @event padShown
* @param {zebkit.ui.Combo} src a combo box component that triggers the event
* @param {Boolean} b a flag that indicates if the combo pad window has been
* shown (true) or hidden (false)
*/
pkg.Combo = Class(pkg.Panel, [
function(list, editable) {
if (arguments.length === 1 && zebkit.isBoolean(list)) {
editable = list;
list = null;
}
if (arguments.length === 0) {
editable = false;
}
if (arguments.length === 0 || list === null) {
list = new this.clazz.List(true);
}
/**
* Reference to combo box list component
* @attribute list
* @readOnly
* @type {zebkit.ui.BaseList}
*/
if (zebkit.instanceOf(list, pkg.BaseList) === false) {
list = list.length > 0 && zebkit.instanceOf(list[0], pkg.Panel) ? new this.clazz.CompList(list, true)
: new this.clazz.List(list, true);
}
/**
* Maximal size the combo box height can have
* @attribute maxPadHeight
* @readOnly
* @type {Integer}
*/
this.maxPadHeight = 0;
this.$lockListSelEvent = false;
this.setList(list);
this.$super();
this.add("center", editable ? new this.clazz.EditableContentPan()
: new this.clazz.ReadonlyContentPan());
this.add("right", new this.clazz.ArrowButton());
},
function $clazz() {
/**
* UI panel class that is used to implement combo box content area
* @class zebkit.ui.Combo.ContentPan
* @extends zebkit.ui.Panel
* @constructor
*/
this.ContentPan = Class(pkg.Panel, [
function $prototype() {
/**
* Called whenever the given combo box value has been updated with the specified
* value. Implement the method to synchronize content panel with updated combo
* box value
* @method comboValueUpdated
* @param {zebkit.ui.Combo} combo a combo box component that has been updated
* @param {Object} value a value with which the combo box has been updated
*/
this.comboValueUpdated = function(combo, value) {};
/**
* Indicates if the content panel is editable. Set the property to true
* to indicate the content panel implementation is editable. Editable
* means the combo box content can be editable by a user
* @attribute isEditable
* @type {Boolean}
* @readOnly
* @default undefined
*/
/**
* Get a combo box the content panel belongs
* @method getCombo
* @return {zebkit.ui.Combo} a combo the content panel belongs
*/
this.getCombo = function() {
for (var p = this.parent; p !== null && zebkit.instanceOf(p, pkg.Combo) === false; p = p.parent) {}
return p;
};
}
]);
/**
* Combo box list pad component class
* @extends zebkit.ui.ScrollPan
* @class zebkit.ui.Combo.ComboPadPan
* @constructor
* @param {zebkit.ui.Panel} c a target component
*/
this.ComboPadPan = Class(pkg.ScrollPan, [
function $prototype() {
this.$closeTime = 0;
this.owner = null;
/**
* A reference to combo that uses the list pad component
* @attribute owner
* @type {zebkit.ui.Combo}
* @readOnly
*/
this.childKeyPressed = function(e){
if (e.code === "Escape" && this.parent !== null) {
this.removeMe();
if (this.owner !== null) {
this.owner.requestFocus();
}
}
};
},
function setParent(l) {
this.$super(l);
if (l === null && this.owner !== null) {
this.owner.requestFocus();
}
this.$closeTime = (l === null ? new Date().getTime() : 0);
}
]);
/**
* Read-only content area combo box component panel class
* @extends zebkit.ui.Combo.ContentPan
* @constructor
* @class zebkit.ui.Combo.ReadonlyContentPan
*/
this.ReadonlyContentPan = Class(this.ContentPan, [
function $prototype() {
this.calcPsByContent = false;
this.getCurrentView = function() {
var list = this.getCombo().list,
selected = list.getSelected();
return selected !== null ? list.provider.getView(list, list.selectedIndex, selected)
: null;
};
this.paintOnTop = function(g){
var v = this.getCurrentView();
if (v !== null) {
var ps = v.getPreferredSize();
v.paint(g, this.getLeft(),
this.getTop() + Math.floor((this.height - this.getTop() - this.getBottom() - ps.height) / 2),
this.width, ps.height, this);
}
};
this.setCalcPsByContent = function(b) {
if (this.calcPsByContent !== b) {
this.calcPsByContent = b;
this.vrp();
}
return this;
};
this.calcPreferredSize = function(l) {
var p = this.getCombo();
if (p !== null && this.calcPsByContent !== true) {
return p.list.calcMaxItemSize();
} else {
var cv = this.getCurrentView();
return cv === null ? { width: 0, height: 0} : cv.getPreferredSize();
}
};
this.comboValueUpdated = function(combo, value) {
if (this.calcPsByContent === true) {
this.invalidate();
}
};
}
]);
/**
* Editable content area combo box component panel class
* @class zebkit.ui.Combo.EditableContentPan
* @constructor
* @extends zebkit.ui.Combo.ContentPan
* @uses zebkit.EventProducer
*/
/**
* Fired when a content value has been updated.
content.on(function(contentPan, newValue) {
...
});
* @param {zebkit.ui.Combo.ContentPan} contentPan a content panel that
* updated its value
* @param {Object} newValue a new value the content panel has been set
* with
* @event contentUpdated
*/
this.EditableContentPan = Class(this.ContentPan, [
function() {
this.$super();
/**
* A reference to a text field component the content panel uses as a
* value editor
* @attribute textField
* @readOnly
* @private
* @type {zebkit.ui.TextField}
*/
this.textField = new this.clazz.TextField("", -1);
this.textField.view.target.on(this);
this.add("center", this.textField);
},
function $clazz() {
this.TextField = Class(pkg.TextField, []);
},
function $prototype() {
this.isEditable = true;
this.dontGenerateUpdateEvent = false;
this.canHaveFocus = true;
this.textUpdated = function(e){
if (this.dontGenerateUpdateEvent === false && e.transaction === false) {
this.fire("contentUpdated", [this, this.textField.getValue()]);
}
};
/**
* Called when the combo box content has been updated
* @param {zebkit.ui.Combo} combo a combo where the new value has been set
* @param {Object} v a new combo box value
* @method comboValueUpdated
*/
this.comboValueUpdated = function(combo, v){
this.dontGenerateUpdateEvent = true;
try {
var txt = (v === null ? "" : v.toString());
this.textField.setValue(txt);
this.textField.select(0, txt.length);
} finally {
this.dontGenerateUpdateEvent = false;
}
};
},
function focused(){
this.$super();
this.textField.requestFocus();
}
]);
this.ArrowButton = Class(pkg.ArrowButton, [
function() {
this.setFireParams(true, -1);
this.$super();
}
]);
this.List = Class(pkg.List, []);
this.CompList = Class(pkg.CompList, []);
},
/**
* @for zebkit.ui.Combo
*/
function $prototype() {
/**
* Reference to combo box winpad list component
* @attribute list
* @readOnly
* @type {zebkit.ui.BaseList}
*/
this.list = null;
/**
* Reference to combo box button component
* @attribute button
* @readOnly
* @type {zebkit.ui.Panel}
*/
this.button = null;
/**
* Reference to combo box content component
* @attribute content
* @readOnly
* @type {zebkit.ui.Panel}
*/
this.content = null;
/**
* Reference to combo box pad component
* @attribute winpad
* @readOnly
* @type {zebkit.ui.Panel}
*/
this.winpad = null;
/**
* Reference to selection view
* @attribute selectView
* @readOnly
* @type {zebkit.draw.View}
*/
this.selectView = null;
/**
* A component the combo win pad has to be adjusted.
* @attribute adjustPadTo
* @default null
* @type {zebkit.ui.Panel}
*/
this.adjustPadTo = null;
this.paint = function(g){
if (this.content !== null &&
this.selectView !== null &&
this.hasFocus())
{
this.selectView.paint(g, this.content.x,
this.content.y,
this.content.width,
this.content.height,
this);
}
};
this.catchInput = function (child) {
return child !== this.button && (this.content === null || this.content.isEditable !== true);
};
this.canHaveFocus = function() {
return this.winpad.parent === null && (this.content !== null && this.content.isEditable !== true);
};
this.contentUpdated = function(src, text){
if (src === this.content) {
try {
this.$lockListSelEvent = true;
if (text === null) {
this.list.select(-1);
} else {
var m = this.list.model;
for(var i = 0;i < m.count(); i++){
var mv = m.get(i);
if (mv !== text) {
this.list.select(i);
break;
}
}
}
} finally {
this.$lockListSelEvent = false;
}
this.fire("selected", [ this, text ]);
}
};
/**
* Select the given value from the list as the combo box value
* @param {Integer} i an index of a list element to be selected
* as the combo box value
* @method select
* @chainable
*/
this.select = function(i) {
this.list.select(i);
return this;
};
// This method has been added to support selectedIndex property setter
this.setSelectedIndex = function(i) {
this.select(i);
return this;
};
/**
* Set combo box value selected value.
* @param {Object} v a value
* @method setValue
* @chainable
*/
this.setValue = function(v) {
this.list.setValue(v);
return this;
};
/**
* Get the current combo box selected value
* @return {Object} a value
* @method getValue
*/
this.getValue = function() {
return this.list.getValue();
};
/**
* Define pointer pressed events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerPressed
*/
this.pointerPressed = function (e) {
if (e.isAction() && this.content !== null &&
(new Date().getTime() - this.winpad.$closeTime) > 100 &&
e.x > this.content.x && e.y > this.content.y &&
e.x < this.content.x + this.content.width &&
e.y < this.content.y + this.content.height )
{
this.showPad();
}
};
/**
* Test if the combo window pad is shown
* @return {Boolean} true if the combo window pad is shown
* @method isPadShown
*/
this.isPadShown = function() {
return this.winpad !== null && this.winpad.parent !== null && this.winpad.isVisible === true;
};
/**
* Hide combo drop down list
* @method hidePad
* @chainable
*/
this.hidePad = function() {
if (this.winpad !== null && this.winpad.parent !== null) {
this.winpad.removeMe();
var d = this.getCanvas();
if (d !== null) {
this.requestFocus();
}
}
return this;
};
/**
* Show combo drop down list
* @method showPad
* @chainable
*/
this.showPad = function() {
var canvas = this.getCanvas();
if (canvas !== null) {
var ps = this.winpad.getPreferredSize(),
p = zebkit.layout.toParentOrigin(0, 0, this.adjustPadTo === null ? this : this.adjustPadTo),
py = p.y;
// if (this.winpad.hbar && ps.width > this.width) {
// ps.height += this.winpad.hbar.getPreferredSize().height;
// }
if (this.maxPadHeight > 0 && ps.height > this.maxPadHeight) {
ps.height = this.maxPadHeight;
}
if (py + this.height + ps.height > canvas.height) {
if (py - ps.height >= 0) {
py -= (ps.height + this.height);
} else {
var hAbove = canvas.height - py - this.height;
if (py > hAbove) {
ps.height = py;
py -= (ps.height + this.height);
} else {
ps.height = hAbove;
}
}
}
this.winpad.setBounds(p.x,
py + (this.adjustPadTo === null ? this.height
: this.adjustPadTo.height),
this.adjustPadTo === null ? this.width
: this.adjustPadTo.width,
ps.height);
this.list.makeItemVisible(this.list.selectedIndex);
canvas.getLayer(pkg.PopupLayerMix.id).add(this, this.winpad);
this.list.requestFocus();
if (this.padShown !== undefined) {
this.padShown(true);
}
return this;
}
};
/**
* Bind the given list component to the combo box component.
* @param {zebkit.ui.BaseList} l a list component
* @method setList
* @chainable
*/
this.setList = function(l){
if (this.list !== l) {
this.hidePad();
if (this.list !== null) {
this.list.off("selected", this);
}
this.list = l;
if (this.list !== null) {
this.list.on("selected", this);
}
var $this = this;
this.winpad = new this.clazz.ComboPadPan(this.list, [
function setParent(p) {
this.$super(p);
if ($this.padShown !== undefined) {
$this.padShown($this, p !== null);
}
}
]);
this.winpad.owner = this;
if (this.content !== null) {
this.content.comboValueUpdated(this, this.list.getSelected());
}
this.vrp();
}
return this;
};
/**
* Define key pressed events handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function (e) {
if (this.list !== null && this.list.model !== null) {
var index = this.list.selectedIndex;
switch(e.code) {
case "Enter" : this.showPad(); break;
case "ArrowLeft" :
case "ArrowUp" : if (index > 0) {
this.list.select(index - 1);
} break;
case "ArrowDown" :
case "ArrowRight": if (this.list.model.count() - 1 > index) {
this.list.select(index + 1);
} break;
}
}
};
/**
* Define key typed events handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyTyped
*/
this.keyTyped = function(e) {
this.list.keyTyped(e);
};
/**
* Set the given combo box selection view
* @param {zebkit.draw.View} c a view
* @method setSelectView
* @chainable
*/
this.setSelectView = function (c){
if (c !== this.selectView) {
this.selectView = zebkit.graphics.$view(c);
this.repaint();
}
return this;
};
/**
* Set the maximal height of the combo box pad element.
* @param {Integer} h a maximal combo box pad size
* @method setMaxPadHeight
* @chainable
*/
this.setMaxPadHeight = function(h){
if (this.maxPadHeight !== h) {
this.hidePad();
this.maxPadHeight = h;
}
return this;
};
/**
* Make the commbo editable
* @param {Boolean} b true to make the combo ediatable
* @chainable
* @method setEditable
*/
this.setEditable = function(b) {
if (this.content === null || this.content.isEditable !== b) {
var ctr = "center";
if (this.content !== null) {
ctr = this.content.constraints;
this.content.removeMe();
}
this.add(ctr, b ? new this.clazz.EditableContentPan()
: new this.clazz.ReadonlyContentPan());
}
return this;
};
/**
* Combo box button listener method. The method triggers showing
* combo box pad window when the combo button has been pressed
* @param {zebkit.ui.Button} src a button that has been pressed
* @method fired
*/
this.fired = function(src) {
if ((new Date().getTime() - this.winpad.$closeTime) > 100) {
this.showPad();
}
};
/**
* Combo pad list listener method. Called every time an item in
* combo pad list has been selected.
* @param {zebkit.ui.BaseList} src a list
* @param {Integer} data a selected index
* @method selected
* @protected
*/
this.selected = function(src, data) {
if (this.$lockListSelEvent === false) {
this.hidePad();
if (this.content !== null) {
this.content.comboValueUpdated(this, this.list.getSelected());
if (this.content.isEditable === true) {
this.content.requestFocus();
}
this.repaint();
}
this.fire("selected", [ this, data ]);
}
};
},
function focused(){
this.$super();
this.repaint();
},
function kidAdded(index, s, c){
if (zebkit.instanceOf(c, pkg.Combo.ContentPan)) {
if (this.content !== null) {
throw new Error("Content panel is set");
}
this.content = c;
if (this.list !== null) {
c.comboValueUpdated(this, this.list.getSelected());
}
} else if (this.button === null) {
this.button = c;
}
if (c.isEventFired() ) {
c.on(this);
}
this.$super(index, s, c);
},
function kidRemoved(index, l, ctr) {
if (l.isEventFired()) {
l.off(this);
}
if (this.content === l) {
this.content = null;
} else if (this.button === l) {
this.button = null;
}
this.$super(index, l, ctr);
},
function setVisible(b) {
if (b === false) {
this.hidePad();
}
this.$super(b);
return this;
},
function setParent(p) {
if (p === null) {
this.hidePad();
}
this.$super(p);
}
]).events("selected");
/**
* Border panel UI component class. The component renders titled border around the
* given content UI component. Border title can be placed on top or
* bottom border line and aligned horizontally (left, center, right). Every
* zebkit UI component can be used as a border title element.
* @param {zebkit.ui.Panel|String} [title] a border panel title. Can be a
* string or any other UI component can be used as the border panel title
* @param {zebkit.ui.Panel} [content] a content UI component of the border
* panel
* @param {Integer} [constraints] a title constraints. The constraints gives
* a possibility to place border panel title in different places. Generally
* the title can be placed on the top or bottom part of the border panel.
* Also the title can be aligned horizontally.
*
* @example
*
* // create border panel with a title located at the
* // top and aligned at the canter
* var bp = new zebkit.ui.BorderPan("Title",
* new zebkit.ui.Panel(),
* "top", "center");
*
* @constructor
* @class zebkit.ui.BorderPan
* @extends zebkit.ui.Panel
*/
pkg.BorderPan = Class(pkg.Panel, [
function(title, center, o, a) {
if (arguments.length > 0) {
title = pkg.$component(title, this);
}
if (arguments.length > 2) {
this.orient = o;
}
if (arguments.length > 3) {
this.alignment = a;
}
this.$super();
if (arguments.length > 0) {
this.add("caption", title);
}
if (arguments.length > 1) {
this.add("center", center);
}
},
function $clazz() {
this.Label = Class(pkg.Label, []);
this.ImageLabel = Class(pkg.ImageLabel, []);
this.Checkbox = Class(pkg.Checkbox, []);
},
function $prototype() {
/**
* Border panel label content component
* @attribute content
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.content = null;
/**
* Border panel label component
* @attribute label
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.label = null;
/**
* Vertical gap. Define top and bottom paddings between
* border panel border and the border panel content
* @attribute vGap
* @type {Integer}
* @readOnly
* @default 0
*/
/**
* Horizontal gap. Define left and right paddings between
* border panel border and the border panel content
* @attribute hGap
* @type {Integer}
* @readOnly
* @default 0
*/
this.vGap = this.hGap = 2;
/**
* Border panel label indent
* @type {Integer}
* @attribute indent
* @readOnly
* @default 4
*/
this.indent = 4;
/**
* Border panel title area arrangement. Border title can be placed
* either at the top or bottom area of border panel component.
* @type {String}
* @attribute orient
* @readOnly
* @default "top"
*/
this.orient = "top";
/**
* Border panel title horizontal alignment.
* @type {String}
* @attribute alignment
* @readOnly
* @default "left"
*/
this.alignment = "left";
/**
* Get the border panel title info. The information
* describes a rectangular area the title occupies, the
* title location and alignment
* @return {Object} a title info
*
* {
* x: {Integer}, y: {Integer},
* width: {Integer}, height: {Integer},
* orient: {Integer}
* }
*
* @method getTitleInfo
* @protected
*/
this.getTitleInfo = function() {
return (this.label !== null) ? { x : this.label.x,
y : this.label.y,
width : this.label.width,
height : this.label.height,
orient: this.orient }
: null;
};
this.calcPreferredSize = function(target){
var ps = this.content !== null && this.content.isVisible === true ? this.content.getPreferredSize()
: { width:0, height:0 };
if (this.label !== null && this.label.isVisible === true){
var lps = this.label.getPreferredSize();
ps.height += lps.height;
ps.width = Math.max(ps.width, lps.width + this.indent);
}
ps.width += (this.hGap * 2);
ps.height += (this.vGap * 2);
return ps;
};
this.doLayout = function (target){
var h = 0,
right = this.getRight(),
left = this.getLeft(),
top = this.orient === "top" ? this.top : this.getTop(),
bottom = this.orient === "bottom"? this.bottom : this.getBottom();
if (this.label !== null && this.label.isVisible === true){
var ps = this.label.getPreferredSize();
h = ps.height;
this.label.setBounds((this.alignment === "left") ? left + this.indent
: ((this.alignment === "right") ? this.width - right - ps.width - this.indent
: Math.floor((this.width - ps.width) / 2)),
(this.orient === "bottom") ? (this.height - bottom - ps.height) : top,
ps.width, h);
}
if (this.content !== null && this.content.isVisible === true){
this.content.setBounds(left + this.hGap,
(this.orient === "bottom" ? top : top + h) + this.vGap,
this.width - right - left - 2 * this.hGap,
this.height - top - bottom - h - 2 * this.vGap);
}
};
/**
* Set vertical and horizontal paddings between the border panel border and the content
* of the border panel
* @param {Integer} vg a top and bottom paddings
* @param {Integer} hg a left and right paddings
* @method setGaps
* @chainable
*/
this.setGaps = function(vg, hg){
if (this.vGap !== vg || hg !== this.hGap){
this.vGap = vg;
this.hGap = hg;
this.vrp();
}
return this;
};
/**
* Set border panel title orientation. The title area can be
* placed either at the top or at the bottom of border panel
* component.
* @param {String} o a border title orientation. Can be "top" or "bottom"
* @method setOrientation
* @chainable
*/
this.setOrientation = function(o) {
if (this.orient !== o) {
this.orient = zebkit.util.validateValue(o, "top", "bottom");
this.vrp();
}
return this;
};
/**
* Set border panel title horizontal alignment.
* @param {String} a a horizontal alignment. Use "left", "right", "center" as
* the parameter value.
* @method setAlignment
* @chainable
*/
this.setAlignment = function(a) {
if (this.alignment !== a) {
this.alignment = zebkit.util.validateValue(a, "left", "right", "center");
this.vrp();
}
return this;
};
},
function setBorder(br) {
if (arguments.length === 0) {
br = "plain";
}
br = zebkit.draw.$view(br);
if (zebkit.instanceOf(br, zebkit.draw.TitledBorder) === false) {
br = new zebkit.draw.TitledBorder(br, "center");
}
return this.$super(br);
},
function kidAdded(index, ctr, lw) {
this.$super(index, ctr, lw);
if ((ctr === null && this.content === null) || "center" === ctr) {
this.content = lw;
} else if (this.label === null) {
this.label = lw;
}
},
function kidRemoved(index, lw, ctr){
this.$super(index, lw, ctr);
if (lw === this.label) {
this.label = null;
} else if (this.content === lw) {
this.content = null;
}
}
]);
/**
* Splitter panel UI component class. The component splits its area horizontally or vertically into two areas.
* Every area hosts an UI component. A size of the parts can be controlled by pointer cursor dragging. Gripper
* element is children UI component that can be customized. For instance:
*
* // create split panel
* var sp = new zebkit.ui.SplitPan(new zebkit.ui.Label("Left panel"),
* new zebkit.ui.Label("Right panel"));
*
* // customize gripper background color depending on its state
* sp.gripper.setBackground(new zebkit.draw.ViewSet({
* "over" : "yellow"
* "out" : null,
* "pressed.over" : "red"
* }));
*
*
* @param {zebkit.ui.Panel} [first] a first UI component in splitter panel
* @param {zebkit.ui.Panel} [second] a second UI component in splitter panel
* @param {String} [o] an orientation of splitter element: "vertical" or "horizontal"
* @class zebkit.ui.SplitPan
* @constructor
* @extends zebkit.ui.Panel
*/
pkg.SplitPan = Class(pkg.Panel, [
function(f,s,o) {
if (arguments.length > 2) {
this.orient = o;
}
this.$super();
if (arguments.length > 0) {
this.add("left", f);
if (arguments.length > 1) {
this.add("right", s);
}
}
this.add("center", new this.clazz.Bar(this));
},
function $clazz() {
this.Bar = Class(pkg.EvStatePan, [
function(target) {
this.target = target;
this.$super();
},
function $prototype() {
this.prevLoc = 0;
this.pointerDragged = function(e){
var x = this.x + e.x, y = this.y + e.y;
if (this.target.orient === "vertical"){
if (this.prevLoc !== x){
x = this.target.normalizeBarLoc(x);
if (x > 0){
this.prevLoc = x;
this.target.setGripperLoc(x);
}
}
} else {
if (this.prevLoc !== y) {
y = this.target.normalizeBarLoc(y);
if (y > 0){
this.prevLoc = y;
this.target.setGripperLoc(y);
}
}
}
};
this.pointerDragStarted = function (e){
var x = this.x + e.x,
y = this.y + e.y;
if (e.isAction()) {
if (this.target.orient === "vertical"){
x = this.target.normalizeBarLoc(x);
if (x > 0) {
this.prevLoc = x;
}
} else {
y = this.target.normalizeBarLoc(y);
if (y > 0) {
this.prevLoc = y;
}
}
}
};
this.pointerDragEnded = function(e){
var xy = this.target.normalizeBarLoc(this.target.orient === "vertical" ? this.x + e.x
: this.y + e.y);
if (xy > 0) {
this.target.setGripperLoc(xy);
}
};
this.getCursorType = function(t, x, y) {
return (this.target.orient === "vertical" ? pkg.Cursor.W_RESIZE
: pkg.Cursor.N_RESIZE);
};
}
]);
},
function $prototype() {
/**
* A minimal size of the left (or top) sizable panel
* @attribute leftMinSize
* @type {Integer}
* @readOnly
* @default 50
*/
/**
* A minimal size of right (or bottom) sizable panel
* @attribute rightMinSize
* @type {Integer}
* @readOnly
* @default 50
*/
/**
* Indicates if the splitter bar can be moved
* @attribute isMoveable
* @type {Boolean}
* @readOnly
* @default true
*/
/**
* A gap between gripper element and first and second UI components
* @attribute gap
* @type {Integer}
* @readOnly
* @default 1
*/
/**
* A reference to gripper UI component
* @attribute gripper
* @type {zebkit.ui.Panel}
* @readOnly
*/
/**
* A reference to left (top) sizable UI component
* @attribute leftComp
* @type {zebkit.ui.Panel}
* @readOnly
*/
/**
* A reference to right (bottom) sizable UI component
* @attribute rightComp
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.leftMinSize = this.rightMinSize = 50;
this.isMoveable = true;
this.gap = 1;
this.orient = "vertical";
this.minXY = this.maxXY = 0;
this.barLocation = 70;
this.leftComp = this.rightComp = this.gripper = null;
this.normalizeBarLoc = function(xy){
if (xy < this.minXY) {
xy = this.minXY;
} else if (xy > this.maxXY) {
xy = this.maxXY;
}
return (xy > this.maxXY || xy < this.minXY) ? -1 : xy;
};
/**
* Set split panel orientation.
* @param {String} o an orientation ("horizontal" or "vertical")
* @method setOrientation
* @chainable
*/
this.setOrientation = function(o) {
if (o !== this.orient) {
this.orient = zebkit.util.validateValue(o, "horizontal", "vertical");
this.vrp();
}
return this;
};
/**
* Set gripper element location
* @param {Integer} l a location of the gripper element
* @method setGripperLoc
* @chainable
*/
this.setGripperLoc = function(l){
if (l !== this.barLocation){
this.barLocation = l;
this.vrp();
}
return this;
};
this.calcPreferredSize = function(c){
var fSize = pkg.$getPS(this.leftComp),
sSize = pkg.$getPS(this.rightComp),
bSize = pkg.$getPS(this.gripper);
if (this.orient === "horizontal"){
bSize.width = Math.max(((fSize.width > sSize.width) ? fSize.width : sSize.width), bSize.width);
bSize.height = fSize.height + sSize.height + bSize.height + 2 * this.gap;
}
else {
bSize.width = fSize.width + sSize.width + bSize.width + 2 * this.gap;
bSize.height = Math.max(((fSize.height > sSize.height) ? fSize.height : sSize.height), bSize.height);
}
return bSize;
};
this.doLayout = function(target){
var right = this.getRight(),
top = this.getTop(),
bottom = this.getBottom(),
left = this.getLeft(),
bSize = pkg.$getPS(this.gripper);
if (this.orient === "horizontal"){
var w = this.width - left - right;
if (this.barLocation < top) {
this.barLocation = top;
} else if (this.barLocation > this.height - bottom - bSize.height) {
this.barLocation = this.height - bottom - bSize.height;
}
if (this.gripper !== null){
if (this.isMoveable){
this.gripper.setBounds(left, this.barLocation, w, bSize.height);
} else {
this.gripper.toPreferredSize();
this.gripper.setLocation(Math.floor((w - bSize.width) / 2), this.barLocation);
}
}
if (this.leftComp !== null){
this.leftComp.setBounds(left, top, w, this.barLocation - this.gap - top);
}
if (this.rightComp !== null){
this.rightComp.setLocation(left, this.barLocation + bSize.height + this.gap);
this.rightComp.setSize(w, this.height - this.rightComp.y - bottom);
}
} else {
var h = this.height - top - bottom;
if (this.barLocation < left) {
this.barLocation = left;
} else if (this.barLocation > this.width - right - bSize.width) {
this.barLocation = this.width - right - bSize.width;
}
if (this.gripper !== null){
if (this.isMoveable === true){
this.gripper.setBounds(this.barLocation, top, bSize.width, h);
} else {
this.gripper.setBounds(this.barLocation, Math.floor((h - bSize.height) / 2),
bSize.width, bSize.height);
}
}
if (this.leftComp !== null){
this.leftComp.setBounds(left, top, this.barLocation - left - this.gap, h);
}
if (this.rightComp !== null){
this.rightComp.setLocation(this.barLocation + bSize.width + this.gap, top);
this.rightComp.setSize(this.width - this.rightComp.x - right, h);
}
}
};
/**
* Set gap between gripper element and sizable panels
* @param {Integer} g a gap
* @method setGap
* @chainable
*/
this.setGap = function (g){
if (this.gap !== g){
this.gap = g;
this.vrp();
}
return this;
};
/**
* Set the minimal size of the left (or top) sizeable panel
* @param {Integer} m a minimal possible size
* @method setLeftMinSize
* @chainable
*/
this.setLeftMinSize = function (m){
if (this.leftMinSize !== m){
this.leftMinSize = m;
this.vrp();
}
return this;
};
/**
* Set the minimal size of the right (or bottom) sizeable panel
* @param {Integer} m a minimal possible size
* @method setRightMinSize
* @chainable
*/
this.setRightMinSize = function(m){
if (this.rightMinSize !== m){
this.rightMinSize = m;
this.vrp();
}
return this;
};
/**
* Set the given gripper movable state
* @param {Boolean} b the gripper movable state.
* @method setGripperMovable
*/
this.setGripperMovable = function (b){
if (b !== this.isMoveable){
this.isMoveable = b;
this.vrp();
}
return this;
};
},
function kidAdded(index, ctr, c){
this.$super(index, ctr, c);
if ((ctr === null && this.leftComp === null) || "left" === ctr) {
this.leftComp = c;
} else if ((ctr === null && this.rightComp === null) || "right" === ctr) {
this.rightComp = c;
} else {
if ("center" === ctr) {
this.gripper = c;
} else {
throw new Error("" + ctr);
}
}
},
function kidRemoved(index, c, ctr){
this.$super(index, c, ctr);
if (c === this.leftComp) {
this.leftComp = null;
} else if (c === this.rightComp) {
this.rightComp = null;
} else if (c === this.gripper) {
this.gripper = null;
}
},
function resized(pw,ph) {
var ps = this.gripper.getPreferredSize();
if (this.orient === "vertical"){
this.minXY = this.getLeft() + this.gap + this.leftMinSize;
this.maxXY = this.width - this.gap - this.rightMinSize - ps.width - this.getRight();
} else {
this.minXY = this.getTop() + this.gap + this.leftMinSize;
this.maxXY = this.height - this.gap - this.rightMinSize - ps.height - this.getBottom();
}
this.$super(pw, ph);
}
]);
/**
* Extendable UI panel class. Implement collapsible panel where
* a user can hide of show content by pressing special control
* element:
*
* // create extendable panel that contains list as its content
* var ext = zebkit.ui.CollapsiblePan("Title", new zebkit.ui.List([
* "Item 1",
* "Item 2",
* "Item 3"
* ]));
*
*
* @constructor
* @class zebkit.ui.CollapsiblePan
* @extends zebkit.ui.Panel
* @param {zebkit.ui.Panel|String} l a title label text or
* @param {zebkit.ui.Panel} c a content of the extender panel
* component
*/
/**
* Fired when extender is collapsed or extended
*
* var ex = new zebkit.ui.CollapsiblePan("Title", pan);
* ex.on(function (src, isCollapsed) {
* ...
* });
*
* @event fired
* @param {zebkit.ui.CollapsiblePan} src an extender UI component that generates the event
* @param {Boolean} isCollapsed a state of the extender UI component
*/
pkg.CollapsiblePan = Class(pkg.Panel, [
function(lab, content){
this.$super();
this.headerPan = new this.clazz.Header();
this.togglePan = new this.clazz.Toogle();
this.togglePan.on(this);
this.add("top", this.headerPan);
this.headerPan.add(this.togglePan);
this.headerPan.add(pkg.$component(arguments.length === 0 || lab === null ? "" : lab, this));
if (arguments.length > 1 && content !== null) {
this.contentPan = content;
content.setVisible(this.getValue());
this.add("center", this.contentPan);
}
},
function $clazz() {
this.Label = Class(pkg.Label,[]);
this.ImageLabel = Class(pkg.ImageLabel, []);
this.Header = Class(pkg.EvStatePan, []);
this.Toogle = Class(pkg.Checkbox, [
function $prototype() {
this.cursorType = pkg.Cursor.HAND;
},
function $clazz() {
this.layout = new zebkit.layout.FlowLayout();
}
]);
this.GroupPan = Class(pkg.Panel, [
function() {
this.group = new pkg.Group(true);
this.$super();
for(var i = 0; i < arguments.length; i++) {
arguments[i].togglePan.setGroup(this.group);
this.add(arguments[i]);
arguments[i].setBorder(null);
}
},
function $prototype() {
this.doLayout = function(t) {
var y = t.getTop(),
x = t.getLeft(),
w = t.width - x - t.getRight(),
eh = t.height - y - t.getBottom(),
kid = null,
i = 0;
// setup sizes for not selected item and calculate the vertical
// space that can be used for an expanded item
for(i = 0; i < t.kids.length; i++) {
kid = t.kids[i];
if (kid.isVisible) {
if (kid.getValue() === false) {
var psh = kid.getPreferredSize().height;
eh -= psh;
kid.setSize(w, psh);
}
}
}
for(i = 0; i < t.kids.length; i++) {
kid = t.kids[i];
if (kid.isVisible) {
kid.setLocation(x, y);
if (kid.getValue()) {
kid.setSize(w, eh);
}
y += kid.height;
}
}
};
this.calcPreferredSize = function(t) {
var w = 0,
h = 0;
for(var i = 0; i < t.kids.length; i++) {
var kid = t.kids[i];
if (kid.isVisible) {
var ps = kid.getPreferredSize();
h += ps.height;
if (ps.width > w) {
w = ps.width;
}
}
}
return { width:w, height:h };
};
this.compAdded = function(e) {
if (this.group.selected === null) {
e.kid.setValue(true);
}
};
this.compRemoved = function(e) {
if (this.group.selected === e.kid.togglePan) {
e.kid.setValue(false);
}
e.kid.setGroup(null);
};
}
]);
},
function $prototype() {
/**
* Title panel
* @type {zebkit.ui.Panel}
* @attribute headerPan
* @readOnly
*/
this.headerPan = null;
/**
* Content panel
* @type {zebkit.ui.Panel}
* @readOnly
* @attribute contentPan
*/
this.contentPan = null;
/**
* Toggle UI element
* @type {zebkit.ui.Checkbox}
* @readOnly
* @attribute togglePan
*/
this.togglePan = null;
this.setValue = function(b) {
if (this.togglePan !== null) {
this.togglePan.setValue(b);
}
return this;
};
this.getValue = function(b) {
return (this.togglePan !== null) ? this.togglePan.getValue() : false;
};
this.setGroup = function(g) {
if (this.togglePan !== null) {
this.togglePan.setGroup(g);
}
return this;
};
this.toggle = function() {
if (this.togglePan !== null) {
this.togglePan.toggle();
}
return this;
};
this.fired = function(src) {
var value = this.getValue();
if (this.contentPan !== null) {
this.contentPan.setVisible(value);
}
};
this.compRemoved = function(e) {
if (this.headerPan === e.kid) {
this.headerPan = null;
} else if (e.kid === this.contentPan) {
this.contentPan = null;
} else if (e.kid === this.togglePan) {
this.togglePan.off(this);
this.togglePan = null;
}
};
}
]);
/**
* Status bar UI component class
* @class zebkit.ui.StatusBarPan
* @constructor
* @param {Integer} [gap] a gap between status bar children elements
* @extends zebkit.ui.Panel
*/
pkg.StatusBarPan = Class(pkg.Panel, [
function (gap){
this.$super(new zebkit.layout.PercentLayout("horizontal", (arguments.length === 0 ? 2 : gap)));
},
function $clazz() {
this.Label = Class(pkg.Label, []);
this.Line = Class(pkg.Line, []);
this.Combo = Class(pkg.Combo, []);
this.Combo.inheritProperties = true;
this.Checkbox = Class(pkg.Checkbox, []);
},
function $prototype() {
this.borderView = null;
/**
* Set the specified border to be applied for status bar children components
* @param {zebkit.draw.View} v a border
* @method setBorderView
* @chainable
*/
this.setBorderView = function(v){
if (v !== this.borderView){
this.borderView = v;
for (var i = 0; i < this.kids.length; i++) {
this.kids[i].setBorder(this.borderView);
}
this.repaint();
}
return this;
};
this.addCombo = function(ctr, data) {
if (arguments.length === 1) {
data = ctr;
ctr = null;
}
return this.add(ctr, new this.clazz.Combo(data));
};
this.addCheckbox = function(ctr, data) {
if (arguments.length === 1) {
data = ctr;
ctr = null;
}
return this.add(ctr, new this.clazz.Checkbox(data));
};
},
function insert(i, s, d) {
if (zebkit.isString(d)) {
if (d === "|") {
if (s !== null) {
s = { ay: "stretch" };
}
d = new this.clazz.Line();
d.direction = "vertical";
d.constraints = { ay : "stretch" };
} else {
d = new this.clazz.Label(d);
}
}
return this.$super(i, s, d.setBorder(this.borderView));
}
]);
/**
* Panel class that uses zebkit.layout.StackLayout as a default layout manager.
* @class zebkit.ui.StackPan
* @param {zebkit.ui.Panel} [varname]* number of components to be added to the stack
* panel
* @constructor
* @extends zebkit.ui.Panel
*/
pkg.StackPan = Class(pkg.Panel, [
function() {
this.$super(new zebkit.layout.StackLayout());
for(var i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
}
]);
/**
* Simple ruler panel class. The ruler can render minimal and maximal values of the
* specified range.
* @param {String} [o] ruler orientation. Use "horizontal" or "vertical" as the
* argument value
* @constructor
* @class zebkit.ui.RulerPan
* @extends zebkit.ui.Panel
*/
pkg.RulerPan = Class(pkg.Panel, [
function(o) {
this.$super();
this.setLabelsRender(new this.clazz.PercentageLabels());
if (arguments.length > 0) {
this.setOrientation(o);
}
},
function $clazz() {
// TODO: complete the class
this.LabelsHighlighter = zebkit.Interface([
function paintLabel(g, x, y, w, h, v, value) {
this.$super(g, x, y, w, h, v, value);
if (this.$labelsInfo === undefined) {
this.$labelsInfo = [];
}
var found = false;
for (var i = 0; i < this.$labelsInfo.length; i++) {
var info = this.$labelsInfo[i];
if (info.value === value) {
if (info.x !== x || info.y !== y || info.w !== w || info.h !== h) {
info.x = x;
info.y = y;
info.w = w;
info.h = h;
}
found = true;
break;
}
}
if (found === false) {
this.$labelsInfo.push({
value : value,
x : x,
y : y,
w : w,
h : h
});
}
},
function invalidate(p) {
this.$labelsInfo = [];
this.$selectedLabel = null;
this.$super();
},
function setParent(p) {
if (p === null && this.$labelsInfo) {
this.$labelsInfo = [];
this.$selectedLabel = null;
}
return this.$super(p);
},
function paint(g) {
if (this.highlighterView !== null && this.$selectedLabel !== null) {
this.highlighterView.paint(g, this.$selectedLabel.x,
this.$selectedLabel.y,
this.$selectedLabel.w,
this.$selectedLabel.h,
this);
}
this.$super(g);
},
function $prototype() {
this.catchInput = true;
this.$selectedLabel = null;
this.highlighterView = zebkit.draw.$view("yellow");
this.getLabelAt = function(x, y) {
if (this.$labelsInfo !== undefined) {
for (var i = 0; i < this.$labelsInfo.length; i++) {
var inf = this.$labelsInfo[i];
if (x >= inf.x && x < inf.w + inf.x && y >= inf.y && y < inf.h + inf.y) {
return inf;
}
}
}
return null;
};
this.pointerMoved = function(e) {
if (this.highlighterView !== null) {
var label = this.getLabelAt(e.x, e.y);
if (this.$selectedLabel !== label) {
this.$selectedLabel = label;
this.repaint();
}
}
};
this.pointerExited = function(e) {
if (this.highlighterView !== null) {
var label = this.getLabelAt(e.x, e.y);
if (this.$selectedLabel !== null) {
this.$selectedLabel = null;
this.repaint();
}
}
};
this.seHighlighterView = function(v) {
if (this.highlighterView !== v) {
this.highlighterView = v;
this.repaint();
}
return this;
};
this.pointerClicked = function(e) {
var label = this.getLabelAt(e.x, e.y);
if (label !== null) {
this.parent.setValue(label.value);
}
};
}
]);
/**
* Numeric label renderer factory.
* @param {Integer} [numPrecision] precision of displayed numbers.
* @class zebkit.ui.RulerPan.NumLabels
* @extends zebkit.draw.BaseViewProvider
*/
this.NumLabels = Class(zebkit.draw.BaseViewProvider, [
function(numPrecision) {
this.$super(new zebkit.draw.BoldTextRender(""));
if (arguments.length > 0) {
this.numPrecision = numPrecision;
}
},
function $prototype() {
/**
* Number precision.
* @attribute numPrecision
* @type {Integer}
* @readOnly
* @default -1
*/
this.numPrecision = -1;
},
/**
* Get a view to render the given number.
* @param {zebkit.ui.RulerPan} t a target ruler panel.
* @param {Number} v a number to be rendered
* @return {zebkit.draw.View} a view to render the number
* @method getView
*/
function getView(t, v) {
if (v !== null && v !== undefined && this.numPrecision !== -1 && zebkit.isNumber(v)) {
v = v.toFixed(this.numPrecision);
}
return this.$super(t, v);
},
function $clazz() {
this.color = "gray";
this.font = new zebkit.Font("Arial", "bold", 12);
}
]);
/**
* Percentage label renderer factory.
* @param {Integer} [numPrecision] precision of displayed numbers.
* @class zebkit.ui.RulerPan.PercentageLabels
* @extends zebkit.ui.RulerPan.NumLabels
*/
this.PercentageLabels = Class(this.NumLabels, [
function(numPrecision) {
if (arguments.length === 0) {
numPrecision = 0;
}
this.$super(numPrecision);
},
function getView(t, v) {
var min = t.getMin(),
max = t.getMax();
v = ((v - min) * 100) / (max - min);
if (this.numPrecision !== -1) {
v = v.toFixed(this.numPrecision);
}
return this.$super(t, v + "%");
}
]);
},
/**
* @for zebkit.ui.RulerPan
*/
function $prototype() {
/**
* Gap between stroke and labels
* @attribute gap
* @type {Integer}
* @readOnly
* @default 2
*/
this.gap = 2;
/**
* Stroke color.
* @attribute color
* @type {String}
* @readOnly
* @default "gray"
*/
this.color = "gray";
/**
* Stroke line width
* @attribute lineWidth
* @type {Integer}
* @default 1
* @readOnly
*/
this.lineWidth = 1;
/**
* Stroke line size
* @attribute strokeSize
* @type {Integer}
* @default 4
* @readOnly
*/
this.strokeSize = 4;
/**
* Ruler orientation ("horizontal" or "vertical").
* @attribute orient
* @type {String}
* @readOnly
* @default "horizontal"
*/
this.orient = "horizontal";
/**
* Ruler labels alignment
* @type {String}
* @attribute labelsAlignment
* @default "normal"
* @readOnly
*/
this.labelsAlignment = "normal"; // "invert"
/**
* Ruler labels provider
* @type {zebkit.draw.BaseViewProvider}
* @attribute provider
* @readOnly
* @protected
*/
this.provider = null;
/**
* Indicates if labels have to be rendered
* @attribute showLabels
* @type {Boolean}
* @default true
* @readOnly
*/
this.showLabels = true;
/**
* Indicate if stroke has to be rendered
* @type {Boolean}
* @attribute showStrokes
* @readOnly
* @default true
*/
this.showStrokes = true;
this.$min = 0;
this.$max = 100;
this.$minGap = this.$maxGap = 0;
this.$psW = this.$psH = 0;
this.$maxLabSize = 0;
/**
* Show ruler labels with percentage.
* @param {Integer} [precision] a precision
* @chainable
* @method showPercentage
*/
this.showPercentage = function(precision) {
this.setLabelsRender(new this.clazz.PercentageLabels(arguments.length > 0 ? precision
: 0));
return this;
};
/**
* Show ruler labels with number.
* @param {Integer} [precision] a precision
* @chainable
* @method showNumbers
*/
this.showNumbers = function(precision) {
this.setLabelsRender(new this.clazz.NumLabels(arguments.length > 0 ? precision : 0));
return this;
};
/**
* Set the ruler color.
* @param {String} c a color
* @method setColor
* @chainable
*/
this.setColor = function(c) {
if (c !== this.color) {
this.color = c;
this.repaint();
}
return this;
};
/**
* Set the ruler gap between stroke and labels.
* @param {Integer} gap a gap
* @method setGap
* @chainable
*/
this.setGap = function(gap) {
if (this.gap !== gap) {
this.gap = gap;
this.vrp();
}
return this;
};
/**
* Set visibility of labels
* @param {Boolean} b a boolean value that indicates if the
* labels has to be shown
* @method setShowLabels
* @chainable
*/
this.setShowLabels = function(b) {
if (this.showLabels !== b) {
this.showLabels = b;
this.vrp();
}
return this;
};
/**
* Set visibility of strokes
* @param {Boolean} b a boolean value that indicates if the
* strokes have to be shown
* @method setShowStrokes
* @chainable
*/
this.setShowStrokes = function(b) {
if (this.showStrokes !== b) {
this.showStrokes = b;
this.vrp();
}
return this;
};
/**
* Set the labels font
* @param {String|zebkit.Font} font a font of labels
* @method setLabelsFont
* @chainable
*/
this.setLabelsFont = function() {
if (this.provider !== null) {
this.provider.setFont.apply(this.provider,
arguments);
this.vrp();
}
return this;
};
/**
* Set the labels color
* @param {String} color a color of labels
* @method setLabelsColor
* @chainable
*/
this.setLabelsColor = function() {
if (this.provider !== null) {
this.provider.setColor.apply(this.provider,
arguments);
this.vrp();
}
return this;
};
/**
* Set the stroke size.
* @param {Integer} strokeSize a stroke size
* @method setStrokeSize
* @chainable
*/
this.setStrokeSize = function(strokeSize) {
if (this.strokeSize !== strokeSize) {
this.strokeSize = strokeSize;
this.vrp();
}
return this;
};
/**
* Set the labels render
* @param {zebkit.draw.BaseViewProvider} r labels render
* @method setLabelsRender
* @chainable
*/
this.setLabelsRender = function(p) {
if (this.provider !== p) {
this.provider = p;
if (this.showLabels === true) {
this.vrp();
}
}
return this;
};
/**
* Set the ruler labels alignment. Label alignment specifies a side the labels has
* to be placed relatively stroke.
* @param {String} a labels alignment. The value can be "normal" or "invert"
* @method setLabelsAlignment
* @chainable
*/
this.setLabelsAlignment = function(a) {
if (this.labelsAlignment !== a) {
zebkit.util.validateValue(a, "normal", "invert");
this.labelsAlignment = a;
this.repaint();
}
return this;
};
/**
* Set the ruler range.
* @param {Number} min a minimal value of the range
* @param {Number} max a maximal value of the range
* @method setRange
* @chainable
*/
this.setRange = function(min, max) {
if (min >= max) {
throw new Error("Invalid range [" + min + "," + max + "]");
}
if (this.$min !== min || this.$max !== max) {
this.$min = min;
this.$max = max;
this.vrp();
}
return this;
};
/**
* Get the ruler effective size. The size includes only pixels that are
* used to be transformed into range values.
* @return {Integer} a ruler size
* @protected
* @method $getRulerSize
*/
this.$getRulerSize = function() {
var s = (this.orient === "horizontal" ? this.width - this.getLeft() - this.getRight()
: this.height - this.getTop() - this.getBottom());
return s - this.$minGap - this.$maxGap;
};
/**
* Get a minimal value in the ruler values range
* @return {Number} a minimal range value
* @method getMin
*/
this.getMin = function() {
return this.$min;
};
/**
* Get a maximal value in the ruler values range
* @return {Number} a maximal range value
* @method getMax
*/
this.getMax = function() {
return this.$max;
};
/**
* Project the given range value to appropriate ruler component coordinate
* @param {Number} v a range value
* @return {Integer} coordinate
* @method toLocation
*/
this.toLocation = function(v) {
var max = this.getMax(),
min = this.getMin(),
xy = Math.floor((this.$getRulerSize() * (v - min)) / (max - min));
return (this.orient === "vertical") ? this.height - this.getBottom() - this.$minGap - xy
: this.getLeft() + this.$minGap + xy;
};
/**
* Project the given ruler component coordinate to a range value.
* @param {Integer} xy a x or y (depending on the ruler orientation) coordinate
* @return {Number} a range value
* @method toValue
*/
this.toValue = function(xy) {
var min = this.getMin(),
max = this.getMax(),
sl = (this.orient === "horizontal") ? this.getLeft() + this.$minGap
: this.getTop() + this.$minGap,
ss = this.$getRulerSize();
if (this.orient === "vertical") {
xy = this.height - xy - 1;
}
if (xy < sl) {
xy = sl;
} else if (xy > sl + ss) {
xy = sl + ss;
}
return min + ((max - min) * (xy - sl)) / ss;
};
/**
* Set the ruler orientation
* @param {String} o an orientation. Use "horizontal" or "vertical" values.
* @method setOrientation
* @chainable
*/
this.setOrientation = function(o) {
if (this.orient !== o) {
this.orient = zebkit.util.validateValue(o, "vertical", "horizontal");
this.vrp();
}
return this;
};
this.calcPreferredSize = function() {
return {
width : this.$psW,
height : this.$psH
};
};
this.recalc = function() {
this.$maxLabSize = this.$psW = this.$psH = this.$maxGap = this.$minGap = 0;
if (this.isVisible) {
this.recalcMetrics();
}
};
/**
* Called when the ruler requires its metric recalculation
* @method recalcMetrics
*/
this.recalcMetrics = function() {
if (this.provider !== null && this.showLabels === true) {
// TODO: pay attention since view render shares single render
// don't store instance of view and then store another instance
// of view
var minView = this.provider.getView(this, this.getMin()),
minViewPs = minView === null ? { width: 0, height: 0 } : minView.getPreferredSize(),
maxView = this.provider.getView(this, this.getMax()),
maxViewPs = maxView === null ? { width: 0, height: 0 } : maxView.getPreferredSize();
if (this.orient === "horizontal") {
this.$minGap = Math.round(minViewPs.width / 2);
this.$maxGap = Math.round(maxViewPs.width / 2);
this.$maxLabSize = Math.max(minViewPs.height, maxViewPs.height);
} else {
this.$maxLabSize = Math.max(minViewPs.width, maxViewPs.width);
this.$minGap = Math.round(minViewPs.height / 2);
this.$maxGap = Math.round(maxViewPs.height / 2);
}
}
if (this.orient === "vertical") {
this.$psH = 50 * this.lineWidth + this.$minGap + this.$maxGap;
this.$psW = (this.showStrokes ? this.strokeSize : 0) +
(this.$maxLabSize === 0 ? 0 : this.$maxLabSize + this.gap);
} else {
this.$psW = 50 * this.lineWidth + this.$minGap + this.$maxGap;
this.$psH = (this.showStrokes ? this.strokeSize : 0) +
(this.$maxLabSize === 0 ? 0 : this.$maxLabSize + this.gap);
}
};
// =================================================================
// ^ ^
// | top | top
// . . . +---------+ . . .
// || ^ | Label | ^
// || | strokeSize | | | $maxLabSize
// || | +---------+ . |
// ^ ^
// | gap . . . . .| gap
// +---------+ . . . || ^
// | Label | ^ || |
// | | | $maxLabSize || | strokeSize
// +---------+ . ^ . . . . .^
// | bottom | bottom
// ==================================================================
this.paint = function(g) {
if (this.provider !== null) {
var min = this.getMin(),
max = this.getMax(),
view = null,
yy = 0,
xx = 0,
ps = null,
ss = this.showStrokes ? this.strokeSize : 0;
g.setColor(this.color);
if (this.orient === "horizontal") {
yy = this.getTop();
xx = this.getLeft() + this.$minGap;
if (this.showLabels) {
view = this.provider.getView(this, min);
if (view !== null) {
ps = view.getPreferredSize();
view.paint(g,
this.toLocation(min) - Math.round(ps.width / 2),
this.labelsAlignment === "normal" ? yy + ss + this.gap
: yy + this.$maxLabSize - ps.height,
ps.width,
ps.height,
this);
}
view = this.provider.getView(this, max);
if (view !== null) {
ps = view.getPreferredSize();
view.paint(g,
this.toLocation(max) - Math.round(ps.width / 2),
this.labelsAlignment === "normal" ? yy + ss + this.gap
: yy + this.$maxLabSize - ps.height,
ps.width,
ps.height,
this);
}
if (this.labelsAlignment !== "normal") {
yy += (this.$maxLabSize + this.gap);
}
}
g.drawLine(xx, yy, xx, yy + ss, this.lineWidth);
xx = this.width - this.getRight() - this.$maxGap - 1;
g.drawLine(xx, yy, xx, yy + ss, this.lineWidth);
} else {
yy = this.getTop() + this.$maxGap;
xx = this.getLeft();
if (this.showLabels) {
view = this.provider.getView(this, min);
if (view !== null) {
ps = view.getPreferredSize();
this.paintLabel(g,
this.labelsAlignment === "normal" ? xx + this.$maxLabSize - ps.width
: ss + this.gap + xx,
this.toLocation(min) - Math.round(ps.height / 2),
ps.width, ps.height,
view, min);
}
view = this.provider.getView(this, max);
if (view !== null) {
ps = view.getPreferredSize();
this.paintLabel(g,
this.labelsAlignment === "normal" ? xx + this.$maxLabSize - ps.width
: ss + this.gap + xx,
this.toLocation(max) - Math.round(ps.height / 2),
ps.width,
ps.height,
view, max);
}
if (this.labelsAlignment === "normal") {
xx += (this.$maxLabSize + this.gap);
}
}
g.drawLine(xx, yy, xx + ss, yy, this.lineWidth);
yy = this.height - this.getBottom() - this.$minGap - 1;
g.drawLine(xx, yy, xx + ss, yy, this.lineWidth);
}
}
};
this.paintLabel = function(g, x, y, w, h, v, value) {
if (v !== null) {
v.paint(g, x, y, w, h, this);
}
};
this.getLabelAt = function(x, y) {
return null;
};
}
]);
/**
* Pointer ruler class. The ruler uses generator class instance to get and render labels values
* @param {String} o an orientation.
* @constructor
* @class zebkit.ui.PointRulerPan
* @extends zebkit.ui.RulerPan
*/
pkg.PointRulerPan = Class(pkg.RulerPan, [
function() {
this.$supera(arguments);
this.$generator = new this.clazz.DeltaPointsGenerator(10);
},
function $clazz() {
/**
* Basic class to implement sequence of points values
* @class zebkit.ui.PointRulerPan.PointsGenerator
* @constructor
*/
this.PointsGenerator = Class([
function $prototype() {
/**
* Generate next point value in the sequence or null if end of sequence has been reached.
* @param {zebkit.ui.RulerPan} ruler a ruler
* @param {Integer} index a point index
* @return {Number} a value for the given point with the specified index
* @method pointValue
*/
this.pointValue = function(ruler, index) {
return null;
};
}
]);
/**
* Delta point generator implementation. The generator uses fixed delta value
* to calculate next value of the points sequence.
* @param {Number} [delta] a delta
* @class zebkit.ui.PointRulerPan.DeltaPointsGenerator
* @extends zebkit.ui.PointRulerPan.PointsGenerator
* @constructor
*/
this.DeltaPointsGenerator = Class(this.PointsGenerator, [
function(delta) {
if (arguments.length > 0) {
this.$delta = delta;
}
},
function $prototype() {
/**
* Delta
* @attribute $delta
* @type {Number}
* @readOnly
* @protected
*/
this.$delta = 0;
this.pointValue = function(ruler, i) {
if (this.$delta === 0) {
return null;
} else {
var v = ruler.getMin() + i * this.$delta;
return (v > ruler.getMax()) ? null : v;
}
};
}
]);
},
/**
* @for zebkit.ui.PointRulerPan
*/
function $prototype() {
this.$generator = null;
/**
* Set the points values generator
* @param {zebkit.ui.PointRulerPan.PointsGenerator} g a point generator
* @method setPointsGenerator
*/
this.setPointsGenerator = function(g) {
if (this.$generator !== g) {
this.$generator = g;
this.vrp();
}
return this;
};
/**
* Setup delta points generator. The generator builds points sequence basing on incrementing
* the sequence with fixed delta number.
* @param {Number} delta a delta
* @chainable
* @method useDeltaPointsGenerator
*/
this.useDeltaPointsGenerator = function(delta) {
this.setPointsGenerator(new this.clazz.DeltaPointsGenerator(delta));
return this;
};
this.recalcMetrics = function() {
if (this.provider !== null && this.showLabels === true) {
var i = 0,
v = null,
min = this.getMin(),
max = this.getMax();
while ((v = this.$generator.pointValue(this, i++)) !== null) {
var view = this.provider.getView(this, v);
if (view !== null) {
var ps = view.getPreferredSize();
if (this.orient === "horizontal") {
if (ps.height > this.$maxLabSize) {
this.$maxLabSize = ps.height;
}
if (min === v) {
this.$minGap = Math.round(ps.width / 2);
} else if (max === v) {
this.$maxGap = Math.round(ps.width / 2);
}
} else {
if (ps.width > this.$maxLabSize) {
this.$maxLabSize = ps.width;
}
if (min === v) {
this.$minGap = Math.round(ps.height / 2);
} else if (max === v) {
this.$maxGap = Math.round(ps.height / 2);
}
}
}
}
}
if (this.orient === "vertical") {
this.$psH = 50 + this.$minGap + this.$maxGap;
this.$psW = (this.showStrokes ? this.strokeSize : 0) +
(this.$maxLabSize === 0 ? 0 : this.$maxLabSize + this.gap);
} else {
this.$psW = 50 + this.$minGap + this.$maxGap;
this.$psH = (this.showStrokes ? this.strokeSize : 0) +
(this.$maxLabSize === 0 ? 0 : this.$maxLabSize + this.gap);
}
};
this.paint = function(g) {
if (this.$generator !== null) {
var y = this.getTop(),
x = this.getLeft(),
prevLabLoc = null,
prevPs = null,
v = null,
i = 0,
j = 0,
ss = this.showStrokes ? this.strokeSize : 0;
g.beginPath();
while ((v = this.$generator.pointValue(this, i++)) !== null) {
var loc = this.toLocation(v);
if (this.provider !== null && this.showLabels === true) {
var view = this.provider.getView(this, v),
rendered = false;
if (view !== null) {
var ps = view.getPreferredSize();
if (this.orient === "horizontal") {
if (prevLabLoc === null || loc > prevLabLoc + prevPs.width) {
this.paintLabel(g,
loc - Math.floor(ps.width / 2),
this.labelsAlignment === "normal" ? y + ss + this.gap
: y,
ps.width, ps.height,
view, v);
prevLabLoc = loc;
prevPs = ps;
rendered = true;
}
} else {
if (prevLabLoc === null || Math.round(loc + ps.height/2) < prevLabLoc) {
prevLabLoc = loc - Math.floor(ps.height / 2);
this.paintLabel(g,
this.labelsAlignment === "normal" ? x + ss + this.gap
: x,
prevLabLoc,
ps.width,
ps.height,
view, v);
rendered = true;
}
}
}
if (rendered === true && this.showStrokes) {
if (this.orient === "horizontal") {
if (this.labelsAlignment === "normal") {
g.moveTo(loc + 0.5, y);
g.lineTo(loc + 0.5, y + this.strokeSize);
} else {
g.moveTo(loc + 0.5, y + this.$maxLabSize + this.gap);
g.lineTo(loc + 0.5, y + this.$maxLabSize + this.gap + this.strokeSize);
}
} else {
if (this.labelsAlignment === "normal") {
g.moveTo(x, loc + 0.5);
g.lineTo(x + this.strokeSize, loc + 0.5);
} else {
g.moveTo(x + this.$maxLabSize + this.gap, loc + 0.5);
g.lineTo(x + this.$maxLabSize + this.gap + this.strokeSize, loc + 0.5);
}
}
}
} else {
if (this.showStrokes) {
if (this.orient === "horizontal") {
if (this.labelsAlignment === "normal") {
g.moveTo(loc + 0.5, y);
g.lineTo(loc + 0.5, y + this.strokeSize);
} else {
g.moveTo(loc + 0.5, y + this.$maxLabSize + this.gap);
g.lineTo(loc + 0.5, y + this.$maxLabSize + this.gap + this.strokeSize);
}
} else {
if (this.labelsAlignment === "normal") {
g.moveTo(x, loc + 0.5);
g.lineTo(x + this.strokeSize, loc + 0.5);
} else {
g.moveTo(x + this.$maxLabSize + this.gap, loc + 0.5);
g.lineTo(x + this.$maxLabSize + this.gap + this.strokeSize, loc + 0.5);
}
}
}
}
}
g.lineWidth = this.lineWidth;
g.setColor(this.color);
g.stroke();
}
};
}
]);
/**
* Linear ruler class. The ruler draws strokes using dedicated pixel delta value.
* @param {String} [o] an orientation (use "vertical" or "horizontal" as the parameter value)
* @class zebkit.ui.LinearRulerPan
* @constructor
* @extends zebkit.ui.RulerPan
*/
pkg.LinearRulerPan = Class(pkg.RulerPan, [
function $prototype() {
this.strokeStep = 2;
this.longStrokeRate = this.strokeStep * 8;
this.setStrokeStep = function(strokeStep, longStrokeRate) {
var b = false;
if (strokeStep !== this.strokeStep) {
this.strokeStep = strokeStep;
b = true;
}
if (arguments.length > 1) {
if (this.longStrokeRate !== longStrokeRate) {
this.longStrokeRate = longStrokeRate;
b = true;
}
} else if (this.longStrokeRate <= 2 * strokeStep) {
this.longStrokeRate = strokeStep * 8;
b = true;
}
if (b) {
this.repaint();
}
return this;
};
this.paint = function(g) {
var i = 0,
ss = this.showStrokes ? this.strokeSize : 0,
ps = null,
prevLabLoc = null,
prevPs = null,
rendered = false,
v = null,
view = null,
loc = 0;
g.beginPath();
if (this.orient === "horizontal") {
var y = this.getTop(),
xx = this.getLeft() + this.$minGap,
maxX = this.width - this.getRight() - this.$maxGap - 1;
for (i = 0; xx <= maxX; i++, xx += this.strokeStep) {
if (i % this.longStrokeRate === 0) {
rendered = false;
if (this.provider !== null && this.showLabels) {
v = this.toValue(xx);
view = this.provider.getView(this, v);
if (view !== null) {
ps = view.getPreferredSize();
loc = xx - Math.round(ps.width / 2);
if (prevLabLoc === null || loc > prevLabLoc + prevPs.width) {
this.paintLabel(g,
loc,
this.labelsAlignment === "normal" ? y + 2 * ss + this.gap
: y,
ps.width, ps.height, view, v);
prevLabLoc = loc;
prevPs = ps;
rendered = true;
}
}
}
if (this.showStrokes) {
if (this.labelsAlignment === "normal") {
g.moveTo(xx + 0.5, y);
g.lineTo(xx + 0.5, y + (rendered ? 2 * ss : ss));
} else {
g.moveTo(xx + 0.5, y + this.$maxLabSize + this.gap + (rendered ? 0 : ss));
g.lineTo(xx + 0.5, y + this.$maxLabSize + this.gap + 2 * ss);
}
}
} else if (this.showStrokes) {
if (this.labelsAlignment === "normal") {
g.moveTo(xx + 0.5, y);
g.lineTo(xx + 0.5, y + ss);
} else {
g.moveTo(xx + 0.5, y + this.$maxLabSize + this.gap + ss);
g.lineTo(xx + 0.5, y + this.$maxLabSize + this.gap + 2 * ss);
}
}
}
} else {
var x = this.getLeft(),
yy = this.height - this.getBottom() - this.$minGap - 1,
minY = this.getTop() + this.$maxGap;
for (i = 0; yy >= minY; i++, yy -= this.strokeStep) {
if (i % this.longStrokeRate === 0) {
rendered = false;
if (this.provider !== null && this.showLabels) {
v = this.toValue(yy);
view = this.provider.getView(this, v);
if (view !== null) {
ps = view.getPreferredSize();
loc = yy - Math.round(ps.height / 2);
if (prevLabLoc === null || (loc + ps.height) < prevLabLoc) {
this.paintLabel(g,
this.labelsAlignment === "normal" ? x + 2 * ss + this.gap
: x,
loc,
ps.width, ps.height, view, v);
prevLabLoc = loc;
rendered = true;
}
}
}
if (this.showStrokes) {
if (this.labelsAlignment === "normal") {
g.moveTo(x, yy + 0.5);
g.lineTo(x + (rendered ? 2 * ss : ss), yy + 0.5);
} else {
g.moveTo(x + this.$maxLabSize + this.gap + (rendered ? 0 : ss), yy + 0.5);
g.lineTo(x + this.$maxLabSize + this.gap + 2 * ss, yy + 0.5);
}
}
} else if (this.showStrokes) {
if (this.labelsAlignment === "normal") {
g.moveTo(x, yy + 0.5);
g.lineTo(x + ss, yy + 0.5);
} else {
g.moveTo(x + this.$maxLabSize + this.gap + ss, yy + 0.5);
g.lineTo(x + this.$maxLabSize + this.gap + 2 * ss, yy + 0.5);
}
}
}
}
g.setColor(this.color);
g.lineWidth = this.lineWidth;
g.stroke();
};
},
function recalcMetrics() {
this.$super();
if (this.orient === "horizontal") {
this.$psH += this.strokeSize;
} else {
this.$psW += this.strokeSize;
}
}
]);
/**
* Slider UI component class.
* @class zebkit.ui.Slider
* @param {String} [o] a slider orientation ("vertical or "horizontal")
* @constructor
* @extends zebkit.ui.Panel
* @uses zebkit.ui.HostDecorativeViews
* @uses zebkit.EvenetProducer
*/
pkg.Slider = Class(pkg.Panel, pkg.HostDecorativeViews, [
function(o) {
this.views = {
marker: null,
gauge : null
};
this.$super();
var ruler = null;
if (arguments.length > 0) {
if (zebkit.instanceOf(o, zebkit.ui.RulerPan)) {
this.orient = o.orient;
ruler = o;
} else {
ruler = new pkg.RulerPan(o);
}
} else {
ruler = new pkg.RulerPan(this.orient);
}
this.add("ruler", ruler);
this.add("gauge", new this.clazz.GaugePan());
},
function $clazz() {
this.GaugePan = Class(pkg.Panel, []);
},
function $prototype() {
/**
* Current slider value
* @type {Number}
* @attribute value
* @readOnly
*/
this.value = 0;
/**
* Slider orientation.
* @type {String}
* @attribute orient
* @readOnly
*/
this.orient = "horizontal";
/**
* Gap between slider handle and ruler
* @type {Integer}
* @attribute gap
* @readOnly
* @default 4
*/
this.gap = 4;
this.canHaveFocus = true;
/**
* Granularity of sliding.
* @type {Number}
* @attribute granularity
* @readOnly
* @default 1
*/
this.granularity = 1;
/**
* Ruler component.
* @type {zebkit.ui.RulerPan}
* @attribute ruler
* @readOnly
*/
this.ruler = null;
this.gauge = null;
this.handle = null;
this.$dragged = false;
this.$dxy = this.$val = 0;
this.compAdded = function(e) {
if (e.constraints === "ruler") {
this.ruler = e.kid;
this.orient = this.ruler.orient;
this.setValue(this.ruler.getMin());
} else if (e.constraints === "gauge") {
this.gauge = e.kid;
}
};
this.compRemoved = function(e) {
if (this.gauge === e.kid) {
this.gauge = null;
}
};
this.setHandleView = function(v) {
if (this.handle !== v) {
this.handle = zebkit.draw.$view(v);
this.vrp();
}
return this;
};
/**
* Get maximal possible value.
* @return {Number} a value
* @method getMax
*/
this.getMax = function() {
return this.ruler.getMax();
};
/**
* Get minimal possible value.
* @return {Number} a value
* @method getMin
*/
this.getMin = function() {
return this.ruler.getMin();
};
this.toLocation = function(v) {
return (this.orient === "horizontal") ? this.ruler.toLocation(v) + this.ruler.x
: this.ruler.toLocation(v) + this.ruler.y;
};
this.getHandleView = function() {
var h = this.orient === "horizontal" ? this.views.horHandle
: this.views.verHandle;
return h === undefined ? null : h;
};
this.getHandlePreferredSize = function() {
var h = this.orient === "horizontal" ? this.views.horHandle
: this.views.verHandle;
return h === undefined || h === null ? { width: 0, height: 0}
: h.getPreferredSize();
};
/**
* Set orientation
* @param {String} o an orientation. Use "horizontal" or "vertical" as the parameter value
* @method setOrientation
* @chainable
*/
this.setOrientation = function(o) {
if (this.orient !== o) {
this.orient = zebkit.util.validateValue(o, "vertical", "horizontal");
this.ruler.setOrientation(o);
this.vrp();
}
return this;
};
this.pointerDragged = function(e){
if (this.$dragged) {
var max = this.ruler.getMax(),
min = this.ruler.getMin(),
dxy = (this.orient === "horizontal" ? e.x - this.$sxy : this.$sxy - e.y);
// TODO: ruler.toValue
this.setValue(this.$val + dxy * ((max - min)/ this.ruler.$getRulerSize()));
}
};
//
// +---------------------------------------------------------
// | ^
// | | top
// | . . . . . . . . . . . . . . . . . . . . . . . . . .
// | left . ------ ----------------------
// |<---->. | | ^
// | . | | |
// | . ==============| |================= | handler
// | . ==============| |================= | preferred
// | . | | | height
// \ . | | |
// | . ------
// | . ^
// | . | gap
// | . |---|---|---|---|---|---|---|---|---|---|---| ^
// | . | | | | 2 * netSize
// | . ^
// | . | gap
// | . Num_1 Num_2 Num_3
//
this.paintOnTop = function(g) {
var left = this.getLeft(),
top = this.getTop(),
right = this.getRight(),
bottom = this.getBottom(),
handleView = this.getHandleView(),
handlePs = this.getHandlePreferredSize(),
w = this.width - left - right,
h = this.height - top - bottom;
if (this.orient === "horizontal") {
if (handleView !== null) {
handleView.paint(g, this.getHandleLoc(),
top,
handlePs.width,
handlePs.height,
this);
}
} else {
if (handleView !== null) {
handleView.paint(g, left,
this.getHandleLoc(),
handlePs.width,
handlePs.height,
this);
}
}
if (this.hasFocus() && this.views.marker) {
this.views.marker.paint(g, left, top, w, h, this);
}
};
this.getHandleLoc = function() {
var hs = this.getHandlePreferredSize();
return (this.orient === "horizontal") ? this.toLocation(this.value) - Math.round(hs.width / 2)
: this.toLocation(this.value) - Math.round(hs.height / 2);
};
this.getHandleBounds = function() {
var bs = this.getHandlePreferredSize();
return this.orient === "horizontal" ? {
x: this.getHandleLoc(),
y: this.getTop(),
width : bs.width,
height: bs.height
}
: {
x: this.getLeft(),
y: this.getHandleLoc(),
width : bs.width,
height: bs.height
};
};
this.catchInput = function(target) {
return target !== this.ruler || this.ruler.catchInput !== true;
};
this.doLayout = function(t) {
var gaugePs = this.gauge !== null && this.gauge.isVisible ? this.gauge.getPreferredSize() : null,
hs = this.getHandlePreferredSize(),
h2s = this.orient === "vertical" ? Math.round(hs.height / 2)
: Math.round(hs.width / 2);
if (this.orient === "vertical") {
var y = this.getTop() + (this.ruler.$maxGap >= h2s ? 0 : h2s - this.ruler.$maxGap);
this.ruler.setLocation(this.getLeft() + hs.width + this.gap, y);
this.ruler.setSize(this.ruler.getPreferredSize().width,
this.height - y - this.getBottom() -
(this.ruler.$minGap >= h2s ? 0 : (h2s - this.ruler.$minGap)));
if (this.gauge !== null && this.gauge.isVisible) {
this.gauge.setBounds(this.getLeft() + Math.floor((hs.width - gaugePs.width) / 2),
this.getTop(),
gaugePs.width,
this.height - this.getTop() - this.getBottom());
}
} else {
var x = this.getLeft() + (this.ruler.$minGap >= h2s ? 0 : h2s - this.ruler.$minGap);
this.ruler.setLocation(x, this.getTop() + hs.height + this.gap);
this.ruler.setSize(this.width - x - this.getRight() -
(this.ruler.$maxGap >= h2s ? 0 : (h2s - this.ruler.$maxGap)),
this.ruler.getPreferredSize().height);
if (this.gauge !== null && this.gauge.isVisible) {
this.gauge.setBounds(this.getLeft(),
this.getTop() + Math.floor((hs.height - gaugePs.height) / 2),
this.width - this.getLeft() - this.getRight(),
gaugePs.height);
}
}
};
this.calcPreferredSize = function(l) {
var ps = this.getHandlePreferredSize();
if (this.ruler.isVisible === true) {
var rps = this.ruler.getPreferredSize(),
h2s = 0;
if (this.orient === "horizontal") {
h2s = Math.round(ps.width / 2);
ps.height += (this.gap + rps.height);
ps.width = 10 * ps.width +
Math.max(h2s, this.ruler.isVisible ? this.ruler.$minGap : 0) +
Math.max(h2s, this.ruler.isVisible ? this.ruler.$maxGap : 0);
} else {
h2s = Math.round(ps.height / 2);
ps.height = 10 * ps.height +
Math.max(h2s, this.ruler.isVisible ? this.ruler.$minGap : 0) +
Math.max(h2s, this.ruler.isVisible ? this.ruler.$maxGap : 0);
ps.width += (this.gap + rps.width);
}
}
return ps;
};
/**
* Set the slider value that has to be withing the given defined range.
* If the value is out of the defined range then the value will be
* adjusted to maximum or minimum possible value.
* @param {Number} v a value
* @method setValue
* @chainable
*/
this.setValue = function(v) {
// normalize value
v = Math.round(v / this.granularity) * this.granularity;
var max = this.getMax(),
min = this.getMin();
// align value
if (v > max) {
v = max;
} else if (v < min) {
v = min;
}
var prev = this.value;
if (this.value !== v){
this.value = v;
this.fire("fired", [this, prev]);
this.repaint();
}
return this;
};
this.keyPressed = function(e) {
switch(e.code) {
case "ArrowDown":
case "ArrowLeft":
this.setValue(this.value - this.granularity);
break;
case "ArrowUp":
case "ArrowRight":
this.setValue(this.value + this.granularity);
break;
case "Home":
this.setValue(this.getMin());
break;
case "End":
this.setValue(this.getMax());
break;
}
};
this.pointerClicked = function (e){
if (e.isAction()) {
var x = e.x,
y = e.y,
handle = this.getHandleBounds();
if (x < handle.x ||
y < handle.y ||
x >= handle.x + handle.width ||
y >= handle.y + handle.height )
{
if (this.getComponentAt(x, y) !== this.ruler) {
var l = ((this.orient === "horizontal") ? x - this.ruler.x
: y - this.ruler.y);
this.setValue(this.ruler.toValue(l));
}
}
}
};
this.pointerDragStarted = function(e){
var r = this.getHandleBounds();
if (e.x >= r.x &&
e.y >= r.y &&
e.x < r.x + r.width &&
e.y < r.y + r.height )
{
this.$dragged = true;
this.$sxy = this.orient === "horizontal" ? e.x : e.y;
this.$val = this.value;
}
};
this.pointerDragEnded = function(e) {
this.$dragged = false;
};
/**
* Set the granularity. Granularity defines a delta to a slider value
* can be decreased or increased.
* @param {Number} g a granularity.
* @method setGranularity
* @chainable
*/
this.setGranularity = function(g) {
if (g >= (this.getMax() - this.getMin())) {
throw new Error("Invalid granularity " + g);
}
if (this.granularity !== g) {
this.granularity = g;
this.setValue(this.value);
}
return this;
};
/**
* Set the range the slider value can be changed.
* @param {Number} min a minimal possible value
* @param {Number} max a maximal possible value
* @param {Number} [granularity] a granularity
* @method setRange
* @chainable
*/
this.setRange = function(min, max, granularity) {
if (this.getMin() !== min || this.getMax() !== max || granularity !== this.granularity) {
this.ruler.setRange(min, max);
this.setGranularity(arguments.length > 2 ? granularity : this.granularity); // validate granularity
this.vrp();
}
return this;
};
/**
* Set the ruler to be used
* @param {zebkit.ui.RulerPan} r a ruler
* @method setRuler
* @chainable
*/
this.setRuler = function(r) {
this.setByConstraints("ruler", r);
return this;
};
this.showPercentage = function() {
this.ruler.showPercentage();
return this;
};
this.showNumbers = function() {
this.ruler.showNumbers();
return this;
};
/**
* Set the gap between the slider handle and the ruler.
* @param {Integer} g a gap
* @method setRulerGap
* @chainable
*/
this.setRulerGap = function(g) {
if (g !== this.gap) {
this.gap = g;
this.vrp();
}
return this;
};
},
function focused() {
this.$super();
this.repaint();
}
]).events("fired");
/**
* Tabs UI panel. The component is used to organize switching between number of pages where every
* page is an UI component.
*
* Filling tabs component with pages is the same to how you add an UI component to a panel. For
* instance in the example below three pages with "Titl1", "Title2", "Title3" are added:
*
* var tabs = new zebkit.ui.Tabs();
* tabs.add("Title1", new zebkit.ui.Label("Label as a page"));
* tabs.add("Title2", new zebkit.ui.Button("Button as a page"));
* tabs.add("Title3", new zebkit.ui.TextArea("Text area as a page"));
*
* You can access tabs pages UI component the same way like you access a panel children components
*
* ...
* tabs.kids[0] // access the first page
*
* And you can remove it with standard panel inherited API:
*
* ...
* tabs.removeAt(0); // remove first tab page
*
*
* To customize tab page caption and icon you should access tab object and do it with API it provides:
*
*
* // update a tab caption
* tabs.getTab(0).setCaption("Test");
*
* // update a tab icon
* tabs.getTab(0).setIcon("my.gif");
*
* // set a particular font and color for the tab in selected state
* tabs.getTab(0).setColor(true, "blue");
* tabs.getTab(0).setFont(true, new zebkit.Font("Arial", "bold", 16));
*
* // set other caption for the tab in not selected state
* tabs.getTab(0).setCaption(false, "Test");
*
* @param {String} [o] the tab panel orientation:
*
* "top"
* "bottom"
* "left"
* "right"
*
* @class zebkit.ui.Tabs
* @uses zebkit.ui.HostDecorativeViews
* @uses zebkit.EventProducer
* @constructor
* @extends zebkit.ui.Panel
*/
/**
* Fired when a new tab page has been selected
*
* tabs.on(function(src, selectedIndex) {
* ...
* });
*
* @event selected
* @param {zebkit.ui.Tabs} src a tabs component that triggers the event
* @param {Integer} selectedIndex a tab page index that has been selected
*/
pkg.Tabs = Class(pkg.Panel, pkg.HostDecorativeViews, [
function(o) {
/**
* Selected tab page index
* @attribute selectedIndex
* @type {Integer}
* @readOnly
*/
this.vgap = this.hgap = this.tabAreaX = 0;
this.repaintWidth = this.repaintHeight = this.repaintX = this.repaintY = 0;
this.tabAreaY = this.tabAreaWidth = this.tabAreaHeight = 0;
this.overTab = this.selectedIndex = -1;
this.pages = [];
this.views = {};
if (pkg.Tabs.font !== undefined) {
this.render.setFont(pkg.Tabs.font);
}
if (pkg.Tabs.fontColor !== undefined) {
this.render.setColor(pkg.Tabs.fontColor);
}
this.$super();
// since alignment pass as the constructor argument the setter has to be called after $super
// because $super can re-set title alignment
if (arguments.length > 0) {
this.setAlignment(o);
}
},
function $clazz() {
/**
* Tab view class that defines the tab page title and icon
* @param {String|Image} [icon] an path to an image or image object
* @param {String} [caption] a tab caption
* @class zebkit.ui.Tabs.TabView
* @extends zebkit.ui.CompRender
* @constructor
*/
this.TabView = Class(pkg.CompRender, [
function(icon, caption) {
if (arguments.length === 0) {
caption = "";
} else if (arguments.length === 1) {
caption = icon;
icon = null;
}
var tp = new this.clazz.TabPan();
this.$super(tp);
var $this = this;
tp.getImagePan().imageLoaded = function(img) {
$this.vrp();
// if the icon has zero width and height the repaint
// doesn't trigger validation. So let's do it on
// parent level
if ($this.owner !== null && $this.owner.parent !== null) {
$this.owner.repaint();
}
};
var r1 = new this.clazz.captionRender(caption),
r2 = new this.clazz.captionRender(caption);
r2.setColor(this.clazz.fontColor);
r1.setColor(this.clazz.selectedFontColor);
r2.setFont (zebkit.$font(this.clazz.font));
r1.setFont (zebkit.$font(this.clazz.selectedFont));
this.getCaptionPan().setView(
new zebkit.draw.ViewSet(
{
"selected": r1,
"*" : r2
},
[
function setFont(id, f) {
var v = this.views[id];
if (v) {
v.setFont(zebkit.$font(f));
this.recalc();
}
return this;
},
function setCaption(id, s) {
var v = this.views[id];
if (v) {
v.setValue(s);
this.recalc();
}
return this;
},
function getCaption(id) {
var v = this.views[id];
return (v === null || v === undefined ? null : v.getValue());
}
]
)
);
this.setIcon(icon);
},
function $clazz() {
this.captionRender = zebkit.draw.StringRender;
this.font = new zebkit.Font("Arial", 14);
this.TabPan = Class(pkg.Panel, [
function() {
this.$super();
this.add(new pkg.ImagePan(null));
this.add(new pkg.ViewPan());
},
function getImagePan() {
return this.kids[0];
},
function getViewPan() {
return this.kids[1];
}
]);
},
function $prototype() {
this.owner = null;
this.ownerChanged = function(v) {
this.owner = v;
};
this.vrp = function() {
if (this.owner !== null) {
this.owner.vrp();
}
};
/**
* Set the given tab caption for the specified tab or both - selected and not selected - states.
* @param {Boolean} [b] the tab state. true means selected state.
* @param {String} s the tab caption
* @method setCaption
* @chainable
*/
this.setCaption = function(b, s) {
if (arguments.length === 1) {
this.setCaption(true, b);
this.setCaption(false, b);
} else {
this.getCaptionPan().view.setCaption(this.$toId(b), s);
this.vrp();
}
return this;
};
/**
* Get the tab caption for the specified tab state
* @param {Boolean} b the tab state. true means selected state.
* @return {String} the tab caption
* @method getCaption
*/
this.getCaption = function (b) {
return this.getCaptionPan().view.getCaption(this.$toId(b));
};
/**
* Set the given tab caption text color for the specified tab or both
* selected and not selected states.
* @param {Boolean} [b] the tab state. true means selected state.
* @param {String} c the tab caption
* @method setColor
* @chainable
*/
this.setColor = function(b, c) {
if (arguments.length === 1) {
this.setColor(true, b);
this.setColor(false, b);
} else {
var v = this.getCaptionPan().view.views[this.$toId(b)];
if (v) {
v.setColor(c);
this.vrp();
}
}
return this;
};
/**
* Set the given tab caption text font for the specified or both
* selected not slected states.
* @param {Boolean} [b] the tab state. true means selected state.
* @param {zebkit.Font} f the tab text font
* @method setFont
* @chainable
*/
this.setFont = function(b, f) {
if (arguments.length === 1) {
this.setFont(true, b);
this.setFont(false, b);
} else {
this.getCaptionPan().view.setFont(this.$toId(b), zebkit.$font(f));
this.vrp();
}
return this;
};
this.getCaptionPan = function () {
return this.target.getViewPan();
};
/**
* Set the tab icon.
* @param {String|Image} c an icon path or image object
* @method setIcon
* @chainable
*/
this.setIcon = function (c) {
this.target.getImagePan().setImage(c);
this.target.getImagePan().setVisible(c !== null);
return this;
};
/**
* The method is invoked every time the tab selection state has been updated
* @param {zebkit.ui.Tabs} tabs the tabs component the tab belongs
* @param {Integer} i an index of the tab
* @param {Boolean} b a new state of the tab
* @method selected
*/
this.selected = function(tabs, i, b) {
this.getCaptionPan().view.activate(this.$toId(b), this);
};
this.$toId = function(b) {
return b ? "selected" : "*";
};
}
]);
},
/**
* @for zebkit.ui.Tabs
*/
function $prototype() {
/**
* Tab orientation
* @attribute orient
* @type {String}
* @readOnly
*/
this.orient = "top";
/**
* Sides gap
* @attribute sideSpace
* @type {Integer}
* @readOnly
* @default 1
*/
this.sideSpace = 1;
/**
* Declare can have focus attribute to make the component focusable
* @type {Boolean}
* @attribute canHaveFocus
* @readOnly
*/
this.canHaveFocus = true;
/**
* Define pointer moved event handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerMoved
*/
this.pointerMoved = function(e) {
var i = this.getTabAt(e.x, e.y);
if (this.overTab !== i) {
this.overTab = i;
if (this.views.overTab) {
this.repaint(this.repaintX, this.repaintY,
this.repaintWidth, this.repaintHeight);
}
}
};
/**
* Define pointer drag ended event handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerDragEnded
*/
this.pointerDragEnded = function(e) {
var i = this.getTabAt(e.x, e.y);
if (this.overTab !== i) {
this.overTab = i;
if (this.views.overTab) {
this.repaint(this.repaintX, this.repaintY,
this.repaintWidth, this.repaintHeight);
}
}
};
/**
* Define pointer exited event handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerExited
*/
this.pointerExited = function(e) {
if (this.overTab >= 0) {
this.overTab = -1;
if (this.views.overTab) {
this.repaint(this.repaintX, this.repaintY,
this.repaintWidth, this.repaintHeight);
}
}
};
/**
* Navigate to a next tab page following the given direction starting
* from the given page
* @param {Integer} page a starting page index
* @param {Integer} d a navigation direction. 1 means forward and -1 means backward
* navigation.
* @return {Integer} a new tab page index
* @method next
*/
this.next = function (page, d){
for(; page >= 0 && page < Math.floor(this.pages.length / 2); page += d) {
if (this.isTabEnabled(page) === true) {
return page;
}
}
return -1;
};
this.getTitleInfo = function(){
var b = (this.orient === "left" || this.orient === "right"),
res = b ? { x : this.tabAreaX,
y : 0,
width : this.tabAreaWidth,
height : 0,
orient : this.orient }
: { x : 0,
y : this.tabAreaY,
width : 0,
height : this.tabAreaHeight,
orient : this.orient };
if (this.selectedIndex >= 0){
var r = this.getTabBounds(this.selectedIndex);
if (b) {
res.y = r.y;
res.height = r.height;
} else {
res.x = r.x;
res.width = r.width;
}
}
return res;
};
/**
* Test if the given tab page is in enabled state
* @param {Integer} index a tab page index
* @return {Boolean} a tab page state
* @method isTabEnabled
*/
this.isTabEnabled = function (index){
return this.kids[index].isEnabled;
};
this.paintOnTop = function(g){
var ts = g.$states[g.$curState];
// stop painting if the tab area is outside of clip area
if (zebkit.util.isIntersect(this.repaintX, this.repaintY,
this.repaintWidth, this.repaintHeight,
ts.x, ts.y, ts.width, ts.height))
{
var i = 0;
for(i = 0; i < this.selectedIndex; i++) {
this.paintTab(g, i);
}
for(i = this.selectedIndex + 1;i < Math.floor(this.pages.length / 2); i++) {
this.paintTab(g, i);
}
if (this.selectedIndex >= 0){
this.paintTab(g, this.selectedIndex);
if (this.hasFocus()) {
this.drawMarker(g, this.getTabBounds(this.selectedIndex));
}
}
}
};
/**
* Draw currently activate tab page marker.
* @param {CanvasRenderingContext2D} g a graphical context
* @param {Object} r a tab page title rectangular area
* @method drawMarker
*/
this.drawMarker = function(g,r){
var marker = this.views.marker;
if (marker) {
//TODO: why only "out" is checked ?
var bv = (this.views.outTab === null || this.views.outTab === undefined ? null : this.views.outTab),
left = bv ? bv.getLeft() : 0,
top = bv ? bv.getTop() : 0;
marker.paint(g, r.x + left, r.y + top,
r.width - left - (bv === null ? 0 : bv.getRight()),
r.height - top - (bv === null ? 0 : bv.getBottom()), this);
}
};
/**
* Paint the given tab page title
* @param {CanvasRenderingContext2D} g a graphical context
* @param {Integer} pageIndex a tab page index
* @method paintTab
*/
this.paintTab = function (g, pageIndex){
var b = this.getTabBounds(pageIndex),
page = this.kids[pageIndex],
tab = this.views.outTab,
tabover = this.views.overTab,
tabon = this.views.selectedTab,
v = this.pages[pageIndex * 2],
ps = v.getPreferredSize();
if (this.selectedIndex === pageIndex && tabon) {
tabon.paint(g, b.x, b.y, b.width, b.height, page);
} else if (tab) {
tab.paint(g, b.x, b.y, b.width, b.height, page);
}
if (this.overTab >= 0 && this.overTab === pageIndex && tabover) {
tabover.paint(g, b.x, b.y, b.width, b.height, page);
}
v.paint(g, b.x + Math.floor((b.width - ps.width ) / 2),
b.y + Math.floor((b.height - ps.height) / 2),
ps.width, ps.height, page);
};
/**
* Get the given tab page title rectangular bounds
* @param {Integer} i a tab page index
* @return {Object} a tab page rectangular bounds
*
* {x:{Integer}, y:{Integer}, width:{Integer}, height:{Integer}}
*
* @protected
* @method getTabBounds
*/
this.getTabBounds = function(i){
return this.pages[2 * i + 1];
};
this.calcPreferredSize = function(target){
var max = zebkit.layout.getMaxPreferredSize(target);
if (this.orient === "bottom" || this.orient === "top"){
max.width = Math.max(max.width, 2 * this.sideSpace + this.tabAreaWidth);
max.height += this.tabAreaHeight + this.sideSpace;
} else {
max.width += this.tabAreaWidth + this.sideSpace;
max.height = Math.max(max.height, 2 * this.sideSpace + this.tabAreaHeight);
}
return max;
};
this.doLayout = function(target) {
var right = this.orient === "right" ? this.right : this.getRight(),
top = this.orient === "top" ? this.top : this.getTop(),
bottom = this.orient === "bottom" ? this.bottom : this.getBottom(),
left = this.orient === "left" ? this.left : this.getLeft(),
b = (this.orient === "top" || this.orient === "bottom");
if (b) {
this.repaintX = this.tabAreaX = left ;
this.repaintY = this.tabAreaY = (this.orient === "top") ? top
: this.height - bottom - this.tabAreaHeight;
if (this.orient === "bottom") {
this.repaintY -= (this.border !== null ? this.border.getBottom() : 0);
}
} else {
this.repaintX = this.tabAreaX = (this.orient === "left" ? left
: this.width - right - this.tabAreaWidth);
this.repaintY = this.tabAreaY = top ;
if (this.orient === "right") {
this.repaintX -= (this.border !== null ? this.border.getRight() : 0);
}
}
var count = this.kids.length,
sp = 2 * this.sideSpace,
xx = (this.orient === "right" ? this.tabAreaX : this.tabAreaX + this.sideSpace),
yy = (this.orient === "bottom" ? this.tabAreaY : this.tabAreaY + this.sideSpace),
r = null,
i = 0;
for(i = 0; i < count; i++ ){
r = this.getTabBounds(i);
r.x = xx;
r.y = yy;
if (b) {
xx += r.width;
if (i === this.selectedIndex) {
xx -= sp;
if (this.orient === "bottom") {
r.y -= (this.border !== null ? this.border.getBottom() : 0);
}
}
} else {
yy += r.height;
if (i === this.selectedIndex) {
yy -= sp;
if (this.orient === "right") {
r.x -= (this.border !== null ? this.border.getRight() : 0);
}
}
}
}
// make visible tab title
if (this.selectedIndex >= 0){
var dt = 0;
r = this.getTabBounds(this.selectedIndex);
if (b) {
r.x -= this.sideSpace;
r.y -= ((this.orient === "top") ? this.sideSpace : 0);
dt = (r.x < left) ? left - r.x
: (r.x + r.width > this.width - right) ? this.width - right - r.x - r.width : 0;
} else {
r.x -= (this.orient === "left") ? this.sideSpace : 0;
r.y -= this.sideSpace;
dt = (r.y < top) ? top - r.y
: (r.y + r.height > this.height - bottom) ? this.height - bottom - r.y - r.height : 0;
}
for(i = 0;i < count; i ++ ){
var br = this.getTabBounds(i);
if (b) {
br.x += dt;
} else {
br.y += dt;
}
}
}
for(i = 0;i < count; i++){
var l = this.kids[i];
if (i === this.selectedIndex) {
// TODO: temporary
l.setVisible(true);
if (b) {
l.setBounds(left + this.hgap,
((this.orient === "top") ? top + this.repaintHeight : top) + this.vgap,
this.width - left - right - 2 * this.hgap,
this.height - this.repaintHeight - top - bottom - 2 * this.vgap);
} else {
l.setBounds(((this.orient === "left") ? left + this.repaintWidth : left) + this.hgap,
top + this.vgap,
this.width - this.repaintWidth - left - right - 2 * this.hgap,
this.height - top - bottom - 2 * this.vgap);
}
} else {
// TODO: bring back
//l.setSize(0, 0);
// TODO: temporary
l.setVisible(false);
}
}
};
/**
* Define recalc method to compute the component metrical characteristics
* @method recalc
*/
this.recalc = function(){
var count = Math.floor(this.pages.length / 2);
if (count > 0) {
this.tabAreaHeight = this.tabAreaWidth = 0;
var bv = this.views.outTab ? this.views.outTab : null,
b = (this.orient === "left" || this.orient === "right"),
max = 0,
i = 0,
r = null,
hadd = bv === null ? 0 : bv.getLeft() + bv.getRight(),
vadd = bv === null ? 0 : bv.getTop() + bv.getBottom();
for(i = 0; i < count; i++){
var ps = this.pages[i * 2] != null ? this.pages[i * 2].getPreferredSize()
: { width:0, height:0};
r = this.getTabBounds(i);
if (b) {
r.height = ps.height + vadd;
if (ps.width + hadd > max) {
max = ps.width + hadd;
}
this.tabAreaHeight += r.height;
} else {
r.width = ps.width + hadd;
if (ps.height + vadd > max) {
max = ps.height + vadd;
}
this.tabAreaWidth += r.width;
}
}
// align tabs widths or heights to have the same size
for(i = 0; i < count; i++ ){
r = this.getTabBounds(i);
if (b) {
r.width = max;
} else {
r.height = max;
}
}
if (b) {
this.tabAreaWidth = max + this.sideSpace;
this.tabAreaHeight += (2 * this.sideSpace);
this.repaintHeight = this.tabAreaHeight;
this.repaintWidth = this.tabAreaWidth + (this.border !== null ? (this.orient === "left" ? this.border.getLeft()
: this.border.getRight())
: 0);
} else {
this.tabAreaWidth += (2 * this.sideSpace);
this.tabAreaHeight = this.sideSpace + max;
this.repaintWidth = this.tabAreaWidth;
this.repaintHeight = this.tabAreaHeight + (this.border !== null ? (this.orient === "top" ? this.border.getTop()
: this.border.getBottom())
: 0);
}
// make selected tab page title bigger
if (this.selectedIndex >= 0) {
r = this.getTabBounds(this.selectedIndex);
if (b) {
r.height += 2 * this.sideSpace;
r.width += this.sideSpace + (this.border !== null ? (this.orient === "left" ? this.border.getLeft()
: this.border.getRight())
: 0);
} else {
r.height += this.sideSpace + (this.border !== null ? (this.orient === "top" ? this.border.getTop()
: this.border.getBottom())
: 0);
r.width += 2 * this.sideSpace;
}
}
}
};
/**
* Get tab index located at the given location
* @param {Integer} x a x coordinate
* @param {Integer} y a y coordinate
* @return {Integer} an index of the tab that is
* detected at the given location. -1 if no any
* tab can be found
* @method getTabAt
*/
this.getTabAt = function(x,y){
this.validate();
if (x >= this.tabAreaX && y >= this.tabAreaY &&
x < this.tabAreaX + this.tabAreaWidth &&
y < this.tabAreaY + this.tabAreaHeight )
{
var tb = null;
// handle selected as a special case since it can overlap neighborhood titles
if (this.selectedIndex >= 0) {
tb = this.getTabBounds(this.selectedIndex);
if (x >= tb.x && y >= tb.y && x < tb.x + tb.width && y < tb.y + tb.height) {
return this.selectedIndex;
}
}
for(var i = 0; i < Math.floor(this.pages.length / 2); i++) {
if (this.selectedIndex !== i) {
tb = this.getTabBounds(i);
if (x >= tb.x && y >= tb.y && x < tb.x + tb.width && y < tb.y + tb.height) {
return i;
}
}
}
}
return -1;
};
/**
* Define key pressed event handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function(e){
if (this.selectedIndex !== -1 && this.pages.length > 0){
var nxt = 0;
switch(e.code) {
case "ArrowUp":
case "ArrowLeft":
nxt = this.next(this.selectedIndex - 1, -1);
if (nxt >= 0) {
this.select(nxt);
}
break;
case "ArrowDown":
case "ArrowRight":
nxt = this.next(this.selectedIndex + 1, 1);
if (nxt >= 0) {
this.select(nxt);
}
break;
}
}
};
/**
* Define pointer clicked event handler
* @param {zebkit.ui.event.PointerEvent} e a key event
* @method pointerClicked
*/
this.pointerClicked = function(e){
if (e.isAction()){
var index = this.getTabAt(e.x, e.y);
if (index >= 0 && this.isTabEnabled(index)) {
this.select(index);
}
}
};
/**
* Switch to the given tab page
* @param {Integer} index a tab page index to be navigated
* @method select
* @chainable
*/
this.select = function(index){
if (this.selectedIndex !== index){
var prev = this.selectedIndex;
this.selectedIndex = index;
if (prev >= 0) {
this.pages[prev * 2].selected(this, prev, false);
}
if (index >= 0) {
this.pages[index * 2].selected(this, index, true);
}
this.fire("selected", [this, this.selectedIndex]);
this.vrp();
}
return this;
};
/**
* Get the given tab. Using the tab you can control tab caption,
* icon.
* @param {Integer} pageIndex a tab page index
* @return {zebkit.ui.Tabs.TabView}
* @method getTab
*/
this.getTab = function(pageIndex){
return this.pages[pageIndex * 2];
};
/**
* Set tab side spaces.
* @param {Integer} sideSpace [description]
* @method setSideSpace
* @chainable
*/
this.setSideSpace = function(sideSpace){
if (sideSpace !== this.sideSpace) {
this.sideSpace = sideSpace;
this.vrp();
}
return this;
};
/**
* Set tab page vertical and horizontal gaps
* @param {Integer} vg a vertical gaps
* @param {Integer} hg a horizontal gaps
* @method setPageGaps
* @chainable
*/
this.setPageGaps = function (vg, hg){
if (this.vgap !== vg || hg !== this.hgap){
this.vgap = vg;
this.hgap = hg;
this.vrp();
}
return this;
};
/**
* Set the tab page element alignments
* @param {String} o an alignment. The valid value is one of the following:
* "left", "right", "top", "bottom"
* @method setAlignment
* @chainable
*/
this.setAlignment = function(o){
if (this.orient !== o) {
this.orient = zebkit.util.validateValue(o, "top", "bottom", "left", "right");
this.vrp();
}
return this;
};
/**
* Set enabled state for the given tab page
* @param {Integer} i a tab page index
* @param {Boolean} b a tab page enabled state
* @method enableTab
* @chainable
*/
this.enableTab = function(i,b){
var c = this.kids[i];
if (c.isEnabled !== b){
c.setEnabled(b);
if (b === false && this.selectedIndex === i) {
this.select(-1);
}
this.repaint();
}
return this;
};
/**
* Set number of views to render different Tab component elements
* @param {Object} a set of views as dictionary where key is a view
* name and the value is a view instance, string(for color), or render
* function. The following view elements can be passed:
*
*
* {
* "out" : <view to render not selected tab page>,
* "over" : <view to render a tab page when pointer is over>
* "selected" : <a view to render selected tab page>
* "marker" : <a marker view to be rendered around tab page title>
* }
*
*
* @method setViews
*/
},
function focused(){
this.$super();
if (this.selectedIndex >= 0){
var r = this.getTabBounds(this.selectedIndex);
this.repaint(r.x, r.y, r.width, r.height);
} else if (this.hasFocus() === false) {
this.select(this.next(0, 1));
}
},
function kidAdded(index,constr,c) {
// correct wrong selection if inserted tab index is less or equals
if (this.selectedIndex >= 0 && index <= this.selectedIndex) {
this.selectedIndex++;
}
if (this.selectedIndex < 0) {
this.select(this.next(0, 1));
}
return this.$super(index, constr, c);
},
function insert(index, constr, c) {
var render = null;
if (zebkit.instanceOf(constr, this.clazz.TabView)) {
render = constr;
} else {
render = new this.clazz.TabView((constr === null ? "Page " + index
: constr ));
render.ownerChanged(this); // TODO: a little bit ugly but setting an owner is required to
// keep tabs component informed when an icon has been updated
}
this.pages.splice(index * 2, 0, render, { x:0, y:0, width:0, height:0 });
var r = this.$super(index, constr, c);
// since we have added new page the repainting area is wider than
// the added component (tab elements), so repaint the whole tab
// component
this.repaint();
return r;
},
function removeAt(i){
if (this.selectedIndex >= 0 && i <= this.selectedIndex) {
if (i === this.selectedIndex) {
this.select(-1);
} else {
this.selectedIndex--;
this.repaint();
}
}
this.pages.splice(i * 2, 2);
return this.$super(i);
},
function removeAll(){
this.select(-1);
this.pages.splice(0, this.pages.length);
this.pages.length = 0;
this.$super();
},
function setSize(w,h){
if (this.width !== w || this.height !== h) {
if (this.orient === "right" || this.orient === "bottom") {
this.tabAreaX = -1;
}
this.$super(w, h);
}
return this;
}
]).events("selected");
pkg.events.regEvents("menuItemSelected");
/**
* Menu event class
* @constructor
* @class zebkit.ui.event.MenuEvent
* @extends zebkit.Event
*/
pkg.event.MenuEvent = Class(zebkit.Event, [
function $prototype() {
/**
* Index of selected menu item
* @type {Integer}
* @attribute index
* @readOnly
*/
this.index = -1;
/**
* Selected menu item component
* @type {zebkit.ui.Panel}
* @attribute item
* @readOnly
*/
this.item = null;
/**
* Fill menu event with specified parameters
* @param {zebkit.ui.Menu} src a source of the menu event
* @param {Integer} index an index of selected menu item
* @param {zebkit.ui.Panel} item a selected menu item
* @protected
* @chainable
* @method $fillWith
*/
this.$fillWith = function(src, index, item) {
this.source = src;
this.index = index;
this.item = item;
return this;
};
}
]);
var MENU_EVENT = new pkg.event.MenuEvent();
/**
* Show the given popup menu.
* @param {zebkit.ui.Panel} context an UI component of zebkit hierarchy
* @param {zebkit.ui.Menu} menu a menu to be shown
* @for zebkit.ui
* @method showPopupMenu
*/
pkg.showPopupMenu = function(context, menu) {
context.getCanvas().getLayer(pkg.PopupLayerMix.id).add(menu);
};
/**
* Menu item panel class. The component holds menu item content like caption, icon, sub-menu
* sign elements. The area of the component is split into three parts: left, right and center.
* Central part keeps content, left side keeps checked sign element and the right side keeps
* sub-menu sign element.
* @param {String|zebkit.ui.Panel} content a menu item content string or component. Caption
* string can encode the item id, item icon and item checked state. For instance:
*
* {
* content: "Test" | {zebkit.ui.Panel}
* checked: {Boolean}, // optional
* group : {zebkit.ui.Group}, // optional
* icon : "path/to/image" | {Image}, // optional
* handler: {Function} // optional
* id : {String} // optional
* }
*
* @example
*
*
* // create menu item with icon and "Item 1" title
* var mi = new zebkit.ui.MenuItem({
* content: "Menu item label"
* });
*
*
* @class zebkit.ui.MenuItem
* @extends zebkit.ui.Panel
* @constructor
*
*/
pkg.MenuItem = Class(pkg.Panel, [
function(c) {
this.$super();
if (zebkit.isString(c)) {
this.add(new this.clazz.Checkbox()).setVisible(false);
c = new this.clazz.ImageLabel(c, null);
} else if (zebkit.instanceOf(c, pkg.Panel) === false) {
var ch = null;
if (c.checked === true || c.checked === false || c.group !== undefined) {
ch = (c.group !== undefined) ? new this.clazz.Radiobox()
: new this.clazz.Checkbox();
if (c.group !== undefined) {
ch.setGroup(c.group);
}
ch.setValue(c.checked === undefined ? false : c.checked);
} else {
ch = new this.clazz.Checkbox();
ch.setVisible(false);
}
this.add(ch);
if (c.id !== undefined) {
this.setId(c.id);
}
if (c.handler !== undefined) {
this.$handler = c.handler;
}
if (zebkit.instanceOf(c.content, zebkit.ui.Panel)) {
c = c.content;
} else if (c.icon !== undefined) {
c = new this.clazz.ImageLabel(c.content, c.icon);
} else {
c = new this.clazz.ImageLabel(c.content, null);
}
} else {
this.add(new this.clazz.Checkbox()).setVisible(false);
}
this.add(c);
this.add(new this.clazz.SubImage());
this.setEnabled(c.isEnabled);
this.setVisible(c.isVisible);
},
function $clazz() {
this.SubImage = Class(pkg.StatePan, []);
this.Label = Class(pkg.Label, []);
this.Checkbox = Class(pkg.Checkbox, []);
this.Radiobox = Class(pkg.Radiobox, []);
this.ImageLabel = Class(pkg.ImageLabel, []);
},
function $prototype() {
this.$handler = null;
/**
* Gap between checked, content and sub menu arrow components
* @attribute gap
* @type {Integer}
* @readOnly
* @default 8
*/
this.gap = 8;
/**
* Callback method that is called every time the menu item has
* been selected.
* @method itemSelected
*/
this.itemSelected = function() {
var content = this.getContent();
if (zebkit.instanceOf(content, pkg.Checkbox)) {
content.setValue(!content.getValue());
}
if (this.getCheck().isVisible) {
this.getCheck().toggle();
}
if (this.$handler !== null) {
this.$handler.call(this);
}
};
/**
* Set the menu item icon.
* @param {String|Image} img a path to an image or image object
* @method setIcon
* @chainable
*/
this.setIcon = function(img) {
this.getContent().setImage(img);
return this;
};
/**
* Get check state component
* @return {zebkit.ui.Panel} a check state component
* @method getCheck
*/
this.getCheck = function() {
return this.kids[0];
};
/**
* Get checked state of the item
* @return {Boolean} a checked state
* @method isChecked
*/
this.isChecked = function() {
return this.getCheck().isVisible && this.getCheck().getValue();
};
/**
* Set group
* @param {zebkit.ui.Group} g a group
* @param {Boolean} [v] a value
* @method setGroup
*/
this.setGroup = function(g, v) {
this.getCheck().setGroup(g);
this.getCheck().setVisible(true);
if (arguments.length > 1) {
this.getCheck().setValue(v);
}
};
/**
* Get content component
* @return {zebkit.ui.Panel} a content component
* @method getContent
*/
this.getContent = function() {
return this.kids.length > 0 ? this.kids[1] : null;
};
/**
* Get menu item child component to render sub item arrow element
* @return {zebkit.ui.Panel} a sub item arrow component
* @method getSub
* @protected
*/
this.getSub = function() {
return this.kids.length > 1 ? this.kids[2] : null;
};
this.activateSub = function(b) {
var kid = this.getSub();
kid.setState(b ? "pressed" : "*");
if (this.parent !== null && this.parent.noSubIfEmpty === true) {
kid.setVisible(b);
}
};
this.$getCheckSize = function() {
var ch = this.getCheck();
return ch === null ? { width: 0, height: 0 } : ch.getPreferredSize() ;
};
this.$getContentSize = function() {
var content = this.getContent();
return (content !== null && content.isVisible === true) ? content.getPreferredSize()
: { width : 0, height : 0 };
};
this.$getSubSize = function() {
var sub = this.getSub();
return (sub !== null && sub.isVisible === true) ? sub.getPreferredSize()
: { width : 0, height : 0 };
};
this.calcPreferredSize = function (target){
var p1 = this.$getCheckSize(),
p2 = this.$getContentSize(),
p3 = this.$getSubSize(),
h = Math.max(p1.height, p2.height, p3.height),
w = p1.width + p2.width + p3.width,
i = -1;
if (p1.width > 0) {
i++;
}
if (p2.width > 0) {
i++;
}
if (p3.width > 0) {
i++;
}
return { width: w + (i > 0 ? this.gap * i : 0), height: h };
};
this.doLayout = function(target) {
var left = this.getCheck(),
right = this.getSub(),
content = this.getContent(),
p1 = this.$getCheckSize(),
p3 = this.$getSubSize(),
t = target.getTop(),
l = target.getLeft(),
eh = target.height - t - target.getBottom(),
ew = target.width - l - target.getRight();
if (left !== null && left.isVisible === true) {
left.toPreferredSize();
left.setLocation(l, t + Math.floor((eh - left.height)/2));
}
var add = (p1.width > 0 ? this.gap : 0) + p1.width;
l += add;
ew -= add;
if (right !== null && right.isVisible === true) {
right.toPreferredSize();
right.setLocation(target.width - target.getRight() - right.width,
t + Math.floor((eh - right.height)/2));
}
ew -= ((p3.width > 0 ? this.gap : 0) + p3.width);
if (content !== null && content.isVisible === true) {
content.toPreferredSize();
if (content.width > ew) {
content.setSize(ew, content.height);
}
content.setLocation(l, t + Math.floor((eh - content.height)/2));
}
};
},
/**
* Override setParent method to catch the moment when the
* item is inserted to a menu
* @param {zebkit.ui.Panel} p a parent
* @method setParent
*/
function setParent(p) {
this.$super(p);
if (p !== null && p.noSubIfEmpty === true) {
this.getSub().setVisible(false);
}
}
]).hashable();
/**
* Menu UI component class. The class implements popup menu UI component.
*
* var m = new Menu([
* {
* content: "Menu Item 1",
* sub : [
* {
* content: "SubMenu Checked Item 1",
* checked: true
* },
* {
* content: "SubMenu Checked Item 2",
* checked: false
* },
* "-", // line
* {
* content: "SubMenu Checked Item 3",
* checked: false
* }
* ]
* },
* "Menu Item 2",
* "Menu Item 3"
* ]);
*
* @class zebkit.ui.Menu
* @constructor
* @param {Object} [list] menu items description
* @extends zebkit.ui.CompList
*/
pkg.Menu = Class(pkg.CompList, [
function (d) {
this.menus = {};
this.$super([], zebkit.isBoolean(d) ? d : true);
if (arguments.length > 0) {
for(var i = 0; i < d.length; i++) {
var item = d[i];
this.add(item);
if (zebkit.isString(item) === false && item.sub !== undefined) {
var sub = item.sub;
this.setMenuAt(this.kids.length - 1, zebkit.instanceOf(sub, pkg.Menu) ? sub
: new pkg.Menu(sub));
}
}
}
},
function $clazz() {
this.MenuItem = Class(pkg.MenuItem, [
function $clazz() {
this.Label = Class(pkg.MenuItem.Label, []);
}
]);
this.Line = Class(pkg.Line, []);
this.Line.prototype.$isDecorative = true;
},
function $prototype() {
this.$parentMenu = null;
this.canHaveFocus = true;
this.noSubIfEmpty = false;
/**
* Test if the given menu item is a decorative (not selectable) menu item.
* Menu item is considered as decorative if it has been added with addDecorative(...)
* method or has "$isDecorative" property set to "true"
* @param {Integer} i a menu item index
* @return {Boolean} true if the given menu item is decorative
* @method isDecorative
*/
this.isDecorative = function(i){
return this.kids[i].$isDecorative === true || this.kids[i].$$isDecorative === true;
};
/**
* Define component events handler.
* @param {zebkit.ui.event.CompEvent} e a component event
* @method childCompEnabled
*/
this.childCompEnabled = this.childCompShown = function(e) {
var src = e.source;
for(var i = 0;i < this.kids.length; i++){
if (this.kids[i] === src) {
// clear selection if an item becomes not selectable
if (this.isItemSelectable(i) === false && (i === this.selectedIndex)) {
this.select(-1);
}
break;
}
}
};
/**
* Get a menu item by the given index
* @param {Integer} i a menu item index
* @return {zebkit.ui.Panel} a menu item component
* @method getMenuItem
*/
this.getMenuItem = function(i) {
if (zebkit.isString(i) === true) {
var item = this.byPath(i);
if (item !== null) {
return item;
}
for (var k in this.menus) {
item = this.menus[k].getMenuItem(i);
if (item !== null) {
return item;
}
}
}
return this.kids[i];
};
/**
* Test if the menu has a selectable item
* @return {Boolean} true if the menu has at least one selectable item
* @method hasSelectableItems
*/
this.hasSelectableItems = function(){
for(var i = 0; i < this.kids.length; i++) {
if (this.isItemSelectable(i)) {
return true;
}
}
return false;
};
/**
* Define pointer exited events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerExited
*/
this.pointerExited = function(e){
this.position.setOffset(null);
};
/**
* Get a sub menu for the given menu item
* @param {Integer} index a menu item index
* @return {zebkit.ui.Menu} a sub menu or null if no sub menu
* is defined for the given menu item
* @method getMenuAt
*/
this.getMenuAt = function(index) {
if (index < this.kids.length) {
var hash = this.kids[index].$hash$;
return this.menus.hasOwnProperty(hash) ? this.menus[hash] : null;
} else {
return null;
}
};
// TODO: not stable API
this.menuById = function(id) {
if (this.id === id) {
return this;
} else {
for (var i = 0; i < this.kids.length; i++) {
var m = this.getMenuAt(i);
if (m !== null) {
var res = m.menuById(id);
if (res !== null) {
return res;
}
}
}
return null;
}
};
// TODO: not stable API
this.menuItemById = function(id) {
for (var i = 0; i < this.kids.length; i++) {
var mi = this.kids[i];
if (mi !== null && mi.id === id) {
return mi;
} else {
var m = this.getMenuAt(i);
if (m !== null) {
var res = m.menuItemById(id);
if (res !== null) {
return res;
}
}
}
}
return null;
};
/**
* Set the given menu as a sub-menu for the specified menu item
* @param {Integer} i an index of a menu item for that a sub menu
* has to be attached
* @param {zebkit.ui.Menu} m a sub menu to be attached
* @method setMenuAt
* @chainable
*/
this.setMenuAt = function (i, m) {
if (m === this) {
throw new Error("Menu cannot be sub-menu of its own");
}
if (this.isDecorative(i)) {
throw new Error("Decorative element cannot have a sub-menu");
}
var p = this.kids[i];
if (p.activateSub !== undefined) {
var sub = this.menus.hasOwnProperty(p) ? this.menus[p] : null;
if (m !== null) {
if (sub === null) {
p.activateSub(true);
}
} else if (sub !== null) {
p.activateSub(false);
}
}
// if the menu is shown and the menu item is selected
if (this.parent !== null && i === this.selectedIndex) {
this.select(-1);
}
if (p.$hash$ === undefined) {
throw new Error("Invalid key");
}
if (m === null) {
delete this.menus[p];
} else {
this.menus[p] = m;
}
return this;
};
/**
* Get the specified sub-menu index
* @param {zebkit.ui.Menu} menu a sub menu
* @return {Integer} a sub menu index. -1 if the menu is
* not a sub menu of the given menu
* @method indexMenuOf
*/
this.indexMenuOf = function(menu) {
for(var i = 0; i < this.kids.length; i++) {
if (this.menus[this.kids[i]] === menu) {
return i;
}
}
return -1;
};
/**
* Called when the menu or a sub-menu has been canceled (key ESCAPE has been pressed).
* @param {zebkit.ui.Menu} m a menu (or sub menu) that has been canceled
* @method $canceled
* @protected
*/
this.$canceled = function(m) {
if (this.$parentMenu !== null && this.$canceled !== undefined) {
this.$parentMenu.$canceled(m);
}
};
/**
* Get the top menu in the given shown popup menu hierarchy
* @return {zebkit.ui.Menu} a top menu
* @method $topMenu
* @protected
*/
this.$topMenu = function() {
if (this.parent !== null) {
var t = this,
p = null;
while ((p = t.$parentMenu) !== null) {
t = p;
}
return t;
}
return null;
};
this.doScroll = function(dx, dy, source) {
var sy = this.scrollManager.getSY(),
ps = this.layout.calcPreferredSize(this),
eh = this.height - this.getTop() - this.getBottom();
if (this.height < ps.height && sy + ps.height >= eh && sy - dy <= 0) {
var nsy = sy - dy;
if (nsy + ps.height < eh) {
nsy = eh - ps.height;
}
if (sy !== nsy) {
this.scrollManager.scrollYTo(nsy);
}
}
};
/**
* Hide the menu and all visible sub-menus
* @method $hideMenu
* @protected
*/
this.$hideMenu = function() {
if (this.parent !== null) {
var ch = this.$childMenu();
if (ch !== null) {
ch.$hideMenu();
}
this.removeMe();
this.select(-1);
}
};
/**
* Get a sub menu that is shown at the given moment.
* @return {zebkit.ui.Menu} a child sub menu. null if no child sub-menu
* has been shown
* @method $childMenu
* @protected
*/
this.$childMenu = function() {
if (this.parent !== null) {
for(var k in this.menus) {
var m = this.menus[k];
if (m.$parentMenu === this) {
return m;
}
}
}
return null;
};
/**
* Show the given sub menu
* @param {zebkit.ui.Menu} sub a sub menu to be shown
* @method $showSubMenu
* @protected
*/
this.$showSubMenu = function(sub) {
sub.setLocation(this.x + this.width - 10,
this.y + this.kids[this.selectedIndex].y);
sub.toPreferredSize();
this.parent.add(sub);
sub.requestFocus();
};
this.triggerSelectionByPos = function(i) {
return this.getMenuAt(i) !== null && this.$triggeredByPointer === true;
};
},
/**
* Override key pressed events handler to handle key events according to
* context menu component requirements
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
function keyPressed(e){
if (e.code === "Escape") {
if (this.parent !== null) {
var p = this.$parentMenu;
this.$canceled(this);
this.$hideMenu();
if (p !== null) {
p.requestFocus();
}
}
} else {
this.$super(e);
}
},
function insert(i, ctr, c) {
if (zebkit.isString(c)) {
return this.$super(i, ctr, (c.match(/^\-+$/) !== null) ? new this.clazz.Line()
: new this.clazz.MenuItem(c));
} else if (zebkit.instanceOf(c, pkg.Panel)) {
return this.$super(i, ctr, c);
} else {
return this.$super(i, ctr, new this.clazz.MenuItem(c));
}
},
function setParent(p) {
if (p !== null) {
this.select(-1);
this.position.setOffset(null);
} else {
this.$parentMenu = null;
}
this.$super(p);
},
/**
* Add the specified component as a decorative item of the menu
* @param {zebkit.ui.Panel} c an UI component
* @method addDecorative
*/
function addDecorative(c) {
if (c.$isDecorative !== true) {
c.$$isDecorative = true;
}
this.$getSuper("insert").call(this, this.kids.length, null, c);
},
function kidRemoved(i, c, ctr) {
if (c.$$isDecorative !== undefined) {
delete c.$$isDecorative;
}
this.setMenuAt(i, null);
this.$super(i, c, ctr);
},
function isItemSelectable(i) {
return this.$super(i) && this.isDecorative(i) === false;
},
function posChanged(target,prevOffset,prevLine,prevCol) {
var off = target.offset;
if (off >= 0) {
var rs = null;
// hide previously shown sub menu if position has been re-newed
if (this.selectedIndex >= 0 && off !== this.selectedIndex) {
var sub = this.getMenuAt(this.selectedIndex);
if (sub !== null) {
sub.$hideMenu();
rs = -1; // request to clear selection
this.requestFocus();
}
}
// request fire selection if the menu is shown and position has moved to new place
if (this.parent !== null && off !== this.selectedIndex && this.isItemSelectable(off)) {
if (this.triggerSelectionByPos(off)) {
rs = off;
}
}
if (rs !== null) {
this.select(rs);
}
}
this.$super(target, prevOffset, prevLine, prevCol);
},
function fireSelected(prev) {
if (this.parent !== null) {
var sub = null;
if (this.selectedIndex >= 0) {
sub = this.getMenuAt(this.selectedIndex);
if (sub !== null) { // handle sub menu here
if (sub.parent !== null) {
// hide menu since it has been already shown
sub.$hideMenu();
} else {
// show menu
sub.$parentMenu = this;
this.$showSubMenu(sub);
}
} else {
// handle an item menu selection here.
// hide the whole menu hierarchy
var k = this.kids[this.selectedIndex];
if (k.itemSelected !== undefined) {
k.itemSelected();
}
pkg.events.fire("menuItemSelected",
MENU_EVENT.$fillWith(this,
this.selectedIndex,
this.kids[this.selectedIndex]));
// an atomic menu, what means a menu item has been selected
// remove this menu an all parents menus
var top = this.$topMenu();
if (top !== null) {
top.$hideMenu();
}
}
} else if (prev >= 0) {
// hide child menus if null item has been selected
sub = this.getMenuAt(prev);
if (sub !== null && sub.parent !== null) {
// hide menu since it has been already shown
sub.$hideMenu();
}
}
}
this.$super(prev);
}
]);
/**
* Menu bar UI component class. Menu bar can be build in any part of UI application.
* There is no restriction regarding the placement of the component.
*
* var canvas = new zebkit.ui.zCanvas(300,200);
* canvas.setBorderLayout();
*
* var mbar = new zebkit.ui.Menubar([
* {
* content: "Item 1",
* sub : [
* "Subitem 1.1",
* "Subitem 1.2",
* "Subitem 1.3"
* ]
* },
* {
* content: "Item 2",
* sub: [
* "Subitem 2.1",
* "Subitem 2.2",
* "Subitem 2.3"
* ]
* },
* {
* content: "Item 3"
* }
* ]);
*
* canvas.root.add("bottom", mbar);
*
* @class zebkit.ui.Menubar
* @constructor
* @extends zebkit.ui.Menu
*/
pkg.Menubar = Class(pkg.Menu, [
function $clazz() {
this.MenuItem = Class(pkg.MenuItem, [
function(c) {
this.$super(c);
this.getSub().setVisible(false);
this.getCheck().setVisible(false);
},
function $prototype() {
this.$getCheckSize = function() {
return pkg.$getPS(this.getCheck());
};
},
function $clazz() {
this.Label = Class(pkg.MenuItem.Label, []);
}
]);
},
function $prototype() {
this.canHaveFocus = false;
this.triggerSelectionByPos = function (i) {
return this.isItemSelectable(i) && this.selectedIndex >= 0;
};
// making menu bar not removable from its layer by overriding the method
this.$hideMenu = function() {
var child = this.$childMenu();
if (child !== null) {
child.$hideMenu();
}
this.select(-1);
};
this.$showSubMenu = function(menu) {
var d = this.getCanvas(),
k = this.kids[this.selectedIndex],
pop = d.getLayer(pkg.PopupLayer.id);
if (menu.hasSelectableItems()) {
var abs = zebkit.layout.toParentOrigin(0, 0, k);
menu.setLocation(abs.x, abs.y + k.height + 1);
menu.toPreferredSize();
pop.add(menu);
menu.requestFocus();
}
};
this.$canceled = function(m) {
this.select(-1);
};
},
// called when an item is selected by user with pointer click or key
function $select(i) {
// if a user again pressed the same item consider it as
// de-selection
if (this.selectedIndex >= 0 && this.selectedIndex === i) {
i = -1;
}
this.$super(i);
}
]);
pkg.PopupLayerLayout = Class(zebkit.layout.Layout, [
function $prototype() {
this.calcPreferredSize = function (target){
return { width:0, height:0 };
};
this.doLayout = function(target) {
for(var i = 0; i < target.kids.length; i++){
var m = target.kids[i];
if (zebkit.instanceOf(m, pkg.Menu)) {
var ps = m.getPreferredSize(),
xx = (m.x + ps.width > target.width ) ? target.width - ps.width : m.x,
yy = (m.y + ps.height > target.height) ? target.height - ps.height : m.y;
m.setSize(ps.width, ps.height);
if (xx < 0) {
xx = 0;
}
if (yy < 0) {
yy = 0;
}
m.setLocation(xx, yy);
}
}
};
}
]);
/**
* UI popup layer interface that defines common part of popup layer
* implementation.
* @class zebkit.ui.PopupLayerMix
* @interface zebkit.ui.PopupLayerMix
*/
pkg.PopupLayerMix = zebkit.Interface([
function $clazz() {
this.id = "popup";
},
function $prototype() {
this.$prevFocusOwner = null;
this.getFocusRoot = function() {
return this;
};
this.childFocusGained = function(e) {
if (zebkit.instanceOf(e.source, pkg.Menu)) {
if (e.related !== null && zebkit.layout.isAncestorOf(this, e.related) === false ) {
this.$prevFocusOwner = e.related;
}
} else {
// means other than menu type of component grabs the focus
// in this case we should not restore focus when the popup
// component will be removed
this.$prevFocusOwner = null;
}
// save the focus owner whose owner was not a pop up layer
if (e.related !== null && zebkit.layout.isAncestorOf(this, e.related) === false && zebkit.instanceOf(e.source, pkg.Menu)) {
this.$prevFocusOwner = e.related;
}
};
this.isTriggeredWith = function(e) {
return e.isAction() === false && (e.identifier === "rmouse" || e.touchCounter === 2);
};
/**
* Define children components input events handler.
* @param {zebkit.ui.event.KeyEvent} e an input event
* @method childKeyPressed
*/
this.childKeyPressed = function(e){
var p = e.source.$parentMenu;
if (p !== undefined && p !== null) {
switch (e.code) {
case "ArrowRight" :
if (p.selectedIndex < p.model.count() - 1) {
p.requestFocus();
p.position.seekLineTo("down");
}
break;
case "ArrowLeft" :
if (p.selectedIndex > 0) {
p.requestFocus();
p.position.seekLineTo("up");
}
break;
}
}
};
this.$topMenu = function() {
if (this.kids.length > 0) {
for (var i = this.kids.length - 1; i >= 0; i--) {
if (zebkit.instanceOf(this.kids[i], pkg.Menu)) {
return this.kids[i].$topMenu();
}
}
}
return null;
};
this.compRemoved = function(e) {
// if last component has been removed and the component is a menu
// than try to restore focus owner
if (this.$prevFocusOwner !== null && this.kids.length === 0 && zebkit.instanceOf(e.kid, pkg.Menu)) {
this.$prevFocusOwner.requestFocus();
this.$prevFocusOwner = null;
}
};
this.pointerPressed = function(e) {
if (this.kids.length > 0) {
var top = this.$topMenu();
if (top !== null) {
top.$hideMenu();
}
// still have a pop up components, than remove it
if (this.kids.length > 0) {
this.removeAll();
}
return true;
} else {
return false;
}
};
// show popup
this.layerPointerClicked = function (e) {
if (this.kids.length === 0 && this.isTriggeredWith(e)) {
var popup = null;
if (e.source.popup !== undefined && e.source.popup !== null) {
popup = e.source.popup;
} else if (e.source.getPopup !== undefined) {
popup = e.source.getPopup(e.source, e.x, e.y);
}
if (popup !== null) {
popup.setLocation(e.absX, e.absY);
this.add(popup);
popup.requestFocus();
}
return true;
} else {
return false;
}
};
},
function getComponentAt(x, y) {
// if there is a component on popup layer and the component is
// not the popup layer itself than return the component otherwise
// return null what delegates getComponentAt() to other layer
if (this.kids.length > 0) {
// if pressed has happened over a popup layer no a menu
var cc = this.$super(x, y);
if (cc === this) {
var top = this.$topMenu();
if (top !== null) {
// if top menu is menu bar. menu bar is located in other layer
// we need check if the pressed has happened not over the
// menu bar
if (zebkit.instanceOf(top, pkg.Menubar)) {
var origin = zebkit.layout.toParentOrigin(top);
// is pointer pressed inside menu bar
if (x >= origin.x && y >= origin.y && x < origin.x + top.width && y < origin.y + top.height) {
return null;
}
}
}
}
return cc;
} else {
return null;
}
}
]);
/**
* Simple popup layer implementation basing on "zebkit.ui.Panel" component.
* @class zebkit.ui.PopupLayer
* @extends zebkit.ui.Panel
* @constructor
* @uses zebkit.ui.PopupLayerMix
*/
pkg.PopupLayer = Class(pkg.Panel, pkg.PopupLayerMix, []);
pkg.events.regEvents('winOpened', 'winActivated');
/**
* Window component event
* @constructor
* @class zebkit.ui.event.WinEvent
* @extends zebkit.Event
*/
pkg.event.WinEvent = Class(zebkit.Event, [
function $prototype() {
/**
* Indicates if the window has been shown
* @attribute isShown
* @type {Boolean}
* @readOnly
*/
this.isShown = false;
/**
* Indicates if the window has been activated
* @attribute isActive
* @type {Boolean}
* @readOnly
*/
this.isActive = false;
/**
* Layer the source window belongs to
* @type {zebkit.ui.Panel}
* @attribute layer
* @readOnly
*/
this.layer = null;
/**
* Fill the event with parameters
* @param {zebkit.ui.Panel} src a source window
* @param {zebkit.ui.Panel} layer a layer the window belongs to
* @param {Boolean} isActive boolean flag that indicates the window status
* @param {Boolean} isShown boolean flag that indicates the window visibility
* @chainable
* @method $fillWidth
*/
this.$fillWith = function(src, layer, isActive, isShown) {
this.source = src;
this.layer = layer;
this.isActive = isActive;
this.isShown = isShown;
return this;
};
}
]);
var WIN_EVENT = new pkg.event.WinEvent();
/**
* Show the given UI component as a modal window
* @param {zebkit.ui.Panel} context an UI component of zebkit hierarchy
* @param {zebkit.ui.Panel} win a component to be shown as the modal window
* @for zebkit.ui
* @method showModalWindow
*/
pkg.showModalWindow = function(context, win) {
pkg.showWindow(context, "modal", win);
};
/**
* Show the given UI component as a window
* @param {zebkit.ui.Panel} context an UI component of zebkit hierarchy
* @param {String} [type] a type of the window: "modal", "mdi", "info". The default
* value is "info"
* @param {zebkit.ui.Panel} win a component to be shown as the window
* @for zebkit.ui
* @method showWindow
*/
pkg.showWindow = function(context, type, win) {
if (arguments.length < 3) {
win = type;
type = "info";
}
return context.getCanvas().getLayer("win").addWin(type, win);
};
/**
* Activate the given window or a window the specified component belongs
* @param {zebkit.ui.Panel} win an UI component to be activated
* @for zebkit.ui
* @method activateWindow
*/
pkg.activateWindow = function(win) {
var l = win.getCanvas().getLayer("win");
l.activate(zebkit.layout.getDirectChild(l, win));
};
/**
* Window layer class. Window layer is supposed to be used for showing
* modal and none modal internal window. There are special ready to use
* "zebkit.ui.Window" UI component that can be shown as internal window, but
* zebkit allows developers to show any UI component as modal or none modal
* window. Add an UI component to window layer to show it as modal o none
* modal window:
*
* // create canvas
* var canvas = new zebkit.ui.zCanvas();
*
* // get windows layer
* var winLayer = canvas.getLayer(zebkit.ui.WinLayerMix.id);
*
* // create standard UI window component
* var win = new zebkit.ui.Window();
* win.setBounds(10,10,200,200);
*
* // show the created window as modal window
* winLayer.addWin("modal", win);
*
* Also shortcut method can be used
*
* // create canvas
* var canvas = new zebkit.ui.zCanvas();
*
* // create standard UI window component
* var win = new zebkit.ui.Window();
* win.setBounds(10,10,200,200);
*
* // show the created window as modal window
* zebkit.ui.showModalWindow(canvas, win);
*
* Window layer supports three types of windows:
*
* - **"modal"** a modal window catches all input till it will be closed
* - **"mdi"** a MDI window can get focus, but it doesn't block switching
* focus to other UI elements
* - **"info"** an INFO window cannot get focus. It is supposed to show
* some information like tooltip.
*
* @class zebkit.ui.WinLayer
* @constructor
* @extends zebkit.ui.HtmlCanvas
*/
pkg.WinLayerMix = zebkit.Interface([
function $clazz() {
this.id = "win";
},
function $prototype() {
/**
* Currently activated as a window children component
* @attribute activeWin
* @type {zebkit.ui.Panel}
* @readOnly
* @protected
*/
this.activeWin = null;
/**
* Top modal window index
* @type {Integer}
* @attribute topModalIndex
* @private
* @default -1
*/
this.topModalIndex = -1;
this.pointerPressed = function(e) {
if (this.topModalIndex < 0 && this.activeWin !== null) { // no a modal window has been shown
this.activate(null);
}
};
this.layerKeyPressed = function(e) {
if (this.kids.length > 0 &&
e.code === "Tab" &&
e.shiftKey === true )
{
if (this.activeWin === null) {
this.activate(this.kids[this.kids.length - 1]);
} else {
var winIndex = this.kids.indexOf(this.activeWin) - 1;
if (winIndex < this.topModalIndex || winIndex < 0) {
winIndex = this.kids.length - 1;
}
this.activate(this.kids[winIndex]);
}
return true;
} else {
return false;
}
};
/**
* Define children components input events handler.
* @param {zebkit.ui.event.FocusEvent} e a focus event
* @method childFocusGained
*/
this.childFocusGained = function (e) {
this.activate(zebkit.layout.getDirectChild(this, e.source));
};
this.childPointerClicked = function (e) {
if (this.kids.length > 0 && (this.activeWin === null || zebkit.layout.isAncestorOf(this.activeWin, e.source) === false)) {
// II) otherwise looking for a window starting from the topest one where the
// pressed event has occurred. Pay attention modal window can open MDI windows
for(var i = this.kids.length - 1; i >= 0 && i >= this.topModalIndex; i--) {
var d = this.kids[i];
if (d.isVisible === true && // check pressed is inside of a MDI window that
d.isEnabled === true && // is shown after currently active modal window
d.winType !== "info" &&
zebkit.layout.isAncestorOf(d, e.source))
{
this.activate(d);
return true;
}
}
}
};
/**
* Get root a component to start focusing traversing
* @return {zebkit.ui.Panel} a root component
* @method getFocusRoot
*/
this.getFocusRoot = function() {
return this.activeWin;
};
/**
* Activate the given win layer children component window.
* @param {zebkit.ui.Panel} c a component to be activated as window
* @method activate
*/
this.activate = function(c) {
if (c !== null && (this.kids.indexOf(c) < 0 ||
c.winType === "info"))
{
throw new Error("Window cannot be activated");
}
if (c !== this.activeWin) {
var old = this.activeWin;
if (c === null) {
var type = this.activeWin.winType;
if (type === "modal") {
throw new Error("Modal window cannot be de-activated");
}
this.activeWin = null;
pkg.events.fire("winActivated", WIN_EVENT.$fillWith(old, this, false, false));
// TODO: special flag $dontGrabFocus is not very elegant solution
if (type === "mdi" && old.$dontGrabFocus !== true) {
pkg.focusManager.requestFocus(null);
}
} else {
if (this.kids.indexOf(c) < this.topModalIndex) {
throw new Error();
}
this.activeWin = c;
this.activeWin.toFront();
if (old !== null) {
pkg.events.fire("winActivated", WIN_EVENT.$fillWith(old, this, false, false));
}
pkg.events.fire("winActivated", WIN_EVENT.$fillWith(c, this, true, false));
this.activeWin.validate();
// TODO: special flag $dontGrabFocus is not very elegant
if (this.activeWin.winType === "mdi" && this.activeWin.$dontGrabFocus !== true) {
var newFocusable = pkg.focusManager.findFocusable(this.activeWin);
pkg.focusManager.requestFocus(newFocusable);
}
}
}
};
/**
* Add the given window with the given type and the listener to the layer.
* @param {String} [type] a type of the window: "modal",
* "mdi" or "info"
* @param {zebkit.ui.Panel} win an UI component to be shown as window
* @method addWin
*/
this.addWin = function(type, win) {
// check if window type argument has been passed
if (arguments.length > 1) {
win.winType = type;
}
this.add(win);
};
this.getComponentAt = function (x, y) {
if (this.kids.length === 0) {
// layer is not active
return null;
} else {
for (var i = this.kids.length - 1 ; i >= 0 && i >= this.topModalIndex; i--) {
var kid = this.kids[i];
if (kid.isVisible &&
kid.winType !== "info" &&
x >= kid.x &&
y >= kid.y &&
x < kid.x + kid.width &&
y < kid.y + kid.height )
{
return kid.getComponentAt(x - kid.x,
y - kid.y);
}
}
return this.activeWin !== null ? this : null;
}
return null;
};
},
function kidAdded(index, constr, lw){
this.$super(index, constr, lw);
if (lw.winType === undefined) {
lw.winType = "mdi";
} else {
zebkit.util.validateValue(lw.winType, "mdi", "modal", "info");
}
if (lw.winType === "modal") {
this.topModalIndex = this.kids.length - 1;
pkg.events.fire("winOpened", WIN_EVENT.$fillWith(lw, this, false, true));
this.activate(lw);
} else {
pkg.events.fire("winOpened", WIN_EVENT.$fillWith(lw, this, false, true));
}
},
function kidRemoved(index, lw, ctr){
this.$getSuper("kidRemoved").call(this, index, lw, ctr);
if (this.activeWin === lw) {
this.activeWin = null;
// TODO: deactivated event can be used as a trigger of a window closing so
// it is better don't fire it here this.fire("winActivated", lw, l);
if (lw.winType === "mdi" && lw.$dontGrabFocus !== true) {
pkg.focusManager.requestFocus(null);
}
}
var ci = index; //this.kids.indexOf(lw);
if (ci < this.topModalIndex) { // correct top modal window index
this.topModalIndex--;
} else if (this.topModalIndex === ci) {
// looking for a new modal window
for (this.topModalIndex = this.kids.length - 1; this.topModalIndex >= 0; this.topModalIndex--){
if (this.kids[this.topModalIndex].winType === "modal") {
break;
}
}
}
pkg.events.fire("winOpened", WIN_EVENT.$fillWith(lw, this, false, false));
if (this.topModalIndex >= 0) {
var aindex = this.kids.length - 1;
while (this.kids[aindex].winType === "info") {
aindex--;
}
this.activate(this.kids[aindex]);
}
}
]);
pkg.WinLayer = Class(pkg.Panel, pkg.WinLayerMix, []);
/**
* Window UI component class. Implements window like UI component. The window component has a header,
* status bar and content areas. The header component is usually placed at the top of window, the
* status bar component is placed at the bottom and the content component at places the central part
* of the window. Also the window defines corner UI component that is supposed to be used to resize
* the window. The window implementation provides the following possibilities:
- Move window by dragging the window on its header
- Resize window by dragging the window corner element
- Place buttons in the header to maximize, minimize, close, etc the window
- Indicates state of window (active or inactive) by changing
the widow header style
- Define a window icon component
- Define a window status bar component
* @class zebkit.ui.Window
*
* @param {String} [s] a window title
* @param {zebkit.ui.Panel} [c] a window content
* @constructor
* @extends zebkit.ui.Panel
*/
pkg.Window = Class(pkg.StatePan, [
function (s, c) {
//!!! for some reason state has to be set beforehand
this.state = "inactive";
this.prevH = this.prevX = this.prevY = 0;
this.px = this.py = this.dx = this.dy = 0;
this.prevW = this.action = -1;
/**
* Window caption panel. The panel contains window
* icons, button and title label
* @attribute caption
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.caption = this.createCaptionPan();
/**
* Window title component
* @type {zebkit.ui.Panel}
* @attribute title
* @readOnly
*/
this.title = this.createTitle();
/**
* Root window panel. The root panel has to be used to
* add any UI components
* @attribute root
* @type {zebkit.ui.Panel}
* @readOnly
*/
if (arguments.length === 0) {
c = s = null;
} else if (arguments.length === 1) {
if (zebkit.instanceOf(s, pkg.Panel)) {
c = s;
s = null;
} else {
c = null;
}
}
this.root = c === null ? this.createContentPan() : c;
this.title.setValue(s === null ? "" : s);
/**
* Icons panel. The panel can contain number of icons.
* @type {zebkit.ui.Panel}
* @attribute icons
* @readOnly
*/
this.icons = new pkg.Panel(new zebkit.layout.FlowLayout("left", "center", "horizontal", 2));
this.icons.add(new this.clazz.Icon());
/**
* Window buttons panel. The panel can contain number of window buttons
* @type {zebkit.ui.Panel}
* @attribute buttons
* @readOnly
*/
this.buttons = new pkg.Panel(new zebkit.layout.FlowLayout("center", "center"));
this.caption.add("center", this.title);
this.caption.add("left", this.icons);
this.caption.add("right", this.buttons);
/**
* Window status panel.
* @attribute status
* @readOnly
* @type {zebkit.ui.Panel}
*/
this.status = new this.clazz.StatusPan();
this.sizer = new this.clazz.SizerPan();
this.status.add(this.sizer);
this.setSizeable(true);
this.$super();
this.setBorderLayout(2,2);
this.add("center", this.root);
this.add("top", this.caption);
this.add("bottom", this.status);
},
function $clazz() {
this.CaptionPan = Class(pkg.StatePan, [
function $prototype() {
this.state = "inactive";
}
]);
this.TitleLab = Class(pkg.Label, []);
this.StatusPan = Class(pkg.Panel, []);
this.ContentPan = Class(pkg.Panel, []);
this.SizerPan = Class(pkg.ViewPan, []);
this.Icon = Class(pkg.ImagePan, []);
this.Button = Class(pkg.Button, []);
},
function $prototype() {
var MOVE_ACTION = 1, SIZE_ACTION = 2;
this.sizer = this.caption = null;
this.isPopupEditor = true;
/**
* Minimal possible size of the window
* @default 40
* @attribute minSize
* @type {Integer}
*/
this.minSize = 40;
/**
* Indicate if the window can be resized by dragging its by corner
* @attribute isSizeable
* @type {Boolean}
* @default true
* @readOnly
*/
this.isSizeable = true;
/**
* Test if the window is shown as a window and activated
* @return {Boolean} true is the window is shown as internal window and
* is active.
* @method isActive
*/
this.isActive = function() {
var c = this.getCanvas();
return c !== null && c.getLayer("win").activeWin === this.getWinContainer();
};
this.pointerDragStarted = function(e){
this.px = e.absX;
this.py = e.absY;
this.action = this.insideCorner(e.x, e.y) ? (this.isSizeable ? SIZE_ACTION : -1)
: MOVE_ACTION;
if (this.action > 0) {
this.dy = this.dx = 0;
}
};
this.pointerDragged = function(e){
if (this.action > 0) {
var container = null;
if (this.action !== MOVE_ACTION){
container = this.getWinContainer();
var nw = this.dx + container.width,
nh = this.dy + container.height;
if (nw > this.minSize && nh > this.minSize) {
container.setSize(nw, nh);
}
}
this.dx = (e.absX - this.px);
this.dy = (e.absY - this.py);
this.px = e.absX;
this.py = e.absY;
if (this.action === MOVE_ACTION){
container = this.getWinContainer();
container.setLocation(this.dx + container.x, this.dy + container.y);
}
}
};
this.pointerDragEnded = function(e){
if (this.action > 0){
if (this.action === MOVE_ACTION){
var container = this.getWinContainer();
container.setLocation(this.dx + container.x, this.dy + container.y);
}
this.action = -1;
}
};
this.getWinContainer = function() {
return this;
};
/**
* Test if the pointer cursor is inside the window corner component
* @protected
* @param {Integer} px a x coordinate of the pointer cursor
* @param {Integer} py a y coordinate of the pointer cursor
* @return {Boolean} true if the pointer cursor is inside window
* corner component
* @method insideCorner
*/
this.insideCorner = function(px,py){
return this.getComponentAt(px, py) === this.sizer;
};
this.getCursorType = function(target,x,y){
return (this.isSizeable && this.insideCorner(x, y)) ? pkg.Cursor.SE_RESIZE
: null;
};
this.catchInput = function(c){
var tp = this.caption;
return c === tp ||
(zebkit.layout.isAncestorOf(tp, c) && zebkit.instanceOf(c, pkg.Button) === false) ||
this.sizer === c;
};
this.winOpened = function(e) {
var state = this.isActive() ? "active" : "inactive";
if (this.caption !== null && this.caption.setState !== undefined) {
this.caption.setState(state);
}
this.setState(state);
};
this.winActivated = function(e) {
this.winOpened(e);
};
this.pointerDoubleClicked = function (e){
var x = e.x, y = e.y, cc = this.caption;
if (this.isSizeable === true &&
x > cc.x &&
x < cc.y + cc.width &&
y > cc.y &&
y < cc.y + cc.height)
{
if (this.prevW < 0) {
this.maximize();
} else {
this.restore();
}
}
};
/**
* Test if the window has been maximized to occupy the whole
* window layer space.
* @return {Boolean} true if the window has been maximized
* @method isMaximized
*/
this.isMaximized = function() {
return this.prevW !== -1;
};
/**
* Create a caption component
* @return {zebkit.ui.Panel} a zebkit caption component
* @method createCaptionPan
* @protected
*/
this.createCaptionPan = function() {
return new this.clazz.CaptionPan();
};
/**
* Create a content component
* @return {zebkit.ui.Panel} a content component
* @method createContentPan
* @protected
*/
this.createContentPan = function() {
return new this.clazz.ContentPan();
};
/**
* Create a caption title label
* @return {zebkit.ui.Label} a caption title label
* @method createTitle
* @protected
*/
this.createTitle = function() {
return new this.clazz.TitleLab();
};
this.setIcon = function(i, icon) {
if (zebkit.isString(icon) || zebkit.instanceOf(icon, zebkit.draw.Picture)) {
icon = new pkg.ImagePan(icon);
}
this.icons.setAt(i, icon);
return this;
};
/**
* Make the window sizable or not sizeable
* @param {Boolean} b a sizeable state of the window
* @chainable
* @method setSizeable
*/
this.setSizeable = function(b){
if (this.isSizeable !== b){
this.isSizeable = b;
if (this.sizer !== null) {
this.sizer.setVisible(b);
}
}
return this;
};
/**
* Maximize the window
* @method maximize
* @chainable
*/
this.maximize = function(){
if (this.prevW < 0){
var d = this.getCanvas(),
cont = this.getWinContainer(),
left = d.getLeft(),
top = d.getTop();
this.prevX = cont.x;
this.prevY = cont.y;
this.prevW = cont.width;
this.prevH = cont.height;
cont.setBounds(left, top,
d.width - left - d.getRight(),
d.height - top - d.getBottom());
}
return this;
};
/**
* Restore the window size
* @method restore
* @chainable
*/
this.restore = function(){
if (this.prevW >= 0){
this.getWinContainer().setBounds(this.prevX, this.prevY,
this.prevW, this.prevH);
this.prevW = -1;
}
return this;
};
/**
* Close the window
* @method close
* @chainable
*/
this.close = function() {
this.getWinContainer().removeMe();
return this;
};
/**
* Set the window buttons set.
* @param {Object} buttons dictionary of buttons icons for window buttons.
* The dictionary key defines a method of the window component to be called
* when the given button has been pressed. So the method has to be defined
* in the window component.
* @method setButtons
*/
this.setButtons = function(buttons) {
// remove previously added buttons
for(var i = 0; i < this.buttons.length; i++) {
var kid = this.buttons.kids[i];
if (kid.isEventFired()) {
kid.off();
}
}
this.buttons.removeAll();
// add new buttons set
for(var k in buttons) {
if (buttons.hasOwnProperty(k)) {
console.log("!!!!!!!!!!!!!!!! apply properties : " + JSON.stringify(buttons[k]));
var b = new this.clazz.Button();
b.properties(buttons[k]);
this.buttons.add(b);
(function(t, f) {
b.on(function() { f.call(t); });
})(this, this[k]);
}
}
return this;
};
},
function focused(){
this.$super();
if (this.caption !== null) {
this.caption.repaint();
}
}
]);
/**
* Tooltip UI component. The component can be used as a tooltip that shows specified content in
* figured border.
* @class zebkit.ui.Tooltip
* @param {zebkit.util.Panel|String} a content component or test label to be shown in tooltip
* @constructor
* @extends zebkit.ui.Panel
*/
pkg.Tooltip = Class(pkg.Panel, [
function(content) {
this.$super();
if (arguments.length > 0) {
this.add(pkg.$component(content, this));
this.toPreferredSize();
}
},
function $clazz() {
this.Label = Class(pkg.Label, []);
this.ImageLabel = Class(pkg.ImageLabel, []);
this.TooltipBorder = Class(zebkit.draw.View, [
function(col, size) {
if (arguments.length > 0) {
this.color = col;
}
if (arguments.length > 1) {
this.size = size;
}
this.gap = 2 * this.size;
},
function $prototype() {
this.color = "black";
this.size = 2;
this.paint = function (g,x,y,w,h,d) {
if (this.color !== null) {
this.outline(g,x,y,w,h,d);
g.setColor(this.color);
g.lineWidth = this.size;
g.stroke();
}
};
this.outline = function(g,x,y,w,h,d) {
g.beginPath();
h -= 2 * this.size;
w -= 2 * this.size;
x += this.size;
y += this.size;
var w2 = Math.round(w /2),
w3_8 = Math.round((3 * w)/8),
h2_3 = Math.round((2 * h)/3),
h3 = Math.round(h/3),
w4 = Math.round(w/4);
g.moveTo(x + w2, y);
g.quadraticCurveTo(x, y, x, y + h3);
g.quadraticCurveTo(x, y + h2_3, x + w4, y + h2_3);
g.quadraticCurveTo(x + w4, y + h, x, y + h);
g.quadraticCurveTo(x + w3_8, y + h, x + w2, y + h2_3);
g.quadraticCurveTo(x + w, y + h2_3, x + w, y + h3);
g.quadraticCurveTo(x + w, y, x + w2, y);
g.closePath();
return true;
};
}
]);
},
function setValue(v) {
this.kids[0].setValue(v);
return this;
},
function recalc() {
this.$contentPs = (this.kids.length === 0 ? this.$super()
: this.kids[0].getPreferredSize());
},
function getBottom() {
return this.$super() + this.$contentPs.height;
},
function getTop() {
return this.$super() + Math.round(this.$contentPs.height / 6);
},
function getLeft() {
return this.$super() + Math.round(this.$contentPs.height / 6);
},
function getRight() {
return this.$super() + Math.round(this.$contentPs.height / 6);
}
]);
/**
* Popup window manager class. The manager registering and triggers showing context popup menu
* and tooltips. Menu appearing is triggered by right pointer click or double fingers touch event.
* To bind a popup menu to an UI component you can either set "tooltip" property of the component
* with a popup menu instance:
// create canvas
var canvas = new zebkit.ui.zCanvas();
// create menu with three items
var m = new zebkit.ui.Menu();
m.add("Menu Item 1");
m.add("Menu Item 2");
m.add("Menu Item 3");
// bind the menu to root panel
canvas.root.popup = m;
* Or implement "getPopup(target,x,y)" method that can rule showing popup menu depending on
* the current cursor location:
// create canvas
var canvas = new zebkit.ui.zCanvas();
// visualize 50x50 pixels hot component spot
// to which the context menu is bound
canvas.root.paint = function(g) {
g.setColor("red");
g.fillRect(50,50,50,50);
}
// create menu with three items
var m = new zebkit.ui.Menu();
m.add("Menu Item 1");
m.add("Menu Item 2");
m.add("Menu Item 3");
// implement "getPopup" method that shows popup menu only
// if pointer cursor located at red rectangular area of the
// component
canvas.root.getPopup = function(target, x, y) {
// test if pointer cursor position is in red spot area
// and return context menu if it is true
if (x > 50 && y > 50 && x < 100 && y < 100) {
return m;
}
return null;
}
* Defining a tooltip for an UI component follows the same approach. Other you
* define set "tooltip" property of your component with a component that has to
* be shown as the tooltip:
// create canvas
var canvas = new zebkit.ui.zCanvas();
// create tooltip
var t = new zebkit.ui.Label("Tooltip");
t.setBorder("plain");
t.setBackground("yellow");
t.setPadding(6);
// bind the tooltip to root panel
canvas.root.popup = t;
* Or you can implement "getTooltip(target,x,y)" method if the tooltip showing depends on
* the pointer cursor location:
// create canvas
var canvas = new zebkit.ui.zCanvas();
// create tooltip
var t = new zebkit.ui.Label("Tooltip");
t.setBorder("plain");
t.setBackground("yellow");
t.setPadding(6);
// bind the tooltip to root panel
canvas.root.getPopup = function(target, x, y) {
return x < 10 && y < 10 ? t : null;
};
* @class zebkit.ui.TooltipManager
* @extends zebkit.ui.event.Manager
* @constructor
*/
/**
* Fired when a menu item has been selected
zebkit.ui.events.on("menuItemSelected", function(menu, index, item) {
...
});
*
* @event menuItemSelected
* @param {zebkit.ui.Menu} menu a menu component that triggers the event
* @param {Integer} index a menu item index that has been selected
* @param {zebkit.ui.Panel} item a menu item component that has been selected
*/
pkg.TooltipManager = Class(zebkit.ui.event.Manager, [
function $prototype() {
this.$tooltipX = this.$tooltipY = 0;
this.$toolTask = this.$targetTooltipLayer = this.$tooltip = this.$target = null;
/**
* Indicates if a shown tooltip has to disappear by pointer pressed event
* @attribute hideTooltipByPress
* @type {Boolean}
* @default true
*/
this.hideTooltipByPress = true;
/**
* Define interval (in milliseconds) between entering a component and showing
* a tooltip for the entered component
* @attribute showTooltipIn
* @type {Integer}
* @default 400
*/
this.showTooltipIn = 400;
/**
* Indicates if tool tip position has to be synchronized with pointer position
* @attribute syncTooltipPosition
* @type {Boolean}
* @default true
*/
this.syncTooltipPosition = true;
/**
* Define pointer clicked event handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerClicked
*/
// this.pointerClicked = function (e){
// // Right button
// // TODO: check if it is ok and compatible with touch
// if (this.isTriggeredWith(e)) {
// var popup = null;
// if (e.source.popup != null) {
// popup = e.source.popup;
// } else {
// if (e.source.getPopup != null) {
// popup = e.source.getPopup(e.source, e.x, e.y);
// }
// }
// if (popup != null) {
// popup.setLocation(e.absX, e.absY);
// e.source.getCanvas().getLayer(pkg.PopupLayer.id).add(popup);
// popup.requestFocus();
// }
// }
// };
/**
* Define pointer entered event handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerEntered
*/
this.pointerEntered = function(e) {
if (this.$target === null &&
((e.source.tooltip !== undefined && e.source.tooltip !== null) || e.source.getTooltip !== undefined))
{
this.$target = e.source;
this.$targetTooltipLayer = e.source.getCanvas().getLayer("win");
this.$tooltipX = e.x;
this.$tooltipY = e.y;
this.$toolTask = zebkit.util.tasksSet.run(
this,
this.showTooltipIn,
this.showTooltipIn
);
}
};
/**
* Define pointer exited event handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerExited
*/
this.pointerExited = function(e) {
// exited triggers tooltip hiding only for "info" tooltips
if (this.$target !== null && (this.$tooltip === null || this.$tooltip.winType === "info")) {
this.stopShowingTooltip();
}
};
/**
* Define pointer moved event handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerMoved
*/
this.pointerMoved = function(e) {
// to prevent handling pointer moved from component of mdi
// tooltip we have to check if target equals to source
// instead of just checking if target is not a null
if (this.$target === e.source) {
// store a new location for a tooltip
this.$tooltipX = e.x;
this.$tooltipY = e.y;
// wake up task try showing a tooltip
// at the new location
if (this.$toolTask !== null) {
this.$toolTask.resume(this.showTooltipIn);
}
}
};
/**
* Task body method
* @private
* @param {Task} t a task context
* @method run
*/
this.run = function(t) {
if (this.$target !== null) {
var ntooltip = (this.$target.tooltip !== undefined &&
this.$target.tooltip !== null) ? this.$target.tooltip
: this.$target.getTooltip(this.$target,
this.$tooltipX,
this.$tooltipY),
p = null,
tx = 0,
ty = 0;
if (this.$tooltip !== ntooltip) {
// hide previously shown tooltip
if (this.$tooltip !== null) {
this.hideTooltip();
}
// set new tooltip
this.$tooltip = ntooltip;
// if new tooltip exists than show it
if (ntooltip !== null) {
p = zebkit.layout.toParentOrigin(this.$tooltipX, this.$tooltipY, this.$target);
this.$tooltip.toPreferredSize();
tx = p.x;
ty = p.y - this.$tooltip.height;
var dw = this.$targetTooltipLayer.width;
if (tx + this.$tooltip.width > dw) {
tx = dw - this.$tooltip.width - 1;
}
this.$tooltip.setLocation(tx < 0 ? 0 : tx, ty < 0 ? 0 : ty);
if (this.$tooltip.winType === undefined) {
this.$tooltip.winType = "info";
}
this.$targetTooltipLayer.add(this.$tooltip);
if (this.$tooltip.winType !== "info") {
pkg.activateWindow(this.$tooltip);
}
}
} else {
if (this.$tooltip !== null && this.syncTooltipPosition === true) {
p = zebkit.layout.toParentOrigin(this.$tooltipX,
this.$tooltipY,
this.$target);
tx = p.x;
ty = p.y - this.$tooltip.height;
this.$tooltip.setLocation(tx < 0 ? 0 : tx, ty < 0 ? 0 : ty);
}
}
}
t.pause();
};
this.winActivated = function(e) {
// this method is called only for mdi window
// consider every deactivation of a mdi window as
// a signal to stop showing tooltip
if (e.isActive === false && this.$tooltip !== null) {
this.$tooltip.removeMe();
}
};
this.winOpened = function(e) {
if (e.isShown === false) {
// cleanup tooltip reference
this.$tooltip = null;
if (e.source.winType !== "info") {
this.stopShowingTooltip();
}
}
};
/**
* Stop showing tooltip
* @private
* @method stopShowingTooltip
*/
this.stopShowingTooltip = function() {
if (this.$target !== null) {
this.$target = null;
}
if (this.$toolTask !== null) {
this.$toolTask.shutdown();
}
this.hideTooltip();
};
/**
* Hide tooltip if it has been shown
* @method hideTooltip
*/
this.hideTooltip = function(){
if (this.$tooltip !== null) {
this.$tooltip.removeMe();
this.$tooltip = null;
}
};
/**
* Define pointer pressed event handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerPressed
*/
this.pointerPressed = function(e) {
if (this.hideTooltipByPress === true &&
e.pointerType === "mouse" &&
this.$target !== null && this.$target.hideTooltipByPress !== false &&
(this.$tooltip === null || this.$tooltip.winType === "info"))
{
this.stopShowingTooltip();
}
};
/**
* Define pointer released event handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerReleased
*/
this.pointerReleased = function(e) {
if ((this.hideTooltipByPress === false || e.pointerType !== "mouse") &&
(this.$target !== null && this.$target.hideTooltipByPress !== false) &&
(this.$tooltip === null || this.$tooltip.winType === "info"))
{
this.stopShowingTooltip();
}
};
}
]);
pkg.Spin = new Class(pkg.Panel, [
function(min, max) {
this.$super(this);
this.step = 1;
this.min = arguments.length === 0 ? 0 : min;
this.max = arguments.length < 2 ? this.min + 10 : max;
this.editor = null;
this.isLooped = true;
this.layoutComponents(new this.clazz.TextField(this, this.min, this.max),
new this.clazz.IncButton(),
new this.clazz.DecButton());
},
function $clazz() {
this.IncButton = Class(pkg.ArrowButton, []);
this.DecButton = Class(pkg.ArrowButton, []);
this.DecButton.prototype.increment = -1;
this.IncButton.prototype.increment = 1;
this.TextField = Class(pkg.TextField, [
function (target, min, max) {
this.target = target;
this.min = min;
this.max = max;
var $this = this;
this.$super(new zebkit.data.SingleLineTxt([
function () {
this.$super("" + min);
},
function validate(value) {
return $this.isValideValue(value);
}
]));
},
function isValideValue(v) {
if (/^[-+]?^[0]*[0-9]+$/.test(v) === true) {
var iv = parseInt(v);
return iv >= this.min && iv <= this.max;
}
return false;
},
function keyTyped(e) {
var model = this.getModel(),
validate = model.validate,
prevValue = this.getValue();
model.validate = null;
var pos = this.position.offset;
if (pos >= 0 && pos < prevValue.length && this.hasSelection() === false) {
this.select(pos, pos + 1);
}
this.$super(e);
if (this.isValideValue(this.getValue()) === false) {
this.setValue(prevValue);
}
model.validate = validate;
},
function calcPreferredSize(target) {
var font = this.getFont();
return { width : Math.max(font.stringWidth("" + this.max), font.stringWidth("" + this.min)),
height : font.height };
},
function textUpdated(src, op, off, size, startLine, lines) {
this.$super(src, op, off, size, startLine, lines);
if (this.getModel().validate !== undefined) {
// TODO: manual text input is not allowed yet
}
}
]);
},
function layoutComponents(text, inc, dec) {
var buttons = new pkg.Panel(new zebkit.layout.PercentLayout("vertical"));
this.setBorderLayout();
var tfPan = new pkg.Panel(new zebkit.layout.FlowLayout("left", "center"));
tfPan.layout.stretchLast = true;
this.add("center", tfPan);
tfPan.add(text);
this.add("right", buttons);
buttons.add(50, inc);
buttons.add(50, dec);
// this.setBorderLayout();
// this.add("center", text);
// var buttons = new pkg.Panel(new zebkit.layout.BorderLayout());
// buttons.add("top", inc);
// buttons.add("bottom", dec);
// this.add("right", buttons);
// this.setBorderLayout();
// this.add("center", text );
// this.add("left", dec);
// this.add("right", inc);
},
function $install(child) {
console.log("$install() : " + child.clazz.$name + "," + child.isEventFired("fired"));
if (child.isEventFired("fired")) {
child.on(this);
} else if (zebkit.instanceOf(child, pkg.TextField)) {
this.editor = child;
}
},
function $uninstall(child) {
if (child.isEventFired("fired")) {
child.off(this);
} else if (zebkit.instanceOf(child, pkg.TextField)) {
this.editor = null;
}
},
function compAdded(e) {
this.$install(e.kid);
},
function compRemoved(e) {
this.$uninstall(e.kid);
},
function childCompAdded(e) {
// TODO: check it is called and kid is proper field
this.$install(e.kid);
},
function childCompRemoved(e) {
// TODO: check it is called and kid is proper field
this.$uninstall(e.kid);
},
function keyPressed(e) {
if (e.code === "ArrowDown") {
this.setValue(this.getValue() - this.step);
} else if (e.code === "ArrowUp") {
this.setValue(this.getValue() + this.step);
}
},
function setMinMax (min, max) {
if (this.min !== min && this.max !== max) {
this.min = min;
this.max = max;
this.setValue(this.getValue());
this.vrp();
}
},
function catchInput(c) {
return zebkit.instanceOf(c, pkg.ArrowButton) === false;
},
function setLoopEnabled(b) {
this.isLooped = b;
},
function fired(src) {
this.setValue(this.getValue() + src.increment * this.step);
},
function getValue(){
var value = this.editor.getValue();
if (value === "") {
return this.min;
} else {
return parseInt((value.indexOf('+') === 0) ? value.substring(1) : value);
}
},
function setValue(v){
if (v < this.min) {
v = this.isLooped ? this.max : this.min;
}
if (v > this.max) {
v = this.isLooped ? this.min : this.max;
}
var prev = this.getValue();
if (prev !== v) {
var prevValue = prev;
this.editor.setValue("" + v);
this.repaint();
this.fire("fired", [ this, prevValue ]);
}
}
]).events("fired");
},true);
zebkit.package("ui.tree", function(pkg, Class) {
'use strict';
var ui = pkg.cd("..");
/**
* Tree UI components and all related to the component classes and interfaces.
* Tree components are graphical representation of a tree model that allows a user
* to navigate over the model item, customize the items rendering and
* organize customizable editing of the items.
*
* // create tree component instance to visualize the given tree model
* var tree = new zebkit.ui.tree.Tree({
* value: "Root",
* kids : [
* "Item 1",
* "Item 2",
* "Item 3"
* ]
* });
*
* // make all tree items editable with text field component
* tree.setEditorProvider(new zebkit.ui.tree.DefEditors());
*
* One more tree component implementation - "CompTree" - allows developers
* to create tree whose nodes are other UI components
*
* // create tree component instance to visualize the given tree model
* var tree = new zebkit.ui.tree.CompTree({
* value: new zebkit.ui.Label("Root label item"),
* kids : [
* new zebkit.ui.Checkbox("Checkbox Item"),
* new zebkit.ui.Button("Button Item"),
* new zebkit.ui.TextField("Text field item")
* ]
* });
*
* @class zebkit.ui.tree
* @access package
*/
// tree node metrics:
// |
// |-- <-gapx-> {icon} -- <-gapx-> {view}
//
/**
* Simple private structure to keep a tree model item metrical characteristics
* @constructor
* @param {Boolean} b a state of an appropriate tree component node of the given
* tree model item. The state is sensible for item that has children items and
* the state indicates if the given tree node is collapsed (false) or expanded
* (true)
* @private
* @class zebkit.ui.tree.ItemMetric
*/
pkg.ItemMetric = function(b) {
/**
* The whole width of tree node that includes a rendered item preferred
* width, all icons and gaps widths
* @attribute width
* @type {Integer}
* @readOnly
*/
/**
* The whole height of tree node that includes a rendered item preferred
* height, all icons and gaps heights
* @attribute height
* @type {Integer}
* @readOnly
*/
/**
* Width of an area of rendered tree model item. It excludes icons, toggle
* and gaps widths
* @attribute viewWidth
* @type {Integer}
* @readOnly
*/
/**
* Height of an area of rendered tree model item. It excludes icons, toggle
* and gaps heights
* @attribute viewHeight
* @type {Integer}
* @readOnly
*/
/**
* Indicates whether a node is in expanded or collapsed state
* @attribute isOpen
* @type {Boolean}
* @readOnly
*/
this.width = this.height = this.x = this.y = this.viewHeight = 0;
this.viewWidth = -1;
this.isOpen = b;
};
/**
* Default tree editor provider
* @constructor
* @class zebkit.ui.tree.DefEditors
*/
pkg.DefEditors = Class([
function() {
/**
* Internal component that are designed as default editor component
* @private
* @readOnly
* @attribute tf
* @type {zebkit.ui.TextField}
*/
this.tf = new this.clazz.TextField(new zebkit.data.SingleLineTxt(""));
},
function $clazz() {
this.TextField = Class(ui.TextField, []);
},
function $prototype() {
/**
* Get an UI component to edit the given tree model element
* @param {zebkit.ui.tree.Tree} src a tree component
* @param {zebkit.data.Item} item an data model item
* @return {zebkit.ui.Panel} an editor UI component
* @method getEditor
*/
this.getEditor = function(src, item){
var o = item.value;
this.tf.setValue(o === null ? "" : o.toString());
return this.tf;
};
/**
* Fetch a model item from the given UI editor component
* @param {zebkit.ui.tree.Tree} src a tree UI component
* @param {zebkit.ui.Panel} editor an editor that has been used to edit the tree model element
* @return {Object} an new tree model element value fetched from the given UI editor component
* @method fetchEditedValue
*/
this.fetchEditedValue = function(src, editor){
return editor.view.target.getValue();
};
/**
* The method is called to ask if the given input event should trigger an tree component item
* @param {zebkit.ui.tree.Tree} src a tree UI component
* @param {zebkit.ui.event.PointerEvent|zebkit.ui.event.KeyEvent} e an input event: pointer
* or key event
* @return {Boolean} true if the event should trigger edition of a tree component item
* @method @shouldStartEdit
*/
this.shouldStartEdit = function(src,e){
return e.id === "pointerDoubleClicked" ||
(e.id === "keyPressed" && e.code === "Enter");
};
}
]);
/**
* Default tree editor view provider
* @class zebkit.ui.tree.DefViews
* @constructor
* @extends zebkit.draw.BaseViewProvider
*/
pkg.DefViews = Class(zebkit.draw.BaseViewProvider, [
/**
* Get a view for the given model item of the UI tree component
* @param {zebkit.ui.tree.Tree} tree a tree component
* @param {zebkit.data.Item} item a tree model element
* @return {zebkit.draw.View} a view to visualize the given tree data model element
* @method getView
*/
function getView(tree, item) {
return this.$super(tree, item.value);
}
]);
/**
* Abstract tree component that can used as basement for building own tree components.
* The component is responsible for rendering tree, calculating tree nodes metrics,
* computing visible area, organizing basic user interaction. Classes that inherit it
* has to provide the following important things:
* **A tree model item metric** Developers have to implement "getItemPreferredSize(item)"
method to say which size the given tree item wants to have.
* **Tree node item rendering** If necessary developers have to implement the way
a tree item has to be visualized by implementing "this.paintItem(...)" method
*
* @class zebkit.ui.tree.BaseTree
* @uses zebkit.EventProducer
* @constructor
* @param {zebkit.data.TreeModel|Object} a tree model. It can be an instance of tree model
* class or an object that described tree model. An example of such object is shown below:
{
value : "Root",
kids : [
{
value: "Child 1",
kids :[
"Sub child 1"
]
},
"Child 2",
"Child 3"
]
}
* @param {Boolean} [nodeState] a default tree nodes state (expanded or collapsed)
* @extends zebkit.ui.Panel
* @uses zebkit.ui.HostDecorativeViews
*/
/**
* Fired when a tree item has been toggled
tree.on("toggled", function(src, item) {
...
});
* @event toggled
* @param {zebkit.ui.tree.BaseTree} src a tree component that triggers the event
* @param {zebkit.data.Item} item an tree item that has been toggled
*/
/**
* Fired when a tree item has been selected
tree.on("selected", function(src, prevItem) {
...
});
* @event selected
* @param {zebkit.ui.tree.BaseTree} src a tree component that triggers the event
* @param {zebkit.data.Item} prevItem a previously selected tree item
*/
/**
* Fired when a tree item editing has been started
tree.on("editingStarted", function(src, item, editor) {
...
});
* @event editingStarted
* @param {zebkit.ui.tree.BaseTree} src an tree component that triggers the event
* @param {zebkit.data.Item} item a tree item to be edited
* @param {zebkit.ui.Panel} editor an editor to be used to edit the given item
*/
/**
* Fired when a tree item editing has been stopped
tree.on("editingStopped", function(src, item, oldValue, editor, isApplied) {
...
});
* @event editingStopped
* @param {zebkit.ui.tree.BaseTree} src a tree component that triggers the event
* @param {zebkit.data.Item} item a tree item that has been edited
* @param {Object} oldValue an old value of the edited tree item
* @param {zebkit.ui.Panel} editor an editor to be used to edit the given item
* @param {Boolean} isApplied flag that indicates if the edited value has been
* applied to the given tree item
*/
pkg.BaseTree = Class(ui.Panel, ui.HostDecorativeViews, [
function (d, b) {
if (arguments.length < 2) {
b = true;
}
this.maxw = this.maxh = 0;
this.views = {};
this.viewSizes = {};
this._isVal = false;
this.nodes = {};
this.setLineColor("gray");
this.isOpenVal = b;
this.setSelectable(true);
this.$super();
this.setModel(d);
this.scrollManager = new ui.ScrollManager(this);
},
function $prototype() {
/**
* Tree component line color
* @attribute lnColor
* @type {String}
* @readOnly
*/
this.visibleArea = this.lnColor = null;
/**
* Selected tree model item
* @attribute selected
* @type {zebkit.data.Item}
* @default null
* @readOnly
*/
this.model = this.selected = this.firstVisible = null;
/**
* Horizontal gap between a node elements: toggle, icons and tree item view
* @attribute gapx
* @readOnly
* @default 2
* @type {Integer}
*/
/**
* Vertical gap between a node elements: toggle, icons and tree item view
* @attribute gapy
* @readOnly
* @default 2
* @type {Integer}
*/
this.gapx = this.gapy = 2;
this.canHaveFocus = true;
/**
* Test if the given tree component item is opened
* @param {zebkit.data.Item} i a tree model item
* @return {Boolean} true if the given tree component item is opened
* @method isOpen
*/
this.isOpen = function(i){
this.validate();
return this.$isOpen(i);
};
/**
* Get calculated for the given tree model item metrics
* @param {zebkit.data.Item} i a tree item
* @return {Object} an tree model item metrics. Th
* @method getItemMetrics
*/
this.getItemMetrics = function(i){
this.validate();
return this.getIM(i);
};
/**
* Called every time a pointer pressed in toggle area.
* @param {zebkit.data.Item} root an tree item where toggle has been done
* @method togglePressed
* @protected
*/
this.togglePressed = function(root) {
this.toggle(root);
};
this.itemPressed = function(root, e) {
this.select(root);
};
this.pointerPressed = function(e){
if (this.firstVisible !== null && e.isAction()) {
var x = e.x,
y = e.y,
root = this.getItemAt(this.firstVisible, x, y);
if (root !== null) {
x -= this.scrollManager.getSX();
y -= this.scrollManager.getSY();
var r = this.getToggleBounds(root);
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height){
this.togglePressed(root);
} else if (x > r.x + r.width) {
this.itemPressed(root, e);
}
}
}
};
this.vVisibility = function (){
if (this.model === null) {
this.firstVisible = null;
}
else {
var nva = ui.$cvp(this, {});
if (nva === null) {
this.firstVisible = null;
} else {
if (this._isVal === false ||
(this.visibleArea === null ||
this.visibleArea.x !== nva.x ||
this.visibleArea.y !== nva.y ||
this.visibleArea.width !== nva.width ||
this.visibleArea.height !== nva.height ))
{
this.visibleArea = nva;
if (this.firstVisible !== null) {
this.firstVisible = this.findOpened(this.firstVisible);
this.firstVisible = this.isOverVisibleArea(this.firstVisible) ? this.nextVisible(this.firstVisible)
: this.prevVisible(this.firstVisible);
} else {
this.firstVisible = (-this.scrollManager.getSY() > Math.floor(this.maxh / 2)) ? this.prevVisible(this.findLast(this.model.root))
: this.nextVisible(this.model.root);
}
}
}
}
this._isVal = true;
};
this.recalc = function() {
this.maxh = this.maxw = 0;
if (this.model !== null && this.model.root !== null) {
this.$recalc(this.getLeft(), this.getTop(), null, this.model.root, true);
this.maxw -= this.getLeft();
this.maxh -= this.gapy;
}
};
/**
* Get tree model item metrical bounds (location and size).
* @param {zebkit.data.Item} root an tree model item
* @return {Object} a structure that keeps an item view location
* and size:
{
x: {Integer},
y: {Integer},
width: {Integer},
height: {Integer}
}
* @method getItemBounds
* @protected
*/
this.getItemBounds = function(root){
var metrics = this.getIM(root),
toggle = this.getToggleBounds(root),
image = this.getIconBounds(root);
toggle.x = image.x + image.width + (image.width > 0 || toggle.width > 0 ? this.gapx : 0);
toggle.y = metrics.y + Math.floor((metrics.height - metrics.viewHeight) / 2);
toggle.width = metrics.viewWidth;
toggle.height = metrics.viewHeight;
return toggle;
};
/**
* Get toggle element bounds for the given tree model item.
* @param {zebkit.data.Item} root an tree model item
* @return {Object} a structure that keeps an item toggle location
* and size:
*
* {
* x: {Integer},
* y: {Integer},
* width: {Integer},
* height: {Integer}
* }
*
* @method getToggleBounds
* @protected
*/
this.getToggleBounds = function(root){
var node = this.getIM(root), d = this.getToggleSize(root);
return { x : node.x,
y : node.y + Math.floor((node.height - d.height) / 2),
width : d.width,
height: d.height };
};
/**
* Get current toggle element view. The view depends on the state of tree item.
* @param {zebkit.data.Item} i a tree model item
* @protected
* @return {zebkit.draw.View} a toggle element view
* @method getToogleView
*/
this.getToggleView = function(i){
var v = i.kids.length > 0 ? (this.getIM(i).isOpen ? this.views.expandedToggle
: this.views.collapsedToggle) : null;
return (v === undefined ? null : v);
};
/**
* An abstract method that a concrete tree component implementations have to
* override. The method has to return a preferred size the given tree model
* item wants to have.
* @param {zebkit.data.Item} root an tree model item
* @return {Object} a structure that keeps an item preferred size:
*
* {
* width: {Integer},
* height: {Integer}
* }
*
* @method getItemPreferredSize
* @protected
*/
this.getItemPreferredSize = function(root) {
throw new Error("Not implemented");
};
/**
* An abstract method that a concrete tree component implementations should
* override. The method has to render the given tree node of the specified
* tree model item at the given location
* @param {CanvasRenderingContext2D} g a graphical context
* @param {zebkit.data.Item} root a tree model item to be rendered
* @param {zebkit.ui.tree.ItemMetric} node a tree node metrics
* @param {Ineteger} x a x location where the tree node has to be rendered
* @param {Ineteger} y a y location where the tree node has to be rendered
* @method paintItem
* @protected
*/
this.$recalc = function (x,y,parent,root,isVis){
var node = this.getIM(root);
if (isVis === true) {
if (node.viewWidth < 0) {
var viewSize = this.getItemPreferredSize(root);
node.viewWidth = viewSize.width;
node.viewHeight = viewSize.height;
}
var imageSize = this.getIconSize(root),
toggleSize = this.getToggleSize(root);
if (parent !== null){
var pImg = this.getIconBounds(parent);
x = pImg.x + Math.floor((pImg.width - toggleSize.width) / 2);
}
node.x = x;
node.y = y;
node.width = toggleSize.width + imageSize.width +
node.viewWidth + (toggleSize.width > 0 ? this.gapx : 0) + 10 +
(imageSize.width > 0 ? this.gapx : 0);
node.height = Math.max(((toggleSize.height > imageSize.height) ? toggleSize.height
: imageSize.height),
node.viewHeight);
if (node.x + node.width > this.maxw) {
this.maxw = node.x + node.width;
}
this.maxh += (node.height + this.gapy);
x = node.x + toggleSize.width + (toggleSize.width > 0 ? this.gapx : 0);
y += (node.height + this.gapy);
}
var b = node.isOpen && isVis === true;
if (b) {
var count = root.kids.length;
for(var i = 0; i < count; i++) {
y = this.$recalc(x, y, root, root.kids[i], b);
}
}
return y;
};
this.$isOpen = function(i) {
return i === null || (i.kids.length > 0 && this.getIM(i).isOpen && this.$isOpen(i.parent));
};
/**
* Get a tree node metrics by the given tree model item.
* @param {zebkit.data.Item} item a tree model item
* @return {zebkit.ui.tree.ItemMetric} a tree node metrics
* @protected
* @method getIM
*/
this.getIM = function (item) {
if (this.nodes.hasOwnProperty(item.$hash$) === false){
var node = new pkg.ItemMetric(this.isOpenVal);
this.nodes[item.$hash$] = node;
return node;
}
return this.nodes[item.$hash$];
};
/**
* Get a tree item that is located at the given location.
* @param {zebkit.data.Item} [root] a starting tree node
* @param {Integer} x a x coordinate
* @param {Integer} y a y coordinate
* @return {zebkit.data.Item} a tree model item
* @method getItemAt
*/
this.getItemAt = function(root, x, y){
this.validate();
if (arguments.length < 3) {
x = arguments[0];
y = arguments[1];
root = this.model.root;
}
if (this.firstVisible !== null && y >= this.visibleArea.y && y < this.visibleArea.y + this.visibleArea.height){
var dx = this.scrollManager.getSX(),
dy = this.scrollManager.getSY(),
found = this.getItemAtInBranch(root, x - dx, y - dy);
if (found !== null) {
return found;
}
var parent = root.parent;
while (parent !== null) {
var count = parent.kids.length;
for(var i = parent.kids.indexOf(root) + 1;i < count; i ++ ){
found = this.getItemAtInBranch(parent.kids[i], x - dx, y - dy);
if (found !== null) {
return found;
}
}
root = parent;
parent = root.parent;
}
}
return null;
};
this.getItemAtInBranch = function(root,x,y){
if (root !== null) {
var node = this.getIM(root);
if (x >= node.x && y >= node.y && x < node.x + node.width && y < node.y + node.height + this.gapy) {
return root;
}
if (this.$isOpen(root)) {
for(var i = 0;i < root.kids.length; i++) {
var res = this.getItemAtInBranch(root.kids[i], x, y);
if (res !== null) {
return res;
}
}
}
}
return null;
};
this.getIconView = function (i){
return i.kids.length > 0 ? (this.getIM(i).isOpen ? this.views.expandedSign
: this.views.collapsedSign)
: this.views.leafSign;
};
this.getIconSize = function (i) {
return i.kids.length > 0 ? (this.getIM(i).isOpen ? this.viewSizes.expandedSign
: this.viewSizes.collapsedSign)
: this.viewSizes.leafSign;
};
/**
* Get icon element bounds for the given tree model item.
* @param {zebkit.data.Item} root an tree model item
* @return {Object} a structure that keeps an item icon location
* and size:
*
* {
* x: {Integer},
* y: {Integer},
* width: {Integer},
* height: {Integer}
* }
*
* @method getToggleBounds
* @protected
*/
this.getIconBounds = function(root) {
var node = this.getIM(root),
id = this.getIconSize(root),
td = this.getToggleSize(root);
return { x:node.x + td.width + (td.width > 0 ? this.gapx : 0),
y:node.y + Math.floor((node.height - id.height) / 2),
width:id.width, height:id.height };
};
this.getToggleSize = function(i) {
return this.$isOpen(i) ? this.viewSizes.expandedToggle
: this.viewSizes.collapsedToggle;
};
this.isOverVisibleArea = function (i) {
var node = this.getIM(i);
return node.y + node.height + this.scrollManager.getSY() < this.visibleArea.y;
};
this.findOpened = function(item) {
var parent = item.parent;
return (parent === null || this.$isOpen(parent)) ? item : this.findOpened(parent);
};
this.findNext = function(item) {
if (item !== null){
if (item.kids.length > 0 && this.$isOpen(item)){
return item.kids[0];
}
var parent = null;
while ((parent = item.parent) !== null){
var index = parent.kids.indexOf(item);
if (index + 1 < parent.kids.length) {
return parent.kids[index + 1];
}
item = parent;
}
}
return null;
};
this.findPrev = function (item){
if (item !== null) {
var parent = item.parent;
if (parent !== null) {
var index = parent.kids.indexOf(item);
return (index - 1 >= 0) ? this.findLast(parent.kids[index - 1]) : parent;
}
}
return null;
};
this.findLast = function (item){
return this.$isOpen(item) && item.kids.length > 0 ? this.findLast(item.kids[item.kids.length - 1])
: item;
};
this.prevVisible = function (item){
if (item === null || this.isOverVisibleArea(item)) {
return this.nextVisible(item);
}
var parent = null;
while((parent = item.parent) !== null){
for(var i = parent.kids.indexOf(item) - 1;i >= 0; i-- ){
var child = parent.kids[i];
if (this.isOverVisibleArea(child)) {
return this.nextVisible(child);
}
}
item = parent;
}
return item;
};
this.isVerVisible = function (item){
if (this.visibleArea === null) {
return false;
}
var node = this.getIM(item),
yy1 = node.y + this.scrollManager.getSY(),
yy2 = yy1 + node.height - 1,
by = this.visibleArea.y + this.visibleArea.height;
return ((this.visibleArea.y <= yy1 && yy1 < by) ||
(this.visibleArea.y <= yy2 && yy2 < by) ||
(this.visibleArea.y > yy1 && yy2 >= by) );
};
this.nextVisible = function(item){
if (item === null || this.isVerVisible(item) === true) {
return item;
}
var res = this.nextVisibleInBranch(item), parent = null;
if (res !== null) {
return res;
}
while ((parent = item.parent) !== null){
var count = parent.kids.length;
for(var i = parent.kids.indexOf(item) + 1;i < count; i++){
res = this.nextVisibleInBranch(parent.kids[i]);
if (res !== null) {
return res;
}
}
item = parent;
}
return null;
};
this.nextVisibleInBranch = function (item){
if (this.isVerVisible(item)) {
return item;
}
if (this.$isOpen(item)){
for(var i = 0;i < item.kids.length; i++){
var res = this.nextVisibleInBranch(item.kids[i]);
if (res !== null) {
return res;
}
}
}
return null;
};
this.paintSelectedItem = function(g, root, node, x, y) {
var v = this.hasFocus() ? this.views.focusOnSelect
: this.views.focusOffSelect;
if (v !== null && v !== undefined) {
v.paint(g, x, y, node.viewWidth, node.viewHeight, this);
}
};
this.paintTree = function (g,item){
this.paintBranch(g, item);
var parent = null;
while( (parent = item.parent) !== null){
this.paintChild(g, parent, parent.kids.indexOf(item) + 1);
item = parent;
}
};
this.paintBranch = function (g, root){
if (root === null) {
return false;
}
var node = this.getIM(root),
dx = this.scrollManager.getSX(),
dy = this.scrollManager.getSY();
if (zebkit.util.isIntersect(node.x + dx, node.y + dy,
node.width, node.height,
this.visibleArea.x, this.visibleArea.y,
this.visibleArea.width, this.visibleArea.height))
{
var toggle = this.getToggleBounds(root),
toggleView = this.getToggleView(root),
image = this.getIconBounds(root),
vx = image.x + image.width + this.gapx,
vy = node.y + Math.floor((node.height - node.viewHeight) / 2);
if (toggleView !== null) {
toggleView.paint(g, toggle.x, toggle.y, toggle.width, toggle.height, this);
}
if (image.width > 0) {
this.getIconView(root).paint(g, image.x, image.y,
image.width, image.height, this);
}
if (this.selected === root){
this.paintSelectedItem(g, root, node, vx, vy);
}
if (this.paintItem !== undefined) {
this.paintItem(g, root, node, vx, vy);
}
if (this.lnColor !== null){
g.setColor(this.lnColor);
var yy = toggle.y + Math.floor(toggle.height / 2) + 0.5;
g.beginPath();
g.moveTo(toggle.x + (toggleView === null ? Math.floor(toggle.width / 2)
: toggle.width - 1), yy);
g.lineTo(image.x, yy);
g.stroke();
}
} else {
if (node.y + dy > this.visibleArea.y + this.visibleArea.height ||
node.x + dx > this.visibleArea.x + this.visibleArea.width )
{
return false;
}
}
return this.paintChild(g, root, 0);
};
this.$y = function (item, isStart){
var node = this.getIM(item),
th = this.getToggleSize(item).height,
ty = node.y + Math.floor((node.height - th) / 2),
dy = this.scrollManager.getSY(),
y = (item.kids.length > 0) ? (isStart ? ty + th : ty - 1)
: ty + Math.floor(th / 2);
return (y + dy < 0) ? -dy - 1
: ((y + dy > this.height) ? this.height - dy : y);
};
/**
* Paint children items of the given root tree item.
* @param {CanvasRenderingContext2D} g a graphical context
* @param {zebkit.data.Item} root a root tree item
* @param {Integer} index an index
* @return {Boolean}
* @protected
* @method paintChild
*/
this.paintChild = function (g, root, index){
var b = this.$isOpen(root);
if (root === this.firstVisible && this.lnColor !== null) {
g.setColor(this.lnColor);
var xx = this.getIM(root).x + Math.floor((b ? this.viewSizes.expandedToggle.width
: this.viewSizes.collapsedToggle.width) / 2);
g.beginPath();
g.moveTo(xx + 0.5, this.getTop());
g.lineTo(xx + 0.5, this.$y(root, false));
g.stroke();
}
if (b === true && root.kids.length > 0){
var firstChild = root.kids.length > 0 ?root.kids[0] : null;
if (firstChild === null) {
return true;
}
var x = this.getIM(firstChild).x + Math.floor((this.$isOpen(firstChild) ? this.viewSizes.expandedToggle.width
: this.viewSizes.collapsedToggle.width) / 2),
count = root.kids.length;
if (index < count) {
var node = this.getIM(root),
y = (index > 0) ? this.$y(root.kids[index - 1], true)
: node.y + Math.floor((node.height + this.getIconSize(root).height) / 2);
for(var i = index;i < count; i++ ) {
var child = root.kids[i];
if (this.lnColor !== null){
g.setColor(this.lnColor);
g.beginPath();
g.moveTo(x + 0.5, y);
g.lineTo(x + 0.5, this.$y(child, false));
g.stroke();
y = this.$y(child, true);
}
if (this.paintBranch(g, child) === false){
if (this.lnColor !== null && i + 1 !== count){
g.setColor(this.lnColor);
g.beginPath();
g.moveTo(x + 0.5, y);
g.lineTo(x + 0.5, this.height - this.scrollManager.getSY());
g.stroke();
}
return false;
}
}
}
}
return true;
};
this.nextPage = function (item,dir){
var sum = 0, prev = item;
while (item !== null && sum < this.visibleArea.height){
sum += (this.getIM(item).height + this.gapy);
prev = item;
item = dir < 0 ? this.findPrev(item) : this.findNext(item);
}
return prev;
};
this.paint = function(g){
if (this.model !== null){
this.vVisibility();
if (this.firstVisible !== null){
var sx = this.scrollManager.getSX(), sy = this.scrollManager.getSY();
try {
g.translate(sx, sy);
this.paintTree(g, this.firstVisible);
g.translate(-sx, -sy);
} catch(e) {
g.translate(-sx, -sy);
throw e;
}
}
}
};
/**
* Select the given item.
* @param {zebkit.data.Item} item an item to be selected. Use null value to clear
* any selection
* @method select
*/
this.select = function(item){
if (this.isSelectable === true && this.selected !== item){
var old = this.selected,
m = null;
this.selected = item;
if (this.selected !== null) {
this.makeVisible(this.selected);
}
this.fire("selected", [ this, old ]);
if (old !== null && this.isVerVisible(old)) {
m = this.getItemMetrics(old);
this.repaint(m.x + this.scrollManager.getSX(),
m.y + this.scrollManager.getSY(),
m.width, m.height);
}
if (this.selected !== null && this.isVerVisible(this.selected)) {
m = this.getItemMetrics(this.selected);
this.repaint(m.x + this.scrollManager.getSX(),
m.y + this.scrollManager.getSY(),
m.width, m.height);
}
}
};
/**
* Make the given tree item visible. Tree component rendered content can takes more space than
* the UI component size is. In this case the content can be scrolled to make visible required
* tree item.
* @param {zebkit.data.Item} item an item to be visible
* @method makeVisible
*/
this.makeVisible = function(item){
this.validate();
var r = this.getItemBounds(item);
this.scrollManager.makeVisible(r.x, r.y, r.width, r.height);
};
/**
* Toggle off or on recursively all items of the given item
* @param {zebkit.data.Item} root a starting item to toggle
* @param {Boolean} b true if all items have to be in opened
* state and false otherwise
* @method toggleAll
* @chainable
*/
this.toggleAll = function (root,b){
if (root.kids.length > 0){
if (this.getItemMetrics(root).isOpen !== b) {
this.toggle(root);
}
for(var i = 0; i < root.kids.length; i++ ){
this.toggleAll(root.kids[i], b);
}
}
return this;
};
/**
* Toggle the given tree item
* @param {zebkit.data.Item} item an item to be toggled
* @method toggle
* @chainable
*/
this.toggle = function(item){
if (item.kids.length > 0){
this.validate();
var node = this.getIM(item);
node.isOpen = (node.isOpen ? false : true);
this.invalidate();
this.fire("toggled", [this, item]);
if (!node.isOpen && this.selected !== null){
var parent = this.selected;
do {
parent = parent.parent;
} while (parent !== item && parent !== null);
if (parent === item) {
this.select(item);
}
}
this.repaint();
}
return this;
};
this.itemInserted = function (model, item){
this.vrp();
};
this.itemRemoved = function (model,item){
if (item === this.firstVisible) {
this.firstVisible = null;
}
if (item === this.selected) {
this.select(null);
}
delete this.nodes[item];
this.vrp();
};
this.itemModified = function (model, item, prevValue){
var node = this.getIM(item);
// invalidate an item metrics
if (node !== null) {
node.viewWidth = -1;
}
this.vrp();
};
this.calcPreferredSize = function(target) {
return this.model === null ? { width:0, height:0 }
: { width:this.maxw, height:this.maxh };
};
/**
* Say if items of the tree component should be selectable
* @param {Boolean} b true is tree component items can be selected
* @method setSelectable
*/
this.setSelectable = function(b){
if (this.isSelectable !== b){
if (b === false && this.selected !== null) {
this.select(null);
}
this.isSelectable = b;
this.repaint();
}
return this;
};
/**
* Set tree component connector lines color
* @param {String} c a color
* @method setLineColor
* @chainable
*/
this.setLineColor = function (c){
this.lnColor = c;
this.repaint();
return this;
};
/**
* Set the given horizontal gaps between tree node graphical elements:
* toggle, icon, item view
* @param {Integer} gx horizontal gap
* @param {Integer} gy vertical gap
* @method setGaps
* @chainable
*/
this.setGaps = function(gx, gy){
if (gx !== this.gapx || gy !== this.gapy){
this.gapx = gx;
this.gapy = gy;
this.vrp();
}
return this;
};
/**
* Set the given tree model to be visualized with the UI component.
* @param {zebkit.data.TreeModel|Object} d a tree model
* @method setModel
* @chainable
*/
this.setModel = function(d){
if (this.model !== d) {
if (zebkit.instanceOf(d, zebkit.data.TreeModel) === false) {
d = new zebkit.data.TreeModel(d);
}
this.select(null);
if (this.model !== null) {
this.model.off(this);
}
this.model = d;
if (this.model !== null) {
this.model.on(this);
}
this.firstVisible = null;
delete this.nodes;
this.nodes = {};
this.vrp();
}
return this;
};
},
function focused(){
this.$super();
if (this.selected !== null) {
var m = this.getItemMetrics(this.selected);
this.repaint(m.x + this.scrollManager.getSX(),
m.y + this.scrollManager.getSY(), m.width, m.height);
}
},
/**
* Set the number of views to customize rendering of different visual elements of the tree
* UI component. The following decorative elements can be customized:
*
* - **"collapsedSign"** - closed tree item icon view
* - **"expandedSign"** - opened tree item icon view
* - **"leafSign"** - leaf tree item icon view
* - **"expandedToggle"** - toggle on view
* - **"collapsedToggle"** - toggle off view
* - **"focusOffSelect"** - a view to express an item selection when tree component doesn't hold focus
* - **"focusOnSelect"** - a view to express an item selection when tree component holds focus
*
* For instance:
// build tree UI component
var tree = new zebkit.ui.tree.Tree({
value: "Root",
kids: [
"Item 1",
"Item 2"
]
});
// set " [x] " text render for toggle on and
// " [o] " text render for toggle off tree elements
tree.setViews({
"expandedToggle" : new zebkit.draw.TextRender(" [x] "),
"collapsedToggle": new zebkit.draw.TextRender(" [o] ")
});
* @param {Object} v dictionary of tree component decorative elements views
* @method setViews
* @chainable
*/
function setViews(v) {
// setting to 0 prevents exception when on/off view is not defined
this.viewSizes.expandedToggle = { width: 0, height : 0};
this.viewSizes.collapsedToggle = { width: 0, height : 0};
this.viewSizes.expandedSign = { width: 0, height : 0};
this.viewSizes.collapsedSign = { width: 0, height : 0};
this.viewSizes.leafSign = { width: 0, height : 0};
for(var k in v) {
this.views[k] = zebkit.draw.$view(v[k]);
if (this.viewSizes.hasOwnProperty(k) && this.views[k]) {
this.viewSizes[k] = this.views[k].getPreferredSize();
}
}
this.vrp();
return this;
},
function invalidate(){
if (this.isValid === true){
this._isVal = false;
}
this.$super();
}
]).events("toggled", "selected", "editingStarted", "editingStopped");
var ui = pkg.cd("..");
/**
* Tree UI component that visualizes a tree data model. The model itself can be passed as JavaScript
* structure or as a instance of zebkit.data.TreeModel. Internally tree component keeps the model always
* as zebkit.data.TreeModel class instance:
var tree = new zebkit.ui.tree.Tree({
value: "Root",
kids : [ "Item 1", "Item 2"]
});
* or
var model = new zebkit.data.TreeModel("Root");
model.add(model.root, "Item 1");
model.add(model.root, "Item 2");
var tree = new zebkit.ui.tree.Tree(model);
* Tree model rendering is fully customizable by defining an own views provider. Default views
* provider renders tree model item as text. The tree node can be made editable by defining an
* editor provider. By default tree modes are not editable.
* @class zebkit.ui.tree.Tree
* @constructor
* @extends zebkit.ui.tree.BaseTree
* @param {Object|zebkit.data.TreeModel} [model] a tree data model passed as JavaScript
* structure or as an instance
* @param {Boolean} [b] the tree component items toggle state. true to have all items
* in opened state.
*/
pkg.Tree = Class(pkg.BaseTree, [
function (d, b){
if (arguments.length < 2) {
b = true;
}
this.setViewProvider(new pkg.DefViews());
this.$super(d, b);
},
function $prototype() {
this.itemGapY = 2;
this.itemGapX = 4;
/**
* A tree model editor provider
* @readOnly
* @attribute editors
* @default null
* @type {zebkit.ui.tree.DefEditors}
*/
this.editors = null;
/**
* A tree model items view provider
* @readOnly
* @attribute provider
* @default an instance of zebkit.ui.tree.DefsViews
* @type {zebkit.ui.tree.DefsViews}
*/
this.provider = this.editedItem = this.pressedItem = null;
this.setFont = function(f) {
this.provider.setFont(f);
this.vrp();
return this;
};
this.childKeyPressed = function(e){
if (e.code === "Escape") {
this.stopEditing(false);
} else {
if (e.code === "Enter" &&
((zebkit.instanceOf(e.source, ui.TextField) === false) ||
(zebkit.instanceOf(e.source.view.target, zebkit.data.SingleLineTxt))))
{
this.stopEditing(true);
}
}
};
this.catchScrolled = function (psx, psy){
if (this.kids.length > 0) {
this.stopEditing(false);
}
if (this.firstVisible === null) {
this.firstVisible = this.model.root;
}
this.firstVisible = (this.y < psy) ? this.nextVisible(this.firstVisible)
: this.prevVisible(this.firstVisible);
this.repaint();
};
this.laidout = function() {
this.vVisibility();
};
this.getItemPreferredSize = function(root) {
var ps = this.provider.getView(this, root).getPreferredSize();
ps.width += this.itemGapX * 2;
ps.height += this.itemGapY * 2;
return ps;
};
this.paintItem = function(g, root, node, x, y) {
if (root !== this.editedItem){
var v = this.provider.getView(this, root);
v.paint(g, x + this.itemGapX, y + this.itemGapY,
node.viewWidth, node.viewHeight, this);
}
};
/**
* Initiate the given item editing if the specified event matches condition
* @param {zebkit.data.Item} item an item to be edited
* @param {zebkit.Event} e an even that may trigger the item editing
* @return {Boolean} return true if an item editing process has been started,
* false otherwise
* @method se
* @private
*/
this.se = function (item, e){
if (item !== null){
this.stopEditing(true);
if (this.editors !== null && this.editors.shouldStartEdit(item, e)) {
this.startEditing(item);
return true;
}
}
return false;
};
this.pointerClicked = function(e){
if (this.se(this.pressedItem, e)) {
this.pressedItem = null;
}
};
this.pointerDoubleClicked = function(e) {
if (this.se(this.pressedItem, e)) {
this.pressedItem = null;
} else {
if (this.selected !== null &&
this.getItemAt(this.firstVisible, e.x, e.y) === this.selected)
{
this.toggle(this.selected);
}
}
};
this.pointerReleased = function(e){
if (this.se(this.pressedItem, e)) {
this.pressedItem = null;
}
};
this.keyTyped = function(e){
if (this.selected !== null){
switch(e.key) {
case '+': if (this.isOpen(this.selected) === false) {
this.toggle(this.selected);
} break;
case '-': if (this.isOpen(this.selected)) {
this.toggle(this.selected);
} break;
}
}
};
this.keyPressed = function(e){
var newSelection = null;
switch(e.code) {
case "ArrowDown" :
case "ArrowRight": newSelection = this.findNext(this.selected); break;
case "ArrowUp" :
case "ArrowLeft" : newSelection = this.findPrev(this.selected); break;
case "Home" :
if (e.ctrlKey) {
this.select(this.model.root);
} break;
case "End" :
if (e.ctrlKey) {
this.select(this.findLast(this.model.root));
} break;
case "PageDown" :
if (this.selected !== null) {
this.select(this.nextPage(this.selected, 1));
} break;
case "PageUp" :
if (this.selected !== null) {
this.select(this.nextPage(this.selected, -1));
} break;
//!!!!case "Enter": if(this.selected !== null) this.toggle(this.selected);break;
}
if (newSelection !== null) {
this.select(newSelection);
}
this.se(this.selected, e);
};
/**
* Start editing the given if an editor for the item has been defined.
* @param {zebkit.data.Item} item an item whose content has to be edited
* @method startEditing
* @protected
*/
this.startEditing = function (item){
this.stopEditing(true);
if (this.editors !== null){
var editor = this.editors.getEditor(this, item);
if (editor !== null) {
this.editedItem = item;
var b = this.getItemBounds(this.editedItem),
ps = editor.getPreferredSize();
editor.setBounds(b.x + this.scrollManager.getSX() + this.itemGapX,
b.y - Math.floor((ps.height - b.height + 2 * this.itemGapY) / 2) +
this.scrollManager.getSY() + this.itemGapY,
ps.width, ps.height);
this.add(editor);
editor.requestFocus();
this.fire("editingStarted", [this, item, editor]);
}
}
};
/**
* Stop editing currently edited tree item and apply or discard the result of the
* editing to tree data model.
* @param {Boolean} true if the editing result has to be applied to tree data model
* @method stopEditing
* @protected
*/
this.stopEditing = function(applyData){
if (this.editors !== null && this.editedItem !== null) {
var item = this.editedItem,
oldValue = item.value,
editor = this.kids[0];
try {
if (applyData) {
this.model.setValue(this.editedItem,
this.editors.fetchEditedValue(this.editedItem, this.kids[0]));
}
} finally {
this.editedItem = null;
this.removeAt(0);
this.requestFocus();
this.fire("editingStopped", [this, item, oldValue, editor, applyData]);
}
}
};
},
function toggle(item) {
this.stopEditing(false);
this.$super(item);
return this;
},
function itemInserted(target,item){
this.stopEditing(false);
this.$super(target,item);
},
function itemRemoved(target,item){
this.stopEditing(false);
this.$super(target,item);
},
/**
* Set the given editor provider. The editor provider is a class that is used to decide which UI
* component has to be used as an item editor, how the editing should be triggered and how the
* edited value has to be fetched from an UI editor.
* @param {zebkit.ui.tree.DefEditors} p an editor provider
* @method setEditorProvider
*/
function setEditorProvider(p){
if (p != this.editors){
this.stopEditing(false);
this.editors = p;
}
return this;
},
/**
* Set tree component items view provider. Provider says how tree model items
* have to be visualized.
* @param {zebkit.ui.tree.DefViews} p a view provider
* @method setViewProvider
* @chainable
*/
function setViewProvider(p){
if (this.provider != p) {
this.stopEditing(false);
this.provider = p;
delete this.nodes;
this.nodes = {};
this.vrp();
}
return this;
},
/**
* Set the given tree model to be visualized with the UI component.
* @param {zebkit.data.TreeModel|Object} d a tree model
* @method setModel
* @chainable
*/
function setModel(d){
this.stopEditing(false);
this.$super(d);
return this;
},
function paintSelectedItem(g, root, node, x, y) {
if (root !== this.editedItem) {
this.$super(g, root, node, x, y);
}
},
function itemPressed(root, e) {
this.$super(root, e);
if (this.se(root, e) === false) {
this.pressedItem = root;
}
},
function pointerPressed(e){
this.pressedItem = null;
this.stopEditing(true);
this.$super(e);
}
]);
var ui = pkg.cd("..");
/**
* Component tree component that expects other UI components to be a tree model values.
* In general the implementation lays out passed via tree model UI components as tree
* component nodes. For instance:
var tree = new zebkit.ui.tree.Tree({
value: new zebkit.ui.Label("Label root item"),
kids : [
new zebkit.ui.Checkbox("Checkbox Item"),
new zebkit.ui.Button("Button item"),
new zebkit.ui.Combo(["Combo item 1", "Combo item 2"])
]
});
* But to prevent unexpected navigation it is better to use number of predefined
* with component tree UI components:
*
* - zebkit.ui.tree.CompTree.Label
* - zebkit.ui.tree.CompTree.Checkbox
* - zebkit.ui.tree.CompTree.Combo
*
* You can describe tree model keeping in mind special notation
var tree = new zebkit.ui.tree.Tree({
value: "Label root item", // zebkit.ui.tree.CompTree.Label
kids : [
"[ ] Checkbox Item 1", // unchecked zebkit.ui.tree.CompTree.Checkbox
"[x] Checkbox Item 2", // checked zebkit.ui.tree.CompTree.Checkbox
["Combo item 1", "Combo item 2"] // zebkit.ui.tree.CompTree.Combo
]
});
*
* @class zebkit.ui.tree.CompTree
* @constructor
* @extends zebkit.ui.tree.BaseTree
* @param {Object|zebkit.data.TreeModel} [model] a tree data model passed as JavaScript
* structure or as an instance
* @param {Boolean} [b] the tree component items toggle state. true to have all items
* in opened state.
*/
pkg.CompTree = Class(pkg.BaseTree, [
function $clazz() {
this.Label = Class(ui.Label, [
function $prototype() {
this.canHaveFocus = true;
}
]);
this.Checkbox = Class(ui.Checkbox, []);
this.Combo = Class(ui.Combo, [
function keyPressed(e) {
if (e.code !== "ArrowUp" && e.code !== "ArrowDown") {
this.$super(e);
}
}
]);
this.TextField = Class(ui.TextField, [
// let's skip parent invalidation
function invalidate() {
this.isValid = false;
this.cachedWidth = -1;
}
]);
this.createModel = function(item, root, tree) {
var mi = new zebkit.data.Item();
if (item.value !== undefined) {
mi.value = item.value !== null ? item.value : "";
} else {
mi.value = item;
}
mi.value = ui.$component(mi.value, tree);
mi.parent = root;
if (item.kids !== undefined && item.kids.length > 0 && zebkit.instanceOf(item, ui.Panel) === false) {
for (var i = 0; i < item.kids.length; i++) {
mi.kids[i] = this.createModel(item.kids[i], mi, tree);
}
}
return mi;
};
},
function $prototype() {
this.$blockCIE = false;
this.canHaveFocus = false;
this.getItemPreferredSize = function(root) {
return root.value.getPreferredSize();
};
this.childKeyTyped = function(e) {
if (this.selected !== null){
switch(e.key) {
case '+': if (this.isOpen(this.selected) === false) {
this.toggle(this.selected);
} break;
case '-': if (this.isOpen(this.selected)) {
this.toggle(this.selected);
} break;
}
}
};
this.setFont = function(f) {
this.font = zebkit.isString(f) ? new zebkit.Font(f) : f;
return this;
};
this.childKeyPressed = function(e) {
if (this.isSelectable === true) {
var newSelection = null;
if (e.code === "ArrowDown") {
newSelection = this.findNext(this.selected);
} else if (e.code === "ArrowUp") {
newSelection = this.findPrev(this.selected);
}
if (newSelection !== null) {
this.select(newSelection);
}
}
};
this.childPointerPressed = this.childFocusGained = function(e) {
if (this.isSelectable === true && this.$blockCIE !== true) {
this.$blockCIE = true;
try {
var item = zebkit.data.TreeModel.findOne(this.model.root,
zebkit.layout.getDirectChild(this,
e.source));
if (item !== null) {
this.select(item);
}
} finally {
this.$blockCIE = false;
}
}
};
this.childFocusLost = function(e) {
if (this.isSelectable === true) {
this.select(null);
}
};
this.catchScrolled = function(psx, psy){
this.vrp();
};
this.doLayout = function() {
this.vVisibility();
// hide all components
for(var i = 0; i < this.kids.length; i++) {
this.kids[i].setVisible(false);
}
if (this.firstVisible !== null) {
var $this = this,
started = 0;
this.model.iterate(this.model.root, function(item) {
var node = $this.nodes[item]; // slightly improve performance
// (instead of calling $this.getIM(...))
if (started === 0 && item === $this.firstVisible) {
started = 1;
}
if (started === 1) {
var sy = $this.scrollManager.getSY();
if (node.y + sy < $this.height) {
var image = $this.getIconBounds(item),
x = image.x + image.width +
(image.width > 0 || $this.getToggleSize(item).width > 0 ? $this.gapx : 0) +
$this.scrollManager.getSX(),
y = node.y + Math.floor((node.height - node.viewHeight) / 2) + sy;
item.value.setVisible(true);
item.value.setLocation(x, y);
item.value.width = node.viewWidth;
item.value.height = node.viewHeight;
} else {
started = 2;
}
}
return (started === 2) ? 2 : (node.isOpen === false ? 1 : 0);
});
}
};
this.itemInserted = function(target, item) {
this.add(item.value);
};
},
function itemRemoved(target,item){
this.$super(target,item);
this.remove(item.value);
},
function setModel(model) {
var old = this.model;
if (model !== null && zebkit.instanceOf(model, zebkit.data.TreeModel) === false) {
model = new zebkit.data.TreeModel(this.clazz.createModel(model, null, this));
}
this.$super(model);
if (old !== this.model) {
this.removeAll();
if (this.model !== null) {
var $this = this;
this.model.iterate(this.model.root, function(item) {
$this.add(item.value);
});
}
}
return this;
},
function recalc() {
// track with the flag a node metrics has been updated
this.$isMetricUpdated = false;
this.$super();
// if a node size has been changed we have to force calling
// repaint method for the whole tree component to render
// tree lines properly
if (this.$isMetricUpdated === true) {
this.repaint();
}
},
function recalc_(x,y,parent,root,isVis) {
// in a case of component tree node view size has to be synced with
// component
var node = this.getIM(root);
if (isVis === true) {
var viewSize = this.getItemPreferredSize(root);
if (this.$isMetricUpdated === false && (node.viewWidth !== viewSize.width ||
node.viewHeight !== viewSize.height ))
{
this.$isMetricUpdated = true;
}
node.viewWidth = viewSize.width;
node.viewHeight = viewSize.height;
}
return this.$super(x,y,parent,root,isVis);
},
function select(item) {
if (this.isSelectable === true && item !== this.selected) {
var old = this.selected;
if (old !== null && old.value.hasFocus()) {
ui.focusManager.requestFocus(null);
}
this.$super(item);
if (item !== null) {
item.value.requestFocus();
}
}
},
function makeVisible(item) {
item.value.setVisible(true);
this.$super(item);
}
]);
},true);
zebkit.package("ui.grid", function(pkg, Class) {
'use strict';
var ui = pkg.cd("..");
// ---------------------------------------------------
// | x | col0 width | x | col2 width | x |
// . .
// Line width
// -->. .<--
/**
* The package contains number of classes and interfaces to implement
* UI Grid component. The grid allows developers to visualize matrix
* model, customize the model data editing and rendering.
*
* // create grid that contains 3 rows and four columns
* var grid = new zebkit.ui.grid.Grid([
* [ "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" ],
* [ "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" ],
* [ "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" ].
* ]);
*
* // add grid top caption
* grid.add("top", new zebkit.ui.grid.GridCaption([
* "Title 1",
* "Title 2",
* "Title 3",
* "Title 5",
* "Title 6"
* ]));
*
* @class zebkit.ui.grid
* @access package
*/
/**
* Structure to keep grid cells visibility.
* @constructor
* @class zebkit.ui.grid.CellsVisibility
*/
pkg.CellsVisibility = function() {
this.hasVisibleCells = function(){
return this.fr !== null && this.fc !== null &&
this.lr !== null && this.lc !== null ;
};
/**
* First visible row.
* @attribute fr
* @type {Integer}
* @default null
*/
/**
* First visible column.
* @attribute fc
* @type {Integer}
* @default null
*/
/**
* Last visible row.
* @attribute lr
* @type {Integer}
* @default null
*/
/**
* Last visible column.
* @attribute lc
* @type {Integer}
* @default null
*/
// first visible row (row and y), first visible
// col, last visible col and row
this.fr = this.fc = this.lr = this.lc = null;
// TODO: replace array with human readable variables
// this.firstCol = -1
// this.firstRow = -1
// this.lastCol = -1
// this.lastRow = -1
// this.firstColX = 0
// this.firstRowY = 0
// this.lastColX = 0
// this.lastRowY = 0
};
/**
* Interface that describes a grid component metrics
* @class zebkit.ui.grid.Metrics
* @interface zebkit.ui.grid.Metrics
*/
pkg.Metrics = zebkit.Interface([
"abstract",
/**
* Get a structure that describes a grid component
* columns and rows visibility
* @return {zebkit.ui.grid.CellsVisibility} a grid cells visibility
* @method getCellsVisibility
*/
function getCellsVisibility() {},
/**
* Get the given column width of a grid component
* @param {Integer} col a column index
* @method getColWidth
* @return {Integer} a column width
*/
function getColWidth(col) {},
/**
* Get the given row height of a grid component
* @param {Integer} row a row index
* @method getRowHeight
* @return {Integer} a row height
*/
function getRowHeight(row) {},
/**
* Get the given column preferred width of a grid component
* @param {Integer} col a column index
* @method getPSColWidth
* @return {Integer} a column preferred width
*/
function getPSColWidth(col) {},
/**
* Get the given row preferred height of a grid component
* @param {Integer} row a row index
* @method getPSRowHeight
* @return {Integer} a row preferred height
*/
function getPSRowHeight(row) {},
/**
* Set the given row height of a grid component
* @param {Integer} row a row index
* @param {Integer} height a row height
* @method setRowHeight
*/
function setRowHeight(row, height) {},
/**
* Set the given column width of a grid component
* @param {Integer} col a column index
* @param {Integer} width a column width
* @method setColWidth
*/
function setColWidth(col, width) {},
/**
* Get number of rows in a grid component
* @return {Integer} a number of rows
* @method getGridRows
*/
function getGridRows() {},
/**
* Get number of columns in a grid component
* @return {Integer} a number of columns
* @method getGridCols
*/
function getGridCols() {}
]);
/**
* Get a x origin of a grid component. Origin indicates how
* the grid component content has been scrolled
* @method getXOrigin
* @return {Integer} a x origin
*/
/**
* Get a y origin of a grid component. Origin indicates how
* the grid component content has been scrolled
* @method getYOrigin
* @return {Integer} a y origin
*/
/**
* Grid line size
* @attribute lineSize
* @type {Integer}
* @readOnly
*/
/**
* Indicate if a grid sizes its rows and cols basing on its preferred sizes
* @attribute isUsePsMetric
* @type {Boolean}
* @readOnly
*/
/**
* Default grid cell views provider. The class rules how a grid cell content,
* background has to be rendered and aligned. Developers can implement an own
* views providers and than setup it for a grid by calling "setViewProvider(...)"
* method.
* @param {zebkit.draw.Render} [render] a string render
* @class zebkit.ui.grid.DefViews
* @extends zebkit.draw.BaseViewProvider
* @constructor
*/
pkg.DefViews = Class(zebkit.draw.BaseViewProvider, [
function $prototype() {
/**
* Default cell background
* @attribute background
* @type {String|zebkit.draw.View}
* @default null
*/
this.background = null;
/**
* Get a renderer to draw the specified grid model value.
* @param {zebkit.ui.grid.Grid} target a target Grid component
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @param {Object} obj a model value for the given grid cell
* @return {zebkit.draw.View} an instance of view to be used to
* paint the given cell model value
* @method getView
*/
/**
* Get an horizontal alignment a content in the given grid cell
* has to be adjusted. The method is optional.
* @param {zebkit.ui.grid.Grid} target a target grid component
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @return {String} a horizontal alignment ("left", "center", "right")
* @method getXAlignment
*/
/**
* Get a vertical alignment a content in the given grid cell
* has to be adjusted. The method is optional.
* @param {zebkit.ui.grid.Grid} target a target grid component
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @return {String} a vertical alignment ("top", "center", "bottom")
* @method getYAlignment
*/
/**
* Get the given grid cell color
* @param {zebkit.ui.grid.Grid} target a target grid component
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @return {String} a cell color to be applied to the given grid cell
* @method getCellColor
*/
}
]);
/**
* Stripped rows interface to extend a grid view provider.
*
* var grid = new zebkit.ui.grid.Grid([ ... ]);
*
* // Make grid rows stripped with blue and green colors
* grid.provider.extend(zebkit.ui.grid.StrippedRows({
* oddView : "blue",
* evenView: "green"
* }));
*
*
* @class zebkit.ui.grid.StrippedRows
* @interface zebkit.ui.grid.StrippedRows
*/
pkg.StrippedRows = zebkit.Interface([
function $prototype() {
/**
* Odd rows view or color
* @attribute oddView
* @type {String|zebkit.draw.View}
*/
this.oddView = null;
/**
* Even rows view or color
* @attribute evenView
* @type {String|zebkit.draw.View}
*/
this.evenView = null;
/**
* Get a cell view.
* @param {zebkit.ui.grid.Grid} grid [description]
* @param {Integer} row a cell row
* @param {Integer} col a cell column
* @return {String|zebkit.draw.View} a color or view
* @method getCellColor
*/
this.getCellColor = function(grid, row, col) {
return row % 2 === 0 ? this.evenView
: this.oddView;
};
}
]);
/**
* Simple grid cells editors provider implementation. By default the editors provider
* uses a text field component or check box component as a cell content editor. Check
* box component is used if a cell data type is boolean, otherwise text filed is applied
* as the cell editor.
// grid with tree columns and three rows
// first and last column will be editable with text field component
// second column will be editable with check box component
var grid = new zebkit.ui.grid.Grid([
["Text Cell", true, "Text cell"],
["Text Cell", false, "Text cell"],
["Text Cell", true, "Text cell"]
]);
// make grid cell editable
grid.setEditorProvider(new zebkit.ui.grid.DefEditors());
* It is possible to customize a grid column editor by specifying setting "editors[col]" property
* value. You can define an UI component that has to be applied as an editor for the given column
* Also you can disable editing by setting appropriate column editor class to null:
// grid with tree columns and three rows
// first and last column will be editable with text field component
// second column will be editable with check box component
var grid = new zebkit.ui.grid.Grid([
["Text Cell", true, "Text cell"],
["Text Cell", false, "Text cell"],
["Text Cell", true, "Text cell"]
]);
// grid cell editors provider
var editorsProvider = new zebkit.ui.grid.DefEditors();
// disable the first column editing
editorsProvider.editors[0] = null;
// make grid cell editable
grid.setEditorProvider(editorsProvider);
* @constructor
* @class zebkit.ui.grid.DefEditors
*/
pkg.DefEditors = Class([
function() {
this.textEditor = new this.clazz.TextField("", 150);
this.boolEditor = new this.clazz.Checkbox(null);
this.selectorEditor = new this.clazz.Combo();
this.editors = {};
},
function $clazz() {
this.TextField = Class(ui.TextField, []);
this.Checkbox = Class(ui.Checkbox, []);
this.Combo = Class(ui.Combo, [
function padShown(src, b) {
if (b === false) {
this.parent.stopEditing(true);
this.setSize(0,0);
}
},
function resized(pw, ph) {
this.$super(pw, ph);
if (this.width > 0 && this.height > 0 && this.hasFocus()) {
this.showPad();
}
}
]);
},
function $prototype() {
/**
* Fetch an edited value from the given UI editor component.
* @param {zebkit.ui.grid.Grid} grid a target grid component
* @param {Integer} row a grid cell row that has been edited
* @param {Integer} col a grid cell column that has been edited
* @param {Object} data an original cell content
* @param {zebkit.ui.Panel} editor an editor that has been used to
* edit the given cell
* @return {Object} a value that can be applied as a new content of
* the edited cell content
* @method fetchEditedValue
*/
this.fetchEditedValue = function(grid, row, col, data, editor) {
return editor.getValue();
};
/**
* Get an editor UI component to be used for the given cell of the specified grid
* @param {zebkit.ui.grid.Grid} grid a grid whose cell is going to be edited
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @param {Object} v a grid cell model data
* @return {zebkit.ui.Panel} an editor UI component to be used to edit the given cell
* @method getEditor
*/
this.getEditor = function(grid, row, col, v) {
var editor = null;
if (this.editors.hasOwnProperty(col)) {
editor = this.editors[col];
if (editor !== null) {
editor.setValue(v);
}
return editor;
} else {
editor = zebkit.isBoolean(v) ? this.boolEditor
: this.textEditor;
editor.setValue(v);
editor.setPadding(0);
var ah = Math.floor((grid.getRowHeight(row) - editor.getPreferredSize().height)/2);
editor.setPadding(ah, grid.cellInsetsLeft, ah, grid.cellInsetsRight);
return editor;
}
};
/**
* Test if the specified input event has to trigger the given grid cell editing
* @param {zebkit.ui.grid.Grid} grid a grid
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @param {zebkit.Event} e an event to be evaluated
* @return {Boolean} true if the given input event triggers the given cell editing
* @method shouldStart
*/
this.shouldStart = function(grid, row, col, e){
return e.id === "pointerClicked";
};
/**
* Test if the specified input event has to canceling the given grid cell editing
* @param {zebkit.ui.grid.Grid} grid a grid
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @param {zebkit.Event} e an event to be evaluated
* @return {Boolean} true if the given input event triggers the given cell editing
* cancellation
* @method shouldCancel
*/
this.shouldCancel = function(grid,row,col,e){
return e.id === "keyPressed" && "Escape" === e.code;
};
/**
* Test if the specified input event has to trigger finishing the given grid cell editing
* @param {zebkit.ui.grid.Grid} grid [description]
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @param {zebkit.Event} e an event to be evaluated
* @return {Boolean} true if the given input event triggers finishing the given cell editing
* @method shouldFinish
*/
this.shouldFinish = function(grid,row,col,e){
return e.id === "keyPressed" && "Enter" === e.code;
};
}
]);
/**
* Grid caption base UI component class. This class has to be used
* as base to implement grid caption components
* @class zebkit.ui.grid.BaseCaption
* @extends zebkit.ui.Panel
* @uses zebkit.EventProducer
* @constructor
* @param {Array} [titles] a caption component titles
*/
/**
* Fire when a grid row selection state has been changed
*
* caption.on("captionResized", function(caption, rowcol, phw) {
* ...
* });
*
* @event captionResized
* @param {zebkit.ui.grid.BaseCaption} caption a caption
* @param {Integer} rowcol a row or column that has been resized
* @param {Integer} pwh a previous row or column size
*/
pkg.BaseCaption = Class(ui.Panel, [
function(titles) {
this.$super();
if (arguments.length > 0) {
for(var i = 0; i < titles.length; i++) {
this.setLabel(i, titles[i]);
}
}
},
function $prototype() {
this.selectedColRow = -1;
this.orient = this.metrics = this.pxy = null;
/**
* Minimal possible grid cell size
* @type {Integer}
* @default 10
* @attribute minSize
*/
this.minSize = 10;
/**
* Size of the active area where cells size can be changed by pointer dragging event
* @attribute activeAreaSize
* @type {Integer}
* @default 5
*/
this.activeAreaSize = 5;
/**
* Indicate if the grid cell size has to be adjusted according
* to the cell preferred size by pointer double click event.
* @attribute isAutoFit
* @default true
* @type {Boolean}
*/
/**
* Indicate if the grid cells are resize-able.
* to the cell preferred size by pointer double click event.
* @attribute isResizable
* @default true
* @type {Boolean}
*/
this.isAutoFit = this.isResizable = true;
this.getCursorType = function (target, x, y) {
return this.metrics !== null &&
this.selectedColRow >= 0 &&
this.isResizable &&
this.metrics.isUsePsMetric === false ? ((this.orient === "horizontal") ? ui.Cursor.W_RESIZE
: ui.Cursor.S_RESIZE)
: null;
};
/**
* Define pointer dragged events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragged
*/
this.pointerDragged = function(e){
if (this.pxy !== null) {
var b = (this.orient === "horizontal"),
rc = this.selectedColRow,
ns = (b ? this.metrics.getColWidth(rc) + e.x
: this.metrics.getRowHeight(rc) + e.y) - this.pxy;
this.captionResized(rc, ns);
if (ns > this.minSize) {
this.pxy = b ? e.x : e.y;
}
}
};
/**
* Define pointer drag started events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragStarted
*/
this.pointerDragStarted = function(e) {
if (this.metrics !== null &&
this.isResizable &&
this.metrics.isUsePsMetric === false)
{
this.calcRowColAt(e.x, e.y);
if (this.selectedColRow >= 0) {
this.pxy = (this.orient === "horizontal") ? e.x
: e.y;
}
}
};
/**
* Define pointer drag ended events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragEnded
*/
this.pointerDragEnded = function (e){
if (this.pxy !== null) {
this.pxy = null;
}
if (this.metrics !== null) {
this.calcRowColAt(e.x, e.y);
}
};
/**
* Define pointer moved events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerMoved
*/
this.pointerMoved = function(e) {
if (this.metrics !== null) {
this.calcRowColAt(e.x, e.y);
}
};
this.pointerExited = function(e) {
if (this.selectedColRow !== -1) {
this.selectedColRow = -1;
this.fire("captionResizeSelected", [this, this.selectedColRow] );
}
};
/**
* Define pointer clicked events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerClicked
*/
this.pointerDoubleClicked = function(e) {
if (this.pxy === null &&
this.metrics !== null &&
this.selectedColRow >= 0 &&
this.isAutoFit === true )
{
var size = this.getCaptionPS(this.selectedColRow);
if (this.orient === "horizontal") {
this.metrics.setColWidth (this.selectedColRow, size);
} else {
this.metrics.setRowHeight(this.selectedColRow, size);
}
this.captionResized(this.selectedColRow, size);
}
};
/**
* Get the given row or column caption preferred size
* @param {Integer} rowcol a row or column of a caption
* @return {Integer} a size of row or column caption
* @method getCaptionPS
*/
this.getCaptionPS = function(rowcol) {
return 0;
};
this.captionResized = function(rowcol, ns) {
if (ns > this.minSize) {
if (this.orient === "horizontal") {
var pw = this.metrics.getColWidth(rowcol);
this.metrics.setColWidth(rowcol, ns);
this.fire("captionResized", [this, rowcol, pw]);
} else {
var ph = this.metrics.getRowHeight(rowcol);
this.metrics.setRowHeight(rowcol, ns);
this.fire("captionResized", [this, rowcol, ph]);
}
}
};
this.calcRowColAt = function(x, y) {
var $this = this,
newSelected = this.getCaptionAt(x, y, function(m, xy, xxyy, wh, i) {
xxyy += (wh + Math.floor(m.lineSize / 2));
return (xy < xxyy + $this.activeAreaSize &&
xy > xxyy - $this.activeAreaSize );
});
if (newSelected !== this.selectedColRow) {
this.selectedColRow = newSelected;
this.fire("captionResizeSelected", [this, this.selectedColRow]);
}
};
/**
* Compute a column (for horizontal caption component) or row (for
* vertically aligned caption component) at the given location
* @param {Integer} x a x coordinate
* @param {Integer} y an y coordinate
* @param {Function} [f] an optional match function. The method can be passed
* if you need to detect a particular area of row or column. The method gets
* a grid metrics as the first argument, a x or y location to be detected,
* a row or column y or x coordinate, a row or column height or width and
* row or column index. The method has to return true if the given location
* is in.
* @return {Integer} a row or column
* @method calcRowColAt
*/
this.getCaptionAt = function(x,y,f) {
if (this.metrics !== null &&
x >= 0 &&
y >= 0 &&
x < this.width &&
y < this.height )
{
var m = this.metrics,
cv = m.getCellsVisibility(),
isHor = (this.orient === "horizontal");
if ((isHor && cv.fc !== null) ||
(isHor === false && cv.fr !== null))
{
var gap = m.lineSize,
xy = isHor ? x : y,
xxyy = isHor ? cv.fc[1] - this.x + m.getXOrigin()
: cv.fr[1] - this.y + m.getYOrigin();
for (var i = (isHor ? cv.fc[0] : cv.fr[0]);i <= (isHor ? cv.lc[0] : cv.lr[0]); i++) {
var wh = isHor ? m.getColWidth(i)
: m.getRowHeight(i);
if ((arguments.length > 2 && f(m, xy, xxyy, wh, i)) ||
(arguments.length < 3 && xy > xxyy && xy < xxyy + wh))
{
return i;
}
xxyy += wh + gap;
}
}
}
return -1;
};
/**
* Set the grid caption labels
* @param {Object} [labels]* labels
* @method setLabels
* @chainable
*/
this.setLabels = function() {
for (var i = 0; i < arguments.length; i++) {
this.setLabel(i, arguments[i]);
}
return this;
};
this.setLabel = function(i, lab) {
return this;
};
/**
* Implement the method to be aware when number of rows or columns in
* a grid model has been updated
* @param {zebkit.ui.grid.Grid} target a target grid
* @param {Integer} prevRows a previous number of rows
* @param {Integer} prevCols a previous number of columns
* @method matrixResized
*/
/**
* Implement the method to be aware when a grid model data has been
* re-ordered.
* @param {zebkit.ui.grid.Grid} target a target grid
* @param {Object} sortInfo an order information
* @method matrixSorted
*/
},
function setParent(p) {
this.$super(p);
this.metrics = this.orient = null;
if (p === null || zebkit.instanceOf(p, pkg.Metrics)) {
this.metrics = p;
if (this.constraints !== null) {
this.orient = (this.constraints === "top" ||
this.constraints === "bottom" ) ? "horizontal"
: "vertical";
}
}
}
]).events("captionResized", "captionResizeSelected");
var ui = pkg.cd("..");
/**
* Caption cell render. This class can be used to customize grid caption
* cells look and feel.
* @param {zebkit.draw.Render} a render to be used to draw grid caption cells.
* @constructor
* @class zebkit.ui.grid.CaptionViewProvider
* @extends zebkit.ui.grid.DefViews
*/
pkg.CaptionViewProvider = Class(pkg.DefViews, [
function $prototype() {
this.meta = null;
this.$getCellMeta = function(rowcol) {
if (this.meta === null) {
this.meta = {};
}
if (this.meta.hasOwnProperty(rowcol)) {
return this.meta[rowcol];
} else {
this.meta[rowcol] = {
ax : null,
ay : null,
bg : null
};
return this.meta[rowcol];
}
};
this.getXAlignment = function(target, rowcol) {
return this.meta === null || this.meta.hasOwnProperty(rowcol) === false ? null
: this.meta[rowcol].ax;
};
this.getYAlignment = function(target, rowcol) {
return this.meta === null || this.meta.hasOwnProperty(rowcol) === false ? null
: this.meta[rowcol].ay;
};
this.getCellBackground = function(target, rowcol) {
return this.meta === null || this.meta.hasOwnProperty(rowcol) === false ? null
: this.meta[rowcol].bg;
};
this.setLabelAlignments = function(rowcol, ax, ay) {
var m = this.$getCellMeta(rowcol);
if (m.ax !== ax || m.ay !== ay) {
m.ax = ax;
m.ay = ay;
return true;
} else {
return false;
}
};
this.setCellBackground = function(rowcol, bg) {
var m = this.$getCellMeta(rowcol);
if (m.bg !== bg) {
m.bg = zebkit.draw.$view(bg);
return true;
} else {
return false;
}
};
}
]);
/**
* Grid caption class that implements rendered caption.
* Rendered means all caption titles, border are painted
* as a number of views.
* @param {Array} [titles] a caption titles. Title can be a string or
* a zebkit.draw.View class instance
* @param {zebkit.draw.BaseTextRender} [render] a text render to be used
* to paint grid titles
* @constructor
* @class zebkit.ui.grid.GridCaption
* @extends zebkit.ui.grid.BaseCaption
*/
pkg.GridCaption = Class(pkg.BaseCaption, [
function(titles, render) {
this.titles = {};
this.setViewProvider(new pkg.CaptionViewProvider(render));
if (arguments.length === 0) {
this.$super();
} else {
this.$super(titles);
}
},
function $prototype() {
this.psW = this.psH = 0;
/**
* Grid caption view provider.
* @attribute provider
* @type {zebkit.ui.grid.CaptionViewProvider}
* @readOnly
*/
this.provider = null;
/**
* Default vertical cell view alignment.
* @attribute defYAlignment
* @type {String}
* @default "center"
*/
this.defYAlignment = "center";
/**
* Default horizontal cell view alignment.
* @attribute defYAlignment
* @type {String}
* @default "center"
*/
this.defXAlignment = "center";
/**
* Default cell background view.
* @attribute defCellBg
* @type {zebkit.draw.View}
* @default null
*/
this.defCellBg = null;
/**
* Set the given caption view provider.
* @param {zebkit.ui.grid.CaptionViewProvider} p a caption view provider.
* @method setViewProvider
* @chainable
*/
this.setViewProvider = function(p) {
if (p !== this.provider) {
this.provider = p;
this.vrp();
}
return this;
};
/**
* Get rendered caption cell object.
* @param {Ineteger} rowcol a row or column
* @return {Object} a rendered caption cell object
* @method getTitle
*/
this.getTitle = function(rowcol) {
return this.titles.hasOwnProperty(rowcol) ? this.titles[rowcol]
: null;
};
this.calcPreferredSize = function (l) {
return { width:this.psW, height:this.psH };
};
this.setFont = function(f) {
this.provider.setFont(f);
this.vrp();
return this;
};
this.setColor = function(c) {
this.provider.setColor(c);
this.repaint();
return this;
};
this.recalc = function(){
this.psW = this.psH = 0;
if (this.metrics !== null){
var m = this.metrics,
isHor = (this.orient === "horizontal"),
size = isHor ? m.getGridCols() : m.getGridRows();
for (var i = 0;i < size; i++) {
var v = this.provider.getView(this, i, this.getTitle(i));
if (v !== null) {
var ps = v.getPreferredSize();
if (isHor === true) {
if (ps.height > this.psH) {
this.psH = ps.height;
}
this.psW += ps.width;
} else {
if (ps.width > this.psW) {
this.psW = ps.width;
}
this.psH += ps.height;
}
}
}
if (this.psH === 0) {
this.psH = pkg.Grid.DEF_ROWHEIGHT;
}
if (this.psW === 0) {
this.psW = pkg.Grid.DEF_COLWIDTH;
}
if (this.lineColor !== null) {
if (isHor) {
this.psH += this.metrics.lineSize;
} else {
this.psW += this.metrics.lineSize;
}
}
}
};
/**
* Put the given title for the given caption cell.
* @param {Integer} rowcol a grid caption cell index
* @param {String|zebkit.draw.View|zebkit.ui.Panel} title a title of the given
* grid caption cell. Can be a string or zebkit.draw.View or zebkit.ui.Panel
* class instance
* @method setLabel
* @chainable
*/
this.setLabel = function(rowcol, value) {
if (value === null) {
if (this.titles.hasOwnProperty(rowcol)) {
delete this.titles[rowcol];
}
} else {
this.titles[rowcol] = value;
}
this.vrp();
return this;
};
/**
* Set the specified alignments of the given caption column or row.
* @param {Integer} rowcol a row or column depending on the caption orientation
* @param {String} xa a horizontal caption cell alignment. Use "left", "right" or
* "center" as the title alignment value.
* @param {String} ya a vertical caption cell alignment. Use "top", "bottom" or
* "center" as the title alignment value.
* @method setLabelAlignments
* @chainable
*/
this.setLabelAlignments = function(rowcol, xa, ya){
if (this.provider.setLabelAlignments(rowcol, xa, ya)) {
this.repaint();
}
return this;
};
/**
* Set the given caption cell background
* @param {Integer} rowcol a caption cell row or column
* @param {zebkit.draw.View|String} bg a color or view
* @method setCellBackground
* @chainable
*/
this.setCellBackground = function(rowcol, bg) {
if (this.provider.setCellBackground(rowcol, bg)) {
this.repaint();
}
return this;
};
/**
* Get cell caption preferred size.
* @param {Integer} rowcol row or col of the cell depending the caption
* orientation.
* @return {Integer} a preferred width or height of the cell
* @method getCaptionPS
* @protected
*/
this.getCaptionPS = function(rowcol) {
var v = this.provider.getView(this, rowcol, this.getTitle(rowcol));
return (v !== null) ? (this.orient === "horizontal" ? v.getPreferredSize().width
: v.getPreferredSize().height)
: 0;
};
this.paintOnTop = function(g) {
if (this.metrics !== null) {
var cv = this.metrics.getCellsVisibility();
if ((cv.fc !== null && cv.lc !== null && this.orient === "horizontal")||
(cv.fr !== null && cv.lr !== null && this.orient === "vertical" ) )
{
var isHor = (this.orient === "horizontal"),
gap = this.metrics.lineSize,
top = this.getTop(),
left = this.getLeft(),
bottom = this.getBottom(),
right = this.getRight(),
x = isHor ? cv.fc[1] - this.x + this.metrics.getXOrigin()// + gap
: left, //left,
y = isHor ? top //top + (this.lineColor !== null ? this.metrics.lineSize : 0)
: cv.fr[1] - this.y + this.metrics.getYOrigin(), // - gap,
size = isHor ? this.metrics.getGridCols()
: this.metrics.getGridRows();
// top
// >|<
// +=========|===========================
// || |
// || +====|============+ +========
// || || | || ||
// ||--------> left || ||
// || ||<-------------->|| ||
// || || ww || ||
// || || || ||
// >-------< lineSize || ||
// || || || ||
// x first
// visible
for(var i = (isHor ? cv.fc[0] : cv.fr[0]); i <= (isHor ? cv.lc[0] : cv.lr[0]); i++) {
var ww = isHor ? this.metrics.getColWidth(i)
: this.width - left - right,
hh = isHor ? this.height - top - bottom
: this.metrics.getRowHeight(i),
v = this.provider.getView(this, i, this.getTitle(i));
if (v !== null) {
var xa = this.provider.getXAlignment(this, i, v),
ya = this.provider.getYAlignment(this, i, v),
bg = this.provider.getCellBackground(this, i, v);
if (xa === null) {
xa = this.defXAlignment;
}
if (ya === null) {
ya = this.defYAlignment;
}
if (bg === null) {
bg = this.defCellBg;
}
var ps = v.getPreferredSize(),
vx = xa === "center" ? Math.floor((ww - ps.width)/2)
: (xa === "right" ? ww - ps.width - ((i === size - 1) ? right : 0)
: (i === 0 ? left: 0)),
vy = ya === "center" ? Math.floor((hh - ps.height)/2)
: (ya === "bottom" ? hh - ps.height - ((i === size - 1) ? bottom : 0)
: (i === 0 ? top: 0));
if (bg !== null) {
if (isHor) {
bg.paint(g, x, 0, ww + gap , this.height, this);
} else {
bg.paint(g, 0, y, this.width, hh + gap, this);
}
}
g.save();
if (isHor) {
g.clipRect(x, y, ww, hh);
try {
v.paint(g, x + vx, y + vy, ps.width, ps.height, this);
} catch(e) {
g.restore();
throw e;
}
} else {
g.clipRect(x, y, ww, hh);
try {
v.paint(g, x + vx, y + vy, ps.width, ps.height, this);
} catch(e) {
g.restore();
throw e;
}
}
g.restore();
}
if (isHor) {
x += ww + gap;
} else {
y += hh + gap;
}
}
}
}
};
}
]);
/**
* Predefined left vertical grid caption.
* @constructor
* @class zebkit.ui.grid.LeftGridCaption
* @extends zebkit.ui.grid.GridCaption
*/
pkg.LeftGridCaption = Class(pkg.GridCaption, [
function $prototype() {
this.constraints = "left";
}
]);
var ui = pkg.cd("..");
/**
* Grid caption class that implements component based caption.
* Component based caption uses other UI component as the
* caption titles.
* @param {Array} a caption titles. Title can be a string or
* a zebkit.ui.Panel class instance
* @constructor
* @class zebkit.ui.grid.CompGridCaption
* @extends zebkit.ui.grid.BaseCaption
*/
pkg.CompGridCaption = Class(pkg.BaseCaption, [
function(titles) {
if (arguments.length === 0) {
this.$super();
} else {
this.$super(titles);
}
this.setLayout(new this.clazz.Layout());
},
function $clazz() {
this.Layout = Class(zebkit.layout.Layout, [
function $prototype() {
this.doLayout = function (target) {
var m = target.metrics,
b = target.orient === "horizontal",
top = target.getTop(),
left = target.getLeft(),
wh = (b ? target.height - top - target.getBottom()
: target.width - left - target.getRight()),
xy = (b ? left + m.getXOrigin()
: top + m.getYOrigin()) + m.lineSize;
for (var i = 0; i < target.kids.length; i++) {
var kid = target.kids[i],
cwh = (b ? m.getColWidth(i)
: m.getRowHeight(i));
if (kid.isVisible === true) {
if (b) {
kid.setBounds(xy, top, cwh, wh);
} else {
kid.setBounds(left, xy, wh, cwh);
}
}
xy += (cwh + m.lineSize);
}
};
this.calcPreferredSize = function (target) {
return zebkit.layout.getMaxPreferredSize(target);
};
}
]);
this.Link = Class(ui.Link, []);
this.StatusPan = Class(ui.StatePan, []);
/**
* Title panel that is designed to be used as CompGridCaption UI component title element.
* The panel keeps a grid column or row title, a column or row sort indicator. Using the
* component you can have sortable grid columns.
* @constructor
* @param {String} a grid column or row title
* @class zebkit.ui.grid.CompGridCaption.TitlePan
*/
var clazz = this;
this.TitlePan = Class(ui.Panel, [
function(content) {
this.$super();
/**
* Image panel to keep grid caption icon
* @attribute icon
* @type {zebkit.ui.ImagePan}
* @readOnly
*/
this.icon = new ui.ImagePan(null);
/**
* Title content
* @attribute content
* @type {zebkit.ui.Panel}
* @readOnly
*/
this.content = zebkit.instanceOf(content, ui.Panel) ? content : new clazz.Link(content);
this.statusPan = new clazz.StatusPan();
this.statusPan.setVisible(this.isSortable);
this.add(this.icon);
this.add(this.content);
this.add(this.statusPan);
},
function $clazz() {
this.layout = new zebkit.layout.FlowLayout("center", "center", "horizontal", 8);
},
function $prototype() {
this.sortState = 0;
/**
* Indicates if the title panel has to initiate a column sorting
* @default false
* @attribute isSortable
* @readOnly
* @type {Boolean}
*/
this.isSortable = false;
},
function getGridCaption() {
var c = this.parent;
while(c !== null && zebkit.instanceOf(c, pkg.BaseCaption) === false) {
c = c.parent;
}
return c;
},
function matrixSorted(target, info) {
if (this.isSortable) {
var col = this.parent.indexOf(this);
if (info.col === col) {
this.sortState = info.name === 'descent' ? 1 : -1;
this.statusPan.setState(info.name);
} else {
this.sortState = 0;
this.statusPan.setState("*");
}
}
},
/**
* Set the caption icon
* @param {String|Image} path a path to an image or image object
* @method setIcon
* @chainable
*/
function setIcon(path) {
this.icon.setImage(path);
return this;
},
function matrixResized(target, prevRows, prevCols){
if (this.isSortable) {
this.sortState = 0;
this.statusPan.setState("*");
}
},
function fired(target) {
if (this.isSortable === true) {
var f = this.sortState === 1 ? zebkit.data.ascent
: zebkit.data.descent,
model = this.getGridCaption().metrics.model,
col = this.parent.indexOf(this);
model.sortCol(col, f);
}
},
function kidRemoved(index, kid, ctr) {
if (kid.isEventFired()) {
kid.off(this);
}
this.$super(index, kid, ctr);
},
function kidAdded(index, constr, kid) {
if (kid.isEventFired()) {
kid.on(this);
}
this.$super(index, constr, kid);
}
]);
},
/**
* @for zebkit.ui.grid.CompGridCaption
*/
function $prototype() {
this.catchInput = function(t) {
return t.isEventFired() === false;
};
this.scrolled = function() {
this.vrp();
};
/**
* Put the given title component for the given caption cell.
* @param {Integer} rowcol a grid caption cell index
* @param {String|zebkit.ui.Panel|zebkit.draw.View} title a title of the given grid caption cell.
* Can be a string or zebkit.draw.View or zebkit.ui.Panel class instance
* @method setLabel
* @chainable
*/
this.setLabel = function(rowcol, t) {
// add empty titles
for(var i = this.kids.length - 1; i >= 0 && i < rowcol; i++) {
this.add(new this.clazz.TitlePan(""));
}
if (zebkit.isString(t)) {
t = new this.clazz.TitlePan(t);
} else if (zebkit.instanceOf(t, zebkit.draw.View)) {
var p = new ui.ViewPan();
p.setView(t);
t = p;
}
if (rowcol < this.kids.length) {
this.setAt(rowcol, t);
} else {
this.add(t);
}
return this;
};
/**
* Set the given column sortable state
* @param {Integer} col a column
* @param {Boolean} b true if the column has to be sortable
* @method setSortable
* @chainable
*/
this.setSortable = function(col, b) {
var c = this.kids[col];
if (c.isSortable !== b) {
c.isSortable = b;
c.statusPan.setVisible(b);
}
return this;
};
this.matrixSorted = function(target, info) {
for(var i = 0; i < this.kids.length; i++) {
if (this.kids[i].matrixSorted) {
this.kids[i].matrixSorted(target, info);
}
}
};
this.matrixResized = function(target,prevRows,prevCols){
for(var i = 0; i < this.kids.length; i++) {
if (this.kids[i].matrixResized) {
this.kids[i].matrixResized(target,prevRows,prevCols);
}
}
};
this.getCaptionPS = function(rowcol) {
return rowcol < this.kids.length ? (this.orient === "horizontal" ? this.kids[rowcol].getPreferredSize().width
: this.kids[rowcol].getPreferredSize().height)
: 0;
};
},
function captionResized(rowcol, ns) {
this.$super(rowcol, ns);
this.vrp();
},
function setParent(p) {
if (this.parent !== null && this.parent.scrollManager !== undefined && this.parent.scrollManager !== null) {
this.parent.scrollManager.off(this);
}
if (p !== null && p.scrollManager !== undefined && p.scrollManager !== null) {
p.scrollManager.on(this);
}
this.$super(p);
},
function insert(i, constr, c) {
if (zebkit.isString(c)) {
c = new this.clazz.TitlePan(c);
}
this.$super(i,constr, c);
}
]);
/**
* Predefined left vertical component grid caption.
* @constructor
* @class zebkit.ui.grid.LeftCompGridCaption
* @extends zebkit.ui.grid.CompGridCaption
*/
pkg.LeftCompGridCaption = Class(pkg.CompGridCaption, [
function $prototype() {
this.constraints = "left";
}
]);
var ui = pkg.cd("..");
//
// -- Grid should be responsible for rendering caption lines
// -- Grid layouts horizontal caption taking in account:
// -- Top gap
// -- Line size
// -- The same with vertical caption
//
//
// ......................................... Grid .........................
// . grid.getTop() .
// . +------------------------------------------------------------+ .
// . | grid.lineSize | .
// . ....+-------------------------+...+----------------------+...GridCaption
// . . | | | | . .
// . . | Caption Title 1 | | Caption Title 2 | . .
// . . | | | | . .
// . ....+-------------------------+...+----------------------+.... .
// . | grid.lineSize | .
// . + +-------------------------+ +----------------------+ | .
// . | |\__ (fc.x, fc.y) | | | | .
// . | | ................... | | | | .
// . | | . cell view . | | | | .
// . | | . . | | | | .
// . | | ................... | | | | .
// . | | cell insets | | | | .
// . | +-------------------------+ +----------------------+ | .
// . | | .
// .<---| grid.getLeft() grid.getRight() |--->.
//
//
/**
* Class to that defines grid cell selection mode. This implementation
* allows users single grid cell selection.
* @constructor
* @param {zebkit.ui.grid.Grid} target a target grid the selection mode
* instance class belongs
* @class zebkit.ui.grid.CellSelectMode
*/
pkg.CellSelectMode = Class([
function(target) {
this.target = target;
},
function $prototype() {
/**
* Target grid cell selection mode belongs
* @attribute target
* @type {zebkit.ui.grid.Grid}
*/
this.target = null;
this.selectedRow = -1;
this.selectedCol = -1;
this.prevSelectedRow = -1;
this.prevSelectedCol = -1;
/**
* Callback method that is called every time the select mode is
* attached to the given target grid component
* @param {zebkit.ui.grid.Grid} target a target grid component
* @method install
*/
/**
* Callback method that is called every time the select mode is
* detached from the given target grid component
* @param {zebkit.ui.grid.Grid} target a target grid component
* @method uninstall
*/
/**
* Evaluates if the given cell is selected.
* @param {Integer} row a cell row
* @param {Integer} col a cell column
* @return {Boolean} true if the given cell is selected
* @method isSelected
*/
this.isSelected = function(row, col) {
return row >= 0 && row === this.selectedRow &&
col >= 0 && col === this.selectedCol;
};
/**
* Callback method that is called every time a grid position
* marker has been updated.
* @param {zebkit.util.Position} pos a position manager
* @param {Integer} prevOffset a previous position offset
* @param {Integer} prevLine a previous position line
* @param {Integer} prevCol a previous position column
* @method posChanged
*/
this.posChanged = function(pos, prevOffset, prevLine, prevCol) {
this.prevSelectedRow = prevLine;
this.prevSelectedCol = prevCol;
this.target.select(pos.currentLine, pos.currentCol, true);
};
/**
* Clear all selected cells
* @chainable
* @method clearSelect
*/
this.clearSelect = function() {
if (this.selectedRow >= 0 || this.selectedCol >= 0) {
var prevRow = this.selectedRow,
prevCol = this.selectedCol;
this.selectedCol = this.selectedRow = -1;
this.fireSelected(prevRow, prevCol, false);
}
return this;
};
/**
* Select or de-select the given grid cell
* @param {Integer} row a row of selected or de-selected cell
* @param {Integer} col a column of selected or de-selected cell
* @param {Boolean} b a selection status
* @chainable
* @method select
*/
this.select = function(row, col, b) {
if (arguments.length === 2) {
b = true;
}
if (this.isSelected(row, col) !== b) {
this.clearSelect();
if (b) {
this.selectedRow = row;
this.selectedCol = col;
this.fireSelected(row, col, b);
}
}
return this;
};
/**
* Fire selected or de-selected event.
* @param {Integer} row a selected or de-selected row
* @param {Integer} col a selected or de-selected column
* @param {Boolean} b a state of selected cell
* @method fireSelected
* @protected
*/
this.fireSelected = function(row, col, b) {
this.target.fire("selected", [ this.target, row, col, b]);
this.repaintTarget(row, col);
};
/**
* Force cells repainting.
* @param {Integer} row a cell row
* @param {Integer} col a cell column
* @method repaintTarget
* @protected
*/
this.repaintTarget = function(row, col) {
this.target.repaintCells(row, col);
};
}
]);
/**
* Row selection mode class. In this mode it is possible to select single
* grid row.
* @param {zebkit.ui.grid.Grid} target a target grid the selection mode
* instance class belongs
* @extends zebkit.ui.grid.CellSelectMode
* @class zebkit.ui.grid.RowSelectMode
*/
pkg.RowSelectMode = Class(pkg.CellSelectMode, [
function $prototype() {
this.isSelected = function(row, col) {
return row >= 0 && this.selectedRow === row;
};
this.repaintTarget = function(row, col) {
this.target.repaintRows(row, row + 1);
};
}
]);
/**
* Column selection mode class. In this mode it is possible to select single
* grid column.
* @param {zebkit.ui.grid.Grid} target a target grid the selection mode
* instance class belongs
* @extends zebkit.ui.grid.CellSelectMode
* @class zebkit.ui.grid.ColSelectMode
*/
pkg.ColSelectMode = Class(pkg.CellSelectMode, [
function $prototype() {
this.isSelected = function(row, col) {
return col >= 0 && this.selectedCol === col;
};
this.repaintTarget = function(row, col) {
this.target.repaintCols(col, col + 1);
};
}
]);
/**
* Grid UI component class. The grid component visualizes "zebkit.data.Matrix" data model.
* Grid cell visualization can be customized by defining and setting an own view provider.
* Grid component supports cell editing. Every existent UI component can be configured
* as a cell editor by defining an own editor provider.
*
*
* // create a grid that contains three rows and tree columns
* var grid = new zebkit.ui.grid.Grid([
* [ "Cell 1.1", "Cell 1.2", "Cell 1.3"],
* [ "Cell 2.1", "Cell 2.2", "Cell 2.3"],
* [ "Cell 3.1", "Cell 3.2", "Cell 3.3"]
* ]);
*
* // add the top caption
* grid.add("top", new zebkit.ui.grid.GridCaption([
* "Caption title 1", "Caption title 2", "Caption title 3"
* ]));
*
* // set rows size
* grid.setRowsHeight(45);
*
*
* Grid can have top and left captions.
* @class zebkit.ui.grid.Grid
* @constructor
* @param {zebkit.data.Matrix|Array} [model] a matrix model to be visualized with the grid
* component. It can be an instance of zebkit.data.Matrix class or an array that contains
* embedded arrays. Every embedded array is a grid row.
* @param {Integer} [rows] a number of rows
* @param {Integer} [columns] a number of columns
* @extends zebkit.ui.Panel
* @uses zebkit.ui.grid.Metrics
* @uses zebkit.ui.HostDecorativeViews
*/
/**
* Fire when a grid row selection state has been changed
*
* grid.on("selected", function(grid, row, col, status) {
* ...
* });
*
* @event selected
* @param {zebkit.ui.grid.Grid} grid a grid that triggers the event
* @param {Integer} row a selected row
* @param {Integer} col a selected column
* @param {Boolean} status a selection status. true means rows have been selected
*/
pkg.Grid = Class(ui.Panel, zebkit.util.Position.Metric, pkg.Metrics, ui.HostDecorativeViews, [
function(model) {
if (arguments.length === 0) {
model = new this.clazz.Matrix(5, 5);
} else if (arguments.length === 2) {
model = new this.clazz.Matrix(arguments[0], arguments[1]);
}
this.setSelectMode("row");
this.views = {};
this.visibility = new pkg.CellsVisibility();
this.$super();
this.add("corner", new this.clazz.CornerPan());
this.setModel(model);
this.setViewProvider(new this.clazz.DefViews());
this.setPosition(new zebkit.util.Position(this));
this.scrollManager = new ui.ScrollManager(this);
},
function $clazz() {
this.Matrix = Class(zebkit.data.Matrix, []);
this.DEF_COLWIDTH = 80;
this.DEF_ROWHEIGHT = 25;
this.CornerPan = Class(ui.Panel, []);
this.DefViews = Class(pkg.DefViews, []);
},
function $prototype() {
this.psWidth_ = this.psHeight_ = this.colOffset = 0;
this.rowOffset = this.pressedCol = 0;
this.visibleArea = null;
/**
* Scroll manager
* @attribute scrollManager
* @type {zebkit.ui.ScrollManager}
* @protected
* @readOnly
*/
this.scrollManager = null;
/**
* Reference to top caption component
* @attribute topCaption
* @type {zebkit.ui.grid.GridCaption|zebkit.ui.grid.CompGridCaption}
* @default null
* @readOnly
*/
this.topCaption = null;
/**
* Reference to left caption component
* @attribute leftCaption
* @type {zebkit.ui.grid.GridCaption|zebkit.ui.grid.CompGridCaption}
* @default null
* @readOnly
*/
this.leftCaption = null;
/**
* Cell editors provider
* @type {zebkit.ui.grid.DefEditors}
* @attribute editors
* @readOnly
* @default null
*/
this.editors = null;
/**
* Currently activated cell editor.
* @type {zebkit.ui.Panel}
* @attribute editor
* @readOnly
* @default null
*/
this.editor = null;
/**
* Grid cell select mode
* @attribute selectMode
* @type {zebkit.ui.grid.SelectMode}
* @readOnly
* @default row
*/
this.selectMode = null;
/**
* Calculated grid columns widths
* @attribute colWidths
* @type {Array}
* @protected
* @readOnly
*/
this.colWidths = null;
/**
* Calculated grid columns heights
* @attribute rowHeights
* @type {Array}
* @protected
* @readOnly
*/
this.rowHeights = null;
this.position = this.stub = null;
/**
* Grid model.
* @type {zebkit.data.Matrix}
* @attribute model
*/
this.model = null;
/**
* Currently editing row. -1 if no row is editing
* @attribute editingRow
* @type {Integer}
* @default -1
* @readOnly
*/
this.editingRow = -1;
/**
* Currently editing column. -1 if no column is editing
* @attribute editingCol
* @type {Integer}
* @default -1
* @readOnly
*/
this.editingCol = this.pressedRow = -1;
/**
* Grid navigation mode
* @attribute navigationMode
* @default "row"
* @type {String}
*/
this.navigationMode = "row";
/**
* Grid line size
* @attribute lineSize
* @default 1
* @type {Integer}
*/
this.lineSize = 1;
/**
* Grid cell top padding
* @attribute cellInsetsTop
* @default 1
* @type {Integer}
* @readOnly
*/
this.cellInsetsTop = 1;
/**
* Grid cell left padding
* @attribute cellInsetsLeft
* @default 2
* @type {Integer}
* @readOnly
*/
this.cellInsetsLeft = 2;
/**
* Grid cell bottom padding
* @attribute cellInsetsBottom
* @default 1
* @type {Integer}
* @readOnly
*/
this.cellInsetsBottom = 1;
/**
* Grid cell right padding
* @attribute cellInsetsRight
* @default 2
* @type {Integer}
* @readOnly
*/
this.cellInsetsRight = 2;
/**
* Default cell content horizontal alignment
* @type {String}
* @attribute defXAlignment
* @default "left"
*/
this.defXAlignment = "left";
/**
* Default cell content vertical alignment
* @type {String}
* @attribute defYAlignment
* @default "center"
*/
this.defYAlignment = "center";
/**
* Indicate if horizontal lines have to be rendered
* @attribute drawHorLines
* @type {Boolean}
* @readOnly
* @default true
*/
this.drawHorLines = true;
/**
* Indicate if vertical lines have to be rendered
* @attribute drawVerLines
* @type {Boolean}
* @readOnly
* @default true
*/
this.drawVerLines = true;
/**
* Line color
* @attribute lineColor
* @type {String}
* @default gray
* @readOnly
*/
this.lineColor = "gray";
/**
* Indicate if caption lines have to be rendered
* @type {Boolean}
* @attribute drawCaptionLines
* @default true
*/
this.drawCaptionLines = true;
/**
* Indicate if size of grid cells have to be calculated
* automatically basing on its preferred heights and widths
* @attribute isUsePsMetric
* @type {Boolean}
* @default false
* @readOnly
*/
this.isUsePsMetric = false;
/**
* Defines if the pos marker has to be renederd over rendered data
* @attribute paintPosMarkerOver
* @type {Boolean}
* @default true
*/
this.paintPosMarkerOver = true;
/**
* Initial (not scrolled) y coordinate of first cell
* @method $initialCellY
* @return {Integer} initial y coordinate
* @private
*/
this.$initialCellY = function() {
var ly = 0;
if (this.topCaption !== null && this.topCaption.isVisible) {
ly = this.topCaption.y + this.topCaption.height;
} else {
ly = this.getTop();
}
ly += this.lineSize;
return ly;
};
/**
* Initial (not scrolled) x coordinate of first cell
* @method $initialCellX
* @return {Integer} initial x coordinate
* @private
*/
this.$initialCellX = function() {
var lx = 0;
if (this.leftCaption !== null && this.leftCaption.isVisible) {
lx += this.leftCaption.x + this.leftCaption.width;
} else {
lx = this.getLeft();
}
lx += this.lineSize;
return lx;
};
/**
* Set the grid cell content default horizontal alignment.
* @param {String} ax a horizontal alignment. Use "left", "right" or "center"
* as the alignment value.
* @method setDefCellXAlignment
* @chainable
*/
this.setDefCellXAlignment = function(ax) {
this.setDefCellAlignments(ax, this.defYAlignment);
return this;
};
/**
* Set the grid cell default vertical alignment.
* @param {String} ay a vertical alignment. Use "top", "bottom" or "center"
* as the alignment value.
* @method setDefCellYAlignment
* @chainable
*/
this.setDefCellYAlignment = function(ay) {
this.setDefCellAlignments(this.defXAlignment, ay);
return this;
};
/**
* Set the grid cell default horizontal and vertical alignments.
* @param {String} ax a horizontal alignment. Use "left", "right" or "center"
* @param {String} ay a horizontal alignment. Use "top", "bottom" or "center"
* as the alignment value.
* @method setDefCellAlignments
* @chainable
*/
this.setDefCellAlignments = function(ax, ay) {
if (this.defXAlignment !== ax || this.defYAlignment !== ay) {
this.defXAlignment = ax;
this.defYAlignment = ay;
this.repaint();
}
return this;
};
/**
* Return a view that is used to render the given grid cell.
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @return {zebkit.draw.View} a cell view
* @method getCellView
*/
this.getCellView = function(row, col) {
return this.provider.getView(this, row, col, this.model.get(row, col));
};
this.colVisibility = function(col, x, d, b){
var cols = this.getGridCols();
if (cols === 0) {
return null;
} else {
var left = this.getLeft(),
dx = this.scrollManager.getSX(),
xx1 = Math.min(this.visibleArea.x + this.visibleArea.width,
this.width - this.getRight()),
xx2 = Math.max(left, this.visibleArea.x +
this.getLeftCaptionWidth());
for(; col < cols && col >= 0; col += d) {
if (x + dx < xx1 && (x + this.colWidths[col] + dx) > xx2) {
if (b) {
return [col, x];
}
} else if (b === false) {
return this.colVisibility(col, x, (d > 0 ? -1 : 1), true);
}
if (d < 0) {
if (col > 0) {
x -= (this.colWidths[col - 1] + this.lineSize);
}
} else {
if (col < cols - 1) {
x += (this.colWidths[col] + this.lineSize);
}
}
}
return b ? null : ((d > 0) ? [col -1, x]
: [0, this.$initialCellX() ]);
}
};
this.rowVisibility = function(row,y,d,b) {
var rows = this.getGridRows();
if (rows === 0) {
return null;
} else {
var top = this.getTop(),
dy = this.scrollManager.getSY(),
yy1 = Math.min(this.visibleArea.y + this.visibleArea.height,
this.height - this.getBottom()),
yy2 = Math.max(this.visibleArea.y,
top + this.getTopCaptionHeight());
for(; row < rows && row >= 0; row += d){
if (y + dy < yy1 && (y + this.rowHeights[row] + dy) > yy2){
if (b) {
return [row, y];
}
} else {
if (b === false) {
return this.rowVisibility(row, y, (d > 0 ? -1 : 1), true);
}
}
if (d < 0){
if (row > 0) {
y -= (this.rowHeights[row - 1] + this.lineSize);
}
} else {
if (row < rows - 1) {
y += (this.rowHeights[row] + this.lineSize);
}
}
}
return b ? null : ((d > 0) ? [row - 1, y]
: [0, this.$initialCellY()]);
}
};
this.vVisibility = function(){
var va = ui.$cvp(this, {});
if (va === null) {
this.visibleArea = null;
this.visibility.fr = null; // say no visible cells are available
} else {
// visible area has not been calculated or
// visible area has been changed
if (this.visibleArea === null ||
va.x !== this.visibleArea.x ||
va.y !== this.visibleArea.y ||
va.width !== this.visibleArea.width ||
va.height !== this.visibleArea.height )
{
this.iColVisibility(0);
this.iRowVisibility(0);
this.visibleArea = va;
}
var v = this.visibility,
b = v.hasVisibleCells();
if (this.colOffset !== 100) {
if (this.colOffset > 0 && b){
v.lc = this.colVisibility(v.lc[0], v.lc[1], -1, true);
v.fc = this.colVisibility(v.lc[0], v.lc[1], -1, false);
} else {
if (this.colOffset < 0 && b) {
v.fc = this.colVisibility(v.fc[0], v.fc[1], 1, true);
v.lc = this.colVisibility(v.fc[0], v.fc[1], 1, false);
} else {
v.fc = this.colVisibility(0, this.$initialCellX(), 1, true);
v.lc = (v.fc !== null) ? this.colVisibility(v.fc[0], v.fc[1], 1, false)
: null;
}
}
this.colOffset = 100;
}
if (this.rowOffset !== 100) {
if (this.rowOffset > 0 && b) {
v.lr = this.rowVisibility(v.lr[0], v.lr[1], -1, true);
v.fr = this.rowVisibility(v.lr[0], v.lr[1], -1, false);
} else {
if(this.rowOffset < 0 && b){
v.fr = this.rowVisibility(v.fr[0], v.fr[1], 1, true);
v.lr = (v.fr !== null) ? this.rowVisibility(v.fr[0], v.fr[1], 1, false) : null;
} else {
v.fr = this.rowVisibility(0, this.$initialCellY(), 1, true);
v.lr = (v.fr !== null) ? this.rowVisibility(v.fr[0], v.fr[1], 1, false) : null;
}
}
this.rowOffset = 100;
}
}
};
/**
* Make the given cell visible.
* @param {Integer} row a cell row
* @param {Integer} col a cell column
* @method makeVisible
* @chainable
*/
this.makeVisible = function(row, col) {
var top = this.getTop() + this.getTopCaptionHeight(),
left = this.getLeft() + this.getLeftCaptionWidth(),
o = ui.calcOrigin(this.getColX(col),
this.getRowY(row),
// width depends on marker mode: cell or row
this.getLineSize(row) > 1 ? this.colWidths[col] + this.lineSize
: this.psWidth_,
this.rowHeights[row] + this.lineSize,
this.scrollManager.getSX(),
this.scrollManager.getSY(),
this, top, left,
this.getBottom(),
this.getRight());
this.scrollManager.scrollTo(o[0], o[1]);
return this;
};
this.$se = function(row, col, e) {
if (row >= 0) {
this.stopEditing(true);
if (this.editors !== null &&
this.editors.shouldStart(this, row, col, e))
{
return this.startEditing(row, col);
}
}
return false;
};
this.getXOrigin = function() {
return this.scrollManager.getSX();
};
this.getYOrigin = function () {
return this.scrollManager.getSY();
};
/**
* Get a preferred width the given column wants to have
* @param {Integer} col a column
* @return {Integer} a preferred width of the given column
* @method getColPSWidth
*/
this.getColPSWidth = function(col){
return this.getPSSize(col, false);
};
/**
* Get a preferred height the given row wants to have
* @param {Integer} col a row
* @return {Integer} a preferred height of the given row
* @method getRowPSHeight
*/
this.getRowPSHeight = function(row) {
return this.getPSSize(row, true);
};
this.recalc = function(){
if (this.isUsePsMetric) {
this.rPsMetric();
} else {
this.rCustomMetric();
}
this.psHeight_ = this.psWidth_ = 0;
var cols = this.getGridCols(),
rows = this.getGridRows();
if (cols > 0) {
this.psWidth_ += ((cols + 1) * this.lineSize);
}
// if left caption is visible add extra line size since vertical line has to
// be rendered at the left side of left caption
if (this.leftCaption !== null && this.leftCaption.isVisible) {
this.psWidth_ += this.lineSize;
}
if (rows > 0) {
this.psHeight_ += ((rows + 1) * this.lineSize);
}
// if top caption is visible add extra line size since horizontal line has to
// be rendered at the top side of top caption
if (this.topCaption !== null && this.topCaption.isVisible) {
this.psHeight_ += this.lineSize;
}
// accumulate column widths
var i = 0;
for (;i < cols; i++) {
this.psWidth_ += this.colWidths[i];
}
// accumulate row heights
for (i = 0; i < rows; i++) {
this.psHeight_ += this.rowHeights[i];
}
};
/**
* Get number of rows in the given grid
* @return {Integer} a number of rows
* @method getGridRows
*/
this.getGridRows = function() {
return this.model !== null ? this.model.rows : 0;
};
/**
* Get number of columns in the given grid
* @return {Integer} a number of columns
* @method getGridColumns
*/
this.getGridCols = function(){
return this.model !== null ? this.model.cols : 0;
};
/**
* Get the given grid row height
* @param {Integer} row a grid row
* @return {Integer} a height of the given row
* @method getRowHeight
*/
this.getRowHeight = function(row){
this.validateMetric();
return this.rowHeights[row];
};
/**
* Get the given grid column width
* @param {Integer} col a grid column
* @return {Integer} a width of the given column
* @method getColWidth
*/
this.getColWidth = function(col){
this.validateMetric();
return this.colWidths[col];
};
this.getCellsVisibility = function(){
this.validateMetric();
return this.visibility;
};
/**
* Get the given column top-left corner x coordinate
* @param {Integer} col a column
* @return {Integer} a top-left corner x coordinate of the given column
* @method getColX
*/
this.getColX = function (col){
// speed up a little bit by avoiding calling validateMetric method
if (this.isValid === false) {
this.validateMetric();
}
var start = 0,
d = 1,
x = 0;
if (this.visibility.hasVisibleCells()) {
start = this.visibility.fc[0];
x = this.visibility.fc[1];
d = (col > this.visibility.fc[0]) ? 1 : -1;
} else {
if (this.leftCaption !== null && this.leftCaption.isVisible) {
x = this.leftCaption.x + this.leftCaption.width + this.lineSize;
} else {
x = this.getLeft() + this.lineSize;
}
}
for(var i = start;i !== col; x += ((this.colWidths[i] + this.lineSize) * d),i += d) {}
return x;
};
/**
* Get the given row top-left corner y coordinate
* @param {Integer} row a row
* @return {Integer} a top-left corner y coordinate
* of the given column
* @method getColX
*/
this.getRowY = function (row){
// speed up a little bit by avoiding calling validateMetric method
if (this.isValid === false) {
this.validateMetric();
}
var start = 0,
d = 1,
y = 0;
if (this.visibility.hasVisibleCells()){
start = this.visibility.fr[0];
y = this.visibility.fr[1];
d = (row > this.visibility.fr[0]) ? 1 : -1;
} else {
if (this.topCaption !== null && this.topCaption.isVisible) {
y = this.topCaption.y + this.topCaption.height + this.lineSize;
} else {
y = this.getTop() + this.lineSize;
}
}
for(var i = start;i !== row; y += ((this.rowHeights[i] + this.lineSize) * d),i += d) {}
return y;
};
this.childPointerEntered =
this.childPointerExited =
this.childPointerReleased =
this.childPointerReleased =
this.childPointerPressed =
this.childKeyReleased =
this.childKeyTyped =
this.childKeyPressed = function(e){
if (this.editingRow >= 0) {
if (this.editors.shouldCancel(this,
this.editingRow,
this.editingCol, e))
{
this.stopEditing(false);
} else {
if (this.editors.shouldFinish(this,
this.editingRow,
this.editingCol, e))
{
this.stopEditing(true);
}
}
}
};
this.iColVisibility = function(off) {
this.colOffset = (this.colOffset === 100) ? this.colOffset = off
: ((off !== this.colOffset) ? 0 : this.colOffset);
};
this.iRowVisibility = function(off) {
this.rowOffset = (this.rowOffset === 100) ? off
: (((off + this.rowOffset) === 0) ? 0 : this.rowOffset);
};
/**
* Get top grid caption height. Return zero if no top caption element has been defined
* @return {Integer} a top caption height
* @protected
* @method getTopCaptionHeight
*/
this.getTopCaptionHeight = function(){
return (this.topCaption !== null && this.topCaption.isVisible === true) ? this.topCaption.height : 0;
};
/**
* Get left grid caption width. Return zero if no left caption element has been defined
* @return {Integer} a left caption width
* @protected
* @method getLeftCaptionWidth
*/
this.getLeftCaptionWidth = function(){
return (this.leftCaption !== null && this.leftCaption.isVisible === true) ? this.leftCaption.width : 0;
};
this.paint = function(g){
this.vVisibility();
if (this.visibility.hasVisibleCells()) {
var dx = this.scrollManager.getSX(),
dy = this.scrollManager.getSY(),
th = this.getTopCaptionHeight(),
tw = this.getLeftCaptionWidth();
g.save();
try {
g.translate(dx, dy);
if (th > 0 || tw > 0) {
g.clipRect(tw - dx, th - dy, this.width - tw, this.height - th);
}
if (this.paintPosMarkerOver !== true) {
this.paintPosMarker(g);
}
this.paintData(g);
if (this.lineSize > 0 && (this.drawHorLines === true || this.drawVerLines === true)) {
this.paintNet(g);
}
if (this.paintPosMarkerOver === true) {
this.paintPosMarker(g);
}
} catch(e) {
g.restore();
throw e;
}
g.restore();
}
};
this.paintOnTop = function(g) {
// paint lines over captions
if (this.drawCaptionLines && (this.drawHorLines === true || this.drawVerLines === true)) {
var v = this.visibility,
i = 0;
if (this.leftCaption !== null && this.leftCaption.isVisible) {
g.setColor(this.lineColor);
g.beginPath();
if (g.lineWidth !== this.lineSize) {
g.lineWidth = this.lineSize;
}
var sx = this.leftCaption.x - this.lineSize,
y = v.fr[1] - this.lineSize / 2 + this.scrollManager.getSY(),
minY = (this.topCaption !== null && this.topCaption.isVisible ? this.topCaption.y + this.topCaption.height
: this.getTop());
g.moveTo(this.leftCaption.x - this.lineSize / 2, this.getTop());
g.lineTo(this.leftCaption.x - this.lineSize / 2,
Math.min(this.leftCaption.y + this.leftCaption.height,
this.height - this.getBottom()));
sx = this.leftCaption.x;
for(;i <= v.lr[0] + 1; i++) {
if (y >= minY) {
g.moveTo(sx, y);
g.lineTo(sx + this.leftCaption.width + this.lineSize, y);
}
y += this.rowHeights[i] + this.lineSize;
}
g.stroke();
}
if (this.topCaption !== null && this.topCaption.isVisible) {
g.setColor(this.lineColor);
g.beginPath();
if (g.lineWidth !== this.lineSize) {
g.lineWidth = this.lineSize;
}
var sy = this.topCaption.y - this.lineSize,
minX = this.leftCaption !== null && this.leftCaption.isVisible ? this.leftCaption.x + this.leftCaption.width
: this.getLeft(),
x = v.fc[1] - this.lineSize / 2 + this.scrollManager.getSX();
g.moveTo(this.topCaption.x - this.getLeftCaptionWidth(), sy + this.lineSize / 2);
g.lineTo(Math.min(this.topCaption.x + this.topCaption.width,
this.width - this.getRight()),
sy + this.lineSize / 2);
sy = this.topCaption.y;
for (i = v.fc[0]; i <= v.lc[0] + 1; i++) {
if (x >= minX) {
g.moveTo(x, sy);
g.lineTo(x, sy + this.topCaption.height + this.lineSize);
}
x += this.colWidths[i] + this.lineSize;
}
g.stroke();
}
}
};
/**
* Scroll action handler
* @param {Integer} psx a previous horizontal scroll offset
* @param {Integer} psy a previous vertical scroll offset
* @method catchScrolled
*/
this.catchScrolled = function (psx, psy){
var offx = this.scrollManager.getSX() - psx,
offy = this.scrollManager.getSY() - psy;
if (offx !== 0) {
this.iColVisibility(offx > 0 ? 1 : - 1);
}
if (offy !== 0) {
this.iRowVisibility(offy > 0 ? 1 : - 1);
}
this.stopEditing(false);
this.repaint();
};
//TODO: zebkit doesn't support yet the method
this.isInvalidatedByChild = function (c){
return c !== this.editor || this.isUsePsMetric;
};
/**
* Stop editing a grid cell.
* @param {Boolean} applyData true if the edited data has to be applied as a new
* grid cell content
* @protected
* @method stopEditing
*/
this.stopEditing = function(applyData){
if (this.editors !== null &&
this.editingRow >= 0 &&
this.editingCol >= 0 )
{
try {
if (zebkit.instanceOf(this.editor, pkg.Grid)) {
this.editor.stopEditing(applyData);
}
var data = this.getDataToEdit(this.editingRow, this.editingCol);
if (applyData){
this.setEditedData(this.editingRow,
this.editingCol,
this.editors.fetchEditedValue( this,
this.editingRow,
this.editingCol,
data, this.editor));
}
this.repaintRows(this.editingRow, this.editingRow);
} finally {
this.editingCol = this.editingRow = -1;
if (this.indexOf(this.editor) >= 0) {
this.remove(this.editor);
}
this.editor = null;
this.requestFocus();
}
}
};
/**
* Set if horizontal and vertical lines have to be painted
* @param {Boolean} hor true if horizontal lines have to be painted
* @param {Boolean} ver true if vertical lines have to be painted
* @method setDrawLines
* @chainable
*/
this.setDrawLines = function(hor, ver){
if (this.drawVerLines !== hor || this.drawHorLines !== ver) {
this.drawHorLines = hor;
this.drawVerLines = ver;
this.repaint();
}
return this;
};
/**
* Set the given grid cell select mode.
* @param {zebki.ui.grid.SelectMode|String} mode a select mode. It is possible
* to specify the mode with one of the following string constant:
*
* - "row" - single row select mode
* - "col" - single column select mode
* - "cell" - single cell select mode
*
*
* @method setSelectMode
* @chainable
*/
this.setSelectMode = function(mode) {
this.clearSelect();
var prevSelMode = this.selectMode;
if (mode !== this.selectMode) {
if (prevSelMode !== null && typeof prevSelMode.uninstall === 'function') {
prevSelMode.uninstall(this);
}
if (zebkit.isString(mode)) {
if (mode.toLowerCase() === "row") {
this.selectMode = new pkg.RowSelectMode(this);
this.setNavigationMode(mode);
} else if (mode.toLowerCase() === "col") {
this.selectMode = new pkg.ColSelectMode(this);
this.setNavigationMode(mode);
} else if (mode.toLowerCase() === "cell") {
this.selectMode = new pkg.CellSelectMode(this);
this.setNavigationMode(mode);
} else {
throw new Error("Invalid select mode '" + mode + "'");
}
} else if (mode === null) {
this.selectMode = null;
} else {
this.selectMode = mode;
}
if (this.selectMode !== null && typeof this.selectMode.install === 'function') {
this.selectMode.install(this);
}
}
return this;
};
/**
* Set navigation mode. It is possible to use "row" or "cell" or "col" navigation mode.
* In first case navigation happens over row, in the second
* case navigation happens over cell.
* @param {String} mode a navigation mode ("row" or "cell" or "col")
* @method setNavigationMode
* @chainable
*/
this.setNavigationMode = function(mode) {
if (this.position !== null) {
this.position.setOffset(null);
}
if (mode.toLowerCase() === "row") {
this.navigationMode = "row";
this.getLineSize = function(row) {
return 1;
};
this.getMaxOffset = function() {
return this.getGridRows() - 1;
};
this.getLines = function() {
return this.getGridRows();
};
} else if (mode.toLowerCase() === "cell") {
this.navigationMode = "cell";
this.getLines = function() {
return this.getGridRows();
};
this.getLineSize = function(row) {
return this.getGridCols();
};
this.getMaxOffset = function() {
return this.getGridRows() * this.getGridCols() - 1;
};
} else if (mode.toLowerCase() === "col") {
this.navigationMode = "col";
this.getLineSize = function(row) {
return this.getGridCols();
};
this.getMaxOffset = function() {
return this.getGridCols() - 1;
};
this.getLines = function() {
return 1;
};
} else if (mode === null) {
this.navigationMode = null;
} else {
throw new Error("Invalid navigation mode value : '" + mode + "'");
}
return this;
};
/**
* Position changed event handler.
* @param {zebkit.util.Position} target a position manager
* @param {Integer} prevOffset a previous position offset
* @param {Integer} prevLine a previous position line
* @param {Integer} prevCol a previous position column
* @method posChanged
*/
this.posChanged = function(target, prevOffset, prevLine, prevCol) {
var row = this.position.currentLine,
col = this.position.currentCol;
if (row >= 0) {
this.makeVisible(row, col);
if (this.selectMode !== null) {
this.selectMode.posChanged(target, prevOffset, prevLine, prevCol);
}
if (this.navigationMode === "row") {
this.repaintRows(prevLine, row);
} else if (this.navigationMode === "col") {
this.repaintCols(prevCol, col);
} else if (this.navigationMode === "cell") {
this.repaintCells(row, col, prevLine, prevCol);
}
} else {
this.repaintRows(prevLine, prevLine);
}
};
/**
* Implement key released handler.
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyReleased
*/
this.keyReleased = function(e) {
if (this.position !== null) {
this.$se(this.position.currentLine,
this.position.currentCol, e);
}
};
/**
* Implement key type handler.
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyTyped
*/
this.keyTyped = function(e){
if (this.position !== null) {
this.$se(this.position.currentLine, this.position.currentCol, e);
}
};
/**
* Implement key pressed handler.
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function(e){
if (this.position !== null) {
switch(e.code) {
case "ArrowLeft" : this.position.seek(-1); break;
case "ArrowUp" : this.position.seekLineTo("up"); break;
case "ArrowRight" : this.position.seek(1); break;
case "ArrowDown" : this.position.seekLineTo("down");break;
case "PageUp" : this.position.seekLineTo("up", this.pageSize(-1));break;
case "PageDown" : this.position.seekLineTo("down", this.pageSize(1));break;
case "End" :
if (e.ctrlKey) {
this.position.setOffset(this.getLines() - 1);
} break;
case "Home" :
if (e.ctrlKey) {
this.position.setOffset(0);
} break;
}
this.$se(this.position.currentLine, this.position.currentCol, e);
}
};
/**
* Checks if the given grid cell is selected
* @param {Integer} row a grid row
* @param {Integer} col a grid col
* @return {Boolean} true if the given row is selected
* @method isSelected
*/
this.isSelected = function(row, col) {
return this.selectMode === null ? false
: this.selectMode.isSelected(row, col);
};
/**
* Repaint range of grid rows
* @param {Integer} r1 the first row to be repainted
* @param {Integer} r2 the last row to be repainted
* @method repaintRows
* @chainable
*/
this.repaintRows = function(r1, r2){
if (r1 < 0) {
r1 = r2;
}
if (r2 < 0) {
r2 = r1;
}
if (r1 > r2) {
var i = r2;
r2 = r1;
r1 = i;
}
var rows = this.getGridRows();
if (r1 >= 0 && r1 < rows) {
if (r2 >= rows) {
r2 = rows - 1;
}
var y1 = this.getRowY(r1),
y2 = ((r1 === r2) ? y1 + 1 : this.getRowY(r2)) + this.rowHeights[r2];
this.repaint(0, y1 + this.scrollManager.getSY(), this.width, y2 - y1);
}
return this;
};
/**
* Repaint range of grid columns
* @param {Integer} c1 the first column to be repainted
* @param {Integer} c2 the last column to be repainted
* @method repaintCols
* @chainable
*/
this.repaintCols = function(c1, c2){
if (c1 < 0) {
c1 = c2;
}
if (c2 < 0) {
c2 = c1;
}
if (c1 > c2) {
var i = c2;
c2 = c1;
c1 = i;
}
var cols = this.getGridCols();
if (c1 >= 0 && c1 < cols) {
if (c2 >= cols) {
c2 = cols - 1;
}
var x1 = this.getColX(c1),
x2 = ((c1 === c2) ? x1 + 1 : this.getColX(c2)) + this.colWidths[c2];
this.repaint(x1 + this.scrollManager.getSX(), 0, x2 - x1, this.height);
}
return this;
};
/**
* Repaint cells.
* @param {Integer} r1 first row
* @param {Integer} c1 first column
* @param {Integer} [r2] second row
* @param {Integer} [c2] second column
* @method repaintCells
* @chainable
*/
this.repaintCells = function(r1, c1, r2, c2) {
var cols = this.getGridCols(),
rows = this.getGridRows(),
i = 0;
if (arguments.length === 2) {
c2 = c1;
r2 = r1;
}
if (r1 < 0) {
r1 = r2;
} else if (r2 < 0) {
r2 = r1;
}
if (c1 < 0) {
c1 = c2;
} else if (c2 < 0) {
c2 = c1;
}
if (c1 > c2) {
i = c2;
c2 = c1;
c1 = i;
}
if (r1 > r2) {
i = r2;
r2 = r1;
r1 = i;
}
if (r1 >= 0 && c1 >= 0 && r1 < rows && c1 < cols) {
if (c2 > cols) {
c2 = cols - 1;
}
if (r2 > rows) {
r2 = rows - 1;
}
var x1 = this.getColX(c1),
x2 = ((c1 === c2) ? x1 + 1 : this.getColX(c2)) + this.colWidths[c2],
y1 = this.getRowY(r1),
y2 = ((r1 === r2) ? y1 + 1 : this.getRowY(r2)) + this.rowHeights[r2];
this.repaint(x1 + this.scrollManager.getSX(),
y1 + this.scrollManager.getSY(),
x2 - x1, y2 - y1);
}
return this;
};
/**
* Detect a cell by the given location
* @param {Integer} x a x coordinate relatively the grid component
* @param {Integer} y a y coordinate relatively the grid component
* @return {Object} an object that contains detected grid cell row as
* "row" field and a grid column as "col" field. null is returned if
* no cell can be detected.
* @method cellByLocation
*/
this.cellByLocation = function(x,y){
this.validate();
var dx = this.scrollManager.getSX(),
dy = this.scrollManager.getSY(),
v = this.visibility,
ry1 = v.fr[1] + dy,
rx1 = v.fc[1] + dx,
row = -1,
col = -1,
i = 0,
ry2 = v.lr[1] + this.rowHeights[v.lr[0]] + dy,
rx2 = v.lc[1] + this.colWidths[v.lc[0]] + dx;
if (y > ry1 && y < ry2) {
for(i = v.fr[0];i <= v.lr[0]; ry1 += this.rowHeights[i] + this.lineSize, i++) {
if (y > ry1 && y < ry1 + this.rowHeights[i]) {
row = i;
break;
}
}
}
if (x > rx1 && x < rx2) {
for (i = v.fc[0];i <= v.lc[0]; rx1 += this.colWidths[i] + this.lineSize, i++ ) {
if (x > rx1 && x < rx1 + this.colWidths[i]) {
col = i;
break;
}
}
}
return (col >= 0 && row >= 0) ? { row: row, col: col } : null;
};
this.doLayout = function(target) {
var topHeight = (this.topCaption !== null &&
this.topCaption.isVisible === true) ? this.topCaption.getPreferredSize().height
: 0,
leftWidth = (this.leftCaption !== null &&
this.leftCaption.isVisible === true) ? this.leftCaption.getPreferredSize().width : 0,
topY = this.getTop(),
leftX = this.getLeft();
if (topHeight > 0) {
// topHeight += this.lineSize;
topY += this.lineSize;
}
if (leftWidth > 0) {
// leftWidth += this.lineSize;
leftX += this.lineSize;
}
if (this.topCaption !== null){
this.topCaption.setBounds(leftX + leftWidth,
topY,
Math.min(target.width - this.getLeft() - this.getRight() - leftWidth,
this.psWidth_),
topHeight);
}
if (this.leftCaption !== null){
this.leftCaption.setBounds(leftX,
topY + topHeight,
leftWidth,
Math.min(target.height - this.getTop() - this.getBottom() - topHeight,
this.psHeight_));
}
if (this.stub !== null && this.stub.isVisible === true)
{
if (leftWidth > 0 && topHeight > 0) {
this.stub.setBounds(leftX, topY,
leftWidth,
topHeight);
} else {
this.stub.setSize(0, 0);
}
}
if (this.editors !== null &&
this.editor !== null &&
this.editor.parent === this &&
this.editor.isVisible === true)
{
var w = this.colWidths[this.editingCol],
h = this.rowHeights[this.editingRow],
x = this.getColX(this.editingCol),
y = this.getRowY(this.editingRow);
if (this.isUsePsMetric){
x += this.cellInsetsLeft;
y += this.cellInsetsTop;
w -= (this.cellInsetsLeft + this.cellInsetsRight);
h -= (this.cellInsetsTop + this.cellInsetsBottom);
}
this.editor.setBounds(x + this.scrollManager.getSX(),
y + this.scrollManager.getSY(), w, h);
}
};
this.canHaveFocus = function (){
return this.editor === null;
};
/**
* Clear grid row or rows selection
* @method clearSelect
* @chainable
*/
this.clearSelect = function() {
if (this.selectMode !== null) {
this.selectMode.clearSelect();
}
return this;
};
/**
* Mark as selected or unselected the given grid cell
* @param {Integer} row a grid row
* @param {Integer} [col] a grid row,
* @param {boolean} [b] a selection status. true if the parameter
* has not been specified
* @method select
* @chainable
*/
this.select = function(row, col, b) {
if (this.selectMode !== null) {
if (arguments.length === 1) {
col = -1;
b = false;
} else if (arguments.length === 2) {
if (zebkit.isInteger(col)) {
b = false;
} else {
b = col;
col = -1;
}
}
this.selectMode.select(row, col, b);
}
return this;
};
this.laidout = function () {
this.vVisibility();
};
this.pointerClicked = function(e) {
if (e.isAction() && this.visibility.hasVisibleCells()){
this.stopEditing(true);
if (e.isAction()){
var p = this.cellByLocation(e.x, e.y);
if (p !== null) {
if (this.position !== null){
var row = this.position.currentLine,
col = this.position.currentCol,
ls = this.getLineSize(p.row),
lns = this.getLines();
// normalize column depending on marker mode: row or cell
// in row mode marker can select only the whole row, so
// column can be only 1 (this.getLineSize returns 1)
if (row === p.row % lns && col === p.col % ls) {
this.makeVisible(row, col);
} else {
this.position.setRowCol(p.row % lns, p.col % ls);
}
}
if (this.$se(p.row, p.col, e)) {
// TODO: initiated editor has to get pointer clicked event
}
}
}
}
};
this.calcPreferredSize = function(target) {
return {
width : this.psWidth_ +
((this.leftCaption !== null &&
this.leftCaption.isVisible === true) ? this.leftCaption.getPreferredSize().width : 0),
height: this.psHeight_ +
((this.topCaption !== null &&
this.topCaption.isVisible === true) ? this.topCaption.getPreferredSize().height : 0)
};
};
/**
* Paint vertical and horizontal grid component lines
* @param {CanvasRenderingContext2D} g a HTML5 canvas 2D context
* @method paintNet
* @protected
*/
this.paintNet = function(g) {
var v = this.visibility,
i = 0,
prevWidth = g.lineWidth;
g.setColor(this.lineColor);
g.lineWidth = this.lineSize;
g.beginPath();
if (this.drawHorLines === true) {
var y = v.fr[1] - this.lineSize / 2,
x1 = v.fc[1] - this.lineSize,
x2 = v.lc[1] + this.colWidths[v.lc[0]] + this.lineSize;
for (i = v.fr[0]; i <= v.lr[0] + 1; i++) {
g.moveTo(x1, y);
g.lineTo(x2, y);
y += this.rowHeights[i] + this.lineSize;
}
}
if (this.drawVerLines === true) {
var x = v.fc[1] - this.lineSize / 2,
y1 = v.fr[1] - this.lineSize,
y2 = v.lr[1] + this.rowHeights[v.lr[0]];
for (i = v.fc[0]; i <= v.lc[0] + 1; i++) {
g.moveTo(x, y1);
g.lineTo(x, y2);
x += this.colWidths[i] + this.lineSize;
}
}
g.stroke();
g.lineWidth = prevWidth;
};
/**
* Paint grid data
* @param {CanvasRenderingContext2D} g a HTML5 canvas 2d context
* @method paintData
* @protected
*/
this.paintData = function(g) {
var y = this.visibility.fr[1],
addW = this.cellInsetsLeft + this.cellInsetsRight,
addH = this.cellInsetsTop + this.cellInsetsBottom,
ts = g.$states[g.$curState],
cx = ts.x,
cy = ts.y,
cw = ts.width,
ch = ts.height,
res = {};
for(var i = this.visibility.fr[0];i <= this.visibility.lr[0] && y < cy + ch; i++) {
if (y + this.rowHeights[i] > cy) {
var x = this.visibility.fc[1],
yv = y + this.cellInsetsTop;
for (var j = this.visibility.fc[0];j <= this.visibility.lc[0]; j++) {
if (this.isSelected(i, j) === true) {
this.paintCellSelection(g, i, j, x, y);
} else {
var bg = this.provider.getCellColor !== undefined ? this.provider.getCellColor(this, i, j)
: this.provider.background;
if (bg !== null) {
if (bg.paint !== undefined) {
bg.paint(g, x, y, this.colWidths[j], this.rowHeights[i], this);
} else {
g.setColor(bg);
g.fillRect(x, y, this.colWidths[j], this.rowHeights[i]);
}
}
}
var v = (i === this.editingRow &&
j === this.editingCol ) ? null
: this.provider.getView(this, i, j,
this.model.get(i, j));
if (v !== null) {
var xv = x + this.cellInsetsLeft,
w = this.colWidths[j] - addW,
h = this.rowHeights[i] - addH;
res.x = xv > cx ? xv : cx;
res.width = Math.min(xv + w, cx + cw) - res.x;
res.y = yv > cy ? yv : cy;
res.height = Math.min(yv + h, cy + ch) - res.y;
if (res.width > 0 && res.height > 0) {
// TODO: most likely the commented section should be removed
// if (this.isUsePsMetric !== true) {
// v.paint(g, x, y, w, h, this);
// }
//else {
var ax = this.provider.getXAlignment !== undefined ? this.provider.getXAlignment(this, i, j)
: this.defXAlignment,
ay = this.provider.getYAlignment !== undefined ? this.provider.getYAlignment(this, i, j)
: this.defYAlignment,
vw = w, // cell width
vh = h, // cell height
xx = xv,
yy = yv,
id = -1,
ps = (ax !== null || ay !== null) ? v.getPreferredSize(vw, vh)
: null;
if (ax !== null) {
xx = xv + ((ax === "center") ? Math.floor((w - ps.width) / 2)
: ((ax === "right") ? w - ps.width : 0));
vw = ps.width;
}
if (ay !== null) {
yy = yv + ((ay === "center") ? Math.floor((h - ps.height) / 2)
: ((ay === "bottom") ? h - ps.height : 0));
vh = ps.height;
}
if (xx < res.x || yy < res.y || (xx + vw) > (xv + w) || (yy + vh) > (yv + h)) {
id = g.save();
g.clipRect(res.x, res.y, res.width, res.height);
}
v.paint(g, xx, yy, vw, vh, this);
if (id >= 0) {
g.restore();
}
// }
}
}
x += (this.colWidths[j] + this.lineSize);
}
}
y += (this.rowHeights[i] + this.lineSize);
}
};
/**
* Get position marker view taking in account focus state.
* @return {zebkit.draw.View} a position marker view
* @private
* @method $getPosMarker
*/
this.$getPosMarker = function() {
return this.hasFocus() ? (this.views.marker === undefined ? null : this.views.marker)
: (this.views.offmarker === undefined ? null : this.views.offmarker);
};
/**
* Paint position marker.
* @param {CanvasRenderingContext2D} g a graphical 2D context
* @protected
* @method paintPosMarker
*/
this.paintPosMarker = function(g) {
if (this.position !== null &&
this.position.offset >= 0 )
{
var view = this.$getPosMarker(),
row = this.position.currentLine,
col = this.position.currentCol,
v = this.visibility;
// depending on position changing mode (cell or row) analyze
// whether the current position is in visible area
if (view !== null) {
if (this.navigationMode === "row") {
if (row >= v.fr[0] && row <= v.lr[0]) {
view.paint(g, v.fc[1],
this.getRowY(row),
v.lc[1] - v.fc[1] + this.colWidths[v.lc[0]],
this.rowHeights[row], this);
}
} else if (this.navigationMode === "cell") {
if (col >= v.fc[0] && col <= v.lc[0] && row >= v.fr[0] && row <= v.lr[0]) {
view.paint(g, this.getColX(col),
this.getRowY(row),
this.colWidths[col],
this.rowHeights[row], this);
}
} else if (this.navigationMode === "col") {
if (col >= v.fc[0] && col <= v.lc[0]) {
view.paint(g, this.getColX(col),
v.fr[1],
this.colWidths[col],
v.lr[1] - v.fr[1] + this.rowHeights[v.lr[0]], this);
}
}
}
}
};
/**
* Paint a selection for the given grid cell
* @param {CanvasRenderingContext2D} g a graphical 2D context
* @param {Integer} row a cell row.
* @param {Integer} col a cell column.
* @param {Integer} x a cell x location.
* @param {Integer} y a cell y location.
* @protected
* @method paintCellSelection
*/
this.paintCellSelection = function(g, row, col, x, y) {
if (this.editingRow < 0) {
var v = ui.focusManager.focusOwner === this ? this.views.focusOnSelect
: this.views.focusOffSelect;
if (v !== null && v !== undefined) {
v.paint(g, x, y, this.colWidths[col], this.rowHeights[row], this);
}
}
};
this.rPsMetric = function(){
var cols = this.getGridCols(),
rows = this.getGridRows(),
addW = this.cellInsetsLeft + this.cellInsetsRight,
addH = this.cellInsetsTop + this.cellInsetsBottom,
capPS = null,
i = 0;
if (this.colWidths === null || this.colWidths.length !== cols) {
this.colWidths = Array(cols);
for (;i < cols; i++) {
this.colWidths[i] = 0;
}
} else {
for (;i < cols; i++) {
this.colWidths[i] = 0;
}
}
if (this.rowHeights === null || this.rowHeights.length !== rows) {
this.rowHeights = Array(rows);
for (i = 0; i < rows; i++) {
this.rowHeights[i] = 0;
}
} else {
for (i = 0;i < rows; i++) {
this.rowHeights[i] = 0;
}
}
for(i = 0; i < cols; i++ ){
for(var j = 0; j < rows; j++ ){
var v = this.provider.getView(this, j, i, this.model.get(j, i));
if (v !== null){
var ps = v.getPreferredSize();
ps.width += addW;
ps.height += addH;
if (ps.width > this.colWidths[i] ) {
this.colWidths [i] = ps.width;
}
if (ps.height > this.rowHeights[j]) {
this.rowHeights[j] = ps.height;
}
} else {
if (pkg.Grid.DEF_COLWIDTH > this.colWidths [i]) {
this.colWidths [i] = pkg.Grid.DEF_COLWIDTH;
}
if (pkg.Grid.DEF_ROWHEIGHT > this.rowHeights[j]) {
this.rowHeights[j] = pkg.Grid.DEF_ROWHEIGHT;
}
}
}
}
if (this.topCaption !== null && this.topCaption.isVisible === true) {
for(i = 0;i < cols; i++ ) {
capPS = this.topCaption.getCaptionPS(i);
if (capPS > this.colWidths[i]) {
this.colWidths[i] = capPS;
}
}
}
if (this.leftCaption !== null && this.leftCaption.isVisible === true) {
for(i = 0;i < rows; i++ ) {
capPS = this.leftCaption.getCaptionPS(i);
if (capPS > this.rowHeights[i]) {
this.rowHeights[i] = capPS;
}
}
}
};
this.getPSSize = function (rowcol, b) {
if (this.isUsePsMetric === true) {
return b ? this.getRowHeight(rowcol) : this.getColWidth(rowcol);
} else {
var max = 0,
count = b ? this.getGridCols()
: this.getGridRows();
for(var j = 0;j < count; j ++ ){
var r = b ? rowcol : j,
c = b ? j : rowcol,
v = this.provider.getView(this, r, c, this.model.get(r, c));
if (v !== null){
var ps = v.getPreferredSize();
if (b) {
if (ps.height > max) {
max = ps.height;
}
} else {
if (ps.width > max) {
max = ps.width;
}
}
}
}
return max +
(b ? this.cellInsetsTop + this.cellInsetsBottom
: this.cellInsetsLeft + this.cellInsetsRight);
}
};
this.rCustomMetric = function(){
var start = 0;
if (this.colWidths !== null) {
start = this.colWidths.length;
if (this.colWidths.length !== this.getGridCols()) {
this.colWidths.length = this.getGridCols();
}
} else {
this.colWidths = Array(this.getGridCols());
}
for(; start < this.colWidths.length; start ++ ) {
this.colWidths[start] = pkg.Grid.DEF_COLWIDTH;
}
start = 0;
if (this.rowHeights !== null) {
start = this.rowHeights.length;
if (this.rowHeights.length !== this.getGridRows()) {
this.rowHeights.length = this.getGridRows();
}
} else {
this.rowHeights = Array(this.getGridRows());
}
for(; start < this.rowHeights.length; start++) {
this.rowHeights[start] = pkg.Grid.DEF_ROWHEIGHT;
}
};
/**
* Calculate number of rows to be scrolled up or down to scroll one page
* @param {Integer} d a direction. 1 for scroll down and -1 for scroll up
* @return {Integer} a page size in rows to be scrolled up or down
* @method pageSize
* @protected
*/
this.pageSize = function(d) {
this.validate();
if (this.visibility.hasVisibleCells() && this.position !== null) {
var off = this.position.offset;
if (off >= 0) {
var hh = this.visibleArea.height - this.getTopCaptionHeight(),
sum = 0,
poff = off;
for (; off >= 0 && off < this.getGridRows() && sum < hh; off += d) {
sum += this.rowHeights[off] + this.lineSize;
}
return Math.abs(poff - off);
}
}
return 0;
};
/**
* Set the given height for the specified grid row. The method has no effect
* if the grid component is forced to use preferred size metric.
* @param {Integer} row a grid row
* @param {Integer} h a height of the grid row
* @method setRowHeight
* @chainable
*/
this.setRowHeight = function(row, h) {
this.setRowsHeight(row, 1, h);
return this;
};
/**
* Set the given height for all or the specified range of rows
* @param {Integer} [row] start row
* @param {Integer} [len] number of rows whose height has to be set
* @param {Integer} h a height
* @method setRowsHeight
* @chainable
*/
this.setRowsHeight = function(row, len, h) {
if (this.isUsePsMetric === false){
if (arguments.length === 1) {
h = arguments[0];
row = 0;
len = this.getGridRows();
}
if (len !== 0) {
this.validateMetric();
var b = false;
for(var i=row; i < row + len; i++) {
if (this.rowHeights[i] !== h) {
this.psHeight_ += (h - this.rowHeights[i]);
this.rowHeights[i] = h;
b = true;
}
}
if (b === true) {
this.stopEditing(false);
this.cachedHeight = this.getTop() + this.getBottom() + this.psHeight_;
if (this.topCaption !== null && this.topCaption.isVisible === true) {
this.cachedHeight += this.topCaption.getPreferredSize().height;
}
if (this.parent !== null) {
this.parent.invalidate();
}
this.iRowVisibility(0);
this.invalidateLayout();
this.repaint();
}
}
return this;
}
};
/**
* Set the given width for the specified grid column. The method has no effect
* if the grid component is forced to use preferred size metric.
* @param {Integer} column a grid column
* @param {Integer} w a width of the grid column
* @method setColWidth
* @chainable
*/
this.setColWidth = function (col,w){
this.setColsWidth(col, 1, w);
return this;
};
/**
* Set the given width for all or the specified range of columns
* @param {Integer} [col] start column
* @param {Integer} [len] number of columns whose height has to be set
* @param {Integer} w a width
* @method setColsWidth
* @chainable
*/
this.setColsWidth = function(col, len, w){
if (this.isUsePsMetric === false){
if (arguments.length === 1) {
w = arguments[0];
col = 0;
len = this.getGridCols();
}
if (len !== 0) {
this.validateMetric();
var b = false;
for(var i = col; i < col + len; i++) {
if (this.colWidths[i] !== w){
this.psWidth_ += (w - this.colWidths[i]);
this.colWidths[i] = w;
b = true;
}
}
if (b === true) {
this.stopEditing(false);
this.cachedWidth = this.getRight() + this.getLeft() + this.psWidth_;
if (this.leftCaption !== null && this.leftCaption.isVisible === true) {
this.cachedWidth += this.leftCaption.getPreferredSize().width;
}
if (this.parent !== null) {
this.parent.invalidate();
}
this.iColVisibility(0);
this.invalidateLayout();
this.repaint();
}
}
return this;
}
};
this.matrixResized = function(target, prevRows, prevCols) {
this.clearSelect();
this.vrp();
if (this.position !== null) {
this.position.setOffset(null);
}
for(var i = 0; i < this.kids.length; i++) {
if (this.kids[i].matrixResized !== undefined) {
this.kids[i].matrixResized(target,prevRows,prevCols);
}
}
};
this.cellModified = function(target,row,col,prevValue) {
if (this.isUsePsMetric){
this.invalidate();
}
for(var i=0; i < this.kids.length; i++) {
if (this.kids[i].cellModified !== undefined) {
this.kids[i].cellModified(target,row,col, prevValue);
}
}
};
this.matrixSorted = function(target, info) {
this.clearSelect();
this.vrp();
for(var i=0; i < this.kids.length; i++) {
if (this.kids[i].matrixSorted !== undefined) {
this.kids[i].matrixSorted(target, info);
}
}
};
/**
* Set the given editor provider. Editor provider is a way to customize
* cell editing.
* @param {Object} p an editor provider
* @method setEditorProvider
* @chainable
*/
this.setEditorProvider = function(p){
if (p !== this.editors){
this.stopEditing(true);
this.editors = p;
}
return this;
};
/**
* Force to size grid columns and rows according to its preferred size
* @param {Boolean} b use true to use preferred size
* @method setUsePsMetric
* @chainable
*/
this.setUsePsMetric = function(b){
if (this.isUsePsMetric !== b){
this.isUsePsMetric = b;
this.vrp();
}
return this;
};
/**
* Set the position controller.
* @param {zebkit.util.Position} p a position controller
* @method setPosition
* @chainable
*/
this.setPosition = function(p){
if (this.position !== p){
if (this.position !== null) {
this.position.off(this);
}
/**
* Virtual cursor position controller
* @readOnly
* @attribute position
* @type {zebkit.util.Position}
*/
this.position = p;
if (this.position !== null) {
this.position.on(this);
this.position.setMetric(this);
}
this.repaint();
}
return this;
};
/**
* Set the given cell view provider. Provider is a special
* class that says how grid cells content has to be rendered,
* aligned, colored
* @param {Object} p a view provider
* @method setViewProvider
* @chainable
*/
this.setViewProvider = function(p){
if (this.provider !== p){
this.provider = p;
this.vrp();
}
return this;
};
/**
* Set the given matrix model to be visualized and controlled
* with the grid component
* @param {zebkit.data.Matrix|Array} d a model passed as an
* instance of matrix model or an array that contains
* model rows as embedded arrays.
* @method setModel
* @chainable
*/
this.setModel = function(d){
if (d !== this.model) {
this.clearSelect();
if (Array.isArray(d)) {
d = new this.clazz.Matrix(d);
}
if (this.model !== null) {
this.model.off(this);
}
this.model = d;
if (this.model !== null) {
this.model.on(this);
}
if (this.position !== null) {
this.position.setOffset(null);
}
this.vrp();
}
return this;
};
/**
* Set the given top, left, right, bottom cell paddings
* @param {Integer} p a top, left, right and bottom cell paddings
* @method setCellPadding
* @chainable
*/
this.setCellPadding = function (p){
return this.setCellPaddings(p,p,p,p);
};
/**
* Set the given top, left, right, bottom cell paddings
* @param {Integer} t a top cell padding
* @param {Integer} l a left cell padding
* @param {Integer} b a bottom cell padding
* @param {Integer} r a right cell padding
* @method setCellPaddings
* @chainable
*/
this.setCellPaddings = function (t,l,b,r){
if (t !== this.cellInsetsTop || l !== this.cellInsetsLeft ||
b !== this.cellInsetsBottom || r !== this.cellInsetsRight)
{
this.cellInsetsTop = t;
this.cellInsetsLeft = l;
this.cellInsetsBottom = b;
this.cellInsetsRight = r;
this.vrp();
}
return this;
};
/**
* Set the given color to render the grid vertical and horizontal lines
* @param {String} c a color
* @method setLineColor
* @chainable
*/
this.setLineColor = function (c){
if (c !== this.lineColor){
this.lineColor = c;
if (this.drawVerLines || this.drawHorLines) {
this.repaint();
}
}
return this;
};
/**
* Control rendering of grid lines on grid caption.
* @param {Boolean} b a flag to control lines rendering on caption
* @method setDrawCaptionLines
* @chainable
*/
this.setDrawCaptionLines = function (b){
if (b !== this.drawCaptionLines){
this.drawCaptionLines = b;
this.vrp();
}
return this;
};
/**
* Set the given grid lines size
* @param {Integer} s a size
* @method setLineSize
* @chainable
*/
this.setLineSize = function (s){
if (s !== this.lineSize){
this.lineSize = s;
this.vrp();
}
return this;
};
/**
* Start editing the given grid cell. Editing is initiated only if an editor
* provider has been set and the editor provider defines not-null UI component
* as an editor for the given cell.
* @param {Integer} row a grid cell row
* @param {Integer} col a grid cell column
* @return {Boolean} true if a cell editor has been initiated, otherwise
* returns false.
* @method startEditing
*/
this.startEditing = function(row, col){
this.stopEditing(true);
if (this.editors !== null) {
var editor = this.editors.getEditor(this, row, col,
this.getDataToEdit(row, col));
if (editor !== null){
this.editingRow = row;
this.editingCol = col;
if (editor.isPopupEditor === true) {
var p = zebkit.layout.toParentOrigin(this.getColX(col) + this.scrollManager.getSX(),
this.getRowY(row) + this.scrollManager.getSY(),
this);
editor.setLocation(p.x, p.y);
ui.makeFullyVisible(this.getCanvas(), editor);
this.editor = editor;
var $this = this;
this.editor.winOpened = function(e) {
if (e.isShown === false){
$this.stopEditing(e.source.isAccepted !== undefined ? e.source.isAccepted() : false);
}
};
ui.showModalWindow(this, editor, this);
} else {
this.add("editor", editor);
this.repaintRows(this.editingRow, this.editingRow);
}
ui.focusManager.requestFocus(editor);
return true;
}
}
return false;
};
/**
* Fetch a data from matrix model that has to be edited
* @param {Integer} row a row
* @param {Integer} col a column
* @return {Object} a matrix model data to be edited
* @method getDataToEdit
* @protected
*/
this.getDataToEdit = function (row, col){
return this.model.get(row, col);
};
/**
* Apply the given edited data to grid matrix model
* @param {Integer} row a row
* @param {Integer} col a column
* @param {Object} an edited matrix model data to be applied
* @method setEditedData
* @protected
*/
this.setEditedData = function (row,col,value){
this.model.put(row, col, value);
};
/**
* Set the grid left caption titles
* @param title* number of titles
* @method setLeftCaption
* @chainable
*/
this.setLeftCaption = function() {
if (this.leftCaption !== null) {
this.leftCaption.removeMe();
}
var a = Array.prototype.slice.call(arguments);
this.add("left", this.$hasPanelIn(a) ? new pkg.CompGridCaption(a)
: new pkg.GridCaption(a));
return this;
};
/**
* Set the grid top caption titles
* @param title* number of titles
* @method setTopCaption
* @chainable
*/
this.setTopCaption = function() {
if (this.topCaption !== null) {
this.topCaption.removeMe();
}
var a = Array.prototype.slice.call(arguments);
this.add("top", this.$hasPanelIn(a) ? new pkg.CompGridCaption(a)
: new pkg.GridCaption(a));
return this;
};
this.$hasPanelIn = function(a) {
for(var i = 0; i < a.length; i++) {
if (zebkit.instanceOf(a[i], zebkit.ui.Panel)) {
return true;
}
}
return false;
};
},
function focused() {
this.$super();
this.repaint();
},
function invalidate(){
this.$super();
this.iColVisibility(0);
this.iRowVisibility(0);
},
function kidAdded(index, ctr, c){
this.$super(index, ctr, c);
if ((ctr === null && this.topCaption === null) || "top" === ctr){
this.topCaption = c;
} else if ("editor" === ctr) {
this.editor = c;
} else if ((ctr === null && this.leftCaption === null) || "left" === ctr) {
this.leftCaption = c;
} else if ((ctr === null && this.stub === null) || "corner" === ctr){
this.stub = c;
}
},
function kidRemoved(index, c, ctr) {
this.$super(index, c, ctr);
if (c === this.editor) {
this.editor = null;
} else if (c === this.topCaption) {
this.topCaption = null;
} else if (c === this.leftCaption){
this.leftCaption = null;
} else if (c === this.stub) {
this.stub = null;
}
}
/**
* Set number of views to render different grid component elements
* @param {Object} a set of views as dictionary where key is a view
* name and the value is a view instance, string (for color, border),
* or render function. The following view elements can be passed:
*
*
* {
* "focusOnSelect" : <view to render selected row for the grid that holds focus>,
* "focusOffSelect": <view to render selected row for the grid that doesn't hold focus>
* }
*
*
* @method setViews
*/
]).events("selected");
var ui = pkg.cd("..");
/**
* Special UI panel that manages to stretch grid columns to occupy the whole panel space.
*
* ...
*
* var canvas = new zebkit.ui.zCanvas(),
* grid = new zebkit.ui.grid.Grid(100,10),
* pan = new zebkit.ui.grid.GridStretchPan(grid);
*
* canvas.root.setBorderLayout();
* canvas.root.add("center", pan);
*
* ...
*
* @constructor
* @param {zebkit.ui.grid.Grid} grid a grid component that has to be added in the panel
* @class zebkit.ui.grid.GridStretchPan
* @extends zebkit.ui.Panel
*/
pkg.GridStretchPan = Class(ui.Panel, [
function (grid) {
this.$super(this);
this.grid = grid;
this.$widths = [];
this.$prevWidth = 0;
this.$propW = -1;
this.add(grid);
},
function $prototype() {
this.$props = this.$strPs = null;
/**
* Target grid component
* @type {zebkit.ui.Grid}
* @readOnly
* @attribute grid
*/
this.grid = null;
this.calcPreferredSize = function(target) {
this.recalcPS();
return (target.kids.length === 0 ||
target.grid.isVisible === false) ? { width:0, height:0 }
: { width:this.$strPs.width,
height:this.$strPs.height };
};
this.doLayout = function(target){
this.recalcPS();
if (target.kids.length > 0){
var grid = this.grid,
left = target.getLeft(),
top = target.getTop();
if (grid.isVisible === true) {
grid.setBounds(left, top,
target.width - left - target.getRight(),
target.height - top - target.getBottom());
for(var i = 0; i < this.$widths.length; i++) {
grid.setColWidth(i, this.$widths[i]);
}
}
}
};
this.captionResized = function(src, col, pw){
console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!");
if (col < this.$widths.length - 1) {
var grid = this.grid,
w = grid.getColWidth(col),
dt = w - pw;
if (dt < 0) {
grid.setColWidth(col + 1, grid.getColWidth(col + 1) - dt);
} else {
var ww = grid.getColWidth(col + 1) - dt,
mw = this.getMinWidth();
if (ww < mw) {
grid.setColWidth(col, w - (mw - ww));
grid.setColWidth(col + 1, mw);
} else {
grid.setColWidth(col + 1, ww);
}
}
this.$propW = -1;
}
};
this.getMinWidth = function () {
return zebkit.instanceOf(this.grid.topCaption, pkg.BaseCaption) ? this.grid.topCaption.minSize
: 10;
};
this.calcColWidths = function(targetAreaW){
var grid = this.grid,
cols = grid.getGridCols(),
ew = targetAreaW - (cols + 1) * grid.lineSize,
sw = 0;
if (this.$widths.length !== cols) {
this.$widths = Array(cols);
}
for(var i = 0; i < cols; i++){
if (this.$props.length - 1 === i) {
this.$widths[i] = ew - sw;
} else {
this.$widths[i] = Math.round(ew * this.$props[i]);
sw += this.$widths[i];
}
}
};
this.recalcPS = function() {
var grid = this.grid;
if (grid !== null && grid.isVisible === true) {
// calculate size excluding padding where
// the target grid columns have to be stretched
var p = this.parent,
isScr = zebkit.instanceOf(p, ui.ScrollPan),
taWidth = (isScr ? p.width - p.getLeft() - p.getRight() - this.getRight() - this.getLeft()
: this.width - this.getRight() - this.getLeft()),
taHeight = (isScr ? p.height - p.getTop() - p.getBottom() - this.getBottom() - this.getTop()
: this.height - this.getBottom() - this.getTop());
// exclude left caption
if (this.grid.leftCaption !== null &&
this.grid.leftCaption.isVisible === true)
{
taWidth -= (this.grid.leftCaption.getPreferredSize().width + this.grid.lineSize);
}
taWidth -= (this.grid.getLeft() + this.grid.getRight());
console.log("GridStretchPan.recalcPS(): " + taWidth + "," + this.$prevWidth);
if (this.$strPs === null || this.$prevWidth !== taWidth) {
var cols = grid.getGridCols();
if (this.$propW < 0 || this.$props === null || this.$props.length !== cols) {
console.log("Grid cols: " + cols);
// calculate col proportions
if (this.$props === null || this.$props.length !== cols) {
this.$props = Array(cols);
}
this.$propW = 0;
var i = 0, w = 0;
for(i = 0; i < cols; i++){
w = grid.getColWidth(i);
console.log(" >>> col width[" + i + "] = " + w);
if (w === 0) {
w = grid.getColPSWidth(i);
}
this.$propW += w;
}
for(i = 0; i < cols; i++) {
w = grid.getColWidth(i);
if (w === 0) {
w = grid.getColPSWidth(i);
}
this.$props[i] = w / this.$propW;
}
}
this.$prevWidth = taWidth;
this.calcColWidths(taWidth);
this.$strPs = {
width : taWidth,
height: grid.getPreferredSize().height
};
// check if the calculated height is greater than
// height of the parent component and re-calculate
// the metrics if vertical scroll bar is required
// taking in account horizontal reduction because of
// the scroll bar visibility
if (isScr === true &&
p.height > 0 &&
(p.vBar !== undefined || p.vBar === null) &&
p.autoHide === false &&
taHeight < this.$strPs.height)
{
taWidth -= p.vBar.getPreferredSize().width;
this.calcColWidths(taWidth);
this.$strPs.width = taWidth;
}
}
}
};
},
function kidAdded(index,constr,l){
this.$propsW = -1;
if (l.topCaption !== null) {
l.topCaption.on(this);
}
this.scrollManager = l.scrollManager;
this.$super(index, constr, l);
},
function kidRemoved(i, l, ctr){
this.$propsW = -1;
if (l.topCaption !== null) {
l.topCaption.off(this);
}
this.scrollManager = null;
this.$super(i, l, ctr);
},
function invalidate(){
this.$strPs = null;
this.$super();
}
]);
},true);
zebkit.package("ui.design", function(pkg, Class) {
var ui = pkg.cd("..");
/**
* The package contains number of UI components that can be helpful to
* perform visual control of an UI component. You can control an UI component
* size and location.
*
* var root = (new zebkit.ui.zCanvas(400, 300)).root;
* root.setRasterLayout();
* root.setPadding(8);
*
* // Add check box component wrapped with shaper panel
* // to control the component size and location
* var ch = new zebkit.ui.Checkbox("Check-box")
* .setBounds(10, 10, 100, 30);
*
* root.add(new zebkit.ui.design.ShaperPan(ch));
*
* @class zebkit.ui.design
* @access package
*/
var CURSORS = {
left : ui.Cursor.W_RESIZE,
right : ui.Cursor.E_RESIZE,
top : ui.Cursor.N_RESIZE,
bottom : ui.Cursor.S_RESIZE,
topLeft : ui.Cursor.NW_RESIZE,
topRight : ui.Cursor.NE_RESIZE,
bottomLeft : ui.Cursor.SW_RESIZE,
bottomRight : ui.Cursor.SE_RESIZE,
center : ui.Cursor.MOVE,
none : ui.Cursor.DEFAULT
};
/**
* A designer border view. The border view visually indicates areas
* of border with different size possibilities. The border logically
* split area around a component to number of predefined areas such
* as: "center", "bottom", "right", "left", "topRight", "topLeft",
* "bottomLeft", "bottomRight", "none". See illustration below:
*
*
* |topLeft|-----------| top |-------------|topRight|
* | |
* | |
* | left | center | right |
* | |
* | |
* |bottomLeft|-------|bottom|-------------|bottomRight|
*
*
* @param {String} [color] a bordar color
* @param {Integer} [gap] a bordar gap
* @constructor
* @class zebkit.ui.design.ShaperBorder
* @extends zebkit.draw.View
*/
pkg.ShaperBorder = Class(zebkit.draw.View, [
function(color, gap) {
if (arguments.length > 0) {
this.color = color;
if (arguments.length > 1) {
this.gap = gap;
}
}
},
function $prototype() {
/**
* Border color
* @attribute color
* @type {String}
* @default "blue"
*/
this.color = "blue";
/**
* Border gap.
* @attribute gap
* @type {Number}
* @default 7
*/
this.gap = 8;
function contains(x, y, gx, gy, ww, hh) {
return gx <= x && (gx + ww) > x && gy <= y && (gy + hh) > y;
}
this.paint = function(g,x,y,w,h,d) {
if (this.color !== null) {
var cx = Math.floor((w - this.gap)/2),
cy = Math.floor((h - this.gap)/2);
g.setColor(this.color);
g.beginPath();
g.rect(x, y, this.gap, this.gap);
g.rect(x + cx, y, this.gap, this.gap);
g.rect(x, y + cy, this.gap, this.gap);
g.rect(x + w - this.gap, y, this.gap, this.gap);
g.rect(x, y + h - this.gap, this.gap, this.gap);
g.rect(x + cx, y + h - this.gap, this.gap, this.gap);
g.rect(x + w - this.gap, y + cy, this.gap, this.gap);
g.rect(x + w - this.gap, y + h - this.gap, this.gap, this.gap);
g.fill();
g.beginPath();
// very strange thing with rect() method if it called with w or h
// without decreasing with gap it is ok, otherwise moving a
// component with the border outside parent component area leaves
// traces !
//
// adding 0.5 (to center line) solves the problem with traces
g.rect(x + Math.floor(this.gap / 2) + 0.5,
y + Math.floor(this.gap / 2) + 0.5,
w - this.gap,
h - this.gap );
g.stroke();
}
};
/**
* Detect area type by the given location of the given component
* @param {zebkit.ui.Panel} target a target component
* @param {Integer} x a x coordinate
* @param {Integer} y an y coordinate
* @return {String} a detected area type
* @protected
* @method detectAt
*/
this.detectAt = function(target, x, y) {
if (contains(x, y, this.gap, this.gap, target.width - 2 * this.gap, target.height - 2 * this.gap)) {
return "center";
}
if (contains(x, y, 0, 0, this.gap, this.gap)) {
return "topLeft";
}
if (contains(x, y, 0, target.height - this.gap, this.gap, this.gap)) {
return "bottomLeft";
}
if (contains(x, y, target.width - this.gap, 0, this.gap, this.gap)) {
return "topRight";
}
if (contains(x, y, target.width - this.gap, target.height - this.gap, this.gap, this.gap)) {
return "bottomRight";
}
var mx = Math.floor((target.width - this.gap) / 2);
if (contains(x, y, mx, 0, this.gap, this.gap)) {
return "top";
}
if (contains(x, y, mx, target.height - this.gap, this.gap, this.gap)) {
return "bottom";
}
var my = Math.floor((target.height - this.gap) / 2);
if (contains(x, y, 0, my, this.gap, this.gap)) {
return "left";
}
return contains(x, y, target.width - this.gap, my, this.gap, this.gap) ? "right"
: null;
};
}
]);
pkg.DesignPan = Class(ui.Panel, [
function() {
this.statusBar = new ui.StatusBarPan();
this.inspectorPan = new ui.Panel();
this.compsPan = new ui.Panel([
function() {
this.shaper = new pkg.ShaperPan();
this.$super();
var $this = this;
this.shaper.on("moved", function(t, px, py) {
$this.repaint();
});
},
function catchInput(c) {
return zebkit.instanceOf(c, pkg.ShaperPan) === false;
},
function getPopup(t, x, y) {
var c = this.getComponentAt(x, y);
console.log(":::: " + c.clazz.$name);
if (c !== null && c !== this) {
return new ui.Menu([
"Remove ",
"To preferred size",
"-",
"Properties"
]);
}
return null;
},
function pointerClicked(e) {
var c = this.getComponentAt(e.x, e.y);
if (c !== null && c !== this && zebkit.instanceOf(c.parent, pkg.ShaperPan) === false) {
c = zebkit.layout.getDirectChild(this, c);
this.shaper.setValue(c);
this.shaper.setState("selected");
}
},
function paintOnTop(g) {
if (this.shaper.isSelected()) {
var tx = this.shaper.getValue().x ,
ty = this.shaper.getValue().y;
console.log("!!! " + tx);
for (var i = 0; i < this.kids.length; i++) {
var kid = this.kids[i];
if (this.shaper.getValue() !== kid && kid.x === tx) {
g.setColor("blue");
g.drawLine(tx, ty, tx, kid.y)
}
}
}
}
]);
this.statusBar.add(10, "(x,y) = 1");
this.statusBar.add("|");
this.statusBar.add(10, "(x,y) = 2");
this.statusBar.add("|");
this.statusBar.add(10, "(x,y) = 3");
// this.statusBar.add(10, new this.statusBar.clazz.Combo([ "Item 1", "Item 2", "Item 3"]));
this.statusBar.addCombo(10, [ "Item 1", "Item 2", "Item 3"]);
this.$super();
this.setBorderLayout();
this.add(new ui.SplitPan(this.inspectorPan, this.compsPan));
this.add("bottom", this.statusBar);
for(var i = 0; i < arguments.length; i++) {
this.compsPan.add(arguments[i]);
}
}
]);
/**
* This is UI component class that implements possibility to embeds another
* UI components to control the component size and location visually.
*
* // create canvas
* var canvas = new zebkit.ui.zCanvas(300,300);
*
* // create two UI components
* var lab = new zebkit.ui.Label("Label");
* var but = new zebkit.ui.Button("Button");
*
* // add created before label component as target of the shaper
* // component and than add the shaper component into root panel
* canvas.root.add(new zebkit.ui.design.ShaperPan(lab).properties({
* bounds: [ 30,30,100,40]
* }));
*
* // add created before button component as target of the shaper
* // component and than add the shaper component into root panel
* canvas.root.add(new zebkit.ui.design.ShaperPan(but).properties({
* bounds: [ 130,130,100,50]
* }));
*
* @class zebkit.ui.design.ShaperPan
* @constructor
* @extends zebkit.ui.Panel
* @param {zebkit.ui.Panel} [target] a target UI component whose size and location
* has to be controlled
*/
pkg.ShaperPan = Class(ui.StatePan, ui.ApplyStateProperties, [
function(t) {
this.border = new pkg.ShaperBorder();
this.$super();
if (arguments.length > 0) {
this.setValue(t);
}
},
function $prototype() {
this.layout = new zebkit.layout.BorderLayout();
/**
* Indicates if controlled component can be moved
* @attribute isMoveEnabled
* @type {Boolean}
* @default true
*/
this.isMoveEnabled = true;
/**
* Indicates if controlled component can be sized
* @attribute isResizeEnabled
* @type {Boolean}
* @default true
*/
this.isResizeEnabled = true;
/**
* Minimal possible height or controlled component
* @attribute minHeight
* @type {Integer}
* @default 12
*/
this.minHeight = 12;
/**
* Minimal possible width or controlled component
* @attribute minWidth
* @type {Integer}
* @default 12
*/
this.minWidth = 12;
/**
* Resize aspect ratio (width/height). 0 value means no aspect ratio
* has been defined.
* @attribute aspectRatio
* @type {Number}
* @default 0
*/
this.aspectRatio = 0; //2/3;
this.$cursorState = null;
this.$dragCursorState = null;
this.$px = 0;
this.$py = 0;
this.$targetParent = null;
this.catchInput = true;
this.canHaveFocus = true;
this.$detectAt = function(t, x, y) {
if (this.border !== null && this.border.detectAt !== undefined) {
return this.border.detectAt(t, x, y);
} else {
return null;
}
};
this.getCursorType = function (t, x ,y) {
this.$cursorState = this.$detectAt(t, x, y);
if (this.$cursorState === null) {
return null
} else if (this.$cursorState === "center") {
if (this.isMoveEnabled === false) {
return null;
}
} else {
if (this.isResizeEnabled === false) {
return null;
}
}
var cur = CURSORS[this.$cursorState];
return cur === undefined ? null : cur;
};
this.pointerExited = function() {
this.$dragCursorState = this.$cursorState = null;
};
/**
* Define key pressed events handler
* @param {zebkit.ui.event.KeyEvent} e a key event
* @method keyPressed
*/
this.keyPressed = function(e) {
if (this.kids.length > 0){
var dx = (e.code === "ArrowLeft" ? -1 : (e.code === "ArrowRight" ? 1 : 0)),
dy = (e.code === "ArrowUp" ? -1 : (e.code === "ArrowDown" ? 1 : 0)),
w = this.width + dx,
h = this.height + dy,
x = this.x + dx,
y = this.y + dy;
if (e.shiftKey) {
var minW = this.border !== null ? this.border.getLeft() + this.border.getRight()
: 10,
minH = this.border !== null ? this.border.getTop() + this.border.getBottom()
: 10;
if (this.isResizeEnabled === true && w > minW && h > minH) {
this.setSize(w, h);
}
} else if (this.isMoveEnabled) {
if (x + this.width/2 > 0 &&
y + this.height/2 > 0 &&
x < this.parent.width - this.width/2 &&
y < this.parent.height - this.height/2 )
{
this.setLocation(x, y);
}
}
}
};
/**
* Define pointer drag started events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragStarted
*/
this.pointerDragStarted = function(e) {
this.$dragCursorState = null;
if (this.$cursorState !== null && (this.isResizeEnabled || this.isMoveEnabled)) {
if ((this.isMoveEnabled && this.$cursorState === "center") ||
(this.isResizeEnabled && this.$cursorState !== "center") )
{
this.$px = e.absX;
this.$py = e.absY;
this.$dragCursorState = this.$cursorState;
}
}
};
/**
* Define pointer dragged events handler
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragged
*/
this.pointerDragged = function(e){
if (this.$dragCursorState !== null) {
var dy = (e.absY - this.$py),
dx = (e.absX - this.$px),
s = this.$dragCursorState;
this.$px = e.absX;
this.$py = e.absY;
if (s === "center") {
this.setLocation(this.x + dx, this.y + dy);
} else {
var m = { top : (s === "top" || s === "topLeft" || s === "topRight" ) ? 1 : 0,
left : (s === "left" || s === "topLeft" || s === "bottomLeft" ) ? 1 : 0,
right : (s === "right" || s === "topRight" || s === "bottomRight") ? 1 : 0,
bottom : (s === "bottom" || s === "bottomRight" || s === "bottomLeft" ) ? 1 : 0 },
nh = this.height - dy * m.top + dy * m.bottom,
nw = this.width - dx * m.left + dx * m.right;
this.setBounds(this.x + m.left * dx, this.y + m.top * dy, nw, nh);
}
}
};
this.pointerDragEnded = function(e) {
this.$px = this.$py = 0;
this.$dragCursorState = null;
};
/**
* Set the border color for the given focus state.
* @param {String} id a focus state. Use "focuson" or "focusoff" as the
* parameter value
* @param {String} color a border color
* @method setBorderColor
* @chainable
*/
this.setBorderColor = function(color) {
if (this.border !== null && this.border.color !== color) {
this.border.color = color;
this.repaint();
}
return this;
};
/**
* Get a component whose shape is controlled
* @return {zebkit.ui.Panel} a controlled component
* @method getValue
*/
this.getValue = function() {
if (this.kids.length > 0) {
var t = this.byConstraints(null) || this.byConstraints("center");
return t;
} else {
return null;
}
};
/**
* Set the controlled with the shape controller component.
* @param {zebkit.ui.Panel} v a component to be controlled
* @method setValue
* @chainable
*/
this.setValue = function(v) {
var ov = this.getValue();
if (ov !== v) {
var top = this.getTop(),
left = this.getLeft();
if (ov !== null) {
ov.removeMe();
// attach the target back to its parent
if (this.$targetParent !== null) {
this.removeMe();
ov.setBounds(this.x + ov.x, this.y + ov.y, ov.width, ov.height);
this.$targetParent.add(ov);
}
}
this.$targetParent = null;
if (v !== null) {
if (v.parent !== null) {
// detach the shaper from old parent
if (this.parent !== null && this.parent !== v.parent) {
this.removeMe();
}
// save target parent and detach it from it
this.$targetParent = v.parent;
v.removeMe();
// add the shaper to the target parent
this.$targetParent.add(this);
}
// calculate location and size the shaper requires
// taking in account gaps
if (v.width === 0 || v.height === 0) {
v.toPreferredSize();
}
// set shaper bounds
this.setBounds(v.x - left, v.y - top,
v.width + left + this.getRight(),
v.height + top + this.getBottom());
this.add(v);
}
}
return this;
};
this.isSelected = function() {
return this.state === "selected";
};
},
function setSize(w, h) {
if (this.aspectRatio !== 0) {
w = Math.floor(h * this.aspectRatio);
}
if (w >= this.minWidth && h >= this.minHeight && (this.width !== w || this.height !== h)) {
var pw = this.width,
ph = this.height;
this.$super(w, h);
this.fire("sized", [ this, (w - pw), (h - ph) ]);
}
return this;
},
function setLocation(x, y) {
if (this.x !== x || this.y !== y) {
var px = this.x,
py = this.y;
this.$super(x, y);
this.fire("moved", [ this, (x - px), (y - py) ]);
}
return this;
},
function setState(s) {
var prev = this.state;
this.$super(s);
if (prev !== this.state) {
if (this.state === "selected") {
this.fire("selected", [ this, true ]);
} else if (prev === "selected") {
this.fire("selected", [ this, false ]);
}
}
return this;
},
function focused() {
this.$super();
this.setState(this.hasFocus() ? "selected"
: "unselected" );
},
function kidRemoved(i, kid, ctr) {
if (ctr === null || ctr === "center") {
this.fire("detached", [this, kid]);
}
},
function kidAdded(i, constr, d) {
if (constr === null || constr === "center") {
this.fire("attached", [ this, d ]);
}
}
]).events("selected", "sized", "moved", "attached", "detached");
/**
* Special tree model implementation that represents zebkit UI component
* hierarchy as a simple tree model.
* @param {zebkit.ui.Panel} target a root UI component
* @constructor
* @class zebkit.ui.design.FormTreeModel
* @extends zebkit.data.TreeModel
*/
pkg.FormTreeModel = Class(zebkit.data.TreeModel, [
function (target) {
this.$super(this.buildModel(target, null));
},
function $prototype() {
/**
* Build tree model by the given UI component.
* @param {zebkit.ui.Panel} comp a component
* @return {zebkit.data.Item} a root tree model item
* @method buildModel
*/
this.buildModel = function(comp, root){
var b = this.exclude !== undefined && this.exclude(comp),
item = b ? root : this.createItem(comp);
for(var i = 0; i < comp.kids.length; i++) {
var r = this.buildModel(comp.kids[i], item);
if (r !== null) {
r.parent = item;
item.kids.push(r);
}
}
return b ? null : item;
};
/**
* Find a tree item that relates to the given component.
* @param {zebkit.ui.Panel} c a component.
* @return {zebkit.data.Item} a tree item.
* @method itemByComponent
*/
this.itemByComponent = function (c, r) {
if (arguments.length < 2) {
r = this.root;
}
if (r.comp === c) {
return c;
} else {
for(var i = 0; i < r.kids.length; i++) {
var item = this.itemByComponent(c, r.kids[i]);
if (item !== null) {
return item;
}
}
return null;
}
};
this.createItem = function(comp){
var name = comp.clazz.$name;
if (name === undefined) {
name = comp.toString();
}
var index = name.lastIndexOf('.'),
item = new zebkit.data.Item(index > 0 ? name.substring(index + 1) : name);
item.comp = comp;
return item;
};
}
])
}, true);
zebkit.package("web", function(pkg, Class) {
'use strict';
/**
* Web specific stuff to provide abstracted method to work in WEB context.
* @class zebkit.web
* @access package
*/
/**
* Device ratio.
* @attribute $deviceRatio
* @readOnly
* @private
* @type {Number}
*/
pkg.$deviceRatio = window.devicePixelRatio !== undefined ? window.devicePixelRatio
: (window.screen.deviceXDPI !== undefined ? window.screen.deviceXDPI / window.screen.logicalXDPI // IE
: 1);
pkg.$windowSize = function() {
// iOS retina devices can have a problem with performance
// in landscape mode because of a bug (full page size is
// just 1 pixels column more than video memory that can keep it)
// So, just make width always one pixel less.
return {
width : window.innerWidth, // - 1,
height: window.innerHeight
};
};
/**
* Calculates view port of a browser window
* @return {Object} a browser window view port size.
*
* ```json
* {
* width : {Integer},
* height: {Integer}
* }
* ```
*
* @method $viewPortSize
* @for zebkit.web
* @private
*/
pkg.$viewPortSize = function() {
var ws = pkg.$windowSize(),
body = document.body,
css = [ "margin-left", "margin-right", "margin-top", "margin-bottom",
"padding-left", "padding-right", "padding-top", "padding-bottom",
"border-left-width", "border-right-width", "border-top-width", "border-bottom-width"];
for(var i = 0; i < css.length;) {
ws.width -= (pkg.$measure(body, css[i++]) + pkg.$measure(body, css[i++]));
ws.height -= (pkg.$measure(body, css[i++]) + pkg.$measure(body, css[i++]));
}
return ws;
};
pkg.$measure = function(e, cssprop) {
var value = window.getComputedStyle(e, null).getPropertyValue(cssprop);
return (value === null || value === '') ? 0
: parseInt(/(^[0-9\.]+)([a-z]+)?/.exec(value)[1], 10);
};
/**
* Tests if the given DOM element is in document
* @private
* @param {Element} element a DOM element
* @return {Boolean} true if the given DOM element is in document
* @method $contains
* @for zebkit.web
*/
pkg.$contains = function(element) {
// TODO: not sure it is required, probably it can be replaced with document.body.contains(e);
return (document.contains !== undefined && document.contains(element)) ||
(document.body.contains !== undefined && document.body.contains(element)); // !!! use body for IE
};
/**
* Test if the given page coordinates is inside the given element
* @private
* @param {Element} element a DOM element
* @param {Number} pageX an x page coordinate
* @param {Number} pageY an y page coordinate
* @return {Boolean} true if the given point is inside the specified DOM element
* @method $isInsideElement
*/
pkg.$isInsideElement = function(element, pageX, pageY) {
var r = element.getBoundingClientRect();
return r !== null &&
pageX >= r.left &&
pageY >= r.top &&
pageX <= r.right - 1 &&
pageY <= r.bottom - 1 ;
};
var $focusInOutSupported = (function() {
var support = false,
parent = document.lastChild,
a = document.createElement('a');
a.href = '#';
a.setAttribute("style", "position:fixed;left:-99em;top:-99em;");
a.addEventListener('focusin', function() {
support = true;
});
parent.appendChild(a).focus();
parent.removeChild(a);
return support;
})();
pkg.$focusin = function(element, f, b) {
return element.addEventListener($focusInOutSupported ? "focusin" : "focus", f, b);
};
pkg.$focusout = function(element, f, b) {
return element.addEventListener($focusInOutSupported ? "focusout" : "blur", f, b);
};
pkg.$eventsBlackHole = function(e) {
e.preventDefault();
e.stopPropagation();
};
/**
* Creates HTML element that "eats" (doesn't propagate and prevents default) all input (touch, mouse, key)
* events that it gets.
* @return {HTMLElement} a created HTML element.
* @method $createBlockedElement
* @protected
* @for zebkit.web
*/
pkg.$createBlockedElement = function() {
var be = document.createElement("div");
be.style.height = be.style.width = "100%";
be.style.left = be.style.top = "0px";
be.style.position = "absolute";
be.style["z-index"] = "100000";
be.setAttribute("zebkit", "blockedElement");
be.onmouseup = be.onmousedown = be.onmouseout =
be.onmouseover = be.onmousemove = be.onkeydown =
be.onkeypress = be.onkeyup = pkg.$eventsBlackHole;
var events = [ "touchstart", "touchend", "touchmove",
"pointerdown", "pointerup", "pointermove",
"pointerenter", "pointerleave" ];
for(var i = 0 ; i < events.length ; i++ ) {
be.addEventListener(events[i], pkg.$eventsBlackHole, false);
}
return be;
};
/**
* Extend standard 2D HTML Canvas context instance with the given set of methods.
* If new methods clash with already existent 2D context method the old one is overwritten
* with new one and old method is saved using its name prefixed with "$" character
* @param {CanvasRenderingContext2D} ctx a 2D HTML Canvas context instance
* @param {Array} methods list of methods to be added to the context
* @method $extendContext
* @private
*/
pkg.$extendContext = function(ctx, methods) {
for(var k in methods) {
if (k === "$init") {
methods[k].call(ctx);
} else {
var old = ctx[k];
if (old !== undefined) {
var kk = "$" + k;
if (ctx[kk] === undefined) {
ctx[kk] = old;
}
}
ctx[k] = methods[k];
}
}
};
/**
* Adjusts the given HTML Canvas element to the required size that takes in account device DPI.
* Extend the canvas 2D context with extra methods and variables that are used with zebkit UI
* engine.
* @param {HTMLCanvasElement} c a HTML canvas element
* @param {Integer} w a required width of the given canvas
* @param {Integer} h a required height of the given canvas
* @param {Boolean} [forceResize] flag to force canvas resizing even if the canvas has identical width and height.
* It is required to re-create canvas 2D context to work properly.
* @return {CanvasRenderingContext2D} a 2D context of the canvas element
* @method $canvas
* @protected
* @for zebkit.web
*/
pkg.$canvas = function(c, w, h, forceResize) {
// fetch current CSS size of canvas
var cs = window.getComputedStyle(c, null),
cw = parseInt(cs.getPropertyValue("width"), 10),
ch = parseInt(cs.getPropertyValue("height"), 10),
ctx = c.getContext("2d"),
updateRatio = false;
// if CSS width or height has not been set for the canvas
// it has to be done, otherwise scaling on hi-DPI screen
// will not work
if (isNaN(parseInt(c.style.width ))||
isNaN(parseInt(c.style.height)) )
{
c.style.width = "" + cw + "px";
c.style.height = "" + ch + "px";
updateRatio = true;
}
// setup new canvas CSS size if appropriate width and height
// parameters have been passed and they don't match current CSS
// width and height
if (arguments.length > 1) {
if (cw !== w || ch !== h) {
c.style.width = "" + w + "px";
c.style.height = "" + h + "px";
updateRatio = true;
}
cw = w;
ch = h;
}
// canvas 2D context is singleton so check if the
// context has already been modified to prevent
// redundancy
if (ctx.$ratio === undefined) {
ctx.$ratio = (ctx.webkitBackingStorePixelRatio || // backing store ratio
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
ctx.$getImageData = ctx.getImageData;
ctx.$scale = ctx.scale; // save original method if at some stage
// it will be overridden (zebkit does it)
// only original method has to be used to
// adjust canvas to screen DPI
if (pkg.$deviceRatio != ctx.$ratio) {
var r = pkg.$deviceRatio / ctx.$ratio;
ctx.getImageData= function(x, y, w, h) {
return this.$getImageData(x * r, y * r, w, h);
};
}
// populate extra method to 2D context
pkg.$extendContext(ctx, zebkit.draw.Context2D);
}
ctx.$scaleRatio = 1;
ctx.$scaleRatioIsInt = true;
// take in account that canvas can be visualized on
// Retina screen where the size of canvas (backstage)
// can be less than it is real screen size. Let's
// make it match each other
if (ctx.$ratio != pkg.$deviceRatio) {
var ratio = ctx.$ratio !== 1 ? pkg.$deviceRatio / ctx.$ratio
: pkg.$deviceRatio;
if (Number.isInteger(ratio)) {
cw = cw * ratio;
ch = ch * ratio;
} else {
if (pkg.config("approximateRatio") === true) {
ratio = Math.round(ratio);
cw = cw * ratio;
ch = ch * ratio;
} else {
// adjust ratio
// -- get adjusted with ratio width
// -- floor it and re-calculate ratio again
// -- the result is slightly corrected ratio that fits better
// to keep width as integer
ratio = Math.floor(cw * ratio) / cw;
cw = Math.floor(cw * ratio);
ch = Math.floor(ch * ratio);
ctx.$scaleRatioIsInt = Number.isInteger(ratio);
}
}
ctx.$scaleRatio = ratio;
// adjust canvas size if it is necessary
if (c.width != cw || c.height != ch || updateRatio === true || forceResize === true) {
c.width = cw;
c.height = ch;
ctx.$scale(ratio, ratio);
}
} else if (c.width != cw || c.height != ch || forceResize === true) { // adjust canvas size if it is necessary
c.width = cw;
c.height = ch;
}
// TODO: top works not good in FF and it is better don't use it
// So, ascent has to be taking in account as it was implemented
// before
if (ctx.textBaseline !== "top" ) {
ctx.textBaseline = "top";
}
return ctx;
};
// zebkit dependencies:
// -- zebkit.ui.event.Clipboard
// -- zebkit.web.$fetchKeyCode
//
//
// IE doesn't allow standard window.Event instantiation
// this is a workaround to avoid the problem
function CustomEvent(event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
function $dupKeyEvent(e, id, target) {
var k = new CustomEvent(id);
k.keyCode = e.keyCode;
k.key = e.key;
k.code = e.code;
// TODO: cannot be set in strict mode and most likely it is set with dispactEvent() function
// properly
// k.target = target;
k.ctrlKey = e.ctrlKey;
k.altKey = e.altKey;
k.shiftKey = e.shiftKey;
k.metaKey = e.metaKey;
k.which = e.which;
// TODO: cannot be set in strict mode and most likely it is set with dispactEvent() function
// properly
// k.timeStamp = e.timeStamp;
return k;
}
/**
* Clipboard support class. The class is light abstraction that helps to perform
* textual data exchange via system (browser) clipboard. Browsers have different approaches
* and features regarding clipboard implementation and clipboard API. This class
* hides the native specific and provides simple way to exchange data via clipboard.
* @param {String} [triggerKeyCode] a key code that starts triggering clipboard copy
* paste actions. It depends on platform. On Linux "Control" + <xxx> combination
* should be used, but on Mac OSX "MetaLeft" + xxx.
* To handle copy, paste and cut event override the following methods:
* - **copy** "clipCopy(focusOwnerComponent, data)"
* - **paste** "clipPaste(focusOwnerComponent, data)"
* - **cut** "clipCut(focusOwnerComponent, data)"
* @constructor
* @class zebkit.web.Clipboard
* @extends zebkit.ui.event.Clipboard
*/
pkg.Clipboard = Class(zebkit.ui.event.Clipboard, [
function(triggerKeyCode) {
if (document.getElementById(this.clazz.id) !== null) {
throw new Error("Duplicated clipboard element");
}
if (arguments.length > 0 && triggerKeyCode !== null) {
this.triggerKeyCode = triggerKeyCode;
} else {
this.triggerKeyCode = zebkit.isMacOS ? "MetaLeft"
: "Control";
}
if (this.triggerKeyCode !== null) {
this.$clipboard = document.createElement("textarea");
this.$clipboard.setAttribute("style", "display:none;position:fixed;left:-99em;top:-99em;");
this.$clipboard.setAttribute("id", this.clazz.id);
this.$element = null;
var $this = this;
window.addEventListener("keydown", function(e) {
var dest = $this.getDestination();
if (dest !== null) {
if (dest.clipCopy !== undefined || dest.clipPaste !== undefined) {
if (zebkit.web.$fetchKeyCode(e) === $this.triggerKeyCode) {
// value has to be set, otherwise some browsers (Safari) do not generate
// "copy" event
$this.$on("1");
}
}
}
}, true);
this.$clipboard.onkeydown = function(ee) {
$this.$element.dispatchEvent($dupKeyEvent(ee, 'keydown', this.$element));
$this.$clipboard.value = "1";
$this.$clipboard.select();
};
this.$clipboard.onkeyup = function(ee) {
if (zebkit.web.$fetchKeyCode(ee) === $this.triggerKeyCode) {
$this.$clipboard.style.display = "none";
$this.$element.focus();
}
$this.$element.dispatchEvent($dupKeyEvent(ee,'keyup', $this.$element));
};
this.$clipboard.onfocus = function(e) {
if ($this.$element === null && e.relatedTarget !== null) {
$this.$element = e.relatedTarget;
}
};
this.$clipboard.onblur = function() {
this.value = "";
this.style.display = "none";
//!!! pass focus back to canvas
// it has to be done for the case when cmd+TAB (switch from browser to
// another application)
$this.$element.focus();
};
this.$clipboard.oncopy = function(ee) {
var dest = $this.getDestination();
if (dest !== null &&
dest.clipCopy !== undefined)
{
var v = dest.clipCopy();
$this.$clipboard.value = (v === null || v === undefined ? "" : v);
$this.$clipboard.select();
if ($this.clipCopy !== undefined) {
$this.clipCopy(v, $this.$clipboard.value);
}
}
};
this.$clipboard.oncut = function(ee) {
var dest = $this.getDestination();
if (dest !== null && dest.cut !== undefined) {
$this.$clipboard.value = dest.cut();
$this.$clipboard.select();
if ($this.clipCut !== undefined) {
$this.clipCut(dest, $this.$clipboard.value);
}
}
};
if (zebkit.isFF === true) {
this.$clipboard.addEventListener("input", function(ee) {
var dest = $this.getDestination();
if (dest !== null && dest.clipPaste !== undefined) {
dest.clipPaste($this.$clipboard.value);
if ($this.clipPaste !== undefined) {
$this.clipPaste(dest, $this.$clipboard.value);
}
}
}, false);
} else {
this.$clipboard.onpaste = function(ee) {
var dest = $this.getDestination();
if (dest !== null && dest.clipPaste !== undefined) {
var txt = (ee.clipboardData === undefined) ? window.clipboardData.getData('Text') // IE
: ee.clipboardData.getData('text/plain');
dest.clipPaste(txt);
if ($this.clipPaste !== undefined) {
$this.clipPaste(dest, txt);
}
}
$this.$clipboard.value = "";
};
}
document.body.appendChild(this.$clipboard);
}
},
function $clazz() {
this.id = "zebkitClipboardBuffer";
},
function $prototype() {
/**
* Clipboard trigger key code.
* @private
* @readOnly
* @attribute triggerKeyCode
* @type {String}
*/
this.triggerKeyCode = null;
/**
* Write the given content into clipboard. This method not necessary work on
* all browsers by default. Many browsers issue security restrictions regarding
* clipboard data manipulation.
* @param {String} txt a content
* @method write
*/
this.write = function(txt) {
try {
this.$on(txt);
if (document.execCommand !== undefined && document.execCommand("copy") !== true) {
throw new Error("Unsupported 'copy' clipboard command");
}
} finally {
this.$off();
}
};
/**
* Read clipboard content. This method not necessary work on
* all browsers by default. Many browsers issue security restrictions regarding
* clipboard data manipulation.
* @return {String} a clipboard content.
* @method read
*/
this.read = function() {
try {
var clip = this.$on("");
if (document.execCommand !== undefined && document.execCommand("paste", null, null)) {
return clip.value;
} else {
throw new Error("Unsupported 'paste' clipboard command");
}
} finally {
this.$off();
}
};
/**
* Return focus from a hidden element back to initial one.
* @private
* @method $off
*/
this.$off = function() {
if (this.$clipboard.style.display !== "none") {
this.$clipboard.value = "";
this.$clipboard.style.display = "none";
//!!! pass focus back to canvas
// it has to be done for the case when cmd+TAB (switch from browser to
// another application)
this.$element.focus();
}
};
/**
* Pass focus to hidden html element to catch input.
* @private
* @method $on
*/
this.$on = function(txt) {
this.$off();
this.$element = document.activeElement;
this.$clipboard.style.display = "block";
// value has to be set, otherwise some browsers (Safari) do not generate
// "copy" event
this.$clipboard.value = arguments.length > 0 ? txt : "1";
this.$clipboard.select();
this.$clipboard.focus();
return this.$clipboard;
};
}
]);
new pkg.Clipboard();
// TODO List:
// [+] add pressure level field to pointer events
// [-] group field
// [+] round for pageX/pageY
// [+] double click
// [+] check if button field is required or can be removed from pointer event
// [+] support global status keeping and updating (ctrl/alt/shift)
// [+] "lmouse" and "rmouse" should be constants
// [-] list of active touches or pointers have to be available
// [-] meX/meY -> (x, y) ?
if (pkg.doubleClickDelta === undefined) {
pkg.doubleClickDelta = 280;
}
var PI4 = Math.PI/4, // used to calculate touch event gamma (direction
PI4_3 = PI4 * 3, // in polar coordinate)
$enteredElement = null,
$tmpWinMouseMoveListener = null,
$lastPointerReleased = null,
$pointerPressedEvents = {}, // collect all pointer pressed events
LMOUSE = "lmouse",
RMOUSE = "rmouse";
/**
* Normalized pointer event that is fired with mouse, touch, pen devices.
* @class zebkit.web.PointerEvent
* @extends zebkit.ui.event.PointerEvent
* @constructor
*/
pkg.PointerEvent = Class(zebkit.ui.event.PointerEvent, [
function $prototype() {
this.isAction = function() {
return this.identifier !== RMOUSE && this.touchCounter === 1;
};
this.$fillWith = function(identifier, e) {
this.pageX = Math.round(e.pageX);
this.pageY = Math.round(e.pageY);
this.target = e.target;
this.identifier = identifier;
this.altKey = e.altKey !== undefined ? e.altKey : false;
this.shiftKey = e.shiftKey !== undefined ? e.shiftKey : false;
this.ctrlKey = e.ctrlKey !== undefined ? e.ctrlKey : false;
this.metaKey = e.metaKey !== undefined ? e.metaKey : false;
this.pressure = e.pressure !== undefined ? e.pressure : 0.5;
};
this.getTouches = function() {
var touches = [], i = 0;
for(var k in pkg.$pointerPressedEvents) {
var pe = pkg.$pointerPressedEvents[k];
touches[i++] = {
pageX : pe.pageX,
pageY : pe.pageY,
identifier : pe.identifier,
target : pe.target,
pressure : pe.pressure,
pointerType: pe.stub.pointerType
};
}
return touches;
};
}
]);
var ME_STUB = new pkg.PointerEvent(), // instance of mouse event
TOUCH_STUB = new pkg.PointerEvent(), // instance of touch event
POINTER_STUB = new pkg.PointerEvent(); // instance of pointer event
ME_STUB.pointerType = "mouse";
TOUCH_STUB.pointerType = "touch";
POINTER_STUB.pointerType = "unknown"; // type of pointer events have to be copied from original WEB PointerEvent
// !!!
// global mouse move events handler (registered by drag out a canvas surface)
// has to be removed every time a mouse button released with the given function
function $cleanDragFix() {
if ($tmpWinMouseMoveListener !== null &&
$pointerPressedEvents.hasOwnProperty(LMOUSE) === false &&
$pointerPressedEvents.hasOwnProperty(RMOUSE) === false )
{
window.removeEventListener("mousemove", $tmpWinMouseMoveListener, true);
$tmpWinMouseMoveListener = null;
return true;
}
return false;
}
function isIn(t, id) {
for(var i = 0; i < t.length; i++) {
if (t[i].identifier === id) {
return true;
}
}
return false;
}
/**
* Pointer event unifier is special class to normalize input events from different pointer devices (like
* mouse, touch screen, pen etc) and various browsers. The class transform all the events to special
* neutral pointer event.
* @param {DOMElement} element a DOM element to normalize pointer events
* @param {Object} destination a destination object that implements number of pointer events
* handlers:
*
* {
* $pointerPressed : function(e) { ... },
* $pointerReleased : function(e) { ... },
* $pointerClicked : function(e) { ... },
* $pointerMoved : function(e) { ... },
* $pointerDragStarted : function(e) { ... },
* $pointerDragged : function(e) { ... },
* $pointerDragEnded : function(e) { ... }
* }
*
*
* @constructor
* @class zebkit.web.PointerEventUnifier
*/
pkg.PointerEventUnifier = Class([
function $clazz() {
// !!!!
// TODO: this method works only for mouse (constant of mouse event ids is in)
// not clear if it is ok
//
// the document mouse up happens when we drag outside a canvas.
// in this case canvas doesn't catch mouse up, so we have to do it
// by global mouseup handler
document.addEventListener("mouseup", function(e) {
// ignore any mouse buttons except left
// and right buttons
if (e.button === 0 || e.button === 2) {
var id = e.button === 0 ? LMOUSE : RMOUSE;
// !!!!
// Check if the event target is not the canvas itself
// On desktop "mouseup" event is generated only if
// you drag mouse outside a canvas and than release a mouse button
// At the same time in Android native browser (and may be other mobile
// browsers) "mouseup" event is fired every time you touch
// canvas or any other element. So check if target is not a canvas
// before doing releasing, otherwise it brings to error on mobile
if ($pointerPressedEvents.hasOwnProperty(id)) {
var mp = $pointerPressedEvents[id];
if (mp.$adapter.element !== e.target && mp.$adapter.element.contains(e.target) === false) {
try {
mp.$adapter.$UP(id, e, ME_STUB);
} finally {
if ($enteredElement !== null) {
$enteredElement = null;
mp.$adapter.destination.$pointerExited(ME_STUB);
}
}
}
}
}
}, false); // false is important since if mouseUp happens on
// canvas the canvas gets the event first and than stops
// propagating to prevent it
},
function $prototype() {
this.$timer = null;
this.$queue = [];
this.$touchedAt = function(pageX, pageY, d) {
var lx = pageX - d,
ty = pageY - d,
rx = pageX + d,
by = pageY + d;
for(var k in $pointerPressedEvents) {
if (k !== LMOUSE && k !== RMOUSE) {
var e = $pointerPressedEvents[k];
if (e.pageX >= lx && e.pageY >= ty && e.pageX <= rx && e.pageY <= by) {
return true;
}
}
}
return false;
};
this.$DRAG = function(id, e, stub) {
// a pointer touched has been pressed and pressed target zebkit component exists
// emulate mouse dragging events if mouse has moved on the canvas where mouse
// pressed event occurred
if ($pointerPressedEvents.hasOwnProperty(id)) {
// get appropriate pointerPressed event that has occurred before
var mp = $pointerPressedEvents[id];
// ignore moved if there still start events that are waiting for to be fired
if (mp.$adapter.element === this.element) {
// target component exists and mouse cursor moved on the same
// canvas where mouse pressed occurred
if (this.$timer === null) { // ignore drag for if the queue of touches is not empty
stub.$fillWith(id, e);
var dx = stub.pageX - mp.pageX,
dy = stub.pageY - mp.pageY,
d = mp.direction;
// accumulate shifting of pointer
mp.$adx += dx;
mp.$ady += dy;
// update stored touch coordinates with a new one
mp.pageX = stub.pageX;
mp.pageY = stub.pageY;
// we can recognize direction only if move was not too small
if (Math.abs(mp.$adx) > 4 || Math.abs(mp.$ady) > 4) {
// compute gamma, this is corner in polar coordinate system
var gamma = Math.atan2(mp.$ady, mp.$adx);
// using gamma we can figure out direction
if (gamma > -PI4) {
d = (gamma < PI4) ? "right" : (gamma < PI4_3 ? "bottom" : "left");
} else {
d = (gamma > -PI4_3) ? "top" : "left";
}
mp.direction = d;
// clear accumulated shift
mp.$ady = mp.$adx = 0;
mp.gamma = gamma;
}
stub.direction = mp.direction;
stub.dx = dx;
stub.dy = dy;
try {
if (mp.isDragged === false) {
this.destination.$pointerDragStarted(stub);
}
if (mp.isDragged === false || dx !== 0 || dy !== 0) {
this.destination.$pointerDragged(stub);
}
} finally {
mp.isDragged = true;
}
}
} else {
mp.$adapter.$DRAG(id, e, stub);
}
}
};
this.$fireUP = function(id, e, mp, stub, destination) {
try {
// store coordinates and target
stub.$fillWith(id, e);
// TODO: uncomment it and replace with sub or so
//if (tt.group != null) tt.group.active = false;
// add press coordinates what can help to detect source
// of the event
stub.pressPageX = mp.pressPageX;
stub.pressPageY = mp.pressPageY;
// fire dragged or clicked
if (mp.isDragged === true) {
destination.$pointerDragEnded(stub);
} else {
// TODO: sometimes browser scrolls page during the click
// to detect it we have to check pageX / pageY coordinates with
// initial one to suppress not valid pointer clicked events
if (mp.pressPageY === stub.pageY && mp.pressPageX === stub.pageX) {
if ($lastPointerReleased !== null &&
$lastPointerReleased.identifier === id &&
(new Date().getTime() - $lastPointerReleased.time) <= pkg.doubleClickDelta)
{
destination.$pointerDoubleClicked(stub);
} else {
if (mp.group === stub.touchCounter) { // TODO: temporary solution
destination.$pointerClicked(stub);
}
}
}
}
// always complete pointer pressed with appropriate
// release event
destination.$pointerReleased(stub);
} finally {
// clear handled pressed and dragged state
if (stub.touchCounter > 0) {
stub.touchCounter--;
}
$lastPointerReleased = $pointerPressedEvents.hasOwnProperty(id) ? $pointerPressedEvents[id] : null;
delete $pointerPressedEvents[id];
// remove global move listener if necessary
$cleanDragFix();
}
};
// Possible cases of mouse up events:
//
// a) +-------------+ b) +----------------+ c) +---------------+
// | E | | E +----+ | | E +-----|
// | p--u | | | p--|-u | | | p--|-u
// | | | +----+ | | +-----|
// +-------------+ +----------------+ +---------------+
// (out to document/body) (out from kid to element) (out from kid to document)
//
// d) +--------+--------+ e) +----------+----------+ f) +---------+-------+
// | E | | | E +-----|-----+ | | E +-----| |
// | p--|--u | | | p---|--u | | | | p--|-u |
// | | | | +-----|-----+ | | +-----| |
// +--------+--------+ +----------+----------+ +---------+-------+
// (out from element to (out from kid of element (out from kid element
// other element) to kid of another element) to another element)
// Contract:
// -- handle only mouse events whose destination is the passed element
// -- does stop propagation if event has been handled
// -- clear drag fix ?
this.$UP = function(id, e, stub) {
// remove timer if it has not been started yet since we already have
// got UP event and have to fire pressed events from queue with the
// UP handler
if (this.$timer !== null) {
clearTimeout(this.$timer);
this.$timer = null;
}
// test if the pressed event for the given id has not been fired yet
var isPressedInQ = false;
for(var i = 0; i < this.$queue.length; i++) {
if (this.$queue[i].identifier === id) {
isPressedInQ = true;
break;
}
}
// fire collected in queue pressed events
this.$firePressedFromQ();
// check if a pointer state is in pressed state
if ($pointerPressedEvents.hasOwnProperty(id)) {
// get pointer pressed state for the given id
var mp = $pointerPressedEvents[id];
// mouse up can happen in another element than
// mouse down occurred. let the original element
// (where mouse down is happened) to handle it
if (this.element !== mp.$adapter.element) {
$enteredElement = null;
// wrap with try-catch to prevent inconsistency
try {
stub.$fillWith(id, e);
mp.$adapter.destination.$pointerExited(stub);
$enteredElement = this.element;
this.destination.$pointerEntered(stub);
} catch(ee) {
// keep it for exceptional cases
$enteredElement = this.element;
throw ee;
} finally {
mp.$adapter.$UP(id, e, stub);
}
} else {
if (isPressedInQ) { // the mouse pressed and mouse released has happened in different
// point in a time to let UI show visual state, for instance mouse
// down and up
var $this = this;
setTimeout(function() {
$this.$fireUP(id, e, mp, stub, $this.destination);
}, 50);
} else {
this.$fireUP(id, e, mp, stub, this.destination);
}
}
}
};
this.$indexOfQ = function(id) {
for(var i = 0; i < this.$queue.length; i++) {
if (id === this.$queue[i].identifier) {
return i;
}
}
return -1;
};
this.$firePressedFromQ = function() {
// fire collected pointer pressed events
if (this.$queue.length > 0) {
var l = this.$queue.length;
for(var i = 0; i < l; i++) {
var t = this.$queue[i];
try {
// reg the event
$pointerPressedEvents[t.identifier] = t;
t.stub.$fillWith(t.identifier, t);
t.group = l; // TODO: temporary solution
if (this.destination.$pointerPressed(t.stub) === true) {
if (t.stub.touchCounter > 0) {
t.stub.touchCounter--;
}
delete $pointerPressedEvents[t.identifier];
}
} catch(ex) {
// don't forget to decrease counter
if (t.stub.touchCounter > 0) {
t.stub.touchCounter--;
}
delete $pointerPressedEvents[t.identifier];
zebkit.dumpError(ex);
}
}
this.$queue.length = 0;
}
};
this.$DOWN = function(id, e, stub) {
$cleanDragFix();
// remove not fired pointer pressed from queue if necessary
var i = this.$indexOfQ(id);
if (i >= 0) {
this.$queue.splice(i, 1);
}
// release mouse pressed if it has not happened before
if ($pointerPressedEvents.hasOwnProperty(id)) {
var mp = $pointerPressedEvents[id];
mp.$adapter.$UP(id, e, mp.stub);
}
// count pointer pressed
stub.touchCounter++;
try {
var q = {
target : e.target,
direction : null,
identifier : id,
shiftKey : e.shiftKey,
altKey : e.altKey,
metaKey : e.metaKey,
ctrlKey : e.ctrlKey,
time : (new Date()).getTime(),
$adapter : this,
$adx : 0,
$ady : 0,
isDragged : false,
stub : stub
};
q.pageX = q.pressPageX = Math.round(e.pageX);
q.pageY = q.pressPageY = Math.round(e.pageY);
// put pointer pressed in queue
this.$queue.push(q);
// initiate timer to send collected new touch events
// if any new has appeared. the timer helps to collect
// events in one group
if (this.$queue.length > 0 && this.$timer === null) {
var $this = this;
this.$timer = setTimeout(function() {
$this.$timer = null;
$this.$firePressedFromQ(); // flush queue
}, 25);
}
} catch(ee) {
// restore touch counter if an error has happened
if (stub.touchCounter > 0) {
stub.touchCounter--;
}
throw ee;
}
};
this.$MMOVE = function(e) {
var pageX = Math.round(e.pageX),
pageY = Math.round(e.pageY);
// ignore extra mouse moved event that can appear in IE
if (this.$mousePageY !== pageY ||
this.$mousePageX !== pageX )
{
this.$mousePageX = pageX;
this.$mousePageY = pageY;
if ($pointerPressedEvents.hasOwnProperty(LMOUSE) ||
$pointerPressedEvents.hasOwnProperty(RMOUSE) )
{
if ($pointerPressedEvents.hasOwnProperty(LMOUSE)) {
this.$DRAG(LMOUSE, e, ME_STUB);
}
if ($pointerPressedEvents.hasOwnProperty(RMOUSE)) {
this.$DRAG(RMOUSE, e, ME_STUB);
}
} else {
// initialize native fields
ME_STUB.$fillWith("mouse", e);
this.destination.$pointerMoved(ME_STUB);
}
}
};
},
function (element, destination) {
if (element === null || element === undefined) {
throw new Error("Invalid DOM element");
}
if (destination === null || destination === undefined) {
throw new Error("Invalid destination");
}
this.destination = destination;
this.element = element;
var $this = this;
element.onmousedown = function(e) {
// ignore any mouse buttons except left
// and right buttons or long touch emulates mouse event what causes generations of
// mouse down event after touch start event. Let's suppress it
if ((e.button !== 0 && e.button !== 2) ||
$this.$touchedAt(e.pageX, e.pageY, 0))
{
e.preventDefault();
} else {
$this.$DOWN(e.button === 0 ? LMOUSE : RMOUSE, e, ME_STUB);
e.stopPropagation();
}
};
// Possible cases of mouse up events:
//
// a) +-------------+ b) +----------------+ c) +---------------+
// | E | | E +----+ | | E +-----|
// | p--u | | | p--|-u | | | p--|-u
// | | | +----+ | | +-----|
// +-------------+ +----------------+ +---------------+
// (out to document/body) (out from kid to element) (out from kid to document)
//
// d) +--------+--------+ e) +----------+----------+ f) +---------+-------+
// | E | | | E +-----|-----+ | | E +-----| |
// | p--|--u | | | p---|--u | | | | p--|-u |
// | | | | +-----|-----+ | | +-----| |
// +--------+--------+ +----------+----------+ +---------+-------+
// (out from element to (out from kid of element (out from kid element
// other element) to kid of another element) to another element)
// Contract:
// -- handle only mouse events whose destination is the passed element
// -- does stop propagation if event has been handled
// -- clear drag fix ?
element.onmouseup = function(e) {
// ignore any mouse buttons except left
// and right buttons
if (e.button !== 0 && e.button !== 2) {
e.preventDefault();
} else {
var id = e.button === 0 ? LMOUSE : RMOUSE;
$this.$UP(id, e, ME_STUB);
if (e.stopPropagation !== undefined) {
e.stopPropagation();
}
}
};
// mouse over has to setup if necessary current over element variable
// it requires to detect repeat mouse over event that happens when
// for instance we switch between browser and other application, but
// mouse cursor stays at the same place
element.onmouseover = function(e) {
// this code prevent mouse over for first touch on iOS and Android
if ($this.$touchedAt(e.pageX, e.pageY, 0)) {
e.preventDefault();
} else {
var id = e.button === 0 ? LMOUSE : RMOUSE;
// if a button has not been pressed handle mouse entered to detect
// zebkit component the mouse pointer entered and send appropriate
// mouse entered event to it
if ($pointerPressedEvents.hasOwnProperty(id) === false) {
// just for the sake of error prevention
// clean global move listeners
$cleanDragFix();
// if entered element is null or the target element
// is not a children/element of the entered element than
// fires pointer entered event
if ($enteredElement === null || ($enteredElement.contains(e.target) === false && $enteredElement !== e.target)) {
ME_STUB.$fillWith("mouse", e);
$enteredElement = element;
destination.$pointerEntered(ME_STUB);
}
} else {
// remove any previously registered listener if
// -- a mouse button has been pressed
// -- a mouse button has been pressed on the canvas we have entered
if (element === e.target || element.contains(e.target)) {
$cleanDragFix();
}
}
e.stopPropagation();
}
};
// Possible cases of mouse out events:
//
// a) +-------------+ b) +----------------+ c) +---------------+
// | E | | E +----+ | | E +-----|
// | *----|-> | | *--|-> | | | *--|->
// | | | +----+ | | +-----|
// +-------------+ +----------------+ +---------------+
// (out to document/body) (out from kid to element) (out from kid to document)
//
// d) +--------+--------+ e) +----------+----------+ f) +---------+-------+
// | E | | | E +-----|-----+ | | E +-----| |
// | *--|--> | | | *---|--> | | | | *--|-> |
// | | | | +-----|-----+ | | +-----| |
// +--------+--------+ +----------+----------+ +---------+-------+
// (out from element to (out from kid of element (out from kid element
// other element) to kid of another element) to another element)
//
// 1) a mouse button doesn't have to be pressed on any element
// 2) e.target always equals to element (E), just because we register event handler
// for element. This guarantees element will get mouse out event only from itself
// and its children elements
// 3) mouse out should trigger pointerExited event only if the relatedTarget element
// is not the element (E) or kid of the element (E)
// 4) if a mouse button has been pressed than mouse out registers mouse move listener
// to track drag events if the listener has nor been registered yet.
// 5) mouse out set to null $enteredElement
element.onmouseout = function(e) {
var id = e.button === 0 ? LMOUSE : RMOUSE;
// no pressed button exists
if ($pointerPressedEvents.hasOwnProperty(id) === false) {
// the target element is the not a kid of the element
if ($enteredElement !== null && (e.relatedTarget !== null &&
e.relatedTarget !== element &&
element.contains(e.relatedTarget) === false))
{
$enteredElement = null;
ME_STUB.$fillWith("mouse", e);
if (zebkit.web.$isInsideElement(element, e.pageX, e.pageY) === false) {
destination.$pointerExited(ME_STUB);
}
}
} else {
var mp = $pointerPressedEvents[id];
// if a button has been pressed but the mouse cursor is outside of
// the canvas, for a time being start listening mouse moved events
// of Window to emulate mouse moved events in canvas
if ($tmpWinMouseMoveListener === null &&
e.relatedTarget !== null &&
element.contains(e.relatedTarget) === false)
{
// !!! ignore touchscreen devices
if (id === LMOUSE || id === RMOUSE) {
$tmpWinMouseMoveListener = function(ee) {
ee.stopPropagation();
if ($pointerPressedEvents.hasOwnProperty(LMOUSE)) {
$this.$DRAG(LMOUSE, {
pageX : ee.pageX,
pageY : ee.pageY,
target : mp.target,
}, ME_STUB);
}
if ($pointerPressedEvents.hasOwnProperty(RMOUSE)) {
$this.$DRAG(RMOUSE, {
pageX : ee.pageX,
pageY : ee.pageY,
target : mp.target,
}, ME_STUB);
}
ee.preventDefault();
};
window.addEventListener("mousemove", $tmpWinMouseMoveListener, true);
}
}
}
$this.$mousePageX = $this.$mousePageY = -1;
e.stopPropagation();
};
if ("onpointerdown" in window || "onmspointerdown" in window) {
var names = "onpointerdown" in window ? [ "pointerdown",
"pointerup",
"pointermove",
"pointerenter",
"pointerleave" ]
: [ "MSPointerDown",
"MSPointerUp",
"MSPointerMove",
"MSPointerEnter",
"MSPointerLeave" ];
//
// in windows 8 IE10 pointerType can be a number !!!
// what is nit the case fo rinstanvce for Win 8.1
//
element.addEventListener(names[0], function(e) {
var pt = e.pointerType;
if (pt === 4) {
pt = "mouse";
} else if (pt === 2) {
pt = "touch";
} else if (pt === 3) {
pt = "pen";
}
if (pt !== "mouse") {
POINTER_STUB.touch = e;
POINTER_STUB.pointerType = pt;
$this.$DOWN(e.pointerId, e, POINTER_STUB);
}
}, false);
element.addEventListener(names[1], function(e) {
var pt = e.pointerType;
if (pt === 4) {
pt = "mouse";
} else if (pt === 2) {
pt = "touch";
} else if (pt === 3) {
pt = "pen";
}
if (pt !== "mouse") {
POINTER_STUB.touch = e;
POINTER_STUB.pointerType = pt;
$this.$UP(e.pointerId, e, POINTER_STUB);
}
}, false);
element.addEventListener(names[2], function(e) {
var pt = e.pointerType;
if (pt === 4) {
pt = "mouse";
} else if (pt === 2) {
pt = "touch";
} else if (pt === 3) {
pt = "pen";
}
if (pt !== "mouse") {
POINTER_STUB.touch = e;
POINTER_STUB.pointerType = pt;
$this.$DRAG(e.pointerId, e, POINTER_STUB);
} else {
//e.pointerType = pt;
$this.$MMOVE(e);
}
}, false);
} else {
element.addEventListener("touchstart", function(e) {
var allTouches = e.touches,
newTouches = e.changedTouches; // list of touch events that become
// active with the current touch start
// fix android bug: parasite event for multi touch
// or stop capturing new touches since it is already fixed
// TODO: have no idea what it is
// if (TOUCH_STUB.touchCounter > e.touches.length) {
// return;
// }
// android devices fire mouse move if touched but not moved
// let save coordinates what should prevent mouse move event
// generation
//
// TODO: not clear if second tap will fire mouse move or if the
// second tap will have any influence to first tap mouse move
// initiation
$this.$mousePageX = Math.round(e.pageX);
$this.$mousePageY = Math.round(e.pageY);
// fire touches that has not been fired yet
for(var i = 0; i < newTouches.length; i++) { // go through all touches
var newTouch = newTouches[i];
$this.$DOWN(newTouch.identifier, newTouch, TOUCH_STUB);
}
// clear touches that still is not in list of touches
for (var k in $pointerPressedEvents) {
if (isIn(allTouches, k) === false) {
var tt = $pointerPressedEvents[k];
if (tt.group != null) {
tt.group.active = false;
}
$this.$UP(tt.identifier, tt, TOUCH_STUB);
}
}
//!!!
//TODO: this calling prevents generation of phantom mouse move event
//but it is not clear if it will stop firing touch end/move events
//for some mobile browsers. Check it !
e.preventDefault();
}, false);
element.addEventListener("touchend", function(e) {
// update touches
var t = e.changedTouches;
for (var i = 0; i < t.length; i++) {
var tt = t[i];
$this.$UP(tt.identifier, tt, TOUCH_STUB);
}
e.preventDefault();
}, false);
element.addEventListener("touchmove", function(e) {
var mt = e.changedTouches;
// clear dx, dy for not updated touches
for(var k in $this.touches) {
$pointerPressedEvents[k].dx = $pointerPressedEvents[k].dy = 0;
}
for(var i = 0; i < mt.length; i++) {
var nmt = mt[i];
if ($pointerPressedEvents.hasOwnProperty(nmt.identifier)) {
var t = $pointerPressedEvents[nmt.identifier];
if (t.pageX !== Math.round(nmt.pageX) ||
t.pageY !== Math.round(nmt.pageY) )
{
// TODO: analyzing time is not enough to generate click event since
// a user can put finger and wait for a long time. the best way is
// normalize time with movement (number of movement of dx/dy accumulation)
//if (t.isDragged) {// || (new Date().getTime() - t.time) > 200) {
if (t.isDragged || Math.abs(nmt.pageX - t.pageX) + Math.abs(nmt.pageY - t.pageY) > 4) {
$this.$DRAG(nmt.identifier, nmt, TOUCH_STUB);
}
}
}
}
e.preventDefault();
}, false);
element.onmousemove = function(e) {
$this.$MMOVE(e);
e.stopPropagation();
};
}
// TODO: not sure it has to be in pointer unifier
element.oncontextmenu = function(e) {
e.preventDefault();
};
}
]);
/**
* Mouse wheel support class. Installs necessary mouse wheel listeners and handles mouse wheel
* events in zebkit UI. The mouse wheel support is plugging that is configured by a JSON
* configuration.
* @class zebkit.web.MouseWheelSupport
* @param {DOMElement} element
* @param {Object} destination
* @constructor
*/
pkg.MouseWheelSupport = Class([
function(element, destination) {
var META = this.clazz.$META;
for(var k in META) {
if (META[k].test()) {
var $wheelMeta = META[k],
$clazz = this.clazz;
element.addEventListener(k,
function(e) {
var dy = e[$wheelMeta.dy] !== undefined ? e[$wheelMeta.dy] * $wheelMeta.dir : 0,
dx = e[$wheelMeta.dx] !== undefined ? e[$wheelMeta.dx] * $wheelMeta.dir : 0;
// some version of FF can generates dx/dy < 1
if (Math.abs(dy) < 1) {
dy *= $clazz.dyZoom;
}
if (Math.abs(dx) < 1) {
dx *= $clazz.dxZoom;
}
dy = Math.abs(dy) > $clazz.dyNorma ? dy % $clazz.dyNorma : dy;
dx = Math.abs(dx) > $clazz.dxNorma ? dx % $clazz.dxNorma : dx;
// do floor since some mouse devices can fire float as
if (destination.$doScroll(Math.floor(dx),
Math.floor(dy), "wheel"))
{
e.preventDefault();
}
},
false);
break;
}
}
},
function $clazz() {
this.dxZoom = this.dyZoom = 20;
this.dxNorma = this.dyNorma = 80;
this.$META = {
wheel: {
dy : "deltaY",
dx : "deltaX",
dir : 1,
test: function() {
return "WheelEvent" in window;
}
},
mousewheel: {
dy : "wheelDelta",
dx : "wheelDeltaX",
dir : -1,
test: function() {
return document.onmousewheel !== undefined;
}
},
DOMMouseScroll: {
dy : "detail",
dir : 1,
test: function() {
return true;
}
}
};
},
function $prototype() {
/**
* Indicates if the wheel scrolling is done following natural
* direction.
* @attribute naturalDirection
* @type {Boolean}
* @default true
*/
this.naturalDirection = true;
}
]);
// Key CODES meta
// pr - preventDefault, false if not defined
// rp - repeatable key, true if not defined
// map - map code to another code
// ignore - don't fire the given event, false by default
var CODES = {
"KeyA" : { keyCode: 65 },
"KeyB" : { keyCode: 66 },
"KeyC" : { keyCode: 67 },
"KeyD" : { keyCode: 68 },
"KeyE" : { keyCode: 69 },
"KeyF" : { keyCode: 70 },
"KeyG" : { keyCode: 71 },
"KeyH" : { keyCode: 72 },
"KeyI" : { keyCode: 73 },
"KeyJ" : { keyCode: 74 },
"KeyK" : { keyCode: 75 },
"KeyL" : { keyCode: 76 },
"KeyM" : { keyCode: 77 },
"KeyN" : { keyCode: 78 },
"KeyO" : { keyCode: 79 },
"KeyP" : { keyCode: 80 },
"KeyQ" : { keyCode: 81 },
"KeyR" : { keyCode: 82 },
"KeyS" : { keyCode: 83 },
"KeyT" : { keyCode: 84 },
"KeyU" : { keyCode: 85 },
"KeyV" : { keyCode: 86 },
"KeyW" : { keyCode: 87 },
"KeyX" : { keyCode: 88 },
"KeyY" : { keyCode: 89 },
"KeyZ" : { keyCode: 90 },
"Digit0": { keyCode: 48 },
"Digit1": { keyCode: 49 },
"Digit2": { keyCode: 50 },
"Digit3": { keyCode: 51 },
"Digit4": { keyCode: 52 },
"Digit5": { keyCode: 53 },
"Digit6": { keyCode: 54 },
"Digit7": { keyCode: 55 },
"Digit8": { keyCode: 56 },
"Digit9": { keyCode: 57 },
"F1": { keyCode: 112, key: "F1", rp: false },
"F2": { keyCode: 113, key: "F2", rp: false },
"F3": { keyCode: 114, key: "F3", rp: false },
"F4": { keyCode: 115, key: "F4", rp: false },
"F5": { keyCode: 116, key: "F5", rp: false },
"F6": { keyCode: 117, key: "F6", rp: false },
"F7": { keyCode: 118, key: "F7", rp: false },
"F8": { keyCode: 119, key: "F8", rp: false },
"F9": { keyCode: 120, key: "F9", rp: false },
"F10": { keyCode: 121, key: "F10", rp: false },
"F11": { keyCode: 122, key: "F11", rp: false },
"F12": { keyCode: 123, key: "F12", rp: false },
"F13": { keyCode: 124, key: "F13", rp: false },
"F14": { keyCode: 125, key: "F14", rp: false },
"F15": { keyCode: 126, key: "F15", rp: false },
"Numpad0" : { keyCode: 96 },
"Numpad1" : { keyCode: 97 },
"Numpad2" : { keyCode: 98 },
"Numpad3" : { keyCode: 99 },
"Numpad4" : { keyCode: 100 },
"Numpad5" : { keyCode: 101 },
"Numpad6" : { keyCode: 102 },
"Numpad7" : { keyCode: 103 },
"Numpad8" : { keyCode: 104 },
"Numpad9" : { keyCode: 105 },
"NumpadDecimal" : { keyCode: 110, key: "Decimal" },
"NumpadSubtract": { keyCode: 109, key: "Subtract" },
"NumpadDivide" : { keyCode: 111, key: "Divide" },
"NumpadMultiply": { keyCode: 106, key: "Multiply" },
"NumpadAdd" : { keyCode: 107, key: "Add" },
"NumLock" : { keyCode: (zebkit.isFF ? 144 : 12) , key: "NumLock", rp: false, ignore : true },
"Comma" : { keyCode: 188 },
"Period" : { keyCode: 190 },
"Semicolon" : { keyCode: (zebkit.isFF ? 59 : 186) },
"Quote" : { keyCode: 222 },
"BracketLeft" : { keyCode: 219 },
"BracketRight" : { keyCode: 221 },
"Backquote" : { keyCode: 192 },
"Backslash" : { keyCode: 220 },
"Minus" : { keyCode: (zebkit.isFF ? 173 : 189) },
"Equal" : { keyCode: (zebkit.isFF ? 61 : 187) },
"NumpadEnter" : { map: "Enter" },
"Enter" : { keyCode: 13, key: "\n" },
"Slash" : { keyCode: 191 },
"Space" : { keyCode: 32, pr: true, key: " " },
"Delete" : { keyCode: 46, key: "Delete" },
"IntlRo" : { keyCode: (zebkit.isFF ? 167 : 193), key: "IntlRo"},
"Backspace" : { keyCode: 8, pr: true, key: "Backspace" },
"Tab": { keyCode: 9, pr: true, key: "\t" },
"ContextMenu": { keyCode: zebkit.isFF ? 93 : 0, pr: true, key: "ContextMenu" },
"ArrowLeft" : { keyCode: 37, pr: true, key: "ArrowLeft" },
"ArrowRight" : { keyCode: 39, pr: true, key: "ArrowRight" },
"ArrowUp" : { keyCode: 38, pr: true, key: "ArrowUp" },
"ArrowDown" : { keyCode: 40, pr: true, key: "ArrowDown" },
"PageUp" : { keyCode: 33, pr: true, key: "PaheUp" },
"PageDown" : { keyCode: 34, pr: true, key: "PageDown" },
"Home" : { keyCode: 36, pr: true, key: "Home" },
"End" : { keyCode: 35, pr: true, key: "End" },
"Escape" : { keyCode: 27, pr: true, key: "Escape", rp: false },
"CapsLock" : { keyCode: 20, key: "CapsLock", rp: false, ignore : true },
"Shift" : { keyCode: 16, pr: true, key: "Shift", rp: false,},
"ShiftLeft" : { map: "Shift" },
"ShiftRight" : { map: "Shift" },
"Alt" : { keyCode: 18, pr: true, key: "Alt", rp: false, },
"AltLeft" : { map: "Alt" },
"AltRight" : { map: "Alt" },
"Control" : { keyCode: 17, pr: true, key: "Control", rp: false },
"ControlRight": { map: "Control" },
"ControlLeft" : { map: "Control" },
"MetaLeft" : { keyCode: 91, pr: true, key: "Meta", rp: false },
"MetaRight" : { keyCode: 93, pr: true, key: "Meta", rp: false },
"OSLeft" : { keyCode: 224, map: "MetaLeft" },
"OSRight" : { keyCode: 224, map: "MetaRight" }
},
CODES_MAP = {};
// codes to that are not the same for different browsers
function $initializeCodesMap() {
var k = null,
code = null;
// validate codes mapping
for(k in CODES) {
code = CODES[k];
if (code.map !== undefined) {
if (CODES[code.map] === undefined) {
throw new Error("Invalid mapping for code = '" + k + "'");
}
} else if (code.keyCode === undefined) {
throw new Error("unknown keyCode for code = '" + k + "'");
}
}
// build codes map table for the cases when "code" property
CODES_MAP = {};
for(k in CODES) {
code = CODES[k];
if (code.map !== undefined) {
if (code.keyCode !== undefined) {
CODES_MAP[code.keyCode] = code.map;
}
} else {
CODES_MAP[code.keyCode] = k;
}
}
}
pkg.$fetchKeyCode = function(e) {
var code = e.code;
if (code !== undefined) {
if (CODES.hasOwnProperty(code) && CODES[code].hasOwnProperty("map")) {
code = CODES[code].map;
}
} else {
code = CODES_MAP[(e.which || e.keyCode || 0)];
if (code === undefined) {
code = null;
}
}
return code;
};
$initializeCodesMap();
/**
* Input key event class.
* @class zebkit.web.KeyEvent
* @extends zebkit.ui.event.KeyEvent
* @constructor
*/
pkg.KeyEvent = Class(zebkit.ui.event.KeyEvent, [
function $prototype() {
/**
* Fulfills the given abstract event with fields from the specified native WEB key event
* @param {KeyboardEvent} e a native WEB event
* @method $fillWith
* @chainable
* @protected
*/
this.$fillWith = function(e) {
// code defines integer that in a case of
// key pressed/released is zero or equals to physical key layout integer identifier
// but for keyTyped should depict Unicode character code
var keyCode = (e.which || e.keyCode || 0);
this.code = pkg.$fetchKeyCode(e);
if (this.code === "Enter" || this.code === "Space" || this.code === "Tab") {
this.key = CODES[this.code].key;
} else if (e.key != null) {
this.key = e.key;
} else if (e.type === "keypress") {
this.key = e.charCode > 0 && keyCode >= 32 ? String.fromCharCode(e.charCode)
: null;
} else {
if (e.keyIdentifier != null) {
if (e.keyIdentifier[0] === 'U' && e.keyIdentifier[1] === '+') {
this.key = String.fromCharCode(parseInt(e.keyIdentifier.substring(2), 16));
} else {
this.key = e.keyIdentifier;
}
} else {
if (this.code != null && CODES.hasOwnProperty(this.code) === true && CODES[this.code].key != null) {
this.key = CODES[this.code].key;
} else {
this.key = e.charCode > 0 && keyCode >= 32 ? String.fromCharCode(e.charCode)
: null;
}
}
}
this.altKey = e.altKey;
this.shiftKey = e.shiftKey;
this.ctrlKey = e.ctrlKey;
this.metaKey = e.metaKey;
return this;
};
}
]);
var KEY_DOWN_EVENT = new pkg.KeyEvent(),
KEY_UP_EVENT = new pkg.KeyEvent(),
KEY_PRESS_EVENT = new pkg.KeyEvent(),
wasMetaLeftPressed = false,
wasMetaRightPressed = false;
/**
* Class that is responsible for translating native DOM element key event into abstract event that further
* can be transfered to zebkit UI engine (or any other destination). Browsers key events support can be
* implemented with slight differences from the standards. The goal of the class is key events unification.
* The class fires three types of key events to passed event destination code:
* - $keyPressed(e)
* - $keyReleased(e)
* - $keyTyped(e)
*
* For instance imagine we have a DOM Element and want to have identical sequence and parameters of key
* events the DOM element triggers. It can be done as follow:
*
* new KeyEventUnifier(domElement, {
* "$keyPressed" : function(e) {
* ...
* },
*
* "$keyReleased" : function(e) {
* ...
* },
*
* "$keyTyped" : function(e) {
* ...
* }
* });
*
* @param {HTMLElement} element
* @param {Object} destination a destination listener that can listen
* @constructor
* @class zebkit.web.KeyEventUninfier
*/
pkg.KeyEventUnifier = Class([
function(element, destination) {
var $this = this;
// Alt + x was pressed (for IE11 consider sequence of execution of "alt" and "x" keys)
// Chrome/Safari/FF keydown -> keydown -> keypressed
// ----------------------------------------------------------------------------------------------------------------------
// | which | keyCode | charCode | code | key | keyIdentifier | char
// ----------------------------------------------------------------------------------------------------------------------
// | | | | | | |
// Chrome | unicode/ | unicode/ | 0 | undefined | undefined | Mnemonic + Unistr | No
// | code | code | | | | "Alt" + "U-0058" |
// | 18 + 88 | 18 + 88 | | | | |
//----------+-----------------------------------------------------------------------------------------------|------------
// | | | | | | |
// IE11 | unicode/ | unicode/ | | | | | Alt => ""
// | code | code | 0 | undefined | "Alt","x" | undefined | x => "x"
// | 18, 88 | 18, 88 | | | | |
// | | | | | | |
//----------+-------------|--------------|----------|------------------|----------------|-------------------|------------
// | unicode/ | unicode/ | | | | |
// | code | code | 0 | undefined | undefined | Mnemonic + Unistr | No
// Safari | 18 + 88 | 18 + 88 | | | | "Alt" + "U-0058" |
// | | | | | | |
//----------+-----------------------------------------------------------------------------------------------|------------
// | | | | | | |
// FF | unicode/ | unicode/ | 0 | Mnemonic | Mnemonic/char | | No
// | code | code | |("AltLeft"+"KeyX")| "Alt"+"≈" | undefined |
// | 18 + 88 | 18 + 88 | | | | |
//
element.onkeydown = function(e) {
var code = KEY_DOWN_EVENT.code,
pts = KEY_DOWN_EVENT.timeStamp,
ts = new Date().getTime();
// fix loosing meta left keyup event in some browsers
var fc = pkg.$fetchKeyCode(e);
// ignore some keys that cannot be handled properly
if (CODES[fc] != null && CODES[fc].ignore === true) {
return;
}
if (wasMetaLeftPressed === true && (e.metaKey !== true || fc === "MetaLeft")) {
wasMetaLeftPressed = false;
try {
KEY_DOWN_EVENT.code = "MetaLeft";
KEY_DOWN_EVENT.repeat = 0;
KEY_DOWN_EVENT.metaKey = true;
KEY_DOWN_EVENT.timeStamp = ts;
destination.$keyReleased(KEY_DOWN_EVENT);
} catch(ex) {
zebkit.dumpError(ex);
} finally {
KEY_DOWN_EVENT.code = null;
code = null;
}
}
// fix loosing meta right keyup event in some browsers
if (wasMetaRightPressed === true && (e.metaKey !== true || fc === "MetaRight")) {
wasMetaRightPressed = false;
try {
KEY_DOWN_EVENT.code = "MetaRight";
KEY_DOWN_EVENT.repeat = 0;
KEY_DOWN_EVENT.metaKey = true;
KEY_DOWN_EVENT.timeStamp = ts;
destination.$keyReleased(KEY_DOWN_EVENT);
} catch(ex) {
zebkit.dumpError(ex);
} finally {
KEY_DOWN_EVENT.code = null;
code = null;
}
}
// we suppose key down object is shared with key up that means it
// holds state of key (we can understand whether a key has been
// still held or was released by checking if the code equals)
KEY_DOWN_EVENT.$fillWith(e);
KEY_DOWN_EVENT.timeStamp = ts;
// calculate repeat counter
if (KEY_DOWN_EVENT.code === code && e.metaKey !== true && (ts - pts) < 1000) {
KEY_DOWN_EVENT.repeat++;
} else {
KEY_DOWN_EVENT.repeat = 1;
}
//!!!!
// Suppress some standard browser actions.
// Since container of zCanvas catch all events from its children DOM
// elements don't prevent the event for the children DOM element
var key = CODES[KEY_DOWN_EVENT.code];
if (key != null && key.pr === true && e.target === element) {
// TODO: may be put the "if" logic into prevent default
$this.preventDefault(e, key);
}
e.stopPropagation();
// fire key pressed event
try {
destination.$keyPressed(KEY_DOWN_EVENT);
} catch(ex) {
zebkit.dumpError(ex);
}
if (KEY_DOWN_EVENT.code === "MetaLeft") {
wasMetaLeftPressed = true;
} else if (KEY_DOWN_EVENT.code === "MetaRight") {
wasMetaRightPressed = true;
} else {
// if meta key is kept than generate key released event for
// all none-Meta keys. it is required since Meta + <a key>
// will never fire key released for <a key> (except state keys
// like shift, control etc)
if (e.metaKey === true) {
// only repeat
if (key == null || key.rp !== false) {
try {
KEY_UP_EVENT.$fillWith(e);
KEY_UP_EVENT.repeat = 0;
KEY_UP_EVENT.timeStamp = ts;
destination.$keyReleased(KEY_UP_EVENT);
} catch(ex) {
zebkit.dumpError(ex);
}
}
} else if (KEY_DOWN_EVENT.code === "Space" ||
KEY_DOWN_EVENT.code === "Enter" ||
KEY_DOWN_EVENT.code === "Tab" )
{
// since space and enter key press event triggers preventDefault
// standard key press can never happen so let's emulate it here
KEY_PRESS_EVENT.$fillWith(e);
KEY_PRESS_EVENT.repeat = KEY_DOWN_EVENT.repeat;
KEY_PRESS_EVENT.timeStamp = ts;
destination.$keyTyped(KEY_PRESS_EVENT);
}
}
};
element.onkeyup = function(e) {
e.stopPropagation();
KEY_UP_EVENT.$fillWith(e);
// ignore some keys that cannot be handled properly
if (CODES[KEY_UP_EVENT.code] != null && CODES[KEY_UP_EVENT.code].ignore === true) {
return;
}
if (wasMetaLeftPressed === true && KEY_UP_EVENT.code === "MetaLeft") {
wasMetaLeftPressed = false;
}
if (wasMetaRightPressed === true && KEY_UP_EVENT.code === "MetaRight") {
wasMetaRightPressed = false;
}
var key = CODES[KEY_UP_EVENT.code];
if (e.metaKey !== true || (key != null && key.rp === false)) {
KEY_UP_EVENT.repeat = 0;
KEY_UP_EVENT.timeStamp = new Date().getTime();
try {
destination.$keyReleased(KEY_UP_EVENT);
} finally {
// clean repeat counter
if (KEY_DOWN_EVENT.code === KEY_UP_EVENT.code) {
KEY_DOWN_EVENT.repeat = 0;
}
}
}
};
// Alt + x was pressed (for IE11 consider sequence of execution of "alt" and "x" keys)
// ----------------------------------------------------------------------------------------------------------------------
// | which | keyCode | charCode | code | key | keyIdentifier | char
// ----------------------------------------------------------------------------------------------------------------------
// | | | | | | |
// Chrome | unicode/ | unicode/ | 8776 | undefined | undefined | Mnemonic + Unistr | No
// | code | code | (≈) | | | "U-0058" |
// | 8776 (≈) | 8776 (≈) | | | | |
//----------+-----------------------------------------------------------------------------------------------|------------
// | | | | | | |
// IE11 | unicode/ | unicode/ | | | | |
// | code | code | 88 (x) | undefined | "x" | undefined | "x"
// | 88 (x) | 88 (x) | | | | |
// | | | | | | |
//----------+-------------|--------------|----------|------------------|----------------|-------------------|------------
// | unicode/ | unicode/ | | | | |
// | code | code | 8776 (≈) | undefined | undefined | | No
// Safari | 8776 (≈) | 8776 (≈) | | | | "" |
// | | | | | | |
//----------+-----------------------------------------------------------------------------------------------|------------
// | | | | | | |
// FF | unicode/ | 0 | 8776 | Mnemonic | Mnemonic/char | | No
// | code | | (≈) | ("KeyX") | "≈" | undefined |
// | 8776 (≈) | | | | | |
//
element.onkeypress = function(e) {
e.stopPropagation();
// pressed meta key should bring to ignorance keypress event since the event
// is emulated in keydown event handler.
if (e.metaKey !== true) {
KEY_PRESS_EVENT.$fillWith(e);
KEY_PRESS_EVENT.code = KEY_DOWN_EVENT.code; // copy code of keydown key since key press can contain undefined code
KEY_PRESS_EVENT.repeat = KEY_DOWN_EVENT.repeat;
if (KEY_PRESS_EVENT.code !== "Space" &&
KEY_PRESS_EVENT.code !== "Enter" &&
KEY_PRESS_EVENT.code !== "Tab" &&
KEY_PRESS_EVENT.code !== "ContextMenu")
{
// Since container of zCanvas catch all events from its children DOM
// elements don't prevent the event for the children DOM element
KEY_PRESS_EVENT.timeStamp = new Date().getTime();
destination.$keyTyped(KEY_PRESS_EVENT);
}
}
};
},
function $prototype() {
this.preventDefault = function(e, key) {
e.preventDefault();
};
}
]);
},false);
zebkit.package("ui.web", function(pkg, Class) {
'use strict';
/**
* Cursor manager class. Allows developers to control pointer cursor type by implementing an own
* getCursorType method or by specifying a cursor by cursorType field. Imagine an UI component
* needs to change cursor type. It
* can be done by one of the following way:
*
* - **Implement getCursorType method by the component itself if the cursor type depends on cursor location**
var p = new zebkit.ui.Panel([
// implement getCursorType method to set required
// pointer cursor type
function getCursorType(target, x, y) {
return zebkit.ui.Cursor.WAIT;
}
]);
* - **Define "cursorType" property in component if the cursor type doesn't depend on cursor location**
var myPanel = new zebkit.ui.Panel();
...
myPanel.cursorType = zebkit.ui.Cursor.WAIT;
* @class zebkit.ui.web.CursorManager
* @constructor
* @extends zebkit.ui.event.Manager
*/
pkg.CursorManager = Class(zebkit.ui.event.CursorManager, [
function $prototype() {
this.$isFunc = false;
this.source = this.target = null;
/**
* Define pointer moved events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerMoved
*/
this.pointerMoved = function(e) {
if (this.$isFunc === true) {
this.cursorType = this.source.getCursorType(this.source, e.x, e.y);
this.target.style.cursor = (this.cursorType === null) ? "default"
: this.cursorType;
}
};
/**
* Define pointer entered events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerEntered
*/
this.pointerEntered = function(e) {
if ((e.source.cursorType !== undefined && e.source.cursorType !== null) ||
e.source.getCursorType !== undefined)
{
this.$isFunc = (typeof e.source.getCursorType === 'function');
this.target = e.target;
this.source = e.source;
this.cursorType = this.$isFunc === true ? this.source.getCursorType(this.source, e.x, e.y)
: this.source.cursorType;
this.target.style.cursor = (this.cursorType === null) ? "default"
: this.cursorType;
}
};
/**
* Define pointer exited events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerExited
*/
this.pointerExited = function(e){
if (this.source !== null) {
this.cursorType = "default";
if (this.target.style.cursor != this.cursorType) {
this.target.style.cursor = this.cursorType;
}
this.source = this.target = null;
this.$isFunc = false;
}
};
/**
* Define pointer dragged events handler.
* @param {zebkit.ui.event.PointerEvent} e a pointer event
* @method pointerDragged
*/
this.pointerDragged = function(e) {
if (this.$isFunc === true) {
this.cursorType = this.source.getCursorType(this.source, e.x, e.y);
this.target.style.cursor = (this.cursorType === null) ? "default"
: this.cursorType;
}
};
}
]);
// TODO: make sure it should be done here, instead of json config
pkg.cd("..").cursorManager = new pkg.CursorManager();
var ui = pkg.cd("..");
/**
* WEB based zebkit UI components.
*
* @class zebkit.ui.web
* @access package
*/
/**
* HTML element UI component wrapper class. The class represents an HTML element as if it is standard
* UI component. It helps to use some standard HTML element as zebkit UI components and embeds it
* in zebkit UI application layout.
* @class zebkit.ui.web.HtmlElement
* @constructor
* @param {String|HTMLElement} [element] an HTML element to be represented as a standard zebkit UI
* component. If the passed parameter is string it denotes a name of an HTML element. In this case
* a new HTML element will be created.
* @extends zebkit.ui.Panel
*/
pkg.HtmlElement = Class(ui.Panel, [
function(e) {
if (arguments.length === 0) {
e = "div";
}
if (zebkit.isString(e)) {
e = document.createElement(e);
e.style.border = "0px solid transparent"; // clean up border
e.style.fontSize = this.clazz.$bodyFontSize; // DOM element is wrapped with a container that
// has zero sized font, so let's set body font
// for the created element
}
// sync padding and margin of the DOM element with
// what appropriate properties are set
e.style.margin = e.style.padding = "0px";
this.element = e;
// this is set to make possible to use set z-index for HTML element
this.element.style.position = "relative";
if (e.parentNode !== null && e.parentNode.getAttribute("data-zebcont") !== null) {
throw new Error("DOM element '" + e + "' already has container");
}
// container is a DIV element that is used as a wrapper around original one
// it is done to make HtmlElement implementation more universal making
// all DOM elements capable to be a container for another one
this.$container = document.createElement("div");
// prevent stretching to a parent container element
this.$container.style.display = "inline-block";
// cut content
this.$container.style.overflow = "hidden";
// it fixes problem with adding, for instance, DOM element as window what can prevent
// showing components added to popup layer
this.$container.style["z-index"] = "0";
// coordinates have to be set to initial zero value in CSS
// otherwise the DOM layout can be wrong !
this.$container.style.left = this.$container.style.top = "0px";
this.$container.visibility = "hidden"; // before the component will be attached
// to parent hierarchy the component has to be hidden
// container div will always few pixel higher than its content
// to prevent the bloody effect set font to zero
// border and margin also have to be zero
this.$container.style.fontSize = this.$container.style.padding = this.$container.style.padding = "0px";
// add id
this.$container.setAttribute("id", "container-" + this.toString());
// mark wrapper with a special attribute to recognize it exists later
this.$container.setAttribute("data-zebcont", "true");
// let html element interact
this.$container.style["pointer-events"] = "auto";
// if passed DOM element already has parent
// attach it to container first and than
// attach the container to the original parent element
if (e.parentNode !== null) {
// !!!
// Pay attention container position cannot be set to absolute
// since how the element has to be laid out is defined by its
// original parent
e.parentNode.replaceChild(this.$container, e);
this.$container.appendChild(e);
} else {
// to force all children element be aligned
// relatively to the wrapper we have to set
// position CSS to absolute or absolute
this.$container.style.position = "absolute";
this.$container.appendChild(e);
}
// set ID if it has not been already defined
if (e.getAttribute("id") === null) {
e.setAttribute("id", this.toString());
}
this.$super();
// attach listeners
if (this.$initListeners !== undefined) {
this.$initListeners();
}
var fe = this.$getElementRootFocus();
// reg native focus listeners for HTML element that can hold focus
if (fe !== null) {
var $this = this;
zebkit.web.$focusin(fe, function(e) {
// sync native focus with zebkit focus if necessary
if ($this.hasFocus() === false) {
$this.requestFocus();
}
}, false);
zebkit.web.$focusout(fe, function(e) {
// sync native focus with zebkit focus if necessary
if ($this.hasFocus()) {
ui.focusManager.requestFocus(null);
}
}, false);
}
},
function $clazz() {
this.$bodyFontSize = window.getComputedStyle(document.body, null)
.getPropertyValue('font-size');
},
function $prototype() {
this.$blockElement = this.$canvas = null;
this.ePsW = this.ePsH = 0;
/**
* Every zebkit HTML element is wrapped with a container (div) HTML element.
* It is required since not all HTML elements are designed to be a container
* (for instance HTMLCanvas element), where every zebkit has to be a container.
* @attribute $container
* @readOnly
* @private
* @type {HTMLElement}
*/
this.$container = null;
/**
* Reference to HTML element the UI component wraps
* @attribute element
* @readOnly
* @type {HTMLElement}
*/
this.element = null;
/**
* Indicates that this component is a DOM element wrapper
* @attribute isDOMElement
* @type {Boolean}
* @private
* @readOnly
*/
this.isDOMElement = true; // indication of the DOM element that is used by DOM element manager to track
// and manage its visibility
this.$sizeAdjusted = false;
this.wrap = function(c) {
this.setStackLayout();
this.add(c);
return this;
};
/**
* Set the CSS font of the wrapped HTML element
* @param {String|zebkit.Font} f a font
* @method setFont
* @chainable
*/
this.setFont = function(f) {
this.setStyle("font", f.toString());
this.vrp();
return this;
};
/**
* Set the CSS color of the wrapped HTML element
* @param {String} c a color
* @chainable
* @method setColor
*/
this.setColor = function(c) {
this.setStyle("color", c.toString());
return this;
};
this.getColor = function() {
return window.getComputedStyle(this.element, "color");
};
/**
* Apply the given set of CSS styles to the wrapped HTML element
* @param {Object} styles a dictionary of CSS styles
* @chainable
* @method setStyles
*/
this.setStyles = function(styles) {
for(var k in styles) {
this.$setStyle(this.element, k, styles[k]);
}
this.vrp();
return this;
};
/**
* Apply the given CSS style to the wrapped HTML element
* @param {String} a name of the CSS style
* @param {String} a value the CSS style has to be set
* @chainable
* @method setStyle
*/
this.setStyle = function(name, value) {
this.$setStyle(this.element, name, value);
this.vrp();
return this;
};
this.$setStyle = function(element, name, value) {
name = name.trim();
var i = name.indexOf(':');
if (i > 0) {
if (zebkit[name.substring(0, i)] !== true) {
return;
}
name = name.substring(i + 1);
}
if (element.style[name] !== value) {
element.style[name] = value;
}
return this;
};
/**
* Set the specified attribute to the wrapped HTML element
* @param {String} name a name of attribute
* @param {String} value a value of the attribute
* @chainable
* @method setAttribute
*/
this.setAttribute = function(name, value) {
this.element.setAttribute(name, value);
return this;
};
/**
* Set the specified attributes set to the wrapped HTML element
* @param {Object} attrs the dictionary of attributes where name of an
* attribute is a key of the dictionary and
* @method setAttributes
* @chainable
*/
this.setAttributes = function(attrs) {
for(var name in attrs) {
this.element.setAttribute(name, attrs[name]);
}
return this;
};
/**
* Implements "update" method to be aware when the component is visible.
* It is used to adjust wrapped HTML element visibility and size. Update
* is the first rendering method that is called, so it is right place
* to sync HTML element visibility before paint method execution
* @param {CanvasRenderingContext2D} g a 2D canvas context
* @method update
*/
this.update = function(g) {
// this method is used as an indication that the component
// is visible and no one of his parent is invisible
if (this.$container.style.visibility === "hidden") {
this.$container.style.visibility = "visible";
}
// calling paint says that the component in DOM tree
// that is time to correct CSS size if necessary
if (this.$sizeAdjusted !== true) {
this.setSize(this.width, this.height);
}
};
this.calcPreferredSize = function(target) {
return {
width : this.ePsW,
height: this.ePsH
};
};
var $store = [
"paddingTop","paddingLeft","paddingBottom","paddingRight",
"border","borderStyle","borderWidth", "borderTopStyle",
"borderTopWidth", "borderBottomStyle","borderBottomWidth",
"borderLeftStyle","borderLeftWidth", "borderRightStyle",
"visibility", "borderRightWidth", "width", "height", "position"
];
// the method calculates the given HTML element preferred size
this.recalc = function() {
// if component has a layout set it is up to a layout manager to calculate
// the component preferred size. In this case the HTML element is a container
// whose preferred size is defined by its content
if (this.layout === this) {
var e = this.element,
vars = {},
domParent = null,
k = null,
cv = this.$container.style.visibility,
b = !zebkit.web.$contains(this.$container);
// element doesn't have preferred size if it is not a member of
// an html page, so add it if for a while
if (b) {
// save previous parent node since
// appendChild will overwrite it
domParent = this.$container.parentNode;
document.body.appendChild(this.$container);
}
// save element metrics
for(var i = 0; i < $store.length; i++) {
k = $store[i];
vars[k] = e.style[k];
}
// force metrics to be calculated automatically
if (cv !== "hidden") {
this.$container.style.visibility = "hidden";
}
e.style.padding = "0px";
e.style.border = "none";
e.style.position = e.style.height = e.style.width = "auto";
// fetch preferred size
this.ePsW = e.offsetWidth;
this.ePsH = e.offsetHeight;
for(k in vars) {
var v = vars[k];
if (v !== null && e.style[k] !== v) {
e.style[k] = v;
}
}
if (this.$container.style.visibility !== cv) {
this.$container.style.visibility = cv;
}
if (b) {
document.body.removeChild(this.$container);
// restore previous parent node
if (domParent !== null) {
domParent.appendChild(this.$container);
}
}
}
};
/**
* Set the inner content of the wrapped HTML element
* @param {String} an inner content
* @method setContent
* @chainable
*/
this.setContent = function(content) {
this.element.innerHTML = content;
this.vrp();
return this;
};
this.$getElementRootFocus = function() {
return null;
};
this.canHaveFocus = function() {
return this.$getElementRootFocus() !== null;
};
this.$focus = function() {
if (this.canHaveFocus() && document.activeElement !== this.$getElementRootFocus()) {
this.$getElementRootFocus().focus();
}
};
this.$blur = function() {
if (this.canHaveFocus() && document.activeElement === this.$getElementRootFocus()) {
this.$getElementRootFocus().blur();
}
};
},
function toFront() {
this.$super();
var pnode = this.$container.parentNode;
if (pnode !== null && pnode.lastChild !== this.$container) {
pnode.removeChild(this.$container);
pnode.appendChild(this.$container);
}
return this;
},
function toBack() {
this.$super();
var pnode = this.$container.parentNode;
if (pnode !== null && pnode.firstChild !== this.$container) {
pnode.removeChild(this.$container);
pnode.insertBefore(this.$container, pnode.firstChild);
}
return this;
},
function setEnabled(b) {
if (this.isEnabled !== b) {
if (b) {
this.$container.removeChild(this.$blockElement);
} else {
if (this.$blockElement === null) {
this.$blockElement = zebkit.web.$createBlockedElement();
}
this.$container.appendChild(this.$blockElement);
}
}
return this.$super(b);
},
function setSize(w, h) {
// by the moment the method setSize is called the DOM element can be not a part of
// HTML layout. In this case offsetWidth/offsetHeihght are always zero what prevents
// us from proper calculation of CSS width and height. Postpone
if (zebkit.web.$contains(this.$container)) {
var contStyle = this.$container.style,
elemStyle = this.element.style,
prevVisibility = contStyle.visibility;
if (contStyle.visibility !== "hidden") {
contStyle.visibility = "hidden"; // to make sizing smooth
}
// HTML element size is calculated as sum of CSS "width"/"height", paddings, border
// So the passed width and height has to be corrected (before it will be applied to
// an HTML element) by reduction of extra HTML gaps. For this we firstly set the
// width and size
elemStyle.width = "" + w + "px";
elemStyle.height = "" + h + "px";
var ww = 2 * w - this.element.offsetWidth,
hh = 2 * h - this.element.offsetHeight;
if (ww !== w || hh !== h) {
// than we know the component metrics and can compute necessary reductions
elemStyle.width = "" + ww + "px";
elemStyle.height = "" + hh + "px";
}
this.$sizeAdjusted = true;
// visibility correction is done by HTML elements manager
if (contStyle.visibility !== prevVisibility) {
contStyle.visibility = prevVisibility;
}
} else {
this.$sizeAdjusted = false;
}
return this.$super(w, h);
},
function setPadding(t,l,b,r) {
if (arguments.length === 1) {
l = b = r = t;
}
this.setStyles({
paddingTop : '' + t + "px",
paddingLeft : '' + l + "px",
paddingRight : '' + r + "px",
paddingBottom : '' + b + "px"
});
if (this.top !== t || this.left !== l || this.right !== r || this.bottom !== b) {
// changing padding has influence to CSS size the component has to have
// so we have to request CSS size recalculation
this.$sizeAdjusted = false;
}
this.$super.apply(this, arguments);
return this;
},
function setBorder(b) {
if (arguments.length === 0) {
b = "plain";
}
b = zebkit.draw.$view(b);
if (b === null) {
this.setStyle("border", "none");
} else {
this.setStyles({
//!!!! bloody FF fix, the border can be made transparent
//!!!! only via "border" style
border : "0px solid transparent",
//!!! FF understands only decoupled border settings
borderTopStyle : "solid",
borderTopColor : "transparent",
borderTopWidth : "" + b.getTop() + "px",
borderLeftStyle : "solid",
borderLeftColor : "transparent",
borderLeftWidth : "" + b.getLeft() + "px",
borderBottomStyle : "solid",
borderBottomColor : "transparent",
borderBottomWidth : "" + b.getBottom() + "px",
borderRightStyle : "solid",
borderRightColor : "transparent",
borderRightWidth : "" + b.getRight() + "px"
});
}
// changing border can have influence to
// CSS size, so request recalculation of the CSS
// size
if (this.border != b) {
this.$sizeAdjusted = false;
}
return this.$super(b);
},
function validate() {
// lookup root canvas
if (this.$canvas === null && this.parent !== null) {
this.$canvas = this.getCanvas();
}
this.$super();
},
function focused() {
this.$super();
// sync state of zebkit focus with native focus of the HTML Element
if (this.hasFocus()) {
this.$focus();
} else {
this.$blur();
}
}
]).hashable();
/**
* This special private manager that plays key role in integration of HTML ELement into zebkit UI hierarchy.
* Description to the class contains technical details of implementation that should not be interested for
* end users.
*
* HTML element integrated into zebkit layout has to be tracked regarding:
* 1) DOM hierarchy. A new added into zebkit layout DOM element has to be attached to the first found
* parent DOM element
* 2) Visibility. If a zebkit UI component change its visibility state it has to have side effect to all
* children HTML elements on any subsequent hierarchy level
* 3) Moving a zebkit UI component has to correct location of children HTML element on any subsequent
* hierarchy level.
*
* The implementation of HTML element component has the following specific:
* 1) Every original HTML is wrapped with "div" element. It is necessary since not all HTML element has been
* designed to be a container for another HTML element. By adding extra div we can consider the wrapper as
* container. The wrapper element is used to control visibility, location, enabled state
* 2) HTML element has "isDOMElement" property set to true
* 3) HTML element visibility depends on an ancestor component visibility. HTML element is visible if:
* - the element isVisible property is true
* - the element has a parent DOM element set
* - all his ancestors are visible
* - size of element is more than zero
* - getCanvas() != null
*
* The visibility state is controlled with "e.style.visibility"
*
* To support effective DOM hierarchy tracking a zebkit UI component defines "$domKid" property that contains
* direct DOM element the UI component hosts and other UI components that host DOM element. This is sort of tree:
*
* <pre>
* +---------------------------------------------------------
* | p1 (zebkit component)
* | +--------------------------------------------------
* | | p2 (zebkit component)
* | | +---------+ +-----------------------+
* | | | h1 | | p3 zebkit component |
* | | +---------+ | +---------------+ |
* | | | | h3 | |
* | | +---------+ | | +---------+ | |
* | | | h2 | | | | p4 | | |
* | | +---------+ | | +---------+ | |
* | | | +---------------+ |
* | | +-----------------------+
*
* p1.$domKids : {
* p2.$domKids : {
* h1, * leaf elements are always DOM element
* h2,
* p3.$domKids : {
* h3
* }
* }
* }
* </pre>
*
* @constructor
* @private
* @class zebkit.ui.web.HtmlElementMan
* @extends zebkit.ui.event.Manager
*/
pkg.HtmlElementMan = Class(zebkit.ui.event.Manager, [
function $prototype() {
/**
* Evaluates if the given zebkit HTML UI component is in invisible state.
* @param {zebkit.ui.HtmlElement} c an UI HTML element wrapper
* @private
* @method $isInInvisibleState
* @return {Boolean} true if the HTML element wrapped with zebkit UI is in invisible state
*/
function $isInInvisibleState(c) {
if (c.isVisible === false ||
c.$container.parentNode === null ||
c.width <= 0 ||
c.height <= 0 ||
c.parent === null ||
zebkit.web.$contains(c.$container) === false)
{
return true;
}
var p = c.parent;
while (p !== null && p.isVisible === true && p.width > 0 && p.height > 0) {
p = p.parent;
}
return p !== null || ui.$cvp(c) === null;
}
// +----------------------------------------
// | ^ DOM1
// | .
// | . (x,y) -> (xx,yy) than correct left
// . and top of DOM2 relatively to DOM1
// | +--------.--------------------------
// | | . zebkit1
// | | .
// | | (left, top)
// |<............+-------------------------
// | | | DOM2
// | | |
//
// Convert DOM (x, y) zebkit coordinates into appropriate CSS top and left
// locations relatively to its immediate DOM element. For instance if a
// zebkit component contains DOM component every movement of zebkit component
// has to bring to correction of the embedded DOM elements
function $adjustLocation(c) {
if (c.$container.parentNode !== null) {
// hide DOM component before move
// makes moving more smooth
var prevVisibility = null;
if (c.$container.style.visibility !== "hidden") {
prevVisibility = c.$container.style.visibility;
c.$container.style.visibility = "hidden";
}
// find a location relatively to the first parent HTML element
var p = c, xx = c.x, yy = c.y;
while (((p = p.parent) !== null) && p.isDOMElement !== true) {
xx += p.x;
yy += p.y;
}
c.$container.style.left = "" + xx + "px";
c.$container.style.top = "" + yy + "px";
if (prevVisibility !== null) {
c.$container.style.visibility = prevVisibility;
}
}
}
// attach to appropriate DOM parent if necessary
// c parameter has to be DOM element
function $resolveDOMParent(c) {
// try to find an HTML element in zebkit (pay attention, in zebkit hierarchy !)
// hierarchy that has to be a DOM parent for the given component
var parentElement = null;
for(var p = c.parent; p !== null; p = p.parent) {
if (p.isDOMElement === true) {
parentElement = p.$container;
break;
}
}
// parentElement is null means the component has
// not been inserted into DOM hierarchy
if (parentElement !== null && c.$container.parentNode === null) {
// parent DOM element of the component is null, but a DOM container
// for the element has been detected. We need to add it to DOM
// than we have to add the DOM to the found DOM parent element
parentElement.appendChild(c.$container);
// adjust location of just attached DOM component
$adjustLocation(c);
} else {
// test consistency whether the DOM element already has
// parent node that doesn't match the discovered
if (parentElement !== null &&
c.$container.parentNode !== null &&
c.$container.parentNode !== parentElement)
{
throw new Error("DOM parent inconsistent state ");
}
}
}
// iterate over all found children HTML elements
// !!! pay attention you have to check existence
// of "$domKids" field before the method calling
function $domElements(c, callback) {
for (var k in c.$domKids) {
var e = c.$domKids[k];
if (e.isDOMElement === true) {
callback.call(this, e);
} else if (e.$domKids !== undefined) { // prevent unnecessary method call by condition
$domElements(e, callback);
}
}
}
this.compShown = function(e) {
// 1) if c is DOM element than we have make it is visible if
// -- c.isVisible == true : the component visible AND
// -- all elements in parent chain is visible AND
// -- the component is in visible area
//
// 2) if c is not a DOM component his visibility state can have
// side effect to his children HTML elements (on any level)
// In this case we have to do the following:
// -- go through all children HTML elements
// -- if c.isVisible == false: make invisible every children element
// -- if c.isVisible != false: make visible every children element whose
// visibility state satisfies the following conditions:
// -- kid.isVisible == true
// -- all parent to c are in visible state
// -- the kid component is in visible area
var c = e.source;
if (c.isDOMElement === true) {
c.$container.style.visibility = (c.isVisible === false || $isInInvisibleState(c) ? "hidden"
: "visible");
} else if (c.$domKids !== undefined) {
$domElements(c, function(e) {
e.$container.style.visibility = (e.isVisible === false || $isInInvisibleState(e) ? "hidden" : "visible");
});
}
};
this.compMoved = function(e) {
var c = e.source;
// if we move a zebkit component that contains
// DOM element(s) we have to correct the DOM elements
// locations relatively to its parent DOM
if (c.isDOMElement === true) {
// root canvas location cannot be adjusted since it is up to DOM tree to do it
if (c.$isRootCanvas !== true) {
var dx = e.prevX - c.x,
dy = e.prevY - c.y,
cont = c.$container;
cont.style.left = ((parseInt(cont.style.left, 10) || 0) - dx) + "px";
cont.style.top = ((parseInt(cont.style.top, 10) || 0) - dy) + "px";
}
} else if (c.$domKids !== undefined) {
$domElements(c, function(e) {
$adjustLocation(e);
});
}
};
function isLeaf(c) {
if (c.$domKids !== undefined) {
for(var k in c.$domKids) {
if (c.$domKids.hasOwnProperty(k)) {
return false;
}
}
}
return true;
}
function detachFromParent(p, c) {
// DOM parent means the detached element doesn't
// have upper parents since it is relative to the
// DOM element
if (p.isDOMElement !== true && p.$domKids !== undefined) {
// delete from parent
delete p.$domKids[c.$hash$];
// parent is not DOM and doesn't have kids anymore
// what means the parent has to be also detached
if (isLeaf(p)) {
// parent of parent is not null and is not a DOM element
if (p.parent !== null && p.parent.isDOMElement !== true) {
detachFromParent(p.parent, p);
}
// remove $domKids from parent since the parent is leaf
delete p.$domKids;
}
}
}
function removeDOMChildren(c) {
// DOM element cannot have children dependency tree
if (c.isDOMElement !== true && c.$domKids !== undefined) {
for(var k in c.$domKids) {
if (c.$domKids.hasOwnProperty(k)) {
var kid = c.$domKids[k];
// DOM element
if (kid.isDOMElement === true) {
kid.$container.parentNode.removeChild(kid.$container);
} else {
removeDOMChildren(kid);
}
}
}
delete c.$domKids;
}
}
this.compRemoved = function(e) {
var c = e.kid;
// if detached element is DOM element we have to
// remove it from DOM tree
if (c.isDOMElement === true) {
// DOM component can be detached from document
// with a parent component removal so let's
// check if it has a DOM parent
if (c.$container.parentNode !== null) {
c.$container.parentNode.removeChild(c.$container);
}
} else {
removeDOMChildren(c);
}
detachFromParent(e.source, c);
};
this.compAdded = function(e) {
var p = e.source, c = e.kid;
if (c.isDOMElement === true) {
$resolveDOMParent(c);
} else {
if (c.$domKids !== undefined) {
$domElements(c, function(e) {
$resolveDOMParent(e);
});
} else {
return;
}
}
if (p.isDOMElement !== true) {
// we come here if parent is not a DOM element and
// inserted children is DOM element or an element that
// embeds DOM elements
while (p !== null && p.isDOMElement !== true) {
if (p.$domKids === undefined) {
// if reference to kid DOM element or kid DOM elements holder
// has bot been created we have to continue go up to parent of
// the parent to register the whole chain of DOM and DOM holders
p.$domKids = {};
p.$domKids[c.$genHash()] = c;
c = p;
p = p.parent;
} else {
var id = c.$genHash();
if (p.$domKids.hasOwnProperty(id)) {
throw new Error("Inconsistent state for " + c + ", " + c.clazz.$name);
}
p.$domKids[id] = c;
break;
}
}
}
};
}
]);
// instantiate manager
pkg.$htmlElementMan = new pkg.HtmlElementMan();
if (zebkit.ui.event.FocusManager !== undefined) {
zebkit.ui.event.FocusManager.extend([
function requestFocus(c) {
this.$super(c);
var canvas = null;
// if the requested for the focus UI componet doesn't belong to a canvas that holds a native
// focus then let's give native focus to the canvas
if (c !== null && c !== this.focusOwner && (c.isDOMElement !== true || c.$getElementRootFocus() === null)) {
canvas = c.getCanvas();
if (canvas !== null && document.activeElement !== canvas.element) {
canvas.element.focus();
}
// if old focus onwer sits on canvas that doesn't hold the native focus
// let's clear it
if (this.focusOwner !== null && this.focusOwner.getCanvas() !== canvas) {
this.requestFocus(null);
}
} else if (this.focusOwner !== null && this.focusOwner.isDOMElement !== true) {
// here we check if focus owner belongs to a canvas that has native focus
// and if it is not true we give native focus to the canvas
canvas = this.focusOwner.getCanvas();
if (canvas !== null && document.activeElement !== canvas.element) {
canvas.element.focus();
}
}
},
function pointerPressed(e){
if (e.isAction()) {
// the problem is a target canvas element get mouse pressed
// event earlier than it gets focus what is inconsistent behavior
// to fix it a timer is used
if (document.activeElement !== e.source.getCanvas().element) {
var $this = this;
setTimeout(function() {
$this.requestFocus(e.source);
});
} else {
this.$$super(e);
}
}
}
]);
}
var ui = pkg.cd("..");
/**
* HTML Canvas native DOM element wrapper.
* @constructor
* @param {HTMLCanvas} [e] HTML canvas element to be wrapped as a zebkit UI
* component or nothing to create a new canvas element
* @class zebkit.ui.web.HtmlCanvas
* @extends zebkit.ui.web.HtmlElement
*/
pkg.HtmlCanvas = Class(pkg.HtmlElement, [
function(e) {
if (arguments.length > 0 && e !== null && e.tagName !== "CANVAS") {
throw new Error("Invalid element '" + e + "'");
}
/**
* Keeps rectangular "dirty" area of the canvas component
* @private
* @attribute $da
* @type {Object}
* { x:Integer, y:Integer, width:Integer, height:Integer }
*/
this.$da = { x: 0, y: 0, width: -1, height: 0 };
this.$super(arguments.length === 0 || e === null ? "canvas" : e);
// let HTML Canvas be WEB event transparent
this.$container.style["pointer-events"] = "none";
// check if this element has been created
if (arguments.length === 0 || e === null) {
// prevent canvas selection
this.element.onselectstart = function() { return false; };
}
},
function $clazz() {
this.$ContextMethods = {
reset : function(w, h) {
this.$curState = 0;
var s = this.$states[0];
s.srot = s.rotateVal = s.x = s.y = s.width = s.height = s.dx = s.dy = 0;
s.crot = s.sx = s.sy = 1;
s.width = w;
s.height = h;
this.setFont(ui.font);
this.setColor("white");
},
$init : function() {
// pre-allocate canvas save $states stack
this.$states = Array(70);
for(var i=0; i < this.$states.length; i++) {
var s = {};
s.srot = s.rotateVal = s.x = s.y = s.width = s.height = s.dx = s.dy = 0;
s.crot = s.sx = s.sy = 1;
this.$states[i] = s;
}
},
translate : function(dx, dy) {
if (dx !== 0 || dy !== 0) {
var c = this.$states[this.$curState];
c.x -= dx;
c.y -= dy;
c.dx += dx;
c.dy += dy;
this.$translate(dx, dy);
}
},
rotate : function(v) {
var c = this.$states[this.$curState];
c.rotateVal += v;
c.srot = Math.sin(c.rotateVal);
c.crot = Math.cos(c.rotateVal);
this.$rotate(v);
},
scale : function(sx, sy) {
var c = this.$states[this.$curState];
c.sx = c.sx * sx;
c.sy = c.sy * sy;
this.$scale(sx, sy);
},
save : function() {
this.$curState++;
var c = this.$states[this.$curState], cc = this.$states[this.$curState - 1];
c.x = cc.x;
c.y = cc.y;
c.width = cc.width;
c.height = cc.height;
c.dx = cc.dx;
c.dy = cc.dy;
c.sx = cc.sx;
c.sy = cc.sy;
c.srot = cc.srot;
c.crot = cc.crot;
c.rotateVal = cc.rotateVal;
this.$save();
return this.$curState - 1;
},
restoreAll : function() {
while(this.$curState > 0) {
this.restore();
}
},
restore : function() {
if (this.$curState === 0) {
throw new Error("Context restore history is empty");
}
this.$curState--;
this.$restore();
return this.$curState;
},
clipRect : function(x,y,w,h){
var c = this.$states[this.$curState];
if (c.x !== x || y !== c.y || w !== c.width || h !== c.height) {
var xx = c.x,
yy = c.y,
ww = c.width,
hh = c.height,
xw = x + w,
xxww = xx + ww,
yh = y + h,
yyhh = yy + hh;
c.x = x > xx ? x : xx;
c.width = (xw < xxww ? xw : xxww) - c.x;
c.y = y > yy ? y : yy;
c.height = (yh < yyhh ? yh : yyhh) - c.y;
if (c.x !== xx || yy !== c.y || ww !== c.width || hh !== c.height) {
// begin path is very important to have proper clip area
this.beginPath();
this.rect(x, y, w, h);
this.closePath();
this.clip();
}
}
}
};
},
function $prototype(clazz) {
this.$rotateValue = 0;
this.$crotate = false;
this.$scaleX = 1;
this.$scaleY = 1;
this.$translateX = 0;
this.$translateY = 0;
/**
* Canvas context
* @attribute $context
* @private
* @type {CanvasRenderingContext2D}
*/
this.$context = null;
// set border for canvas has to be set as zebkit border, since canvas
// is DOM component designed for rendering, so setting DOM border
// doesn't allow us to render zebkit border
this.setBorder = function(b) {
return ui.Panel.prototype.setBorder.apply(this, arguments);
};
this.rotate = function(r) {
this.$rotateValue += r;
if (this.$context !== null) {
this.$context.rotate(r);
}
this.vrp();
return this;
};
this.translate = function(dx, dy) {
this.$translateX += dx;
this.$translateY += dy;
if (this.$context !== null) {
this.$context.translate(this.$translateX,
this.$translateY);
}
this.vrp();
return this;
};
/**
* Rotate the component coordinate system around the component center.
* @param {Number} r a rotation value
* @chainable
* @method crotate
*/
this.crotate = function(r) {
this.$rotateValue += r;
this.$crotate = true;
if (this.$context !== null) {
var cx = Math.floor(this.width / 2),
cy = Math.floor(this.height / 2);
this.$context.translate(cx, cy);
this.$context.rotate(r);
this.$context.translate(-cx, -cy);
}
this.vrp();
return this;
};
this.scale = function(sx, sy) {
if (this.$context !== null) {
this.$context.scale(sx, sy);
}
this.$scaleX = this.$scaleX * sx;
this.$scaleY = this.$scaleY * sy;
this.vrp();
return this;
};
this.clearTransformations = function() {
this.$scaleX = 1;
this.$scaleY = 1;
this.$rotateValue = 0;
this.$crotate = false;
this.$translateX = 0;
this.$translateY = 0;
if (this.$context !== null) {
this.$context = zebkit.web.$canvas(this.element, this.width, this.height, true);
this.$context.reset(this.width, this.height);
}
this.vrp();
return this;
};
// set passing for canvas has to be set as zebkit padding, since canvas
// is DOM component designed for rendering, so setting DOM padding
// doesn't allow us to hold painting area proper
this.setPadding = function() {
return ui.Panel.prototype.setPadding.apply(this, arguments);
};
this.setSize = function(w, h) {
if (this.width !== w || h !== this.height) {
var pw = this.width,
ph = this.height;
this.$context = zebkit.web.$canvas(this.element, w, h);
// canvas has one instance of context, the code below
// test if the context has been already full filled
// with necessary methods and if it is not true
// fill it
if (this.$context.$states === undefined) {
zebkit.web.$extendContext(this.$context, clazz.$ContextMethods);
}
this.$context.reset(w, h);
// if canvas has been rotated apply the rotation to the context
if (this.$rotateValue !== 0) {
var cx = Math.floor(w / 2),
cy = Math.floor(h / 2);
if (this.$crotate) {
this.$context.translate(cx, cy);
}
this.$context.rotate(this.$rotateValue);
if (this.$crotate) {
this.$context.translate(-cx, -cy);
}
}
// if canvas has been scaled apply it to it
if (this.$scaleX !== 1 || this.$scaleY !== 1) {
this.$context.scale(this.$scaleX, this.$scaleY);
}
// if canvas has been scaled apply it to it
if (this.$translateX !== 0 || this.$translateY !== 0) {
this.$context.translate(this.$translateX, this.$translateY);
}
this.width = w;
this.height = h;
// sync state of visibility
// TODO: probably it should be in html element manager, manager has
// to catch resize event and if size is not 0 correct visibility
// now manager doesn't set style visibility to "visible" state
// if component size is zero
if (this.$container.style.visibility === "hidden" && this.isVisible) {
this.$container.style.visibility = "visible";
}
this.invalidate();
// TODO: think to replace it with vrp()
this.validate();
this.repaint();
if (w !== pw || h !== ph) {
this.resized(pw, ph);
}
}
return this;
};
/**
* Convert (x, y) location in
* @param {Integer} x a x coordinate
* @param {Integer} y an y coordinate
* @return {Array} two elements array where first
* element is converted x coordinate and the second element
* is converted y coordinate.
* @protected
* @method project
*/
this.project = function(x, y) {
var c = this.$context.$states[this.$context.$curState],
xx = x,
yy = y;
if (c.sx !== 1 || c.sy !== 1 || c.rotateVal !== 0) {
xx = Math.round((c.crot * x + y * c.srot) / c.sx);
yy = Math.round((y * c.crot - c.srot * x) / c.sy);
}
xx -= c.dx;
yy -= c.dy;
if (this.$crotate) {
// rotation relatively rect center should add correction basing
// on idea the center coordinate in new coordinate system has
// to have the same coordinate like it has in initial coordinate
// system
var cx = Math.floor(this.width / 2),
cy = Math.floor(this.height / 2),
dx = Math.round((c.crot * cx + cy * c.srot) / c.sx),
dy = Math.round((c.crot * cy - cx * c.srot) / c.sy);
xx -= (dx - cx);
yy -= (dy - cy);
}
return [ xx, yy ];
};
},
// TODO: make sure it is workable solution
function getComponentAt(x, y) {
if (this.$translateX !== 0 || this.$translateY !== 0 || this.$rotateValue !== 0 || this.$scaleX !== 1 || this.$scaleY !== 1) {
var xy = this.project(x, y);
return this.$super(xy[0], xy[1]);
} else {
return this.$super(x, y);
}
}
]);
/**
* Class that wrapped window component with own HTML Canvas.
* @param {zebkit.ui.Window} [content] a window component or window root. If
* content is not defined it will be instantiated automatically. If the component
* is not passed the new window component (zebkit.ui.Window) will be created.
* @constructor
* @extends zebkit.ui.web.HtmlCanvas
* @class zebkit.ui.web.HtmlWindow
*/
pkg.HtmlWindow = Class(pkg.HtmlCanvas, [
function(content) {
this.$super();
if (arguments.length === 0) {
this.win = new ui.Window();
} else if (zebkit.instanceOf(content, ui.Window)) {
this.win = content;
} else {
this.win = new ui.Window(content);
}
var $this = this;
this.win.getWinContainer = function() {
return $this;
};
/**
* Root window panel
* @attribute root
* @type {zebkit.ui.Panel}
*/
this.root = this.win.root;
this.setBorderLayout();
this.add("center", this.win);
},
function $prototype() {
/**
* Target window
* @attribute win
* @type {zebkit.ui.Window}
* @readOnly
*/
this.win = null;
this.winOpened = function(e) {
this.win.winOpened(e);
};
this.winActivated = function(e){
this.win.winActivated(e);
};
}
]);
pkg.HtmlButton = Class(pkg.HtmlElement, [
function(c) {
this.$super("button");
this.setAttribute("type", "button");
this.setContent(c);
var $this = this;
this.element.onclick = function() {
$this.fire("fired", $this);
};
}
]).events("fired");
/**
* WEB based HTML components wrapped with as zebkit components.
* @class zebkit.ui.web.HtmlFocusableElement
* @constructor
* @extends zebkit.ui.web.HtmlElement
*/
pkg.HtmlFocusableElement = Class(pkg.HtmlElement, [
function $prototype() {
this.$getElementRootFocus = function() {
return this.element;
};
}
]);
/**
* HTML input element wrapper class. The class can be used as basis class
* to wrap HTML elements that can be used to enter a textual information.
* @constructor
* @param {String} text a text the text input component has to be filled with
* @param {String} element an input element name
* @class zebkit.ui.web.HtmlTextInput
* @extends zebkit.ui.web.HtmlElement
*/
pkg.HtmlTextInput = Class(pkg.HtmlFocusableElement, [
function(text, e) {
if (text === null) {
text = "";
}
this.$super(e);
this.setAttribute("tabindex", 0);
this.setValue(text);
this.$keyUnifier = new zebkit.web.KeyEventUnifier(this.element, this);
this.$keyUnifier.preventDefault = function(e, key) {};
},
function $prototype() {
this.cursorType = ui.Cursor.TEXT;
this.$keyTyped = function(e) {
e.source = this;
ui.events.fire("keyTyped", e);
};
this.$keyPressed = function(e) {
e.source = this;
return ui.events.fire("keyPressed", e);
};
this.$keyReleased = function(e) {
e.source = this;
return ui.events.fire("keyReleased", e);
};
/**
* Get a text of the text input element
* @return {String} a text of the text input element
* @method getValue
*/
this.getValue = function() {
return this.element.value.toString();
};
/**
* Set the text
* @param {String} t a text
* @method setValue
* @chainable
*/
this.setValue = function(t) {
if (this.element.value !== t) {
this.element.value = t;
this.vrp();
}
return this;
};
}
]);
/**
* HTML input text element wrapper class. The class wraps standard HTML text field
* and represents it as zebkit UI component.
* @constructor
* @class zebkit.ui.web.HtmlTextField
* @param {String} [text] a text the text field component has to be filled with
* @extends zebkit.ui.web.HtmlTextInput
*/
pkg.HtmlTextField = Class(pkg.HtmlTextInput, [
function(text) {
this.$super(text, "input");
this.element.setAttribute("type", "text");
}
]);
/**
* HTML input text area element wrapper class. The class wraps standard HTML text area
* element and represents it as zebkit UI component.
* @constructor
* @param {String} [text] a text the text area component has to be filled with
* @class zebkit.ui.web.HtmlTextArea
* @extends zebkit.ui.web.HtmlTextInput
*/
pkg.HtmlTextArea = Class(pkg.HtmlTextInput, [
function(text) {
this.$super(text, "textarea");
this.element.setAttribute("rows", 10);
},
/**
* Set the text area resizeable or not resizeable.
* @param {Boolean} b true to make the text area component resizeable
* @method setResizeable
* @chainable
*/
function setResizeable(b) {
this.setStyle("resize", b === false ? "none" : "both");
return this;
}
]);
/**
* HTML Link component.
* @param {String} text a text of link
* @param {String} [href] an href of the link
* @extends zebkit.ui.web.HtmlElement
* @class zebkit.ui.web.HtmlLink
* @uses zebkit.EventProducer
* @constructor
* @event fired
* @param {zebkit.ui.web.Link} src a link that has been pressed
*/
pkg.HtmlLink = Class(pkg.HtmlElement, [
function(text, href) {
this.$super("a");
this.setContent(text);
this.setAttribute("href", arguments.length < 2 ? "#": href);
var $this = this;
this.element.onclick = function(e) {
$this.fire("fired", $this);
};
}
]).events("fired");
/**
* This special wrapper component that has to be used to put HtmlElement into
* "zebkit.ui.ScrollPan"
* @example
*
* var htmlElement = new zebkit.ui.web.HtmlElement();
* ...
* var scrollPan = new zebkit.ui.ScrollPan(new zebkit.ui.web.HtmlScrollContent(htmlElement));
*
*
* @param {zebkit.ui.web.HtmlElement} t target html component that is going to
* scrolled.
* @class zebkit.ui.web.HtmlScrollContent
* @extends zebkit.ui.web.HtmlElement
* @constructor
*/
pkg.HtmlScrollContent = Class(pkg.HtmlElement, [
function(t) {
this.$super();
this.scrollManager = new ui.ScrollPan.ContentPanScrollManager(t);
this.setLayout(new ui.ScrollPan.ContentPanLayout());
this.add("center",t);
this.setBackground("blue");
}
]);
var ui = pkg.cd("..");
/**
* The base class for HTML developing HTML layers.
* @class zebkit.ui.web.HtmlLayer
* @constructor
* @extends zebkit.ui.web.HtmlCanvas
*/
pkg.HtmlLayer = Class(pkg.HtmlCanvas, []);
/**
* Root layer implementation. This is the simplest UI layer implementation
* where the layer always try grabbing all input event
* @class zebkit.ui.web.RootLayer
* @constructor
* @extends zebkit.ui.web.HtmlLayer
* @uses zebkit.ui.RootLayerMix
*/
pkg.RootLayer = Class(pkg.HtmlLayer, ui.RootLayerMix, [
function $clazz() {
this.layout = new zebkit.layout.RasterLayout();
}
]);
/**
* Window layer implementation.
* @class zebkit.ui.web.WinLayer
* @constructor
* @extends zebkit.ui.web.HtmlLayer
* @uses zebkit.ui.WinLayerMix
*/
pkg.WinLayer = Class(pkg.HtmlLayer, ui.WinLayerMix, [
function() {
this.$super();
// TODO: why 1000 and how to avoid z-index manipulation
// the layer has to be placed above other elements that are virtually
// inserted in the layer
this.element.style["z-index"] = 10000;
},
function $clazz() {
this.layout = new zebkit.layout.RasterLayout();
}
]);
/**
* Popup layer implementation.
* @class zebkit.ui.web.PopupLayer
* @constructor
* @extends zebkit.ui.web.HtmlLayer
* @uses zebkit.ui.PopupLayerMix
*/
pkg.PopupLayer = Class(pkg.HtmlLayer, ui.PopupLayerMix, [
function $clazz() {
this.layout = new ui.PopupLayerLayout([
function doLayout(target){
// TODO:
// prove of concept. if layer is active don't allow WEB events comes to upper layer
// since there can be another HtmlElement that should not be part of interaction
if (target.kids.length > 0) {
if (target.$container.style["pointer-events"] !== "auto") {
target.$container.style["pointer-events"] = "auto";
}
} else if (target.$container.style["pointer-events"] !== "none") {
target.$container.style["pointer-events"] = "none"; // make the layer transparent for pointer events
}
this.$super(target);
}
]);
}
]);
// TODO: dependencies to remove
// -- taskSets (util.js)
pkg.CanvasEvent = Class(zebkit.Event, []);
var ui = pkg.cd(".."),
COMP_EVENT = new ui.event.CompEvent();
// keep pointer owners (the component where cursor/finger placed in)
pkg.$pointerOwner = {};
pkg.$pointerPressedOwner = {};
/**
* zCanvas zebkit UI component class. This is starting point for building zebkit UI. The class is a wrapper
* for HTML5 Canvas element. The main goals of the class is catching all native HTML5 Canvas element events
* and translating its into Zebkit UI events.
*
* zCanvas instantiation can trigger a new HTML Canvas will be created and added to HTML DOM tree.
* It happens if developer doesn't pass an HTML Canvas element reference or an ID of existing HTML
* Canvas element. To re-use an existent in DOM tree HTML5 canvas element pass an id of the canvas
* element:
*
* // a new HTML canvas element is created and added into HTML DOM tree
* var canvas = zebkit.ui.zCanvas();
*
* // a new HTML canvas element is created into HTML DOM tree
* var canvas = zebkit.ui.zCanvas(400,500); // pass canvas size
*
* // stick to existent HTML canvas element
* var canvas = zebkit.ui.zCanvas("ExistentCanvasID");
*
* zCanvas has layered structure. Every layer is responsible for showing and controlling a dedicated
* type of UI elements like windows pop-up menus, tool tips and so on. To start building UI use root layer.
* The layer is standard zebkit UI panel that is accessible via "root" zCanvas field:
*
* // create canvas
* var canvas = zebkit.ui.zCanvas(400,500);
*
* // save reference to canvas root layer where
* // hierarchy of UI components have to be hosted
* var root = canvas.root;
*
* // fill root with UI components
* var label = new zebkit.ui.Label("Label");
* label.setBounds(10,10,100,50);
* root.add(label);
*
* @class zebkit.ui.zCanvas
* @extends zebkit.ui.web.HtmlCanvas
* @constructor
* @param {String|Canvas} [element] an ID of a HTML canvas element or reference to an HTML Canvas element.
* @param {Integer} [width] a width of an HTML canvas element
* @param {Integer} [height] a height of an HTML canvas element
*/
/**
* Implement the event handler method to catch canvas initialized event. The event is triggered once the
* canvas has been initiated and all properties listeners of the canvas are set upped. The event can be
* used to load saved data.
*
* var p = new zebkit.ui.zCanvas(300, 300, [
* function canvasInitialized() {
* // do something
* }
* ]);
*
* @event canvasInitialized
*/
ui.zCanvas = pkg.zCanvas = Class(pkg.HtmlCanvas, [
function(element, w, h) {
// no arguments
if (arguments.length === 0) {
w = 400;
h = 400;
element = null;
} else if (arguments.length === 1) {
w = -1;
h = -1;
} else if (arguments.length === 2) {
h = w;
w = element;
element = null;
}
// if passed element is string than consider it as
// an ID of an element that is already in DOM tree
if (element !== null && zebkit.isString(element)) {
var id = element;
element = document.getElementById(id);
// no canvas can be detected
if (element === null) {
throw new Error("Canvas id='" + id + "' element cannot be found");
}
}
/**
* Dictionary to track layers by its ids.
* @attribute $layers
* @private
* @type {Object}
*/
this.$layers = {};
this.$super(element);
// since zCanvas is top level element it doesn't have to have
// absolute position
this.$container.style.position = "relative";
// let canvas zCanvas listen WEB event
this.$container.style["pointer-events"] = "auto";
// if canvas is not yet part of HTML let's attach it to
// body.
if (this.$container.parentNode === null) {
document.body.appendChild(this.$container);
}
// force canvas to have a focus
if (this.element.getAttribute("tabindex") === null) {
this.element.setAttribute("tabindex", "1");
}
if (w < 0) {
w = this.element.offsetWidth;
}
if (h < 0) {
h = this.element.offsetHeight;
}
// !!!
// save canvas in list of created Zebkit canvases
// do it before calling setSize(w,h) method
this.clazz.$canvases.push(this);
this.setSize(w, h);
// sync canvas visibility with what canvas style says
var cvis = (this.element.style.visibility === "hidden" ? false : true);
if (this.isVisible !== cvis) {
this.setVisible(cvis);
}
// call event method if it is defined
if (this.canvasInitialized !== undefined) {
this.canvasInitialized();
}
//var $this = this;
// this method should clean focus if
// one of of a child DOM element gets focus
zebkit.web.$focusin(this.$container, function(e) {
// TODO: fix and uncomment
// if (e.target !== $this.$container &&
// e.target.parentNode !== null &&
// e.target.parentNode.getAttribute("data-zebcont") === null) // TODO: BUG, data-zebcont is not set anymore, use $canvases instead
// {
// ui.focusManager.requestFocus(null);
// } else {
// // clear focus if a focus owner component is hosted with another zCanvas
// if (e.target === $this.$container &&
// ui.focusManager.focusOwner !== null &&
// ui.focusManager.focusOwner.getCanvas() !== $this)
// {
// ui.focusManager.requestFocus(null);
// }
// }
}, true);
},
function $clazz () {
this.$canvases = [];
this.$getCanvasByElement = function(e) {
for (var i = 0; i < this.$canvases.length; i++) {
if (this.$canvases[i] === e) {
return this.$canvases[i];
}
}
return null;
};
},
function $prototype() {
/**
* Indicates this the root canvas element
* @attribute $isRootCanvas
* @type {Boolean}
* @private
* @default true
* @readOnly
*/
this.$isRootCanvas = true;
/**
* Indicate if the canvas has to be stretched to fill the whole view port area.
* @type {Boolean}
* @attribute isSizeFull
* @readOnly
*/
this.isSizeFull = false;
this.offx = this.offy = 0;
/**
* Transforms the pageX coordinate into relatively to the canvas origin
* coordinate taking in account the canvas transformation
* @param {Number} pageX a pageX coordinate
* @param {Number} pageY a pageY coordinate
* @return {Integer} an x coordinate that is relative to the canvas origin
* @method $toElementX
* @protected
*/
this.$toElementX = function(pageX, pageY) {
// offset has to be added here since "calcOffset" can called (for instance page reloading)
// to early
pageX -= (this.offx);
pageY -= (this.offy);
var c = this.$context.$states[this.$context.$curState];
return ((c.sx !== 1 || c.sy !== 1 || c.rotateVal !== 0) ? Math.round((c.crot * pageX + pageY * c.srot)/c.sx)
: pageX) - c.dx;
};
/**
* Transforms the pageY coordinate into relatively to the canvas origin
* coordinate taking in account the canvas transformation
* @param {Number} pageX a pageX coordinate
* @param {Number} pageY a pageY coordinate
* @return {Integer} an y coordinate that is relative to the canvas origin
* @method $toElementY
* @protected
*/
this.$toElementY = function(pageX, pageY) {
// offset has to be added here since "calcOffset" can called (for instance page reloading)
// to early
pageX -= (this.offx);
pageY -= (this.offy);
var c = this.$context.$states[this.$context.$curState];
return ((c.sx !== 1 || c.sy !== 1 || c.rotateVal !== 0) ? Math.round((pageY * c.crot - c.srot * pageX)/c.sy)
: pageY) - c.dy;
};
this.load = function(jsonPath){
return this.root.load(jsonPath);
};
// TODO: may be rename to dedicated method $doWheelScroll
this.$doScroll = function(dx, dy, src) {
if (src === "wheel" && pkg.$pointerOwner.mouse !== null && pkg.$pointerOwner.mouse !== undefined) {
var owner = pkg.$pointerOwner.mouse;
while (owner !== null && owner.doScroll === undefined) {
owner = owner.parent;
}
if (owner !== null) {
return owner.doScroll(dx, dy, src);
}
}
return false;
};
/**
* Catches key typed events, adjusts and distributes it to UI hierarchy
* @param {zebkit.ui.event.KeyEvent} e an event
* @private
* @method $keyTyped
* @return {Boolean} true if the event has been processed
*/
this.$keyTyped = function(e) {
if (ui.focusManager.focusOwner !== null) {
e.source = ui.focusManager.focusOwner;
return ui.events.fire("keyTyped", e);
} else {
return false;
}
};
/**
* Catches key pressed events, adjusts and distributes it to UI hierarchy
* @param {zebkit.ui.event.KeyEvent} e an event
* @private
* @method $keyPressed
* @return {Boolean} true if the event has been processed
*/
this.$keyPressed = function(e) {
// go through layers to detect layerKeyPressed event handler
for(var i = this.kids.length - 1;i >= 0; i--){
var l = this.kids[i];
if (l.layerKeyPressed !== undefined && l.layerKeyPressed(e) === true) {
return true;
}
}
if (ui.focusManager.focusOwner !== null) {
e.source = ui.focusManager.focusOwner;
return ui.events.fire("keyPressed", e);
} else {
e.source = this;
return ui.events.fire("keyPressed", e);
}
};
/**
* Catches key released events, adjusts and distributes it to UI hierarchy
* @param {zebkit.ui.event.KeyEvent} e an event
* @private
* @method $keyReleased
* @return {Boolean} true if the event has been processed
*/
this.$keyReleased = function(e){
if (ui.focusManager.focusOwner !== null) {
e.source = ui.focusManager.focusOwner;
return ui.events.fire("keyReleased", e);
} else {
return false;
}
};
/**
* Catches pointer entered events, adjusts and distributes it to UI hierarchy
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerEntered
*/
this.$pointerEntered = function(e) {
// TODO: review it quick and dirty fix try to track a situation
// when the canvas has been moved
this.recalcOffset();
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY),
d = this.getComponentAt(x, y),
o = pkg.$pointerOwner.hasOwnProperty(e.identifier) ? pkg.$pointerOwner[e.identifier] : null;
// also correct current component on that pointer is located
if (d !== o) {
// if pointer owner is not null but doesn't match new owner
// generate pointer exit and clean pointer owner
if (o !== null) {
delete pkg.$pointerOwner[e.identifier];
ui.events.fire("pointerExited", e.update(o, x, y));
}
// if new pointer owner is not null and enabled
// generate pointer entered event ans set new pointer owner
if (d !== null && d.isEnabled === true){
delete pkg.$pointerOwner[e.identifier];
ui.events.fire("pointerEntered", e.update(d, x, y));
}
}
};
/**
* Catches pointer exited events, adjusts and distributes it to UI hierarchy
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerExited
*/
this.$pointerExited = function(e) {
var o = pkg.$pointerOwner.hasOwnProperty(e.identifier) ? pkg.$pointerOwner[e.identifier] : null;
if (o !== null) {
delete pkg.$pointerOwner[e.identifier];
return ui.events.fire("pointerExited", e.update(o,
this.$toElementX(e.pageX, e.pageY),
this.$toElementY(e.pageX, e.pageY)));
}
};
/**
* Catches pointer moved events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerMoved
*/
this.$pointerMoved = function(e){
// if a pointer button has not been pressed handle the normal pointer moved event
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY),
d = this.getComponentAt(x, y),
o = pkg.$pointerOwner.hasOwnProperty(e.identifier) ? pkg.$pointerOwner[e.identifier] : null,
b = false;
// check if pointer already inside a component
if (o !== null) {
if (d !== o) {
delete pkg.$pointerOwner[e.identifier];
b = ui.events.fire("pointerExited", e.update(o, x, y));
if (d !== null && d.isEnabled === true) {
pkg.$pointerOwner[e.identifier] = d;
b = ui.events.fire("pointerEntered", e.update(d, x, y)) || b;
}
} else if (d !== null && d.isEnabled === true) {
b = ui.events.fire("pointerMoved", e.update(d, x, y));
}
} else if (d !== null && d.isEnabled === true) {
pkg.$pointerOwner[e.identifier] = d;
b = ui.events.fire("pointerEntered", e.update(d, x, y));
}
return b;
};
/**
* Catches pointer drag started events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerDragStarted
*/
this.$pointerDragStarted = function(e) {
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY),
d = this.getComponentAt(x, y);
// if target component can be detected fire pointer start dragging and
// pointer dragged events to the component
if (d !== null && d.isEnabled === true) {
return ui.events.fire("pointerDragStarted", e.update(d, x, y));
}
return false;
};
/**
* Catches pointer dragged events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerDragged
*/
this.$pointerDragged = function(e){
if (pkg.$pointerOwner.hasOwnProperty(e.identifier)) {
return ui.events.fire("pointerDragged", e.update(pkg.$pointerOwner[e.identifier],
this.$toElementX(e.pageX, e.pageY),
this.$toElementY(e.pageX, e.pageY)));
}
return false;
};
/**
* Catches pointer drag ended events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerDragEnded
*/
this.$pointerDragEnded = function(e) {
if (pkg.$pointerOwner.hasOwnProperty(e.identifier)) {
return ui.events.fire("pointerDragEnded", e.update(pkg.$pointerOwner[e.identifier],
this.$toElementX(e.pageX, e.pageY),
this.$toElementY(e.pageX, e.pageY)));
}
return false;
};
this.$isAbsorbedByLayer = function(id, method, e) {
e.id = id;
for(var i = this.kids.length - 1; i >= 0; i--){
var layer = this.kids[i];
if (layer[method] !== undefined) {
if (layer[method](e) === true) {
return true;
}
}
}
return false;
};
/**
* Catches pointer clicked events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerClicked
*/
this.$pointerClicked = function(e) {
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY),
d = this.getComponentAt(x, y);
// zoom in zoom out can bring to a situation
// d is null, in this case offset should be recalculated
// TODO: the cause of the issue has to be investigated deeper
if (d === null) {
this.recalcOffset();
x = this.$toElementX(e.pageX, e.pageY);
y = this.$toElementY(e.pageX, e.pageY);
d = this.getComponentAt(x, y);
}
if (d !== null) {
e = e.update(d, x, y);
if (this.$isAbsorbedByLayer("pointerClicked", "layerPointerClicked", e)) {
return true;
} else {
return ui.events.fire("pointerClicked", e);
}
} else {
return false;
}
};
this.$pointerDoubleClicked = function(e) {
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY),
d = this.getComponentAt(x, y);
return d !== null ? ui.events.fire("pointerDoubleClicked", e.update(d, x, y))
: false;
};
/**
* Catches pointer released events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerReleased
*/
this.$pointerReleased = function(e) {
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY);
// release pressed state
if (pkg.$pointerPressedOwner.hasOwnProperty(e.identifier)) {
try {
e = e.update(pkg.$pointerPressedOwner[e.identifier], x, y);
if (this.$isAbsorbedByLayer("pointerReleased", "layerPointerReleased", e) !== true) {
ui.events.fire("pointerReleased", e);
}
} finally {
delete pkg.$pointerPressedOwner[e.identifier];
}
}
// mouse released can happen at new location, so move owner has to be corrected
// and mouse exited entered event has to be generated.
// the correction takes effect if we have just completed dragging or mouse pressed
// event target doesn't match pkg.$pointerOwner
if (e.pointerType === "mouse" && (e.pressPageX !== e.pageX || e.pressPageY !== e.pageY)) {
var nd = this.getComponentAt(x, y),
po = this.getComponentAt(this.$toElementX(e.pressPageX, e.pressPageY),
this.$toElementY(e.pressPageX, e.pressPageY));
if (nd !== po) {
if (po !== null) {
delete pkg.$pointerOwner[e.identifier];
ui.events.fire("pointerExited", e.update(po, x, y));
}
if (nd !== null && nd.isEnabled === true){
pkg.$pointerOwner[e.identifier] = nd;
ui.events.fire("pointerEntered", e.update(nd, x, y));
}
}
}
};
/**
* Catches pointer pressed events, adjusts and distributes it to UI hierarchy.
* @param {zebkit.ui.event.PointerEvent} e an event
* @private
* @method $pointerPressed
*/
this.$pointerPressed = function(e) {
var x = this.$toElementX(e.pageX, e.pageY),
y = this.$toElementY(e.pageX, e.pageY);
// free previous pointer pressed state if it was hung up
if (pkg.$pointerPressedOwner.hasOwnProperty(e.identifier)) {
try {
ui.events.fire("pointerReleased", e.update(pkg.$pointerPressedOwner[e.identifier], x, y));
} finally {
delete pkg.$pointerPressedOwner[e.identifier];
}
}
e.source = null;
e.x = x;
e.y = y;
if (this.$isAbsorbedByLayer("pointerPressed", "layerPointerPressed", e)) {
return true;
}
var d = this.getComponentAt(x, y);
if (d !== null && d.isEnabled === true) {
if (pkg.$pointerOwner[e.identifier] !== d) {
pkg.$pointerOwner[e.identifier] = d;
ui.events.fire("pointerEntered", e.update(d, x, y));
}
pkg.$pointerPressedOwner[e.identifier] = d;
// TODO: prove the solution (return true) !?
if (ui.events.fire("pointerPressed", e.update(d, x, y)) === true) {
delete pkg.$pointerPressedOwner[e.identifier];
return true;
}
}
return false;
};
this.getComponentAt = function(x, y) {
// goes through the layers from top to bottom
for(var i = this.kids.length; --i >= 0; ){
var c = this.kids[i].getComponentAt(x, y);
if (c !== null) {
// detect a composite parent component that catches
// input and return the found composite
// TODO: probably this is not good place to detect composition, but it is done here
// since real destination component has to be detected before delegating it to event
// manager. One of the reason is adjusting (pointer) event coordinates to found
// destination component. Event manager knows nothing about an event structure,
// whether it has or not coordinates.
var p = c;
while ((p = p.parent) !== null) {
// test if the parent catches input events (what means the parent is a composite component)
// and store the composite as result
if (p.catchInput !== undefined && (p.catchInput === true || (p.catchInput !== false && p.catchInput(c)))) {
c = p;
}
}
return c;
}
}
return null;
};
this.recalcOffset = function() {
// calculate the DOM element offset relative to window taking in account scrolling
var poffx = this.offx,
poffy = this.offy,
ba = this.$container.getBoundingClientRect();
this.offx = Math.round(ba.left + zebkit.web.$measure(this.$container, "border-left-width") +
zebkit.web.$measure(this.$container, "padding-left") + window.pageXOffset);
this.offy = Math.round(ba.top + zebkit.web.$measure(this.$container, "padding-top" ) +
zebkit.web.$measure(this.$container, "border-top-width") + window.pageYOffset);
if (this.offx !== poffx || this.offy !== poffy) {
// force to fire component re-located event
this.relocated(this, poffx, poffy);
}
};
/**
* Get the canvas layer by the specified layer ID. Layer is a children component
* of the canvas UI component. Every layer has an ID assigned to it the method
* actually allows developers to get the canvas children component by its ID
* @param {String} id a layer ID
* @return {zebkit.ui.Panel} a layer (children) component
* @method getLayer
*/
this.getLayer = function(id) {
return this.$layers[id];
};
// override relocated and resized
// to prevent unnecessary repainting
this.relocated = function(px,py) {
COMP_EVENT.source = this;
COMP_EVENT.px = px;
COMP_EVENT.py = py;
ui.events.fire("compMoved", COMP_EVENT);
};
this.resized = function(pw,ph) {
COMP_EVENT.source = this;
COMP_EVENT.prevWidth = pw;
COMP_EVENT.prevHeight = ph;
ui.events.fire("compSized", COMP_EVENT);
// don't forget repaint it
this.repaint();
};
this.$initListeners = function() {
// TODO: hard-coded
new zebkit.web.PointerEventUnifier(this.$container, this);
new zebkit.web.KeyEventUnifier(this.element, this); // element has to be used since canvas is
// styled to have focus and get key events
new zebkit.web.MouseWheelSupport(this.$container, this);
};
/**
* Force the canvas to occupy the all available view port area
* @param {Boolean} b true to force the canvas be stretched over all
* available view port area
* @chainable
* @method setSizeFull
*/
this.setSizeFull = function(b) {
if (this.isSizeFull !== b) {
this.isSizeFull = b;
if (b === true) {
if (zebkit.web.$contains(this.$container) !== true) {
throw new Error("zCanvas is not a part of DOM tree");
}
this.setLocation(0, 0);
// adjust body to kill unnecessary gap for in-line-block zCanvas element
// otherwise body size will be slightly horizontally bigger than visual
// view-port height what causes scroll appears
document.body.style["font-size"] = "0px";
var ws = zebkit.web.$viewPortSize();
this.setSize(ws.width, ws.height);
}
}
return this;
};
},
function setSize(w, h) {
if (this.width !== w || h !== this.height) {
this.$super(w, h);
// let know to other zebkit canvases that
// the size of an element on the page has
// been updated and they have to correct
// its anchor.
pkg.$elBoundsUpdated();
}
return this;
},
function setVisible(b) {
var prev = this.isVisible;
this.$super(b);
// Since zCanvas has no parent component calling the super
// method above doesn't trigger repainting. So, do it here.
if (b !== prev) {
this.repaint();
}
return this;
},
function vrp() {
this.$super();
if (zebkit.web.$contains(this.element) && this.element.style.visibility === "visible") {
this.repaint();
}
},
function kidAdded(i,constr,c){
if (this.$layers.hasOwnProperty(c.id)) {
throw new Error("Layer '" + c.id + "' already exist");
}
this.$layers[c.id] = c;
if (c.id === "root") {
this.root = c;
}
this.$super(i, constr, c);
},
function kidRemoved(i, c, ctr) {
delete this.$layers[c.id];
if (c.id === "root") {
this.root = null;
}
this.$super(i, c, ctr);
}
]);
// canvases location has to be corrected if document layout is invalid
pkg.$elBoundsUpdated = function() {
for(var i = pkg.zCanvas.$canvases.length - 1; i >= 0; i--) {
var c = pkg.zCanvas.$canvases[i];
if (c.isSizeFull === true) {
//c.setLocation(window.pageXOffset, -window.pageYOffset);
var ws = zebkit.web.$viewPortSize();
// browser (mobile) can reduce size of browser window by
// the area a virtual keyboard occupies. Usually the
// content scrolls up to the size the VK occupies, so
// to leave zebkit full screen content in the window
// with the real size (not reduced) size take in account
// scrolled metrics
c.setSize(ws.width + window.pageXOffset,
ws.height + window.pageYOffset);
}
c.recalcOffset();
}
};
var $wrt = null, $winSizeUpdated = false, $wpw = -1, $wph = -1;
window.addEventListener("resize", function(e) {
if ($wpw !== window.innerWidth || $wph !== window.innerHeight) {
$wpw = window.innerWidth;
$wph = window.innerHeight;
if ($wrt !== null) {
$winSizeUpdated = true;
} else {
$wrt = zebkit.util.tasksSet.run(
function() {
if ($winSizeUpdated === false) {
pkg.$elBoundsUpdated();
this.shutdown();
$wrt = null;
}
$winSizeUpdated = false;
}, 200, 150
);
}
}
}, false);
window.onbeforeunload = function(e) {
var msgs = [];
for (var i = pkg.zCanvas.$canvases.length - 1; i >= 0; i--) {
if (pkg.zCanvas.$canvases[i].saveBeforeLeave !== undefined) {
var m = pkg.zCanvas.$canvases[i].saveBeforeLeave();
if (m !== null && m !== undefined) {
msgs.push(m);
}
}
}
if (msgs.length > 0) {
var message = msgs.join(" ");
if (e === undefined) {
e = window.event;
}
if (e) {
e.returnValue = message;
}
return message;
}
};
// TODO: this is deprecated events that can have significant impact to
// page performance. That means it has to be removed and replace with something
// else
//
// bunch of handlers to track HTML page metrics update
// it is necessary since to correct zebkit canvases anchor
// and track when a canvas has been removed
document.addEventListener("DOMNodeInserted", function(e) {
pkg.$elBoundsUpdated();
}, false);
document.addEventListener("DOMNodeRemoved", function(e) {
// remove canvas from list
for(var i = pkg.zCanvas.$canvases.length - 1; i >= 0; i--) {
var canvas = pkg.zCanvas.$canvases[i];
if (zebkit.web.$contains(canvas.element) !== true) {
pkg.zCanvas.$canvases.splice(i, 1);
if (canvas.saveBeforeLeave !== undefined) {
canvas.saveBeforeLeave();
}
}
}
pkg.$elBoundsUpdated();
}, false);
var ui = pkg.cd("..");
/**
* Simple video panel that can be used to play a video:
*
*
* // create canvas, add video panel to the center and
* // play video
* var canvas = zebkit.ui.zCanvas(500,500).root.properties({
* layout: new zebkit.layout.BorderLayout(),
* center: new zebkit.ui.web.VideoPan("trailer.mpg")
* });
*
*
* @param {String} url an URL to a video
* @class zebkit.ui.web.VideoPan
* @extends zebkit.ui.Panel
* @constructor
*/
pkg.VideoPan = Class(ui.Panel, [
function(src) {
var $this = this;
/**
* Original video DOM element that is created
* to play video
* @type {Video}
* @readOnly
* @attribute video
*/
this.video = document.createElement("video");
this.source = document.createElement("source");
this.source.setAttribute("src", src);
this.video.appendChild(this.source);
this.$super();
// canplaythrough is video event
this.video.addEventListener("canplaythrough", function() {
$this.fire("playbackStateUpdated", [$this, "ready"]);
$this.repaint();
$this.$continuePlayback();
}, false);
this.video.addEventListener("ended", function() {
$this.fire("playbackStateUpdated", [$this, "end"]);
$this.$interruptCancelTask();
}, false);
this.video.addEventListener("pause", function() {
$this.fire("playbackStateUpdated", [$this, "pause"]);
$this.$interruptCancelTask();
}, false);
this.video.addEventListener("play", function() {
$this.$continuePlayback();
$this.fire("playbackStateUpdated", [$this, "play"]);
}, false);
// progress event indicates a loading progress
// the event is useful to detect recovering from network
// error
this.video.addEventListener("progress", function() {
// if playback has been postponed due to an error
// let's say that problem seems fixed and delete
// the cancel task
if ($this.$cancelTask !== null) {
$this.$interruptCancelTask();
// detect if progress event has to try to start animation that has not been
// started yet or has been canceled for a some reason
if ($this.video.paused === false) {
$this.$continuePlayback();
$this.fire("playbackStateUpdated", [$this, "continue"]);
}
}
}, false);
this.source.addEventListener("error", function(e) {
$this.$interruptCancelTask();
$this.$lastError = e.toString();
$this.fire("playbackStateUpdated", [$this, "error"]);
$this.repaint();
$this.pause();
}, false);
this.video.addEventListener("stalled", function() {
$this.$cancelPlayback();
}, false);
this.video.addEventListener("loadedmetadata", function (e) {
$this.videoWidth = this.videoWidth;
$this.videoHeight = this.videoHeight;
$this.$aspectRatio = $this.videoHeight > 0 ? $this.videoWidth / $this.videoHeight : 0;
$this.vrp();
}, false);
},
function $clazz() {
this.SignLabel = Class(ui.Panel, [
function $clazz() {
this.font = new zebkit.Font("bold", 18);
},
function setColor(c) {
this.kids[0].setColor(c);
return this;
},
function(title) {
this.$super(new zebkit.layout.FlowLayout("center", "center"));
this.add(new ui.Label(title).setFont(this.clazz.font));
this.setBorder(new zebkit.draw.Border("gray", 1, 8));
this.setPadding(6);
this.setBackground("white");
this.setColor("black");
}
]);
},
function $prototype(clazz) {
this.videoWidth = this.videoHeight = 0;
this.cancelationTimeout = 20000; // 20 seconds
this.showSign = true;
this.$animStallCounter = this.$aspectRatio = 0;
this.$adjustProportions = true;
this.$lastError = this.$videoBound = this.$cancelTask = null;
this.$animCurrentTime = -1;
this.views = {
pause : new clazz.SignLabel("Pause, press to continue").toView(),
replay : new clazz.SignLabel("Press to re-play").toView(),
play : new clazz.SignLabel("Press to play").toView(),
error : new clazz.SignLabel("Failed, press to re-try").setColor("red").toView(),
waiting: new clazz.SignLabel("Waiting ...").setColor("orange").toView()
};
this.paint = function(g) {
if (this.video.paused === false &&
this.video.ended === false &&
this.$cancelTask === null )
{
if (this.video.currentTime !== this.$animCurrentTime) {
this.$animStallCounter = 0;
this.repaint();
} else {
if (this.$animStallCounter > 180) {
this.$cancelPlayback();
} else {
this.$animStallCounter++;
this.repaint();
}
}
}
this.$animCurrentTime = this.video.currentTime;
if (this.$videoBound === null) {
this.calcVideoBound();
}
g.drawImage(this.video, this.$videoBound.x,
this.$videoBound.y,
this.$videoBound.width,
this.$videoBound.height);
// draw status sign
if (this.showSign) {
var sign = null;
if (this.$lastError !== null) {
sign = this.views.error;
} else {
if (this.$cancelTask !== null) {
sign = this.views.waiting;
} else if (this.video.ended) {
sign = this.views.replay;
} else if (this.video.paused) {
if (this.video.currentTime === 0) {
sign = this.views.play;
} else {
sign = this.views.pause;
}
}
}
if (sign !== null) {
this.paintViewAt(g, "center", "center", sign);
}
}
};
/**
* Set autoplay for video
* @param {Boolean} b an autoplay flag
* @method autoplay
* @chainable
*/
this.autoplay = function(b) {
this.video.autoplay = b;
return this;
};
/**
* Pause video
* @method pause
* @chainable
*/
this.pause = function() {
if (this.video.paused === false) {
this.video.pause();
this.repaint();
}
return this;
};
/**
* Mute sound
* @param {Boolean} b true to mute the video sound
* @method mute
* @chainable
*/
this.mute = function(b) {
this.video.muted = b;
return this;
};
/**
* Start or continue playing video
* @method play
* @chainable
*/
this.play = function() {
if (this.video.paused === true) {
if (this.$lastError !== null) {
this.$lastError = null;
this.video.load();
}
this.video.play();
this.repaint();
}
return this;
};
/**
* Adjust video proportion to fill maximal space with correct ratio
* @param {Boolean} b true if the video proportion has to be adjusted
* @method adjustProportions
* @chainable
*/
this.adjustProportions = function(b) {
if (this.$adjustProportions !== b) {
this.$adjustProportions = b;
this.vrp();
}
return this;
};
this.calcPreferredSize = function(target) {
return {
width : this.videoWidth,
height : this.videoHeight
};
};
this.pointerClicked = function(e) {
if (this.isPaused()) {
this.play();
} else {
this.pause();
}
};
/**
* Check if the video is paused
* @method isPaused
* @return {Boolean} true if the video has been paused
*/
this.isPaused = function() {
return this.video.paused;
};
/**
* Check if the video is ended
* @method isEnded
* @return {Boolean} true if the video has been ended
*/
this.isEnded = function() {
return this.video.ended;
};
this.getDuration = function() {
return this.video.duration;
};
this.compSized = function(e) {
this.$calcVideoBound();
};
this.recalc = function() {
this.$calcVideoBound();
};
this.$calcVideoBound = function() {
this.$videoBound = {
x : this.getLeft(),
y : this.getTop(),
width : this.width - this.getLeft() - this.getBottom(),
height : this.height - this.getTop() - this.getBottom()
};
if (this.$adjustProportions === true && this.$aspectRatio !== 0) {
var ar = this.$videoBound.width / this.$videoBound.height;
// ar = 3:1 ar' = 10:3 ar' > ar
// +-------+ +--------------+
// | video | | canvas | => decrease canvas width proportionally ar/ar'
// +-------+ +--------------+
//
// ar = 3:1 ar' = 2:1 ar' < ar
// +-----------+ +------+
// | video | |canvas| => decrease canvas height proportionally ar'/ar
// +-----------+ +------+
if (ar < this.$aspectRatio) {
this.$videoBound.height = Math.floor((this.$videoBound.height * ar) / this.$aspectRatio);
} else {
this.$videoBound.width = Math.floor((this.$videoBound.width * this.$aspectRatio)/ ar);
}
this.$videoBound.x = Math.floor((this.width - this.$videoBound.width )/2);
this.$videoBound.y = Math.floor((this.height - this.$videoBound.height)/2);
}
};
this.$continuePlayback = function() {
this.$interruptCancelTask();
if (this.video.paused === false && this.video.ended === false) {
this.$animCurrentTime = this.video.currentTime;
this.$animStallCounter = 0;
this.repaint();
}
};
this.$cancelPlayback = function() {
if (this.video.paused === true || this.video.ended === true) {
this.$interruptCancelTask();
} else {
if (this.$cancelTask === null) {
var $this = this;
this.$postponedTime = new Date().getTime();
this.$cancelTask = zebkit.environment.setInterval(function() {
var dt = new Date().getTime() - $this.$postponedTime;
if (dt > $this.cancelationTimeout) {
try {
if ($this.video.paused === false) {
$this.$lastError = "Playback failed";
$this.pause();
$this.repaint();
$this.fire("playbackStateUpdated", [$this, "error"]);
}
} finally {
$this.$interruptCancelTask();
}
} else {
$this.fire("playbackStateUpdated", [$this, "wait"]);
}
}, 200);
}
}
};
this.$interruptCancelTask = function() {
if (this.$cancelTask !== null) {
zebkit.environment.clearInterval(this.$cancelTask);
this.$postponedTime = this.$cancelTask = null;
}
};
}
]).events("playbackStateUpdated");
},true);