Ext.define(null, {
override: 'Ext.ux.gauge.needle.Abstract',
compatibility: Ext.isIE10p,
setTransform: function(centerX, centerY, rotation) {
var needleGroup = this.getNeedleGroup();
this.callParent([
centerX,
centerY,
rotation
]);
needleGroup.set({
transform: getComputedStyle(needleGroup.dom).getPropertyValue('transform')
});
},
updateStyle: function(style) {
var pathElement;
this.callParent([
style
]);
if (Ext.isObject(style) && 'transform' in style) {
pathElement = this.getNeedlePath();
pathElement.set({
transform: getComputedStyle(pathElement.dom).getPropertyValue('transform')
});
}
}
});
/**
* This is a base class for more advanced "simlets" (simulated servers). A simlet is asked
* to provide a response given a {@link Ext.ux.ajax.SimXhr} instance.
*/
Ext.define('Ext.ux.ajax.Simlet', function() {
var urlRegex = /([^?#]*)(#.*)?$/,
dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/,
intRegex = /^[+-]?\d+$/,
floatRegex = /^[+-]?\d+\.\d+$/;
function parseParamValue(value) {
var m;
if (Ext.isDefined(value)) {
value = decodeURIComponent(value);
if (intRegex.test(value)) {
value = parseInt(value, 10);
} else if (floatRegex.test(value)) {
value = parseFloat(value);
} else if (!!(m = dateRegex.exec(value))) {
value = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]));
}
}
return value;
}
return {
alias: 'simlet.basic',
isSimlet: true,
responseProps: [
'responseText',
'responseXML',
'status',
'statusText',
'responseHeaders'
],
/**
* @cfg {String/Function} responseText
*/
/**
* @cfg {String/Function} responseXML
*/
/**
* @cfg {Object/Function} responseHeaders
*/
/**
* @cfg {Number/Function} status
*/
status: 200,
/**
* @cfg {String/Function} statusText
*/
statusText: 'OK',
constructor: function(config) {
Ext.apply(this, config);
},
doGet: function(ctx) {
return this.handleRequest(ctx);
},
doPost: function(ctx) {
return this.handleRequest(ctx);
},
doRedirect: function(ctx) {
return false;
},
doDelete: function(ctx) {
var me = this,
xhr = ctx.xhr,
records = xhr.options.records;
me.removeFromData(ctx, records);
},
/**
* Performs the action requested by the given XHR and returns an object to be applied
* on to the XHR (containing `status`, `responseText`, etc.). For the most part,
* this is delegated to `doMethod` methods on this class, such as `doGet`.
*
* @param {Ext.ux.ajax.SimXhr} xhr The simulated XMLHttpRequest instance.
* @return {Object} The response properties to add to the XMLHttpRequest.
*/
exec: function(xhr) {
var me = this,
ret = {},
method = 'do' + Ext.String.capitalize(xhr.method.toLowerCase()),
// doGet
fn = me[method];
if (fn) {
ret = fn.call(me, me.getCtx(xhr.method, xhr.url, xhr));
} else {
ret = {
status: 405,
statusText: 'Method Not Allowed'
};
}
return ret;
},
getCtx: function(method, url, xhr) {
return {
method: method,
params: this.parseQueryString(url),
url: url,
xhr: xhr
};
},
handleRequest: function(ctx) {
var me = this,
ret = {},
val;
Ext.Array.forEach(me.responseProps, function(prop) {
if (prop in me) {
val = me[prop];
if (Ext.isFunction(val)) {
val = val.call(me, ctx);
}
ret[prop] = val;
}
});
return ret;
},
openRequest: function(method, url, options, async) {
var ctx = this.getCtx(method, url),
redirect = this.doRedirect(ctx),
xhr;
if (options.action === 'destroy') {
method = 'delete';
}
if (redirect) {
xhr = redirect;
} else {
xhr = new Ext.ux.ajax.SimXhr({
mgr: this.manager,
simlet: this,
options: options
});
xhr.open(method, url, async);
}
return xhr;
},
parseQueryString: function(str) {
var m = urlRegex.exec(str),
ret = {},
key, value, i, n;
if (m && m[1]) {
var pair,
parts = m[1].split('&');
for (i = 0 , n = parts.length; i < n; ++i) {
if ((pair = parts[i].split('='))[0]) {
key = decodeURIComponent(pair.shift());
value = parseParamValue((pair.length > 1) ? pair.join('=') : pair[0]);
if (!(key in ret)) {
ret[key] = value;
} else if (Ext.isArray(ret[key])) {
ret[key].push(value);
} else {
ret[key] = [
ret[key],
value
];
}
}
}
}
return ret;
},
redirect: function(method, url, params) {
switch (arguments.length) {
case 2:
if (typeof url == 'string') {
break;
};
params = url;
// fall...
case 1:
url = method;
method = 'GET';
break;
}
if (params) {
url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
}
return this.manager.openRequest(method, url);
},
removeFromData: function(ctx, records) {
var me = this,
data = me.getData(ctx),
model = (ctx.xhr.options.proxy && ctx.xhr.options.proxy.getModel()) || {},
idProperty = model.idProperty || 'id';
Ext.each(records, function(record) {
var id = record.get(idProperty);
for (var i = data.length; i-- > 0; ) {
if (data[i][idProperty] === id) {
me.deleteRecord(i);
break;
}
}
});
}
};
}());
/**
* This base class is used to handle data preparation (e.g., sorting, filtering and
* group summary).
*/
Ext.define('Ext.ux.ajax.DataSimlet', function() {
function makeSortFn(def, cmp) {
var order = def.direction,
sign = (order && order.toUpperCase() === 'DESC') ? -1 : 1;
return function(leftRec, rightRec) {
var lhs = leftRec[def.property],
rhs = rightRec[def.property],
c = (lhs < rhs) ? -1 : ((rhs < lhs) ? 1 : 0);
if (c || !cmp) {
return c * sign;
}
return cmp(leftRec, rightRec);
};
}
function makeSortFns(defs, cmp) {
for (var sortFn = cmp,
i = defs && defs.length; i; ) {
sortFn = makeSortFn(defs[--i], sortFn);
}
return sortFn;
}
return {
extend: 'Ext.ux.ajax.Simlet',
buildNodes: function(node, path) {
var me = this,
nodeData = {
data: []
},
len = node.length,
children, i, child, name;
me.nodes[path] = nodeData;
for (i = 0; i < len; ++i) {
nodeData.data.push(child = node[i]);
name = child.text || child.title;
child.id = path ? path + '/' + name : name;
children = child.children;
if (!(child.leaf = !children)) {
delete child.children;
me.buildNodes(children, child.id);
}
}
},
deleteRecord: function(pos) {
if (this.data && typeof this.data !== 'function') {
Ext.Array.removeAt(this.data, pos);
}
},
fixTree: function(ctx, tree) {
var me = this,
node = ctx.params.node,
nodes;
if (!(nodes = me.nodes)) {
me.nodes = nodes = {};
me.buildNodes(tree, '');
}
node = nodes[node];
if (node) {
if (me.node) {
me.node.sortedData = me.sortedData;
me.node.currentOrder = me.currentOrder;
}
me.node = node;
me.data = node.data;
me.sortedData = node.sortedData;
me.currentOrder = node.currentOrder;
} else {
me.data = null;
}
},
getData: function(ctx) {
var me = this,
params = ctx.params,
order = (params.filter || '') + (params.group || '') + '-' + (params.sort || '') + '-' + (params.dir || ''),
tree = me.tree,
dynamicData, data, fields, sortFn;
if (tree) {
me.fixTree(ctx, tree);
}
data = me.data;
if (typeof data === 'function') {
dynamicData = true;
data = data.call(this, ctx);
}
// If order is '--' then it means we had no order passed, due to the string concat above
if (!data || order === '--') {
return data || [];
}
if (!dynamicData && order == me.currentOrder) {
return me.sortedData;
}
ctx.filterSpec = params.filter && Ext.decode(params.filter);
ctx.groupSpec = params.group && Ext.decode(params.group);
fields = params.sort;
if (params.dir) {
fields = [
{
direction: params.dir,
property: fields
}
];
} else if (params.sort) {
fields = Ext.decode(params.sort);
} else {
fields = null;
}
if (ctx.filterSpec) {
var filters = new Ext.util.FilterCollection();
filters.add(this.processFilters(ctx.filterSpec));
data = Ext.Array.filter(data, filters.getFilterFn());
}
sortFn = makeSortFns((ctx.sortSpec = fields));
if (ctx.groupSpec) {
sortFn = makeSortFns([
ctx.groupSpec
], sortFn);
}
// If a straight Ajax request, data may not be an array.
// If an Array, preserve 'physical' order of raw data...
data = Ext.isArray(data) ? data.slice(0) : data;
if (sortFn) {
Ext.Array.sort(data, sortFn);
}
me.sortedData = data;
me.currentOrder = order;
return data;
},
processFilters: Ext.identityFn,
getPage: function(ctx, data) {
var ret = data,
length = data.length,
start = ctx.params.start || 0,
end = ctx.params.limit ? Math.min(length, start + ctx.params.limit) : length;
if (start || end < length) {
ret = ret.slice(start, end);
}
return ret;
},
getGroupSummary: function(groupField, rows, ctx) {
return rows[0];
},
getSummary: function(ctx, data, page) {
var me = this,
groupField = ctx.groupSpec.property,
accum,
todo = {},
summary = [],
fieldValue, lastFieldValue;
Ext.each(page, function(rec) {
fieldValue = rec[groupField];
todo[fieldValue] = true;
});
function flush() {
if (accum) {
summary.push(me.getGroupSummary(groupField, accum, ctx));
accum = null;
}
}
// data is ordered primarily by the groupField, so one pass can pick up all
// the summaries one at a time.
Ext.each(data, function(rec) {
fieldValue = rec[groupField];
if (lastFieldValue !== fieldValue) {
flush();
lastFieldValue = fieldValue;
}
if (!todo[fieldValue]) {
// if we have even 1 summary, we have summarized all that we need
// (again because data and page are ordered by groupField)
return !summary.length;
}
if (accum) {
accum.push(rec);
} else {
accum = [
rec
];
}
return true;
});
flush();
// make sure that last pesky summary goes...
return summary;
}
};
}());
/**
* JSON Simlet.
*/
Ext.define('Ext.ux.ajax.JsonSimlet', {
extend: 'Ext.ux.ajax.DataSimlet',
alias: 'simlet.json',
doGet: function(ctx) {
var me = this,
data = me.getData(ctx),
page = me.getPage(ctx, data),
reader = ctx.xhr.options.proxy && ctx.xhr.options.proxy.getReader(),
root = reader && reader.getRootProperty(),
ret = me.callParent(arguments),
// pick up status/statusText
response = {};
if (root && Ext.isArray(page)) {
response[root] = page;
response[reader.getTotalProperty()] = data.length;
} else {
response = page;
}
if (ctx.groupSpec) {
response.summaryData = me.getSummary(ctx, data, page);
}
ret.responseText = Ext.encode(response);
return ret;
},
doPost: function(ctx) {
return this.doGet(ctx);
}
});
/**
* Pivot Simlet does remote pivot calculations.
* Filtering the pivot results doesn't work.
*/
Ext.define('Ext.ux.ajax.PivotSimlet', {
extend: 'Ext.ux.ajax.JsonSimlet',
alias: 'simlet.pivot',
lastPost: null,
// last Ajax params sent to this simlet
lastResponse: null,
// last JSON response produced by this simlet
keysSeparator: '',
grandTotalKey: '',
doPost: function(ctx) {
var me = this,
ret = me.callParent(arguments);
// pick up status/statusText
me.lastResponse = me.processData(me.getData(ctx), Ext.decode(ctx.xhr.body));
ret.responseText = Ext.encode(me.lastResponse);
return ret;
},
processData: function(data, params) {
var me = this,
len = data.length,
response = {
success: true,
leftAxis: [],
topAxis: [],
results: []
},
leftAxis = new Ext.util.MixedCollection(),
topAxis = new Ext.util.MixedCollection(),
results = new Ext.util.MixedCollection(),
i, j, k, leftKeys, topKeys, item, agg;
me.lastPost = params;
me.keysSeparator = params.keysSeparator;
me.grandTotalKey = params.grandTotalKey;
for (i = 0; i < len; i++) {
leftKeys = me.extractValues(data[i], params.leftAxis, leftAxis);
topKeys = me.extractValues(data[i], params.topAxis, topAxis);
// add record to grand totals
me.addResult(data[i], me.grandTotalKey, me.grandTotalKey, results);
for (j = 0; j < leftKeys.length; j++) {
// add record to col grand totals
me.addResult(data[i], leftKeys[j], me.grandTotalKey, results);
// add record to left/top keys pair
for (k = 0; k < topKeys.length; k++) {
me.addResult(data[i], leftKeys[j], topKeys[k], results);
}
}
// add record to row grand totals
for (j = 0; j < topKeys.length; j++) {
me.addResult(data[i], me.grandTotalKey, topKeys[j], results);
}
}
// extract items from their left/top collections and build the json response
response.leftAxis = leftAxis.getRange();
response.topAxis = topAxis.getRange();
len = results.getCount();
for (i = 0; i < len; i++) {
item = results.getAt(i);
item.values = {};
for (j = 0; j < params.aggregate.length; j++) {
agg = params.aggregate[j];
item.values[agg.id] = me[agg.aggregator](item.records, agg.dataIndex, item.leftKey, item.topKey);
}
delete (item.records);
response.results.push(item);
}
leftAxis.clear();
topAxis.clear();
results.clear();
return response;
},
getKey: function(value) {
var me = this;
me.keysMap = me.keysMap || {};
if (!Ext.isDefined(me.keysMap[value])) {
me.keysMap[value] = Ext.id();
}
return me.keysMap[value];
},
extractValues: function(record, dimensions, col) {
var len = dimensions.length,
keys = [],
i, j, key, item, dim;
key = '';
for (j = 0; j < len; j++) {
dim = dimensions[j];
key += (j > 0 ? this.keysSeparator : '') + this.getKey(record[dim.dataIndex]);
item = col.getByKey(key);
if (!item) {
item = col.add(key, {
key: key,
value: record[dim.dataIndex],
dimensionId: dim.id
});
}
keys.push(key);
}
return keys;
},
addResult: function(record, leftKey, topKey, results) {
var item = results.getByKey(leftKey + '/' + topKey);
if (!item) {
item = results.add(leftKey + '/' + topKey, {
leftKey: leftKey,
topKey: topKey,
records: []
});
}
item.records.push(record);
},
sum: function(records, measure, rowGroupKey, colGroupKey) {
var length = records.length,
total = 0,
i;
for (i = 0; i < length; i++) {
total += Ext.Number.from(records[i][measure], 0);
}
return total;
},
avg: function(records, measure, rowGroupKey, colGroupKey) {
var length = records.length,
total = 0,
i;
for (i = 0; i < length; i++) {
total += Ext.Number.from(records[i][measure], 0);
}
return length > 0 ? (total / length) : 0;
},
min: function(records, measure, rowGroupKey, colGroupKey) {
var data = [],
length = records.length,
i, v;
for (i = 0; i < length; i++) {
data.push(records[i][measure]);
}
v = Ext.Array.min(data);
return v;
},
max: function(records, measure, rowGroupKey, colGroupKey) {
var data = [],
length = records.length,
i;
for (i = 0; i < length; i++) {
data.push(records[i][measure]);
}
v = Ext.Array.max(data);
return v;
},
count: function(records, measure, rowGroupKey, colGroupKey) {
return records.length;
},
variance: function(records, measure, rowGroupKey, colGroupKey) {
var me = Ext.pivot.Aggregators,
length = records.length,
avg = me.avg.apply(me, arguments),
total = 0,
i;
if (avg > 0) {
for (i = 0; i < length; i++) {
total += Math.pow(Ext.Number.from(records[i][measure], 0) - avg, 2);
}
}
return (total > 0 && length > 1) ? (total / (length - 1)) : 0;
},
varianceP: function(records, measure, rowGroupKey, colGroupKey) {
var me = Ext.pivot.Aggregators,
length = records.length,
avg = me.avg.apply(me, arguments),
total = 0,
i;
if (avg > 0) {
for (i = 0; i < length; i++) {
total += Math.pow(Ext.Number.from(records[i][measure], 0) - avg, 2);
}
}
return (total > 0 && length > 0) ? (total / length) : 0;
},
stdDev: function(records, measure, rowGroupKey, colGroupKey) {
var me = Ext.pivot.Aggregators,
v = me.variance.apply(me, arguments);
return v > 0 ? Math.sqrt(v) : 0;
},
stdDevP: function(records, measure, rowGroupKey, colGroupKey) {
var me = Ext.pivot.Aggregators,
v = me.varianceP.apply(me, arguments);
return v > 0 ? Math.sqrt(v) : 0;
}
});
/**
* Simulates an XMLHttpRequest object's methods and properties but is backed by a
* {@link Ext.ux.ajax.Simlet} instance that provides the data.
*/
Ext.define('Ext.ux.ajax.SimXhr', {
readyState: 0,
mgr: null,
simlet: null,
constructor: function(config) {
var me = this;
Ext.apply(me, config);
me.requestHeaders = {};
},
abort: function() {
var me = this;
if (me.timer) {
Ext.undefer(me.timer);
me.timer = null;
}
me.aborted = true;
},
getAllResponseHeaders: function() {
var headers = [];
if (Ext.isObject(this.responseHeaders)) {
Ext.Object.each(this.responseHeaders, function(name, value) {
headers.push(name + ': ' + value);
});
}
return headers.join('\r\n');
},
getResponseHeader: function(header) {
var headers = this.responseHeaders;
return (headers && headers[header]) || null;
},
open: function(method, url, async, user, password) {
var me = this;
me.method = method;
me.url = url;
me.async = async !== false;
me.user = user;
me.password = password;
me.setReadyState(1);
},
overrideMimeType: function(mimeType) {
this.mimeType = mimeType;
},
schedule: function() {
var me = this,
delay = me.simlet.delay || me.mgr.delay;
if (delay) {
me.timer = Ext.defer(function() {
me.onTick();
}, delay);
} else {
me.onTick();
}
},
send: function(body) {
var me = this;
me.body = body;
if (me.async) {
me.schedule();
} else {
me.onComplete();
}
},
setReadyState: function(state) {
var me = this;
if (me.readyState != state) {
me.readyState = state;
me.onreadystatechange();
}
},
setRequestHeader: function(header, value) {
this.requestHeaders[header] = value;
},
// handlers
onreadystatechange: Ext.emptyFn,
onComplete: function() {
var me = this,
callback;
me.readyState = 4;
Ext.apply(me, me.simlet.exec(me));
callback = me.jsonpCallback;
if (callback) {
var text = callback + '(' + me.responseText + ')';
eval(text);
}
},
onTick: function() {
var me = this;
me.timer = null;
me.onComplete();
me.onreadystatechange && me.onreadystatechange();
}
});
/**
* This singleton manages simulated Ajax responses. This allows application logic to be
* written unaware that its Ajax calls are being handled by simulations ("simlets"). This
* is currently done by hooking {@link Ext.data.Connection} methods, so all users of that
* class (and {@link Ext.Ajax} since it is a derived class) qualify for simulation.
*
* The requires hooks are inserted when either the {@link #init} method is called or the
* first {@link Ext.ux.ajax.Simlet} is registered. For example:
*
* Ext.onReady(function () {
* initAjaxSim();
*
* // normal stuff
* });
*
* function initAjaxSim () {
* Ext.ux.ajax.SimManager.init({
* delay: 300
* }).register({
* '/app/data/url': {
* type: 'json', // use JsonSimlet (type is like xtype for components)
* data: [
* { foo: 42, bar: 'abc' },
* ...
* ]
* }
* });
* }
*
* As many URL's as desired can be registered and associated with a {@link Ext.ux.ajax.Simlet}. To make
* non-simulated Ajax requests once this singleton is initialized, add a `nosim:true` option
* to the Ajax options:
*
* Ext.Ajax.request({
* url: 'page.php',
* nosim: true, // ignored by normal Ajax request
* params: {
* id: 1
* },
* success: function(response){
* var text = response.responseText;
* // process server response here
* }
* });
*/
Ext.define('Ext.ux.ajax.SimManager', {
singleton: true,
requires: [
'Ext.data.Connection',
'Ext.ux.ajax.SimXhr',
'Ext.ux.ajax.Simlet',
'Ext.ux.ajax.JsonSimlet'
],
/**
* @cfg {Ext.ux.ajax.Simlet} defaultSimlet
* The {@link Ext.ux.ajax.Simlet} instance to use for non-matching URL's. By default, this will
* return 404. Set this to null to use real Ajax calls for non-matching URL's.
*/
/**
* @cfg {String} defaultType
* The default `type` to apply to generic {@link Ext.ux.ajax.Simlet} configuration objects. The
* default is 'basic'.
*/
defaultType: 'basic',
/**
* @cfg {Number} delay
* The number of milliseconds to delay before delivering a response to an async request.
*/
delay: 150,
/**
* @property {Boolean} ready
* True once this singleton has initialized and applied its Ajax hooks.
* @private
*/
ready: false,
constructor: function() {
this.simlets = [];
},
getSimlet: function(url) {
// Strip down to base URL (no query parameters or hash):
var me = this,
index = url.indexOf('?'),
simlets = me.simlets,
len = simlets.length,
i, simlet, simUrl, match;
if (index < 0) {
index = url.indexOf('#');
}
if (index > 0) {
url = url.substring(0, index);
}
for (i = 0; i < len; ++i) {
simlet = simlets[i];
simUrl = simlet.url;
if (simUrl instanceof RegExp) {
match = simUrl.test(url);
} else {
match = simUrl === url;
}
if (match) {
return simlet;
}
}
return me.defaultSimlet;
},
getXhr: function(method, url, options, async) {
var simlet = this.getSimlet(url);
if (simlet) {
return simlet.openRequest(method, url, options, async);
}
return null;
},
/**
* Initializes this singleton and applies configuration options.
* @param {Object} config An optional object with configuration properties to apply.
* @return {Ext.ux.ajax.SimManager} this
*/
init: function(config) {
var me = this;
Ext.apply(me, config);
if (!me.ready) {
me.ready = true;
if (!('defaultSimlet' in me)) {
me.defaultSimlet = new Ext.ux.ajax.Simlet({
status: 404,
statusText: 'Not Found'
});
}
me._openRequest = Ext.data.Connection.prototype.openRequest;
Ext.data.request.Ajax.override({
openRequest: function(options, requestOptions, async) {
var xhr = !options.nosim && me.getXhr(requestOptions.method, requestOptions.url, options, async);
if (!xhr) {
xhr = this.callParent(arguments);
}
return xhr;
}
});
if (Ext.data.JsonP) {
Ext.data.JsonP.self.override({
createScript: function(url, params, options) {
var fullUrl = Ext.urlAppend(url, Ext.Object.toQueryString(params)),
script = !options.nosim && me.getXhr('GET', fullUrl, options, true);
if (!script) {
script = this.callParent(arguments);
}
return script;
},
loadScript: function(request) {
var script = request.script;
if (script.simlet) {
script.jsonpCallback = request.params[request.callbackKey];
script.send(null);
// Ext.data.JsonP will attempt dom removal of a script tag, so emulate its presence
request.script = document.createElement('script');
} else {
this.callParent(arguments);
}
}
});
}
}
return me;
},
openRequest: function(method, url, async) {
var opt = {
method: method,
url: url
};
return this._openRequest.call(Ext.data.Connection.prototype, {}, opt, async);
},
/**
* Registeres one or more {@link Ext.ux.ajax.Simlet} instances.
* @param {Array/Object} simlet Either a {@link Ext.ux.ajax.Simlet} instance or config, an Array
* of such elements or an Object keyed by URL with values that are {@link Ext.ux.ajax.Simlet}
* instances or configs.
*/
register: function(simlet) {
var me = this;
me.init();
function reg(one) {
var simlet = one;
if (!simlet.isSimlet) {
simlet = Ext.create('simlet.' + (simlet.type || simlet.stype || me.defaultType), one);
}
me.simlets.push(simlet);
simlet.manager = me;
}
if (Ext.isArray(simlet)) {
Ext.each(simlet, reg);
} else if (simlet.isSimlet || simlet.url) {
reg(simlet);
} else {
Ext.Object.each(simlet, function(url, s) {
s.url = url;
reg(s);
});
}
return me;
}
});
/**
* This class simulates XML-based requests.
*/
Ext.define('Ext.ux.ajax.XmlSimlet', {
extend: 'Ext.ux.ajax.DataSimlet',
alias: 'simlet.xml',
/**
* This template is used to populate the XML response. The configuration of the Reader
* is available so that its `root` and `record` properties can be used as well as the
* `fields` of the associated `model`. But beyond that, the way these pieces are put
* together in the document requires the flexibility of a template.
*/
xmlTpl: [
'<{root}>\n',
'',
' <{parent.record}>\n',
'',
' <{name}>{[parent[values.name]]}{name}>\n',
'',
' {parent.record}>\n',
'',
'{root}>'
],
doGet: function(ctx) {
var me = this,
data = me.getData(ctx),
page = me.getPage(ctx, data),
proxy = ctx.xhr.options.operation.getProxy(),
reader = proxy && proxy.getReader(),
model = reader && reader.getModel(),
ret = me.callParent(arguments),
// pick up status/statusText
response = {
data: page,
reader: reader,
fields: model && model.fields,
root: reader && reader.getRootProperty(),
record: reader && reader.record
},
tpl, xml, doc;
if (ctx.groupSpec) {
response.summaryData = me.getSummary(ctx, data, page);
}
// If a straight Ajax request there won't be an xmlTpl.
if (me.xmlTpl) {
tpl = Ext.XTemplate.getTpl(me, 'xmlTpl');
xml = tpl.apply(response);
} else {
xml = data;
}
if (typeof DOMParser != 'undefined') {
doc = (new DOMParser()).parseFromString(xml, "text/xml");
} else {
// IE doesn't have DOMParser, but fortunately, there is an ActiveX for XML
doc = new ActiveXObject("Microsoft.XMLDOM");
doc.async = false;
doc.loadXML(xml);
}
ret.responseText = xml;
ret.responseXML = doc;
return ret;
},
fixTree: function() {
this.callParent(arguments);
var buffer = [];
this.buildTreeXml(this.data, buffer);
this.data = buffer.join('');
},
buildTreeXml: function(nodes, buffer) {
var rootProperty = this.rootProperty,
recordProperty = this.recordProperty;
buffer.push('<', rootProperty, '>');
Ext.Array.forEach(nodes, function(node) {
buffer.push('<', recordProperty, '>');
for (var key in node) {
if (key == 'children') {
this.buildTreeXml(node.children, buffer);
} else {
buffer.push('<', key, '>', node[key], '', key, '>');
}
}
buffer.push('', recordProperty, '>');
});
buffer.push('', rootProperty, '>');
}
});
/**
* This is the base class for {@link Ext.ux.event.Recorder} and {@link Ext.ux.event.Player}.
*/
Ext.define('Ext.ux.event.Driver', {
extend: 'Ext.util.Observable',
active: null,
specialKeysByName: {
PGUP: 33,
PGDN: 34,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
},
specialKeysByCode: {},
/**
* @event start
* Fires when this object is started.
* @param {Ext.ux.event.Driver} this
*/
/**
* @event stop
* Fires when this object is stopped.
* @param {Ext.ux.event.Driver} this
*/
getTextSelection: function(el) {
// See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js
var doc = el.ownerDocument,
range, range2, start, end;
if (typeof el.selectionStart === "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else if (doc.selection) {
range = doc.selection.createRange();
range2 = el.createTextRange();
range2.setEndPoint('EndToStart', range);
start = range2.text.length;
end = start + range.text.length;
}
return [
start,
end
];
},
getTime: function() {
return new Date().getTime();
},
/**
* Returns the number of milliseconds since start was called.
*/
getTimestamp: function() {
var d = this.getTime();
return d - this.startTime;
},
onStart: function() {},
onStop: function() {},
/**
* Starts this object. If this object is already started, nothing happens.
*/
start: function() {
var me = this;
if (!me.active) {
me.active = new Date();
me.startTime = me.getTime();
me.onStart();
me.fireEvent('start', me);
}
},
/**
* Stops this object. If this object is not started, nothing happens.
*/
stop: function() {
var me = this;
if (me.active) {
me.active = null;
me.onStop();
me.fireEvent('stop', me);
}
}
}, function() {
var proto = this.prototype;
Ext.Object.each(proto.specialKeysByName, function(name, value) {
proto.specialKeysByCode[value] = name;
});
});
/**
* Event maker.
*/
Ext.define('Ext.ux.event.Maker', {
eventQueue: [],
startAfter: 500,
timerIncrement: 500,
currentTiming: 0,
constructor: function(config) {
var me = this;
me.currentTiming = me.startAfter;
if (!Ext.isArray(config)) {
config = [
config
];
}
Ext.Array.each(config, function(item) {
item.el = item.el || 'el';
Ext.Array.each(Ext.ComponentQuery.query(item.cmpQuery), function(cmp) {
var event = {},
x, y, el;
if (!item.domQuery) {
el = cmp[item.el];
} else {
el = cmp.el.down(item.domQuery);
}
event.target = '#' + el.dom.id;
event.type = item.type;
event.button = config.button || 0;
x = el.getX() + (el.getWidth() / 2);
y = el.getY() + (el.getHeight() / 2);
event.xy = [
x,
y
];
event.ts = me.currentTiming;
me.currentTiming += me.timerIncrement;
me.eventQueue.push(event);
});
if (item.screenshot) {
me.eventQueue[me.eventQueue.length - 1].screenshot = true;
}
});
return me.eventQueue;
}
});
/**
* @extends Ext.ux.event.Driver
* This class manages the playback of an array of "event descriptors". For details on the
* contents of an "event descriptor", see {@link Ext.ux.event.Recorder}. The events recorded by the
* {@link Ext.ux.event.Recorder} class are designed to serve as input for this class.
*
* The simplest use of this class is to instantiate it with an {@link #eventQueue} and call
* {@link #method-start}. Like so:
*
* var player = Ext.create('Ext.ux.event.Player', {
* eventQueue: [ ... ],
* speed: 2, // play at 2x speed
* listeners: {
* stop: function () {
* player = null; // all done
* }
* }
* });
*
* player.start();
*
* A more complex use would be to incorporate keyframe generation after playing certain
* events.
*
* var player = Ext.create('Ext.ux.event.Player', {
* eventQueue: [ ... ],
* keyFrameEvents: {
* click: true
* },
* listeners: {
* stop: function () {
* // play has completed... probably time for another keyframe...
* player = null;
* },
* keyframe: onKeyFrame
* }
* });
*
* player.start();
*
* If a keyframe can be handled immediately (synchronously), the listener would be:
*
* function onKeyFrame () {
* handleKeyFrame();
* }
*
* If the keyframe event is always handled asynchronously, then the event listener is only
* a bit more:
*
* function onKeyFrame (p, eventDescriptor) {
* eventDescriptor.defer(); // pause event playback...
*
* handleKeyFrame(function () {
* eventDescriptor.finish(); // ...resume event playback
* });
* }
*
* Finally, if the keyframe could be either handled synchronously or asynchronously (perhaps
* differently by browser), a slightly more complex listener is required.
*
* function onKeyFrame (p, eventDescriptor) {
* var async;
*
* handleKeyFrame(function () {
* // either this callback is being called immediately by handleKeyFrame (in
* // which case async is undefined) or it is being called later (in which case
* // async will be true).
*
* if (async) {
* eventDescriptor.finish();
* } else {
* async = false;
* }
* });
*
* // either the callback was called (and async is now false) or it was not
* // called (and async remains undefined).
*
* if (async !== false) {
* eventDescriptor.defer();
* async = true; // let the callback know that we have gone async
* }
* }
*/
Ext.define('Ext.ux.event.Player', function(Player) {
var defaults = {},
mouseEvents = {},
keyEvents = {},
doc,
//HTML events supported
uiEvents = {},
//events that bubble by default
bubbleEvents = {
//scroll: 1,
resize: 1,
reset: 1,
submit: 1,
change: 1,
select: 1,
error: 1,
abort: 1
};
Ext.each([
'click',
'dblclick',
'mouseover',
'mouseout',
'mousedown',
'mouseup',
'mousemove'
], function(type) {
bubbleEvents[type] = defaults[type] = mouseEvents[type] = {
bubbles: true,
cancelable: (type != "mousemove"),
// mousemove cannot be cancelled
detail: 1,
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
button: 0
};
});
Ext.each([
'keydown',
'keyup',
'keypress'
], function(type) {
bubbleEvents[type] = defaults[type] = keyEvents[type] = {
bubbles: true,
cancelable: true,
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
keyCode: 0,
charCode: 0
};
});
Ext.each([
'blur',
'change',
'focus',
'resize',
'scroll',
'select'
], function(type) {
defaults[type] = uiEvents[type] = {
bubbles: (type in bubbleEvents),
cancelable: false,
detail: 1
};
});
var inputSpecialKeys = {
8: function(target, start, end) {
// backspace: 8,
if (start < end) {
target.value = target.value.substring(0, start) + target.value.substring(end);
} else if (start > 0) {
target.value = target.value.substring(0, --start) + target.value.substring(end);
}
this.setTextSelection(target, start, start);
},
46: function(target, start, end) {
// delete: 46
if (start < end) {
target.value = target.value.substring(0, start) + target.value.substring(end);
} else if (start < target.value.length - 1) {
target.value = target.value.substring(0, start) + target.value.substring(start + 1);
}
this.setTextSelection(target, start, start);
}
};
return {
extend: 'Ext.ux.event.Driver',
/**
* @cfg {Array} eventQueue The event queue to playback. This must be provided before
* the {@link #method-start} method is called.
*/
/**
* @cfg {Object} keyFrameEvents An object that describes the events that should generate
* keyframe events. For example, `{ click: true }` would generate keyframe events after
* each `click` event.
*/
keyFrameEvents: {
click: true
},
/**
* @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false
* to ignore animations. Default is true.
*/
pauseForAnimations: true,
/**
* @cfg {Number} speed The playback speed multiplier. Default is 1.0 (to playback at the
* recorded speed). A value of 2 would playback at 2x speed.
*/
speed: 1,
stallTime: 0,
_inputSpecialKeys: {
INPUT: inputSpecialKeys,
TEXTAREA: Ext.apply({}, //13: function (target, start, end) { // enter: 8,
//TODO ?
//}
inputSpecialKeys)
},
tagPathRegEx: /(\w+)(?:\[(\d+)\])?/,
/**
* @event beforeplay
* Fires before an event is played.
* @param {Ext.ux.event.Player} this
* @param {Object} eventDescriptor The event descriptor about to be played.
*/
/**
* @event keyframe
* Fires when this player reaches a keyframe. Typically, this is after events
* like `click` are injected and any resulting animations have been completed.
* @param {Ext.ux.event.Player} this
* @param {Object} eventDescriptor The keyframe event descriptor.
*/
constructor: function(config) {
var me = this;
me.callParent(arguments);
me.timerFn = function() {
me.onTick();
};
me.attachTo = me.attachTo || window;
doc = me.attachTo.document;
},
/**
* Returns the element given is XPath-like description.
* @param {String} xpath The XPath-like description of the element.
* @return {HTMLElement}
*/
getElementFromXPath: function(xpath) {
var me = this,
parts = xpath.split('/'),
regex = me.tagPathRegEx,
i, n, m, count, tag, child,
el = me.attachTo.document;
el = (parts[0] == '~') ? el.body : el.getElementById(parts[0].substring(1));
// remove '#'
for (i = 1 , n = parts.length; el && i < n; ++i) {
m = regex.exec(parts[i]);
count = m[2] ? parseInt(m[2], 10) : 1;
tag = m[1].toUpperCase();
for (child = el.firstChild; child; child = child.nextSibling) {
if (child.tagName == tag) {
if (count == 1) {
break;
}
--count;
}
}
el = child;
}
return el;
},
// Moving across a line break only counts as moving one character in a TextRange, whereas a line break in
// the textarea value is two characters. This function corrects for that by converting a text offset into a
// range character offset by subtracting one character for every line break in the textarea prior to the
// offset
offsetToRangeCharacterMove: function(el, offset) {
return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
},
setTextSelection: function(el, startOffset, endOffset) {
// See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js
if (startOffset < 0) {
startOffset += el.value.length;
}
if (endOffset == null) {
endOffset = startOffset;
}
if (endOffset < 0) {
endOffset += el.value.length;
}
if (typeof el.selectionStart === "number") {
el.selectionStart = startOffset;
el.selectionEnd = endOffset;
} else {
var range = el.createTextRange();
var startCharMove = this.offsetToRangeCharacterMove(el, startOffset);
range.collapse(true);
if (startOffset == endOffset) {
range.move("character", startCharMove);
} else {
range.moveEnd("character", this.offsetToRangeCharacterMove(el, endOffset));
range.moveStart("character", startCharMove);
}
range.select();
}
},
getTimeIndex: function() {
var t = this.getTimestamp() - this.stallTime;
return t * this.speed;
},
makeToken: function(eventDescriptor, signal) {
var me = this,
t0;
eventDescriptor[signal] = true;
eventDescriptor.defer = function() {
eventDescriptor[signal] = false;
t0 = me.getTime();
};
eventDescriptor.finish = function() {
eventDescriptor[signal] = true;
me.stallTime += me.getTime() - t0;
me.schedule();
};
},
/**
* This method is called after an event has been played to prepare for the next event.
* @param {Object} eventDescriptor The descriptor of the event just played.
*/
nextEvent: function(eventDescriptor) {
var me = this,
index = ++me.queueIndex;
// keyframe events are inserted after a keyFrameEvent is played.
if (me.keyFrameEvents[eventDescriptor.type]) {
Ext.Array.insert(me.eventQueue, index, [
{
keyframe: true,
ts: eventDescriptor.ts
}
]);
}
},
/**
* This method returns the event descriptor at the front of the queue. This does not
* dequeue the event. Repeated calls return the same object (until {@link #nextEvent}
* is called).
*/
peekEvent: function() {
return this.eventQueue[this.queueIndex] || null;
},
/**
* Replaces an event in the queue with an array of events. This is often used to roll
* up a multi-step pseudo-event and expand it just-in-time to be played. The process
* for doing this in a derived class would be this:
*
* Ext.define('My.Player', {
* extend: 'Ext.ux.event.Player',
*
* peekEvent: function () {
* var event = this.callParent();
*
* if (event.multiStepSpecial) {
* this.replaceEvent(null, [
* ... expand to actual events
* ]);
*
* event = this.callParent(); // get the new next event
* }
*
* return event;
* }
* });
*
* This method ensures that the `beforeplay` hook (if any) from the replaced event is
* placed on the first new event and the `afterplay` hook (if any) is placed on the
* last new event.
*
* @param {Number} index The queue index to replace. Pass `null` to replace the event
* at the current `queueIndex`.
* @param {Event[]} events The array of events with which to replace the specified
* event.
*/
replaceEvent: function(index, events) {
for (var t,
i = 0,
n = events.length; i < n; ++i) {
if (i) {
t = events[i - 1];
delete t.afterplay;
delete t.screenshot;
delete events[i].beforeplay;
}
}
Ext.Array.replace(this.eventQueue, (index == null) ? this.queueIndex : index, 1, events);
},
/**
* This method dequeues and injects events until it has arrived at the time index. If
* no events are ready (based on the time index), this method does nothing.
* @return {Boolean} True if there is more to do; false if not (at least for now).
*/
processEvents: function() {
var me = this,
animations = me.pauseForAnimations && me.attachTo.Ext.fx.Manager.items,
eventDescriptor;
while ((eventDescriptor = me.peekEvent()) !== null) {
if (animations && animations.getCount()) {
return true;
}
if (eventDescriptor.keyframe) {
if (!me.processKeyFrame(eventDescriptor)) {
return false;
}
me.nextEvent(eventDescriptor);
} else if (eventDescriptor.ts <= me.getTimeIndex() && me.fireEvent('beforeplay', me, eventDescriptor) !== false && me.playEvent(eventDescriptor)) {
me.nextEvent(eventDescriptor);
} else {
return true;
}
}
me.stop();
return false;
},
/**
* This method is called when a keyframe is reached. This will fire the keyframe event.
* If the keyframe has been handled, true is returned. Otherwise, false is returned.
* @param {Object} eventDescriptor The event descriptor of the keyframe.
* @return {Boolean} True if the keyframe was handled, false if not.
*/
processKeyFrame: function(eventDescriptor) {
var me = this;
// only fire keyframe event (and setup the eventDescriptor) once...
if (!eventDescriptor.defer) {
me.makeToken(eventDescriptor, 'done');
me.fireEvent('keyframe', me, eventDescriptor);
}
return eventDescriptor.done;
},
/**
* Called to inject the given event on the specified target.
* @param {HTMLElement} target The target of the event.
* @param {Object} event The event to inject. The properties of this object should be
* those of standard DOM events but vary based on the `type` property. For details on
* event types and their properties, see the class documentation.
*/
injectEvent: function(target, event) {
var me = this,
type = event.type,
options = Ext.apply({}, event, defaults[type]),
handler;
if (type === 'type') {
handler = me._inputSpecialKeys[target.tagName];
if (handler) {
return me.injectTypeInputEvent(target, event, handler);
}
return me.injectTypeEvent(target, event);
}
if (type === 'focus' && target.focus) {
target.focus();
return true;
}
if (type === 'blur' && target.blur) {
target.blur();
return true;
}
if (type === 'scroll') {
target.scrollLeft = event.pos[0];
target.scrollTop = event.pos[1];
return true;
}
if (type === 'mduclick') {
return me.injectEvent(target, Ext.applyIf({
type: 'mousedown'
}, event)) && me.injectEvent(target, Ext.applyIf({
type: 'mouseup'
}, event)) && me.injectEvent(target, Ext.applyIf({
type: 'click'
}, event));
}
if (mouseEvents[type]) {
return Player.injectMouseEvent(target, options, me.attachTo);
}
if (keyEvents[type]) {
return Player.injectKeyEvent(target, options, me.attachTo);
}
if (uiEvents[type]) {
return Player.injectUIEvent(target, type, options.bubbles, options.cancelable, options.view || me.attachTo, options.detail);
}
return false;
},
injectTypeEvent: function(target, event) {
var me = this,
text = event.text,
xlat = [],
ch, chUp, i, n, sel, upper, isInput;
if (text) {
delete event.text;
upper = text.toUpperCase();
for (i = 0 , n = text.length; i < n; ++i) {
ch = text.charCodeAt(i);
chUp = upper.charCodeAt(i);
xlat.push(Ext.applyIf({
type: 'keydown',
charCode: chUp,
keyCode: chUp
}, event), Ext.applyIf({
type: 'keypress',
charCode: ch,
keyCode: ch
}, event), Ext.applyIf({
type: 'keyup',
charCode: chUp,
keyCode: chUp
}, event));
}
} else {
xlat.push(Ext.applyIf({
type: 'keydown',
charCode: event.keyCode
}, event), Ext.applyIf({
type: 'keyup',
charCode: event.keyCode
}, event));
}
for (i = 0 , n = xlat.length; i < n; ++i) {
me.injectEvent(target, xlat[i]);
}
return true;
},
injectTypeInputEvent: function(target, event, handler) {
var me = this,
text = event.text,
sel, n;
if (handler) {
sel = me.getTextSelection(target);
if (text) {
n = sel[0];
target.value = target.value.substring(0, n) + text + target.value.substring(sel[1]);
n += text.length;
me.setTextSelection(target, n, n);
} else {
if (!(handler = handler[event.keyCode])) {
// no handler for the special key for this element
if ('caret' in event) {
me.setTextSelection(target, event.caret, event.caret);
} else if (event.selection) {
me.setTextSelection(target, event.selection[0], event.selection[1]);
}
return me.injectTypeEvent(target, event);
}
handler.call(this, target, sel[0], sel[1]);
return true;
}
}
return true;
},
playEvent: function(eventDescriptor) {
var me = this,
target = me.getElementFromXPath(eventDescriptor.target),
event;
if (!target) {
// not present (yet)... wait for element present...
// TODO - need a timeout here
return false;
}
if (!me.playEventHook(eventDescriptor, 'beforeplay')) {
return false;
}
if (!eventDescriptor.injected) {
eventDescriptor.injected = true;
event = me.translateEvent(eventDescriptor, target);
me.injectEvent(target, event);
}
return me.playEventHook(eventDescriptor, 'afterplay');
},
playEventHook: function(eventDescriptor, hookName) {
var me = this,
doneName = hookName + '.done',
firedName = hookName + '.fired',
hook = eventDescriptor[hookName];
if (hook && !eventDescriptor[doneName]) {
if (!eventDescriptor[firedName]) {
eventDescriptor[firedName] = true;
me.makeToken(eventDescriptor, doneName);
if (me.eventScope && Ext.isString(hook)) {
hook = me.eventScope[hook];
}
if (hook) {
hook.call(me.eventScope || me, eventDescriptor);
}
}
return false;
}
return true;
},
schedule: function() {
var me = this;
if (!me.timer) {
me.timer = Ext.defer(me.timerFn, 10);
}
},
_translateAcross: [
'type',
'button',
'charCode',
'keyCode',
'caret',
'pos',
'text',
'selection'
],
translateEvent: function(eventDescriptor, target) {
var me = this,
event = {},
modKeys = eventDescriptor.modKeys || '',
names = me._translateAcross,
i = names.length,
name, xy;
while (i--) {
name = names[i];
if (name in eventDescriptor) {
event[name] = eventDescriptor[name];
}
}
event.altKey = modKeys.indexOf('A') > 0;
event.ctrlKey = modKeys.indexOf('C') > 0;
event.metaKey = modKeys.indexOf('M') > 0;
event.shiftKey = modKeys.indexOf('S') > 0;
if (target && 'x' in eventDescriptor) {
xy = Ext.fly(target).getXY();
xy[0] += eventDescriptor.x;
xy[1] += eventDescriptor.y;
} else if ('x' in eventDescriptor) {
xy = [
eventDescriptor.x,
eventDescriptor.y
];
} else if ('px' in eventDescriptor) {
xy = [
eventDescriptor.px,
eventDescriptor.py
];
}
if (xy) {
event.clientX = event.screenX = xy[0];
event.clientY = event.screenY = xy[1];
}
if (eventDescriptor.key) {
event.keyCode = me.specialKeysByName[eventDescriptor.key];
}
if (eventDescriptor.type === 'wheel') {
if ('onwheel' in me.attachTo.document) {
event.wheelX = eventDescriptor.dx;
event.wheelY = eventDescriptor.dy;
} else {
event.type = 'mousewheel';
event.wheelDeltaX = -40 * eventDescriptor.dx;
event.wheelDeltaY = event.wheelDelta = -40 * eventDescriptor.dy;
}
}
return event;
},
//---------------------------------
// Driver overrides
onStart: function() {
var me = this;
me.queueIndex = 0;
me.schedule();
},
onStop: function() {
var me = this;
if (me.timer) {
Ext.undefer(me.timer);
me.timer = null;
}
},
//---------------------------------
onTick: function() {
var me = this;
me.timer = null;
if (me.processEvents()) {
me.schedule();
}
},
statics: {
ieButtonCodeMap: {
0: 1,
1: 4,
2: 2
},
/**
* Injects a key event using the given event information to populate the event
* object.
*
* **Note:** `keydown` causes Safari 2.x to crash.
*
* @param {HTMLElement} target The target of the given event.
* @param {Object} options Object object containing all of the event injection
* options.
* @param {String} options.type The type of event to fire. This can be any one of
* the following: `keyup`, `keydown` and `keypress`.
* @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.
* DOM Level 3 specifies that all key events bubble by default.
* @param {Boolean} [options.cancelable=true] `true` if the event can be canceled
* using `preventDefault`. DOM Level 3 specifies that all key events can be
* cancelled.
* @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is
* pressed while the event is firing.
* @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is
* pressed while the event is firing.
* @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is
* pressed while the event is firing.
* @param {Boolean} [options.metaKey=false] `true` if one of the META keys is
* pressed while the event is firing.
* @param {Number} [options.keyCode=0] The code for the key that is in use.
* @param {Number} [options.charCode=0] The Unicode code for the character
* associated with the key being used.
* @param {Window} [view=window] The view containing the target. This is typically
* the window object.
* @private
*/
injectKeyEvent: function(target, options, view) {
var type = options.type,
customEvent = null;
if (type === 'textevent') {
type = 'keypress';
}
view = view || window;
//check for DOM-compliant browsers first
if (doc.createEvent) {
try {
customEvent = doc.createEvent("KeyEvents");
// Interesting problem: Firefox implemented a non-standard
// version of initKeyEvent() based on DOM Level 2 specs.
// Key event was removed from DOM Level 2 and re-introduced
// in DOM Level 3 with a different interface. Firefox is the
// only browser with any implementation of Key Events, so for
// now, assume it's Firefox if the above line doesn't error.
// @TODO: Decipher between Firefox's implementation and a correct one.
customEvent.initKeyEvent(type, options.bubbles, options.cancelable, view, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode);
} catch (ex) {
// If it got here, that means key events aren't officially supported.
// Safari/WebKit is a real problem now. WebKit 522 won't let you
// set keyCode, charCode, or other properties if you use a
// UIEvent, so we first must try to create a generic event. The
// fun part is that this will throw an error on Safari 2.x. The
// end result is that we need another try...catch statement just to
// deal with this mess.
try {
//try to create generic event - will fail in Safari 2.x
customEvent = doc.createEvent("Events");
} catch (uierror) {
//the above failed, so create a UIEvent for Safari 2.x
customEvent = doc.createEvent("UIEvents");
} finally {
customEvent.initEvent(type, options.bubbles, options.cancelable);
customEvent.view = view;
customEvent.altKey = options.altKey;
customEvent.ctrlKey = options.ctrlKey;
customEvent.shiftKey = options.shiftKey;
customEvent.metaKey = options.metaKey;
customEvent.keyCode = options.keyCode;
customEvent.charCode = options.charCode;
}
}
target.dispatchEvent(customEvent);
} else if (doc.createEventObject) {
//IE
customEvent = doc.createEventObject();
customEvent.bubbles = options.bubbles;
customEvent.cancelable = options.cancelable;
customEvent.view = view;
customEvent.ctrlKey = options.ctrlKey;
customEvent.altKey = options.altKey;
customEvent.shiftKey = options.shiftKey;
customEvent.metaKey = options.metaKey;
// IE doesn't support charCode explicitly. CharCode should
// take precedence over any keyCode value for accurate
// representation.
customEvent.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;
target.fireEvent("on" + type, customEvent);
} else {
return false;
}
return true;
},
/**
* Injects a mouse event using the given event information to populate the event
* object.
*
* @param {HTMLElement} target The target of the given event.
* @param {Object} options Object object containing all of the event injection
* options.
* @param {String} options.type The type of event to fire. This can be any one of
* the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`,
* `mouseover` and `mousemove`.
* @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.
* DOM Level 2 specifies that all mouse events bubble by default.
* @param {Boolean} [options.cancelable=true] `true` if the event can be canceled
* using `preventDefault`. DOM Level 2 specifies that all mouse events except
* `mousemove` can be cancelled. This defaults to `false` for `mousemove`.
* @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is
* pressed while the event is firing.
* @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is
* pressed while the event is firing.
* @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is
* pressed while the event is firing.
* @param {Boolean} [options.metaKey=false] `true` if one of the META keys is
* pressed while the event is firing.
* @param {Number} [options.detail=1] The number of times the mouse button has
* been used.
* @param {Number} [options.screenX=0] The x-coordinate on the screen at which point
* the event occurred.
* @param {Number} [options.screenY=0] The y-coordinate on the screen at which point
* the event occurred.
* @param {Number} [options.clientX=0] The x-coordinate on the client at which point
* the event occurred.
* @param {Number} [options.clientY=0] The y-coordinate on the client at which point
* the event occurred.
* @param {Number} [options.button=0] The button being pressed while the event is
* executing. The value should be 0 for the primary mouse button (typically the
* left button), 1 for the tertiary mouse button (typically the middle button),
* and 2 for the secondary mouse button (typically the right button).
* @param {HTMLElement} [options.relatedTarget=null] For `mouseout` events, this
* is the element that the mouse has moved to. For `mouseover` events, this is
* the element that the mouse has moved from. This argument is ignored for all
* other events.
* @param {Window} [view=window] The view containing the target. This is typically
* the window object.
* @private
*/
injectMouseEvent: function(target, options, view) {
var type = options.type,
customEvent = null;
view = view || window;
//check for DOM-compliant browsers first
if (doc.createEvent) {
customEvent = doc.createEvent("MouseEvents");
//Safari 2.x (WebKit 418) still doesn't implement initMouseEvent()
if (customEvent.initMouseEvent) {
customEvent.initMouseEvent(type, options.bubbles, options.cancelable, view, options.detail, options.screenX, options.screenY, options.clientX, options.clientY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, options.relatedTarget);
} else {
//Safari
//the closest thing available in Safari 2.x is UIEvents
customEvent = doc.createEvent("UIEvents");
customEvent.initEvent(type, options.bubbles, options.cancelable);
customEvent.view = view;
customEvent.detail = options.detail;
customEvent.screenX = options.screenX;
customEvent.screenY = options.screenY;
customEvent.clientX = options.clientX;
customEvent.clientY = options.clientY;
customEvent.ctrlKey = options.ctrlKey;
customEvent.altKey = options.altKey;
customEvent.metaKey = options.metaKey;
customEvent.shiftKey = options.shiftKey;
customEvent.button = options.button;
customEvent.relatedTarget = options.relatedTarget;
}
/*
* Check to see if relatedTarget has been assigned. Firefox
* versions less than 2.0 don't allow it to be assigned via
* initMouseEvent() and the property is readonly after event
* creation, so in order to keep YAHOO.util.getRelatedTarget()
* working, assign to the IE proprietary toElement property
* for mouseout event and fromElement property for mouseover
* event.
*/
if (options.relatedTarget && !customEvent.relatedTarget) {
if (type == "mouseout") {
customEvent.toElement = options.relatedTarget;
} else if (type == "mouseover") {
customEvent.fromElement = options.relatedTarget;
}
}
target.dispatchEvent(customEvent);
} else if (doc.createEventObject) {
//IE
customEvent = doc.createEventObject();
customEvent.bubbles = options.bubbles;
customEvent.cancelable = options.cancelable;
customEvent.view = view;
customEvent.detail = options.detail;
customEvent.screenX = options.screenX;
customEvent.screenY = options.screenY;
customEvent.clientX = options.clientX;
customEvent.clientY = options.clientY;
customEvent.ctrlKey = options.ctrlKey;
customEvent.altKey = options.altKey;
customEvent.metaKey = options.metaKey;
customEvent.shiftKey = options.shiftKey;
customEvent.button = Player.ieButtonCodeMap[options.button] || 0;
/*
* Have to use relatedTarget because IE won't allow assignment
* to toElement or fromElement on generic events. This keeps
* YAHOO.util.customEvent.getRelatedTarget() functional.
*/
customEvent.relatedTarget = options.relatedTarget;
target.fireEvent('on' + type, customEvent);
} else {
return false;
}
return true;
},
/**
* Injects a UI event using the given event information to populate the event
* object.
*
* @param {HTMLElement} target The target of the given event.
* @param {Object} options
* @param {String} options.type The type of event to fire. This can be any one of
* the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`,
* `mouseover` and `mousemove`.
* @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.
* DOM Level 2 specifies that all mouse events bubble by default.
* @param {Boolean} [options.cancelable=true] `true` if the event can be canceled
* using `preventDefault`. DOM Level 2 specifies that all mouse events except
* `mousemove` can be canceled. This defaults to `false` for `mousemove`.
* @param {Number} [options.detail=1] The number of times the mouse button has been
* used.
* @param {Window} [view=window] The view containing the target. This is typically
* the window object.
* @private
*/
injectUIEvent: function(target, options, view) {
var customEvent = null;
view = view || window;
//check for DOM-compliant browsers first
if (doc.createEvent) {
//just a generic UI Event object is needed
customEvent = doc.createEvent("UIEvents");
customEvent.initUIEvent(options.type, options.bubbles, options.cancelable, view, options.detail);
target.dispatchEvent(customEvent);
} else if (doc.createEventObject) {
//IE
customEvent = doc.createEventObject();
customEvent.bubbles = options.bubbles;
customEvent.cancelable = options.cancelable;
customEvent.view = view;
customEvent.detail = options.detail;
target.fireEvent("on" + options.type, customEvent);
} else {
return false;
}
return true;
}
}
};
});
// statics
/**
* @extends Ext.ux.event.Driver
* Event recorder.
*/
Ext.define('Ext.ux.event.Recorder', function(Recorder) {
function apply() {
var a = arguments,
n = a.length,
obj = {
kind: 'other'
},
i;
for (i = 0; i < n; ++i) {
Ext.apply(obj, arguments[i]);
}
if (obj.alt && !obj.event) {
obj.event = obj.alt;
}
return obj;
}
function key(extra) {
return apply({
kind: 'keyboard',
modKeys: true,
key: true
}, extra);
}
function mouse(extra) {
return apply({
kind: 'mouse',
button: true,
modKeys: true,
xy: true
}, extra);
}
var eventsToRecord = {
keydown: key(),
keypress: key(),
keyup: key(),
dragmove: mouse({
alt: 'mousemove',
pageCoords: true,
whileDrag: true
}),
mousemove: mouse({
pageCoords: true
}),
mouseover: mouse(),
mouseout: mouse(),
click: mouse(),
wheel: mouse({
wheel: true
}),
mousedown: mouse({
press: true
}),
mouseup: mouse({
release: true
}),
scroll: apply({
listen: false
}),
focus: apply(),
blur: apply()
};
for (var key in eventsToRecord) {
if (!eventsToRecord[key].event) {
eventsToRecord[key].event = key;
}
}
eventsToRecord.wheel.event = null;
// must detect later
return {
extend: 'Ext.ux.event.Driver',
/**
* @event add
* Fires when an event is added to the recording.
* @param {Ext.ux.event.Recorder} this
* @param {Object} eventDescriptor The event descriptor.
*/
/**
* @event coalesce
* Fires when an event is coalesced. This edits the tail of the recorded
* event list.
* @param {Ext.ux.event.Recorder} this
* @param {Object} eventDescriptor The event descriptor that was coalesced.
*/
eventsToRecord: eventsToRecord,
ignoreIdRegEx: /ext-gen(?:\d+)/,
inputRe: /^(input|textarea)$/i,
constructor: function(config) {
var me = this,
events = config && config.eventsToRecord;
if (events) {
me.eventsToRecord = Ext.apply(Ext.apply({}, me.eventsToRecord), // duplicate
events);
// and merge
delete config.eventsToRecord;
}
// don't smash
me.callParent(arguments);
me.clear();
me.modKeys = [];
me.attachTo = me.attachTo || window;
},
clear: function() {
this.eventsRecorded = [];
},
listenToEvent: function(event) {
var me = this,
el = me.attachTo.document.body,
fn = function() {
return me.onEvent.apply(me, arguments);
},
cleaner = {};
if (el.attachEvent && el.ownerDocument.documentMode < 10) {
event = 'on' + event;
el.attachEvent(event, fn);
cleaner.destroy = function() {
if (fn) {
el.detachEvent(event, fn);
fn = null;
}
};
} else {
el.addEventListener(event, fn, true);
cleaner.destroy = function() {
if (fn) {
el.removeEventListener(event, fn, true);
fn = null;
}
};
}
return cleaner;
},
coalesce: function(rec, ev) {
var me = this,
events = me.eventsRecorded,
length = events.length,
tail = length && events[length - 1],
tail2 = (length > 1) && events[length - 2],
tail3 = (length > 2) && events[length - 3];
if (!tail) {
return false;
}
if (rec.type === 'mousemove') {
if (tail.type === 'mousemove' && rec.ts - tail.ts < 200) {
rec.ts = tail.ts;
events[length - 1] = rec;
return true;
}
} else if (rec.type === 'click') {
if (tail2 && tail.type === 'mouseup' && tail2.type === 'mousedown') {
if (rec.button == tail.button && rec.button == tail2.button && rec.target == tail.target && rec.target == tail2.target && me.samePt(rec, tail) && me.samePt(rec, tail2)) {
events.pop();
// remove mouseup
tail2.type = 'mduclick';
return true;
}
}
} else if (rec.type === 'keyup') {
// tail3 = { type: "type", text: "..." },
// tail2 = { type: "keydown", charCode: 65, keyCode: 65 },
// tail = { type: "keypress", charCode: 97, keyCode: 97 },
// rec = { type: "keyup", charCode: 65, keyCode: 65 },
if (tail2 && tail.type === 'keypress' && tail2.type === 'keydown') {
if (rec.target === tail.target && rec.target === tail2.target) {
events.pop();
// remove keypress
tail2.type = 'type';
tail2.text = String.fromCharCode(tail.charCode);
delete tail2.charCode;
delete tail2.keyCode;
if (tail3 && tail3.type === 'type') {
if (tail3.text && tail3.target === tail2.target) {
tail3.text += tail2.text;
events.pop();
}
}
return true;
}
}
// tail = { type: "keydown", charCode: 40, keyCode: 40 },
// rec = { type: "keyup", charCode: 40, keyCode: 40 },
else if (me.completeKeyStroke(tail, rec)) {
tail.type = 'type';
me.completeSpecialKeyStroke(ev.target, tail, rec);
return true;
}
// tail2 = { type: "keydown", charCode: 40, keyCode: 40 },
// tail = { type: "scroll", ... },
// rec = { type: "keyup", charCode: 40, keyCode: 40 },
else if (tail.type === 'scroll' && me.completeKeyStroke(tail2, rec)) {
tail2.type = 'type';
me.completeSpecialKeyStroke(ev.target, tail2, rec);
// swap the order of type and scroll events
events.pop();
events.pop();
events.push(tail, tail2);
return true;
}
}
return false;
},
completeKeyStroke: function(down, up) {
if (down && down.type === 'keydown' && down.keyCode === up.keyCode) {
delete down.charCode;
return true;
}
return false;
},
completeSpecialKeyStroke: function(target, down, up) {
var key = this.specialKeysByCode[up.keyCode];
if (key && this.inputRe.test(target.tagName)) {
// home,end,arrow keys + shift get crazy, so encode selection/caret
delete down.keyCode;
down.key = key;
down.selection = this.getTextSelection(target);
if (down.selection[0] === down.selection[1]) {
down.caret = down.selection[0];
delete down.selection;
}
return true;
}
return false;
},
getElementXPath: function(el) {
var me = this,
good = false,
xpath = [],
count, sibling, t, tag;
for (t = el; t; t = t.parentNode) {
if (t == me.attachTo.document.body) {
xpath.unshift('~');
good = true;
break;
}
if (t.id && !me.ignoreIdRegEx.test(t.id)) {
xpath.unshift('#' + t.id);
good = true;
break;
}
for (count = 1 , sibling = t; !!(sibling = sibling.previousSibling); ) {
if (sibling.tagName == t.tagName) {
++count;
}
}
tag = t.tagName.toLowerCase();
if (count < 2) {
xpath.unshift(tag);
} else {
xpath.unshift(tag + '[' + count + ']');
}
}
return good ? xpath.join('/') : null;
},
getRecordedEvents: function() {
return this.eventsRecorded;
},
onEvent: function(ev) {
var me = this,
e = new Ext.event.Event(ev),
info = me.eventsToRecord[e.type],
root, modKeys, elXY,
rec = {
type: e.type,
ts: me.getTimestamp(),
target: me.getElementXPath(e.target)
},
xy;
if (!info || !rec.target) {
return;
}
root = e.target.ownerDocument;
root = root.defaultView || root.parentWindow;
// Standards || IE
if (root !== me.attachTo) {
return;
}
if (me.eventsToRecord.scroll) {
me.syncScroll(e.target);
}
if (info.xy) {
xy = e.getXY();
if (info.pageCoords || !rec.target) {
rec.px = xy[0];
rec.py = xy[1];
} else {
elXY = Ext.fly(e.getTarget()).getXY();
xy[0] -= elXY[0];
xy[1] -= elXY[1];
rec.x = xy[0];
rec.y = xy[1];
}
}
if (info.button) {
if ('buttons' in ev) {
rec.button = ev.buttons;
} else // LEFT=1, RIGHT=2, MIDDLE=4, etc.
{
rec.button = ev.button;
}
if (!rec.button && info.whileDrag) {
return;
}
}
if (info.wheel) {
rec.type = 'wheel';
if (info.event === 'wheel') {
// Current FireFox (technically IE9+ if we use addEventListener but
// checking document.onwheel does not detect this)
rec.dx = ev.deltaX;
rec.dy = ev.deltaY;
} else if (typeof ev.wheelDeltaX === 'number') {
// new WebKit has both X & Y
rec.dx = -1 / 40 * ev.wheelDeltaX;
rec.dy = -1 / 40 * ev.wheelDeltaY;
} else if (ev.wheelDelta) {
// old WebKit and IE
rec.dy = -1 / 40 * ev.wheelDelta;
} else if (ev.detail) {
// Old Gecko
rec.dy = ev.detail;
}
}
if (info.modKeys) {
me.modKeys[0] = e.altKey ? 'A' : '';
me.modKeys[1] = e.ctrlKey ? 'C' : '';
me.modKeys[2] = e.metaKey ? 'M' : '';
me.modKeys[3] = e.shiftKey ? 'S' : '';
modKeys = me.modKeys.join('');
if (modKeys) {
rec.modKeys = modKeys;
}
}
if (info.key) {
rec.charCode = e.getCharCode();
rec.keyCode = e.getKey();
}
if (me.coalesce(rec, e)) {
me.fireEvent('coalesce', me, rec);
} else {
me.eventsRecorded.push(rec);
me.fireEvent('add', me, rec);
}
},
onStart: function() {
var me = this,
ddm = me.attachTo.Ext.dd.DragDropManager,
evproto = me.attachTo.Ext.EventObjectImpl.prototype,
special = [];
// FireFox does not support the 'mousewheel' event but does support the
// 'wheel' event instead.
Recorder.prototype.eventsToRecord.wheel.event = ('onwheel' in me.attachTo.document) ? 'wheel' : 'mousewheel';
me.listeners = [];
Ext.Object.each(me.eventsToRecord, function(name, value) {
if (value && value.listen !== false) {
if (!value.event) {
value.event = name;
}
if (value.alt && value.alt !== name) {
// The 'drag' event is just mousemove while buttons are pressed,
// so if there is a mousemove entry as well, ignore the drag
if (!me.eventsToRecord[value.alt]) {
special.push(value);
}
} else {
me.listeners.push(me.listenToEvent(value.event));
}
}
});
Ext.each(special, function(info) {
me.eventsToRecord[info.alt] = info;
me.listeners.push(me.listenToEvent(info.alt));
});
me.ddmStopEvent = ddm.stopEvent;
ddm.stopEvent = Ext.Function.createSequence(ddm.stopEvent, function(e) {
me.onEvent(e);
});
me.evStopEvent = evproto.stopEvent;
evproto.stopEvent = Ext.Function.createSequence(evproto.stopEvent, function() {
me.onEvent(this);
});
},
onStop: function() {
var me = this;
Ext.destroy(me.listeners);
me.listeners = null;
me.attachTo.Ext.dd.DragDropManager.stopEvent = me.ddmStopEvent;
me.attachTo.Ext.EventObjectImpl.prototype.stopEvent = me.evStopEvent;
},
samePt: function(pt1, pt2) {
return pt1.x == pt2.x && pt1.y == pt2.y;
},
syncScroll: function(el) {
var me = this,
ts = me.getTimestamp(),
oldX, oldY, x, y, scrolled, rec;
for (var p = el; p; p = p.parentNode) {
oldX = p.$lastScrollLeft;
oldY = p.$lastScrollTop;
x = p.scrollLeft;
y = p.scrollTop;
scrolled = false;
if (oldX !== x) {
if (x) {
scrolled = true;
}
p.$lastScrollLeft = x;
}
if (oldY !== y) {
if (y) {
scrolled = true;
}
p.$lastScrollTop = y;
}
if (scrolled) {
//console.log('scroll x:' + x + ' y:' + y, p);
me.eventsRecorded.push(rec = {
type: 'scroll',
target: me.getElementXPath(p),
ts: ts,
pos: [
x,
y
]
});
me.fireEvent('add', me, rec);
}
if (p.tagName === 'BODY') {
break;
}
}
}
};
});
/**
* Describes a gauge needle as a shape defined in SVG path syntax.
*
* Note: this class and its subclasses are not supposed to be instantiated directly
* - an object should be passed the gauge's {@link Ext.ux.gauge.Gauge#needle}
* config instead. Needle instances are also not supposed to be moved
* between gauges.
*/
Ext.define('Ext.ux.gauge.needle.Abstract', {
mixins: [
'Ext.mixin.Factoryable'
],
alias: 'gauge.needle.abstract',
isNeedle: true,
config: {
/**
* The generator function for the needle's shape.
* Because the gauge component is resizable, and it is generally
* desirable to resize the needle along with the gauge, the needle's
* shape should have an ability to grow, typically non-uniformly,
* which necessitates a generator function that will update the needle's
* path, so that its proportions are appropriate for the current gauge size.
*
* The generator function is given two parameters: the inner and outer
* radius of the needle. For example, for a straight arrow, the path
* definition is expected to have the base of the needle at the origin
* - (0, 0) coordinates - and point downwards. The needle will be automatically
* translated to the center of the gauge and rotated to represent the current
* gauge {@link Ext.ux.gauge.Gauge#value value}.
*
* @param {Function} path The path generator function.
* @param {Number} path.innerRadius The function's first parameter.
* @param {Number} path.outerRadius The function's second parameter.
* @return {String} path.return The shape of the needle in the SVG path syntax returned by
* the generator function.
*/
path: null,
/**
* The inner radius of the needle. This works just like the `innerRadius`
* config of the {@link Ext.ux.gauge.Gauge#trackStyle}.
* The default value is `25` to make sure the needle doesn't overlap with
* the value of the gauge shown at its center by default.
*
* @param {Number/String} [innerRadius=25]
*/
innerRadius: 25,
/**
* The outer radius of the needle. This works just like the `outerRadius`
* config of the {@link Ext.ux.gauge.Gauge#trackStyle}.
*
* @param {Number/String} [outerRadius='100% - 20']
*/
outerRadius: '100% - 20',
/**
* The shape generated by the {@link #path} function is used as the value
* for the `d` attribute of the SVG `` element. This element
* has the default class name of `.x-gauge-needle`, so that CSS can be used
* to give all gauge needles some common styling. To style a particular needle,
* one can use this config to add styles to the needle's `` element directly,
* or use a custom {@link Ext.ux.gauge.Gauge#cls class} for the needle's gauge
* and style the needle from there.
*
* This config is not supposed to be updated manually, the styles should
* always be updated by the means of the `setStyle` call. For example,
* this is not allowed:
*
* gauge.getStyle().fill = 'red'; // WRONG!
* gauge.setStyle({ 'fill': 'red' }); // correct
*
* Subsequent calls to the `setStyle` will add to the styles set previously
* or overwrite their values, but won't remove them. If you'd like to style
* from a clean slate, setting the style to `null` first will remove the styles
* previously set:
*
* gauge.getNeedle().setStyle(null);
*
* If an SVG shape was produced by a designer rather than programmatically,
* in other words, the {@link #path} function returns the same shape regardless
* of the parameters it was given, the uniform scaling of said shape is the only
* option, if one wants to use gauges of different sizes. In this case,
* it's possible to specify the desired scale by using the `transform` style,
* for example:
*
* transform: 'scale(0.35)'
*
* @param {Object} style
*/
style: null,
/**
* @private
* @param {Number} radius
*/
radius: 0,
/**
* @private
* Expected in the initial config, required during construction.
* @param {Ext.ux.gauge.Gauge} gauge
*/
gauge: null
},
constructor: function(config) {
this.initConfig(config);
},
applyInnerRadius: function(innerRadius) {
return this.getGauge().getRadiusFn(innerRadius);
},
applyOuterRadius: function(outerRadius) {
return this.getGauge().getRadiusFn(outerRadius);
},
updateRadius: function() {
this.regeneratePath();
},
setTransform: function(centerX, centerY, rotation) {
var needleGroup = this.getNeedleGroup();
needleGroup.setStyle('transform', 'translate(' + centerX + 'px,' + centerY + 'px) ' + 'rotate(' + rotation + 'deg)');
},
applyPath: function(path) {
return Ext.isFunction(path) ? path : null;
},
updatePath: function(path) {
this.regeneratePath(path);
},
regeneratePath: function(path) {
path = path || this.getPath();
var me = this,
radius = me.getRadius(),
inner = me.getInnerRadius()(radius),
outer = me.getOuterRadius()(radius),
d = outer > inner ? path(inner, outer) : '';
me.getNeedlePath().dom.setAttribute('d', d);
},
getNeedleGroup: function() {
var gauge = this.getGauge(),
group = this.needleGroup;
// The gauge positions the needle by calling its `setTransform` method,
// which applies a transformation to the needle's group, that contains
// the actual path element. This is done because we need the ability to
// transform the path independently from it's position in the gauge.
// For example, if the needle has to be made bigger, is shouldn't be
// part of the transform that centers it in the gauge and rotates it
// to point at the current value.
if (!group) {
group = this.needleGroup = Ext.get(document.createElementNS(gauge.svgNS, 'g'));
gauge.getSvg().appendChild(group);
}
return group;
},
getNeedlePath: function() {
var me = this,
pathElement = me.pathElement;
if (!pathElement) {
pathElement = me.pathElement = Ext.get(document.createElementNS(me.getGauge().svgNS, 'path'));
pathElement.dom.setAttribute('class', Ext.baseCSSPrefix + 'gauge-needle');
me.getNeedleGroup().appendChild(pathElement);
}
return pathElement;
},
updateStyle: function(style) {
var pathElement = this.getNeedlePath();
// Note that we are setting the `style` attribute, e.g `style="fill: red"`,
// instead of path attributes individually, e.g. `fill="red"` because
// the attribute styles defined in CSS classes will override the values
// of attributes set on the elements individually.
if (Ext.isObject(style)) {
pathElement.setStyle(style);
} else {
pathElement.dom.removeAttribute('style');
}
},
destroy: function() {
var me = this;
me.pathElement = Ext.destroy(me.pathElement);
me.needleGroup = Ext.destroy(me.needleGroup);
me.setGauge(null);
}
});
/**
* Displays a value within the given interval as a gauge. For example:
*
* @example
* Ext.create({
* xtype: 'panel',
* renderTo: document.body,
* width: 200,
* height: 200,
* layout: 'fit',
* items: {
* xtype: 'gauge',
* padding: 20,
* value: 55,
* minValue: 40,
* maxValue: 80
* }
* });
*
* It's also possible to use gauges to create loading indicators:
*
* @example
* Ext.create({
* xtype: 'panel',
* renderTo: document.body,
* width: 200,
* height: 200,
* layout: 'fit',
* items: {
* xtype: 'gauge',
* padding: 20,
* trackStart: 0,
* trackLength: 360,
* value: 20,
* valueStyle: {
* round: true
* },
* textTpl: 'Loading...',
* animation: {
* easing: 'linear',
* duration: 100000
* }
* }
* }).items.first().setAngleOffset(360 * 100);
*
* Gauges can contain needles as well.
*
* @example
* Ext.create({
* xtype: 'panel',
* renderTo: document.body,
* width: 200,
* height: 200,
* layout: 'fit',
* items: {
* xtype: 'gauge',
* padding: 20,
* value: 55,
* minValue: 40,
* maxValue: 80,
* needle: 'wedge'
* }
* });
*
*/
Ext.define('Ext.ux.gauge.Gauge', {
alternateClassName: 'Ext.ux.Gauge',
extend: 'Ext.Gadget',
xtype: 'gauge',
requires: [
'Ext.ux.gauge.needle.Abstract',
'Ext.util.Region'
],
config: {
/**
* @cfg {Number/String} padding
* Gauge sector padding in pixels or percent of width/height, whichever is smaller.
*/
padding: 10,
/**
* @cfg {Number} trackStart
* The angle in the [0, 360) interval at which the gauge's track sector starts.
* E.g. 0 for 3 o-clock, 90 for 6 o-clock, 180 for 9 o-clock, 270 for noon.
*/
trackStart: 135,
/**
* @cfg {Number} trackLength
* The angle in the (0, 360] interval to add to the {@link #trackStart} angle
* to determine the angle at which the track ends.
*/
trackLength: 270,
/**
* @cfg {Number} angleOffset
* The angle at which the {@link #minValue} starts in case of a circular gauge.
*/
angleOffset: 0,
/**
* @cfg {Number} minValue
* The minimum value that the gauge can represent.
*/
minValue: 0,
/**
* @cfg {Number} maxValue
* The maximum value that the gauge can represent.
*/
maxValue: 100,
/**
* @cfg {Number} value
* The current value of the gauge.
*/
value: 50,
/**
* @cfg {Ext.ux.gauge.needle.Abstract} needle
* A config object for the needle to be used by the gauge.
* The needle will track the current {@link #value}.
* The default needle type is 'diamond', so if a config like
*
* needle: {
* outerRadius: '100%'
* }
*
* is used, the app/view still has to require
* the `Ext.ux.gauge.needle.Diamond` class.
* If a type is specified explicitly
*
* needle: {
* type: 'arrow'
* }
*
* it's straightforward which class should be required.
*/
needle: null,
needleDefaults: {
cached: true,
$value: {
type: 'diamond'
}
},
/**
* @cfg {Boolean} [clockwise=true]
* `true` - {@link #cfg!value} increments in a clockwise fashion
* `false` - {@link #cfg!value} increments in an anticlockwise fashion
*/
clockwise: true,
/**
* @cfg {Ext.XTemplate} textTpl
* The template for the text in the center of the gauge.
* The available data values are:
* - `value` - The {@link #cfg!value} of the gauge.
* - `percent` - The value as a percentage between 0 and 100.
* - `minValue` - The value of the {@link #cfg!minValue} config.
* - `maxValue` - The value of the {@link #cfg!maxValue} config.
* - `delta` - The delta between the {@link #cfg!minValue} and {@link #cfg!maxValue}.
*/
textTpl: [
'{value:number("0.00")}%'
],
/**
* @cfg {String} [textAlign='c-c']
* If the gauge has a donut hole, the text will be centered inside it.
* Otherwise, the text will be centered in the middle of the gauge's
* bounding box. This config allows to alter the position of the text
* in the latter case. See the docs for the `align` option to the
* {@link Ext.util.Region#alignTo} method for possible ways of alignment
* of the text to the guage's bounding box.
*/
textAlign: 'c-c',
/**
* @cfg {Object} textOffset
* This config can be used to displace the {@link #textTpl text} from its default
* position in the center of the gauge by providing values for horizontal and
* vertical displacement.
* @cfg {Number} textOffset.dx Horizontal displacement.
* @cfg {Number} textOffset.dy Vertical displacement.
*/
textOffset: {
dx: 0,
dy: 0
},
/**
* @cfg {Object} trackStyle
* Track sector styles.
* @cfg {String/Object[]} trackStyle.fill Track sector fill color. Defaults to CSS value.
* It's also possible to have a linear gradient fill that starts at the top-left corner
* of the gauge and ends at its bottom-right corner, by providing an array of color stop
* objects. For example:
*
* trackStyle: {
* fill: [{
* offset: 0,
* color: 'green',
* opacity: 0.8
* }, {
* offset: 1,
* color: 'gold'
* }]
* }
*
* @cfg {Number} trackStyle.fillOpacity Track sector fill opacity. Defaults to CSS value.
* @cfg {String} trackStyle.stroke Track sector stroke color. Defaults to CSS value.
* @cfg {Number} trackStyle.strokeOpacity Track sector stroke opacity. Defaults to CSS value.
* @cfg {Number} trackStyle.strokeWidth Track sector stroke width. Defaults to CSS value.
* @cfg {Number/String} [trackStyle.outerRadius='100%'] The outer radius of the track sector.
* For example:
*
* outerRadius: '90%', // 90% of the maximum radius
* outerRadius: 100, // radius of 100 pixels
* outerRadius: '70% + 5', // 70% of the maximum radius plus 5 pixels
* outerRadius: '80% - 10', // 80% of the maximum radius minus 10 pixels
*
* @cfg {Number/String} [trackStyle.innerRadius='50%'] The inner radius of the track sector.
* See the `trackStyle.outerRadius` config documentation for more information.
* @cfg {Boolean} [trackStyle.round=false] Whether to round the track sector edges or not.
*/
trackStyle: {
outerRadius: '100%',
innerRadius: '100% - 20',
round: false
},
/**
* @cfg {Object} valueStyle
* Value sector styles.
* @cfg {String/Object[]} valueStyle.fill Value sector fill color. Defaults to CSS value.
* See the `trackStyle.fill` config documentation for more information.
* @cfg {Number} valueStyle.fillOpacity Value sector fill opacity. Defaults to CSS value.
* @cfg {String} valueStyle.stroke Value sector stroke color. Defaults to CSS value.
* @cfg {Number} valueStyle.strokeOpacity Value sector stroke opacity. Defaults to CSS value.
* @cfg {Number} valueStyle.strokeWidth Value sector stroke width. Defaults to CSS value.
* @cfg {Number/String} [valueStyle.outerRadius='100% - 4'] The outer radius of the value sector.
* See the `trackStyle.outerRadius` config documentation for more information.
* @cfg {Number/String} [valueStyle.innerRadius='50% + 4'] The inner radius of the value sector.
* See the `trackStyle.outerRadius` config documentation for more information.
* @cfg {Boolean} [valueStyle.round=false] Whether to round the value sector edges or not.
*/
valueStyle: {
outerRadius: '100% - 2',
innerRadius: '100% - 18',
round: false
},
/**
* @cfg {Object/Boolean} [animation=true]
* The animation applied to the gauge on changes to the {@link #value}
* and the {@link #angleOffset} configs. Defaults to 1 second animation
* with the 'out' easing.
* @cfg {Number} animation.duration The duraction of the animation.
* @cfg {String} animation.easing The easing function to use for the animation.
* Possible values are:
* - `linear` - no easing, no acceleration
* - `in` - accelerating from zero velocity
* - `out` - (default) decelerating to zero velocity
* - `inOut` - acceleration until halfway, then deceleration
*/
animation: true
},
baseCls: Ext.baseCSSPrefix + 'gauge',
template: [
{
reference: 'bodyElement',
children: [
{
reference: 'textElement',
cls: Ext.baseCSSPrefix + 'gauge-text'
}
]
}
],
defaultBindProperty: 'value',
pathAttributes: {
// The properties in the `trackStyle` and `valueStyle` configs
// that are path attributes.
fill: true,
fillOpacity: true,
stroke: true,
strokeOpacity: true,
strokeWidth: true
},
easings: {
linear: Ext.identityFn,
// cubic easings
'in': function(t) {
return t * t * t;
},
out: function(t) {
return (--t) * t * t + 1;
},
inOut: function(t) {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}
},
resizeDelay: 0,
// in milliseconds
resizeTimerId: 0,
size: null,
// cached size
svgNS: 'http://www.w3.org/2000/svg',
svg: null,
// SVG document
defs: null,
// the `defs` section of the SVG document
trackArc: null,
valueArc: null,
trackGradient: null,
valueGradient: null,
fx: null,
// either the `value` or the `angleOffset` animation
fxValue: 0,
// the actual value rendered/animated
fxAngleOffset: 0,
constructor: function(config) {
var me = this;
me.fitSectorInRectCache = {
startAngle: null,
lengthAngle: null,
minX: null,
maxX: null,
minY: null,
maxY: null
};
me.interpolator = me.createInterpolator();
me.callParent([
config
]);
me.el.on('resize', 'onElementResize', me);
},
doDestroy: function() {
var me = this;
Ext.undefer(me.resizeTimerId);
me.el.un('resize', 'onElementResize', me);
me.stopAnimation();
me.setNeedle(null);
me.trackGradient = Ext.destroy(me.trackGradient);
me.valueGradient = Ext.destroy(me.valueGradient);
me.defs = Ext.destroy(me.defs);
me.svg = Ext.destroy(me.svg);
me.callParent();
},
//
afterComponentLayout: function(width, height, oldWidth, oldHeight) {
this.callParent([
width,
height,
oldWidth,
oldHeight
]);
if (Ext.isIE9) {
this.handleResize();
}
},
//
onElementResize: function(element, size) {
this.handleResize(size);
},
handleResize: function(size, instantly) {
var me = this,
el = me.element;
if (!(el && (size = size || el.getSize()) && size.width && size.height)) {
return;
}
me.resizeTimerId = Ext.undefer(me.resizeTimerId);
if (!instantly && me.resizeDelay) {
me.resizeTimerId = Ext.defer(me.handleResize, me.resizeDelay, me, [
size,
true
]);
return;
}
me.size = size;
me.resizeHandler(size);
},
updateMinValue: function(minValue) {
var me = this;
me.interpolator.setDomain(minValue, me.getMaxValue());
if (!me.isConfiguring) {
me.render();
}
},
updateMaxValue: function(maxValue) {
var me = this;
me.interpolator.setDomain(me.getMinValue(), maxValue);
if (!me.isConfiguring) {
me.render();
}
},
updateAngleOffset: function(angleOffset, oldAngleOffset) {
var me = this,
animation = me.getAnimation();
me.fxAngleOffset = angleOffset;
if (me.isConfiguring) {
return;
}
if (animation.duration) {
me.animate(oldAngleOffset, angleOffset, animation.duration, me.easings[animation.easing], function(angleOffset) {
me.fxAngleOffset = angleOffset;
me.render();
});
} else {
me.render();
}
},
//
applyTrackStart: function(trackStart) {
if (trackStart < 0 || trackStart >= 360) {
Ext.raise("'trackStart' should be within [0, 360).");
}
return trackStart;
},
applyTrackLength: function(trackLength) {
if (trackLength <= 0 || trackLength > 360) {
Ext.raise("'trackLength' should be within (0, 360].");
}
return trackLength;
},
//
updateTrackStart: function(trackStart) {
var me = this;
if (!me.isConfiguring) {
me.render();
}
},
updateTrackLength: function(trackLength) {
var me = this;
me.interpolator.setRange(0, trackLength);
if (!me.isConfiguring) {
me.render();
}
},
applyPadding: function(padding) {
if (typeof padding === 'string') {
var ratio = parseFloat(padding) / 100;
return function(x) {
return x * ratio;
};
}
return function() {
return padding;
};
},
updatePadding: function() {
if (!this.isConfiguring) {
this.render();
}
},
applyValue: function(value) {
var minValue = this.getMinValue(),
maxValue = this.getMaxValue();
return Math.min(Math.max(value, minValue), maxValue);
},
updateValue: function(value, oldValue) {
var me = this,
animation = me.getAnimation();
me.fxValue = value;
if (me.isConfiguring) {
return;
}
me.writeText();
if (animation.duration) {
me.animate(oldValue, value, animation.duration, me.easings[animation.easing], function(value) {
me.fxValue = value;
me.render();
});
} else {
me.render();
}
},
applyTextTpl: function(textTpl) {
if (textTpl && !textTpl.isTemplate) {
textTpl = new Ext.XTemplate(textTpl);
}
return textTpl;
},
applyTextOffset: function(offset) {
offset = offset || {};
offset.dx = offset.dx || 0;
offset.dy = offset.dy || 0;
return offset;
},
updateTextTpl: function() {
this.writeText();
if (!this.isConfiguring) {
this.centerText();
}
},
// text will be centered on first size
writeText: function(options) {
var me = this,
value = me.getValue(),
minValue = me.getMinValue(),
maxValue = me.getMaxValue(),
delta = maxValue - minValue,
textTpl = me.getTextTpl();
textTpl.overwrite(me.textElement, {
value: value,
percent: (value - minValue) / delta * 100,
minValue: minValue,
maxValue: maxValue,
delta: delta
});
},
centerText: function(cx, cy, sectorRegion, innerRadius, outerRadius) {
var textElement = this.textElement,
textAlign = this.getTextAlign(),
alignedRegion, textBox;
if (Ext.Number.isEqual(innerRadius, 0, 0.1) || sectorRegion.isOutOfBound({
x: cx,
y: cy
})) {
alignedRegion = textElement.getRegion().alignTo({
align: textAlign,
// align text region's center to sector region's center
target: sectorRegion
});
textElement.setLeft(alignedRegion.left);
textElement.setTop(alignedRegion.top);
} else {
textBox = textElement.getBox();
textElement.setLeft(cx - textBox.width / 2);
textElement.setTop(cy - textBox.height / 2);
}
},
camelCaseRe: /([a-z])([A-Z])/g,
/**
* @private
*/
camelToHyphen: function(name) {
return name.replace(this.camelCaseRe, '$1-$2').toLowerCase();
},
applyTrackStyle: function(trackStyle) {
var me = this,
trackGradient;
trackStyle.innerRadius = me.getRadiusFn(trackStyle.innerRadius);
trackStyle.outerRadius = me.getRadiusFn(trackStyle.outerRadius);
if (Ext.isArray(trackStyle.fill)) {
trackGradient = me.getTrackGradient();
me.setGradientStops(trackGradient, trackStyle.fill);
trackStyle.fill = 'url(#' + trackGradient.dom.getAttribute('id') + ')';
}
return trackStyle;
},
updateTrackStyle: function(trackStyle) {
var me = this,
trackArc = Ext.fly(me.getTrackArc()),
name;
for (name in trackStyle) {
if (name in me.pathAttributes) {
trackArc.setStyle(me.camelToHyphen(name), trackStyle[name]);
} else {
trackArc.setStyle(name, trackStyle[name]);
}
}
},
applyValueStyle: function(valueStyle) {
var me = this,
valueGradient;
valueStyle.innerRadius = me.getRadiusFn(valueStyle.innerRadius);
valueStyle.outerRadius = me.getRadiusFn(valueStyle.outerRadius);
if (Ext.isArray(valueStyle.fill)) {
valueGradient = me.getValueGradient();
me.setGradientStops(valueGradient, valueStyle.fill);
valueStyle.fill = 'url(#' + valueGradient.dom.getAttribute('id') + ')';
}
return valueStyle;
},
updateValueStyle: function(valueStyle) {
var me = this,
valueArc = Ext.fly(me.getValueArc()),
name;
for (name in valueStyle) {
if (name in me.pathAttributes) {
valueArc.setStyle(me.camelToHyphen(name), valueStyle[name]);
} else {
valueArc.setStyle(name, valueStyle[name]);
}
}
},
/**
* @private
*/
getRadiusFn: function(radius) {
var result, pos, ratio,
increment = 0;
if (Ext.isNumber(radius)) {
result = function() {
return radius;
};
} else if (Ext.isString(radius)) {
radius = radius.replace(/ /g, '');
ratio = parseFloat(radius) / 100;
pos = radius.search('%');
// E.g. '100% - 4'
if (pos < radius.length - 1) {
increment = parseFloat(radius.substr(pos + 1));
}
result = function(radius) {
return radius * ratio + increment;
};
result.ratio = ratio;
}
return result;
},
getSvg: function() {
var me = this,
svg = me.svg;
if (!svg) {
svg = me.svg = Ext.get(document.createElementNS(me.svgNS, 'svg'));
me.bodyElement.append(svg);
}
return svg;
},
getTrackArc: function() {
var me = this,
trackArc = me.trackArc;
if (!trackArc) {
trackArc = me.trackArc = document.createElementNS(me.svgNS, 'path');
me.getSvg().append(trackArc, true);
// Note: Ext.dom.Element.addCls doesn't work on SVG elements,
// as it simply assigns a class string to el.dom.className,
// which in case of SVG is no simple string:
// SVGAnimatedString {baseVal: "x-gauge-track", animVal: "x-gauge-track"}
trackArc.setAttribute('class', Ext.baseCSSPrefix + 'gauge-track');
}
return trackArc;
},
getValueArc: function() {
var me = this,
valueArc = me.valueArc;
me.getTrackArc();
// make sure the track arc is created first for proper draw order
if (!valueArc) {
valueArc = me.valueArc = document.createElementNS(me.svgNS, 'path');
me.getSvg().append(valueArc, true);
valueArc.setAttribute('class', Ext.baseCSSPrefix + 'gauge-value');
}
return valueArc;
},
applyNeedle: function(needle, oldNeedle) {
// Make sure the track and value elements have been already created,
// so that the needle element renders on top.
this.getValueArc();
return Ext.Factory.gaugeNeedle.update(oldNeedle, needle, this, 'createNeedle', 'needleDefaults');
},
createNeedle: function(config) {
return Ext.apply({
gauge: this
}, config);
},
getDefs: function() {
var me = this,
defs = me.defs;
if (!defs) {
defs = me.defs = Ext.get(document.createElementNS(me.svgNS, 'defs'));
me.getSvg().appendChild(defs);
}
return defs;
},
/**
* @private
*/
setGradientSize: function(gradient, x1, y1, x2, y2) {
gradient.setAttribute('x1', x1);
gradient.setAttribute('y1', y1);
gradient.setAttribute('x2', x2);
gradient.setAttribute('y2', y2);
},
/**
* @private
*/
resizeGradients: function(size) {
var me = this,
trackGradient = me.getTrackGradient(),
valueGradient = me.getValueGradient(),
x1 = 0,
y1 = size.height / 2,
x2 = size.width,
y2 = size.height / 2;
me.setGradientSize(trackGradient.dom, x1, y1, x2, y2);
me.setGradientSize(valueGradient.dom, x1, y1, x2, y2);
},
/**
* @private
*/
setGradientStops: function(gradient, stops) {
var ln = stops.length,
i, stopCfg, stopEl;
while (gradient.firstChild) {
gradient.removeChild(gradient.firstChild);
}
for (i = 0; i < ln; i++) {
stopCfg = stops[i];
stopEl = document.createElementNS(this.svgNS, 'stop');
gradient.appendChild(stopEl);
stopEl.setAttribute('offset', stopCfg.offset);
stopEl.setAttribute('stop-color', stopCfg.color);
('opacity' in stopCfg) && stopEl.setAttribute('stop-opacity', stopCfg.opacity);
}
},
getTrackGradient: function() {
var me = this,
trackGradient = me.trackGradient;
if (!trackGradient) {
trackGradient = me.trackGradient = Ext.get(document.createElementNS(me.svgNS, 'linearGradient'));
// Using absolute values for x1, y1, x2, y2 attributes.
trackGradient.dom.setAttribute('gradientUnits', 'userSpaceOnUse');
me.getDefs().appendChild(trackGradient);
Ext.get(trackGradient);
}
// assign unique ID
return trackGradient;
},
getValueGradient: function() {
var me = this,
valueGradient = me.valueGradient;
if (!valueGradient) {
valueGradient = me.valueGradient = Ext.get(document.createElementNS(me.svgNS, 'linearGradient'));
// Using absolute values for x1, y1, x2, y2 attributes.
valueGradient.dom.setAttribute('gradientUnits', 'userSpaceOnUse');
me.getDefs().appendChild(valueGradient);
Ext.get(valueGradient);
}
// assign unique ID
return valueGradient;
},
getArcPoint: function(centerX, centerY, radius, degrees) {
var radians = degrees / 180 * Math.PI;
return [
centerX + radius * Math.cos(radians),
centerY + radius * Math.sin(radians)
];
},
isCircle: function(startAngle, endAngle) {
return Ext.Number.isEqual(Math.abs(endAngle - startAngle), 360, 0.001);
},
getArcPath: function(centerX, centerY, innerRadius, outerRadius, startAngle, endAngle, round) {
var me = this,
isCircle = me.isCircle(startAngle, endAngle),
// It's not possible to draw a circle using arcs.
endAngle = endAngle - 0.01,
innerStartPoint = me.getArcPoint(centerX, centerY, innerRadius, startAngle),
innerEndPoint = me.getArcPoint(centerX, centerY, innerRadius, endAngle),
outerStartPoint = me.getArcPoint(centerX, centerY, outerRadius, startAngle),
outerEndPoint = me.getArcPoint(centerX, centerY, outerRadius, endAngle),
large = endAngle - startAngle <= 180 ? 0 : 1,
path = [
'M',
innerStartPoint[0],
innerStartPoint[1],
'A',
innerRadius,
innerRadius,
0,
large,
1,
innerEndPoint[0],
innerEndPoint[1]
],
capRadius = (outerRadius - innerRadius) / 2;
if (isCircle) {
path.push('M', outerEndPoint[0], outerEndPoint[1]);
} else {
if (round) {
path.push('A', capRadius, capRadius, 0, 0, 0, outerEndPoint[0], outerEndPoint[1]);
} else {
path.push('L', outerEndPoint[0], outerEndPoint[1]);
}
}
path.push('A', outerRadius, outerRadius, 0, large, 0, outerStartPoint[0], outerStartPoint[1]);
if (round && !isCircle) {
path.push('A', capRadius, capRadius, 0, 0, 0, innerStartPoint[0], innerStartPoint[1]);
}
path.push('Z');
return path.join(' ');
},
resizeHandler: function(size) {
var me = this,
svg = me.getSvg();
svg.setSize(size);
me.resizeGradients(size);
me.render();
},
/**
* @private
* Creates a linear interpolator function that itself has a few methods:
* - `setDomain(from, to)`
* - `setRange(from, to)`
* - `getDomain` - returns the domain as a [from, to] array
* - `getRange` - returns the range as a [from, to] array
* @param {Boolean} [rangeCheck=false]
* Whether to allow out of bounds values for domain and range.
* @return {Function} The interpolator function:
* `interpolator(domainValue, isInvert)`.
* If the `isInvert` parameter is `true`, the start of domain will correspond
* to the end of range. This is useful, for example, when you want to render
* increasing domain values counter-clockwise instead of clockwise.
*/
createInterpolator: function(rangeCheck) {
var domainStart = 0,
domainDelta = 1,
rangeStart = 0,
rangeEnd = 1;
var interpolator = function(x, invert) {
var t = 0;
if (domainDelta) {
t = (x - domainStart) / domainDelta;
if (rangeCheck) {
t = Math.max(0, t);
t = Math.min(1, t);
}
if (invert) {
t = 1 - t;
}
}
return (1 - t) * rangeStart + t * rangeEnd;
};
interpolator.setDomain = function(a, b) {
domainStart = a;
domainDelta = b - a;
return this;
};
interpolator.setRange = function(a, b) {
rangeStart = a;
rangeEnd = b;
return this;
};
interpolator.getDomain = function() {
return [
domainStart,
domainStart + domainDelta
];
};
interpolator.getRange = function() {
return [
rangeStart,
rangeEnd
];
};
return interpolator;
},
applyAnimation: function(animation) {
if (true === animation) {
animation = {};
} else if (false === animation) {
animation = {
duration: 0
};
}
if (!('duration' in animation)) {
animation.duration = 1000;
}
if (!(animation.easing in this.easings)) {
animation.easing = 'out';
}
return animation;
},
updateAnimation: function() {
this.stopAnimation();
},
/**
* @private
* @param {Number} from
* @param {Number} to
* @param {Number} duration
* @param {Function} easing
* @param {Function} fn Function to execute on every frame of animation.
* The function takes a single parameter - the value in the [from, to]
* range, interpolated based on current time and easing function.
* With certain easings, the value may overshoot the range slighly.
* @param {Object} scope
*/
animate: function(from, to, duration, easing, fn, scope) {
var me = this,
start = Ext.now(),
interpolator = me.createInterpolator().setRange(from, to);
function frame() {
var now = Ext.AnimationQueue.frameStartTime,
t = Math.min(now - start, duration) / duration,
value = interpolator(easing(t));
if (scope) {
if (typeof fn === 'string') {
scope[fn].call(scope, value);
} else {
fn.call(scope, value);
}
} else {
fn(value);
}
if (t >= 1) {
Ext.AnimationQueue.stop(frame, scope);
me.fx = null;
}
}
me.stopAnimation();
Ext.AnimationQueue.start(frame, scope);
me.fx = {
frame: frame,
scope: scope
};
},
/**
* Stops the current {@link #value} or {@link #angleOffset} animation.
*/
stopAnimation: function() {
var me = this;
if (me.fx) {
Ext.AnimationQueue.stop(me.fx.frame, me.fx.scope);
me.fx = null;
}
},
unitCircleExtrema: {
0: [
1,
0
],
90: [
0,
1
],
180: [
-1,
0
],
270: [
0,
-1
],
360: [
1,
0
],
450: [
0,
1
],
540: [
-1,
0
],
630: [
0,
-1
]
},
/**
* @private
*/
getUnitSectorExtrema: function(startAngle, lengthAngle) {
var extrema = this.unitCircleExtrema,
points = [],
angle;
for (angle in extrema) {
if (angle > startAngle && angle < startAngle + lengthAngle) {
points.push(extrema[angle]);
}
}
return points;
},
/**
* @private
* Given a rect with a known width and height, find the maximum radius of the donut
* sector that can fit into it, as well as the center point of such a sector.
* The end and start angles of the sector are also known, as well as the relationship
* between the inner and outer radii.
*/
fitSectorInRect: function(width, height, startAngle, lengthAngle, ratio) {
if (Ext.Number.isEqual(lengthAngle, 360, 0.001)) {
return {
cx: width / 2,
cy: height / 2,
radius: Math.min(width, height) / 2,
region: new Ext.util.Region(0, width, height, 0)
};
}
var me = this,
points, xx, yy, minX, maxX, minY, maxY,
cache = me.fitSectorInRectCache,
sameAngles = cache.startAngle === startAngle && cache.lengthAngle === lengthAngle;
if (sameAngles) {
minX = cache.minX;
maxX = cache.maxX;
minY = cache.minY;
maxY = cache.maxY;
} else {
points = me.getUnitSectorExtrema(startAngle, lengthAngle).concat([
me.getArcPoint(0, 0, 1, startAngle),
// start angle outer radius point
me.getArcPoint(0, 0, ratio, startAngle),
// start angle inner radius point
me.getArcPoint(0, 0, 1, startAngle + lengthAngle),
// end angle outer radius point
me.getArcPoint(0, 0, ratio, startAngle + lengthAngle)
]);
// end angle inner radius point
xx = points.map(function(point) {
return point[0];
});
yy = points.map(function(point) {
return point[1];
});
// The bounding box of a unit sector with the given properties.
minX = Math.min.apply(null, xx);
maxX = Math.max.apply(null, xx);
minY = Math.min.apply(null, yy);
maxY = Math.max.apply(null, yy);
cache.startAngle = startAngle;
cache.lengthAngle = lengthAngle;
cache.minX = minX;
cache.maxX = maxX;
cache.minY = minY;
cache.maxY = maxY;
}
var sectorWidth = maxX - minX,
sectorHeight = maxY - minY,
scaleX = width / sectorWidth,
scaleY = height / sectorHeight,
scale = Math.min(scaleX, scaleY),
// Region constructor takes: top, right, bottom, left.
sectorRegion = new Ext.util.Region(minY * scale, maxX * scale, maxY * scale, minX * scale),
rectRegion = new Ext.util.Region(0, width, height, 0),
alignedRegion = sectorRegion.alignTo({
align: 'c-c',
// align sector region's center to rect region's center
target: rectRegion
}),
dx = alignedRegion.left - minX * scale,
dy = alignedRegion.top - minY * scale;
return {
cx: dx,
cy: dy,
radius: scale,
region: alignedRegion
};
},
/**
* @private
*/
fitSectorInPaddedRect: function(width, height, padding, startAngle, lengthAngle, ratio) {
var result = this.fitSectorInRect(width - padding * 2, height - padding * 2, startAngle, lengthAngle, ratio);
result.cx += padding;
result.cy += padding;
result.region.translateBy(padding, padding);
return result;
},
/**
* @private
*/
normalizeAngle: function(angle) {
return (angle % 360 + 360) % 360;
},
render: function() {
if (!this.size) {
return;
}
var me = this,
textOffset = me.getTextOffset(),
trackArc = me.getTrackArc(),
valueArc = me.getValueArc(),
needle = me.getNeedle(),
clockwise = me.getClockwise(),
value = me.fxValue,
angleOffset = me.fxAngleOffset,
trackLength = me.getTrackLength(),
width = me.size.width,
height = me.size.height,
paddingFn = me.getPadding(),
padding = paddingFn(Math.min(width, height)),
trackStart = me.normalizeAngle(me.getTrackStart() + angleOffset),
// in the range of [0, 360)
trackEnd = trackStart + trackLength,
// in the range of (0, 720)
valueLength = me.interpolator(value),
trackStyle = me.getTrackStyle(),
valueStyle = me.getValueStyle(),
sector = me.fitSectorInPaddedRect(width, height, padding, trackStart, trackLength, trackStyle.innerRadius.ratio),
cx = sector.cx,
cy = sector.cy,
radius = sector.radius,
trackInnerRadius = Math.max(0, trackStyle.innerRadius(radius)),
trackOuterRadius = Math.max(0, trackStyle.outerRadius(radius)),
valueInnerRadius = Math.max(0, valueStyle.innerRadius(radius)),
valueOuterRadius = Math.max(0, valueStyle.outerRadius(radius)),
trackPath = me.getArcPath(cx, cy, trackInnerRadius, trackOuterRadius, trackStart, trackEnd, trackStyle.round),
valuePath = me.getArcPath(cx, cy, valueInnerRadius, valueOuterRadius, clockwise ? trackStart : trackEnd - valueLength, clockwise ? trackStart + valueLength : trackEnd, valueStyle.round);
me.centerText(cx + textOffset.dx, cy + textOffset.dy, sector.region, trackInnerRadius, trackOuterRadius);
trackArc.setAttribute('d', trackPath);
valueArc.setAttribute('d', valuePath);
if (needle) {
needle.setRadius(radius);
needle.setTransform(cx, cy, -90 + trackStart + valueLength);
}
me.fireEvent('render', me);
}
});
Ext.define('Ext.ux.gauge.needle.Arrow', {
extend: 'Ext.ux.gauge.needle.Abstract',
alias: 'gauge.needle.arrow',
config: {
path: function(ir, or) {
return or - ir > 30 ? "M0," + (ir + 5) + " L-4," + ir + " L-4," + (ir + 10) + " L-1," + (ir + 15) + " L-1," + (or - 7) + " L-5," + (or - 10) + " L0," + or + " L5," + (or - 10) + " L1," + (or - 7) + " L1," + (ir + 15) + " L4," + (ir + 10) + " L4," + ir + " Z" : '';
}
}
});
Ext.define('Ext.ux.gauge.needle.Diamond', {
extend: 'Ext.ux.gauge.needle.Abstract',
alias: 'gauge.needle.diamond',
config: {
path: function(ir, or) {
return or - ir > 10 ? 'M0,' + ir + ' L-4,' + (ir + 5) + ' L0,' + or + ' L4,' + (ir + 5) + ' Z' : '';
}
}
});
Ext.define('Ext.ux.gauge.needle.Rectangle', {
extend: 'Ext.ux.gauge.needle.Abstract',
alias: 'gauge.needle.rectangle',
config: {
path: function(ir, or) {
return or - ir > 10 ? "M-2," + ir + " L2," + ir + " L2," + or + " L-2," + or + " Z" : '';
}
}
});
Ext.define('Ext.ux.gauge.needle.Spike', {
extend: 'Ext.ux.gauge.needle.Abstract',
alias: 'gauge.needle.spike',
config: {
path: function(ir, or) {
return or - ir > 10 ? "M0," + (ir + 5) + " L-4," + ir + " L0," + or + " L4," + ir + " Z" : '';
}
}
});
Ext.define('Ext.ux.gauge.needle.Wedge', {
extend: 'Ext.ux.gauge.needle.Abstract',
alias: 'gauge.needle.wedge',
config: {
path: function(ir, or) {
return or - ir > 10 ? "M-4," + ir + " L0," + or + " L4," + ir + " Z" : '';
}
}
});
/**
* A ratings picker based on `Ext.Gadget`.
*
* @example
* Ext.create({
* xtype: 'rating',
* renderTo: Ext.getBody(),
* listeners: {
* change: function (picker, value) {
* console.log('Rating ' + value);
* }
* }
* });
*/
Ext.define('Ext.ux.rating.Picker', {
extend: 'Ext.Gadget',
xtype: 'rating',
focusable: true,
/*
* The "cachedConfig" block is basically the same as "config" except that these
* values are applied specially to the first instance of the class. After processing
* these configs, the resulting values are stored on the class `prototype` and the
* template DOM element also reflects these default values.
*/
cachedConfig: {
/**
* @cfg {String} [family]
* The CSS `font-family` to use for displaying the `{@link #glyphs}`.
*/
family: 'monospace',
/**
* @cfg {String/String[]/Number[]} [glyphs]
* Either a string containing the two glyph characters, or an array of two strings
* containing the individual glyph characters or an array of two numbers with the
* character codes for the individual glyphs.
*
* For example:
*
* @example
* Ext.create({
* xtype: 'rating',
* renderTo: Ext.getBody(),
* glyphs: [ 9671, 9670 ], // '◇◆',
* listeners: {
* change: function (picker, value) {
* console.log('Rating ' + value);
* }
* }
* });
*/
glyphs: '☆★',
/**
* @cfg {Number} [minimum=1]
* The minimum allowed `{@link #value}` (rating).
*/
minimum: 1,
/**
* @cfg {Number} [limit]
* The maximum allowed `{@link #value}` (rating).
*/
limit: 5,
/**
* @cfg {String/Object} [overStyle]
* Optional styles to apply to the rating glyphs when `{@link #trackOver}` is
* enabled.
*/
overStyle: null,
/**
* @cfg {Number} [rounding=1]
* The rounding to apply to values. Common choices are 0.5 (for half-steps) or
* 0.25 (for quarter steps).
*/
rounding: 1,
/**
* @cfg {String} [scale="125%"]
* The CSS `font-size` to apply to the glyphs. This value defaults to 125% because
* glyphs in the stock font tend to be too small. When using specially designed
* "icon fonts" you may want to set this to 100%.
*/
scale: '125%',
/**
* @cfg {String/Object} [selectedStyle]
* Optional styles to apply to the rating value glyphs.
*/
selectedStyle: null,
/**
* @cfg {Object/String/String[]/Ext.XTemplate/Function} tip
* A template or a function that produces the tooltip text. The `Object`, `String`
* and `String[]` forms are converted to an `Ext.XTemplate`. If a function is given,
* it will be called with an object parameter and should return the tooltip text.
* The object contains these properties:
*
* - component: The rating component requesting the tooltip.
* - tracking: The current value under the mouse cursor.
* - trackOver: The value of the `{@link #trackOver}` config.
* - value: The current value.
*
* Templates can use these properties to generate the proper text.
*/
tip: null,
/**
* @cfg {Boolean} [trackOver=true]
* Determines if mouse movements should temporarily update the displayed value.
* The actual `value` is only updated on `click` but this rather acts as the
* "preview" of the value prior to click.
*/
trackOver: true,
/**
* @cfg {Number} value
* The rating value. This value is bounded by `minimum` and `limit` and is also
* adjusted by the `rounding`.
*/
value: null,
//---------------------------------------------------------------------
// Private configs
/**
* @cfg {String} tooltipText
* The current tooltip text. This value is set into the DOM by the updater (hence
* only when it changes). This is intended for use by the tip manager
* (`{@link Ext.tip.QuickTipManager}`). Developers should never need to set this
* config since it is handled by virtue of setting other configs (such as the
* {@link #tooltip} or the {@link #value}.).
* @private
*/
tooltipText: null,
/**
* @cfg {Number} trackingValue
* This config is used to when `trackOver` is `true` and represents the tracked
* value. This config is maintained by our `mousemove` handler. This should not
* need to be set directly by user code.
* @private
*/
trackingValue: null
},
config: {
/**
* @cfg {Boolean/Object} [animate=false]
* Specifies an animation to use when changing the `{@link #value}`. When setting
* this config, it is probably best to set `{@link #trackOver}` to `false`.
*/
animate: null
},
// This object describes our element tree from the root.
element: {
cls: 'u' + Ext.baseCSSPrefix + 'rating-picker',
// Since we are replacing the entire "element" tree, we have to assign this
// "reference" as would our base class.
reference: 'element',
children: [
{
reference: 'innerEl',
cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-inner',
listeners: {
click: 'onClick',
mousemove: 'onMouseMove',
mouseenter: 'onMouseEnter',
mouseleave: 'onMouseLeave'
},
children: [
{
reference: 'valueEl',
cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-value'
},
{
reference: 'trackerEl',
cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-tracker'
}
]
}
]
},
// Tell the Binding system to default to our "value" config.
defaultBindProperty: 'value',
// Enable two-way data binding for the "value" config.
twoWayBindable: 'value',
overCls: 'u' + Ext.baseCSSPrefix + 'rating-picker-over',
trackOverCls: 'u' + Ext.baseCSSPrefix + 'rating-picker-track-over',
//-------------------------------------------------------------------------
// Config Appliers
applyGlyphs: function(value) {
if (typeof value === 'string') {
//
if (value.length !== 2) {
Ext.raise('Expected 2 characters for "glyphs" not "' + value + '".');
}
//
value = [
value.charAt(0),
value.charAt(1)
];
} else if (typeof value[0] === 'number') {
value = [
String.fromCharCode(value[0]),
String.fromCharCode(value[1])
];
}
return value;
},
applyOverStyle: function(style) {
this.trackerEl.applyStyles(style);
},
applySelectedStyle: function(style) {
this.valueEl.applyStyles(style);
},
applyTip: function(tip) {
if (tip && typeof tip !== 'function') {
if (!tip.isTemplate) {
tip = new Ext.XTemplate(tip);
}
tip = tip.apply.bind(tip);
}
return tip;
},
applyTrackingValue: function(value) {
return this.applyValue(value);
},
// same rounding as normal value
applyValue: function(v) {
if (v !== null) {
var rounding = this.getRounding(),
limit = this.getLimit(),
min = this.getMinimum();
v = Math.round(Math.round(v / rounding) * rounding * 1000) / 1000;
v = (v < min) ? min : (v > limit ? limit : v);
}
return v;
},
//-------------------------------------------------------------------------
// Event Handlers
onClick: function(event) {
var value = this.valueFromEvent(event);
this.setValue(value);
},
onMouseEnter: function() {
this.element.addCls(this.overCls);
},
onMouseLeave: function() {
this.element.removeCls(this.overCls);
},
onMouseMove: function(event) {
var value = this.valueFromEvent(event);
this.setTrackingValue(value);
},
//-------------------------------------------------------------------------
// Config Updaters
updateFamily: function(family) {
this.element.setStyle('fontFamily', "'" + family + "'");
},
updateGlyphs: function() {
this.refreshGlyphs();
},
updateLimit: function() {
this.refreshGlyphs();
},
updateScale: function(size) {
this.element.setStyle('fontSize', size);
},
updateTip: function() {
this.refreshTip();
},
updateTooltipText: function(text) {
this.setTooltip(text);
},
// modern only (replaced by classic override)
updateTrackingValue: function(value) {
var me = this,
trackerEl = me.trackerEl,
newWidth = me.valueToPercent(value);
trackerEl.setStyle('width', newWidth);
me.refreshTip();
},
updateTrackOver: function(trackOver) {
this.element.toggleCls(this.trackOverCls, trackOver);
},
updateValue: function(value, oldValue) {
var me = this,
animate = me.getAnimate(),
valueEl = me.valueEl,
newWidth = me.valueToPercent(value),
column, record;
if (me.isConfiguring || !animate) {
valueEl.setStyle('width', newWidth);
} else {
valueEl.stopAnimation();
valueEl.animate(Ext.merge({
from: {
width: me.valueToPercent(oldValue)
},
to: {
width: newWidth
}
}, animate));
}
me.refreshTip();
if (!me.isConfiguring) {
// Since we are (re)configured many times as we are used in a grid cell, we
// avoid firing the change event unless there are listeners.
if (me.hasListeners.change) {
me.fireEvent('change', me, value, oldValue);
}
column = me.getWidgetColumn && me.getWidgetColumn();
record = column && me.getWidgetRecord && me.getWidgetRecord();
if (record && column.dataIndex) {
// When used in a widgetcolumn, we should update the backing field. The
// linkages will be cleared as we are being recycled, so this will only
// reach this line when we are properly attached to a record and the
// change is coming from the user (or a call to setValue).
record.set(column.dataIndex, value);
}
}
},
//-------------------------------------------------------------------------
// Config System Optimizations
//
// These are to deal with configs that combine to determine what should be
// rendered in the DOM. For example, "glyphs" and "limit" must both be known
// to render the proper text nodes. The "tip" and "value" likewise are
// used to update the tooltipText.
//
// To avoid multiple updates to the DOM (one for each config), we simply mark
// the rendering as invalid and post-process these flags on the tail of any
// bulk updates.
afterCachedConfig: function() {
// Now that we are done setting up the initial values we need to refresh the
// DOM before we allow Ext.Widget's implementation to cloneNode on it.
this.refresh();
return this.callParent(arguments);
},
initConfig: function(instanceConfig) {
this.isConfiguring = true;
this.callParent([
instanceConfig
]);
// The firstInstance will already have refreshed the DOM (in afterCacheConfig)
// but all instances beyond the first need to refresh if they have custom values
// for one or more configs that affect the DOM (such as "glyphs" and "limit").
this.refresh();
},
setConfig: function() {
var me = this;
// Since we could be updating multiple configs, save any updates that need
// multiple values for afterwards.
me.isReconfiguring = true;
me.callParent(arguments);
me.isReconfiguring = false;
// Now that all new values are set, we can refresh the DOM.
me.refresh();
return me;
},
//-------------------------------------------------------------------------
privates: {
/**
* This method returns the DOM text node into which glyphs are placed.
* @param {HTMLElement} dom The DOM node parent of the text node.
* @return {HTMLElement} The text node.
* @private
*/
getGlyphTextNode: function(dom) {
var node = dom.lastChild;
// We want all our text nodes to be at the end of the child list, most
// especially the text node on the innerEl. That text node affects the
// default left/right position of our absolutely positioned child divs
// (trackerEl and valueEl).
if (!node || node.nodeType !== 3) {
node = dom.ownerDocument.createTextNode('');
dom.appendChild(node);
}
return node;
},
getTooltipData: function() {
var me = this;
return {
component: me,
tracking: me.getTrackingValue(),
trackOver: me.getTrackOver(),
value: me.getValue()
};
},
/**
* Forcibly refreshes both glyph and tooltip rendering.
* @private
*/
refresh: function() {
var me = this;
if (me.invalidGlyphs) {
me.refreshGlyphs(true);
}
if (me.invalidTip) {
me.refreshTip(true);
}
},
/**
* Refreshes the glyph text rendering unless we are currently performing a
* bulk config change (initConfig or setConfig).
* @param {Boolean} now Pass `true` to force the refresh to happen now.
* @private
*/
refreshGlyphs: function(now) {
var me = this,
later = !now && (me.isConfiguring || me.isReconfiguring),
el, glyphs, limit, on, off, trackerEl, valueEl;
if (!later) {
el = me.getGlyphTextNode(me.innerEl.dom);
valueEl = me.getGlyphTextNode(me.valueEl.dom);
trackerEl = me.getGlyphTextNode(me.trackerEl.dom);
glyphs = me.getGlyphs();
limit = me.getLimit();
for (on = off = ''; limit--; ) {
off += glyphs[0];
on += glyphs[1];
}
el.nodeValue = off;
valueEl.nodeValue = on;
trackerEl.nodeValue = on;
}
me.invalidGlyphs = later;
},
/**
* Refreshes the tooltip text rendering unless we are currently performing a
* bulk config change (initConfig or setConfig).
* @param {Boolean} now Pass `true` to force the refresh to happen now.
* @private
*/
refreshTip: function(now) {
var me = this,
later = !now && (me.isConfiguring || me.isReconfiguring),
data, text, tooltip;
if (!later) {
tooltip = me.getTip();
if (tooltip) {
data = me.getTooltipData();
text = tooltip(data);
me.setTooltipText(text);
}
}
me.invalidTip = later;
},
/**
* Convert the coordinates of the given `Event` into a rating value.
* @param {Ext.event.Event} event The event.
* @return {Number} The rating based on the given event coordinates.
* @private
*/
valueFromEvent: function(event) {
var me = this,
el = me.innerEl,
ex = event.getX(),
rounding = me.getRounding(),
cx = el.getX(),
x = ex - cx,
w = el.getWidth(),
limit = me.getLimit(),
v;
if (me.getInherited().rtl) {
x = w - x;
}
v = x / w * limit;
// We have to round up here so that the area we are over is considered
// the value.
v = Math.ceil(v / rounding) * rounding;
return v;
},
/**
* Convert the given rating into a width percentage.
* @param {Number} value The rating value to convert.
* @return {String} The width percentage to represent the given value.
* @private
*/
valueToPercent: function(value) {
value = (value / this.getLimit()) * 100;
return value + '%';
}
}
});
/**
* @class Ext.ux.rating.Picker
*/
Ext.define('Ext.ux.overrides.rating.Picker', {
override: 'Ext.ux.rating.Picker',
//
initConfig: function(config) {
if (config && config.tooltip) {
config.tip = config.tooltip;
Ext.log.warn('[Ext.ux.rating.Picker] The "tooltip" config was replaced by "tip"');
}
this.callParent([
config
]);
},
//
updateTooltipText: function(text) {
var innerEl = this.innerEl,
QuickTips = Ext.tip && Ext.tip.QuickTipManager,
tip = QuickTips && QuickTips.tip,
target;
if (QuickTips) {
innerEl.dom.setAttribute('data-qtip', text);
this.trackerEl.dom.setAttribute('data-qtip', text);
// If the QuickTipManager is active over our widget, we need to update
// the tooltip text directly.
target = tip && tip.activeTarget;
target = target && target.el;
if (target && innerEl.contains(target)) {
tip.update(text);
}
}
}
});
/**
* Base class from Ext.ux.TabReorderer.
*/
Ext.define('Ext.ux.BoxReorderer', {
extend: 'Ext.plugin.Abstract',
alias: 'plugin.boxreorderer',
requires: [
'Ext.dd.DD'
],
mixins: {
observable: 'Ext.util.Observable'
},
/**
* @cfg {String} itemSelector
* A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child
* Components which participate in reordering.
*/
itemSelector: '.x-box-item',
/**
* @cfg {Mixed} animate
* If truthy, child reordering is animated so that moved boxes slide smoothly into position.
* If this option is numeric, it is used as the animation duration in milliseconds.
*/
animate: 100,
/**
* @event StartDrag
* Fires when dragging of a child Component begins.
* @param {Ext.ux.BoxReorderer} this
* @param {Ext.container.Container} container The owning Container
* @param {Ext.Component} dragCmp The Component being dragged
* @param {Number} idx The start index of the Component being dragged.
*/
/**
* @event Drag
* Fires during dragging of a child Component.
* @param {Ext.ux.BoxReorderer} this
* @param {Ext.container.Container} container The owning Container
* @param {Ext.Component} dragCmp The Component being dragged
* @param {Number} startIdx The index position from which the Component was initially dragged.
* @param {Number} idx The current closest index to which the Component would drop.
*/
/**
* @event ChangeIndex
* Fires when dragging of a child Component causes its drop index to change.
* @param {Ext.ux.BoxReorderer} this
* @param {Ext.container.Container} container The owning Container
* @param {Ext.Component} dragCmp The Component being dragged
* @param {Number} startIdx The index position from which the Component was initially dragged.
* @param {Number} idx The current closest index to which the Component would drop.
*/
/**
* @event Drop
* Fires when a child Component is dropped at a new index position.
* @param {Ext.ux.BoxReorderer} this
* @param {Ext.container.Container} container The owning Container
* @param {Ext.Component} dragCmp The Component being dropped
* @param {Number} startIdx The index position from which the Component was initially dragged.
* @param {Number} idx The index at which the Component is being dropped.
*/
constructor: function() {
this.callParent(arguments);
this.mixins.observable.constructor.call(this);
},
init: function(container) {
var me = this,
layout = container.getLayout();
me.container = container;
// We must use LTR method names and properties.
// The underlying Element APIs normalize them.
me.names = layout._props[layout.type].names;
// Set our animatePolicy to animate the start position (ie x for HBox, y for VBox)
me.animatePolicy = {};
me.animatePolicy[me.names.x] = true;
// Initialize the DD on first layout, when the innerCt has been created.
me.container.on({
scope: me,
boxready: me.onBoxReady,
beforedestroy: me.onContainerDestroy
});
},
/**
* @private
* Clear up on Container destroy
*/
onContainerDestroy: function() {
var dd = this.dd;
if (dd) {
dd.unreg();
this.dd = null;
}
},
onBoxReady: function() {
var me = this,
layout = me.container.getLayout(),
names = me.names,
dd;
// Create a DD instance. Poke the handlers in.
// TODO: Ext5's DD classes should apply config to themselves.
// TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin
// TODO: Ext5's DD classes should be Observable.
// TODO: When all the above are trus, this plugin should extend the DD class.
dd = me.dd = new Ext.dd.DD(layout.innerCt, me.container.id + '-reorderer');
Ext.apply(dd, {
animate: me.animate,
reorderer: me,
container: me.container,
getDragCmp: me.getDragCmp,
clickValidator: Ext.Function.createInterceptor(dd.clickValidator, me.clickValidator, me, false),
onMouseDown: me.onMouseDown,
startDrag: me.startDrag,
onDrag: me.onDrag,
endDrag: me.endDrag,
getNewIndex: me.getNewIndex,
doSwap: me.doSwap,
findReorderable: me.findReorderable,
names: names
});
// Decide which dimension we are measuring, and which measurement metric defines
// the *start* of the box depending upon orientation.
dd.dim = names.width;
dd.startAttr = names.beforeX;
dd.endAttr = names.afterX;
},
getDragCmp: function(e) {
return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
},
// check if the clicked component is reorderable
clickValidator: function(e) {
var cmp = this.getDragCmp(e);
// If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false
return !!(cmp && cmp.reorderable !== false);
},
onMouseDown: function(e) {
var me = this,
container = me.container,
containerBox, cmpEl, cmpBox;
// Ascertain which child Component is being mousedowned
me.dragCmp = me.getDragCmp(e);
if (me.dragCmp) {
cmpEl = me.dragCmp.getEl();
me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);
// Start position of dragged Component
cmpBox = cmpEl.getBox();
// Last tracked start position
me.lastPos = cmpBox[me.startAttr];
// Calculate constraints depending upon orientation
// Calculate offset from mouse to dragEl position
containerBox = container.el.getBox();
if (me.dim === 'width') {
me.minX = containerBox.left;
me.maxX = containerBox.right - cmpBox.width;
me.minY = me.maxY = cmpBox.top;
me.deltaX = e.getX() - cmpBox.left;
} else {
me.minY = containerBox.top;
me.maxY = containerBox.bottom - cmpBox.height;
me.minX = me.maxX = cmpBox.left;
me.deltaY = e.getY() - cmpBox.top;
}
me.constrainY = me.constrainX = true;
}
},
startDrag: function() {
var me = this,
dragCmp = me.dragCmp;
if (dragCmp) {
// For the entire duration of dragging the *Element*, defeat any positioning and animation of the dragged *Component*
dragCmp.setPosition = Ext.emptyFn;
dragCmp.animate = false;
// Animate the BoxLayout just for the duration of the drag operation.
if (me.animate) {
me.container.getLayout().animatePolicy = me.reorderer.animatePolicy;
}
// We drag the Component element
me.dragElId = dragCmp.getEl().id;
me.reorderer.fireEvent('StartDrag', me, me.container, dragCmp, me.curIndex);
// Suspend events, and set the disabled flag so that the mousedown and mouseup events
// that are going to take place do not cause any other UI interaction.
dragCmp.suspendEvents();
dragCmp.disabled = true;
dragCmp.el.setStyle('zIndex', 100);
} else {
me.dragElId = null;
}
},
/**
* @private
* Find next or previous reorderable component index.
* @param {Number} newIndex The initial drop index.
* @return {Number} The index of the reorderable component.
*/
findReorderable: function(newIndex) {
var me = this,
items = me.container.items,
newItem;
if (items.getAt(newIndex).reorderable === false) {
newItem = items.getAt(newIndex);
if (newIndex > me.startIndex) {
while (newItem && newItem.reorderable === false) {
newIndex++;
newItem = items.getAt(newIndex);
}
} else {
while (newItem && newItem.reorderable === false) {
newIndex--;
newItem = items.getAt(newIndex);
}
}
}
newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
if (items.getAt(newIndex).reorderable === false) {
return -1;
}
return newIndex;
},
/**
* @private
* Swap 2 components.
* @param {Number} newIndex The initial drop index.
*/
doSwap: function(newIndex) {
var me = this,
items = me.container.items,
container = me.container,
orig, dest, tmpIndex;
newIndex = me.findReorderable(newIndex);
if (newIndex === -1 || newIndex === me.curIndex) {
return;
}
me.reorderer.fireEvent('ChangeIndex', me, container, me.dragCmp, me.startIndex, newIndex);
orig = items.getAt(me.curIndex);
dest = items.getAt(newIndex);
items.remove(orig);
tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
items.insert(tmpIndex, orig);
items.remove(dest);
items.insert(me.curIndex, dest);
// Make the Box Container the topmost layout participant during the layout.
container.updateLayout({
isRoot: true
});
me.curIndex = newIndex;
},
onDrag: function(e) {
var me = this,
newIndex;
newIndex = me.getNewIndex(e.getPoint());
if ((newIndex !== undefined)) {
me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
me.doSwap(newIndex);
}
},
endDrag: function(e) {
if (e) {
e.stopEvent();
}
var me = this,
layout = me.container.getLayout(),
temp;
if (me.dragCmp) {
delete me.dragElId;
// Reinstate the Component's positioning method after mouseup, and allow the layout system to animate it.
delete me.dragCmp.setPosition;
me.dragCmp.animate = true;
// Ensure the lastBox is correct for the animation system to restore to when it creates the "from" animation frame
me.dragCmp.lastBox[me.names.x] = me.dragCmp.getPosition(true)[me.names.widthIndex];
// Make the Box Container the topmost layout participant during the layout.
me.container.updateLayout({
isRoot: true
});
// Attempt to hook into the afteranimate event of the drag Component to call the cleanup
temp = Ext.fx.Manager.getFxQueue(me.dragCmp.el.id)[0];
if (temp) {
temp.on({
afteranimate: me.reorderer.afterBoxReflow,
scope: me
});
} else // If not animated, clean up after the mouseup has happened so that we don't click the thing being dragged
{
Ext.asap(me.reorderer.afterBoxReflow, me);
}
if (me.animate) {
delete layout.animatePolicy;
}
me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
}
},
/**
* @private
* Called after the boxes have been reflowed after the drop.
* Re-enabled the dragged Component.
*/
afterBoxReflow: function() {
var me = this;
me.dragCmp.el.setStyle('zIndex', '');
me.dragCmp.disabled = false;
me.dragCmp.resumeEvents();
},
/**
* @private
* Calculate drop index based upon the dragEl's position.
*/
getNewIndex: function(pointerPos) {
var me = this,
dragEl = me.getDragEl(),
dragBox = Ext.fly(dragEl).getBox(),
targetEl, targetBox, targetMidpoint,
i = 0,
it = me.container.items.items,
ln = it.length,
lastPos = me.lastPos;
me.lastPos = dragBox[me.startAttr];
for (; i < ln; i++) {
targetEl = it[i].getEl();
// Only look for a drop point if this found item is an item according to our selector
// and is not the item being dragged
if (targetEl.dom !== dragEl && targetEl.is(me.reorderer.itemSelector)) {
targetBox = targetEl.getBox();
targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1);
if (i < me.curIndex) {
if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) {
return i;
}
} else if (i > me.curIndex) {
if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {
return i;
}
}
}
}
}
});
/**
* This plugin can enable a cell to cell drag and drop operation within the same grid view.
*
* Note that the plugin must be added to the grid view, not to the grid panel. For example,
* using {@link Ext.panel.Table viewConfig}:
*
* viewConfig: {
* plugins: {
* celldragdrop: {
* // Remove text from source cell and replace with value of emptyText.
* applyEmptyText: true,
*
* //emptyText: Ext.String.htmlEncode('<>'),
*
* // Will only allow drops of the same type.
* enforceType: true
* }
* }
* }
*/
Ext.define('Ext.ux.CellDragDrop', {
extend: 'Ext.plugin.Abstract',
alias: 'plugin.celldragdrop',
uses: [
'Ext.view.DragZone'
],
/**
* @cfg {Boolean} enforceType
* Set to `true` to only allow drops of the same type.
*
* Defaults to `false`.
*/
enforceType: false,
/**
* @cfg {Boolean} applyEmptyText
* If `true`, then use the value of {@link #emptyText} to replace the drag record's value after a node drop.
* Note that, if dropped on a cell of a different type, it will convert the default text according to its own conversion rules.
*
* Defaults to `false`.
*/
applyEmptyText: false,
/**
* @cfg {String} emptyText
* If {@link #applyEmptyText} is `true`, then this value as the drag record's value after a node drop.
*
* Defaults to an empty string.
*/
emptyText: '',
/**
* @cfg {String} dropBackgroundColor
* The default background color for when a drop is allowed.
*
* Defaults to green.
*/
dropBackgroundColor: 'green',
/**
* @cfg {String} noDropBackgroundColor
* The default background color for when a drop is not allowed.
*
* Defaults to red.
*/
noDropBackgroundColor: 'red',
/**
* @cfg {String} dragText
* The text to show while dragging.
*
* Two placeholders can be used in the text:
*
* - `{0}` The number of selected items.
* - `{1}` 's' when more than 1 items (only useful for English).
* @locale
*/
dragText: '{0} selected row{1}',
/**
* @cfg {String} ddGroup
* A named drag drop group to which this object belongs. If a group is specified, then both the DragZones and
* DropZone used by this plugin will only interact with other drag drop objects in the same group.
*/
ddGroup: "GridDD",
/**
* @cfg {Boolean} enableDrop
* Set to `false` to disallow the View from accepting drop gestures.
*/
enableDrop: true,
/**
* @cfg {Boolean} enableDrag
* Set to `false` to disallow dragging items from the View.
*/
enableDrag: true,
/**
* @cfg {Object/Boolean} containerScroll
* True to register this container with the Scrollmanager for auto scrolling during drag operations.
* A {@link Ext.dd.ScrollManager} configuration may also be passed.
*/
containerScroll: false,
init: function(view) {
var me = this;
view.on('render', me.onViewRender, me, {
single: true
});
},
destroy: function() {
var me = this;
me.dragZone = me.dropZone = Ext.destroy(me.dragZone, me.dropZone);
me.callParent();
},
enable: function() {
var me = this;
if (me.dragZone) {
me.dragZone.unlock();
}
if (me.dropZone) {
me.dropZone.unlock();
}
me.callParent();
},
disable: function() {
var me = this;
if (me.dragZone) {
me.dragZone.lock();
}
if (me.dropZone) {
me.dropZone.lock();
}
me.callParent();
},
onViewRender: function(view) {
var me = this,
scrollEl;
if (me.enableDrag) {
if (me.containerScroll) {
scrollEl = view.getEl();
}
me.dragZone = new Ext.view.DragZone({
view: view,
ddGroup: me.dragGroup || me.ddGroup,
dragText: me.dragText,
containerScroll: me.containerScroll,
scrollEl: scrollEl,
getDragData: function(e) {
var view = this.view,
item = e.getTarget(view.getItemSelector()),
record = view.getRecord(item),
cell = e.getTarget(view.getCellSelector()),
dragEl, header;
if (item) {
dragEl = document.createElement('div');
dragEl.className = 'x-form-text';
dragEl.appendChild(document.createTextNode(cell.textContent || cell.innerText));
header = view.getHeaderByCell(cell);
return {
event: new Ext.EventObjectImpl(e),
ddel: dragEl,
item: e.target,
columnName: header.dataIndex,
record: record
};
}
},
onInitDrag: function(x, y) {
var self = this,
data = self.dragData,
view = self.view,
selectionModel = view.getSelectionModel(),
record = data.record,
el = data.ddel;
// Update the selection to match what would have been selected if the user had
// done a full click on the target node rather than starting a drag from it.
if (!selectionModel.isSelected(record)) {
selectionModel.select(record, true);
}
Ext.fly(self.ddel).update(el.textContent || el.innerText);
self.proxy.update(self.ddel);
self.onStartDrag(x, y);
return true;
}
});
}
if (me.enableDrop) {
me.dropZone = new Ext.dd.DropZone(view.el, {
view: view,
ddGroup: me.dropGroup || me.ddGroup,
containerScroll: true,
getTargetFromEvent: function(e) {
var self = this,
view = self.view,
cell = e.getTarget(view.cellSelector),
row, header;
// Ascertain whether the mousemove is within a grid cell.
if (cell) {
row = view.findItemByChild(cell);
header = view.getHeaderByCell(cell);
if (row && header) {
return {
node: cell,
record: view.getRecord(row),
columnName: header.dataIndex
};
}
}
},
// On Node enter, see if it is valid for us to drop the field on that type of column.
onNodeEnter: function(target, dd, e, dragData) {
var self = this,
destType = target.record.getField(target.columnName).type.toUpperCase(),
sourceType = dragData.record.getField(dragData.columnName).type.toUpperCase();
delete self.dropOK;
// Return if no target node or if over the same cell as the source of the drag.
if (!target || target.node === dragData.item.parentNode) {
return;
}
// Check whether the data type of the column being dropped on accepts the
// dragged field type. If so, set dropOK flag, and highlight the target node.
if (me.enforceType && destType !== sourceType) {
self.dropOK = false;
if (me.noDropCls) {
Ext.fly(target.node).addCls(me.noDropCls);
} else {
Ext.fly(target.node).applyStyles({
backgroundColor: me.noDropBackgroundColor
});
}
return false;
}
self.dropOK = true;
if (me.dropCls) {
Ext.fly(target.node).addCls(me.dropCls);
} else {
Ext.fly(target.node).applyStyles({
backgroundColor: me.dropBackgroundColor
});
}
},
// Return the class name to add to the drag proxy. This provides a visual indication
// of drop allowed or not allowed.
onNodeOver: function(target, dd, e, dragData) {
return this.dropOK ? this.dropAllowed : this.dropNotAllowed;
},
// Highlight the target node.
onNodeOut: function(target, dd, e, dragData) {
var cls = this.dropOK ? me.dropCls : me.noDropCls;
if (cls) {
Ext.fly(target.node).removeCls(cls);
} else {
Ext.fly(target.node).applyStyles({
backgroundColor: ''
});
}
},
// Process the drop event if we have previously ascertained that a drop is OK.
onNodeDrop: function(target, dd, e, dragData) {
if (this.dropOK) {
target.record.set(target.columnName, dragData.record.get(dragData.columnName));
if (me.applyEmptyText) {
dragData.record.set(dragData.columnName, me.emptyText);
}
return true;
}
},
onCellDrop: Ext.emptyFn
});
}
}
});
/**
* @class Ext.ux.DataTip
* @extends Ext.tip.ToolTip
* This plugin implements automatic tooltip generation for an arbitrary number of child nodes *within* a Component.
*
* This plugin is applied to a high level Component, which contains repeating elements, and depending on the host Component type,
* it automatically selects a {@link Ext.ToolTip#delegate delegate} so that it appears when the mouse enters a sub-element.
*
* When applied to a GridPanel, this ToolTip appears when over a row, and the Record's data is applied
* using this object's {@link #tpl} template.
*
* When applied to a DataView, this ToolTip appears when over a view node, and the Record's data is applied
* using this object's {@link #tpl} template.
*
* When applied to a TreePanel, this ToolTip appears when over a tree node, and the Node's {@link Ext.data.Model} record data is applied
* using this object's {@link #tpl} template.
*
* When applied to a FormPanel, this ToolTip appears when over a Field, and the Field's `tooltip` property is used is applied
* using this object's {@link #tpl} template, or if it is a string, used as HTML content. If there is no `tooltip` property,
* the field itself is used as the template's data object.
*
* If more complex logic is needed to determine content, then the {@link #beforeshow} event may be used.
* This class also publishes a **`beforeshowtip`** event through its host Component. The *host Component* fires the
* **`beforeshowtip`** event.
*/
Ext.define('Ext.ux.DataTip', function(DataTip) {
// Target the body (if the host is a Panel), or, if there is no body, the main Element.
function onHostRender() {
var e = this.isXType('panel') ? this.body : this.el;
if (this.dataTip.renderToTarget) {
this.dataTip.render(e);
}
this.dataTip.setTarget(e);
}
function updateTip(tip, data) {
if (tip.rendered) {
if (tip.host.fireEvent('beforeshowtip', tip.eventHost, tip, data) === false) {
return false;
}
tip.update(data);
} else {
if (Ext.isString(data)) {
tip.html = data;
} else {
tip.data = data;
}
}
}
function beforeViewTipShow(tip) {
var rec = this.view.getRecord(tip.triggerElement),
data;
if (rec) {
data = tip.initialConfig.data ? Ext.apply(tip.initialConfig.data, rec.data) : rec.data;
return updateTip(tip, data);
} else {
return false;
}
}
function beforeFormTipShow(tip) {
var field = Ext.getCmp(tip.triggerElement.id);
if (field && (field.tooltip || tip.tpl)) {
return updateTip(tip, field.tooltip || field);
} else {
return false;
}
}
return {
extend: 'Ext.tip.ToolTip',
mixins: {
plugin: 'Ext.plugin.Abstract'
},
alias: 'plugin.datatip',
lockableScope: 'both',
constructor: function(config) {
var me = this;
me.callParent([
config
]);
me.mixins.plugin.constructor.call(me, config);
},
init: function(host) {
var me = this;
me.mixins.plugin.init.call(me, host);
host.dataTip = me;
me.host = host;
if (host.isXType('tablepanel')) {
me.view = host.getView();
if (host.ownerLockable) {
me.host = host.ownerLockable;
}
me.delegate = me.delegate || me.view.rowSelector;
me.on('beforeshow', beforeViewTipShow);
} else if (host.isXType('dataview')) {
me.view = me.host;
me.delegate = me.delegate || host.itemSelector;
me.on('beforeshow', beforeViewTipShow);
} else if (host.isXType('form')) {
me.delegate = '.' + Ext.form.Labelable.prototype.formItemCls;
me.on('beforeshow', beforeFormTipShow);
} else if (host.isXType('combobox')) {
me.view = host.getPicker();
me.delegate = me.delegate || me.view.getItemSelector();
me.on('beforeshow', beforeViewTipShow);
}
if (host.rendered) {
onHostRender.call(host);
} else {
host.onRender = Ext.Function.createSequence(host.onRender, onHostRender);
}
}
};
});
/**
* Transition plugin for DataViews
*/
Ext.define('Ext.ux.DataView.Animated', {
alias: 'plugin.ux-animated-dataview',
/**
* @property defaults
* @type Object
* Default configuration options for all DataViewTransition instances
*/
defaults: {
duration: 750,
idProperty: 'id'
},
/**
* Creates the plugin instance, applies defaults
* @constructor
* @param {Object} config Optional config object
*/
constructor: function(config) {
Ext.apply(this, config || {}, this.defaults);
},
/**
* Initializes the transition plugin. Overrides the dataview's default refresh function
* @param {Ext.view.View} dataview The dataview
*/
init: function(dataview) {
var me = this,
store = dataview.store,
items = dataview.all,
task = {
interval: 20
},
duration = me.duration;
/**
* @property dataview
* @type Ext.view.View
* Reference to the DataView this instance is bound to
*/
me.dataview = dataview;
dataview.blockRefresh = true;
dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() {
this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) {
element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, store.getAt(index).internalId);
}, this);
}, dataview);
/**
* @property dataviewID
* @type String
* The string ID of the DataView component. This is used internally when animating child objects
*/
me.dataviewID = dataview.id;
/**
* @property cachedStoreData
* @type Object
* A cache of existing store data, keyed by id. This is used to determine
* whether any items were added or removed from the store on data change
*/
me.cachedStoreData = {};
//catch the store data with the snapshot immediately
me.cacheStoreData(store.data || store.snapshot);
dataview.on('resize', function() {
var store = dataview.store;
if (store.getCount() > 0) {}
}, // reDraw.call(this, store);
this);
// Buffer listenher so that rapid calls, for example a filter followed by a sort
// Only produce one redraw.
dataview.store.on({
datachanged: reDraw,
scope: this,
buffer: 50
});
function reDraw() {
var parentEl = dataview.getTargetEl(),
parentElY = parentEl.getY(),
parentElPaddingTop = parentEl.getPadding('t'),
added = me.getAdded(store),
removed = me.getRemoved(store),
remaining = me.getRemaining(store),
itemArray, i, id,
itemFly = new Ext.dom.Fly(),
rtl = me.dataview.getInherited().rtl,
oldPos, newPos,
styleSide = rtl ? 'right' : 'left',
newStyle = {};
// Not yet rendered
if (!parentEl) {
return;
}
// Collect nodes that will be removed in the forthcoming refresh so
// that we can put them back in order to fade them out
Ext.iterate(removed, function(recId, item) {
id = me.dataviewID + '-' + recId;
// Stop any animations for removed items and ensure th.
Ext.fx.Manager.stopAnimation(id);
item.dom = Ext.getDom(id);
if (!item.dom) {
delete removed[recId];
}
});
me.cacheStoreData(store);
// stores the current top and left values for each element (discovered below)
var oldPositions = {},
newPositions = {};
// Find current positions of elements which are to remain after the refresh.
Ext.iterate(remaining, function(id, item) {
if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) {
oldPos = oldPositions[id] = {
top: itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop
};
oldPos[styleSide] = me.getItemX(itemFly);
} else {
delete remaining[id];
}
});
// The view MUST refresh, creating items in the natural flow, and collecting the items
// so that its item collection is consistent.
dataview.refresh();
// Replace removed nodes so that they can be faded out, THEN removed
Ext.iterate(removed, function(id, item) {
parentEl.dom.appendChild(item.dom);
itemFly.attach(item.dom).animate({
duration: duration,
opacity: 0,
callback: function(anim) {
var el = Ext.get(anim.target.id);
if (el) {
el.destroy();
}
}
});
delete item.dom;
});
// We have taken care of any removals.
// If the store is empty, we are done.
if (!store.getCount()) {
return;
}
// Collect the correct new positions after the refresh
itemArray = items.slice();
// Reverse order so that moving to absolute position does not affect the position of
// the next one we're looking at.
for (i = itemArray.length - 1; i >= 0; i--) {
id = store.getAt(i).internalId;
itemFly.attach(itemArray[i]);
newPositions[id] = {
dom: itemFly.dom,
top: itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop
};
newPositions[id][styleSide] = me.getItemX(itemFly);
// We're going to absolutely position each item.
// If it is a "remaining" one from last refesh, shunt it back to
// its old position from where it will be animated.
newPos = oldPositions[id] || newPositions[id];
// set absolute positioning on all DataView items. We need to set position, left and
// top at the same time to avoid any flickering
newStyle.position = 'absolute';
newStyle.top = newPos.top + "px";
newStyle[styleSide] = newPos.left + "px";
itemFly.applyStyles(newStyle);
}
// This is the function which moves remaining items to their new position
var doAnimate = function() {
var elapsed = new Date() - task.taskStartTime,
fraction = elapsed / duration;
if (fraction >= 1) {
// At end, return all items to natural flow.
newStyle.position = newStyle.top = newStyle[styleSide] = '';
for (id in newPositions) {
itemFly.attach(newPositions[id].dom).applyStyles(newStyle);
}
Ext.TaskManager.stop(task);
} else {
// In frame, move each "remaining" item according to time elapsed
for (id in remaining) {
var oldPos = oldPositions[id],
newPos = newPositions[id],
oldTop = oldPos.top,
newTop = newPos.top,
oldLeft = oldPos[styleSide],
newLeft = newPos[styleSide],
diffTop = fraction * Math.abs(oldTop - newTop),
diffLeft = fraction * Math.abs(oldLeft - newLeft),
midTop = oldTop > newTop ? oldTop - diffTop : oldTop + diffTop,
midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft;
newStyle.top = midTop + "px";
newStyle[styleSide] = midLeft + "px";
itemFly.attach(newPos.dom).applyStyles(newStyle);
}
}
};
// Fade in new items
Ext.iterate(added, function(id, item) {
if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) {
itemFly.setOpacity(0);
itemFly.animate({
duration: duration,
opacity: 1
});
}
});
// Stop any previous animations
Ext.TaskManager.stop(task);
task.run = doAnimate;
Ext.TaskManager.start(task);
me.cacheStoreData(store);
}
},
getItemX: function(el) {
var rtl = this.dataview.getInherited().rtl,
parentEl = el.up('');
if (rtl) {
return parentEl.getViewRegion().right - el.getRegion().right + el.getMargin('r');
} else {
return el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l');
}
},
/**
* Caches the records from a store locally for comparison later
* @param {Ext.data.Store} store The store to cache data from
*/
cacheStoreData: function(store) {
var cachedStoreData = this.cachedStoreData = {};
store.each(function(record) {
cachedStoreData[record.internalId] = record;
});
},
/**
* Returns all records that were already in the DataView
* @return {Object} All existing records
*/
getExisting: function() {
return this.cachedStoreData;
},
/**
* Returns the total number of items that are currently visible in the DataView
* @return {Number} The number of existing items
*/
getExistingCount: function() {
var count = 0,
items = this.getExisting();
for (var k in items) {
count++;
}
return count;
},
/**
* Returns all records in the given store that were not already present
* @param {Ext.data.Store} store The updated store instance
* @return {Object} Object of records not already present in the dataview in format {id: record}
*/
getAdded: function(store) {
var cachedStoreData = this.cachedStoreData,
added = {};
store.each(function(record) {
if (cachedStoreData[record.internalId] == null) {
added[record.internalId] = record;
}
});
return added;
},
/**
* Returns all records that are present in the DataView but not the new store
* @param {Ext.data.Store} store The updated store instance
* @return {Array} Array of records that used to be present
*/
getRemoved: function(store) {
var cachedStoreData = this.cachedStoreData,
removed = {},
id;
for (id in cachedStoreData) {
if (store.findBy(function(record) {
return record.internalId == id;
}) == -1) {
removed[id] = cachedStoreData[id];
}
}
return removed;
},
/**
* Returns all records that are already present and are still present in the new store
* @param {Ext.data.Store} store The updated store instance
* @return {Object} Object of records that are still present from last time in format {id: record}
*/
getRemaining: function(store) {
var cachedStoreData = this.cachedStoreData,
remaining = {};
store.each(function(record) {
if (cachedStoreData[record.internalId] != null) {
remaining[record.internalId] = record;
}
});
return remaining;
}
});
/**
*
*/
Ext.define('Ext.ux.DataView.DragSelector', {
requires: [
'Ext.dd.DragTracker',
'Ext.util.Region'
],
alias: 'plugin.dataviewdragselector',
/**
* Initializes the plugin by setting up the drag tracker
*/
init: function(dataview) {
var scroller = dataview.getScrollable();
// If the client dataview is scrollable, and this is a PointerEvents device
// we cannot intercept the pointer to inplement dragselect.
if (scroller && (scroller.getX() || scroller.getY()) && (Ext.supports.PointerEvents || Ext.supports.MSPointerEvents)) {
//
Ext.log.warn('DragSelector not available on PointerEvent devices');
//
return;
}
/**
* @property dataview
* @type Ext.view.View
* The DataView bound to this instance
*/
this.dataview = dataview;
dataview.mon(dataview, {
beforecontainerclick: this.cancelClick,
scope: this,
render: {
fn: this.onRender,
scope: this,
single: true
}
});
},
/**
* @private
* Called when the attached DataView is rendered. This sets up the DragTracker instance that will be used
* to created a dragged selection area
*/
onRender: function() {
/**
* @property tracker
* @type Ext.dd.DragTracker
* The DragTracker attached to this instance. Note that the 4 on* functions are called in the scope of the
* DragTracker ('this' refers to the DragTracker inside those functions), so we pass a reference to the
* DragSelector so that we can call this class's functions.
*/
this.tracker = Ext.create('Ext.dd.DragTracker', {
dataview: this.dataview,
el: this.dataview.el,
onBeforeStart: this.onBeforeStart,
onStart: this.onStart.bind(this),
onDrag: this.onDrag.bind(this),
onEnd: Ext.Function.createDelayed(this.onEnd, 100, this)
});
/**
* @property dragRegion
* @type Ext.util.Region
* Represents the region currently dragged out by the user. This is used to figure out which dataview nodes are
* in the selected area and to set the size of the Proxy element used to highlight the current drag area
*/
this.dragRegion = Ext.create('Ext.util.Region');
},
/**
* @private
* Listener attached to the DragTracker's onBeforeStart event. Returns false if the drag didn't start within the
* DataView's el
*/
onBeforeStart: function(e) {
return e.target === this.dataview.getEl().dom;
},
/**
* @private
* Listener attached to the DragTracker's onStart event. Cancel's the DataView's containerclick event from firing
* and sets the start co-ordinates of the Proxy element. Clears any existing DataView selection
* @param {Ext.event.Event} e The click event
*/
onStart: function(e) {
var dataview = this.dataview;
// Flag which controls whether the cancelClick method vetoes the processing of the DataView's containerclick event.
// On IE (where else), this needs to remain set for a millisecond after mouseup because even though the mouse has
// moved, the mouseup will still trigger a click event.
this.dragging = true;
//here we reset and show the selection proxy element and cache the regions each item in the dataview take up
this.fillRegions();
this.getProxy().show();
dataview.getSelectionModel().deselectAll();
},
/**
* @private
* Reusable handler that's used to cancel the container click event when dragging on the dataview. See onStart for
* details
*/
cancelClick: function() {
return !this.dragging;
},
/**
* @private
* Listener attached to the DragTracker's onDrag event. Figures out how large the drag selection area should be and
* updates the proxy element's size to match. Then iterates over all of the rendered items and marks them selected
* if the drag region touches them
* @param {Ext.event.Event} e The drag event
*/
onDrag: function(e) {
var selModel = this.dataview.getSelectionModel(),
dragRegion = this.dragRegion,
bodyRegion = this.bodyRegion,
proxy = this.getProxy(),
regions = this.regions,
length = regions.length,
startXY = this.tracker.startXY,
currentXY = this.tracker.getXY(),
minX = Math.min(startXY[0], currentXY[0]),
minY = Math.min(startXY[1], currentXY[1]),
width = Math.abs(startXY[0] - currentXY[0]),
height = Math.abs(startXY[1] - currentXY[1]),
region, selected, i;
Ext.apply(dragRegion, {
top: minY,
left: minX,
right: minX + width,
bottom: minY + height
});
dragRegion.constrainTo(bodyRegion);
proxy.setBox(dragRegion);
for (i = 0; i < length; i++) {
region = regions[i];
selected = dragRegion.intersect(region);
if (selected) {
selModel.select(i, true);
} else {
selModel.deselect(i);
}
}
},
/**
* @method
* @private
* Listener attached to the DragTracker's onEnd event. This is a delayed function which executes 1
* millisecond after it has been called. This is because the dragging flag must remain active to cancel
* the containerclick event which the mouseup event will trigger.
* @param {Ext.event.Event} e The event object
*/
onEnd: function(e) {
var dataview = this.dataview,
selModel = dataview.getSelectionModel();
this.dragging = false;
this.getProxy().hide();
},
/**
* @private
* Creates a Proxy element that will be used to highlight the drag selection region
* @return {Ext.Element} The Proxy element
*/
getProxy: function() {
if (!this.proxy) {
this.proxy = this.dataview.getEl().createChild({
tag: 'div',
cls: 'x-view-selector'
});
}
return this.proxy;
},
/**
* @private
* Gets the region taken up by each rendered node in the DataView. We use these regions to figure out which nodes
* to select based on the selector region the user has dragged out
*/
fillRegions: function() {
var dataview = this.dataview,
regions = this.regions = [];
dataview.all.each(function(node) {
regions.push(node.getRegion());
});
this.bodyRegion = dataview.getEl().getRegion();
}
});
/**
* ## Basic DataView with Draggable mixin.
*
* Ext.Loader.setPath('Ext.ux', '../../../SDK/extjs/examples/ux');
*
* Ext.define('My.cool.View', {
* extend: 'Ext.view.View',
*
* mixins: {
* draggable: 'Ext.ux.DataView.Draggable'
* },
*
* initComponent: function() {
* this.mixins.draggable.init(this, {
* ddConfig: {
* ddGroup: 'someGroup'
* }
* });
*
* this.callParent(arguments);
* }
* });
*
* Ext.onReady(function () {
* Ext.create('Ext.data.Store', {
* storeId: 'baseball',
* fields: ['team', 'established'],
* data: [
* { team: 'Atlanta Braves', established: '1871' },
* { team: 'Miami Marlins', established: '1993' },
* { team: 'New York Mets', established: '1962' },
* { team: 'Philadelphia Phillies', established: '1883' },
* { team: 'Washington Nationals', established: '1969' }
* ]
* });
*
* Ext.create('My.cool.View', {
* store: Ext.StoreMgr.get('baseball'),
* tpl: [
* '',
* '
',
* 'The {team} were founded in {established}.',
* '
',
* ''
* ],
* itemSelector: 'p.team',
* renderTo: Ext.getBody()
* });
* });
*/
Ext.define('Ext.ux.DataView.Draggable', {
requires: 'Ext.dd.DragZone',
/**
* @cfg {String} ghostCls The CSS class added to the outermost element of the created ghost proxy
* (defaults to 'x-dataview-draggable-ghost')
*/
ghostCls: 'x-dataview-draggable-ghost',
/**
* @cfg {Ext.XTemplate/Array} ghostTpl The template used in the ghost DataView
*/
ghostTpl: [
'',
'{title}',
''
],
/**
* @cfg {Object} ddConfig Config object that is applied to the internally created DragZone
*/
/**
* @cfg {String} ghostConfig Config object that is used to configure the internally created DataView
*/
init: function(dataview, config) {
/**
* @property dataview
* @type Ext.view.View
* The Ext.view.View instance that this DragZone is attached to
*/
this.dataview = dataview;
dataview.on('render', this.onRender, this);
Ext.apply(this, {
itemSelector: dataview.itemSelector,
ghostConfig: {}
}, config || {});
Ext.applyIf(this.ghostConfig, {
itemSelector: 'img',
cls: this.ghostCls,
tpl: this.ghostTpl
});
},
/**
* @private
* Called when the attached DataView is rendered. Sets up the internal DragZone
*/
onRender: function() {
var me = this,
config = Ext.apply({}, me.ddConfig || {}, {
dvDraggable: me,
dataview: me.dataview,
getDragData: me.getDragData,
getTreeNode: me.getTreeNode,
afterRepair: me.afterRepair,
getRepairXY: me.getRepairXY
});
/**
* @property dragZone
* @type Ext.dd.DragZone
* The attached DragZone instane
*/
me.dragZone = Ext.create('Ext.dd.DragZone', me.dataview.getEl(), config);
// This is for https://www.w3.org/TR/pointerevents/ platforms.
// On these platforms, the pointerdown event (single touchstart) is reserved for
// initiating a scroll gesture. Setting the items draggable defeats that and
// enables the touchstart event to trigger a drag.
//
// Two finger dragging will still scroll on these platforms.
me.dataview.setItemsDraggable(true);
},
getDragData: function(e) {
var draggable = this.dvDraggable,
dataview = this.dataview,
selModel = dataview.getSelectionModel(),
target = e.getTarget(draggable.itemSelector),
selected, dragData;
if (target) {
// preventDefault is needed here to avoid the browser dragging the image
// instead of dragging the container like it's supposed to
e.preventDefault();
if (!dataview.isSelected(target)) {
selModel.select(dataview.getRecord(target));
}
selected = dataview.getSelectedNodes();
dragData = {
copy: true,
nodes: selected,
records: selModel.getSelection(),
item: true
};
if (selected.length === 1) {
dragData.single = true;
dragData.ddel = target;
} else {
dragData.multi = true;
dragData.ddel = draggable.prepareGhost(selModel.getSelection());
}
return dragData;
}
return false;
},
getTreeNode: function() {},
// console.log('test');
afterRepair: function() {
this.dragging = false;
var nodes = this.dragData.nodes,
length = nodes.length,
i;
//FIXME: Ext.fly does not work here for some reason, only frames the last node
for (i = 0; i < length; i++) {
Ext.get(nodes[i]).frame('#8db2e3', 1);
}
},
/**
* @private
* Returns the x and y co-ordinates that the dragged item should be animated back to if it was dropped on an
* invalid drop target. If we're dragging more than one item we don't animate back and just allow afterRepair
* to frame each dropped item.
*/
getRepairXY: function(e) {
if (this.dragData.multi) {
return false;
} else {
var repairEl = Ext.get(this.dragData.ddel),
repairXY = repairEl.getXY();
//take the item's margins and padding into account to make the repair animation line up perfectly
repairXY[0] += repairEl.getPadding('t') + repairEl.getMargin('t');
repairXY[1] += repairEl.getPadding('l') + repairEl.getMargin('l');
return repairXY;
}
},
/**
* Updates the internal ghost DataView by ensuring it is rendered and contains the correct records
* @param {Array} records The set of records that is currently selected in the parent DataView
* @return {HTMLElement} The Ghost DataView's encapsulating HTMLElement.
*/
prepareGhost: function(records) {
return this.createGhost(records).getEl().dom;
},
/**
* @private
* Creates the 'ghost' DataView that follows the mouse cursor during the drag operation. This div is usually a
* lighter-weight representation of just the nodes that are selected in the parent DataView.
*/
createGhost: function(records) {
var me = this,
store;
if (me.ghost) {
(store = me.ghost.store).loadRecords(records);
} else {
store = Ext.create('Ext.data.Store', {
model: records[0].self
});
store.loadRecords(records);
me.ghost = Ext.create('Ext.view.View', Ext.apply({
renderTo: document.createElement('div'),
store: store
}, me.ghostConfig));
me.ghost.container.skipGarbageCollection = me.ghost.el.skipGarbageCollection = true;
}
store.clearData();
return me.ghost;
},
destroy: function() {
var ghost = this.ghost;
if (ghost) {
ghost.container.destroy();
ghost.destroy();
}
this.callParent();
}
});
/**
*
*/
Ext.define('Ext.ux.DataView.LabelEditor', {
extend: 'Ext.Editor',
alias: 'plugin.dataviewlabeleditor',
alignment: 'tl-tl',
completeOnEnter: true,
cancelOnEsc: true,
shim: false,
autoSize: {
width: 'boundEl',
height: 'field'
},
labelSelector: 'x-editable',
requires: [
'Ext.form.field.Text'
],
constructor: function(config) {
config.field = config.field || Ext.create('Ext.form.field.Text', {
allowOnlyWhitespace: false,
selectOnFocus: true
});
this.callParent([
config
]);
},
init: function(view) {
this.view = view;
this.mon(view, 'afterrender', this.bindEvents, this);
this.on('complete', this.onSave, this);
},
// initialize events
bindEvents: function() {
this.mon(this.view.getEl(), {
click: {
fn: this.onClick,
scope: this
}
});
},
// on mousedown show editor
onClick: function(e, target) {
var me = this,
item, record;
if (Ext.fly(target).hasCls(me.labelSelector) && !me.editing && !e.ctrlKey && !e.shiftKey) {
e.stopEvent();
item = me.view.findItemByChild(target);
record = me.view.store.getAt(me.view.indexOf(item));
me.startEdit(target, record.data[me.dataIndex]);
me.activeRecord = record;
} else if (me.editing) {
me.field.blur();
e.preventDefault();
}
},
// update record
onSave: function(ed, value) {
this.activeRecord.set(this.dataIndex, value);
}
});
/**
* @class Ext.ux.DataViewTransition
* Transition plugin for DataViews
*/
Ext.ux.DataViewTransition = Ext.extend(Object, {
/**
* @property defaults
* @type Object
* Default configuration options for all DataViewTransition instances
*/
defaults: {
duration: 750,
idProperty: 'id'
},
/**
* Creates the plugin instance, applies defaults
* @constructor
* @param {Object} config Optional config object
*/
constructor: function(config) {
Ext.apply(this, config || {}, this.defaults);
},
/**
* Initializes the transition plugin. Overrides the dataview's default refresh function
* @param {Ext.view.View} dataview The dataview
*/
init: function(dataview) {
/**
* @property dataview
* @type Ext.view.View
* Reference to the DataView this instance is bound to
*/
this.dataview = dataview;
var idProperty = this.idProperty;
dataview.blockRefresh = true;
dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() {
this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) {
element.id = element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, dataview.store.getAt(index).get(idProperty));
}, this);
}, dataview);
/**
* @property dataviewID
* @type String
* The string ID of the DataView component. This is used internally when animating child objects
*/
this.dataviewID = dataview.id;
/**
* @property cachedStoreData
* @type Object
* A cache of existing store data, keyed by id. This is used to determine
* whether any items were added or removed from the store on data change
*/
this.cachedStoreData = {};
//var store = dataview.store;
//catch the store data with the snapshot immediately
this.cacheStoreData(dataview.store.snapshot);
dataview.store.on('datachanged', function(store) {
var parentEl = dataview.getTargetEl(),
calcItem = store.getAt(0),
added = this.getAdded(store),
removed = this.getRemoved(store),
previous = this.getRemaining(store),
existing = Ext.apply({}, previous, added);
//hide old items
Ext.each(removed, function(item) {
Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).animate({
remove: false,
duration: duration,
opacity: 0,
useDisplay: true
});
}, this);
//store is empty
if (calcItem == undefined) {
this.cacheStoreData(store);
return;
}
var el = Ext.get(this.dataviewID + "-" + calcItem.get(this.idProperty));
//calculate the number of rows and columns we have
var itemCount = store.getCount(),
itemWidth = el.getMargin('lr') + el.getWidth(),
itemHeight = el.getMargin('bt') + el.getHeight(),
dvWidth = parentEl.getWidth(),
columns = Math.floor(dvWidth / itemWidth),
rows = Math.ceil(itemCount / columns),
currentRows = Math.ceil(this.getExistingCount() / columns);
//make sure the correct styles are applied to the parent element
parentEl.applyStyles({
display: 'block',
position: 'relative'
});
//stores the current top and left values for each element (discovered below)
var oldPositions = {},
newPositions = {},
elCache = {};
//find current positions of each element and save a reference in the elCache
Ext.iterate(previous, function(id, item) {
var id = item.get(this.idProperty),
el = elCache[id] = Ext.get(this.dataviewID + '-' + id);
oldPositions[id] = {
top: el.getY() - parentEl.getY() - el.getMargin('t') - parentEl.getPadding('t'),
left: el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l')
};
}, this);
//set absolute positioning on all DataView items. We need to set position, left and
//top at the same time to avoid any flickering
Ext.iterate(previous, function(id, item) {
var oldPos = oldPositions[id],
el = elCache[id];
if (el.getStyle('position') != 'absolute') {
elCache[id].applyStyles({
position: 'absolute',
left: oldPos.left + "px",
top: oldPos.top + "px",
//we set the width here to make ListViews work correctly. This is not needed for DataViews
width: el.getWidth(!Ext.isIE || Ext.isStrict),
height: el.getHeight(!Ext.isIE || Ext.isStrict)
});
}
});
//get new positions
var index = 0;
Ext.iterate(store.data.items, function(item) {
var id = item.get(idProperty),
el = elCache[id];
var column = index % columns,
row = Math.floor(index / columns),
top = row * itemHeight,
left = column * itemWidth;
newPositions[id] = {
top: top,
left: left
};
index++;
}, this);
//do the movements
var startTime = new Date(),
duration = this.duration,
dataviewID = this.dataviewID;
var doAnimate = function() {
var elapsed = new Date() - startTime,
fraction = elapsed / duration;
if (fraction >= 1) {
for (var id in newPositions) {
Ext.fly(dataviewID + '-' + id).applyStyles({
top: newPositions[id].top + "px",
left: newPositions[id].left + "px"
});
}
Ext.TaskManager.stop(task);
} else {
//move each item
for (var id in newPositions) {
if (!previous[id]) {
continue;
}
var oldPos = oldPositions[id],
newPos = newPositions[id],
oldTop = oldPos.top,
newTop = newPos.top,
oldLeft = oldPos.left,
newLeft = newPos.left,
diffTop = fraction * Math.abs(oldTop - newTop),
diffLeft = fraction * Math.abs(oldLeft - newLeft),
midTop = oldTop > newTop ? oldTop - diffTop : oldTop + diffTop,
midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft;
Ext.fly(dataviewID + '-' + id).applyStyles({
top: midTop + "px",
left: midLeft + "px"
});
}
}
};
var task = {
run: doAnimate,
interval: 20,
scope: this
};
Ext.TaskManager.start(task);
//
var count = 0;
for (var k in added) {
count++;
}
if (Ext.global.console && Ext.global.console.log) {
Ext.global.console.log('added:', count);
}
//
//show new items
Ext.iterate(added, function(id, item) {
Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).applyStyles({
top: newPositions[item.get(this.idProperty)].top + "px",
left: newPositions[item.get(this.idProperty)].left + "px"
});
Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).animate({
remove: false,
duration: duration,
opacity: 1
});
}, this);
this.cacheStoreData(store);
}, this);
},
/**
* Caches the records from a store locally for comparison later
* @param {Ext.data.Store} store The store to cache data from
*/
cacheStoreData: function(store) {
this.cachedStoreData = {};
store.each(function(record) {
this.cachedStoreData[record.get(this.idProperty)] = record;
}, this);
},
/**
* Returns all records that were already in the DataView
* @return {Object} All existing records
*/
getExisting: function() {
return this.cachedStoreData;
},
/**
* Returns the total number of items that are currently visible in the DataView
* @return {Number} The number of existing items
*/
getExistingCount: function() {
var count = 0,
items = this.getExisting();
for (var k in items) count++;
return count;
},
/**
* Returns all records in the given store that were not already present
* @param {Ext.data.Store} store The updated store instance
* @return {Object} Object of records not already present in the dataview in format {id: record}
*/
getAdded: function(store) {
var added = {};
store.each(function(record) {
if (this.cachedStoreData[record.get(this.idProperty)] == undefined) {
added[record.get(this.idProperty)] = record;
}
}, this);
return added;
},
/**
* Returns all records that are present in the DataView but not the new store
* @param {Ext.data.Store} store The updated store instance
* @return {Array} Array of records that used to be present
*/
getRemoved: function(store) {
var removed = [];
for (var id in this.cachedStoreData) {
if (store.findExact(this.idProperty, Number(id)) == -1) {
removed.push(this.cachedStoreData[id]);
}
}
return removed;
},
/**
* Returns all records that are already present and are still present in the new store
* @param {Ext.data.Store} store The updated store instance
* @return {Object} Object of records that are still present from last time in format {id: record}
*/
getRemaining: function(store) {
var remaining = {};
store.each(function(record) {
if (this.cachedStoreData[record.get(this.idProperty)] != undefined) {
remaining[record.get(this.idProperty)] = record;
}
}, this);
return remaining;
}
});
/**
* An explorer component for navigating hierarchical content. Consists of a breadcrumb bar
* at the top, tree navigation on the left, and a center panel which displays the contents
* of a given node.
*/
Ext.define('Ext.ux.Explorer', {
extend: 'Ext.panel.Panel',
xtype: 'explorer',
requires: [
'Ext.layout.container.Border',
'Ext.toolbar.Breadcrumb',
'Ext.tree.Panel'
],
config: {
/**
* @cfg {Object} breadcrumb
* Configuration object for the breadcrumb toolbar
*/
breadcrumb: {
dock: 'top',
xtype: 'breadcrumb',
reference: 'breadcrumb'
},
/**
* @cfg {Object} contentView
* Configuration object for the "content" data view
*/
contentView: {
xtype: 'dataview',
reference: 'contentView',
region: 'center',
cls: Ext.baseCSSPrefix + 'explorer-view',
itemSelector: '.' + Ext.baseCSSPrefix + 'explorer-item',
tpl: '' + '
' + '
' + '
' + '
' + '
{text}
' + '
' + '
' + ''
},
/**
* @cfg {Ext.data.TreeStore} store
* The TreeStore to use as the data source
*/
store: null,
/**
* @cfg {Object} tree
* Configuration object for the tree
*/
tree: {
xtype: 'treepanel',
reference: 'tree',
region: 'west',
width: 200
}
},
renderConfig: {
/**
* @cfg {Ext.data.TreeModel} selection
* The selected node
* @accessor
*/
selection: null
},
layout: 'border',
referenceHolder: true,
defaultListenerScope: true,
cls: Ext.baseCSSPrefix + 'explorer',
initComponent: function() {
var me = this,
store = me.getStore();
//
if (!store) {
Ext.raise('Ext.ux.Explorer requires a store.');
}
//
me.dockedItems = [
me.getBreadcrumb()
];
me.items = [
me.getTree(),
me.getContentView()
];
me.callParent();
},
applyBreadcrumb: function(breadcrumb) {
var store = this.getStore();
breadcrumb = Ext.create(Ext.apply({
store: store,
selection: store.getRoot()
}, breadcrumb));
breadcrumb.on('selectionchange', '_onBreadcrumbSelectionChange', this);
return breadcrumb;
},
applyContentView: function(contentView) {
/**
* @property {Ext.data.Store} contentStore
* @private
* The backing store for the content view
*/
var contentStore = this.contentStore = new Ext.data.Store({
model: this.getStore().model
});
contentView = Ext.create(Ext.apply({
store: contentStore
}, contentView));
return contentView;
},
applyTree: function(tree) {
tree = Ext.create(Ext.apply({
store: this.getStore()
}, tree));
tree.on('selectionchange', '_onTreeSelectionChange', this);
return tree;
},
updateSelection: function(node) {
var me = this,
refs = me.getReferences(),
breadcrumb = refs.breadcrumb,
tree = refs.tree,
treeSelectionModel = tree.getSelectionModel(),
contentStore = me.contentStore,
parentNode, treeView;
if (breadcrumb.getSelection() !== node) {
breadcrumb.setSelection(node);
}
if (treeSelectionModel.getSelection()[0] !== node) {
treeSelectionModel.select([
node
]);
parentNode = node.parentNode;
if (parentNode) {
parentNode.expand();
}
treeView = tree.getView();
treeView.scrollRowIntoView(treeView.getRow(node));
}
contentStore.removeAll();
contentStore.add(node.hasChildNodes() ? node.childNodes : [
node
]);
},
updateStore: function(store) {
this.getBreadcrumb().setStore(store);
},
privates: {
/**
* Handles the tree's selectionchange event
* @private
* @param {Ext.tree.Panel} tree
* @param {Ext.data.TreeModel[]} selection
*/
_onTreeSelectionChange: function(tree, selection) {
this.setSelection(selection[0]);
},
/**
* Handles the breadcrumb bar's selectionchange event
*/
_onBreadcrumbSelectionChange: function(breadcrumb, selection) {
this.setSelection(selection);
}
}
});
/**
* A plugin for Field Components which creates clones of the Field for as
* long as the user keeps filling them. Leaving the final one blank ends the repeating series.
*
* Usage:
*
* items: [{
* xtype: 'combo',
* plugins: {
* fieldreplicator: true
* },
* triggerAction: 'all',
* fieldLabel: 'Select recipient',
* store: recipientStore
* }]
*
*/
Ext.define('Ext.ux.FieldReplicator', {
alias: 'plugin.fieldreplicator',
init: function(field) {
// Assign the field an id grouping it with fields cloned from it. If it already
// has an id that means it is itself a clone.
if (!field.replicatorId) {
field.replicatorId = Ext.id();
}
field.on('blur', this.onBlur, this);
},
onBlur: function(field) {
var ownerCt = field.ownerCt,
replicatorId = field.replicatorId,
isEmpty = Ext.isEmpty(field.getRawValue()),
siblings = ownerCt.query('[replicatorId=' + replicatorId + ']'),
isLastInGroup = siblings[siblings.length - 1] === field,
clone, idx;
// If a field before the final one was blanked out, remove it
if (isEmpty && !isLastInGroup) {
Ext.defer(field.destroy, 10, field);
}
//delay to allow tab key to move focus first
// If the field is the last in the list and has a value, add a cloned field after it
else if (!isEmpty && isLastInGroup) {
if (field.onReplicate) {
field.onReplicate();
}
clone = field.cloneConfig({
replicatorId: replicatorId
});
idx = ownerCt.items.indexOf(field);
ownerCt.add(idx + 1, clone);
}
}
});
/**
* The GMap Panel UX extends `Ext.panel.Panel` in order to display Google Maps.
*
* It is important to note that you must include the following Google Maps API above bootstrap.js in your
* application's index.html file (or equivilant).
*
*
*
* It is important to note that due to the Google Maps loader, you cannot currently include
* the above JS resource in the Cmd generated app.json file. Doing so interferes with the loading of
* Ext JS and Google Maps.
*
* The following example creates a window containing a GMap Panel. In this case, the center
* is set as geoCodeAddr, which is a string that Google translates into longitude and latitude.
*
* var mapwin = Ext.create('Ext.Window', {
* layout: 'fit',
* title: 'GMap Window',
* width: 450,
* height: 250,
* items: {
* xtype: 'gmappanel',
* gmapType: 'map',
* center: {
* geoCodeAddr: "221B Baker Street",
* marker: {
* title: 'Holmes Home'
* }
* },
* mapOptions : {
* mapTypeId: google.maps.MapTypeId.ROADMAP
* }
* }
* }).show();
*
*/
Ext.define('Ext.ux.GMapPanel', {
extend: 'Ext.panel.Panel',
alias: 'widget.gmappanel',
requires: [
'Ext.window.MessageBox'
],
initComponent: function() {
Ext.applyIf(this, {
plain: true,
gmapType: 'map',
border: false
});
this.callParent();
},
onBoxReady: function() {
var center = this.center;
this.callParent(arguments);
if (center) {
if (center.geoCodeAddr) {
this.lookupCode(center.geoCodeAddr, center.marker);
} else {
this.createMap(center);
}
} else {
Ext.raise('center is required');
}
},
createMap: function(center, marker) {
var options = Ext.apply({}, this.mapOptions);
options = Ext.applyIf(options, {
zoom: 14,
center: center,
mapTypeId: google.maps.MapTypeId.HYBRID
});
this.gmap = new google.maps.Map(this.body.dom, options);
if (marker) {
this.addMarker(Ext.applyIf(marker, {
position: center
}));
}
Ext.each(this.markers, this.addMarker, this);
this.fireEvent('mapready', this, this.gmap);
},
addMarker: function(marker) {
marker = Ext.apply({
map: this.gmap
}, marker);
if (!marker.position) {
marker.position = new google.maps.LatLng(marker.lat, marker.lng);
}
var o = new google.maps.Marker(marker);
Ext.Object.each(marker.listeners, function(name, fn) {
google.maps.event.addListener(o, name, fn);
});
return o;
},
lookupCode: function(addr, marker) {
this.geocoder = new google.maps.Geocoder();
this.geocoder.geocode({
address: addr
}, Ext.Function.bind(this.onLookupComplete, this, [
marker
], true));
},
onLookupComplete: function(data, response, marker) {
if (response != 'OK') {
Ext.MessageBox.alert('Error', 'An error occured: "' + response + '"');
return;
}
this.createMap(data[0].geometry.location, marker);
},
afterComponentLayout: function(w, h) {
this.callParent(arguments);
this.redraw();
},
redraw: function() {
var map = this.gmap;
if (map) {
google.maps.event.trigger(map, 'resize');
}
}
});
/**
* Barebones iframe implementation.
*/
Ext.define('Ext.ux.IFrame', {
extend: 'Ext.Component',
alias: 'widget.uxiframe',
loadMask: 'Loading...',
src: 'about:blank',
renderTpl: [
''
],
childEls: [
'iframeEl'
],
initComponent: function() {
this.callParent();
this.frameName = this.frameName || this.id + '-frame';
},
initEvents: function() {
var me = this;
me.callParent();
me.iframeEl.on('load', me.onLoad, me);
},
initRenderData: function() {
return Ext.apply(this.callParent(), {
src: this.src,
frameName: this.frameName
});
},
getBody: function() {
var doc = this.getDoc();
return doc.body || doc.documentElement;
},
getDoc: function() {
try {
return this.getWin().document;
} catch (ex) {
return null;
}
},
getWin: function() {
var me = this,
name = me.frameName,
win = Ext.isIE ? me.iframeEl.dom.contentWindow : window.frames[name];
return win;
},
getFrame: function() {
var me = this;
return me.iframeEl.dom;
},
onLoad: function() {
var me = this,
doc = me.getDoc();
if (doc) {
this.el.unmask();
this.fireEvent('load', this);
} else if (me.src) {
this.el.unmask();
this.fireEvent('error', this);
}
},
load: function(src) {
var me = this,
text = me.loadMask,
frame = me.getFrame();
if (me.fireEvent('beforeload', me, src) !== false) {
if (text && me.el) {
me.el.mask(text);
}
frame.src = me.src = (src || me.src);
}
}
});
/*
* Note: Event relayers are not needed here because the combination of the gesture system and
* normal focus/blur will handle it.
* Tested with the examples/classic/desktop app.
*/
/*
* TODO items:
*
* Iframe should clean up any Ext.dom.Element wrappers around its window, document
* documentElement and body when it is destroyed. This helps prevent "Permission Denied"
* errors in IE when Ext.dom.GarbageCollector tries to access those objects on an orphaned
* iframe. Permission Denied errors can occur in one of the following 2 scenarios:
*
* a. When an iframe is removed from the document, and all references to it have been
* removed, IE will "clear" the window object. At this point the window object becomes
* completely inaccessible - accessing any of its properties results in a "Permission
* Denied" error. http://msdn.microsoft.com/en-us/library/ie/hh180174(v=vs.85).aspx
*
* b. When an iframe is unloaded (either by navigating to a new url, or via document.open/
* document.write, new html and body elements are created and the old the html and body
* elements are orphaned. Accessing the html and body elements or any of their properties
* results in a "Permission Denied" error.
*/
/**
* Basic status bar component that can be used as the bottom toolbar of any {@link Ext.Panel}. In addition to
* supporting the standard {@link Ext.toolbar.Toolbar} interface for adding buttons, menus and other items, the StatusBar
* provides a greedy status element that can be aligned to either side and has convenient methods for setting the
* status text and icon. You can also indicate that something is processing using the {@link #showBusy} method.
*
* Ext.create('Ext.Panel', {
* title: 'StatusBar',
* // etc.
* bbar: Ext.create('Ext.ux.StatusBar', {
* id: 'my-status',
*
* // defaults to use when the status is cleared:
* defaultText: 'Default status text',
* defaultIconCls: 'default-icon',
*
* // values to set initially:
* text: 'Ready',
* iconCls: 'ready-icon',
*
* // any standard Toolbar items:
* items: [{
* text: 'A Button'
* }, '-', 'Plain Text']
* })
* });
*
* // Update the status bar later in code:
* var sb = Ext.getCmp('my-status');
* sb.setStatus({
* text: 'OK',
* iconCls: 'ok-icon',
* clear: true // auto-clear after a set interval
* });
*
* // Set the status bar to show that something is processing:
* sb.showBusy();
*
* // processing....
*
* sb.clearStatus(); // once completeed
*
*/
Ext.define('Ext.ux.statusbar.StatusBar', {
extend: 'Ext.toolbar.Toolbar',
xtype: 'statusbar',
alternateClassName: 'Ext.ux.StatusBar',
requires: [
'Ext.toolbar.TextItem'
],
/**
* @cfg {String} statusAlign
* The alignment of the status element within the overall StatusBar layout. When the StatusBar is rendered,
* it creates an internal div containing the status text and icon. Any additional Toolbar items added in the
* StatusBar's {@link #cfg-items} config, or added via {@link #method-add} or any of the supported add* methods, will be
* rendered, in added order, to the opposite side. The status element is greedy, so it will automatically
* expand to take up all sapce left over by any other items. Example usage:
*
* // Create a left-aligned status bar containing a button,
* // separator and text item that will be right-aligned (default):
* Ext.create('Ext.Panel', {
* title: 'StatusBar',
* // etc.
* bbar: Ext.create('Ext.ux.statusbar.StatusBar', {
* defaultText: 'Default status text',
* id: 'status-id',
* items: [{
* text: 'A Button'
* }, '-', 'Plain Text']
* })
* });
*
* // By adding the statusAlign config, this will create the
* // exact same toolbar, except the status and toolbar item
* // layout will be reversed from the previous example:
* Ext.create('Ext.Panel', {
* title: 'StatusBar',
* // etc.
* bbar: Ext.create('Ext.ux.statusbar.StatusBar', {
* defaultText: 'Default status text',
* id: 'status-id',
* statusAlign: 'right',
* items: [{
* text: 'A Button'
* }, '-', 'Plain Text']
* })
* });
*/
/**
* @cfg {String} [defaultText='']
* The default {@link #text} value. This will be used anytime the status bar is cleared with the
* `useDefaults:true` option.
*/
/**
* @cfg {String} [defaultIconCls='']
* The default {@link #iconCls} value (see the iconCls docs for additional details about customizing the icon).
* This will be used anytime the status bar is cleared with the `useDefaults:true` option.
*/
/**
* @cfg {String} text
* A string that will be initially set as the status message. This string
* will be set as innerHTML (html tags are accepted) for the toolbar item.
* If not specified, the value set for {@link #defaultText} will be used.
*/
/**
* @cfg [iconCls='']
* @inheritdoc Ext.panel.Header#cfg-iconCls
* @localdoc **Note:** This CSS class will be **initially** set as the status bar
* icon. See also {@link #defaultIconCls} and {@link #busyIconCls}.
*
* Example usage:
*
* // Example CSS rule:
* .x-statusbar .x-status-custom {
* padding-left: 25px;
* background: transparent url(images/custom-icon.gif) no-repeat 3px 2px;
* }
*
* // Setting a default icon:
* var sb = Ext.create('Ext.ux.statusbar.StatusBar', {
* defaultIconCls: 'x-status-custom'
* });
*
* // Changing the icon:
* sb.setStatus({
* text: 'New status',
* iconCls: 'x-status-custom'
* });
*/
/**
* @cfg {String} cls
* The base class applied to the containing element for this component on render.
*/
cls: 'x-statusbar',
/**
* @cfg {String} busyIconCls
* The default {@link #iconCls} applied when calling {@link #showBusy}.
* It can be overridden at any time by passing the `iconCls` argument into {@link #showBusy}.
*/
busyIconCls: 'x-status-busy',
/**
* @cfg {String} busyText
* The default {@link #text} applied when calling {@link #showBusy}.
* It can be overridden at any time by passing the `text` argument into {@link #showBusy}.
*/
busyText: 'Loading...',
/**
* @cfg {Number} autoClear
* The number of milliseconds to wait after setting the status via
* {@link #setStatus} before automatically clearing the status text and icon.
* Note that this only applies when passing the `clear` argument to {@link #setStatus}
* since that is the only way to defer clearing the status. This can
* be overridden by specifying a different `wait` value in {@link #setStatus}.
* Calls to {@link #clearStatus} always clear the status bar immediately and ignore this value.
*/
autoClear: 5000,
/**
* @cfg {String} emptyText
* The text string to use if no text has been set. If there are no other items in
* the toolbar using an empty string (`''`) for this value would end up in the toolbar
* height collapsing since the empty string will not maintain the toolbar height.
* Use `''` if the toolbar should collapse in height vertically when no text is
* specified and there are no other items in the toolbar.
*/
emptyText: ' ',
/**
* @private
*/
activeThreadId: 0,
initComponent: function() {
var right = this.statusAlign === 'right';
this.callParent(arguments);
this.currIconCls = this.iconCls || this.defaultIconCls;
this.statusEl = Ext.create('Ext.toolbar.TextItem', {
cls: 'x-status-text ' + (this.currIconCls || ''),
text: this.text || this.defaultText || ''
});
if (right) {
this.cls += ' x-status-right';
this.add('->');
this.add(this.statusEl);
} else {
this.insert(0, this.statusEl);
this.insert(1, '->');
}
},
/**
* Sets the status {@link #text} and/or {@link #iconCls}. Also supports automatically clearing the
* status that was set after a specified interval.
*
* Example usage:
*
* // Simple call to update the text
* statusBar.setStatus('New status');
*
* // Set the status and icon, auto-clearing with default options:
* statusBar.setStatus({
* text: 'New status',
* iconCls: 'x-status-custom',
* clear: true
* });
*
* // Auto-clear with custom options:
* statusBar.setStatus({
* text: 'New status',
* iconCls: 'x-status-custom',
* clear: {
* wait: 8000,
* anim: false,
* useDefaults: false
* }
* });
*
* @param {Object/String} config A config object specifying what status to set, or a string assumed
* to be the status text (and all other options are defaulted as explained below). A config
* object containing any or all of the following properties can be passed:
*
* @param {String} config.text The status text to display. If not specified, any current
* status text will remain unchanged.
*
* @param {String} config.iconCls The CSS class used to customize the status icon (see
* {@link #iconCls} for details). If not specified, any current iconCls will remain unchanged.
*
* @param {Boolean/Number/Object} config.clear Allows you to set an internal callback that will
* automatically clear the status text and iconCls after a specified amount of time has passed. If clear is not
* specified, the new status will not be auto-cleared and will stay until updated again or cleared using
* {@link #clearStatus}. If `true` is passed, the status will be cleared using {@link #autoClear},
* {@link #defaultText} and {@link #defaultIconCls} via a fade out animation. If a numeric value is passed,
* it will be used as the callback interval (in milliseconds), overriding the {@link #autoClear} value.
* All other options will be defaulted as with the boolean option. To customize any other options,
* you can pass an object in the format:
*
* @param {Number} config.clear.wait The number of milliseconds to wait before clearing
* (defaults to {@link #autoClear}).
* @param {Boolean} config.clear.anim False to clear the status immediately once the callback
* executes (defaults to true which fades the status out).
* @param {Boolean} config.clear.useDefaults False to completely clear the status text and iconCls
* (defaults to true which uses {@link #defaultText} and {@link #defaultIconCls}).
*
* @return {Ext.ux.statusbar.StatusBar} this
*/
setStatus: function(config) {
var me = this;
config = config || {};
Ext.suspendLayouts();
if (Ext.isString(config)) {
config = {
text: config
};
}
if (config.text !== undefined) {
me.setText(config.text);
}
if (config.iconCls !== undefined) {
me.setIcon(config.iconCls);
}
if (config.clear) {
var c = config.clear,
wait = me.autoClear,
defaults = {
useDefaults: true,
anim: true
};
if (Ext.isObject(c)) {
c = Ext.applyIf(c, defaults);
if (c.wait) {
wait = c.wait;
}
} else if (Ext.isNumber(c)) {
wait = c;
c = defaults;
} else if (Ext.isBoolean(c)) {
c = defaults;
}
c.threadId = this.activeThreadId;
Ext.defer(me.clearStatus, wait, me, [
c
]);
}
Ext.resumeLayouts(true);
return me;
},
/**
* Clears the status {@link #text} and {@link #iconCls}. Also supports clearing via an optional fade out animation.
*
* @param {Object} [config] A config object containing any or all of the following properties. If this
* object is not specified the status will be cleared using the defaults below:
* @param {Boolean} config.anim True to clear the status by fading out the status element (defaults
* to false which clears immediately).
* @param {Boolean} config.useDefaults True to reset the text and icon using {@link #defaultText} and
* {@link #defaultIconCls} (defaults to false which sets the text to '' and removes any existing icon class).
*
* @return {Ext.ux.statusbar.StatusBar} this
*/
clearStatus: function(config) {
config = config || {};
var me = this,
statusEl = me.statusEl;
if (me.destroyed || config.threadId && config.threadId !== me.activeThreadId) {
// this means the current call was made internally, but a newer
// thread has set a message since this call was deferred. Since
// we don't want to overwrite a newer message just ignore.
return me;
}
var text = config.useDefaults ? me.defaultText : me.emptyText,
iconCls = config.useDefaults ? (me.defaultIconCls ? me.defaultIconCls : '') : '';
if (config.anim) {
// animate the statusEl Ext.Element
statusEl.el.puff({
remove: false,
useDisplay: true,
callback: function() {
statusEl.el.show();
me.setStatus({
text: text,
iconCls: iconCls
});
}
});
} else {
me.setStatus({
text: text,
iconCls: iconCls
});
}
return me;
},
/**
* Convenience method for setting the status text directly. For more flexible options see {@link #setStatus}.
* @param {String} text (optional) The text to set (defaults to '')
* @return {Ext.ux.statusbar.StatusBar} this
*/
setText: function(text) {
var me = this;
me.activeThreadId++;
me.text = text || '';
if (me.rendered) {
me.statusEl.setText(me.text);
}
return me;
},
/**
* Returns the current status text.
* @return {String} The status text
*/
getText: function() {
return this.text;
},
/**
* Convenience method for setting the status icon directly. For more flexible options see {@link #setStatus}.
* See {@link #iconCls} for complete details about customizing the icon.
* @param {String} cls (optional) The icon class to set (defaults to '', and any current icon class is removed)
* @return {Ext.ux.statusbar.StatusBar} this
*/
setIcon: function(cls) {
var me = this;
me.activeThreadId++;
cls = cls || '';
if (me.rendered) {
if (me.currIconCls) {
me.statusEl.removeCls(me.currIconCls);
me.currIconCls = null;
}
if (cls.length > 0) {
me.statusEl.addCls(cls);
me.currIconCls = cls;
}
} else {
me.currIconCls = cls;
}
return me;
},
/**
* Convenience method for setting the status text and icon to special values that are pre-configured to indicate
* a "busy" state, usually for loading or processing activities.
*
* @param {Object/String} config (optional) A config object in the same format supported by {@link #setStatus}, or a
* string to use as the status text (in which case all other options for setStatus will be defaulted). Use the
* `text` and/or `iconCls` properties on the config to override the default {@link #busyText}
* and {@link #busyIconCls} settings. If the config argument is not specified, {@link #busyText} and
* {@link #busyIconCls} will be used in conjunction with all of the default options for {@link #setStatus}.
* @return {Ext.ux.statusbar.StatusBar} this
*/
showBusy: function(config) {
if (Ext.isString(config)) {
config = {
text: config
};
}
config = Ext.applyIf(config || {}, {
text: this.busyText,
iconCls: this.busyIconCls
});
return this.setStatus(config);
}
});
/**
* A GridPanel class with live search support.
*/
Ext.define('Ext.ux.LiveSearchGridPanel', {
extend: 'Ext.grid.Panel',
requires: [
'Ext.toolbar.TextItem',
'Ext.form.field.Checkbox',
'Ext.form.field.Text',
'Ext.ux.statusbar.StatusBar'
],
/**
* @private
* search value initialization
*/
searchValue: null,
/**
* @private
* The matched positions from the most recent search
*/
matches: [],
/**
* @private
* The current index matched.
*/
currentIndex: null,
/**
* @private
* The generated regular expression used for searching.
*/
searchRegExp: null,
/**
* @private
* Case sensitive mode.
*/
caseSensitive: false,
/**
* @private
* Regular expression mode.
*/
regExpMode: false,
/**
* @cfg {String} matchCls
* The matched string css classe.
*/
matchCls: 'x-livesearch-match',
defaultStatusText: 'Nothing Found',
// Component initialization override: adds the top and bottom toolbars and setup headers renderer.
initComponent: function() {
var me = this;
me.tbar = [
'Search',
{
xtype: 'textfield',
name: 'searchField',
hideLabel: true,
width: 200,
listeners: {
change: {
fn: me.onTextFieldChange,
scope: this,
buffer: 500
}
}
},
{
xtype: 'button',
text: '<',
tooltip: 'Find Previous Row',
handler: me.onPreviousClick,
scope: me
},
{
xtype: 'button',
text: '>',
tooltip: 'Find Next Row',
handler: me.onNextClick,
scope: me
},
'-',
{
xtype: 'checkbox',
hideLabel: true,
margin: '0 0 0 4px',
handler: me.regExpToggle,
scope: me
},
'Regular expression',
{
xtype: 'checkbox',
hideLabel: true,
margin: '0 0 0 4px',
handler: me.caseSensitiveToggle,
scope: me
},
'Case sensitive'
];
me.bbar = new Ext.ux.StatusBar({
defaultText: me.defaultStatusText,
name: 'searchStatusBar'
});
me.callParent(arguments);
},
// afterRender override: it adds textfield and statusbar reference and start monitoring keydown events in textfield input
afterRender: function() {
var me = this;
me.callParent(arguments);
me.textField = me.down('textfield[name=searchField]');
me.statusBar = me.down('statusbar[name=searchStatusBar]');
me.view.on('cellkeydown', me.focusTextField, me);
},
focusTextField: function(view, td, cellIndex, record, tr, rowIndex, e, eOpts) {
if (e.getKey() === e.S) {
e.preventDefault();
this.textField.focus();
}
},
// detects html tag
tagsRe: /<[^>]*>/gm,
// DEL ASCII code
tagsProtect: '\x0f',
/**
* In normal mode it returns the value with protected regexp characters.
* In regular expression mode it returns the raw value except if the regexp is invalid.
* @return {String} The value to process or null if the textfield value is blank or invalid.
* @private
*/
getSearchValue: function() {
var me = this,
value = me.textField.getValue();
if (value === '') {
return null;
}
if (!me.regExpMode) {
value = Ext.String.escapeRegex(value);
} else {
try {
new RegExp(value);
} catch (error) {
me.statusBar.setStatus({
text: error.message,
iconCls: 'x-status-error'
});
return null;
}
// this is stupid
if (value === '^' || value === '$') {
return null;
}
}
return value;
},
/**
* Finds all strings that matches the searched value in each grid cells.
* @private
*/
onTextFieldChange: function() {
var me = this,
count = 0,
view = me.view,
cellSelector = view.cellSelector,
innerSelector = view.innerSelector,
columns = me.visibleColumnManager.getColumns();
view.refresh();
// reset the statusbar
me.statusBar.setStatus({
text: me.defaultStatusText,
iconCls: ''
});
me.searchValue = me.getSearchValue();
me.matches = [];
me.currentIndex = null;
if (me.searchValue !== null) {
me.searchRegExp = new RegExp(me.getSearchValue(), 'g' + (me.caseSensitive ? '' : 'i'));
me.store.each(function(record, idx) {
var node = view.getNode(record);
if (node) {
Ext.Array.forEach(columns, function(column) {
var cell = Ext.fly(node).down(column.getCellInnerSelector(), true),
matches, cellHTML, seen;
if (cell) {
matches = cell.innerHTML.match(me.tagsRe);
cellHTML = cell.innerHTML.replace(me.tagsRe, me.tagsProtect);
// populate indexes array, set currentIndex, and replace wrap matched string in a span
cellHTML = cellHTML.replace(me.searchRegExp, function(m) {
++count;
if (!seen) {
me.matches.push({
record: record,
column: column
});
seen = true;
}
return '' + m + '';
}, me);
// restore protected tags
Ext.each(matches, function(match) {
cellHTML = cellHTML.replace(me.tagsProtect, match);
});
// update cell html
cell.innerHTML = cellHTML;
}
});
}
}, me);
// results found
if (count) {
me.currentIndex = 0;
me.gotoCurrent();
me.statusBar.setStatus({
text: Ext.String.format('{0} match{1} found.', count, count === 1 ? 'es' : ''),
iconCls: 'x-status-valid'
});
}
}
// no results found
if (me.currentIndex === null) {
me.getSelectionModel().deselectAll();
me.textField.focus();
}
},
/**
* Selects the previous row containing a match.
* @private
*/
onPreviousClick: function() {
var me = this,
matches = me.matches,
len = matches.length,
idx = me.currentIndex;
if (len) {
me.currentIndex = idx === 0 ? len - 1 : idx - 1;
me.gotoCurrent();
}
},
/**
* Selects the next row containing a match.
* @private
*/
onNextClick: function() {
var me = this,
matches = me.matches,
len = matches.length,
idx = me.currentIndex;
if (len) {
me.currentIndex = idx === len - 1 ? 0 : idx + 1;
me.gotoCurrent();
}
},
/**
* Switch to case sensitive mode.
* @private
*/
caseSensitiveToggle: function(checkbox, checked) {
this.caseSensitive = checked;
this.onTextFieldChange();
},
/**
* Switch to regular expression mode
* @private
*/
regExpToggle: function(checkbox, checked) {
this.regExpMode = checked;
this.onTextFieldChange();
},
privates: {
gotoCurrent: function() {
var pos = this.matches[this.currentIndex];
this.getNavigationModel().setPosition(pos.record, pos.column);
this.getSelectionModel().select(pos.record);
}
}
});
/**
* The Preview Plugin enables toggle of a configurable preview of all visible records.
*
* Note: This plugin does NOT assert itself against an existing RowBody feature and may conflict with
* another instance of the same plugin.
*/
Ext.define('Ext.ux.PreviewPlugin', {
extend: 'Ext.plugin.Abstract',
alias: 'plugin.preview',
requires: [
'Ext.grid.feature.RowBody'
],
/**
* @private
* css class to use to hide the body
*/
hideBodyCls: 'x-grid-row-body-hidden',
/**
* @cfg {String} bodyField
* Field to display in the preview. Must be a field within the Model definition
* that the store is using.
*/
bodyField: '',
/**
* @cfg {Boolean} previewExpanded
*/
previewExpanded: true,
/**
* Plugin may be safely declared on either a panel.Grid or a Grid View/viewConfig
* @param {Ext.grid.Panel/Ext.view.View} target
*/
setCmp: function(target) {
this.callParent(arguments);
// Resolve grid from view as necessary
var me = this,
grid = me.cmp = target.isXType('gridview') ? target.grid : target,
bodyField = me.bodyField,
hideBodyCls = me.hideBodyCls,
feature = Ext.create('Ext.grid.feature.RowBody', {
grid: grid,
getAdditionalData: function(data, idx, model, rowValues) {
var getAdditionalData = Ext.grid.feature.RowBody.prototype.getAdditionalData,
additionalData = {
rowBody: data[bodyField],
rowBodyCls: grid.getView().previewExpanded ? '' : hideBodyCls
};
if (Ext.isFunction(getAdditionalData)) {
// "this" is the RowBody object hjere. Do not change to "me"
Ext.apply(additionalData, getAdditionalData.apply(this, arguments));
}
return additionalData;
}
}),
initFeature = function(grid, view) {
view.previewExpanded = me.previewExpanded;
// By this point, existing features are already in place, so this must be initialized and added
view.featuresMC.add(feature);
feature.init(grid);
};
// The grid has already created its view
if (grid.view) {
initFeature(grid, grid.view);
} else // At the time a grid creates its plugins, it has not created all the things
// it needs to create its view correctly.
// Process the view and init the RowBody Feature as soon as the view is created.
{
grid.on({
viewcreated: initFeature,
single: true
});
}
},
/**
* Toggle between the preview being expanded/hidden on all rows
* @param {Boolean} expanded Pass true to expand the record and false to not show the preview.
*/
toggleExpanded: function(expanded) {
var grid = this.getCmp(),
view = grid && grid.getView(),
bufferedRenderer = view.bufferedRenderer,
scrollManager = view.scrollManager;
if (grid && view && expanded !== view.previewExpanded) {
this.previewExpanded = view.previewExpanded = !!expanded;
view.refreshView();
// If we are using the touch scroller, ensure that the scroller knows about
// the correct scrollable range
if (scrollManager) {
if (bufferedRenderer) {
bufferedRenderer.stretchView(view, bufferedRenderer.getScrollHeight(true));
} else {
scrollManager.refresh(true);
}
}
}
}
});
/**
* Plugin for displaying a progressbar inside of a paging toolbar
* instead of plain text.
*/
Ext.define('Ext.ux.ProgressBarPager', {
alias: 'plugin.ux-progressbarpager',
requires: [
'Ext.ProgressBar'
],
/**
* @cfg {Number} width
*
The default progress bar width. Default is 225.
*/
width: 225,
/**
* @cfg {String} defaultText
*
The text to display while the store is loading. Default is 'Loading...'
* The afterLayout function can also be overridden, and is called after a new item has been
* created and inserted into the Toolbar. Use this for any logic that needs to be run after
* the item has been created.
*/
Ext.define('Ext.ux.ToolbarDroppable', {
/**
* Creates new ToolbarDroppable.
* @param {Object} config Config options.
*/
constructor: function(config) {
Ext.apply(this, config);
},
/**
* Initializes the plugin and saves a reference to the toolbar
* @param {Ext.toolbar.Toolbar} toolbar The toolbar instance
*/
init: function(toolbar) {
/**
* @property toolbar
* @type Ext.toolbar.Toolbar
* The toolbar instance that this plugin is tied to
*/
this.toolbar = toolbar;
this.toolbar.on({
scope: this,
render: this.createDropTarget
});
},
/**
* Creates a drop target on the toolbar
*/
createDropTarget: function() {
/**
* @property dropTarget
* @type Ext.dd.DropTarget
* The drop target attached to the toolbar instance
*/
this.dropTarget = Ext.create('Ext.dd.DropTarget', this.toolbar.getEl(), {
notifyOver: this.notifyOver.bind(this),
notifyDrop: this.notifyDrop.bind(this)
});
},
/**
* Adds the given DD Group to the drop target
* @param {String} ddGroup The DD Group
*/
addDDGroup: function(ddGroup) {
this.dropTarget.addToGroup(ddGroup);
},
/**
* Calculates the location on the toolbar to create the new sorter button based on the XY of the
* drag event
* @param {Ext.event.Event} e The event object
* @return {Number} The index at which to insert the new button
*/
calculateEntryIndex: function(e) {
var entryIndex = 0,
toolbar = this.toolbar,
items = toolbar.items.items,
count = items.length,
xHover = e.getXY()[0],
index = 0,
el, xTotal, width, midpoint;
for (; index < count; index++) {
el = items[index].getEl();
xTotal = el.getXY()[0];
width = el.getWidth();
midpoint = xTotal + width / 2;
if (xHover < midpoint) {
entryIndex = index;
break;
} else {
entryIndex = index + 1;
}
}
return entryIndex;
},
/**
* Returns true if the drop is allowed on the drop target. This function can be overridden
* and defaults to simply return true
* @param {Object} data Arbitrary data from the drag source
* @return {Boolean} True if the drop is allowed
*/
canDrop: function(data) {
return true;
},
/**
* Custom notifyOver method which will be used in the plugin's internal DropTarget
* @return {String} The CSS class to add
*/
notifyOver: function(dragSource, event, data) {
return this.canDrop.apply(this, arguments) ? this.dropTarget.dropAllowed : this.dropTarget.dropNotAllowed;
},
/**
* Called when the drop has been made. Creates the new toolbar item, places it at the correct location
* and calls the afterLayout callback.
*/
notifyDrop: function(dragSource, event, data) {
var canAdd = this.canDrop(dragSource, event, data),
tbar = this.toolbar;
if (canAdd) {
var entryIndex = this.calculateEntryIndex(event);
tbar.insert(entryIndex, this.createItem(data));
this.afterLayout();
}
return canAdd;
},
/**
* Creates the new toolbar item based on drop data. This method must be implemented by the plugin instance
* @param {Object} data Arbitrary data from the drop
* @return {Mixed} An item that can be added to a toolbar
*/
createItem: function(data) {
//
Ext.raise("The createItem method must be implemented in the ToolbarDroppable plugin");
},
//
/**
* @method
* Called after a new button has been created and added to the toolbar. Add any required cleanup logic here
*/
afterLayout: Ext.emptyFn
});
/**
* A Picker field that contains a tree panel on its popup, enabling selection of tree nodes.
*/
Ext.define('Ext.ux.TreePicker', {
extend: 'Ext.form.field.Picker',
xtype: 'treepicker',
uses: [
'Ext.tree.Panel'
],
triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
config: {
/**
* @cfg {Ext.data.TreeStore} store
* A tree store that the tree picker will be bound to
*/
store: null,
/**
* @cfg {String} displayField
* The field inside the model that will be used as the node's text.
* Defaults to the default value of {@link Ext.tree.Panel}'s `displayField` configuration.
*/
displayField: null,
/**
* @cfg {Array} columns
* An optional array of columns for multi-column trees
*/
columns: null,
/**
* @cfg {Boolean} selectOnTab
* Whether the Tab key should select the currently highlighted item. Defaults to `true`.
*/
selectOnTab: true,
/**
* @cfg {Number} maxPickerHeight
* The maximum height of the tree dropdown. Defaults to 300.
*/
maxPickerHeight: 300,
/**
* @cfg {Number} minPickerHeight
* The minimum height of the tree dropdown. Defaults to 100.
*/
minPickerHeight: 100
},
editable: false,
/**
* @event select
* Fires when a tree node is selected
* @param {Ext.ux.TreePicker} picker This tree picker
* @param {Ext.data.Model} record The selected record
*/
initComponent: function() {
var me = this;
me.callParent(arguments);
me.mon(me.store, {
scope: me,
load: me.onLoad,
update: me.onUpdate
});
},
/**
* Creates and returns the tree panel to be used as this field's picker.
*/
createPicker: function() {
var me = this,
picker = new Ext.tree.Panel({
baseCls: Ext.baseCSSPrefix + 'boundlist',
shrinkWrapDock: 2,
store: me.store,
floating: true,
displayField: me.displayField,
columns: me.columns,
minHeight: me.minPickerHeight,
maxHeight: me.maxPickerHeight,
manageHeight: false,
shadow: false,
listeners: {
scope: me,
itemclick: me.onItemClick,
itemkeydown: me.onPickerKeyDown
}
}),
view = picker.getView();
if (Ext.isIE9 && Ext.isStrict) {
// In IE9 strict mode, the tree view grows by the height of the horizontal scroll bar when the items are highlighted or unhighlighted.
// Also when items are collapsed or expanded the height of the view is off. Forcing a repaint fixes the problem.
view.on({
scope: me,
highlightitem: me.repaintPickerView,
unhighlightitem: me.repaintPickerView,
afteritemexpand: me.repaintPickerView,
afteritemcollapse: me.repaintPickerView
});
}
return picker;
},
/**
* repaints the tree view
*/
repaintPickerView: function() {
var style = this.picker.getView().getEl().dom.style;
// can't use Element.repaint because it contains a setTimeout, which results in a flicker effect
style.display = style.display;
},
/**
* Handles a click even on a tree node
* @private
* @param {Ext.tree.View} view
* @param {Ext.data.Model} record
* @param {HTMLElement} node
* @param {Number} rowIndex
* @param {Ext.event.Event} e
*/
onItemClick: function(view, record, node, rowIndex, e) {
this.selectItem(record);
},
/**
* Handles a keypress event on the picker element
* @private
* @param {Ext.tree.View} treeView
* @param {Ext.data.Model} record
* @param {HTMLElement} item
* @param {Number} index
* @param {Ext.event.Event} e
*/
onPickerKeyDown: function(treeView, record, item, index, e) {
var key = e.getKey();
if (key === e.ENTER || (key === e.TAB && this.selectOnTab)) {
this.selectItem(record);
}
},
/**
* Changes the selection to a given record and closes the picker
* @private
* @param {Ext.data.Model} record
*/
selectItem: function(record) {
var me = this;
me.setValue(record.getId());
me.fireEvent('select', me, record);
me.collapse();
},
/**
* Runs when the picker is expanded. Selects the appropriate tree node based on the value of the input element,
* and focuses the picker so that keyboard navigation will work.
* @private
*/
onExpand: function() {
var picker = this.picker,
store = picker.store,
value = this.value,
node;
if (value) {
node = store.getNodeById(value);
}
if (!node) {
node = store.getRoot();
}
picker.ensureVisible(node, {
select: true,
focus: true
});
},
/**
* Sets the specified value into the field
* @param {Mixed} value
* @return {Ext.ux.TreePicker} this
*/
setValue: function(value) {
var me = this,
record;
me.value = value;
if (me.store.loading) {
// Called while the Store is loading. Ensure it is processed by the onLoad method.
return me;
}
// try to find a record in the store that matches the value
record = value ? me.store.getNodeById(value) : me.store.getRoot();
if (value === undefined) {
record = me.store.getRoot();
me.value = record.getId();
} else {
record = me.store.getNodeById(value);
}
// set the raw value to the record's display field if a record was found
me.setRawValue(record ? record.get(me.displayField) : '');
return me;
},
getSubmitValue: function() {
return this.value;
},
/**
* Returns the current data value of the field (the idProperty of the record)
* @return {Number}
*/
getValue: function() {
return this.value;
},
/**
* Handles the store's load event.
* @private
*/
onLoad: function() {
var value = this.value;
if (value) {
this.setValue(value);
}
},
onUpdate: function(store, rec, type, modifiedFieldNames) {
var display = this.displayField;
if (type === 'edit' && modifiedFieldNames && Ext.Array.contains(modifiedFieldNames, display) && this.value === rec.getId()) {
this.setRawValue(rec.get(display));
}
}
});
/**
* @private
*/
Ext.define('Ext.ux.colorpick.Selection', {
mixinId: 'colorselection',
config: {
/**
* @cfg {"hex6"/"hex8"/"#hex6"/"#hex8"/"rgb"/"rgba"/"HEX6"/"HEX8"/"#HEX6"/"#HEX8"/"RGB"/"RGBA"} [format=hex6]
* The color format to for the `value` config. The `value` can be set using any
* supported format or named color, but the stored value will always be in this
* format.
*
* Supported formats are:
*
* - hex6 - For example "ffaa00" (Note: does not preserve transparency).
* - hex8 - For example "ffaa00ff" - the last 2 digits represent transparency
* - #hex6 - For example "#ffaa00" (same as "hex6" but with a leading "#").
* - #hex8 - For example "#ffaa00ff" (same as "hex8" but with a leading "#").
* - rgb - For example "rgb(255,255,0)" (Note: does not preserve transparency).
* - rgba - For example "rgba(255,255,0,.25)"
* - HEX6 - Same as "hex6" but upper case.
* - HEX8 - Same as "hex8" but upper case.
* - #HEX6 - Same as "#hex6" but upper case.
* - #HEX8 - Same as "#hex8" but upper case.
* - RGB - Same as "rgb" but upper case.
* - RGBA - Same as "rgba" but upper case.
*/
format: 'hex6',
/**
* @cfg {String} [value=FF0000]
* The initial color to highlight; see {@link #format} for supported formats.
*/
value: 'FF0000',
/**
* @cfg {Object} color
* This config property is used internally by the UI to maintain the full color.
* Changes to this config are automatically reflected in `value` and vise-versa.
* Setting `value` can, however, cause the alpha to be dropped if the new value
* does not contain an alpha component.
* @private
*/
color: null,
previousColor: null,
/**
* @cfg {String} [alphaDecimalFormat=#.##]
* The format used by {@link Ext.util.Format#number} to format the alpha channel's
* value.
* @since 7.0.0
*/
alphaDecimalFormat: '#.##'
},
applyColor: function(color) {
var c = color;
if (Ext.isString(c)) {
c = Ext.ux.colorpick.ColorUtils.parseColor(color, this.getAlphaDecimalFormat());
}
return c;
},
applyFormat: function(format) {
var formats = Ext.ux.colorpick.ColorUtils.formats;
if (!formats.hasOwnProperty(format)) {
//
Ext.raise('The specified format "' + format + '" is invalid.');
//
return;
}
return format;
},
applyValue: function(color) {
// Transform whatever incoming color we get to the proper format
var c = Ext.ux.colorpick.ColorUtils.parseColor(color || '#000000', this.getAlphaDecimalFormat());
return this.formatColor(c);
},
formatColor: function(color) {
return Ext.ux.colorpick.ColorUtils.formats[this.getFormat()](color);
},
updateColor: function(color) {
var me = this;
// If the "color" is changed (via internal changes in the UI), update "value" as
// well. Since these are always tracking each other, we guard against the case
// where we are being updated *because* "value" is being set.
if (!me.syncing) {
me.syncing = true;
me.setValue(me.formatColor(color));
me.syncing = false;
}
},
updateValue: function(value, oldValue) {
var me = this;
// If the "value" is changed, update "color" as well. Since these are always
// tracking each other, we guard against the case where we are being updated
// *because* "color" is being set.
if (!me.syncing) {
me.syncing = true;
me.setColor(value);
me.syncing = false;
}
this.fireEvent('change', me, value, oldValue);
}
});
/**
* @private
*/
Ext.define('Ext.ux.colorpick.ColorUtils', function(ColorUtils) {
var oldIE = Ext.isIE && Ext.ieVersion < 10;
return {
singleton: true,
constructor: function() {
ColorUtils = this;
},
backgroundTpl: oldIE ? 'filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, ' + 'startColorstr=\'#{alpha}{hex}\', endColorstr=\'#{alpha}{hex}\');' : 'background: {rgba};',
setBackground: oldIE ? function(el, color) {
if (el) {
var tpl = Ext.XTemplate.getTpl(ColorUtils, 'backgroundTpl'),
data = {
hex: ColorUtils.rgb2hex(color.r, color.g, color.b),
alpha: Math.floor(color.a * 255).toString(16)
},
bgStyle = tpl.apply(data);
el.applyStyles(bgStyle);
}
} : function(el, color) {
if (el) {
var tpl = Ext.XTemplate.getTpl(ColorUtils, 'backgroundTpl'),
data = {
rgba: ColorUtils.getRGBAString(color)
},
bgStyle = tpl.apply(data);
el.applyStyles(bgStyle);
}
},
// parse and format functions under objects that match supported format config
// values of the color picker; parse() methods receive the supplied color value
// as a string (i.e "FFAAAA") and return an object form, just like the one
// ColorPickerModel vm "selectedColor" uses. That same object form is used as a
// parameter to the format() methods, where the appropriate string form is expected
// for the return result
formats: {
// "RGB(100,100,100)"
RGB: function(colorO) {
return ColorUtils.getRGBString(colorO).toUpperCase();
},
// "RGBA(100,100,100,0.5)"
RGBA: function(colorO) {
return ColorUtils.getRGBAString(colorO).toUpperCase();
},
// "FFAA00"
HEX6: function(colorO) {
return ColorUtils.rgb2hex(colorO.r, colorO.g, colorO.b);
},
// "FFAA00FF" (last 2 are opacity)
HEX8: function(colorO) {
var hex = ColorUtils.rgb2hex(colorO.r, colorO.g, colorO.b),
opacityHex = Math.round(colorO.a * 255).toString(16);
if (opacityHex.length < 2) {
hex += '0';
}
hex += opacityHex.toUpperCase();
return hex;
}
},
hexRe: /^#?(([0-9a-f]{8})|((?:[0-9a-f]{3}){1,2}))$/i,
rgbaAltRe: /^rgba\(\s*([\w#\d]+)\s*,\s*([\d\.]+)\s*\)$/i,
rgbaRe: /^rgba\(\s*([\d\.]+)\s*,\s*([\d\.]+)\s*,\s*([\d\.]+)\s*,\s*([\d\.]+)\s*\)$/i,
rgbRe: /^rgb\(\s*([\d\.]+)\s*,\s*([\d\.]+)\s*,\s*([\d\.]+)\s*\)$/i,
/**
* Turn a string to a color object. Supports these formats:
*
* - "#ABC" (HEX short)
* - "#ABCDEF" (HEX)
* - "#ABCDEFDD" (HEX with opacity)
* - "red" (named colors - see
* [Web Colors](http://en.wikipedia.org/wiki/Web_colors) for a full list)
* - "rgba(r,g,b,a)" i.e "rgba(255,0,0,1)" (a == alpha == 0-1)
* - "rgba(red, 0.4)"
* - "rgba(#ABC, 0.9)"
* - "rgba(#ABCDEF, 0.8)"
*
* @param {String} color The color string to parse.
* @param {String} alphaFormat The format of decimal places for the Alpha channel.
* @return {Object} Object with various color properties.
* @return {Number} return.r The red component (0-255).
* @return {Number} return.g The green component (0-255).
* @return {Number} return.b The blue component (0-255).
* @return {Number} return.a The red component (0-1).
* @return {Number} return.h The hue component (0-1).
* @return {Number} return.s The saturation component (0-1).
* @return {Number} return.v The value component (0-1).
*/
parseColor: function(color, alphaFormat) {
if (!color) {
return null;
}
var me = this,
rgb = me.colorMap[color],
match, ret, hsv;
if (rgb) {
ret = {
r: rgb[0],
g: rgb[1],
b: rgb[2],
a: 1
};
} else if (color === 'transparent') {
ret = {
r: 0,
g: 0,
b: 0,
a: 0
};
} else {
match = me.hexRe.exec(color);
if (match) {
match = match[1];
// the captured hex
switch (match.length) {
default:
return null;
case 3:
ret = {
//double the number (e.g. 6 - > 66, a -> aa) and convert to decimal
r: parseInt(match[0] + match[0], 16),
g: parseInt(match[1] + match[1], 16),
b: parseInt(match[2] + match[2], 16),
a: 1
};
break;
case 6:
case 8:
ret = {
r: parseInt(match.substr(0, 2), 16),
g: parseInt(match.substr(2, 2), 16),
b: parseInt(match.substr(4, 2), 16),
a: parseInt(match.substr(6, 2) || 'ff', 16) / 255
};
break;
}
} else {
match = me.rgbaRe.exec(color);
if (match) {
// proper css => rgba(r,g,b,a)
ret = {
r: parseFloat(match[1]),
g: parseFloat(match[2]),
b: parseFloat(match[3]),
a: parseFloat(match[4])
};
} else {
match = me.rgbaAltRe.exec(color);
if (match) {
// scss shorthands =? rgba(red, 0.4), rgba(#222, 0.9), rgba(#444433, 0.8)
ret = me.parseColor(match[1]);
// we have HSV filled in, so poke on "a" and we're done
ret.a = parseFloat(match[2]);
return ret;
}
match = me.rgbRe.exec(color);
if (match) {
ret = {
r: parseFloat(match[1]),
g: parseFloat(match[2]),
b: parseFloat(match[3]),
a: 1
};
} else {
return null;
}
}
}
}
// format alpha channel
if (alphaFormat) {
ret.a = Ext.util.Format.number(ret.a, alphaFormat);
}
hsv = this.rgb2hsv(ret.r, ret.g, ret.b);
return Ext.apply(ret, hsv);
},
isValid: function(color) {
return ColorUtils.parseColor(color) !== null;
},
/**
*
* @param rgba
* @return {String}
*/
getRGBAString: function(rgba) {
return "rgba(" + rgba.r + "," + rgba.g + "," + rgba.b + "," + rgba.a + ")";
},
/**
* Returns a rgb css string whith this color (without the alpha channel)
* @param rgb
* @return {String}
*/
getRGBString: function(rgb) {
return "rgb(" + rgb.r + "," + rgb.g + "," + rgb.b + ")";
},
/**
* Following standard math to convert from hsl to rgb
* Check out wikipedia page for more information on how this works
* h => [0,1]
* s,l => [0,1]
* @param h
* @param s
* @param v
* @return {Object} An object with "r", "g" and "b" color properties.
*/
hsv2rgb: function(h, s, v) {
h = h * 360;
if (h === 360) {
h = 0;
}
var c = v * s;
var hprime = h / 60;
var x = c * (1 - Math.abs(hprime % 2 - 1));
var rgb = [
0,
0,
0
];
switch (Math.floor(hprime)) {
case 0:
rgb = [
c,
x,
0
];
break;
case 1:
rgb = [
x,
c,
0
];
break;
case 2:
rgb = [
0,
c,
x
];
break;
case 3:
rgb = [
0,
x,
c
];
break;
case 4:
rgb = [
x,
0,
c
];
break;
case 5:
rgb = [
c,
0,
x
];
break;
default:
//
console.error("unknown color " + h + ' ' + s + " " + v);
//
break;
}
var m = v - c;
rgb[0] += m;
rgb[1] += m;
rgb[2] += m;
rgb[0] = Math.round(rgb[0] * 255);
rgb[1] = Math.round(rgb[1] * 255);
rgb[2] = Math.round(rgb[2] * 255);
return {
r: rgb[0],
g: rgb[1],
b: rgb[2]
};
},
/**
* http://en.wikipedia.org/wiki/HSL_and_HSV
* @param {Number} r The red component (0-255).
* @param {Number} g The green component (0-255).
* @param {Number} b The blue component (0-255).
* @return {Object} An object with "h", "s" and "v" color properties.
*/
rgb2hsv: function(r, g, b) {
r = r / 255;
g = g / 255;
b = b / 255;
var M = Math.max(r, g, b);
var m = Math.min(r, g, b);
var c = M - m;
var hprime = 0;
if (c !== 0) {
if (M === r) {
hprime = ((g - b) / c) % 6;
} else if (M === g) {
hprime = ((b - r) / c) + 2;
} else if (M === b) {
hprime = ((r - g) / c) + 4;
}
}
var h = hprime * 60;
if (h === 360) {
h = 0;
}
var v = M;
var s = 0;
if (c !== 0) {
s = c / v;
}
h = h / 360;
if (h < 0) {
h = h + 1;
}
return {
h: h,
s: s,
v: v
};
},
/**
*
* @param r
* @param g
* @param b
* @return {String}
*/
rgb2hex: function(r, g, b) {
r = r.toString(16);
g = g.toString(16);
b = b.toString(16);
if (r.length < 2) {
r = '0' + r;
}
if (g.length < 2) {
g = '0' + g;
}
if (b.length < 2) {
b = '0' + b;
}
return (r + g + b).toUpperCase();
},
colorMap: {
aliceblue: [
240,
248,
255
],
antiquewhite: [
250,
235,
215
],
aqua: [
0,
255,
255
],
aquamarine: [
127,
255,
212
],
azure: [
240,
255,
255
],
beige: [
245,
245,
220
],
bisque: [
255,
228,
196
],
black: [
0,
0,
0
],
blanchedalmond: [
255,
235,
205
],
blue: [
0,
0,
255
],
blueviolet: [
138,
43,
226
],
brown: [
165,
42,
42
],
burlywood: [
222,
184,
135
],
cadetblue: [
95,
158,
160
],
chartreuse: [
127,
255,
0
],
chocolate: [
210,
105,
30
],
coral: [
255,
127,
80
],
cornflowerblue: [
100,
149,
237
],
cornsilk: [
255,
248,
220
],
crimson: [
220,
20,
60
],
cyan: [
0,
255,
255
],
darkblue: [
0,
0,
139
],
darkcyan: [
0,
139,
139
],
darkgoldenrod: [
184,
132,
11
],
darkgray: [
169,
169,
169
],
darkgreen: [
0,
100,
0
],
darkgrey: [
169,
169,
169
],
darkkhaki: [
189,
183,
107
],
darkmagenta: [
139,
0,
139
],
darkolivegreen: [
85,
107,
47
],
darkorange: [
255,
140,
0
],
darkorchid: [
153,
50,
204
],
darkred: [
139,
0,
0
],
darksalmon: [
233,
150,
122
],
darkseagreen: [
143,
188,
143
],
darkslateblue: [
72,
61,
139
],
darkslategray: [
47,
79,
79
],
darkslategrey: [
47,
79,
79
],
darkturquoise: [
0,
206,
209
],
darkviolet: [
148,
0,
211
],
deeppink: [
255,
20,
147
],
deepskyblue: [
0,
191,
255
],
dimgray: [
105,
105,
105
],
dimgrey: [
105,
105,
105
],
dodgerblue: [
30,
144,
255
],
firebrick: [
178,
34,
34
],
floralwhite: [
255,
255,
240
],
forestgreen: [
34,
139,
34
],
fuchsia: [
255,
0,
255
],
gainsboro: [
220,
220,
220
],
ghostwhite: [
248,
248,
255
],
gold: [
255,
215,
0
],
goldenrod: [
218,
165,
32
],
gray: [
128,
128,
128
],
green: [
0,
128,
0
],
greenyellow: [
173,
255,
47
],
grey: [
128,
128,
128
],
honeydew: [
240,
255,
240
],
hotpink: [
255,
105,
180
],
indianred: [
205,
92,
92
],
indigo: [
75,
0,
130
],
ivory: [
255,
255,
240
],
khaki: [
240,
230,
140
],
lavender: [
230,
230,
250
],
lavenderblush: [
255,
240,
245
],
lawngreen: [
124,
252,
0
],
lemonchiffon: [
255,
250,
205
],
lightblue: [
173,
216,
230
],
lightcoral: [
240,
128,
128
],
lightcyan: [
224,
255,
255
],
lightgoldenrodyellow: [
250,
250,
210
],
lightgray: [
211,
211,
211
],
lightgreen: [
144,
238,
144
],
lightgrey: [
211,
211,
211
],
lightpink: [
255,
182,
193
],
lightsalmon: [
255,
160,
122
],
lightseagreen: [
32,
178,
170
],
lightskyblue: [
135,
206,
250
],
lightslategray: [
119,
136,
153
],
lightslategrey: [
119,
136,
153
],
lightsteelblue: [
176,
196,
222
],
lightyellow: [
255,
255,
224
],
lime: [
0,
255,
0
],
limegreen: [
50,
205,
50
],
linen: [
250,
240,
230
],
magenta: [
255,
0,
255
],
maroon: [
128,
0,
0
],
mediumaquamarine: [
102,
205,
170
],
mediumblue: [
0,
0,
205
],
mediumorchid: [
186,
85,
211
],
mediumpurple: [
147,
112,
219
],
mediumseagreen: [
60,
179,
113
],
mediumslateblue: [
123,
104,
238
],
mediumspringgreen: [
0,
250,
154
],
mediumturquoise: [
72,
209,
204
],
mediumvioletred: [
199,
21,
133
],
midnightblue: [
25,
25,
112
],
mintcream: [
245,
255,
250
],
mistyrose: [
255,
228,
225
],
moccasin: [
255,
228,
181
],
navajowhite: [
255,
222,
173
],
navy: [
0,
0,
128
],
oldlace: [
253,
245,
230
],
olive: [
128,
128,
0
],
olivedrab: [
107,
142,
35
],
orange: [
255,
165,
0
],
orangered: [
255,
69,
0
],
orchid: [
218,
112,
214
],
palegoldenrod: [
238,
232,
170
],
palegreen: [
152,
251,
152
],
paleturquoise: [
175,
238,
238
],
palevioletred: [
219,
112,
147
],
papayawhip: [
255,
239,
213
],
peachpuff: [
255,
218,
185
],
peru: [
205,
133,
63
],
pink: [
255,
192,
203
],
plum: [
221,
160,
203
],
powderblue: [
176,
224,
230
],
purple: [
128,
0,
128
],
red: [
255,
0,
0
],
rosybrown: [
188,
143,
143
],
royalblue: [
65,
105,
225
],
saddlebrown: [
139,
69,
19
],
salmon: [
250,
128,
114
],
sandybrown: [
244,
164,
96
],
seagreen: [
46,
139,
87
],
seashell: [
255,
245,
238
],
sienna: [
160,
82,
45
],
silver: [
192,
192,
192
],
skyblue: [
135,
206,
235
],
slateblue: [
106,
90,
205
],
slategray: [
119,
128,
144
],
slategrey: [
119,
128,
144
],
snow: [
255,
255,
250
],
springgreen: [
0,
255,
127
],
steelblue: [
70,
130,
180
],
tan: [
210,
180,
140
],
teal: [
0,
128,
128
],
thistle: [
216,
191,
216
],
tomato: [
255,
99,
71
],
turquoise: [
64,
224,
208
],
violet: [
238,
130,
238
],
wheat: [
245,
222,
179
],
white: [
255,
255,
255
],
whitesmoke: [
245,
245,
245
],
yellow: [
255,
255,
0
],
yellowgreen: [
154,
205,
5
]
}
};
}, function(ColorUtils) {
var formats = ColorUtils.formats,
lowerized = {};
formats['#HEX6'] = function(color) {
return '#' + formats.HEX6(color);
};
formats['#HEX8'] = function(color) {
return '#' + formats.HEX8(color);
};
Ext.Object.each(formats, function(name, fn) {
lowerized[name.toLowerCase()] = function(color) {
var ret = fn(color);
return ret.toLowerCase();
};
});
Ext.apply(formats, lowerized);
});
/**
* @private
*/
Ext.define('Ext.ux.colorpick.ColorMapController', {
extend: 'Ext.app.ViewController',
alias: 'controller.colorpickercolormapcontroller',
requires: [
'Ext.ux.colorpick.ColorUtils'
],
// After the component is rendered
onFirstBoxReady: function() {
var me = this,
colorMap = me.getView(),
dragHandle = colorMap.down('#dragHandle'),
dd = dragHandle.dd;
// configure draggable constraints
dd.constrain = true;
dd.constrainTo = colorMap.getEl();
dd.initialConstrainTo = dd.constrainTo;
// needed otheriwse error EXTJS-13187
// event handlers
dd.on('drag', Ext.bind(me.onHandleDrag, me));
me.mon(colorMap.getEl(), {
mousedown: me.onMouseDown,
dragstart: me.onDragStart,
scope: me
});
},
// Fires when handle is dragged; propagates "handledrag" event on the ColorMap
// with parameters "percentX" and "percentY", both 0-1, representing the handle
// position on the color map, relative to the container
onHandleDrag: function(componentDragger, e) {
var me = this,
container = me.getView(),
// the Color Map
dragHandle = container.down('#dragHandle'),
x = dragHandle.getX() - container.getX(),
y = dragHandle.getY() - container.getY(),
containerEl = container.getEl(),
containerWidth = containerEl.getWidth(),
containerHeight = containerEl.getHeight(),
xRatio = x / containerWidth,
yRatio = y / containerHeight;
// Adjust x/y ratios for dragger always being 1 pixel from the edge on the right
if (xRatio > 0.99) {
xRatio = 1;
}
if (yRatio > 0.99) {
yRatio = 1;
}
container.fireEvent('handledrag', xRatio, yRatio);
},
// Whenever we mousedown over the colormap area
onMouseDown: function(e) {
var me = this,
container = me.getView(),
dragHandle = container.down('#dragHandle');
// position drag handle accordingly
dragHandle.setY(e.getY());
dragHandle.setX(e.getX());
me.onHandleDrag();
// tie into the default dd mechanism
dragHandle.dd.onMouseDown(e, dragHandle.dd.el);
},
// Whenever we start a drag over the colormap area
onDragStart: function(e) {
var me = this,
container = me.getView(),
dragHandle = container.down('#dragHandle');
// tie into the default dd mechanism
dragHandle.dd.onDragStart(e, dragHandle.dd.el);
},
// Whenever the map is clicked (but not the drag handle) we need to position
// the drag handle to the point of click
onMapClick: function(e) {
var me = this,
container = me.getView(),
// the Color Map
dragHandle = container.down('#dragHandle'),
cXY = container.getXY(),
eXY = e.getXY(),
left, top;
left = eXY[0] - cXY[0];
top = eXY[1] - cXY[1];
dragHandle.getEl().setStyle({
left: left + 'px',
top: top + 'px'
});
me.onHandleDrag();
},
// Whenever the underlying binding data is changed we need to
// update position of the dragger; active drag state has been
// accounted for earlier
onColorBindingChanged: function(selectedColor) {
var me = this,
vm = me.getViewModel(),
rgba = vm.get('selectedColor'),
hsv,
container = me.getView(),
// the Color Map
dragHandle = container.down('#dragHandle'),
containerEl = container.getEl(),
containerWidth = containerEl.getWidth(),
containerHeight = containerEl.getHeight(),
xRatio, yRatio, left, top;
// Color map selection really only depends on saturation and value of the color
hsv = Ext.ux.colorpick.ColorUtils.rgb2hsv(rgba.r, rgba.g, rgba.b);
// x-axis of color map with value 0-1 translates to saturation
xRatio = hsv.s;
left = containerWidth * xRatio;
// y-axis of color map with value 0-1 translates to reverse of "value"
yRatio = 1 - hsv.v;
top = containerHeight * yRatio;
// Position dragger
dragHandle.getEl().setStyle({
left: left + 'px',
top: top + 'px'
});
},
// Whenever only Hue changes we can update the
// background color of the color map
// Param "hue" has value of 0-1
onHueBindingChanged: function(hue) {
var me = this,
vm = me.getViewModel(),
fullColorRGB, hex;
fullColorRGB = Ext.ux.colorpick.ColorUtils.hsv2rgb(hue, 1, 1);
hex = Ext.ux.colorpick.ColorUtils.rgb2hex(fullColorRGB.r, fullColorRGB.g, fullColorRGB.b);
me.getView().getEl().applyStyles({
'background-color': '#' + hex
});
}
});
/**
* The main colorful square for selecting color shades by dragging around the
* little circle.
* @private
*/
Ext.define('Ext.ux.colorpick.ColorMap', {
extend: 'Ext.container.Container',
alias: 'widget.colorpickercolormap',
controller: 'colorpickercolormapcontroller',
requires: [
'Ext.ux.colorpick.ColorMapController'
],
cls: Ext.baseCSSPrefix + 'colorpicker-colormap',
// This is the drag "circle"; note it's 1x1 in size to allow full
// travel around the color map; the inner div has the bigger image
items: [
{
xtype: 'component',
cls: Ext.baseCSSPrefix + 'colorpicker-colormap-draghandle-container',
itemId: 'dragHandle',
width: 1,
height: 1,
draggable: true,
html: ''
}
],
listeners: {
boxready: {
single: true,
fn: 'onFirstBoxReady',
scope: 'controller'
},
colorbindingchanged: {
fn: 'onColorBindingChanged',
scope: 'controller'
},
huebindingchanged: {
fn: 'onHueBindingChanged',
scope: 'controller'
}
},
afterRender: function() {
var me = this,
src = me.mapGradientUrl,
el = me.el;
me.callParent();
if (!src) {
// We do this trick to allow the Sass to calculate resource image path for
// our package and pick up the proper image URL here.
src = el.getStyle('background-image');
src = src.substring(4, src.length - 1);
// strip off outer "url(...)"
// In IE8 this path will have quotes around it
if (src.indexOf('"') === 0) {
src = src.substring(1, src.length - 1);
}
// Then remember it on our prototype for any subsequent instances.
Ext.ux.colorpick.ColorMap.prototype.mapGradientUrl = src;
}
// Now clear that style because it will conflict with the background-color
el.setStyle('background-image', 'none');
// Create the image with transparent PNG with black and white gradient shades;
// it blends with the background color (which changes with hue selection). This
// must be an IMG in order to properly stretch to fit.
el = me.layout.getElementTarget();
// the el for items and html
el.createChild({
tag: 'img',
cls: Ext.baseCSSPrefix + 'colorpicker-colormap-blender',
src: src
});
},
// Called via data binding whenever selectedColor changes; fires "colorbindingchanged"
setPosition: function(data) {
var me = this,
dragHandle = me.down('#dragHandle');
// Too early in the render cycle? Skip event
if (!dragHandle.dd || !dragHandle.dd.constrain) {
return;
}
// User actively dragging? Skip event
if (typeof dragHandle.dd.dragEnded !== 'undefined' && !dragHandle.dd.dragEnded) {
return;
}
me.fireEvent('colorbindingchanged', data);
},
// Called via data binding whenever selectedColor.h changes; fires "huebindingchanged" event
setHue: function(hue) {
var me = this;
// Too early in the render cycle? Skip event
if (!me.getEl()) {
return;
}
me.fireEvent('huebindingchanged', hue);
}
});
/**
* View Model that holds the "selectedColor" of the color picker container.
*/
Ext.define('Ext.ux.colorpick.SelectorModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.colorpick-selectormodel',
requires: [
'Ext.ux.colorpick.ColorUtils'
],
data: {
selectedColor: {
r: 255,
// red
g: 255,
// green
b: 255,
// blue
h: 0,
// hue,
s: 1,
// saturation
v: 1,
// value
a: 1
},
// alpha (opacity)
previousColor: {
r: 0,
// red
g: 0,
// green
b: 0,
// blue
h: 0,
// hue,
s: 1,
// saturation
v: 1,
// value
a: 1
}
},
// alpha (opacity)
formulas: {
// Hexadecimal representation of the color
hex: {
get: function(get) {
var r = get('selectedColor.r').toString(16),
g = get('selectedColor.g').toString(16),
b = get('selectedColor.b').toString(16),
result;
result = Ext.ux.colorpick.ColorUtils.rgb2hex(r, g, b);
return '#' + result;
},
set: function(hex) {
var rgb = Ext.ux.colorpick.ColorUtils.parseColor(hex);
this.changeRGB(rgb);
}
},
// "R" in "RGB"
red: {
get: function(get) {
return get('selectedColor.r');
},
set: function(r) {
this.changeRGB({
r: r
});
}
},
// "G" in "RGB"
green: {
get: function(get) {
return get('selectedColor.g');
},
set: function(g) {
this.changeRGB({
g: g
});
}
},
// "B" in "RGB"
blue: {
get: function(get) {
return get('selectedColor.b');
},
set: function(b) {
this.changeRGB({
b: b
});
}
},
// "H" in HSV
hue: {
get: function(get) {
return get('selectedColor.h') * 360;
},
set: function(hue) {
this.changeHSV({
h: hue / 360
});
}
},
// "S" in HSV
saturation: {
get: function(get) {
return get('selectedColor.s') * 100;
},
set: function(saturation) {
this.changeHSV({
s: saturation / 100
});
}
},
// "V" in HSV
value: {
get: function(get) {
var v = get('selectedColor.v');
return v * 100;
},
set: function(value) {
this.changeHSV({
v: value / 100
});
}
},
alpha: {
get: function(data) {
var a = data('selectedColor.a');
return a * 100;
},
set: function(alpha) {
this.set('selectedColor', Ext.applyIf({
a: alpha / 100
}, this.data.selectedColor));
}
}
},
// formulas
changeHSV: function(hsv) {
Ext.applyIf(hsv, this.data.selectedColor);
var rgb = Ext.ux.colorpick.ColorUtils.hsv2rgb(hsv.h, hsv.s, hsv.v);
hsv.r = rgb.r;
hsv.g = rgb.g;
hsv.b = rgb.b;
this.set('selectedColor', hsv);
},
changeRGB: function(rgb) {
Ext.applyIf(rgb, this.data.selectedColor);
var hsv = Ext.ux.colorpick.ColorUtils.rgb2hsv(rgb.r, rgb.g, rgb.b);
rgb.h = hsv.h;
rgb.s = hsv.s;
rgb.v = hsv.v;
this.set('selectedColor', rgb);
}
});
/**
* @private
*/
Ext.define('Ext.ux.colorpick.SelectorController', {
extend: 'Ext.app.ViewController',
alias: 'controller.colorpick-selectorcontroller',
requires: [
'Ext.ux.colorpick.ColorUtils'
],
destroy: function() {
var me = this,
view = me.getView(),
childViewModel = view.childViewModel;
if (childViewModel) {
childViewModel.destroy();
view.childViewModel = null;
}
me.callParent();
},
changeHSV: function(hsv) {
var view = this.getView(),
color = view.getColor(),
rgb;
// Put in values we are not changing (like A, but also missing HSV values)
Ext.applyIf(hsv, color);
// Now that HSV is complete, recalculate RGB and combine them
rgb = Ext.ux.colorpick.ColorUtils.hsv2rgb(hsv.h, hsv.s, hsv.v);
Ext.apply(hsv, rgb);
view.setColor(hsv);
},
// Updates Saturation/Value in the model based on ColorMap; params:
// xPercent - where is the handle relative to the color map width
// yPercent - where is the handle relative to the color map height
onColorMapHandleDrag: function(xPercent, yPercent) {
this.changeHSV({
s: xPercent,
v: 1 - yPercent
});
},
// Updates HSV Value in the model and downstream RGB settings
onValueSliderHandleDrag: function(yPercent) {
this.changeHSV({
v: 1 - yPercent
});
},
// Updates HSV Saturation in the model and downstream RGB settings
onSaturationSliderHandleDrag: function(yPercent) {
this.changeHSV({
s: 1 - yPercent
});
},
// Updates Hue in the model and downstream RGB settings
onHueSliderHandleDrag: function(yPercent) {
this.changeHSV({
h: 1 - yPercent
});
},
onAlphaSliderHandleDrag: function(yPercent) {
var view = this.getView(),
color = view.getColor(),
newColor = Ext.applyIf({
a: 1 - yPercent
}, color);
view.setColor(newColor);
view.el.repaint();
},
onPreviousColorSelected: function(comp, color) {
var view = this.getView();
view.setColor(color);
},
onOK: function() {
var me = this,
view = me.getView();
view.fireEvent('ok', view, view.getValue());
},
onCancel: function() {
this.fireViewEvent('cancel', this.getView());
},
onResize: function() {
var me = this,
view = me.getView(),
vm = view.childViewModel,
refs = me.getReferences(),
h, s, v, a;
// Skip initial rendering resize
if (!me.hasResizedOnce) {
me.hasResizedOnce = true;
return;
}
h = vm.get('hue');
s = vm.get('saturation');
v = vm.get('value');
a = vm.get('alpha');
// Reposition the colormap's & sliders' drag handles
refs.colorMap.setPosition(vm.getData());
refs.hueSlider.setHue(h);
refs.satSlider.setSaturation(s);
refs.valueSlider.setValue(v);
refs.alphaSlider.setAlpha(a);
}
});
/**
* A basic component that changes background color, with considerations for opacity
* support (checkered background image and IE8 support).
*/
Ext.define('Ext.ux.colorpick.ColorPreview', {
extend: 'Ext.Component',
alias: 'widget.colorpickercolorpreview',
requires: [
'Ext.util.Format',
'Ext.XTemplate'
],
//hack to solve issue with IE, when applying a filter the click listener is not being fired.
style: 'position: relative',
html: '' + '',
//eo hack
cls: Ext.baseCSSPrefix + 'colorpreview',
height: 256,
onRender: function() {
var me = this;
me.callParent(arguments);
me.mon(me.el.down('.btn'), 'click', me.onClick, me);
},
onClick: function() {
this.fireEvent('click', this, this.color);
},
// Called via databinding - update background color whenever ViewModel changes
setColor: function(color) {
var me = this,
el = me.getEl();
// Too early in rendering cycle; skip
if (!el) {
return;
}
me.color = color;
me.applyBgStyle(color);
},
bgStyleTpl: Ext.create('Ext.XTemplate', Ext.isIE && Ext.ieVersion < 10 ? 'filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=\'#{hexAlpha}{hex}\', endColorstr=\'#{hexAlpha}{hex}\');' : /* IE6-9 */
'background: {rgba};'),
applyBgStyle: function(color) {
var me = this,
colorUtils = Ext.ux.colorpick.ColorUtils,
filterSelector = '.' + Ext.baseCSSPrefix + 'colorpreview-filter',
el = me.getEl().down(filterSelector),
hex, alpha, rgba, bgStyle;
hex = colorUtils.rgb2hex(color.r, color.g, color.b);
alpha = Ext.util.Format.hex(Math.floor(color.a * 255), 2);
rgba = colorUtils.getRGBAString(color);
bgStyle = this.bgStyleTpl.apply({
hex: hex,
hexAlpha: alpha,
rgba: rgba
});
el.applyStyles(bgStyle);
}
});
/**
* @private
*/
Ext.define('Ext.ux.colorpick.SliderController', {
extend: 'Ext.app.ViewController',
alias: 'controller.colorpick-slidercontroller',
// After the component is rendered
boxReady: function(view) {
var me = this,
container = me.getDragContainer(),
dragHandle = me.getDragHandle(),
dd = dragHandle.dd;
// configure draggable constraints
dd.constrain = true;
dd.constrainTo = container.getEl();
dd.initialConstrainTo = dd.constrainTo;
// needed otherwise error EXTJS-13187
// event handlers
dd.on('drag', me.onHandleDrag, me);
},
getDragHandle: function() {
return this.view.lookupReference('dragHandle');
},
getDragContainer: function() {
return this.view.lookupReference('dragHandleContainer');
},
// Fires when handle is dragged; fires "handledrag" event on the slider
// with parameter "percentY" 0-1, representing the handle position on the slider
// relative to the height
onHandleDrag: function(e) {
var me = this,
view = me.getView(),
container = me.getDragContainer(),
dragHandle = me.getDragHandle(),
y = dragHandle.getY() - container.getY(),
containerEl = container.getEl(),
containerHeight = containerEl.getHeight(),
yRatio = y / containerHeight;
// Adjust y ratio for dragger always being 1 pixel from the edge on the bottom
if (yRatio > 0.99) {
yRatio = 1;
}
view.fireEvent('handledrag', yRatio);
},
// Whenever we mousedown over the slider area
onMouseDown: function(e) {
var me = this,
dragHandle = me.getDragHandle(),
y = e.getY();
// position drag handle accordingly
dragHandle.setY(y);
me.onHandleDrag();
dragHandle.el.repaint();
// tie into the default dd mechanism
dragHandle.dd.onMouseDown(e, dragHandle.dd.el);
},
// Whenever we start a drag over the colormap area
onDragStart: function(e) {
var me = this,
dragHandle = me.getDragHandle();
// tie into the default dd mechanism
dragHandle.dd.onDragStart(e, dragHandle.dd.el);
},
onMouseUp: function() {
var dragHandle = this.getDragHandle();
dragHandle.dd.dragEnded = true;
}
});
// work around DragTracker bug
/**
* Parent view for the 4 sliders seen on the color picker window.
* @private
*/
Ext.define('Ext.ux.colorpick.Slider', {
extend: 'Ext.container.Container',
xtype: 'colorpickerslider',
controller: 'colorpick-slidercontroller',
afterRender: function() {
this.callParent(arguments);
var width = this.width,
dragCt = this.lookupReference('dragHandleContainer'),
dragWidth = dragCt.getWidth();
dragCt.el.setStyle('left', ((width - dragWidth) / 2) + 'px');
},
baseCls: Ext.baseCSSPrefix + 'colorpicker-slider',
requires: [
'Ext.ux.colorpick.SliderController'
],
referenceHolder: true,
listeners: {
element: 'el',
mousedown: 'onMouseDown',
mouseup: 'onMouseUp',
dragstart: 'onDragStart'
},
// Container for the drag handle; needed since the slider
// is of static size, while the parent container positions
// it in the center; this is what receives the beautiful
// color gradients for the visual
items: {
xtype: 'container',
cls: Ext.baseCSSPrefix + 'colorpicker-draghandle-container',
reference: 'dragHandleContainer',
height: '100%',
// This is the drag handle; note it's 100%x1 in size to allow full
// vertical drag travel; the inner div has the bigger image
items: {
xtype: 'component',
cls: Ext.baseCSSPrefix + 'colorpicker-draghandle-outer',
reference: 'dragHandle',
width: '100%',
height: 1,
draggable: true,
html: ''
}
},
//
// Called via data binding whenever selectedColor.h changes;
setHue: function() {
Ext.raise('Must implement setHue() in a child class!');
},
//
getDragHandle: function() {
return this.lookupReference('dragHandle');
},
getDragContainer: function() {
return this.lookupReference('dragHandleContainer');
}
});
/**
* Used for "Alpha" slider.
* @private
*/
Ext.define('Ext.ux.colorpick.SliderAlpha', {
extend: 'Ext.ux.colorpick.Slider',
alias: 'widget.colorpickerslideralpha',
cls: Ext.baseCSSPrefix + 'colorpicker-alpha',
requires: [
'Ext.XTemplate'
],
gradientStyleTpl: Ext.create('Ext.XTemplate', Ext.isIE && Ext.ieVersion < 10 ? 'filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=\'#FF{hex}\', endColorstr=\'#00{hex}\');' : /* IE6-9 */
'background: -moz-linear-gradient(top, rgba({r}, {g}, {b}, 1) 0%, rgba({r}, {g}, {b}, 0) 100%);' + /* FF3.6+ */
'background: -webkit-linear-gradient(top,rgba({r}, {g}, {b}, 1) 0%, rgba({r}, {g}, {b}, 0) 100%);' + /* Chrome10+,Safari5.1+ */
'background: -o-linear-gradient(top, rgba({r}, {g}, {b}, 1) 0%, rgba({r}, {g}, {b}, 0) 100%);' + /* Opera 11.10+ */
'background: -ms-linear-gradient(top, rgba({r}, {g}, {b}, 1) 0%, rgba({r}, {g}, {b}, 0) 100%);' + /* IE10+ */
'background: linear-gradient(to bottom, rgba({r}, {g}, {b}, 1) 0%, rgba({r}, {g}, {b}, 0) 100%);'),
/* W3C */
// Called via data binding whenever selectedColor.a changes; param is 0-100
setAlpha: function(value) {
var me = this,
container = me.getDragContainer(),
dragHandle = me.getDragHandle(),
containerEl = container.getEl(),
containerHeight = containerEl.getHeight(),
el, top;
// Too early in the render cycle? Skip event
if (!dragHandle.dd || !dragHandle.dd.constrain) {
return;
}
// User actively dragging? Skip event
if (typeof dragHandle.dd.dragEnded !== 'undefined' && !dragHandle.dd.dragEnded) {
return;
}
// y-axis of slider with value 0-1 translates to reverse of "value"
top = containerHeight * (1 - (value / 100));
// Position dragger
el = dragHandle.getEl();
el.setStyle({
top: top + 'px'
});
},
// Called via data binding whenever selectedColor.h changes; hue param is 0-1
setColor: function(color) {
var me = this,
container = me.getDragContainer(),
hex, el;
// Too early in the render cycle? Skip event
if (!me.getEl()) {
return;
}
// Determine HEX for new hue and set as background based on template
hex = Ext.ux.colorpick.ColorUtils.rgb2hex(color.r, color.g, color.b);
el = container.getEl().first();
el.applyStyles(me.gradientStyleTpl.apply({
hex: hex,
r: color.r,
g: color.g,
b: color.b
}));
}
});
/**
* Used for "Saturation" slider
* @private
*/
Ext.define('Ext.ux.colorpick.SliderSaturation', {
extend: 'Ext.ux.colorpick.Slider',
alias: 'widget.colorpickerslidersaturation',
cls: Ext.baseCSSPrefix + 'colorpicker-saturation',
gradientStyleTpl: Ext.create('Ext.XTemplate', Ext.isIE && Ext.ieVersion < 10 ? 'filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=\'#{hex}\', endColorstr=\'#ffffff\');' : /* IE6-9 */
'background: -mox-linear-gradient(top, #{hex} 0%, #ffffff 100%);' + /* FF3.6+ */
'background: -webkit-linear-gradient(top, #{hex} 0%,#ffffff 100%);' + /* Chrome10+,Safari5.1+ */
'background: -o-linear-gradient(top, #{hex} 0%,#ffffff 100%);' + /* Opera 11.10+ */
'background: -ms-linear-gradient(top, #{hex} 0%,#ffffff 100%);' + /* IE10+ */
'background: linear-gradient(to bottom, #{hex} 0%,#ffffff 100%);'),
/* W3C */
// Called via data binding whenever selectedColor.s changes; saturation param is 0-100
setSaturation: function(saturation) {
var me = this,
container = me.getDragContainer(),
dragHandle = me.getDragHandle(),
containerEl = container.getEl(),
containerHeight = containerEl.getHeight(),
yRatio, top;
// Too early in the render cycle? Skip event
if (!dragHandle.dd || !dragHandle.dd.constrain) {
return;
}
// User actively dragging? Skip event
if (typeof dragHandle.dd.dragEnded !== 'undefined' && !dragHandle.dd.dragEnded) {
return;
}
// y-axis of slider with value 0-1 translates to reverse of "saturation"
yRatio = 1 - (saturation / 100);
top = containerHeight * yRatio;
// Position dragger
dragHandle.getEl().setStyle({
top: top + 'px'
});
},
// Called via data binding whenever selectedColor.h changes; hue param is 0-1
setHue: function(hue) {
var me = this,
container = me.getDragContainer(),
rgb, hex;
// Too early in the render cycle? Skip event
if (!me.getEl()) {
return;
}
// Determine HEX for new hue and set as background based on template
rgb = Ext.ux.colorpick.ColorUtils.hsv2rgb(hue, 1, 1);
hex = Ext.ux.colorpick.ColorUtils.rgb2hex(rgb.r, rgb.g, rgb.b);
container.getEl().applyStyles(me.gradientStyleTpl.apply({
hex: hex
}));
}
});
/**
* Used for "Value" slider.
* @private
*/
Ext.define('Ext.ux.colorpick.SliderValue', {
extend: 'Ext.ux.colorpick.Slider',
alias: 'widget.colorpickerslidervalue',
cls: Ext.baseCSSPrefix + 'colorpicker-value',
requires: [
'Ext.XTemplate'
],
gradientStyleTpl: Ext.create('Ext.XTemplate', Ext.isIE && Ext.ieVersion < 10 ? 'filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=\'#{hex}\', endColorstr=\'#000000\');' : /* IE6-9 */
'background: -mox-linear-gradient(top, #{hex} 0%, #000000 100%);' + /* FF3.6+ */
'background: -webkit-linear-gradient(top, #{hex} 0%,#000000 100%);' + /* Chrome10+,Safari5.1+ */
'background: -o-linear-gradient(top, #{hex} 0%,#000000 100%);' + /* Opera 11.10+ */
'background: -ms-linear-gradient(top, #{hex} 0%,#000000 100%);' + /* IE10+ */
'background: linear-gradient(to bottom, #{hex} 0%,#000000 100%);'),
/* W3C */
// Called via data binding whenever selectedColor.v changes; value param is 0-100
setValue: function(value) {
var me = this,
container = me.getDragContainer(),
dragHandle = me.getDragHandle(),
containerEl = container.getEl(),
containerHeight = containerEl.getHeight(),
yRatio, top;
// Too early in the render cycle? Skip event
if (!dragHandle.dd || !dragHandle.dd.constrain) {
return;
}
// User actively dragging? Skip event
if (typeof dragHandle.dd.dragEnded !== 'undefined' && !dragHandle.dd.dragEnded) {
return;
}
// y-axis of slider with value 0-1 translates to reverse of "value"
yRatio = 1 - (value / 100);
top = containerHeight * yRatio;
// Position dragger
dragHandle.getEl().setStyle({
top: top + 'px'
});
},
// Called via data binding whenever selectedColor.h changes; hue param is 0-1
setHue: function(hue) {
var me = this,
container = me.getDragContainer(),
rgb, hex;
// Too early in the render cycle? Skip event
if (!me.getEl()) {
return;
}
// Determine HEX for new hue and set as background based on template
rgb = Ext.ux.colorpick.ColorUtils.hsv2rgb(hue, 1, 1);
hex = Ext.ux.colorpick.ColorUtils.rgb2hex(rgb.r, rgb.g, rgb.b);
container.getEl().applyStyles(me.gradientStyleTpl.apply({
hex: hex
}));
}
});
/**
* Used for "Hue" slider.
* @private
*/
Ext.define('Ext.ux.colorpick.SliderHue', {
extend: 'Ext.ux.colorpick.Slider',
alias: 'widget.colorpickersliderhue',
cls: Ext.baseCSSPrefix + 'colorpicker-hue',
afterRender: function() {
var me = this,
src = me.gradientUrl,
el = me.el;
me.callParent();
if (!src) {
// We do this trick to allow the Sass to calculate resource image path for
// our package and pick up the proper image URL here.
src = el.getStyle('background-image');
src = src.substring(4, src.length - 1);
// strip off outer "url(...)"
// In IE8 this path will have quotes around it
if (src.indexOf('"') === 0) {
src = src.substring(1, src.length - 1);
}
// Then remember it on our prototype for any subsequent instances.
Ext.ux.colorpick.SliderHue.prototype.gradientUrl = src;
}
// Now clear that style because it will conflict with the background-color
el.setStyle('background-image', 'none');
// Create the image with the background PNG
el = me.getDragContainer().layout.getElementTarget();
// the el for items and html
el.createChild({
tag: 'img',
cls: Ext.baseCSSPrefix + 'colorpicker-hue-gradient',
src: src
});
},
// Called via data binding whenever selectedColor.h changes; hue param is 0-1
setHue: function(hue) {
var me = this,
container = me.getDragContainer(),
dragHandle = me.getDragHandle(),
containerEl = container.getEl(),
containerHeight = containerEl.getHeight(),
el, top;
// Too early in the render cycle? Skip event
if (!dragHandle.dd || !dragHandle.dd.constrain) {
return;
}
// User actively dragging? Skip event
if (typeof dragHandle.dd.dragEnded !== 'undefined' && !dragHandle.dd.dragEnded) {
return;
}
// y-axis of slider with value 0-1 translates to reverse of "hue"
top = containerHeight * (1 - hue);
// Position dragger
el = dragHandle.getEl();
el.setStyle({
top: top + 'px'
});
}
});
/**
* Sencha Pro Services presents xtype "colorselector".
* API has been kept as close to the regular colorpicker as possible. The Selector can be
* rendered to any container.
*
* The defaul selected color is configurable via {@link #value} config. Usually used in
* forms via {@link Ext.ux.colorpick.Button} or {@link Ext.ux.colorpick.Field}.
*
* Typically you will need to listen for the change event to be notified when the user
* chooses a color. Alternatively, you can bind to the "value" config
*
* @example
* Ext.create('Ext.ux.colorpick.Selector', {
* value : '993300', // initial selected color
* renderTo : Ext.getBody(),
* listeners: {
* change: function (colorselector, color) {
* console.log('New color: ' + color);
* }
* }
* });
*/
Ext.define('Ext.ux.colorpick.Selector', {
extend: 'Ext.container.Container',
xtype: 'colorselector',
mixins: [
'Ext.ux.colorpick.Selection'
],
controller: 'colorpick-selectorcontroller',
requires: [
'Ext.layout.container.HBox',
'Ext.form.field.Text',
'Ext.form.field.Number',
'Ext.ux.colorpick.ColorMap',
'Ext.ux.colorpick.SelectorModel',
'Ext.ux.colorpick.SelectorController',
'Ext.ux.colorpick.ColorPreview',
'Ext.ux.colorpick.Slider',
'Ext.ux.colorpick.SliderAlpha',
'Ext.ux.colorpick.SliderSaturation',
'Ext.ux.colorpick.SliderValue',
'Ext.ux.colorpick.SliderHue'
],
config: {
hexReadOnly: true
},
width: 580,
// default width and height gives 255x255 color map in Crisp
height: 337,
cls: Ext.baseCSSPrefix + 'colorpicker',
padding: 10,
layout: {
type: 'hbox',
align: 'stretch'
},
defaultBindProperty: 'value',
twoWayBindable: [
'value'
],
/**
* @cfg fieldWidth {Number} Width of the text fields on the container (excluding HEX);
* since the width of the slider containers is the same as the text field under it
* (it's the same vbox column), changing this value will also affect the spacing between
* the sliders.
*/
fieldWidth: 50,
/**
* @cfg fieldPad {Number} padding between the sliders and HEX/R/G/B fields.
*/
fieldPad: 5,
/**
* @cfg {Boolean} [showPreviousColor]
* Whether "previous color" region (in upper right, below the selected color preview) should be shown;
* these are relied upon by the {@link Ext.ux.colorpick.Button} and the {@link Ext.ux.colorpick.Field}.
*/
showPreviousColor: false,
/**
* @cfg {Boolean} [showOkCancelButtons]
* Whether Ok and Cancel buttons (in upper right, below the selected color preview) should be shown;
* these are relied upon by the {@link Ext.ux.colorpick.Button} and the {@link Ext.ux.colorpick.Field}.
*/
showOkCancelButtons: false,
/**
* @event change
* Fires when a color is selected. Simply dragging sliders around will trigger this.
* @param {Ext.ux.colorpick.Selector} this
* @param {String} color The value of the selected color as per specified {@link #format}.
* @param {String} previousColor The previous color value.
*/
/**
* @event ok
* Fires when OK button is clicked (see {@link #showOkCancelButtons}).
* @param {Ext.ux.colorpick.Selector} this
* @param {String} color The value of the selected color as per specified {@link #format}.
*/
/**
* @event cancel
* Fires when Cancel button is clicked (see {@link #showOkCancelButtons}).
* @param {Ext.ux.colorpick.Selector} this
*/
listeners: {
resize: 'onResize'
},
constructor: function(config) {
var me = this,
childViewModel = Ext.Factory.viewModel('colorpick-selectormodel');
// Since this component needs to present its value as a thing to which users can
// bind, we create an internal VM for our purposes.
me.childViewModel = childViewModel;
me.items = [
me.getMapAndHexRGBFields(childViewModel),
me.getSliderAndHField(childViewModel),
me.getSliderAndSField(childViewModel),
me.getSliderAndVField(childViewModel),
me.getSliderAndAField(childViewModel),
me.getPreviewAndButtons(childViewModel, config)
];
me.childViewModel.bind('{selectedColor}', function(color) {
me.setColor(color);
});
me.callParent([
config
]);
},
updateColor: function(color) {
var me = this;
me.mixins.colorselection.updateColor.call(me, color);
me.childViewModel.set('selectedColor', color);
},
updatePreviousColor: function(color) {
this.childViewModel.set('previousColor', color);
},
// Splits up view declaration for readability
// "Map" and HEX/R/G/B fields
getMapAndHexRGBFields: function(childViewModel) {
var me = this,
fieldMargin = {
top: 0,
right: me.fieldPad,
bottom: 0,
left: 0
},
fieldWidth = me.fieldWidth;
return {
xtype: 'container',
viewModel: childViewModel,
cls: Ext.baseCSSPrefix + 'colorpicker-escape-overflow',
flex: 1,
layout: {
type: 'vbox',
align: 'stretch'
},
margin: '0 10 0 0',
items: [
// "MAP"
{
xtype: 'colorpickercolormap',
reference: 'colorMap',
flex: 1,
bind: {
position: {
bindTo: '{selectedColor}',
deep: true
},
hue: '{selectedColor.h}'
},
listeners: {
handledrag: 'onColorMapHandleDrag'
}
},
// HEX/R/G/B FIELDS
{
xtype: 'container',
layout: 'hbox',
defaults: {
labelAlign: 'top',
labelSeparator: '',
allowBlank: false,
onChange: function() {
// prevent data binding propagation if bad value
if (this.isValid()) {
// this is kind of dirty and ideally we would extend these fields
// and override the method, but works for now
Ext.form.field.Base.prototype.onChange.apply(this, arguments);
}
}
},
items: [
{
xtype: 'textfield',
fieldLabel: 'HEX',
flex: 1,
bind: '{hex}',
margin: fieldMargin,
regex: /^#[0-9a-f]{6}$/i,
readonly: me.getHexReadOnly()
},
{
xtype: 'numberfield',
fieldLabel: 'R',
bind: '{red}',
width: fieldWidth,
hideTrigger: true,
maxValue: 255,
minValue: 0,
margin: fieldMargin
},
{
xtype: 'numberfield',
fieldLabel: 'G',
bind: '{green}',
width: fieldWidth,
hideTrigger: true,
maxValue: 255,
minValue: 0,
margin: fieldMargin
},
{
xtype: 'numberfield',
fieldLabel: 'B',
bind: '{blue}',
width: fieldWidth,
hideTrigger: true,
maxValue: 255,
minValue: 0,
margin: 0
}
]
}
]
};
},
// Splits up view declaration for readability
// Slider and H field
getSliderAndHField: function(childViewModel) {
var me = this,
fieldWidth = me.fieldWidth;
return {
xtype: 'container',
viewModel: childViewModel,
cls: Ext.baseCSSPrefix + 'colorpicker-escape-overflow',
width: fieldWidth,
layout: {
type: 'vbox',
align: 'stretch'
},
items: [
{
xtype: 'colorpickersliderhue',
reference: 'hueSlider',
flex: 1,
bind: {
hue: '{selectedColor.h}'
},
width: fieldWidth,
listeners: {
handledrag: 'onHueSliderHandleDrag'
}
},
{
xtype: 'numberfield',
fieldLabel: 'H',
labelAlign: 'top',
labelSeparator: '',
bind: '{hue}',
hideTrigger: true,
maxValue: 360,
minValue: 0,
allowBlank: false,
margin: 0
}
]
};
},
// Splits up view declaration for readability
// Slider and S field
getSliderAndSField: function(childViewModel) {
var me = this,
fieldWidth = me.fieldWidth;
return {
xtype: 'container',
viewModel: childViewModel,
cls: Ext.baseCSSPrefix + 'colorpicker-escape-overflow',
width: fieldWidth,
layout: {
type: 'vbox',
align: 'stretch'
},
margin: {
right: me.fieldPad,
left: me.fieldPad
},
items: [
{
xtype: 'colorpickerslidersaturation',
reference: 'satSlider',
flex: 1,
bind: {
saturation: '{saturation}',
hue: '{selectedColor.h}'
},
width: fieldWidth,
listeners: {
handledrag: 'onSaturationSliderHandleDrag'
}
},
{
xtype: 'numberfield',
fieldLabel: 'S',
labelAlign: 'top',
labelSeparator: '',
bind: '{saturation}',
hideTrigger: true,
maxValue: 100,
minValue: 0,
allowBlank: false,
margin: 0
}
]
};
},
// Splits up view declaration for readability
// Slider and V field
getSliderAndVField: function(childViewModel) {
var me = this,
fieldWidth = me.fieldWidth;
return {
xtype: 'container',
viewModel: childViewModel,
cls: Ext.baseCSSPrefix + 'colorpicker-escape-overflow',
width: fieldWidth,
layout: {
type: 'vbox',
align: 'stretch'
},
items: [
{
xtype: 'colorpickerslidervalue',
reference: 'valueSlider',
flex: 1,
bind: {
value: '{value}',
hue: '{selectedColor.h}'
},
width: fieldWidth,
listeners: {
handledrag: 'onValueSliderHandleDrag'
}
},
{
xtype: 'numberfield',
fieldLabel: 'V',
labelAlign: 'top',
labelSeparator: '',
bind: '{value}',
hideTrigger: true,
maxValue: 100,
minValue: 0,
allowBlank: false,
margin: 0
}
]
};
},
// Splits up view declaration for readability
// Slider and A field
getSliderAndAField: function(childViewModel) {
var me = this,
fieldWidth = me.fieldWidth;
return {
xtype: 'container',
viewModel: childViewModel,
cls: Ext.baseCSSPrefix + 'colorpicker-escape-overflow',
width: fieldWidth,
layout: {
type: 'vbox',
align: 'stretch'
},
margin: {
left: me.fieldPad
},
items: [
{
xtype: 'colorpickerslideralpha',
reference: 'alphaSlider',
flex: 1,
bind: {
alpha: '{alpha}',
color: {
bindTo: '{selectedColor}',
deep: true
}
},
width: fieldWidth,
listeners: {
handledrag: 'onAlphaSliderHandleDrag'
}
},
{
xtype: 'numberfield',
fieldLabel: 'A',
labelAlign: 'top',
labelSeparator: '',
bind: '{alpha}',
hideTrigger: true,
maxValue: 100,
minValue: 0,
allowBlank: false,
margin: 0
}
]
};
},
// Splits up view declaration for readability
// Preview current/previous color squares and OK and Cancel buttons
getPreviewAndButtons: function(childViewModel, config) {
// selected color preview is always shown
var items = [
{
xtype: 'colorpickercolorpreview',
flex: 1,
bind: {
color: {
bindTo: '{selectedColor}',
deep: true
}
}
}
];
// previous color preview is optional
if (config.showPreviousColor) {
items.push({
xtype: 'colorpickercolorpreview',
flex: 1,
bind: {
color: {
bindTo: '{previousColor}',
deep: true
}
},
listeners: {
click: 'onPreviousColorSelected'
}
});
}
// Ok/Cancel buttons are optional
if (config.showOkCancelButtons) {
items.push({
xtype: 'button',
text: 'OK',
margin: '10 0 0 0',
padding: '10 0 10 0',
handler: 'onOK'
}, {
xtype: 'button',
text: 'Cancel',
margin: '10 0 0 0',
padding: '10 0 10 0',
handler: 'onCancel'
});
}
return {
xtype: 'container',
viewModel: childViewModel,
width: 70,
margin: '0 0 0 10',
items: items,
layout: {
type: 'vbox',
align: 'stretch'
}
};
}
});
/**
* @private
*/
Ext.define('Ext.ux.colorpick.ButtonController', {
extend: 'Ext.app.ViewController',
alias: 'controller.colorpick-buttoncontroller',
requires: [
'Ext.window.Window',
'Ext.layout.container.Fit',
'Ext.ux.colorpick.Selector',
'Ext.ux.colorpick.ColorUtils'
],
afterRender: function(view) {
view.updateColor(view.getColor());
},
destroy: function() {
var view = this.getView(),
colorPickerWindow = view.colorPickerWindow;
if (colorPickerWindow) {
colorPickerWindow.destroy();
view.colorPickerWindow = view.colorPicker = null;
}
this.callParent();
},
getPopup: function() {
var view = this.getView(),
popup = view.colorPickerWindow,
selector;
if (!popup) {
popup = Ext.create(view.getPopup());
view.colorPickerWindow = popup;
popup.colorPicker = view.colorPicker = selector = popup.lookupReference('selector');
selector.setFormat(view.getFormat());
selector.on({
ok: 'onColorPickerOK',
cancel: 'onColorPickerCancel',
scope: this
});
popup.on({
close: 'onColorPickerCancel',
scope: this
});
}
return popup;
},
// When button is clicked show the color picker window
onClick: function() {
var me = this,
view = me.getView(),
color = view.getColor(),
popup = me.getPopup(),
colorPicker = popup.colorPicker;
colorPicker.setColor(color);
colorPicker.setPreviousColor(color);
popup.showBy(view, 'tl-br?');
},
onColorPickerOK: function(picker) {
var view = this.getView(),
color = picker.getColor(),
cpWin = view.colorPickerWindow;
cpWin.hide();
view.setColor(color);
},
onColorPickerCancel: function() {
var view = this.getView(),
cpWin = view.colorPickerWindow;
cpWin.hide();
},
syncColor: function(color) {
var view = this.getView();
Ext.ux.colorpick.ColorUtils.setBackground(view.filterEl, color);
}
});
/**
* A simple color swatch that can be clicked to bring up the color selector.
*
* The selected color is configurable via {@link #value}.
*
* @example
* Ext.create('Ext.ux.colorpick.Button', {
* value: '993300', // initial selected color
* renderTo: Ext.getBody(),
*
* listeners: {
* select: function(picker, selColor) {
* Ext.Msg.alert('Color', selColor);
* }
* }
* });
*/
Ext.define('Ext.ux.colorpick.Button', {
extend: 'Ext.Component',
xtype: 'colorbutton',
controller: 'colorpick-buttoncontroller',
mixins: [
'Ext.ux.colorpick.Selection'
],
requires: [
'Ext.ux.colorpick.ButtonController'
],
baseCls: Ext.baseCSSPrefix + 'colorpicker-button',
width: 20,
height: 20,
childEls: [
'btnEl',
'filterEl'
],
config: {
/**
* @cfg {Object} popup
* This object configures the popup window and colorselector component displayed
* when this button is clicked. Applications should not need to configure this.
* @private
*/
popup: {
lazy: true,
$value: {
xtype: 'window',
closeAction: 'hide',
referenceHolder: true,
minWidth: 540,
minHeight: 200,
layout: 'fit',
header: false,
resizable: true,
items: {
xtype: 'colorselector',
reference: 'selector',
showPreviousColor: true,
showOkCancelButtons: true
}
}
}
},
defaultBindProperty: 'value',
twoWayBindable: 'value',
// Solve issue with IE, when applying a filter the click listener is not being fired.
renderTpl: '' + '',
listeners: {
click: 'onClick',
element: 'btnEl'
},
/**
* @event change
* Fires when a color is selected.
* @param {Ext.ux.colorpick.Selector} this
* @param {String} color The value of the selected color as per specified {@link #format}.
* @param {String} previousColor The previous color value.
*/
updateColor: function(color) {
var me = this,
cp = me.colorPicker;
me.mixins.colorselection.updateColor.call(me, color);
Ext.ux.colorpick.ColorUtils.setBackground(me.filterEl, color);
if (cp) {
cp.setColor(color);
}
},
// Sets this.format and color picker's setFormat()
updateFormat: function(format) {
var cp = this.colorPicker;
if (cp) {
cp.setFormat(format);
}
}
});
/**
* A field that can be clicked to bring up the color picker.
* The selected color is configurable via {@link #value}.
*
* @example
* Ext.create({
* xtype: 'colorfield',
* renderTo: Ext.getBody(),
*
* value: '#993300', // initial selected color
*
* listeners : {
* change: function (field, color) {
* console.log('New color: ' + color);
* }
* }
* });
*/
Ext.define('Ext.ux.colorpick.Field', {
extend: 'Ext.form.field.Picker',
xtype: 'colorfield',
mixins: [
'Ext.ux.colorpick.Selection'
],
requires: [
'Ext.window.Window',
'Ext.ux.colorpick.Selector',
'Ext.ux.colorpick.ColorUtils',
'Ext.layout.container.Fit'
],
editable: false,
matchFieldWidth: false,
// picker is usually wider than field
// "Color Swatch" shown on the left of the field
beforeBodyEl: [
'
' + '' + '
'
],
cls: Ext.baseCSSPrefix + 'colorpicker-field',
childEls: [
'swatchEl'
],
checkChangeEvents: [
'change'
],
config: {
/**
* @cfg {Object} popup
* This object configures the popup window and colorselector component displayed
* when this button is clicked. Applications should not need to configure this.
* @private
*/
popup: {
lazy: true,
$value: {
xtype: 'window',
closeAction: 'hide',
referenceHolder: true,
minWidth: 540,
minHeight: 200,
layout: 'fit',
header: false,
resizable: true,
items: {
xtype: 'colorselector',
reference: 'selector',
showPreviousColor: true,
showOkCancelButtons: true
}
}
}
},
/**
* @event change
* Fires when a color is selected or if the field value is updated (if {@link #editable}).
* @param {Ext.ux.colorpick.Field} this
* @param {String} color The value of the selected color as per specified {@link #format}.
* @param {String} previousColor The previous color value.
*/
initComponent: function() {
var me = this;
me.callParent();
me.on('change', me.onHexChange);
},
// NOTE: Since much of the logic of a picker class is overriding methods from the
// base class, we don't bother to split out the small remainder as a controller.
afterRender: function() {
this.callParent();
this.updateValue(this.value);
},
// override as required by parent pickerfield
createPicker: function() {
var me = this,
popup = me.getPopup(),
picker;
// the window will actually be shown and will house the picker
me.colorPickerWindow = popup = Ext.create(popup);
me.colorPicker = picker = popup.lookupReference('selector');
picker.setFormat(me.getFormat());
picker.setColor(me.getColor());
picker.setHexReadOnly(!me.editable);
picker.on({
ok: 'onColorPickerOK',
cancel: 'onColorPickerCancel',
scope: me
});
popup.on({
close: 'onColorPickerCancel',
scope: me
});
return me.colorPickerWindow;
},
// When the Ok button is clicked on color picker, preserve the previous value
onColorPickerOK: function(colorPicker) {
this.setColor(colorPicker.getColor());
this.collapse();
},
onColorPickerCancel: function() {
this.collapse();
},
onExpand: function() {
var color = this.getColor();
this.colorPicker.setPreviousColor(color);
},
onHexChange: function(field) {
if (field.validate()) {
this.setValue(field.getValue());
}
},
// Expects value formatted as per "format" config
setValue: function(color) {
var me = this;
if (Ext.ux.colorpick.ColorUtils.isValid(color)) {
color = me.applyValue(color);
me.callParent([
color
]);
// always update in case opacity changes, even if value doesn't have it
// to handle "hex6" non-opacity type of format
me.updateValue(color);
}
},
// Sets this.format and color picker's setFormat()
updateFormat: function(format) {
var cp = this.colorPicker;
if (cp) {
cp.setFormat(format);
}
},
updateValue: function(color) {
var me = this,
c;
// If the "value" is changed, update "color" as well. Since these are always
// tracking each other, we guard against the case where we are being updated
// *because* "color" is being set.
if (!me.syncing) {
me.syncing = true;
me.setColor(color);
me.syncing = false;
}
c = me.getColor();
if (c) {
Ext.ux.colorpick.ColorUtils.setBackground(me.swatchEl, c);
if (me.colorPicker) {
me.colorPicker.setColor(c);
}
}
},
validator: function(val) {
if (!Ext.ux.colorpick.ColorUtils.isValid(val)) {
return this.invalidText;
}
return true;
}
});
/**
* Paging Memory Proxy, allows to use paging grid with in memory dataset
*/
Ext.define('Ext.ux.data.PagingMemoryProxy', {
extend: 'Ext.data.proxy.Memory',
alias: 'proxy.pagingmemory',
alternateClassName: 'Ext.data.PagingMemoryProxy',
constructor: function() {
Ext.log.warn('Ext.ux.data.PagingMemoryProxy functionality has been merged into Ext.data.proxy.Memory by using the enablePaging flag.');
this.callParent(arguments);
},
read: function(operation, callback, scope) {
var reader = this.getReader(),
result = reader.read(this.data),
sorters, filters, sorterFn, records;
scope = scope || this;
// filtering
filters = operation.filters;
if (filters.length > 0) {
//at this point we have an array of Ext.util.Filter objects to filter with,
//so here we construct a function that combines these filters by ANDing them together
records = [];
Ext.each(result.records, function(record) {
var isMatch = true,
length = filters.length,
i;
for (i = 0; i < length; i++) {
var filter = filters[i],
fn = filter.filterFn,
scope = filter.scope;
isMatch = isMatch && fn.call(scope, record);
}
if (isMatch) {
records.push(record);
}
}, this);
result.records = records;
result.totalRecords = result.total = records.length;
}
// sorting
sorters = operation.sorters;
if (sorters.length > 0) {
//construct an amalgamated sorter function which combines all of the Sorters passed
sorterFn = function(r1, r2) {
var result = sorters[0].sort(r1, r2),
length = sorters.length,
i;
//if we have more than one sorter, OR any additional sorter functions together
for (i = 1; i < length; i++) {
result = result || sorters[i].sort.call(this, r1, r2);
}
return result;
};
result.records.sort(sorterFn);
}
// paging (use undefined cause start can also be 0 (thus false))
if (operation.start !== undefined && operation.limit !== undefined) {
result.records = result.records.slice(operation.start, operation.start + operation.limit);
result.count = result.records.length;
}
Ext.apply(operation, {
resultSet: result
});
operation.setCompleted();
operation.setSuccessful();
Ext.defer(function() {
Ext.callback(callback, scope, [
operation
]);
}, 10);
}
});
/**
* This class is used as a grid `plugin`. It provides a DropZone which cooperates with
* DragZones whose dragData contains a "field" property representing a form Field.
* Fields may be dropped onto grid data cells containing a matching data type.
*/
Ext.define('Ext.ux.dd.CellFieldDropZone', {
extend: 'Ext.dd.DropZone',
alias: 'plugin.ux-cellfielddropzone',
containerScroll: true,
/**
* @cfg {Function/String} onCellDrop
* The function to call on a cell data drop, or the name of the function on the
* corresponding `{@link Ext.app.ViewController controller}`. For details on the
* parameters, see `{@link #method!onCellDrop onCellDrop}`.
*/
/**
* This method is called when a field is dropped on a cell. This method is normally
* replaced by the `{@link #cfg!onCellDrop onCellDrop}` config property passed to the
* constructor.
* @param {String} fieldName The name of the field.
* @param {Mixed} value The value of the field.
* @method onCellDrop
*/
onCellDrop: Ext.emptyFn,
constructor: function(cfg) {
if (cfg) {
var me = this,
ddGroup = cfg.ddGroup,
onCellDrop = cfg.onCellDrop;
if (onCellDrop) {
if (typeof onCellDrop === 'string') {
me.onCellDropFn = onCellDrop;
me.onCellDrop = me.callCellDrop;
} else {
me.onCellDrop = onCellDrop;
}
}
if (ddGroup) {
me.ddGroup = ddGroup;
}
}
},
init: function(grid) {
var me = this;
// Call the DropZone constructor using the View's scrolling element
// only after the grid has been rendered.
if (grid.rendered) {
me.grid = grid;
grid.getView().on({
render: function(v) {
me.view = v;
Ext.ux.dd.CellFieldDropZone.superclass.constructor.call(me, me.view.el);
},
single: true
});
} else {
grid.on('render', me.init, me, {
single: true
});
}
},
getTargetFromEvent: function(e) {
var me = this,
v = me.view;
// Ascertain whether the mousemove is within a grid cell
var cell = e.getTarget(v.getCellSelector());
if (cell) {
// We *are* within a grid cell, so ask the View exactly which one,
// Extract data from the Model to create a target object for
// processing in subsequent onNodeXXXX methods. Note that the target does
// not have to be a DOM element. It can be whatever the noNodeXXX methods are
// programmed to expect.
var row = v.findItemByChild(cell),
columnIndex = cell.cellIndex;
if (row && Ext.isDefined(columnIndex)) {
return {
node: cell,
record: v.getRecord(row),
fieldName: me.grid.getVisibleColumnManager().getColumns()[columnIndex].dataIndex
};
}
}
},
onNodeEnter: function(target, dd, e, dragData) {
// On Node enter, see if it is valid for us to drop the field on that type of
// column.
delete this.dropOK;
if (!target) {
return;
}
// Check that a field is being dragged.
var f = dragData.field;
if (!f) {
return;
}
// Check whether the data type of the column being dropped on accepts the
// dragged field type. If so, set dropOK flag, and highlight the target node.
var field = target.record.fieldsMap[target.fieldName];
if (field.isNumeric) {
if (!f.isXType('numberfield')) {
return;
}
} else if (field.isDateField) {
if (!f.isXType('datefield')) {
return;
}
} else if (field.isBooleanField) {
if (!f.isXType('checkbox')) {
return;
}
}
this.dropOK = true;
Ext.fly(target.node).addCls('x-drop-target-active');
},
onNodeOver: function(target, dd, e, dragData) {
// Return the class name to add to the drag proxy. This provides a visual
// indication of drop allowed or not allowed.
return this.dropOK ? this.dropAllowed : this.dropNotAllowed;
},
onNodeOut: function(target, dd, e, dragData) {
Ext.fly(target.node).removeCls('x-drop-target-active');
},
onNodeDrop: function(target, dd, e, dragData) {
// Process the drop event if we have previously ascertained that a drop is OK.
if (this.dropOK) {
var value = dragData.field.getValue();
target.record.set(target.fieldName, value);
this.onCellDrop(target.fieldName, value);
return true;
}
},
callCellDrop: function(fieldName, value) {
Ext.callback(this.onCellDropFn, null, [
fieldName,
value
], 0, this.grid);
}
});
Ext.define('Ext.ux.dd.PanelFieldDragZone', {
extend: 'Ext.dd.DragZone',
alias: 'plugin.ux-panelfielddragzone',
scroll: false,
constructor: function(cfg) {
if (cfg) {
if (cfg.ddGroup) {
this.ddGroup = cfg.ddGroup;
}
}
},
init: function(panel) {
var el;
// Call the DragZone's constructor. The Panel must have been rendered.
// Panel is an HtmlElement
if (panel.nodeType) {
// Called via dragzone::init
Ext.ux.dd.PanelFieldDragZone.superclass.init.apply(this, arguments);
} else // Panel is a Component - need the el
{
// Called via plugin::init
if (panel.rendered) {
el = panel.getEl();
el.unselectable();
Ext.ux.dd.PanelFieldDragZone.superclass.constructor.call(this, el);
} else {
panel.on('afterrender', this.init, this, {
single: true
});
}
}
},
getDragData: function(e) {
// On mousedown, we ascertain whether it is on one of our draggable Fields.
// If so, we collect data about the draggable object, and return a drag data
// object which contains our own data, plus a "ddel" property which is a DOM
// node which provides a "view" of the dragged data.
var targetLabel = e.getTarget('label', null, true),
text, oldMark, field, dragEl;
if (targetLabel) {
// Get the data we are dragging: the Field
// create a ddel for the drag proxy to display
field = Ext.getCmp(targetLabel.up('.' + Ext.form.Labelable.prototype.formItemCls).id);
// Temporary prevent marking the field as invalid, since it causes changes
// to the underlying dom element which can cause problems in IE
oldMark = field.preventMark;
field.preventMark = true;
if (field.isValid()) {
field.preventMark = oldMark;
dragEl = document.createElement('div');
dragEl.className = Ext.baseCSSPrefix + 'form-text';
text = field.getRawValue();
dragEl.innerHTML = Ext.isEmpty(text) ? ' ' : text;
Ext.fly(dragEl).setWidth(field.getEl().getWidth());
return {
field: field,
ddel: dragEl
};
}
e.stopEvent();
field.preventMark = oldMark;
}
},
getRepairXY: function() {
// The coordinates to slide the drag proxy back to on failed drop.
return this.dragData.field.getEl().getXY();
}
});
/*!
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
*/
/**
* @class Ext.ux.desktop.Desktop
* @extends Ext.panel.Panel
*
This class manages the wallpaper, shortcuts and taskbar.
*/
Ext.define('Ext.ux.desktop.Desktop', {
extend: 'Ext.panel.Panel',
alias: 'widget.desktop',
uses: [
'Ext.util.MixedCollection',
'Ext.menu.Menu',
'Ext.view.View',
// dataview
'Ext.window.Window',
'Ext.ux.desktop.TaskBar',
'Ext.ux.desktop.Wallpaper'
],
activeWindowCls: 'ux-desktop-active-win',
inactiveWindowCls: 'ux-desktop-inactive-win',
lastActiveWindow: null,
border: false,
html: ' ',
layout: 'fit',
xTickSize: 1,
yTickSize: 1,
app: null,
/**
* @cfg {Array/Ext.data.Store} shortcuts
* The items to add to the DataView. This can be a {@link Ext.data.Store Store} or a
* simple array. Items should minimally provide the fields in the
* {@link Ext.ux.desktop.ShortcutModel Shortcut}.
*/
shortcuts: null,
/**
* @cfg {String} shortcutItemSelector
* This property is passed to the DataView for the desktop to select shortcut items.
* If the {@link #shortcutTpl} is modified, this will probably need to be modified as
* well.
*/
shortcutItemSelector: 'div.ux-desktop-shortcut',
/**
* @cfg {String} shortcutTpl
* This XTemplate is used to render items in the DataView. If this is changed, the
* {@link #shortcutItemSelector} will probably also need to changed.
*/
shortcutTpl: [
'',
'
',
'
',
'',
'
',
'{name}',
'
',
'',
''
],
/**
* @cfg {Object} taskbarConfig
* The config object for the TaskBar.
*/
taskbarConfig: null,
windowMenu: null,
initComponent: function() {
var me = this;
me.windowMenu = new Ext.menu.Menu(me.createWindowMenu());
me.bbar = me.taskbar = new Ext.ux.desktop.TaskBar(me.taskbarConfig);
me.taskbar.windowMenu = me.windowMenu;
me.windows = new Ext.util.MixedCollection();
me.contextMenu = new Ext.menu.Menu(me.createDesktopMenu());
me.items = [
{
xtype: 'wallpaper',
id: me.id + '_wallpaper'
},
me.createDataView()
];
me.callParent();
me.shortcutsView = me.items.getAt(1);
me.shortcutsView.on('itemclick', me.onShortcutItemClick, me);
var wallpaper = me.wallpaper;
me.wallpaper = me.items.getAt(0);
if (wallpaper) {
me.setWallpaper(wallpaper, me.wallpaperStretch);
}
},
afterRender: function() {
var me = this;
me.callParent();
me.el.on('contextmenu', me.onDesktopMenu, me);
},
//------------------------------------------------------
// Overrideable configuration creation methods
createDataView: function() {
var me = this;
return {
xtype: 'dataview',
overItemCls: 'x-view-over',
trackOver: true,
itemSelector: me.shortcutItemSelector,
store: me.shortcuts,
style: {
position: 'absolute'
},
x: 0,
y: 0,
tpl: new Ext.XTemplate(me.shortcutTpl)
};
},
createDesktopMenu: function() {
var me = this,
ret = {
items: me.contextMenuItems || []
};
if (ret.items.length) {
ret.items.push('-');
}
ret.items.push({
text: 'Tile',
handler: me.tileWindows,
scope: me,
minWindows: 1
}, {
text: 'Cascade',
handler: me.cascadeWindows,
scope: me,
minWindows: 1
});
return ret;
},
createWindowMenu: function() {
var me = this;
return {
defaultAlign: 'br-tr',
items: [
{
text: 'Restore',
handler: me.onWindowMenuRestore,
scope: me
},
{
text: 'Minimize',
handler: me.onWindowMenuMinimize,
scope: me
},
{
text: 'Maximize',
handler: me.onWindowMenuMaximize,
scope: me
},
'-',
{
text: 'Close',
handler: me.onWindowMenuClose,
scope: me
}
],
listeners: {
beforeshow: me.onWindowMenuBeforeShow,
hide: me.onWindowMenuHide,
scope: me
}
};
},
//------------------------------------------------------
// Event handler methods
onDesktopMenu: function(e) {
var me = this,
menu = me.contextMenu;
e.stopEvent();
if (!menu.rendered) {
menu.on('beforeshow', me.onDesktopMenuBeforeShow, me);
}
menu.showAt(e.getXY());
menu.doConstrain();
},
onDesktopMenuBeforeShow: function(menu) {
var me = this,
count = me.windows.getCount();
menu.items.each(function(item) {
var min = item.minWindows || 0;
item.setDisabled(count < min);
});
},
onShortcutItemClick: function(dataView, record) {
var me = this,
module = me.app.getModule(record.data.module),
win = module && module.createWindow();
if (win) {
me.restoreWindow(win);
}
},
onWindowClose: function(win) {
var me = this;
me.windows.remove(win);
me.taskbar.removeTaskButton(win.taskButton);
me.updateActiveWindow();
},
//------------------------------------------------------
// Window context menu handlers
onWindowMenuBeforeShow: function(menu) {
var items = menu.items.items,
win = menu.theWin;
items[0].setDisabled(win.maximized !== true && win.hidden !== true);
// Restore
items[1].setDisabled(win.minimized === true);
// Minimize
items[2].setDisabled(win.maximized === true || win.hidden === true);
},
// Maximize
onWindowMenuClose: function() {
var me = this,
win = me.windowMenu.theWin;
win.close();
},
onWindowMenuHide: function(menu) {
Ext.defer(function() {
menu.theWin = null;
}, 1);
},
onWindowMenuMaximize: function() {
var me = this,
win = me.windowMenu.theWin;
win.maximize();
win.toFront();
},
onWindowMenuMinimize: function() {
var me = this,
win = me.windowMenu.theWin;
win.minimize();
},
onWindowMenuRestore: function() {
var me = this,
win = me.windowMenu.theWin;
me.restoreWindow(win);
},
//------------------------------------------------------
// Dynamic (re)configuration methods
getWallpaper: function() {
return this.wallpaper.wallpaper;
},
setTickSize: function(xTickSize, yTickSize) {
var me = this,
xt = me.xTickSize = xTickSize,
yt = me.yTickSize = (arguments.length > 1) ? yTickSize : xt;
me.windows.each(function(win) {
var dd = win.dd,
resizer = win.resizer;
dd.xTickSize = xt;
dd.yTickSize = yt;
resizer.widthIncrement = xt;
resizer.heightIncrement = yt;
});
},
setWallpaper: function(wallpaper, stretch) {
this.wallpaper.setWallpaper(wallpaper, stretch);
return this;
},
//------------------------------------------------------
// Window management methods
cascadeWindows: function() {
var x = 0,
y = 0,
zmgr = this.getDesktopZIndexManager();
zmgr.eachBottomUp(function(win) {
if (win.isWindow && win.isVisible() && !win.maximized) {
win.setPosition(x, y);
x += 20;
y += 20;
}
});
},
createWindow: function(config, cls) {
var me = this,
win,
cfg = Ext.applyIf(config || {}, {
stateful: false,
isWindow: true,
constrainHeader: true,
minimizable: true,
maximizable: true
});
cls = cls || Ext.window.Window;
win = me.add(new cls(cfg));
me.windows.add(win);
win.taskButton = me.taskbar.addTaskButton(win);
win.animateTarget = win.taskButton.el;
win.on({
activate: me.updateActiveWindow,
beforeshow: me.updateActiveWindow,
deactivate: me.updateActiveWindow,
minimize: me.minimizeWindow,
destroy: me.onWindowClose,
scope: me
});
win.on({
boxready: function() {
win.dd.xTickSize = me.xTickSize;
win.dd.yTickSize = me.yTickSize;
if (win.resizer) {
win.resizer.widthIncrement = me.xTickSize;
win.resizer.heightIncrement = me.yTickSize;
}
},
single: true
});
// replace normal window close w/fadeOut animation:
win.doClose = function() {
win.doClose = Ext.emptyFn;
// dblclick can call again...
win.el.disableShadow();
win.el.fadeOut({
listeners: {
afteranimate: function() {
win.destroy();
}
}
});
};
return win;
},
getActiveWindow: function() {
var win = null,
zmgr = this.getDesktopZIndexManager();
if (zmgr) {
// We cannot rely on activate/deactive because that fires against non-Window
// components in the stack.
zmgr.eachTopDown(function(comp) {
if (comp.isWindow && !comp.hidden) {
win = comp;
return false;
}
return true;
});
}
return win;
},
getDesktopZIndexManager: function() {
var windows = this.windows;
// TODO - there has to be a better way to get this...
return (windows.getCount() && windows.getAt(0).zIndexManager) || null;
},
getWindow: function(id) {
return this.windows.get(id);
},
minimizeWindow: function(win) {
win.minimized = true;
win.hide();
},
restoreWindow: function(win) {
if (win.isVisible()) {
win.restore();
win.toFront();
} else {
win.show();
}
return win;
},
tileWindows: function() {
var me = this,
availWidth = me.body.getWidth(true);
var x = me.xTickSize,
y = me.yTickSize,
nextY = y;
me.windows.each(function(win) {
if (win.isVisible() && !win.maximized) {
var w = win.el.getWidth();
// Wrap to next row if we are not at the line start and this Window will
// go off the end
if (x > me.xTickSize && x + w > availWidth) {
x = me.xTickSize;
y = nextY;
}
win.setPosition(x, y);
x += w + me.xTickSize;
nextY = Math.max(nextY, y + win.el.getHeight() + me.yTickSize);
}
});
},
updateActiveWindow: function() {
var me = this,
activeWindow = me.getActiveWindow(),
last = me.lastActiveWindow;
if (last && last.destroyed) {
me.lastActiveWindow = null;
return;
}
if (activeWindow === last) {
return;
}
if (last) {
if (last.el.dom) {
last.addCls(me.inactiveWindowCls);
last.removeCls(me.activeWindowCls);
}
last.active = false;
}
me.lastActiveWindow = activeWindow;
if (activeWindow) {
activeWindow.addCls(me.activeWindowCls);
activeWindow.removeCls(me.inactiveWindowCls);
activeWindow.minimized = false;
activeWindow.active = true;
}
me.taskbar.setActiveButton(activeWindow && activeWindow.taskButton);
}
});
/**
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
* @class Ext.ux.desktop.App
*/
Ext.define('Ext.ux.desktop.App', {
mixins: {
observable: 'Ext.util.Observable'
},
requires: [
'Ext.container.Viewport',
'Ext.ux.desktop.Desktop'
],
isReady: false,
modules: null,
useQuickTips: true,
constructor: function(config) {
var me = this;
me.mixins.observable.constructor.call(this, config);
if (Ext.isReady) {
Ext.defer(me.init, 10, me);
} else {
Ext.onReady(me.init, me);
}
},
init: function() {
var me = this,
desktopCfg;
if (me.useQuickTips) {
Ext.QuickTips.init();
}
me.modules = me.getModules();
if (me.modules) {
me.initModules(me.modules);
}
desktopCfg = me.getDesktopConfig();
me.desktop = new Ext.ux.desktop.Desktop(desktopCfg);
me.viewport = new Ext.container.Viewport({
layout: 'fit',
items: [
me.desktop
]
});
Ext.getWin().on('beforeunload', me.onUnload, me);
me.isReady = true;
me.fireEvent('ready', me);
},
/**
* This method returns the configuration object for the Desktop object. A derived
* class can override this method, call the base version to build the config and
* then modify the returned object before returning it.
*/
getDesktopConfig: function() {
var me = this,
cfg = {
app: me,
taskbarConfig: me.getTaskbarConfig()
};
Ext.apply(cfg, me.desktopConfig);
return cfg;
},
getModules: Ext.emptyFn,
/**
* This method returns the configuration object for the Start Button. A derived
* class can override this method, call the base version to build the config and
* then modify the returned object before returning it.
*/
getStartConfig: function() {
var me = this,
cfg = {
app: me,
menu: []
},
launcher;
Ext.apply(cfg, me.startConfig);
Ext.each(me.modules, function(module) {
launcher = module.launcher;
if (launcher) {
launcher.handler = launcher.handler || Ext.bind(me.createWindow, me, [
module
]);
cfg.menu.push(module.launcher);
}
});
return cfg;
},
createWindow: function(module) {
var window = module.createWindow();
window.show();
},
/**
* This method returns the configuration object for the TaskBar. A derived class
* can override this method, call the base version to build the config and then
* modify the returned object before returning it.
*/
getTaskbarConfig: function() {
var me = this,
cfg = {
app: me,
startConfig: me.getStartConfig()
};
Ext.apply(cfg, me.taskbarConfig);
return cfg;
},
initModules: function(modules) {
var me = this;
Ext.each(modules, function(module) {
module.app = me;
});
},
getModule: function(name) {
var ms = this.modules;
for (var i = 0,
len = ms.length; i < len; i++) {
var m = ms[i];
if (m.id == name || m.appType == name) {
return m;
}
}
return null;
},
onReady: function(fn, scope) {
if (this.isReady) {
fn.call(scope, this);
} else {
this.on({
ready: fn,
scope: scope,
single: true
});
}
},
getDesktop: function() {
return this.desktop;
},
onUnload: function(e) {
if (this.fireEvent('beforeunload', this) === false) {
e.stopEvent();
}
}
});
/*!
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
*/
Ext.define('Ext.ux.desktop.Module', {
mixins: {
observable: 'Ext.util.Observable'
},
constructor: function(config) {
this.mixins.observable.constructor.call(this, config);
this.init();
},
init: Ext.emptyFn
});
/*!
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
*/
/**
* @class Ext.ux.desktop.ShortcutModel
* @extends Ext.data.Model
* This model defines the minimal set of fields for desktop shortcuts.
*/
Ext.define('Ext.ux.desktop.ShortcutModel', {
extend: 'Ext.data.Model',
fields: [
{
name: 'name',
convert: Ext.String.createVarName
},
{
name: 'iconCls'
},
{
name: 'module'
}
]
});
/**
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
* @class Ext.ux.desktop.StartMenu
*/
Ext.define('Ext.ux.desktop.StartMenu', {
extend: 'Ext.menu.Menu',
// We want header styling like a Panel
baseCls: Ext.baseCSSPrefix + 'panel',
// Special styling within
cls: 'x-menu ux-start-menu',
bodyCls: 'ux-start-menu-body',
defaultAlign: 'bl-tl',
iconCls: 'user',
bodyBorder: true,
width: 300,
initComponent: function() {
var me = this;
me.layout.align = 'stretch';
me.items = me.menu;
me.callParent();
me.toolbar = new Ext.toolbar.Toolbar(Ext.apply({
dock: 'right',
cls: 'ux-start-menu-toolbar',
vertical: true,
width: 100,
layout: {
align: 'stretch'
}
}, me.toolConfig));
me.addDocked(me.toolbar);
delete me.toolItems;
},
addMenuItem: function() {
var cmp = this.menu;
cmp.add.apply(cmp, arguments);
},
addToolItem: function() {
var cmp = this.toolbar;
cmp.add.apply(cmp, arguments);
}
});
// StartMenu
/*!
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
*/
/**
* @class Ext.ux.desktop.TaskBar
* @extends Ext.toolbar.Toolbar
*/
Ext.define('Ext.ux.desktop.TaskBar', {
// This must be a toolbar. we rely on acquired toolbar classes and inherited toolbar methods for our
// child items to instantiate and render correctly.
extend: 'Ext.toolbar.Toolbar',
requires: [
'Ext.button.Button',
'Ext.resizer.Splitter',
'Ext.menu.Menu',
'Ext.ux.desktop.StartMenu'
],
alias: 'widget.taskbar',
cls: 'ux-taskbar',
/**
* @cfg {String} startBtnText
* The text for the Start Button.
*/
startBtnText: 'Start',
initComponent: function() {
var me = this;
me.startMenu = new Ext.ux.desktop.StartMenu(me.startConfig);
me.quickStart = new Ext.toolbar.Toolbar(me.getQuickStart());
me.windowBar = new Ext.toolbar.Toolbar(me.getWindowBarConfig());
me.tray = new Ext.toolbar.Toolbar(me.getTrayConfig());
me.items = [
{
xtype: 'button',
cls: 'ux-start-button',
iconCls: 'ux-start-button-icon',
menu: me.startMenu,
menuAlign: 'bl-tl',
text: me.startBtnText
},
me.quickStart,
{
xtype: 'splitter',
html: ' ',
height: 14,
width: 2,
// TODO - there should be a CSS way here
cls: 'x-toolbar-separator x-toolbar-separator-horizontal'
},
me.windowBar,
'-',
me.tray
];
me.callParent();
},
afterLayout: function() {
var me = this;
me.callParent();
me.windowBar.el.on('contextmenu', me.onButtonContextMenu, me);
},
/**
* This method returns the configuration object for the Quick Start toolbar. A derived
* class can override this method, call the base version to build the config and
* then modify the returned object before returning it.
*/
getQuickStart: function() {
var me = this,
ret = {
minWidth: 20,
width: Ext.themeName === 'neptune' ? 70 : 60,
items: [],
enableOverflow: true
};
Ext.each(this.quickStart, function(item) {
ret.items.push({
tooltip: {
text: item.name,
align: 'bl-tl'
},
//tooltip: item.name,
overflowText: item.name,
iconCls: item.iconCls,
module: item.module,
handler: me.onQuickStartClick,
scope: me
});
});
return ret;
},
/**
* This method returns the configuration object for the Tray toolbar. A derived
* class can override this method, call the base version to build the config and
* then modify the returned object before returning it.
*/
getTrayConfig: function() {
var ret = {
items: this.trayItems
};
delete this.trayItems;
return ret;
},
getWindowBarConfig: function() {
return {
flex: 1,
cls: 'ux-desktop-windowbar',
items: [
' '
],
layout: {
overflowHandler: 'Scroller'
}
};
},
getWindowBtnFromEl: function(el) {
var c = this.windowBar.getChildByElement(el);
return c || null;
},
onQuickStartClick: function(btn) {
var module = this.app.getModule(btn.module),
window;
if (module) {
window = module.createWindow();
window.show();
}
},
onButtonContextMenu: function(e) {
var me = this,
t = e.getTarget(),
btn = me.getWindowBtnFromEl(t);
if (btn) {
e.stopEvent();
me.windowMenu.theWin = btn.win;
me.windowMenu.showBy(t);
}
},
onWindowBtnClick: function(btn) {
var win = btn.win;
if (win.minimized || win.hidden) {
btn.disable();
win.show(null, function() {
btn.enable();
});
} else if (win.active) {
btn.disable();
win.on('hide', function() {
btn.enable();
}, null, {
single: true
});
win.minimize();
} else {
win.toFront();
}
},
addTaskButton: function(win) {
var config = {
iconCls: win.iconCls,
enableToggle: true,
toggleGroup: 'all',
width: 140,
margin: '0 2 0 3',
text: Ext.util.Format.ellipsis(win.title, 20),
listeners: {
click: this.onWindowBtnClick,
scope: this
},
win: win
};
var cmp = this.windowBar.add(config);
cmp.toggle(true);
return cmp;
},
removeTaskButton: function(btn) {
var found,
me = this;
me.windowBar.items.each(function(item) {
if (item === btn) {
found = item;
}
return !found;
});
if (found) {
me.windowBar.remove(found);
}
return found;
},
setActiveButton: function(btn) {
if (btn) {
btn.toggle(true);
} else {
this.windowBar.items.each(function(item) {
if (item.isButton) {
item.toggle(false);
}
});
}
}
});
/**
* @class Ext.ux.desktop.TrayClock
* @extends Ext.toolbar.TextItem
* This class displays a clock on the toolbar.
*/
Ext.define('Ext.ux.desktop.TrayClock', {
extend: 'Ext.toolbar.TextItem',
alias: 'widget.trayclock',
cls: 'ux-desktop-trayclock',
html: ' ',
timeFormat: 'g:i A',
tpl: '{time}',
initComponent: function() {
var me = this;
me.callParent();
if (typeof (me.tpl) == 'string') {
me.tpl = new Ext.XTemplate(me.tpl);
}
},
afterRender: function() {
var me = this;
Ext.defer(me.updateTime, 100, me);
me.callParent();
},
doDestroy: function() {
var me = this;
if (me.timer) {
window.clearTimeout(me.timer);
me.timer = null;
}
me.callParent();
},
updateTime: function() {
var me = this,
time = Ext.Date.format(new Date(), me.timeFormat),
text = me.tpl.apply({
time: time
});
if (me.lastText != text) {
me.setText(text);
me.lastText = text;
}
me.timer = Ext.defer(me.updateTime, 10000, me);
}
});
/*!
* Ext JS Library
* Copyright(c) 2006-2015 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
*/
/**
* From code originally written by David Davis
*
* For HTML5 video to work, your server must
* send the right content type, for more info see:
*
*/
Ext.define('Ext.ux.desktop.Video', {
extend: 'Ext.panel.Panel',
alias: 'widget.video',
layout: 'fit',
autoplay: false,
controls: true,
bodyStyle: 'background-color:#000;color:#fff',
html: '',
tpl: [
''
],
initComponent: function() {
var me = this,
fallback, size, cfg, el;
if (me.fallbackHTML) {
fallback = me.fallbackHTML;
} else {
fallback = "Your browser does not support HTML5 Video. ";
if (Ext.isChrome) {
fallback += 'Upgrade Chrome.';
} else if (Ext.isGecko) {
fallback += 'Upgrade to Firefox 3.5 or newer.';
} else {
var chrome = 'Chrome';
fallback += 'Please try Firefox';
if (Ext.isIE) {
fallback += ', ' + chrome + ' or Safari.';
} else {
fallback += ' or ' + chrome + '.';
}
}
}
me.fallbackHTML = fallback;
cfg = me.data = Ext.copyTo({
tag: 'video',
html: fallback
}, me, 'id,poster,start,loopstart,loopend,playcount,autobuffer,loop');
// just having the params exist enables them
if (me.autoplay) {
cfg.autoplay = 1;
}
if (me.controls) {
cfg.controls = 1;
}
// handle multiple sources
if (Ext.isArray(me.src)) {
cfg.src = me.src;
} else {
cfg.src = [
{
src: me.src
}
];
}
me.callParent();
},
afterRender: function() {
var me = this;
me.callParent();
me.video = me.body.getById(me.id + '-video');
el = me.video.dom;
me.supported = (el && el.tagName.toLowerCase() == 'video');
if (me.supported) {
me.video.on('error', me.onVideoError, me);
}
},
getFallback: function() {
return '
' + this.fallbackHTML + '
';
},
onVideoError: function() {
var me = this;
me.video.remove();
me.supported = false;
me.body.createChild(me.getFallback());
},
doDestroy: function() {
var me = this;
var video = me.video;
if (me.supported && video) {
var videoDom = video.dom;
if (videoDom && videoDom.pause) {
videoDom.pause();
}
video.remove();
me.video = null;
}
me.callParent();
}
});
/*!
* Ext JS Library
* Copyright(c) 2006-2014 Sencha Inc.
* licensing@sencha.com
* http://www.sencha.com/license
*/
/**
* @class Ext.ux.desktop.Wallpaper
* @extends Ext.Component
*
This component renders an image that stretches to fill the component.
*/
Ext.define('Ext.ux.desktop.Wallpaper', {
extend: 'Ext.Component',
alias: 'widget.wallpaper',
cls: 'ux-wallpaper',
html: '',
stretch: false,
wallpaper: null,
stateful: true,
stateId: 'desk-wallpaper',
afterRender: function() {
var me = this;
me.callParent();
me.setWallpaper(me.wallpaper, me.stretch);
},
applyState: function() {
var me = this,
old = me.wallpaper;
me.callParent(arguments);
if (old != me.wallpaper) {
me.setWallpaper(me.wallpaper);
}
},
getState: function() {
return this.wallpaper && {
wallpaper: this.wallpaper
};
},
setWallpaper: function(wallpaper, stretch) {
var me = this,
imgEl, bkgnd;
me.stretch = (stretch !== false);
me.wallpaper = wallpaper;
if (me.rendered) {
imgEl = me.el.dom.firstChild;
if (!wallpaper || wallpaper == Ext.BLANK_IMAGE_URL) {
Ext.fly(imgEl).hide();
} else if (me.stretch) {
imgEl.src = wallpaper;
me.el.removeCls('ux-wallpaper-tiled');
Ext.fly(imgEl).setStyle({
width: '100%',
height: '100%'
}).show();
} else {
Ext.fly(imgEl).hide();
bkgnd = 'url(' + wallpaper + ')';
me.el.addCls('ux-wallpaper-tiled');
}
me.el.setStyle({
backgroundImage: bkgnd || ''
});
if (me.stateful) {
me.saveState();
}
}
return me;
}
});
/**
* Recorder manager.
* Used as a bookmarklet:
*
* javascript:void(window.open("../ux/event/RecorderManager.html","recmgr"))
*/
Ext.define('Ext.ux.event.RecorderManager', {
extend: 'Ext.panel.Panel',
alias: 'widget.eventrecordermanager',
uses: [
'Ext.ux.event.Recorder',
'Ext.ux.event.Player'
],
layout: 'fit',
buttonAlign: 'left',
eventsToIgnore: {
mousemove: 1,
mouseover: 1,
mouseout: 1
},
bodyBorder: false,
playSpeed: 1,
initComponent: function() {
var me = this;
me.recorder = new Ext.ux.event.Recorder({
attachTo: me.attachTo,
listeners: {
add: me.updateEvents,
coalesce: me.updateEvents,
buffer: 200,
scope: me
}
});
me.recorder.eventsToRecord = Ext.apply({}, me.recorder.eventsToRecord);
function speed(text, value) {
return {
text: text,
speed: value,
group: 'speed',
checked: value == me.playSpeed,
handler: me.onPlaySpeed,
scope: me
};
}
me.tbar = [
{
text: 'Record',
xtype: 'splitbutton',
whenIdle: true,
handler: me.onRecord,
scope: me,
menu: me.makeRecordButtonMenu()
},
{
text: 'Play',
xtype: 'splitbutton',
whenIdle: true,
handler: me.onPlay,
scope: me,
menu: [
speed('Qarter Speed (0.25x)', 0.25),
speed('Half Speed (0.5x)', 0.5),
speed('3/4 Speed (0.75x)', 0.75),
'-',
speed('Recorded Speed (1x)', 1),
speed('Double Speed (2x)', 2),
speed('Quad Speed (4x)', 4),
'-',
speed('Full Speed', 1000)
]
},
{
text: 'Clear',
whenIdle: true,
handler: me.onClear,
scope: me
},
'->',
{
text: 'Stop',
whenActive: true,
disabled: true,
handler: me.onStop,
scope: me
}
];
var events = me.attachTo && me.attachTo.testEvents;
me.items = [
{
xtype: 'textarea',
itemId: 'eventView',
fieldStyle: 'font-family: monospace',
selectOnFocus: true,
emptyText: 'Events go here!',
value: events ? me.stringifyEvents(events) : '',
scrollToBottom: function() {
var inputEl = this.inputEl.dom;
inputEl.scrollTop = inputEl.scrollHeight;
}
}
];
me.fbar = [
{
xtype: 'tbtext',
text: 'Attached To: ' + (me.attachTo && me.attachTo.location.href)
}
];
me.callParent();
},
makeRecordButtonMenu: function() {
var ret = [],
subs = {},
eventsToRec = this.recorder.eventsToRecord,
ignoredEvents = this.eventsToIgnore;
Ext.Object.each(eventsToRec, function(name, value) {
var sub = subs[value.kind];
if (!sub) {
subs[value.kind] = sub = [];
ret.push({
text: value.kind,
menu: sub
});
}
sub.push({
text: name,
checked: true,
handler: function(menuItem) {
if (menuItem.checked) {
eventsToRec[name] = value;
} else {
delete eventsToRec[name];
}
}
});
if (ignoredEvents[name]) {
sub[sub.length - 1].checked = false;
Ext.defer(function() {
delete eventsToRec[name];
}, 1);
}
});
function less(lhs, rhs) {
return (lhs.text < rhs.text) ? -1 : ((rhs.text < lhs.text) ? 1 : 0);
}
ret.sort(less);
Ext.Array.each(ret, function(sub) {
sub.menu.sort(less);
});
return ret;
},
getEventView: function() {
return this.down('#eventView');
},
onClear: function() {
var view = this.getEventView();
view.setValue('');
},
onPlay: function() {
var me = this,
view = me.getEventView(),
events = view.getValue();
if (events) {
events = Ext.decode(events);
if (events.length) {
me.player = Ext.create('Ext.ux.event.Player', {
attachTo: window.opener,
eventQueue: events,
speed: me.playSpeed,
listeners: {
stop: me.onPlayStop,
scope: me
}
});
me.player.start();
me.syncBtnUI();
}
}
},
onPlayStop: function() {
this.player = null;
this.syncBtnUI();
},
onPlaySpeed: function(menuitem) {
this.playSpeed = menuitem.speed;
},
onRecord: function() {
this.recorder.start();
this.syncBtnUI();
},
onStop: function() {
var me = this;
if (me.player) {
me.player.stop();
me.player = null;
} else {
me.recorder.stop();
}
me.syncBtnUI();
me.updateEvents();
},
syncBtnUI: function() {
var me = this,
idle = !me.player && !me.recorder.active;
Ext.each(me.query('[whenIdle]'), function(btn) {
btn.setDisabled(!idle);
});
Ext.each(me.query('[whenActive]'), function(btn) {
btn.setDisabled(idle);
});
var view = me.getEventView();
view.setReadOnly(!idle);
},
stringifyEvents: function(events) {
var line,
lines = [];
Ext.each(events, function(ev) {
line = [];
Ext.Object.each(ev, function(name, value) {
if (line.length) {
line.push(', ');
} else {
line.push(' { ');
}
line.push(name, ': ');
line.push(Ext.encode(value));
});
line.push(' }');
lines.push(line.join(''));
});
return '[\n' + lines.join(',\n') + '\n]';
},
updateEvents: function() {
var me = this,
text = me.stringifyEvents(me.recorder.getRecordedEvents()),
view = me.getEventView();
view.setValue(text);
view.scrollToBottom();
}
});
/**
* A control that allows selection of multiple items in a list.
*/
Ext.define('Ext.ux.form.MultiSelect', {
extend: 'Ext.form.FieldContainer',
mixins: [
'Ext.util.StoreHolder',
'Ext.form.field.Field'
],
alternateClassName: 'Ext.ux.Multiselect',
alias: [
'widget.multiselectfield',
'widget.multiselect'
],
requires: [
'Ext.panel.Panel',
'Ext.view.BoundList',
'Ext.layout.container.Fit'
],
uses: [
'Ext.view.DragZone',
'Ext.view.DropZone'
],
layout: 'anchor',
/**
* @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect DragZone.
*/
/**
* @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect DropZone.
*/
/**
* @cfg {String} [title=""] A title for the underlying panel.
*/
/**
* @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect list are drag/drop reorderable.
*/
ddReorder: false,
/**
* @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
* This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
* to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
*/
/**
* @cfg {String} [appendOnly=false] `true` if the list should only allow append drops when drag/drop is enabled.
* This is useful for lists which are sorted.
*/
appendOnly: false,
/**
* @cfg {String} [displayField="text"] Name of the desired display field in the dataset.
*/
displayField: 'text',
/**
* @cfg {String} [valueField="text"] Name of the desired value field in the dataset.
*/
/**
* @cfg {Boolean} [allowBlank=true] `false` to require at least one item in the list to be selected, `true` to allow no
* selection.
*/
allowBlank: true,
/**
* @cfg {Number} [minSelections=0] Minimum number of selections allowed.
*/
minSelections: 0,
/**
* @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of selections allowed.
*/
maxSelections: Number.MAX_VALUE,
/**
* @cfg {String} [blankText="This field is required"] Default text displayed when the control contains no items.
*/
blankText: 'This field is required',
/**
* @cfg {String} [minSelectionsText="Minimum {0}item(s) required"]
* Validation message displayed when {@link #minSelections} is not met.
* The {0} token will be replaced by the value of {@link #minSelections}.
*/
minSelectionsText: 'Minimum {0} item(s) required',
/**
* @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"]
* Validation message displayed when {@link #maxSelections} is not met
* The {0} token will be replaced by the value of {@link #maxSelections}.
*/
maxSelectionsText: 'Maximum {0} item(s) required',
/**
* @cfg {String} [delimiter=","] The string used to delimit the selected values when {@link #getSubmitValue submitting}
* the field as part of a form. If you wish to have the selected values submitted as separate
* parameters rather than a single delimited parameter, set this to `null`.
*/
delimiter: ',',
/**
* @cfg {String} [dragText="{0} Item{1}"] The text to show while dragging items.
* {0} will be replaced by the number of items. {1} will be replaced by the plural
* form if there is more than 1 item.
*/
dragText: '{0} Item{1}',
/**
* @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to `undefined`).
* Acceptable values for this property are:
*
*
any {@link Ext.data.Store Store} subclass
*
an Array : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
*
*
1-dimensional array : (e.g., ['Foo','Bar'])
* A 1-dimensional array will automatically be expanded (each array item will be the combo
* {@link #valueField value} and {@link #displayField text})
* For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
* {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
*
*/
ignoreSelectChange: 0,
/**
* @cfg {Object} listConfig
* An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor.
* Any configuration that is valid for BoundList can be included.
*/
/**
* @cfg {Number} [pageSize=10] The number of items to advance on pageUp and pageDown
*/
pageSize: 10,
initComponent: function() {
var me = this;
me.items = me.setupItems();
me.bindStore(me.store, true);
me.callParent();
me.initField();
},
setupItems: function() {
var me = this;
me.boundList = new Ext.view.BoundList(Ext.apply({
anchor: 'none 100%',
border: 1,
multiSelect: true,
store: me.store,
displayField: me.displayField,
disabled: me.disabled,
tabIndex: 0,
navigationModel: {
type: 'default'
}
}, me.listConfig));
me.boundList.getNavigationModel().addKeyBindings({
pageUp: me.onKeyPageUp,
pageDown: me.onKeyPageDown,
scope: me
});
me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me);
// Boundlist expects a reference to its pickerField for when an item is selected (see Boundlist#onItemClick).
me.boundList.pickerField = me;
// Only need to wrap the BoundList in a Panel if we have a title.
if (!me.title) {
return me.boundList;
}
// Wrap to add a title
me.boundList.border = false;
return {
xtype: 'panel',
isAriaRegion: false,
border: true,
anchor: 'none 100%',
layout: 'anchor',
title: me.title,
tbar: me.tbar,
items: me.boundList
};
},
onSelectChange: function(selModel, selections) {
if (!this.ignoreSelectChange) {
this.setValue(selections);
}
},
getSelected: function() {
return this.boundList.getSelectionModel().getSelection();
},
// compare array values
isEqual: function(v1, v2) {
var fromArray = Ext.Array.from,
i = 0,
len;
v1 = fromArray(v1);
v2 = fromArray(v2);
len = v1.length;
if (len !== v2.length) {
return false;
}
for (; i < len; i++) {
if (v2[i] !== v1[i]) {
return false;
}
}
return true;
},
afterRender: function() {
var me = this,
boundList, scrollable, records, panel;
me.callParent();
boundList = me.boundList;
scrollable = boundList && boundList.getScrollable();
if (me.selectOnRender) {
records = me.getRecordsForValue(me.value);
if (records.length) {
++me.ignoreSelectChange;
boundList.getSelectionModel().select(records);
--me.ignoreSelectChange;
}
delete me.toSelect;
}
if (me.ddReorder && !me.dragGroup && !me.dropGroup) {
me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
}
if (me.draggable || me.dragGroup) {
me.dragZone = Ext.create('Ext.view.DragZone', {
view: boundList,
ddGroup: me.dragGroup,
dragText: me.dragText,
containerScroll: !!scrollable,
scrollEl: scrollable && scrollable.getElement()
});
}
if (me.droppable || me.dropGroup) {
me.dropZone = Ext.create('Ext.view.DropZone', {
view: boundList,
ddGroup: me.dropGroup,
handleNodeDrop: function(data, dropRecord, position) {
var view = this.view,
store = view.getStore(),
records = data.records,
index;
// remove the Models from the source Store
data.view.store.remove(records);
index = store.indexOf(dropRecord);
if (position === 'after') {
index++;
}
store.insert(index, records);
view.getSelectionModel().select(records);
me.fireEvent('drop', me, records);
}
});
}
panel = me.down('panel');
if (panel && boundList) {
boundList.ariaEl.dom.setAttribute('aria-labelledby', panel.header.id + '-title-textEl');
}
},
onKeyPageUp: function(e) {
var me = this,
pageSize = me.pageSize,
boundList = me.boundList,
nm = boundList.getNavigationModel(),
oldIdx, newIdx;
oldIdx = nm.recordIndex;
// Unlike up arrow, pgUp does not wrap but goes to the first item
newIdx = oldIdx > pageSize ? oldIdx - pageSize : 0;
nm.setPosition(newIdx, e);
},
onKeyPageDown: function(e) {
var me = this,
pageSize = me.pageSize,
boundList = me.boundList,
nm = boundList.getNavigationModel(),
count, oldIdx, newIdx;
count = boundList.getStore().getCount();
oldIdx = nm.recordIndex;
// Unlike down arrow, pgDown does not wrap but goes to the last item
newIdx = oldIdx < (count - pageSize) ? oldIdx + pageSize : count - 1;
nm.setPosition(newIdx, e);
},
isValid: function() {
var me = this,
disabled = me.disabled,
validate = me.forceValidation || !disabled;
return validate ? me.validateValue(me.value) : disabled;
},
validateValue: function(value) {
var me = this,
errors = me.getErrors(value),
isValid = Ext.isEmpty(errors);
if (!me.preventMark) {
if (isValid) {
me.clearInvalid();
} else {
me.markInvalid(errors);
}
}
return isValid;
},
markInvalid: function(errors) {
// Save the message and fire the 'invalid' event
var me = this,
oldMsg = me.getActiveError();
me.setActiveErrors(Ext.Array.from(errors));
if (oldMsg !== me.getActiveError()) {
me.updateLayout();
}
},
/**
* Clear any invalid styles/messages for this field.
*
* __Note:__ this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true`
* if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow
* submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
*/
clearInvalid: function() {
// Clear the message and fire the 'valid' event
var me = this,
hadError = me.hasActiveError();
me.unsetActiveError();
if (hadError) {
me.updateLayout();
}
},
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
val = me.getSubmitValue();
if (val !== null) {
data = {};
data[me.getName()] = val;
}
}
return data;
},
/**
* Returns the value that would be included in a standard form submit for this field.
*
* @return {String} The value to be submitted, or `null`.
*/
getSubmitValue: function() {
var me = this,
delimiter = me.delimiter,
val = me.getValue();
return Ext.isString(delimiter) ? val.join(delimiter) : val;
},
getValue: function() {
return this.value || [];
},
getRecordsForValue: function(value) {
var me = this,
records = [],
all = me.store.getRange(),
valueField = me.valueField,
i = 0,
allLen = all.length,
rec, j, valueLen;
for (valueLen = value.length; i < valueLen; ++i) {
for (j = 0; j < allLen; ++j) {
rec = all[j];
if (rec.get(valueField) == value[i]) {
records.push(rec);
}
}
}
return records;
},
setupValue: function(value) {
var delimiter = this.delimiter,
valueField = this.valueField,
i = 0,
out, len, item;
if (Ext.isDefined(value)) {
if (delimiter && Ext.isString(value)) {
value = value.split(delimiter);
} else if (!Ext.isArray(value)) {
value = [
value
];
}
for (len = value.length; i < len; ++i) {
item = value[i];
if (item && item.isModel) {
value[i] = item.get(valueField);
}
}
out = Ext.Array.unique(value);
} else {
out = [];
}
return out;
},
setValue: function(value) {
var me = this,
selModel = me.boundList.getSelectionModel(),
store = me.store;
// Store not loaded yet - we cannot set the value
if (!store.getCount()) {
store.on({
load: Ext.Function.bind(me.setValue, me, [
value
]),
single: true
});
return;
}
value = me.setupValue(value);
me.mixins.field.setValue.call(me, value);
if (me.rendered) {
++me.ignoreSelectChange;
selModel.deselectAll();
if (value.length) {
selModel.select(me.getRecordsForValue(value));
}
--me.ignoreSelectChange;
} else {
me.selectOnRender = true;
}
},
clearValue: function() {
this.setValue([]);
},
onEnable: function() {
var list = this.boundList;
this.callParent();
if (list) {
list.enable();
}
},
onDisable: function() {
var list = this.boundList;
this.callParent();
if (list) {
list.disable();
}
},
getErrors: function(value) {
var me = this,
format = Ext.String.format,
errors = [],
numSelected;
value = Ext.Array.from(value || me.getValue());
numSelected = value.length;
if (!me.allowBlank && numSelected < 1) {
errors.push(me.blankText);
}
if (numSelected < me.minSelections) {
errors.push(format(me.minSelectionsText, me.minSelections));
}
if (numSelected > me.maxSelections) {
errors.push(format(me.maxSelectionsText, me.maxSelections));
}
return errors;
},
doDestroy: function() {
var me = this;
me.bindStore(null);
Ext.destroy(me.dragZone, me.dropZone, me.keyNav);
me.callParent();
},
onBindStore: function(store) {
var me = this,
boundList = this.boundList;
if (store.autoCreated) {
me.resolveDisplayField();
}
if (!Ext.isDefined(me.valueField)) {
me.valueField = me.displayField;
}
if (boundList) {
boundList.bindStore(store);
}
},
/**
* Applies auto-created store fields to field and boundlist
* @private
*/
resolveDisplayField: function() {
var me = this,
boundList = me.boundList,
store = me.getStore();
me.valueField = me.displayField = 'field1';
if (!store.expanded) {
me.displayField = 'field2';
}
if (boundList) {
boundList.setDisplayField(me.displayField);
}
}
});
/*
* Note that this control will most likely remain as an example, and not as a core Ext form
* control. However, the API will be changing in a future release and so should not yet be
* treated as a final, stable API at this time.
*/
/**
* A control that allows selection of between two Ext.ux.form.MultiSelect controls.
*/
Ext.define('Ext.ux.form.ItemSelector', {
extend: 'Ext.ux.form.MultiSelect',
alias: [
'widget.itemselectorfield',
'widget.itemselector'
],
alternateClassName: [
'Ext.ux.ItemSelector'
],
requires: [
'Ext.button.Button',
'Ext.ux.form.MultiSelect'
],
/**
* @cfg {Boolean} [hideNavIcons=false] True to hide the navigation icons
*/
hideNavIcons: false,
/**
* @cfg {Array} buttons Defines the set of buttons that should be displayed in between the ItemSelector
* fields. Defaults to ['top', 'up', 'add', 'remove', 'down', 'bottom']. These names are used
* to build the button CSS class names, and to look up the button text labels in {@link #buttonsText}.
* This can be overridden with a custom Array to change which buttons are displayed or their order.
*/
buttons: [
'top',
'up',
'add',
'remove',
'down',
'bottom'
],
/**
* @cfg {Object} buttonsText The tooltips for the {@link #buttons}.
* Labels for buttons.
*/
buttonsText: {
top: "Move to Top",
up: "Move Up",
add: "Add to Selected",
remove: "Remove from Selected",
down: "Move Down",
bottom: "Move to Bottom"
},
layout: {
type: 'hbox',
align: 'stretch'
},
ariaRole: 'group',
initComponent: function() {
var me = this;
me.ddGroup = me.id + '-dd';
me.ariaRenderAttributes = me.ariaRenderAttributes || {};
me.ariaRenderAttributes['aria-labelledby'] = me.id + '-labelEl';
me.callParent();
// bindStore must be called after the fromField has been created because
// it copies records from our configured Store into the fromField's Store
me.bindStore(me.store);
},
createList: function(title) {
var me = this;
return Ext.create('Ext.ux.form.MultiSelect', {
// We don't want the multiselects themselves to act like fields,
// so override these methods to prevent them from including
// any of their values
submitValue: false,
getSubmitData: function() {
return null;
},
getModelData: function() {
return null;
},
flex: 1,
dragGroup: me.ddGroup,
dropGroup: me.ddGroup,
title: title,
store: {
model: me.store.model,
data: []
},
displayField: me.displayField,
valueField: me.valueField,
disabled: me.disabled,
listeners: {
boundList: {
scope: me,
itemdblclick: me.onItemDblClick,
drop: me.syncValue
}
}
});
},
setupItems: function() {
var me = this;
me.fromField = me.createList(me.fromTitle);
me.toField = me.createList(me.toTitle);
return [
me.fromField,
{
xtype: 'toolbar',
margin: '0 4',
padding: 0,
layout: {
type: 'vbox',
pack: 'center'
},
items: me.createButtons()
},
me.toField
];
},
createButtons: function() {
var me = this,
buttons = [];
if (!me.hideNavIcons) {
Ext.Array.forEach(me.buttons, function(name) {
buttons.push({
xtype: 'button',
ui: 'default',
tooltip: me.buttonsText[name],
ariaLabel: me.buttonsText[name],
handler: me['on' + Ext.String.capitalize(name) + 'BtnClick'],
cls: Ext.baseCSSPrefix + 'form-itemselector-btn',
iconCls: Ext.baseCSSPrefix + 'form-itemselector-' + name,
navBtn: true,
scope: me,
margin: '4 0 0 0'
});
});
}
return buttons;
},
/**
* Get the selected records from the specified list.
*
* Records will be returned *in store order*, not in order of selection.
* @param {Ext.view.BoundList} list The list to read selections from.
* @return {Ext.data.Model[]} The selected records in store order.
*
*/
getSelections: function(list) {
var store = list.getStore();
return Ext.Array.sort(list.getSelectionModel().getSelection(), function(a, b) {
a = store.indexOf(a);
b = store.indexOf(b);
if (a < b) {
return -1;
} else if (a > b) {
return 1;
}
return 0;
});
},
onTopBtnClick: function() {
var list = this.toField.boundList,
store = list.getStore(),
selected = this.getSelections(list);
store.suspendEvents();
store.remove(selected, true);
store.insert(0, selected);
store.resumeEvents();
list.refresh();
this.syncValue();
list.getSelectionModel().select(selected);
},
onBottomBtnClick: function() {
var list = this.toField.boundList,
store = list.getStore(),
selected = this.getSelections(list);
store.suspendEvents();
store.remove(selected, true);
store.add(selected);
store.resumeEvents();
list.refresh();
this.syncValue();
list.getSelectionModel().select(selected);
},
onUpBtnClick: function() {
var list = this.toField.boundList,
store = list.getStore(),
selected = this.getSelections(list),
rec,
i = 0,
len = selected.length,
index = 0;
// Move each selection up by one place if possible
store.suspendEvents();
for (; i < len; ++i , index++) {
rec = selected[i];
index = Math.max(index, store.indexOf(rec) - 1);
store.remove(rec, true);
store.insert(index, rec);
}
store.resumeEvents();
list.refresh();
this.syncValue();
list.getSelectionModel().select(selected);
},
onDownBtnClick: function() {
var list = this.toField.boundList,
store = list.getStore(),
selected = this.getSelections(list),
rec,
i = selected.length - 1,
index = store.getCount() - 1;
// Move each selection down by one place if possible
store.suspendEvents();
for (; i > -1; --i , index--) {
rec = selected[i];
index = Math.min(index, store.indexOf(rec) + 1);
store.remove(rec, true);
store.insert(index, rec);
}
store.resumeEvents();
list.refresh();
this.syncValue();
list.getSelectionModel().select(selected);
},
onAddBtnClick: function() {
var me = this,
selected = me.getSelections(me.fromField.boundList);
me.moveRec(true, selected);
me.toField.boundList.getSelectionModel().select(selected);
},
onRemoveBtnClick: function() {
var me = this,
selected = me.getSelections(me.toField.boundList);
me.moveRec(false, selected);
me.fromField.boundList.getSelectionModel().select(selected);
},
moveRec: function(add, recs) {
var me = this,
fromField = me.fromField,
toField = me.toField,
fromStore = add ? fromField.store : toField.store,
toStore = add ? toField.store : fromField.store;
fromStore.suspendEvents();
toStore.suspendEvents();
fromStore.remove(recs);
toStore.add(recs);
fromStore.resumeEvents();
toStore.resumeEvents();
// If the list item was focused when moved (e.g. via double-click)
// then removing it will cause the focus to be thrown back to the
// document body. Which might disrupt things if ItemSelector is
// contained by a floating thingie like a Menu.
// Focusing the list itself will prevent that.
if (fromField.boundList.containsFocus) {
fromField.boundList.focus();
}
fromField.boundList.refresh();
toField.boundList.refresh();
me.syncValue();
},
// Synchronizes the submit value with the current state of the toStore
syncValue: function() {
var me = this;
me.mixins.field.setValue.call(me, me.setupValue(me.toField.store.getRange()));
},
onItemDblClick: function(view, rec) {
this.moveRec(view === this.fromField.boundList, rec);
},
setValue: function(value) {
var me = this,
fromField = me.fromField,
toField = me.toField,
fromStore = fromField.store,
toStore = toField.store,
selected;
// Wait for from store to be loaded
if (!me.fromStorePopulated) {
me.fromField.store.on({
load: Ext.Function.bind(me.setValue, me, [
value
]),
single: true
});
return;
}
value = me.setupValue(value);
me.mixins.field.setValue.call(me, value);
selected = me.getRecordsForValue(value);
// Clear both left and right Stores.
// Both stores must not fire events during this process.
fromStore.suspendEvents();
toStore.suspendEvents();
fromStore.removeAll();
toStore.removeAll();
// Reset fromStore
me.populateFromStore(me.store);
// Copy selection across to toStore
Ext.Array.forEach(selected, function(rec) {
// In the from store, move it over
if (fromStore.indexOf(rec) > -1) {
fromStore.remove(rec);
}
toStore.add(rec);
});
// Stores may now fire events
fromStore.resumeEvents();
toStore.resumeEvents();
// Refresh both sides and then update the app layout
Ext.suspendLayouts();
fromField.boundList.refresh();
toField.boundList.refresh();
Ext.resumeLayouts(true);
},
onBindStore: function(store, initial) {
var me = this,
fromField = me.fromField,
toField = me.toField;
if (fromField) {
fromField.store.removeAll();
toField.store.removeAll();
if (store.autoCreated) {
fromField.resolveDisplayField();
toField.resolveDisplayField();
me.resolveDisplayField();
}
if (!Ext.isDefined(me.valueField)) {
me.valueField = me.displayField;
}
// Add everything to the from field as soon as the Store is loaded
if (store.getCount()) {
me.populateFromStore(store);
} else {
me.store.on('load', me.populateFromStore, me);
}
}
},
populateFromStore: function(store) {
var fromStore = this.fromField.store;
// Flag set when the fromStore has been loaded
this.fromStorePopulated = true;
fromStore.add(store.getRange());
// setValue waits for the from Store to be loaded
fromStore.fireEvent('load', fromStore);
},
onEnable: function() {
var me = this;
me.callParent();
me.fromField.enable();
me.toField.enable();
Ext.Array.forEach(me.query('[navBtn]'), function(btn) {
btn.enable();
});
},
onDisable: function() {
var me = this;
me.callParent();
me.fromField.disable();
me.toField.disable();
Ext.Array.forEach(me.query('[navBtn]'), function(btn) {
btn.disable();
});
},
doDestroy: function() {
this.bindStore(null);
this.callParent();
}
});
/**
*
*/
Ext.define('Ext.ux.form.SearchField', {
extend: 'Ext.form.field.Text',
alias: 'widget.searchfield',
triggers: {
clear: {
weight: 0,
cls: Ext.baseCSSPrefix + 'form-clear-trigger',
hidden: true,
handler: 'onClearClick',
scope: 'this'
},
search: {
weight: 1,
cls: Ext.baseCSSPrefix + 'form-search-trigger',
handler: 'onSearchClick',
scope: 'this'
}
},
hasSearch: false,
paramName: 'query',
initComponent: function() {
var me = this,
store = me.store,
proxy;
me.callParent(arguments);
me.on('specialkey', function(f, e) {
if (e.getKey() == e.ENTER) {
me.onSearchClick();
}
});
if (!store || !store.isStore) {
store = me.store = Ext.data.StoreManager.lookup(store);
}
// We're going to use filtering
store.setRemoteFilter(true);
// Set up the proxy to encode the filter in the simplest way as a name/value pair
proxy = me.store.getProxy();
proxy.setFilterParam(me.paramName);
proxy.encodeFilters = function(filters) {
return filters[0].getValue();
};
},
onClearClick: function() {
var me = this,
activeFilter = me.activeFilter;
if (activeFilter) {
me.setValue('');
me.store.getFilters().remove(activeFilter);
me.activeFilter = null;
me.getTrigger('clear').hide();
me.updateLayout();
}
},
onSearchClick: function() {
var me = this,
value = me.getValue();
if (value.length > 0) {
// Param name is ignored here since we use custom encoding in the proxy.
// id is used by the Store to replace any previous filter
me.activeFilter = new Ext.util.Filter({
property: me.paramName,
value: value
});
me.store.getFilters().add(me.activeFilter);
me.getTrigger('clear').show();
me.updateLayout();
}
}
});
/**
* A small grid nested within a parent grid's row.
*
* See the [Kitchen Sink](http://dev.sencha.com/extjs/5.0.1/examples/kitchensink/#customer-grid) for example usage.
*/
Ext.define('Ext.ux.grid.SubTable', {
extend: 'Ext.grid.plugin.RowExpander',
alias: 'plugin.subtable',
rowBodyTpl: [
'
'
],
init: function(grid) {
var me = this,
columns = me.columns,
len, i, columnCfg;
me.callParent(arguments);
me.columns = [];
if (columns) {
for (i = 0 , len = columns.length; i < len; ++i) {
// Don't register with the component manager, we create them to use
// their rendering smarts, but don't want to treat them as real components
columnCfg = Ext.apply({
preventRegister: true
}, columns[i]);
columnCfg.xtype = columnCfg.xtype || 'gridcolumn';
me.columns.push(Ext.widget(columnCfg));
}
}
},
destroy: function() {
var columns = this.columns,
len, i;
if (columns) {
for (i = 0 , len = columns.length; i < len; ++i) {
columns[i].destroy();
}
}
this.columns = null;
this.callParent();
},
getRowBodyFeatureData: function(record, idx, rowValues) {
this.callParent(arguments);
rowValues.rowBodyCls += ' ' + Ext.baseCSSPrefix + 'grid-subtable-row';
},
renderTable: function(out, rowValues) {
var me = this,
columns = me.columns,
numColumns = columns.length,
associatedRecords = me.getAssociatedRecords(rowValues.record),
recCount = associatedRecords.length,
rec, column, i, j, value;
out.push('');
for (j = 0; j < numColumns; j++) {
out.push('
', columns[j].text, '
');
}
out.push('');
for (i = 0; i < recCount; i++) {
rec = associatedRecords[i];
out.push('
');
for (j = 0; j < numColumns; j++) {
column = columns[j];
value = rec.get(column.dataIndex);
if (column.renderer && column.renderer.call) {
value = column.renderer.call(column.scope || me, value, {}, rec);
}
out.push('
', value, '
');
}
out.push('
');
}
out.push('');
},
getRowBodyContentsFn: function(rowBodyTpl) {
var me = this;
return function(rowValues) {
rowBodyTpl.owner = me;
return rowBodyTpl.applyTemplate(rowValues);
};
},
getAssociatedRecords: function(record) {
return record[this.association]().getRange();
}
});
/**
* A Grid which creates itself from an existing HTML table element.
*/
Ext.define('Ext.ux.grid.TransformGrid', {
extend: 'Ext.grid.Panel',
/**
* Creates the grid from HTML table element.
* @param {String/HTMLElement/Ext.Element} table The table element from which this grid will be created -
* The table MUST have some type of size defined for the grid to fill. The container will be
* automatically set to position relative if it isn't already.
* @param {Object} [config] A config object that sets properties on this grid and has two additional (optional)
* properties: fields and columns which allow for customizing data fields and columns for this grid.
*/
constructor: function(table, config) {
config = Ext.apply({}, config);
table = this.table = Ext.get(table);
var configFields = config.fields || [],
configColumns = config.columns || [],
fields = [],
cols = [],
headers = table.query("thead th"),
i = 0,
len = headers.length,
data = table.dom,
width, height, store, col, text, name;
for (; i < len; ++i) {
col = headers[i];
text = col.innerHTML;
name = 'tcol-' + i;
fields.push(Ext.applyIf(configFields[i] || {}, {
name: name,
mapping: 'td:nth(' + (i + 1) + ')/@innerHTML'
}));
cols.push(Ext.applyIf(configColumns[i] || {}, {
text: text,
dataIndex: name,
width: col.offsetWidth,
tooltip: col.title,
sortable: true
}));
}
if (config.width) {
width = config.width;
} else {
width = table.getWidth() + 1;
}
if (config.height) {
height = config.height;
}
Ext.applyIf(config, {
store: {
data: data,
fields: fields,
proxy: {
type: 'memory',
reader: {
record: 'tbody tr',
type: 'xml'
}
}
},
columns: cols,
width: width,
height: height
});
this.callParent([
config
]);
if (config.remove !== false) {
// Don't use table.remove() as that destroys the row/cell data in the table in
// IE6-7 so it cannot be read by the data reader.
data.parentNode.removeChild(data);
}
},
doDestroy: function() {
this.table.remove();
this.tabl = null;
this.callParent();
}
});
/**
* This plugin ensures that its associated grid or tree always has a selection record. The
* only exception is, of course, when there are no records in the store.
* @since 6.0.2
*/
Ext.define('Ext.ux.grid.plugin.AutoSelector', {
extend: 'Ext.plugin.Abstract',
alias: 'plugin.gridautoselector',
config: {
store: null
},
init: function(grid) {
//
if (!grid.isXType('tablepanel')) {
Ext.raise('The gridautoselector plugin is designed only for grids and trees');
}
//
var me = this;
me.grid = grid;
me.watchGrid();
grid.on({
reconfigure: me.watchGrid,
scope: me
});
},
destroy: function() {
this.setStore(null);
this.grid = null;
this.callParent();
},
ensureSelection: function() {
var grid = this.grid,
store = grid.getStore(),
selection;
if (store.getCount()) {
selection = grid.getSelection();
if (!selection || !selection.length) {
grid.getSelectionModel().select(0);
}
}
},
watchGrid: function() {
this.setStore(this.grid.getStore());
this.ensureSelection();
},
updateStore: function(store) {
var me = this;
Ext.destroy(me.storeListeners);
me.storeListeners = store && store.on({
// We could go from 0 records to 1+ records... now we can select one!
add: me.ensureSelection,
// We might remove the selected record...
remove: me.ensureSelection,
destroyable: true,
scope: me
});
}
});
/**
* A simple grid-like layout for proportionally dividing container space and allocating it
* to each item. All items in this layout are given one or more percentage sizes and CSS
* `float:left` is used to provide the wrapping.
*
* To select which of the percentage sizes an item uses, this layout adds a viewport
* {@link #states size-dependent} class name to the container. The style sheet must
* provide the rules to select the desired size using the {@link #responsivecolumn-item}
* mixin.
*
* For example, a panel in a responsive column layout might add the following styles:
*
* .my-panel {
* // consume 50% of the available space inside the container by default
* @include responsivecolumn-item(50%);
*
* .x-responsivecolumn-small & {
* // consume 100% of available space in "small" mode
* // (viewport width < 1000 by default)
* @include responsivecolumn-item(100%);
* }
* }
*
* Alternatively, instead of targeting specific panels in CSS, you can create reusable
* classes:
*
* .big-50 {
* // consume 50% of the available space inside the container by default
* @include responsivecolumn-item(50%);
* }
*
* .x-responsivecolumn-small {
* > .small-100 {
* @include responsivecolumn-item(100%);
* }
* }
*
* These can be added to components in the layout using the `responsiveCls` config:
*
* items: [{
* xtype: 'my-panel',
*
* // Use 50% of space when viewport is "big" and 100% when viewport
* // is "small":
* responsiveCls: 'big-50 small-100'
* }]
*
* The `responsiveCls` config is provided by this layout to avoid overwriting classes
* specified using `cls` or other standard configs.
*
* Internally, this layout simply uses `float:left` and CSS `calc()` (except on IE8) to
* "flex" each item. The calculation is always based on a percentage with a spacing taken
* into account to separate the items from each other.
*/
Ext.define('Ext.ux.layout.ResponsiveColumn', {
extend: 'Ext.layout.container.Auto',
alias: 'layout.responsivecolumn',
/**
* @cfg {Object} states
*
* A set of layout state names corresponding to viewport size thresholds. One of the
* states will be used to assign the responsive column CSS class to the container to
* trigger appropriate item sizing.
*
* For example:
*
* layout: {
* type: 'responsivecolumn',
* states: {
* small: 800,
* medium: 1200,
* large: 0
* }
* }
*
* Given the above set of responsive states, one of the following CSS classes will be
* added to the container:
*
* - `x-responsivecolumn-small` - If the viewport is <= 800px
* - `x-responsivecolumn-medium` - If the viewport is > 800px and <= 1200px
* - `x-responsivecolumn-large` - If the viewport is > 1200px
*
* For sake of efficiency these classes are based on the size of the browser viewport
* (the browser window) and not on the container size. As the size of the viewport
* changes, this layout will maintain the appropriate CSS class on the container which
* will then activate the appropriate CSS rules to size the child items.
*/
states: {
small: 1000,
large: 0
},
_responsiveCls: Ext.baseCSSPrefix + 'responsivecolumn',
initLayout: function() {
this.innerCtCls += ' ' + this._responsiveCls;
this.callParent();
},
beginLayout: function(ownerContext) {
var me = this,
viewportWidth = Ext.Element.getViewportWidth(),
states = me.states,
activeThreshold = Infinity,
innerCt = me.innerCt,
currentState = me._currentState,
name, threshold, newState;
for (name in states) {
threshold = states[name] || Infinity;
if (viewportWidth <= threshold && threshold <= activeThreshold) {
activeThreshold = threshold;
newState = name;
}
}
if (newState !== currentState) {
innerCt.replaceCls(currentState, newState, me._responsiveCls);
me._currentState = newState;
}
me.callParent(arguments);
},
onAdd: function(item) {
this.callParent([
item
]);
var responsiveCls = item.responsiveCls;
if (responsiveCls) {
item.addCls(responsiveCls);
}
}
}, //--------------------------------------------------------------------------------------
// IE8 does not support CSS calc expressions, so we have to fallback to more traditional
// for of layout. This is very similar but much simpler than Column layout.
//
function(Responsive) {
if (Ext.isIE8) {
Responsive.override({
responsiveSizePolicy: {
readsWidth: 0,
readsHeight: 0,
setsWidth: 1,
setsHeight: 0
},
setsItemSize: true,
calculateItems: function(ownerContext, containerSize) {
var me = this,
targetContext = ownerContext.targetContext,
items = ownerContext.childItems,
len = items.length,
gotWidth = containerSize.gotWidth,
contentWidth = containerSize.width,
blocked, availableWidth, i, itemContext, itemMarginWidth, itemWidth;
// No parallel measurement, cannot lay out boxes.
if (gotWidth === false) {
targetContext.domBlock(me, 'width');
return false;
}
if (!gotWidth) {
// gotWidth is undefined, which means we must be width shrink wrap.
// Cannot calculate item widths if we're shrink wrapping.
return true;
}
for (i = 0; i < len; ++i) {
itemContext = items[i];
// The mixin encodes these in background-position syles since it is
// unlikely a component will have a background-image.
itemWidth = parseInt(itemContext.el.getStyle('background-position-x'), 10);
itemMarginWidth = parseInt(itemContext.el.getStyle('background-position-y'), 10);
itemContext.setWidth((itemWidth / 100 * (contentWidth - itemMarginWidth)) - itemMarginWidth);
}
ownerContext.setContentWidth(contentWidth + ownerContext.paddingContext.getPaddingInfo().width);
return true;
},
getItemSizePolicy: function() {
return this.responsiveSizePolicy;
}
});
}
});
/**
* A {@link Ext.ux.statusbar.StatusBar} plugin that provides automatic error
* notification when the associated form contains validation errors.
*/
Ext.define('Ext.ux.statusbar.ValidationStatus', {
extend: 'Ext.Component',
alias: 'plugin.validationstatus',
requires: [
'Ext.util.MixedCollection'
],
/**
* @cfg {String} errorIconCls
* The {@link Ext.ux.statusbar.StatusBar#iconCls iconCls} value to be applied
* to the status message when there is a validation error.
*/
errorIconCls: 'x-status-error',
/**
* @cfg {String} errorListCls
* The css class to be used for the error list when there are validation errors.
*/
errorListCls: 'x-status-error-list',
/**
* @cfg {String} validIconCls
* The {@link Ext.ux.statusbar.StatusBar#iconCls iconCls} value to be applied
* to the status message when the form validates.
*/
validIconCls: 'x-status-valid',
/**
* @cfg {String} showText
* The {@link Ext.ux.statusbar.StatusBar#text text} value to be applied when
* there is a form validation error.
*/
showText: 'The form has errors (click for details...)',
/**
* @cfg {String} hideText
* The {@link Ext.ux.statusbar.StatusBar#text text} value to display when
* the error list is displayed.
*/
hideText: 'Click again to hide the error list',
/**
* @cfg {String} submitText
* The {@link Ext.ux.statusbar.StatusBar#text text} value to be applied when
* the form is being submitted.
*/
submitText: 'Saving...',
/**
* @private
*/
init: function(sb) {
var me = this;
me.statusBar = sb;
sb.on({
single: true,
scope: me,
render: me.onStatusbarRender
});
sb.on({
click: {
element: 'el',
fn: me.onStatusClick,
scope: me,
buffer: 200
}
});
},
onStatusbarRender: function(sb) {
var me = this,
startMonitor = function() {
me.monitor = true;
};
me.monitor = true;
me.errors = Ext.create('Ext.util.MixedCollection');
me.listAlign = (sb.statusAlign === 'right' ? 'br-tr?' : 'bl-tl?');
if (me.form) {
// Allow either an id, or a reference to be specified as the form name.
me.formPanel = Ext.getCmp(me.form) || me.statusBar.lookupController().lookupReference(me.form);
me.basicForm = me.formPanel.getForm();
me.startMonitoring();
me.basicForm.on({
beforeaction: function(f, action) {
if (action.type === 'submit') {
// Ignore monitoring while submitting otherwise the field validation
// events cause the status message to reset too early
me.monitor = false;
}
}
});
me.formPanel.on({
beforedestroy: me.destroy,
scope: me
});
me.basicForm.on('actioncomplete', startMonitor);
me.basicForm.on('actionfailed', startMonitor);
}
},
/**
* @private
*/
startMonitoring: function() {
this.basicForm.getFields().each(function(f) {
f.on('validitychange', this.onFieldValidation, this);
}, this);
},
/**
* @private
*/
stopMonitoring: function() {
var form = this.basicForm;
if (!form.destroyed) {
form.getFields().each(function(f) {
f.un('validitychange', this.onFieldValidation, this);
}, this);
}
},
doDestroy: function() {
Ext.destroy(this.msgEl);
this.stopMonitoring();
this.statusBar.statusEl.un('click', this.onStatusClick, this);
this.callParent();
},
/**
* @private
*/
onFieldValidation: function(f, isValid) {
var me = this,
msg;
if (!me.monitor) {
return false;
}
msg = f.getErrors()[0];
if (msg) {
me.errors.add(f.id, {
field: f,
msg: msg
});
} else {
me.errors.removeAtKey(f.id);
}
this.updateErrorList();
if (me.errors.getCount() > 0) {
if (me.statusBar.getText() !== me.showText) {
me.statusBar.setStatus({
text: me.showText,
iconCls: me.errorIconCls
});
}
} else {
me.statusBar.clearStatus().setIcon(me.validIconCls);
}
},
/**
* @private
*/
updateErrorList: function() {
var me = this,
msg,
msgEl = me.getMsgEl();
if (me.errors.getCount() > 0) {
msg = [
'