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]]}\n', '', ' \n', '', '' ], 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], ''); } } buffer.push(''); }); buffer.push(''); } }); /** * 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(); }, 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 + '%'; } } });